feat: scanning mode on receipt page
This commit is contained in:
@@ -33,6 +33,7 @@ import AssignUserModal from "../pages/LeadsPage/tabs/EmployeesTab/modals/AssignU
|
|||||||
import ResidualProductModal from "../pages/ResiduesPage/modals/ResidualProductModal/ResidualProductModal.tsx";
|
import ResidualProductModal from "../pages/ResiduesPage/modals/ResidualProductModal/ResidualProductModal.tsx";
|
||||||
import NewReceiptModal from "../pages/ReceiptPage/components/NewReceipt/modals/NewReceiptModal.tsx";
|
import NewReceiptModal from "../pages/ReceiptPage/components/NewReceipt/modals/NewReceiptModal.tsx";
|
||||||
import ReceiptModal from "../pages/ReceiptPage/components/ReceiptEditing/modals/ReceiptModal.tsx";
|
import ReceiptModal from "../pages/ReceiptPage/components/ReceiptEditing/modals/ReceiptModal.tsx";
|
||||||
|
import SelectScannedProductModal from "../pages/ReceiptPage/modals/SelectScannedProductModal.tsx";
|
||||||
|
|
||||||
export const modals = {
|
export const modals = {
|
||||||
enterDeadline: EnterDeadlineModal,
|
enterDeadline: EnterDeadlineModal,
|
||||||
@@ -63,6 +64,7 @@ export const modals = {
|
|||||||
transactionTagsModal: TransactionTagsModal,
|
transactionTagsModal: TransactionTagsModal,
|
||||||
shippingProductModal: ShippingProductModal,
|
shippingProductModal: ShippingProductModal,
|
||||||
residualProductModal: ResidualProductModal,
|
residualProductModal: ResidualProductModal,
|
||||||
|
selectScannedProductModal: SelectScannedProductModal,
|
||||||
newReceiptModal: NewReceiptModal,
|
newReceiptModal: NewReceiptModal,
|
||||||
receiptModal: ReceiptModal,
|
receiptModal: ReceiptModal,
|
||||||
departmentModal: DepartmentModal,
|
departmentModal: DepartmentModal,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Accordion, ActionIcon, Center } from "@mantine/core";
|
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 { ReceiptBox, ReceiptPallet } from "../types/types.tsx";
|
||||||
import { useReceiptContext } from "../contexts/ReceiptContext.tsx";
|
import { useReceiptContext } from "../contexts/ReceiptContext.tsx";
|
||||||
import ReceiptProducts from "./ReceiptProducts.tsx";
|
import ReceiptProducts from "./ReceiptProducts.tsx";
|
||||||
@@ -10,7 +10,17 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const AccordionBoxes = ({ pallet, palletIdx }: 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) => {
|
const deleteBox = (boxId: number) => {
|
||||||
if (palletIdx && pallet) {
|
if (palletIdx && pallet) {
|
||||||
@@ -23,9 +33,22 @@ const AccordionBoxes = ({ pallet, palletIdx }: Props) => {
|
|||||||
const boxActions = (box: ReceiptBox) => {
|
const boxActions = (box: ReceiptBox) => {
|
||||||
return (
|
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
|
<ActionIcon
|
||||||
variant={"default"}
|
variant={"default"}
|
||||||
onClick={() => onObjectEditClick(box, true, pallet)}
|
onClick={() => onObjectEditClick(box, true, pallet)}
|
||||||
|
disabled={isScanning}
|
||||||
mr={"sm"}
|
mr={"sm"}
|
||||||
>
|
>
|
||||||
<IconPlus />
|
<IconPlus />
|
||||||
@@ -33,6 +56,7 @@ const AccordionBoxes = ({ pallet, palletIdx }: Props) => {
|
|||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant={"default"}
|
variant={"default"}
|
||||||
onClick={() => deleteBox(box.id)}
|
onClick={() => deleteBox(box.id)}
|
||||||
|
disabled={isScanning}
|
||||||
mr={"sm"}
|
mr={"sm"}
|
||||||
>
|
>
|
||||||
<IconTrash />
|
<IconTrash />
|
||||||
@@ -60,6 +84,7 @@ const AccordionBoxes = ({ pallet, palletIdx }: Props) => {
|
|||||||
products={box.residualProducts}
|
products={box.residualProducts}
|
||||||
object={box}
|
object={box}
|
||||||
setObjectData={(box: ReceiptBox) => setBoxData(box, pallet)}
|
setObjectData={(box: ReceiptBox) => setBoxData(box, pallet)}
|
||||||
|
disabled={isScanning}
|
||||||
/>
|
/>
|
||||||
</Accordion.Panel>
|
</Accordion.Panel>
|
||||||
</Accordion.Item>
|
</Accordion.Item>
|
||||||
|
|||||||
@@ -1,12 +1,22 @@
|
|||||||
import { Accordion, ActionIcon, Box, Button, Center, Flex, Group } from "@mantine/core";
|
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 { ReceiptBox, ReceiptPallet } from "../types/types.tsx";
|
||||||
import { useReceiptContext } from "../contexts/ReceiptContext.tsx";
|
import { useReceiptContext } from "../contexts/ReceiptContext.tsx";
|
||||||
import AccordionBoxes from "./AccordionBoxes.tsx";
|
import AccordionBoxes from "./AccordionBoxes.tsx";
|
||||||
import ReceiptProducts from "./ReceiptProducts.tsx";
|
import ReceiptProducts from "./ReceiptProducts.tsx";
|
||||||
|
|
||||||
const AccordionPallets = () => {
|
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) => {
|
const deletePallet = (palletId: number) => {
|
||||||
palletsHandlers.filter(item => item.id !== palletId);
|
palletsHandlers.filter(item => item.id !== palletId);
|
||||||
@@ -23,10 +33,26 @@ const AccordionPallets = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const palletActions = (pallet: ReceiptPallet) => {
|
const palletActions = (pallet: ReceiptPallet) => {
|
||||||
|
const isScanModeVisible = pallet.boxes.length === 0;
|
||||||
const isCreateButtonVisible = pallet.boxes.length > 0 || pallet.residualProducts.length > 0;
|
const isCreateButtonVisible = pallet.boxes.length > 0 || pallet.residualProducts.length > 0;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isCreateButtonVisible && <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"}
|
variant={"default"}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (pallet.residualProducts.length > 0) {
|
if (pallet.residualProducts.length > 0) {
|
||||||
@@ -35,13 +61,16 @@ const AccordionPallets = () => {
|
|||||||
createBox(pallet);
|
createBox(pallet);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
disabled={isScanning}
|
||||||
mr={"sm"}
|
mr={"sm"}
|
||||||
>
|
>
|
||||||
<IconPlus />
|
<IconPlus />
|
||||||
</ActionIcon>}
|
</ActionIcon>
|
||||||
|
)}
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant={"default"}
|
variant={"default"}
|
||||||
onClick={() => deletePallet(pallet.id)}
|
onClick={() => deletePallet(pallet.id)}
|
||||||
|
disabled={isScanning}
|
||||||
mr={"md"}
|
mr={"md"}
|
||||||
>
|
>
|
||||||
<IconTrash />
|
<IconTrash />
|
||||||
@@ -55,6 +84,7 @@ const AccordionPallets = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant={"default"}
|
variant={"default"}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
disabled={isScanning}
|
||||||
flex={1}
|
flex={1}
|
||||||
>
|
>
|
||||||
<Group gap={"md"}>
|
<Group gap={"md"}>
|
||||||
@@ -98,6 +128,7 @@ const AccordionPallets = () => {
|
|||||||
products={pallet.residualProducts}
|
products={pallet.residualProducts}
|
||||||
object={pallet}
|
object={pallet}
|
||||||
setObjectData={object => setPalletData(object as ReceiptPallet)}
|
setObjectData={object => setPalletData(object as ReceiptPallet)}
|
||||||
|
disabled={isScanning}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Accordion.Panel>
|
</Accordion.Panel>
|
||||||
|
|||||||
@@ -7,7 +7,15 @@ import InlineButton from "../../../../../components/InlineButton/InlineButton.ts
|
|||||||
import { IconPlus } from "@tabler/icons-react";
|
import { IconPlus } from "@tabler/icons-react";
|
||||||
|
|
||||||
const ReceiptEditor = () => {
|
const ReceiptEditor = () => {
|
||||||
const { fixed, palletsHandlers, boxesHandlers, nextId } = useReceiptContext();
|
const {
|
||||||
|
fixed,
|
||||||
|
palletsHandlers,
|
||||||
|
boxesHandlers,
|
||||||
|
nextId,
|
||||||
|
scanningData,
|
||||||
|
} = useReceiptContext();
|
||||||
|
|
||||||
|
const { isScanning } = scanningData;
|
||||||
|
|
||||||
if (!fixed) return;
|
if (!fixed) return;
|
||||||
|
|
||||||
@@ -31,11 +39,19 @@ const ReceiptEditor = () => {
|
|||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
<Flex gap="md">
|
<Flex gap="md">
|
||||||
<InlineButton onClick={() => createPallet()} flex={1}>
|
<InlineButton
|
||||||
|
onClick={() => createPallet()}
|
||||||
|
flex={1}
|
||||||
|
disabled={isScanning}
|
||||||
|
>
|
||||||
<IconPlus />
|
<IconPlus />
|
||||||
Паллет
|
Паллет
|
||||||
</InlineButton>
|
</InlineButton>
|
||||||
<InlineButton onClick={() => createBox()} flex={1}>
|
<InlineButton
|
||||||
|
onClick={() => createBox()}
|
||||||
|
flex={1}
|
||||||
|
disabled={isScanning}
|
||||||
|
>
|
||||||
<IconPlus />
|
<IconPlus />
|
||||||
Короб
|
Короб
|
||||||
</InlineButton>
|
</InlineButton>
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ type Props = {
|
|||||||
products: ReceiptProduct[];
|
products: ReceiptProduct[];
|
||||||
object: ReceiptBox | ReceiptPallet;
|
object: ReceiptBox | ReceiptPallet;
|
||||||
setObjectData: (object: ReceiptBox | ReceiptPallet) => void;
|
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;
|
if (products.length === 0) return;
|
||||||
|
|
||||||
const deleteProduct = (productId: number) => {
|
const deleteProduct = (productId: number) => {
|
||||||
@@ -31,6 +32,7 @@ const ReceiptProducts = ({ products, object, setObjectData }: Props) => {
|
|||||||
allowDecimal={false}
|
allowDecimal={false}
|
||||||
allowNegative={false}
|
allowNegative={false}
|
||||||
w={rem(100)}
|
w={rem(100)}
|
||||||
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
@@ -50,6 +52,7 @@ const ReceiptProducts = ({ products, object, setObjectData }: Props) => {
|
|||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant={"default"}
|
variant={"default"}
|
||||||
onClick={() => deleteProduct(receiptProduct.id)}
|
onClick={() => deleteProduct(receiptProduct.id)}
|
||||||
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<IconTrash />
|
<IconTrash />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
|
|||||||
@@ -4,21 +4,29 @@ import { ReceiptBox, ReceiptPallet } from "../types/types.tsx";
|
|||||||
import { modals } from "@mantine/modals";
|
import { modals } from "@mantine/modals";
|
||||||
import nextId from "../utils/nextId.ts";
|
import nextId from "../utils/nextId.ts";
|
||||||
import { useListState, UseListStateHandlers } from "@mantine/hooks";
|
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 = {
|
type ReceiptContextState = {
|
||||||
client?: ClientSchema;
|
client?: ClientSchema;
|
||||||
setClient: Dispatch<SetStateAction<ClientSchema | undefined>>;
|
setClient: Dispatch<SetStateAction<ClientSchema | undefined>>;
|
||||||
fixed: boolean;
|
fixed: boolean;
|
||||||
setFixed: Dispatch<SetStateAction<boolean>>;
|
setFixed: Dispatch<SetStateAction<boolean>>;
|
||||||
|
|
||||||
pallets: ReceiptPallet[];
|
pallets: ReceiptPallet[];
|
||||||
palletsHandlers: UseListStateHandlers<ReceiptPallet>;
|
palletsHandlers: UseListStateHandlers<ReceiptPallet>;
|
||||||
boxes: ReceiptBox[];
|
boxes: ReceiptBox[];
|
||||||
boxesHandlers: UseListStateHandlers<ReceiptBox>;
|
boxesHandlers: UseListStateHandlers<ReceiptBox>;
|
||||||
|
|
||||||
nextId: () => number;
|
nextId: () => number;
|
||||||
onObjectEditClick: (object: ReceiptBox | ReceiptPallet, isBox: boolean, parentPallet?: ReceiptPallet) => void;
|
onObjectEditClick: (object: ReceiptBox | ReceiptPallet, isBox: boolean, parentPallet?: ReceiptPallet) => void;
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
setBoxData: (box: ReceiptBox, pallet?: ReceiptPallet) => void;
|
setBoxData: (box: ReceiptBox, pallet?: ReceiptPallet) => void;
|
||||||
setPalletData: (pallet: ReceiptPallet) => void;
|
setPalletData: (pallet: ReceiptPallet) => void;
|
||||||
|
|
||||||
|
scanningData: ScanningData;
|
||||||
|
toggleScanning: (boxId?: number, palletId?: number) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ReceiptContext = createContext<ReceiptContextState | undefined>(
|
const ReceiptContext = createContext<ReceiptContextState | undefined>(
|
||||||
@@ -32,6 +40,8 @@ const useReceiptContextState = () => {
|
|||||||
const [pallets, palletsHandlers] = useListState<ReceiptPallet>([]);
|
const [pallets, palletsHandlers] = useListState<ReceiptPallet>([]);
|
||||||
const [boxes, boxesHandlers] = useListState<ReceiptBox>([]);
|
const [boxes, boxesHandlers] = useListState<ReceiptBox>([]);
|
||||||
|
|
||||||
|
const { barcodesProductsMap } = useBarcodesProductsMap({ clientId: client?.id });
|
||||||
|
|
||||||
const setBoxData = (box: ReceiptBox, pallet?: ReceiptPallet) => {
|
const setBoxData = (box: ReceiptBox, pallet?: ReceiptPallet) => {
|
||||||
if (pallet) {
|
if (pallet) {
|
||||||
setBoxOnPalletData(box, 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 = (
|
const onObjectEditClick = (
|
||||||
object: ReceiptBox | ReceiptPallet,
|
object: ReceiptBox | ReceiptPallet,
|
||||||
isBox: boolean,
|
isBox: boolean,
|
||||||
@@ -83,13 +102,24 @@ const useReceiptContextState = () => {
|
|||||||
clientId: client.id,
|
clientId: client.id,
|
||||||
object: object,
|
object: object,
|
||||||
setObjectData: (object: ReceiptBox | ReceiptPallet) => {
|
setObjectData: (object: ReceiptBox | ReceiptPallet) => {
|
||||||
if (isBox) setBoxData(object, parentPallet);
|
editObject(object, isBox, parentPallet);
|
||||||
setPalletData(object as ReceiptPallet);
|
|
||||||
},
|
},
|
||||||
|
barcodesProductsMap,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
onScanningFinish,
|
||||||
|
} = useApplyingScanningResult({
|
||||||
|
pallets,
|
||||||
|
boxes,
|
||||||
|
barcodesProductsMap,
|
||||||
|
editObject,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { scanningData, toggleScanning } = useScanningMode({ onScanningFinish });
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
palletsHandlers.setState([]);
|
palletsHandlers.setState([]);
|
||||||
boxesHandlers.setState([]);
|
boxesHandlers.setState([]);
|
||||||
@@ -111,6 +141,8 @@ const useReceiptContextState = () => {
|
|||||||
reset,
|
reset,
|
||||||
setBoxData,
|
setBoxData,
|
||||||
setPalletData,
|
setPalletData,
|
||||||
|
scanningData,
|
||||||
|
toggleScanning,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -14,6 +14,7 @@ type Props = {
|
|||||||
clientId: number;
|
clientId: number;
|
||||||
object: ReceiptBox | ReceiptPallet;
|
object: ReceiptBox | ReceiptPallet;
|
||||||
setObjectData: (object: ReceiptBox | ReceiptPallet) => void;
|
setObjectData: (object: ReceiptBox | ReceiptPallet) => void;
|
||||||
|
barcodesProductsMap: Map<string, ProductSchema[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NewReceiptModal = ({
|
const NewReceiptModal = ({
|
||||||
@@ -25,6 +26,7 @@ const NewReceiptModal = ({
|
|||||||
clientId,
|
clientId,
|
||||||
setObjectData,
|
setObjectData,
|
||||||
object,
|
object,
|
||||||
|
barcodesProductsMap,
|
||||||
} = innerProps;
|
} = innerProps;
|
||||||
const { isScanning, setIsScanning, scannedValue, setScannedValue } = useScanning();
|
const { isScanning, setIsScanning, scannedValue, setScannedValue } = useScanning();
|
||||||
|
|
||||||
@@ -112,12 +114,12 @@ const NewReceiptModal = ({
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<ScanBarcode
|
<ScanBarcode
|
||||||
clientId={clientId}
|
|
||||||
isScanning={isScanning}
|
isScanning={isScanning}
|
||||||
setIsScanning={setIsScanning}
|
setIsScanning={setIsScanning}
|
||||||
onProductSelect={onProductAfterScanningSelect}
|
onProductSelect={onProductAfterScanningSelect}
|
||||||
scannedValue={scannedValue}
|
scannedValue={scannedValue}
|
||||||
object={object}
|
object={object}
|
||||||
|
barcodesProductsMap={barcodesProductsMap}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -1,17 +1,33 @@
|
|||||||
import { Accordion, ActionIcon, Center } from "@mantine/core";
|
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 ReceiptProducts from "./ReceiptProducts.tsx";
|
||||||
import { ResidualBoxSchema, ResidualPalletSchema, ResiduesService } from "../../../../../client";
|
import { ProductSchema, ResidualBoxSchema, ResidualPalletSchema, ResiduesService } from "../../../../../client";
|
||||||
import { modals } from "@mantine/modals";
|
import { modals } from "@mantine/modals";
|
||||||
import { notifications } from "../../../../../shared/lib/notifications.ts";
|
import { notifications } from "../../../../../shared/lib/notifications.ts";
|
||||||
|
import { ScanningData } from "../../../hooks/useScanningMode.tsx";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
pallet: ResidualPalletSchema;
|
pallet: ResidualPalletSchema;
|
||||||
clientId: number;
|
clientId: number;
|
||||||
fetchPallet: () => void;
|
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) => {
|
const deleteBox = (boxId: number) => {
|
||||||
ResiduesService.deleteResidualBox({
|
ResiduesService.deleteResidualBox({
|
||||||
boxId,
|
boxId,
|
||||||
@@ -36,6 +52,7 @@ const AccordionBoxesOnPallet = ({ pallet, clientId, fetchPallet }: Props) => {
|
|||||||
isBox: true,
|
isBox: true,
|
||||||
object: box,
|
object: box,
|
||||||
fetchObject: fetchPallet,
|
fetchObject: fetchPallet,
|
||||||
|
barcodesProductsMap,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -43,10 +60,23 @@ const AccordionBoxesOnPallet = ({ pallet, clientId, fetchPallet }: Props) => {
|
|||||||
const boxActions = (box: ResidualBoxSchema) => {
|
const boxActions = (box: ResidualBoxSchema) => {
|
||||||
return (
|
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
|
<ActionIcon
|
||||||
variant={"default"}
|
variant={"default"}
|
||||||
onClick={() => onCreateProductClick(box)}
|
onClick={() => onCreateProductClick(box)}
|
||||||
mr={"sm"}
|
mr={"sm"}
|
||||||
|
disabled={isScanning}
|
||||||
>
|
>
|
||||||
<IconPlus />
|
<IconPlus />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
@@ -54,6 +84,7 @@ const AccordionBoxesOnPallet = ({ pallet, clientId, fetchPallet }: Props) => {
|
|||||||
variant={"default"}
|
variant={"default"}
|
||||||
onClick={() => deleteBox(box.id)}
|
onClick={() => deleteBox(box.id)}
|
||||||
mr={"sm"}
|
mr={"sm"}
|
||||||
|
disabled={isScanning}
|
||||||
>
|
>
|
||||||
<IconTrash />
|
<IconTrash />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
@@ -78,6 +109,7 @@ const AccordionBoxesOnPallet = ({ pallet, clientId, fetchPallet }: Props) => {
|
|||||||
clientId={clientId}
|
clientId={clientId}
|
||||||
object={box}
|
object={box}
|
||||||
updateObject={fetchPallet}
|
updateObject={fetchPallet}
|
||||||
|
disabled={isScanning}
|
||||||
/>
|
/>
|
||||||
</Accordion.Panel>
|
</Accordion.Panel>
|
||||||
</Accordion.Item>
|
</Accordion.Item>
|
||||||
@@ -5,9 +5,10 @@ import { notifications } from "../../../../../shared/lib/notifications.ts";
|
|||||||
type Props = {
|
type Props = {
|
||||||
residualProduct: ResidualProductSchema;
|
residualProduct: ResidualProductSchema;
|
||||||
updateObject: () => void;
|
updateObject: () => void;
|
||||||
|
disabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProductQuantityField = ({ residualProduct, updateObject }: Props) => {
|
const ProductQuantityField = ({ residualProduct, updateObject, disabled }: Props) => {
|
||||||
const updateProduct = (quantity: number) => {
|
const updateProduct = (quantity: number) => {
|
||||||
if (residualProduct.quantity === quantity) return;
|
if (residualProduct.quantity === quantity) return;
|
||||||
ResiduesService.updateResidualProduct({
|
ResiduesService.updateResidualProduct({
|
||||||
@@ -38,6 +39,7 @@ const ProductQuantityField = ({ residualProduct, updateObject }: Props) => {
|
|||||||
allowDecimal={false}
|
allowDecimal={false}
|
||||||
allowNegative={false}
|
allowNegative={false}
|
||||||
w={rem(100)}
|
w={rem(100)}
|
||||||
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 useReceiptBox from "../hooks/useReceiptBox.tsx";
|
||||||
import ReceiptProducts from "./ReceiptProducts.tsx";
|
import ReceiptProducts from "./ReceiptProducts.tsx";
|
||||||
import InlineButton from "../../../../../components/InlineButton/InlineButton.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 = {
|
type Props = {
|
||||||
boxId: number;
|
boxId: number;
|
||||||
@@ -14,8 +14,12 @@ const ReceiptBoxEditor = ({ boxId }: Props) => {
|
|||||||
fetchBox,
|
fetchBox,
|
||||||
clientId,
|
clientId,
|
||||||
onCreateProductClick,
|
onCreateProductClick,
|
||||||
|
scanningData,
|
||||||
|
toggleScanning,
|
||||||
} = useReceiptBox({ boxId });
|
} = useReceiptBox({ boxId });
|
||||||
|
|
||||||
|
const { isScanning } = scanningData;
|
||||||
|
|
||||||
if (!box) return <Text>Короб c ID K{boxId} не найден</Text>;
|
if (!box) return <Text>Короб c ID K{boxId} не найден</Text>;
|
||||||
|
|
||||||
const backButton = (
|
const backButton = (
|
||||||
@@ -23,20 +27,39 @@ const ReceiptBoxEditor = ({ boxId }: Props) => {
|
|||||||
variant={"default"}
|
variant={"default"}
|
||||||
onClick={() => window.location.reload()}
|
onClick={() => window.location.reload()}
|
||||||
flex={1}
|
flex={1}
|
||||||
|
disabled={isScanning}
|
||||||
>
|
>
|
||||||
<IconArrowLeft />
|
<IconArrowLeft />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const getScanningModeAction = () => {
|
||||||
|
return (
|
||||||
|
<ActionIcon
|
||||||
|
variant={"default"}
|
||||||
|
onClick={() => toggleScanning(box?.id)}
|
||||||
|
>
|
||||||
|
{isScanning ? (
|
||||||
|
<IconPlayerPause />
|
||||||
|
) : (
|
||||||
|
<IconBarcode />
|
||||||
|
)}
|
||||||
|
</ActionIcon>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
|
<Flex align={"center"} justify={"space-between"}>
|
||||||
<Flex align={"center"} flex={9} gap={"md"}>
|
<Flex align={"center"} flex={9} gap={"md"}>
|
||||||
{backButton}
|
{backButton}
|
||||||
<Title flex={8} order={3}>
|
<Title flex={8} order={3}>
|
||||||
Короб ID: К{box?.id}
|
Короб ID: К{box?.id}
|
||||||
</Title>
|
</Title>
|
||||||
</Flex>
|
</Flex>
|
||||||
<InlineButton onClick={onCreateProductClick}>
|
{getScanningModeAction()}
|
||||||
|
</Flex>
|
||||||
|
<InlineButton onClick={onCreateProductClick} disabled={isScanning}>
|
||||||
<IconPlus />
|
<IconPlus />
|
||||||
Товар
|
Товар
|
||||||
</InlineButton>
|
</InlineButton>
|
||||||
@@ -44,6 +67,7 @@ const ReceiptBoxEditor = ({ boxId }: Props) => {
|
|||||||
object={box}
|
object={box}
|
||||||
clientId={clientId}
|
clientId={clientId}
|
||||||
updateObject={fetchBox}
|
updateObject={fetchBox}
|
||||||
|
disabled={isScanning}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import useReceiptPallet from "../hooks/useReceiptPallet.tsx";
|
import useReceiptPallet from "../hooks/useReceiptPallet.tsx";
|
||||||
import { Button, Flex, Group, Stack, Text, Title } from "@mantine/core";
|
import { ActionIcon, Button, Flex, Group, Stack, Text, Title } from "@mantine/core";
|
||||||
import { IconArrowLeft, IconPlus } from "@tabler/icons-react";
|
import { IconArrowLeft, IconBarcode, IconPlayerPause, IconPlus } from "@tabler/icons-react";
|
||||||
import InlineButton from "../../../../../components/InlineButton/InlineButton.tsx";
|
import InlineButton from "../../../../../components/InlineButton/InlineButton.tsx";
|
||||||
import ReceiptProducts from "./ReceiptProducts.tsx";
|
import ReceiptProducts from "./ReceiptProducts.tsx";
|
||||||
import AccordionBoxesOnPallet from "./AccordionBoxes.tsx";
|
import AccordionBoxesOnPallet from "./AccordionBoxesOnPallet.tsx";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
palletId: number;
|
palletId: number;
|
||||||
@@ -16,9 +16,19 @@ const ReceiptPalletEditor = ({ palletId }: Props) => {
|
|||||||
clientId,
|
clientId,
|
||||||
onCreateProductClick,
|
onCreateProductClick,
|
||||||
onCreateBoxClick,
|
onCreateBoxClick,
|
||||||
|
barcodesProductsMap,
|
||||||
|
scanningData,
|
||||||
|
toggleScanning,
|
||||||
} = useReceiptPallet({ palletId });
|
} = 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 createButtons = () => {
|
||||||
const isBoxes = pallet.boxes.length > 0;
|
const isBoxes = pallet.boxes.length > 0;
|
||||||
@@ -27,13 +37,13 @@ const ReceiptPalletEditor = ({ palletId }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<Group>
|
<Group>
|
||||||
{(isBoxes || isBoth) && (
|
{(isBoxes || isBoth) && (
|
||||||
<InlineButton onClick={onCreateBoxClick} flex={1}>
|
<InlineButton onClick={onCreateBoxClick} flex={1} disabled={isScanning}>
|
||||||
<IconPlus />
|
<IconPlus />
|
||||||
Короб
|
Короб
|
||||||
</InlineButton>
|
</InlineButton>
|
||||||
)}
|
)}
|
||||||
{(isProducts || isBoth) && (
|
{(isProducts || isBoth) && (
|
||||||
<InlineButton onClick={onCreateProductClick} flex={1}>
|
<InlineButton onClick={onCreateProductClick} flex={1} disabled={isScanning}>
|
||||||
<IconPlus />
|
<IconPlus />
|
||||||
Товар
|
Товар
|
||||||
</InlineButton>
|
</InlineButton>
|
||||||
@@ -50,6 +60,9 @@ const ReceiptPalletEditor = ({ palletId }: Props) => {
|
|||||||
pallet={pallet}
|
pallet={pallet}
|
||||||
clientId={clientId}
|
clientId={clientId}
|
||||||
fetchPallet={fetchPallet}
|
fetchPallet={fetchPallet}
|
||||||
|
barcodesProductsMap={barcodesProductsMap}
|
||||||
|
scanningData={scanningData}
|
||||||
|
toggleScanning={toggleScanning}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -59,6 +72,7 @@ const ReceiptPalletEditor = ({ palletId }: Props) => {
|
|||||||
clientId={clientId}
|
clientId={clientId}
|
||||||
object={pallet}
|
object={pallet}
|
||||||
updateObject={fetchPallet}
|
updateObject={fetchPallet}
|
||||||
|
disabled={isScanning}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -74,15 +88,35 @@ const ReceiptPalletEditor = ({ palletId }: Props) => {
|
|||||||
</Button>
|
</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 (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
|
<Flex align={"center"} justify={"space-between"}>
|
||||||
<Flex align={"center"} flex={9} gap={"md"}>
|
<Flex align={"center"} flex={9} gap={"md"}>
|
||||||
{backButton}
|
{backButton}
|
||||||
<Title flex={8} order={3}>
|
<Title flex={8} order={3}>
|
||||||
Паллет ID: П{pallet?.id}
|
Паллет ID: П{pallet?.id}
|
||||||
</Title>
|
</Title>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
{getScanningModeAction()}
|
||||||
|
</Flex>
|
||||||
{createButtons()}
|
{createButtons()}
|
||||||
{renderPalletData()}
|
{renderPalletData()}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ type Props = {
|
|||||||
clientId: number | null;
|
clientId: number | null;
|
||||||
object: ResidualBoxSchema | ResidualPalletSchema | null;
|
object: ResidualBoxSchema | ResidualPalletSchema | null;
|
||||||
updateObject: () => void;
|
updateObject: () => void;
|
||||||
|
disabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ReceiptProducts = ({ clientId, object, updateObject }: Props) => {
|
const ReceiptProducts = ({ clientId, object, updateObject, disabled }: Props) => {
|
||||||
if (!object || !clientId) return;
|
if (!object || !clientId) return;
|
||||||
|
|
||||||
const deleteProduct = (residualProductId: number) => {
|
const deleteProduct = (residualProductId: number) => {
|
||||||
@@ -37,11 +38,13 @@ const ReceiptProducts = ({ clientId, object, updateObject }: Props) => {
|
|||||||
<ProductQuantityField
|
<ProductQuantityField
|
||||||
residualProduct={residualProduct}
|
residualProduct={residualProduct}
|
||||||
updateObject={updateObject}
|
updateObject={updateObject}
|
||||||
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant={"default"}
|
variant={"default"}
|
||||||
onClick={() => deleteProduct(residualProduct.id)}
|
onClick={() => deleteProduct(residualProduct.id)}
|
||||||
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<IconTrash />
|
<IconTrash />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { ResidualBoxSchema, ResiduesService } from "../../../../../client";
|
import { ResidualBoxSchema, ResiduesService } from "../../../../../client";
|
||||||
import { modals } from "@mantine/modals";
|
import { modals } from "@mantine/modals";
|
||||||
|
import useBarcodesProductsMap from "../../../hooks/useBarcodesProductsMap.tsx";
|
||||||
|
import useScanningMode from "../../../hooks/useScanningMode.tsx";
|
||||||
|
import useApplyingScannedResult from "./useApplyingScannedResult.tsx";
|
||||||
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -27,6 +30,16 @@ const useReceiptBox = ({ boxId }: Props) => {
|
|||||||
.catch(err => console.log(err));
|
.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 = () => {
|
const onCreateProductClick = () => {
|
||||||
if (!(box && clientId)) return;
|
if (!(box && clientId)) return;
|
||||||
modals.openContextModal({
|
modals.openContextModal({
|
||||||
@@ -38,6 +51,7 @@ const useReceiptBox = ({ boxId }: Props) => {
|
|||||||
isBox: true,
|
isBox: true,
|
||||||
object: box,
|
object: box,
|
||||||
fetchObject: fetchBox,
|
fetchObject: fetchBox,
|
||||||
|
barcodesProductsMap,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -47,6 +61,8 @@ const useReceiptBox = ({ boxId }: Props) => {
|
|||||||
fetchBox,
|
fetchBox,
|
||||||
clientId,
|
clientId,
|
||||||
onCreateProductClick,
|
onCreateProductClick,
|
||||||
|
scanningData,
|
||||||
|
toggleScanning,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import { useEffect, useState } from "react";
|
|||||||
import { ResidualPalletSchema, ResiduesService } from "../../../../../client";
|
import { ResidualPalletSchema, ResiduesService } from "../../../../../client";
|
||||||
import { modals } from "@mantine/modals";
|
import { modals } from "@mantine/modals";
|
||||||
import { notifications } from "../../../../../shared/lib/notifications.ts";
|
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 = {
|
type Props = {
|
||||||
palletId: number;
|
palletId: number;
|
||||||
@@ -27,6 +30,16 @@ const useReceiptPallet = ({ palletId }: Props) => {
|
|||||||
.catch(err => console.log(err));
|
.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 = () => {
|
const onCreateProductClick = () => {
|
||||||
if (!(pallet && clientId)) return;
|
if (!(pallet && clientId)) return;
|
||||||
modals.openContextModal({
|
modals.openContextModal({
|
||||||
@@ -38,6 +51,7 @@ const useReceiptPallet = ({ palletId }: Props) => {
|
|||||||
isBox: false,
|
isBox: false,
|
||||||
object: pallet,
|
object: pallet,
|
||||||
fetchObject: fetchPallet,
|
fetchObject: fetchPallet,
|
||||||
|
barcodesProductsMap,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -64,6 +78,9 @@ const useReceiptPallet = ({ palletId }: Props) => {
|
|||||||
clientId,
|
clientId,
|
||||||
onCreateProductClick,
|
onCreateProductClick,
|
||||||
onCreateBoxClick,
|
onCreateBoxClick,
|
||||||
|
barcodesProductsMap,
|
||||||
|
scanningData,
|
||||||
|
toggleScanning,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ type Props = {
|
|||||||
object: ResidualBoxSchema | ResidualPalletSchema;
|
object: ResidualBoxSchema | ResidualPalletSchema;
|
||||||
fetchObject: () => void;
|
fetchObject: () => void;
|
||||||
isBox: boolean;
|
isBox: boolean;
|
||||||
|
barcodesProductsMap: Map<string, ProductSchema[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ReceiptModal = ({
|
const ReceiptModal = ({
|
||||||
@@ -25,7 +26,9 @@ const ReceiptModal = ({
|
|||||||
clientId,
|
clientId,
|
||||||
object,
|
object,
|
||||||
fetchObject,
|
fetchObject,
|
||||||
|
barcodesProductsMap,
|
||||||
} = innerProps;
|
} = innerProps;
|
||||||
|
|
||||||
const { isScanning, setIsScanning, scannedValue, setScannedValue } = useScanning();
|
const { isScanning, setIsScanning, scannedValue, setScannedValue } = useScanning();
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
product: null,
|
product: null,
|
||||||
@@ -115,12 +118,12 @@ const ReceiptModal = ({
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<ScanBarcode
|
<ScanBarcode
|
||||||
clientId={clientId}
|
|
||||||
isScanning={isScanning}
|
isScanning={isScanning}
|
||||||
setIsScanning={setIsScanning}
|
setIsScanning={setIsScanning}
|
||||||
onProductSelect={onProductAfterScanningSelect}
|
onProductSelect={onProductAfterScanningSelect}
|
||||||
scannedValue={scannedValue}
|
scannedValue={scannedValue}
|
||||||
object={object}
|
object={object}
|
||||||
|
barcodesProductsMap={barcodesProductsMap}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,29 +1,28 @@
|
|||||||
import { ProductSchema, ResidualBoxSchema, ResidualPalletSchema } from "../../../../client";
|
import { ProductSchema, ResidualBoxSchema, ResidualPalletSchema } from "../../../../client";
|
||||||
import { useEffect, useState } from "react";
|
import { useState } from "react";
|
||||||
import useProductsList from "../../../ProductsPage/hooks/useProductsList.tsx";
|
|
||||||
import { Button, Group, Radio, Stack, Text } from "@mantine/core";
|
import { Button, Group, Radio, Stack, Text } from "@mantine/core";
|
||||||
import { notifications } from "../../../../shared/lib/notifications.ts";
|
import { notifications } from "../../../../shared/lib/notifications.ts";
|
||||||
import { ReceiptBox, ReceiptPallet } from "../NewReceipt/types/types.tsx";
|
import { ReceiptBox, ReceiptPallet } from "../NewReceipt/types/types.tsx";
|
||||||
|
import findProductInObject from "../NewReceipt/utils/findProductInObject.tsx";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onProductSelect: (product?: ProductSchema) => void;
|
onProductSelect: (product?: ProductSchema) => void;
|
||||||
clientId: number;
|
|
||||||
isScanning: boolean;
|
isScanning: boolean;
|
||||||
setIsScanning: (isScanning: boolean) => void;
|
setIsScanning: (isScanning: boolean) => void;
|
||||||
scannedValue: string;
|
scannedValue: string;
|
||||||
object: ReceiptBox | ReceiptPallet | ResidualPalletSchema | ResidualBoxSchema;
|
object: ReceiptBox | ReceiptPallet | ResidualPalletSchema | ResidualBoxSchema;
|
||||||
|
barcodesProductsMap: Map<string, ProductSchema[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ScanBarcode = ({
|
const ScanBarcode = ({
|
||||||
onProductSelect,
|
onProductSelect,
|
||||||
clientId,
|
|
||||||
isScanning,
|
isScanning,
|
||||||
setIsScanning,
|
setIsScanning,
|
||||||
scannedValue,
|
scannedValue,
|
||||||
object,
|
object,
|
||||||
|
barcodesProductsMap,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const productsData = useProductsList({ clientId, searchInput: "" });
|
|
||||||
const [barcodesProducts, setBarcodesProducts] = useState(new Map<string, ProductSchema[]>());
|
|
||||||
let productsToSelect: ProductSchema[] = [];
|
let productsToSelect: ProductSchema[] = [];
|
||||||
const [selectedProduct, setSelectedProduct] = useState<ProductSchema>();
|
const [selectedProduct, setSelectedProduct] = useState<ProductSchema>();
|
||||||
|
|
||||||
@@ -31,38 +30,8 @@ const ScanBarcode = ({
|
|||||||
setIsScanning(!isScanning);
|
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 = () => {
|
const renderScanningResults = () => {
|
||||||
productsToSelect = barcodesProducts.get(scannedValue) ?? [];
|
productsToSelect = barcodesProductsMap.get(scannedValue) ?? [];
|
||||||
if (productsToSelect?.length === 0) {
|
if (productsToSelect?.length === 0) {
|
||||||
notifications.error({ message: `Товара с штрихкодом ${scannedValue} не найдено` });
|
notifications.error({ message: `Товара с штрихкодом ${scannedValue} не найдено` });
|
||||||
onProductSelect();
|
onProductSelect();
|
||||||
@@ -72,7 +41,7 @@ const ScanBarcode = ({
|
|||||||
onProductSelect(productsToSelect[0]);
|
onProductSelect(productsToSelect[0]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const product = findProductInObject(productsToSelect);
|
const product = findProductInObject(object, productsToSelect);
|
||||||
if (product) {
|
if (product) {
|
||||||
onProductSelect(product);
|
onProductSelect(product);
|
||||||
return;
|
return;
|
||||||
|
|||||||
42
src/pages/ReceiptPage/hooks/useBarcodesProductsMap.tsx
Normal file
42
src/pages/ReceiptPage/hooks/useBarcodesProductsMap.tsx
Normal 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;
|
||||||
63
src/pages/ReceiptPage/hooks/useScanningMode.tsx
Normal file
63
src/pages/ReceiptPage/hooks/useScanningMode.tsx
Normal 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;
|
||||||
61
src/pages/ReceiptPage/modals/SelectScannedProductModal.tsx
Normal file
61
src/pages/ReceiptPage/modals/SelectScannedProductModal.tsx
Normal 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;
|
||||||
Reference in New Issue
Block a user