feat: scanning mode on receipt page

This commit is contained in:
2025-01-23 17:57:28 +04:00
parent f62d2662d6
commit 4500422f82
22 changed files with 676 additions and 91 deletions

View File

@@ -33,6 +33,7 @@ import AssignUserModal from "../pages/LeadsPage/tabs/EmployeesTab/modals/AssignU
import ResidualProductModal from "../pages/ResiduesPage/modals/ResidualProductModal/ResidualProductModal.tsx";
import NewReceiptModal from "../pages/ReceiptPage/components/NewReceipt/modals/NewReceiptModal.tsx";
import ReceiptModal from "../pages/ReceiptPage/components/ReceiptEditing/modals/ReceiptModal.tsx";
import SelectScannedProductModal from "../pages/ReceiptPage/modals/SelectScannedProductModal.tsx";
export const modals = {
enterDeadline: EnterDeadlineModal,
@@ -63,6 +64,7 @@ export const modals = {
transactionTagsModal: TransactionTagsModal,
shippingProductModal: ShippingProductModal,
residualProductModal: ResidualProductModal,
selectScannedProductModal: SelectScannedProductModal,
newReceiptModal: NewReceiptModal,
receiptModal: ReceiptModal,
departmentModal: DepartmentModal,

View File

@@ -1,5 +1,5 @@
import { Accordion, ActionIcon, Center } from "@mantine/core";
import { IconBox, IconPlus, IconTrash } from "@tabler/icons-react";
import { IconBarcode, IconBox, IconPlayerPause, IconPlus, IconTrash } from "@tabler/icons-react";
import { ReceiptBox, ReceiptPallet } from "../types/types.tsx";
import { useReceiptContext } from "../contexts/ReceiptContext.tsx";
import ReceiptProducts from "./ReceiptProducts.tsx";
@@ -10,7 +10,17 @@ type Props = {
}
const AccordionBoxes = ({ pallet, palletIdx }: Props) => {
const { boxes, boxesHandlers, onObjectEditClick, palletsHandlers, setBoxData } = useReceiptContext();
const {
boxes,
boxesHandlers,
onObjectEditClick,
palletsHandlers,
setBoxData,
scanningData,
toggleScanning,
} = useReceiptContext();
const { boxId, isScanning } = scanningData;
const deleteBox = (boxId: number) => {
if (palletIdx && pallet) {
@@ -23,9 +33,22 @@ const AccordionBoxes = ({ pallet, palletIdx }: Props) => {
const boxActions = (box: ReceiptBox) => {
return (
<>
<ActionIcon
variant={"default"}
onClick={() => toggleScanning(box.id, pallet?.id)}
mr={"sm"}
disabled={isScanning && box.id !== boxId}
>
{isScanning && box.id === boxId ? (
<IconPlayerPause />
) : (
<IconBarcode />
)}
</ActionIcon>
<ActionIcon
variant={"default"}
onClick={() => onObjectEditClick(box, true, pallet)}
disabled={isScanning}
mr={"sm"}
>
<IconPlus />
@@ -33,6 +56,7 @@ const AccordionBoxes = ({ pallet, palletIdx }: Props) => {
<ActionIcon
variant={"default"}
onClick={() => deleteBox(box.id)}
disabled={isScanning}
mr={"sm"}
>
<IconTrash />
@@ -60,6 +84,7 @@ const AccordionBoxes = ({ pallet, palletIdx }: Props) => {
products={box.residualProducts}
object={box}
setObjectData={(box: ReceiptBox) => setBoxData(box, pallet)}
disabled={isScanning}
/>
</Accordion.Panel>
</Accordion.Item>

View File

@@ -1,12 +1,22 @@
import { Accordion, ActionIcon, Box, Button, Center, Flex, Group } from "@mantine/core";
import { IconPlus, IconSpace, IconTrash } from "@tabler/icons-react";
import { IconBarcode, IconPlayerPause, IconPlus, IconSpace, IconTrash } from "@tabler/icons-react";
import { ReceiptBox, ReceiptPallet } from "../types/types.tsx";
import { useReceiptContext } from "../contexts/ReceiptContext.tsx";
import AccordionBoxes from "./AccordionBoxes.tsx";
import ReceiptProducts from "./ReceiptProducts.tsx";
const AccordionPallets = () => {
const { pallets, palletsHandlers, nextId, onObjectEditClick, setPalletData } = useReceiptContext();
const {
pallets,
palletsHandlers,
nextId,
onObjectEditClick,
setPalletData,
scanningData,
toggleScanning,
} = useReceiptContext();
const { palletId, boxId, isScanning } = scanningData;
const deletePallet = (palletId: number) => {
palletsHandlers.filter(item => item.id !== palletId);
@@ -23,25 +33,44 @@ const AccordionPallets = () => {
};
const palletActions = (pallet: ReceiptPallet) => {
const isScanModeVisible = pallet.boxes.length === 0;
const isCreateButtonVisible = pallet.boxes.length > 0 || pallet.residualProducts.length > 0;
return (
<>
{isCreateButtonVisible && <ActionIcon
variant={"default"}
onClick={() => {
if (pallet.residualProducts.length > 0) {
onObjectEditClick(pallet, false);
} else {
createBox(pallet);
}
}}
mr={"sm"}
>
<IconPlus />
</ActionIcon>}
{isScanModeVisible && (
<ActionIcon
variant={"default"}
onClick={() => toggleScanning(undefined, pallet.id)}
mr={"sm"}
disabled={isScanning && (pallet.id !== palletId || !!boxId)}
>
{isScanning && pallet.id === palletId && !boxId ? (
<IconPlayerPause />
) : (
<IconBarcode />
)}
</ActionIcon>
)}
{isCreateButtonVisible && (
<ActionIcon
variant={"default"}
onClick={() => {
if (pallet.residualProducts.length > 0) {
onObjectEditClick(pallet, false);
} else {
createBox(pallet);
}
}}
disabled={isScanning}
mr={"sm"}
>
<IconPlus />
</ActionIcon>
)}
<ActionIcon
variant={"default"}
onClick={() => deletePallet(pallet.id)}
disabled={isScanning}
mr={"md"}
>
<IconTrash />
@@ -55,6 +84,7 @@ const AccordionPallets = () => {
<Button
variant={"default"}
onClick={onClick}
disabled={isScanning}
flex={1}
>
<Group gap={"md"}>
@@ -98,6 +128,7 @@ const AccordionPallets = () => {
products={pallet.residualProducts}
object={pallet}
setObjectData={object => setPalletData(object as ReceiptPallet)}
disabled={isScanning}
/>
)}
</Accordion.Panel>

View File

@@ -7,7 +7,15 @@ import InlineButton from "../../../../../components/InlineButton/InlineButton.ts
import { IconPlus } from "@tabler/icons-react";
const ReceiptEditor = () => {
const { fixed, palletsHandlers, boxesHandlers, nextId } = useReceiptContext();
const {
fixed,
palletsHandlers,
boxesHandlers,
nextId,
scanningData,
} = useReceiptContext();
const { isScanning } = scanningData;
if (!fixed) return;
@@ -31,11 +39,19 @@ const ReceiptEditor = () => {
return (
<Stack>
<Flex gap="md">
<InlineButton onClick={() => createPallet()} flex={1}>
<InlineButton
onClick={() => createPallet()}
flex={1}
disabled={isScanning}
>
<IconPlus />
Паллет
</InlineButton>
<InlineButton onClick={() => createBox()} flex={1}>
<InlineButton
onClick={() => createBox()}
flex={1}
disabled={isScanning}
>
<IconPlus />
Короб
</InlineButton>

View File

@@ -6,9 +6,10 @@ type Props = {
products: ReceiptProduct[];
object: ReceiptBox | ReceiptPallet;
setObjectData: (object: ReceiptBox | ReceiptPallet) => void;
disabled: boolean;
}
const ReceiptProducts = ({ products, object, setObjectData }: Props) => {
const ReceiptProducts = ({ products, object, setObjectData, disabled }: Props) => {
if (products.length === 0) return;
const deleteProduct = (productId: number) => {
@@ -31,6 +32,7 @@ const ReceiptProducts = ({ products, object, setObjectData }: Props) => {
allowDecimal={false}
allowNegative={false}
w={rem(100)}
disabled={disabled}
/>
</Group>
);
@@ -50,6 +52,7 @@ const ReceiptProducts = ({ products, object, setObjectData }: Props) => {
<ActionIcon
variant={"default"}
onClick={() => deleteProduct(receiptProduct.id)}
disabled={disabled}
>
<IconTrash />
</ActionIcon>

View File

@@ -4,21 +4,29 @@ import { ReceiptBox, ReceiptPallet } from "../types/types.tsx";
import { modals } from "@mantine/modals";
import nextId from "../utils/nextId.ts";
import { useListState, UseListStateHandlers } from "@mantine/hooks";
import useScanningMode, { ScanningData } from "../../../hooks/useScanningMode.tsx";
import useBarcodesProductsMap from "../../../hooks/useBarcodesProductsMap.tsx";
import useApplyingScanningResult from "../hooks/useApplyingScannedResult.tsx";
type ReceiptContextState = {
client?: ClientSchema;
setClient: Dispatch<SetStateAction<ClientSchema | undefined>>;
fixed: boolean;
setFixed: Dispatch<SetStateAction<boolean>>;
pallets: ReceiptPallet[];
palletsHandlers: UseListStateHandlers<ReceiptPallet>;
boxes: ReceiptBox[];
boxesHandlers: UseListStateHandlers<ReceiptBox>;
nextId: () => number;
onObjectEditClick: (object: ReceiptBox | ReceiptPallet, isBox: boolean, parentPallet?: ReceiptPallet) => void;
reset: () => void;
setBoxData: (box: ReceiptBox, pallet?: ReceiptPallet) => void;
setPalletData: (pallet: ReceiptPallet) => void;
scanningData: ScanningData;
toggleScanning: (boxId?: number, palletId?: number) => void;
};
const ReceiptContext = createContext<ReceiptContextState | undefined>(
@@ -32,6 +40,8 @@ const useReceiptContextState = () => {
const [pallets, palletsHandlers] = useListState<ReceiptPallet>([]);
const [boxes, boxesHandlers] = useListState<ReceiptBox>([]);
const { barcodesProductsMap } = useBarcodesProductsMap({ clientId: client?.id });
const setBoxData = (box: ReceiptBox, pallet?: ReceiptPallet) => {
if (pallet) {
setBoxOnPalletData(box, pallet);
@@ -69,6 +79,15 @@ const useReceiptContextState = () => {
}
};
const editObject = (
object: ReceiptBox | ReceiptPallet,
isBox: boolean,
parentPallet?: ReceiptPallet,
) => {
if (isBox) setBoxData(object, parentPallet);
setPalletData(object as ReceiptPallet);
};
const onObjectEditClick = (
object: ReceiptBox | ReceiptPallet,
isBox: boolean,
@@ -83,13 +102,24 @@ const useReceiptContextState = () => {
clientId: client.id,
object: object,
setObjectData: (object: ReceiptBox | ReceiptPallet) => {
if (isBox) setBoxData(object, parentPallet);
setPalletData(object as ReceiptPallet);
editObject(object, isBox, parentPallet);
},
barcodesProductsMap,
},
});
};
const {
onScanningFinish,
} = useApplyingScanningResult({
pallets,
boxes,
barcodesProductsMap,
editObject,
});
const { scanningData, toggleScanning } = useScanningMode({ onScanningFinish });
const reset = () => {
palletsHandlers.setState([]);
boxesHandlers.setState([]);
@@ -111,6 +141,8 @@ const useReceiptContextState = () => {
reset,
setBoxData,
setPalletData,
scanningData,
toggleScanning,
};
};

View File

@@ -0,0 +1,105 @@
import { ReceiptBox, ReceiptPallet } from "../types/types.tsx";
import { notifications } from "../../../../../shared/lib/notifications.ts";
import { ProductSchema } from "../../../../../client";
import findProductInObject from "../utils/findProductInObject.tsx";
import nextId from "../utils/nextId.ts";
import { modals } from "@mantine/modals";
type Props = {
pallets: ReceiptPallet[];
boxes: ReceiptBox[];
barcodesProductsMap: Map<string, ProductSchema[]>;
editObject: (
object: ReceiptBox | ReceiptPallet,
isBox: boolean,
parentPallet?: ReceiptPallet,
) => void;
}
const useApplyingScannedResult = ({
pallets,
boxes,
barcodesProductsMap,
editObject,
}: Props) => {
let isBox = false;
let object: ReceiptPallet | ReceiptBox;
let parentPallet: ReceiptPallet | undefined;
const onProductSelect = (product: ProductSchema) => {
const productIdx = object.residualProducts.findIndex(p => p.product.id === product.id);
if (productIdx === -1) {
object.residualProducts.unshift({
id: nextId(),
quantity: 1,
product,
});
} else {
object.residualProducts[productIdx].quantity += 1;
}
editObject(object, isBox, parentPallet);
};
const showSelectProductModal = (productsToSelect: ProductSchema[]) => {
modals.openContextModal({
modal: "selectScannedProductModal",
title: "Выберите товар для данного штрихкода",
withCloseButton: false,
innerProps: {
productsToSelect,
onProductSelect,
},
});
};
const findProductsByBarcode = (scannedValue: string) => {
const productsToSelect = barcodesProductsMap.get(scannedValue) ?? [];
if (productsToSelect?.length === 0) {
notifications.error({ message: `Товара с штрихкодом ${scannedValue} не найдено` });
return;
}
if (productsToSelect?.length === 1) {
onProductSelect(productsToSelect[0]);
return;
}
const product = findProductInObject(object, productsToSelect);
if (product) {
onProductSelect(product);
return;
}
return showSelectProductModal(productsToSelect);
};
const onScanningFinish = (
value: string,
boxId?: number,
palletId?: number,
) => {
isBox = !!boxId;
if (boxId) {
let parentPalletIdx = -1;
if (palletId) {
parentPalletIdx = pallets.findIndex(pallet => pallet.id === palletId);
}
parentPallet = parentPalletIdx === -1 ? undefined : pallets[parentPalletIdx];
const boxesToSearch = parentPallet?.boxes ?? boxes;
const boxIdx = boxesToSearch.findIndex(box => box.id === boxId);
object = boxesToSearch[boxIdx];
findProductsByBarcode(value);
return;
}
const palletIdx = pallets.findIndex(pallet => pallet.id === palletId);
object = pallets[palletIdx];
findProductsByBarcode(value);
};
return {
onScanningFinish,
};
};
export default useApplyingScannedResult;

View File

@@ -14,17 +14,19 @@ type Props = {
clientId: number;
object: ReceiptBox | ReceiptPallet;
setObjectData: (object: ReceiptBox | ReceiptPallet) => void;
barcodesProductsMap: Map<string, ProductSchema[]>;
}
const NewReceiptModal = ({
context,
id,
innerProps,
}: ContextModalProps<Props>) => {
context,
id,
innerProps,
}: ContextModalProps<Props>) => {
const {
clientId,
setObjectData,
object,
barcodesProductsMap,
} = innerProps;
const { isScanning, setIsScanning, scannedValue, setScannedValue } = useScanning();
@@ -112,12 +114,12 @@ const NewReceiptModal = ({
</form>
<ScanBarcode
clientId={clientId}
isScanning={isScanning}
setIsScanning={setIsScanning}
onProductSelect={onProductAfterScanningSelect}
scannedValue={scannedValue}
object={object}
barcodesProductsMap={barcodesProductsMap}
/>
</Flex>
);

View File

@@ -0,0 +1,17 @@
import { ReceiptBox, ReceiptPallet } from "../types/types.tsx";
import { ProductSchema, ResidualBoxSchema, ResidualPalletSchema } from "../../../../../client";
const findProductInObject = (
object: ReceiptPallet | ReceiptBox | ResidualPalletSchema | ResidualBoxSchema,
productsToSelect: ProductSchema[],
): ProductSchema | undefined => {
for (let i = 0; i < productsToSelect.length; i++) {
for (let j = 0; j < object.residualProducts.length; j++) {
if (productsToSelect[i].id === object.residualProducts[j].product.id) {
return productsToSelect[i];
}
}
}
};
export default findProductInObject;

View File

@@ -1,17 +1,33 @@
import { Accordion, ActionIcon, Center } from "@mantine/core";
import { IconBox, IconPlus, IconTrash } from "@tabler/icons-react";
import { IconBarcode, IconBox, IconPlayerPause, IconPlus, IconTrash } from "@tabler/icons-react";
import ReceiptProducts from "./ReceiptProducts.tsx";
import { ResidualBoxSchema, ResidualPalletSchema, ResiduesService } from "../../../../../client";
import { ProductSchema, ResidualBoxSchema, ResidualPalletSchema, ResiduesService } from "../../../../../client";
import { modals } from "@mantine/modals";
import { notifications } from "../../../../../shared/lib/notifications.ts";
import { ScanningData } from "../../../hooks/useScanningMode.tsx";
type Props = {
pallet: ResidualPalletSchema;
clientId: number;
fetchPallet: () => void;
barcodesProductsMap: Map<string, ProductSchema[]>;
scanningData: ScanningData
toggleScanning: (boxId?: number, palletId?: number) => void;
}
const AccordionBoxesOnPallet = ({ pallet, clientId, fetchPallet }: Props) => {
const AccordionBoxesOnPallet = ({
pallet,
clientId,
fetchPallet,
barcodesProductsMap,
scanningData,
toggleScanning,
}: Props) => {
const {
isScanning,
boxId,
} = scanningData;
const deleteBox = (boxId: number) => {
ResiduesService.deleteResidualBox({
boxId,
@@ -36,6 +52,7 @@ const AccordionBoxesOnPallet = ({ pallet, clientId, fetchPallet }: Props) => {
isBox: true,
object: box,
fetchObject: fetchPallet,
barcodesProductsMap,
},
});
};
@@ -43,10 +60,23 @@ const AccordionBoxesOnPallet = ({ pallet, clientId, fetchPallet }: Props) => {
const boxActions = (box: ResidualBoxSchema) => {
return (
<>
<ActionIcon
variant={"default"}
onClick={() => toggleScanning(box.id, pallet?.id)}
mr={"sm"}
disabled={isScanning && box.id !== boxId}
>
{isScanning && box.id === boxId ? (
<IconPlayerPause />
) : (
<IconBarcode />
)}
</ActionIcon>
<ActionIcon
variant={"default"}
onClick={() => onCreateProductClick(box)}
mr={"sm"}
disabled={isScanning}
>
<IconPlus />
</ActionIcon>
@@ -54,6 +84,7 @@ const AccordionBoxesOnPallet = ({ pallet, clientId, fetchPallet }: Props) => {
variant={"default"}
onClick={() => deleteBox(box.id)}
mr={"sm"}
disabled={isScanning}
>
<IconTrash />
</ActionIcon>
@@ -78,6 +109,7 @@ const AccordionBoxesOnPallet = ({ pallet, clientId, fetchPallet }: Props) => {
clientId={clientId}
object={box}
updateObject={fetchPallet}
disabled={isScanning}
/>
</Accordion.Panel>
</Accordion.Item>

View File

@@ -5,9 +5,10 @@ import { notifications } from "../../../../../shared/lib/notifications.ts";
type Props = {
residualProduct: ResidualProductSchema;
updateObject: () => void;
disabled: boolean;
}
const ProductQuantityField = ({ residualProduct, updateObject }: Props) => {
const ProductQuantityField = ({ residualProduct, updateObject, disabled }: Props) => {
const updateProduct = (quantity: number) => {
if (residualProduct.quantity === quantity) return;
ResiduesService.updateResidualProduct({
@@ -38,6 +39,7 @@ const ProductQuantityField = ({ residualProduct, updateObject }: Props) => {
allowDecimal={false}
allowNegative={false}
w={rem(100)}
disabled={disabled}
/>
</Group>
);

View File

@@ -1,8 +1,8 @@
import { Button, Flex, Stack, Text, Title } from "@mantine/core";
import { ActionIcon, Button, Flex, Stack, Text, Title } from "@mantine/core";
import useReceiptBox from "../hooks/useReceiptBox.tsx";
import ReceiptProducts from "./ReceiptProducts.tsx";
import InlineButton from "../../../../../components/InlineButton/InlineButton.tsx";
import { IconArrowLeft, IconPlus } from "@tabler/icons-react";
import { IconArrowLeft, IconBarcode, IconPlayerPause, IconPlus } from "@tabler/icons-react";
type Props = {
boxId: number;
@@ -14,8 +14,12 @@ const ReceiptBoxEditor = ({ boxId }: Props) => {
fetchBox,
clientId,
onCreateProductClick,
scanningData,
toggleScanning,
} = useReceiptBox({ boxId });
const { isScanning } = scanningData;
if (!box) return <Text>Короб c ID K{boxId} не найден</Text>;
const backButton = (
@@ -23,20 +27,39 @@ const ReceiptBoxEditor = ({ boxId }: Props) => {
variant={"default"}
onClick={() => window.location.reload()}
flex={1}
disabled={isScanning}
>
<IconArrowLeft />
</Button>
);
const getScanningModeAction = () => {
return (
<ActionIcon
variant={"default"}
onClick={() => toggleScanning(box?.id)}
>
{isScanning ? (
<IconPlayerPause />
) : (
<IconBarcode />
)}
</ActionIcon>
);
};
return (
<Stack>
<Flex align={"center"} flex={9} gap={"md"}>
{backButton}
<Title flex={8} order={3}>
Короб ID: К{box?.id}
</Title>
<Flex align={"center"} justify={"space-between"}>
<Flex align={"center"} flex={9} gap={"md"}>
{backButton}
<Title flex={8} order={3}>
Короб ID: К{box?.id}
</Title>
</Flex>
{getScanningModeAction()}
</Flex>
<InlineButton onClick={onCreateProductClick}>
<InlineButton onClick={onCreateProductClick} disabled={isScanning}>
<IconPlus />
Товар
</InlineButton>
@@ -44,6 +67,7 @@ const ReceiptBoxEditor = ({ boxId }: Props) => {
object={box}
clientId={clientId}
updateObject={fetchBox}
disabled={isScanning}
/>
</Stack>
);

View File

@@ -1,9 +1,9 @@
import useReceiptPallet from "../hooks/useReceiptPallet.tsx";
import { Button, Flex, Group, Stack, Text, Title } from "@mantine/core";
import { IconArrowLeft, IconPlus } from "@tabler/icons-react";
import { ActionIcon, Button, Flex, Group, Stack, Text, Title } from "@mantine/core";
import { IconArrowLeft, IconBarcode, IconPlayerPause, IconPlus } from "@tabler/icons-react";
import InlineButton from "../../../../../components/InlineButton/InlineButton.tsx";
import ReceiptProducts from "./ReceiptProducts.tsx";
import AccordionBoxesOnPallet from "./AccordionBoxes.tsx";
import AccordionBoxesOnPallet from "./AccordionBoxesOnPallet.tsx";
type Props = {
palletId: number;
@@ -16,9 +16,19 @@ const ReceiptPalletEditor = ({ palletId }: Props) => {
clientId,
onCreateProductClick,
onCreateBoxClick,
barcodesProductsMap,
scanningData,
toggleScanning,
} = useReceiptPallet({ palletId });
if (!pallet) return <Text>Паллет c ID P{palletId} не найден</Text>;
if (!pallet) {
return <Text>Паллет c ID P{palletId} не найден</Text>;
}
const {
isScanning,
boxId,
} = scanningData;
const createButtons = () => {
const isBoxes = pallet.boxes.length > 0;
@@ -27,13 +37,13 @@ const ReceiptPalletEditor = ({ palletId }: Props) => {
return (
<Group>
{(isBoxes || isBoth) && (
<InlineButton onClick={onCreateBoxClick} flex={1}>
<InlineButton onClick={onCreateBoxClick} flex={1} disabled={isScanning}>
<IconPlus />
Короб
</InlineButton>
)}
{(isProducts || isBoth) && (
<InlineButton onClick={onCreateProductClick} flex={1}>
<InlineButton onClick={onCreateProductClick} flex={1} disabled={isScanning}>
<IconPlus />
Товар
</InlineButton>
@@ -50,6 +60,9 @@ const ReceiptPalletEditor = ({ palletId }: Props) => {
pallet={pallet}
clientId={clientId}
fetchPallet={fetchPallet}
barcodesProductsMap={barcodesProductsMap}
scanningData={scanningData}
toggleScanning={toggleScanning}
/>
);
}
@@ -59,6 +72,7 @@ const ReceiptPalletEditor = ({ palletId }: Props) => {
clientId={clientId}
object={pallet}
updateObject={fetchPallet}
disabled={isScanning}
/>
);
}
@@ -74,14 +88,34 @@ const ReceiptPalletEditor = ({ palletId }: Props) => {
</Button>
);
const getScanningModeAction = () => {
if (pallet.boxes.length !== 0) return;
return (
<ActionIcon
variant={"default"}
onClick={() => toggleScanning(undefined, pallet?.id)}
disabled={isScanning && !!boxId}
>
{isScanning && !boxId ? (
<IconPlayerPause />
) : (
<IconBarcode />
)}
</ActionIcon>
);
};
return (
<Stack>
<Flex align={"center"} flex={9} gap={"md"}>
{backButton}
<Title flex={8} order={3}>
Паллет ID: П{pallet?.id}
</Title>
<Flex align={"center"} justify={"space-between"}>
<Flex align={"center"} flex={9} gap={"md"}>
{backButton}
<Title flex={8} order={3}>
Паллет ID: П{pallet?.id}
</Title>
</Flex>
{getScanningModeAction()}
</Flex>
{createButtons()}
{renderPalletData()}

View File

@@ -9,9 +9,10 @@ type Props = {
clientId: number | null;
object: ResidualBoxSchema | ResidualPalletSchema | null;
updateObject: () => void;
disabled: boolean;
}
const ReceiptProducts = ({ clientId, object, updateObject }: Props) => {
const ReceiptProducts = ({ clientId, object, updateObject, disabled }: Props) => {
if (!object || !clientId) return;
const deleteProduct = (residualProductId: number) => {
@@ -37,11 +38,13 @@ const ReceiptProducts = ({ clientId, object, updateObject }: Props) => {
<ProductQuantityField
residualProduct={residualProduct}
updateObject={updateObject}
disabled={disabled}
/>
</Stack>
<ActionIcon
variant={"default"}
onClick={() => deleteProduct(residualProduct.id)}
disabled={disabled}
>
<IconTrash />
</ActionIcon>

View File

@@ -0,0 +1,86 @@
import { notifications } from "../../../../../shared/lib/notifications.ts";
import { ProductSchema, ResidualBoxSchema, ResidualPalletSchema, ResiduesService } from "../../../../../client";
import { modals } from "@mantine/modals";
import findProductInObject from "../../NewReceipt/utils/findProductInObject.tsx";
type Props = {
object: ResidualBoxSchema | ResidualPalletSchema | null;
barcodesProductsMap: Map<string, ProductSchema[]>;
refetch: () => void;
}
const useApplyingScannedResult = ({
object,
barcodesProductsMap,
refetch,
}: Props) => {
let boxIdValue: number | null = null;
const onProductSelect = (product: ProductSchema) => {
if (!object) return;
ResiduesService.createResidualProduct({
requestBody: {
data: {
productId: product.id,
quantity: 1,
palletId: boxIdValue ? null : object.id,
boxId: boxIdValue,
},
},
})
.then(({ ok, message }) => {
notifications.guess(ok, { message });
refetch();
})
.catch(err => console.log(err));
};
const showSelectProductModal = (productsToSelect: ProductSchema[]) => {
modals.openContextModal({
modal: "selectScannedProductModal",
title: "Выберите товар для данного штрихкода",
withCloseButton: false,
innerProps: {
productsToSelect,
onProductSelect,
},
});
};
const onScanningFinish = (
value: string,
boxId?: number,
palletId?: number,
) => {
if (!object) return;
boxIdValue = boxId ?? null;
let objectValue: ResidualPalletSchema | ResidualBoxSchema = object;
if (palletId && boxId) {
const pallet = object as ResidualPalletSchema;
const boxOnPallet = pallet.boxes.find(box => box.id === boxId);
if (!boxOnPallet) return;
objectValue = boxOnPallet;
}
const productsToSelect = barcodesProductsMap.get(value) ?? [];
if (productsToSelect?.length === 0) {
notifications.error({ message: `Товара с штрихкодом ${value} не найдено` });
return;
}
if (productsToSelect?.length === 1) {
onProductSelect(productsToSelect[0]);
return;
}
const product = findProductInObject(objectValue, productsToSelect);
if (product) {
onProductSelect(product);
return;
}
return showSelectProductModal(productsToSelect);
};
return { onScanningFinish };
};
export default useApplyingScannedResult;

View File

@@ -1,6 +1,9 @@
import { useEffect, useState } from "react";
import { ResidualBoxSchema, ResiduesService } from "../../../../../client";
import { modals } from "@mantine/modals";
import useBarcodesProductsMap from "../../../hooks/useBarcodesProductsMap.tsx";
import useScanningMode from "../../../hooks/useScanningMode.tsx";
import useApplyingScannedResult from "./useApplyingScannedResult.tsx";
type Props = {
@@ -27,6 +30,16 @@ const useReceiptBox = ({ boxId }: Props) => {
.catch(err => console.log(err));
};
const { barcodesProductsMap } = useBarcodesProductsMap({ clientId: clientId ?? -1 });
const { onScanningFinish } = useApplyingScannedResult({
object: box,
barcodesProductsMap,
refetch: fetchBox,
});
const { scanningData, toggleScanning } = useScanningMode({ onScanningFinish });
const onCreateProductClick = () => {
if (!(box && clientId)) return;
modals.openContextModal({
@@ -38,6 +51,7 @@ const useReceiptBox = ({ boxId }: Props) => {
isBox: true,
object: box,
fetchObject: fetchBox,
barcodesProductsMap,
},
});
};
@@ -47,6 +61,8 @@ const useReceiptBox = ({ boxId }: Props) => {
fetchBox,
clientId,
onCreateProductClick,
scanningData,
toggleScanning,
};
};

View File

@@ -2,6 +2,9 @@ import { useEffect, useState } from "react";
import { ResidualPalletSchema, ResiduesService } from "../../../../../client";
import { modals } from "@mantine/modals";
import { notifications } from "../../../../../shared/lib/notifications.ts";
import useBarcodesProductsMap from "../../../hooks/useBarcodesProductsMap.tsx";
import useApplyingScannedResult from "./useApplyingScannedResult.tsx";
import useScanningMode from "../../../hooks/useScanningMode.tsx";
type Props = {
palletId: number;
@@ -27,6 +30,16 @@ const useReceiptPallet = ({ palletId }: Props) => {
.catch(err => console.log(err));
};
const { barcodesProductsMap } = useBarcodesProductsMap({ clientId: clientId ?? -1 });
const { onScanningFinish } = useApplyingScannedResult({
object: pallet,
barcodesProductsMap,
refetch: fetchPallet,
});
const { scanningData, toggleScanning } = useScanningMode({ onScanningFinish });
const onCreateProductClick = () => {
if (!(pallet && clientId)) return;
modals.openContextModal({
@@ -38,6 +51,7 @@ const useReceiptPallet = ({ palletId }: Props) => {
isBox: false,
object: pallet,
fetchObject: fetchPallet,
barcodesProductsMap,
},
});
};
@@ -64,6 +78,9 @@ const useReceiptPallet = ({ palletId }: Props) => {
clientId,
onCreateProductClick,
onCreateBoxClick,
barcodesProductsMap,
scanningData,
toggleScanning,
};
};

View File

@@ -14,6 +14,7 @@ type Props = {
object: ResidualBoxSchema | ResidualPalletSchema;
fetchObject: () => void;
isBox: boolean;
barcodesProductsMap: Map<string, ProductSchema[]>;
}
const ReceiptModal = ({
@@ -25,7 +26,9 @@ const ReceiptModal = ({
clientId,
object,
fetchObject,
barcodesProductsMap,
} = innerProps;
const { isScanning, setIsScanning, scannedValue, setScannedValue } = useScanning();
const initialValues = {
product: null,
@@ -115,12 +118,12 @@ const ReceiptModal = ({
</form>
<ScanBarcode
clientId={clientId}
isScanning={isScanning}
setIsScanning={setIsScanning}
onProductSelect={onProductAfterScanningSelect}
scannedValue={scannedValue}
object={object}
barcodesProductsMap={barcodesProductsMap}
/>
</Flex>
);

View File

@@ -1,29 +1,28 @@
import { ProductSchema, ResidualBoxSchema, ResidualPalletSchema } from "../../../../client";
import { useEffect, useState } from "react";
import useProductsList from "../../../ProductsPage/hooks/useProductsList.tsx";
import { useState } from "react";
import { Button, Group, Radio, Stack, Text } from "@mantine/core";
import { notifications } from "../../../../shared/lib/notifications.ts";
import { ReceiptBox, ReceiptPallet } from "../NewReceipt/types/types.tsx";
import findProductInObject from "../NewReceipt/utils/findProductInObject.tsx";
type Props = {
onProductSelect: (product?: ProductSchema) => void;
clientId: number;
isScanning: boolean;
setIsScanning: (isScanning: boolean) => void;
scannedValue: string;
object: ReceiptBox | ReceiptPallet | ResidualPalletSchema | ResidualBoxSchema;
barcodesProductsMap: Map<string, ProductSchema[]>;
}
const ScanBarcode = ({
onProductSelect,
clientId,
isScanning,
setIsScanning,
scannedValue,
object,
barcodesProductsMap,
}: Props) => {
const productsData = useProductsList({ clientId, searchInput: "" });
const [barcodesProducts, setBarcodesProducts] = useState(new Map<string, ProductSchema[]>());
let productsToSelect: ProductSchema[] = [];
const [selectedProduct, setSelectedProduct] = useState<ProductSchema>();
@@ -31,38 +30,8 @@ const ScanBarcode = ({
setIsScanning(!isScanning);
};
const productsToBarcodesProducts = () => {
const data = new Map<string, ProductSchema[]>();
productsData.products.forEach(product => {
product.barcodes.forEach(barcode => {
if (data.has(barcode)) {
data.set(barcode, [...data.get(barcode)!, product]);
} else {
data.set(barcode, [product]);
}
});
});
setBarcodesProducts(data);
};
useEffect(() => {
if (productsData.products.length !== 0) {
productsToBarcodesProducts();
}
}, [productsData.isLoading]);
const findProductInObject = (productsToSelect: ProductSchema[]): ProductSchema | undefined => {
for (let i = 0; i < productsToSelect.length; i++) {
for (let j = 0; j < object.residualProducts.length; j++) {
if (productsToSelect[i].id === object.residualProducts[j].product.id) {
return productsToSelect[i];
}
}
}
};
const renderScanningResults = () => {
productsToSelect = barcodesProducts.get(scannedValue) ?? [];
productsToSelect = barcodesProductsMap.get(scannedValue) ?? [];
if (productsToSelect?.length === 0) {
notifications.error({ message: `Товара с штрихкодом ${scannedValue} не найдено` });
onProductSelect();
@@ -72,7 +41,7 @@ const ScanBarcode = ({
onProductSelect(productsToSelect[0]);
return;
}
const product = findProductInObject(productsToSelect);
const product = findProductInObject(object, productsToSelect);
if (product) {
onProductSelect(product);
return;

View File

@@ -0,0 +1,42 @@
import useProductsList from "../../ProductsPage/hooks/useProductsList.tsx";
import { useEffect, useState } from "react";
import { ProductSchema } from "../../../client";
type Props = {
clientId?: number;
}
const useBarcodesProductsMap = ({ clientId }: Props) => {
const {
products,
isLoading: isProductsLoading,
} = useProductsList({ clientId: clientId ?? 0, searchInput: "" });
const [barcodesProductsMap, setBarcodesProductsMap] = useState(new Map<string, ProductSchema[]>());
const productsToBarcodesProducts = () => {
const data = new Map<string, ProductSchema[]>();
products.forEach(product => {
product.barcodes.forEach(barcode => {
if (data.has(barcode)) {
data.set(barcode, [...data.get(barcode)!, product]);
} else {
data.set(barcode, [product]);
}
});
});
setBarcodesProductsMap(data);
};
useEffect(() => {
if (products.length !== 0) {
productsToBarcodesProducts();
}
}, [isProductsLoading]);
return {
barcodesProductsMap,
};
};
export default useBarcodesProductsMap;

View File

@@ -0,0 +1,63 @@
import { useState } from "react";
import { useWindowEvent } from "@mantine/hooks";
export type ScanningData = {
boxId?: number;
palletId?: number;
isScanning: boolean;
}
type Props = {
onScanningFinish: (value: string, boxId?: number, palletId?: number) => void;
}
const useScanningMode = ({ onScanningFinish }: Props) => {
const [scanningValue, setScanningValue] = useState<string>("");
const [boxId, setBoxId] = useState<number>();
const [palletId, setPalletId] = useState<number>();
const [isScanning, setIsScanning] = useState<boolean>(false);
const setIsScanningValue = (isScanning: boolean) => {
if (!isScanning) {
setScanningValue("");
setBoxId(undefined);
setPalletId(undefined);
}
setIsScanning(isScanning);
};
useWindowEvent("keydown", (event) => {
if (!isScanning) return;
event.preventDefault();
setScanningValue(prevState => prevState + event.key);
if (["\n", "\r", "Enter"].includes(event.key)) {
onScanningFinish(scanningValue, boxId, palletId);
setScanningValue("");
}
});
const toggleScanning = (boxId?: number, palletId?: number) => {
if (isScanning) {
setBoxId(undefined);
setPalletId(undefined);
} else {
setBoxId(boxId);
setPalletId(palletId);
}
setIsScanningValue(!isScanning);
setScanningValue("");
};
const scanningData: ScanningData = {
boxId,
palletId,
isScanning,
};
return {
scanningData,
toggleScanning,
};
};
export default useScanningMode;

View File

@@ -0,0 +1,61 @@
import { ContextModalProps } from "@mantine/modals";
import { Button, Group, Radio, Stack, Text } from "@mantine/core";
import { ProductSchema } from "../../../client";
import { notifications } from "../../../shared/lib/notifications.ts";
import { useState } from "react";
type Props = {
productsToSelect: ProductSchema[];
onProductSelect: (product: ProductSchema) => void;
}
const SelectScannedProductModal = ({
context,
id,
innerProps,
}: ContextModalProps<Props>) => {
const {
productsToSelect,
onProductSelect,
} = innerProps;
const [selectedProduct, setSelectedProduct] = useState<ProductSchema>();
const renderProductsToSelect = () => {
return productsToSelect.map(product => (
<Group key={product.id} wrap={"nowrap"}>
<Radio
checked={selectedProduct?.id === product.id}
onChange={() => setSelectedProduct(product)}
/>
<Stack>
<Text>{product.name}</Text>
{product.size && <Text>{product.size}</Text>}
</Stack>
</Group>
));
};
return (
<Stack>
{renderProductsToSelect()}
<Button
variant={"default"}
onClick={() => {
if (!selectedProduct) {
notifications.error({ message: "Товар не выбран" });
} else {
onProductSelect(selectedProduct);
setSelectedProduct(undefined);
context.closeContextModal(id);
}
}}
>
Выбрать
</Button>
</Stack>
);
};
export default SelectScannedProductModal;