feat: residues accounting
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
import { Accordion, ActionIcon, Center } from "@mantine/core";
|
||||
import { IconBox, IconPlus, IconTrash } from "@tabler/icons-react";
|
||||
import { ReceiptBox, ReceiptPallet } from "../types/types.tsx";
|
||||
import { useReceiptContext } from "../contexts/ReceiptContext.tsx";
|
||||
import ReceiptProducts from "./ReceiptProducts.tsx";
|
||||
|
||||
type Props = {
|
||||
pallet?: ReceiptPallet;
|
||||
palletIdx?: number;
|
||||
}
|
||||
|
||||
const AccordionBoxes = ({ pallet, palletIdx }: Props) => {
|
||||
const { boxes, boxesHandlers, onObjectEditClick, palletsHandlers, setBoxData } = useReceiptContext();
|
||||
|
||||
const deleteBox = (boxId: number) => {
|
||||
if (palletIdx && pallet) {
|
||||
palletsHandlers.setItemProp(palletIdx, "boxes", pallet.boxes.filter(box => box.id !== boxId));
|
||||
} else {
|
||||
boxesHandlers.filter(item => item.id !== boxId);
|
||||
}
|
||||
};
|
||||
|
||||
const boxActions = (box: ReceiptBox) => {
|
||||
return (
|
||||
<>
|
||||
<ActionIcon
|
||||
variant={"default"}
|
||||
onClick={() => onObjectEditClick(box, true, pallet)}
|
||||
mr={"sm"}
|
||||
>
|
||||
<IconPlus />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
variant={"default"}
|
||||
onClick={() => deleteBox(box.id)}
|
||||
mr={"sm"}
|
||||
>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const boxesData = pallet ? pallet.boxes : boxes;
|
||||
|
||||
if (boxesData.length === 0) return;
|
||||
|
||||
return (
|
||||
<Accordion multiple={true} bd={"solid 1px gray"}>
|
||||
{boxesData.map(box => (
|
||||
<Accordion.Item key={box.id} value={box.id.toString()}>
|
||||
<Center>
|
||||
<Accordion.Control icon={<IconBox />}>
|
||||
Короб {box.id}
|
||||
</Accordion.Control>
|
||||
{boxActions(box)}
|
||||
</Center>
|
||||
<Accordion.Panel>
|
||||
<ReceiptProducts
|
||||
products={box.residualProducts}
|
||||
object={box}
|
||||
setObjectData={(box: ReceiptBox) => setBoxData(box, pallet)}
|
||||
/>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
))}
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccordionBoxes;
|
||||
@@ -0,0 +1,117 @@
|
||||
import { Accordion, ActionIcon, Box, Button, Center, Flex, Group } from "@mantine/core";
|
||||
import { 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 deletePallet = (palletId: number) => {
|
||||
palletsHandlers.filter(item => item.id !== palletId);
|
||||
};
|
||||
|
||||
const createBox = (pallet: ReceiptPallet) => {
|
||||
const box = {
|
||||
id: nextId(),
|
||||
residualProducts: [],
|
||||
} as ReceiptBox;
|
||||
|
||||
const palletIdx = pallets.findIndex(p => p.id === pallet.id);
|
||||
palletsHandlers.setItemProp(palletIdx, "boxes", [box, ...pallets[palletIdx].boxes]);
|
||||
};
|
||||
|
||||
const palletActions = (pallet: ReceiptPallet) => {
|
||||
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>}
|
||||
<ActionIcon
|
||||
variant={"default"}
|
||||
onClick={() => deletePallet(pallet.id)}
|
||||
mr={"md"}
|
||||
>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const palletButton = (label: string, onClick: () => void) => {
|
||||
return (
|
||||
<Button
|
||||
variant={"default"}
|
||||
onClick={onClick}
|
||||
flex={1}
|
||||
>
|
||||
<Group gap={"md"}>
|
||||
<IconPlus />
|
||||
{label}
|
||||
</Group>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const palletButtons = (pallet: ReceiptPallet) => {
|
||||
if (pallet.boxes.length > 0 || pallet.residualProducts.length > 0) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<Flex gap={"md"}>
|
||||
{palletButton("Короб", () => createBox(pallet))}
|
||||
{palletButton("Товар", () => onObjectEditClick(pallet, false))}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
const palletItem = (pallet: ReceiptPallet, palletIdx: number) => {
|
||||
return (
|
||||
<Accordion.Item key={pallet.id} value={pallet.id.toString()}>
|
||||
<Center>
|
||||
<Accordion.Control icon={<IconSpace />}>
|
||||
Паллет {pallet.id}
|
||||
</Accordion.Control>
|
||||
{palletActions(pallet)}
|
||||
</Center>
|
||||
<Accordion.Panel>
|
||||
{palletButtons(pallet)}
|
||||
{pallet.boxes &&
|
||||
<Box pt={"md"}>
|
||||
<AccordionBoxes pallet={pallet} palletIdx={palletIdx} />
|
||||
</Box>
|
||||
}
|
||||
{pallet.residualProducts && (
|
||||
<ReceiptProducts
|
||||
products={pallet.residualProducts}
|
||||
object={pallet}
|
||||
setObjectData={object => setPalletData(object as ReceiptPallet)}
|
||||
/>
|
||||
)}
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
);
|
||||
};
|
||||
|
||||
if (pallets.length === 0) return;
|
||||
|
||||
return (
|
||||
<Accordion multiple={true} bd={"solid 1px gray"}>
|
||||
{pallets.map(((pallet, idx) => palletItem(pallet, idx)))}
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccordionPallets;
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Button, Group } from "@mantine/core";
|
||||
import { IconCheck } from "@tabler/icons-react";
|
||||
import { useReceiptContext } from "../contexts/ReceiptContext.tsx";
|
||||
import { type ProductAndQuantitySchema, ReceiptBoxSchema, ReceiptPalletSchema, ResiduesService } from "../../../../../client";
|
||||
import { ReceiptBox, ReceiptPallet, ReceiptProduct } from "../types/types.tsx";
|
||||
import { notifications } from "../../../../../shared/lib/notifications.ts";
|
||||
|
||||
const FinishReceiptButton = () => {
|
||||
const { pallets, boxes, client, reset } = useReceiptContext();
|
||||
const finishReceiptDisabled = pallets.length === 0 && boxes.length === 0;
|
||||
|
||||
const processProducts = (products: ReceiptProduct[]): ProductAndQuantitySchema[] => {
|
||||
return products.map(p => ({
|
||||
productId: p.product.id,
|
||||
quantity: p.quantity,
|
||||
}));
|
||||
};
|
||||
|
||||
const processBoxes = (boxes: ReceiptBox[]): ReceiptBoxSchema[] => {
|
||||
return boxes.map(box => ({
|
||||
products: processProducts(box.residualProducts),
|
||||
}));
|
||||
};
|
||||
|
||||
const processPallets = (pallets: ReceiptPallet[]): ReceiptPalletSchema[] => {
|
||||
return pallets.map(pallet => ({
|
||||
products: processProducts(pallet.residualProducts),
|
||||
boxes: processBoxes(pallet.boxes),
|
||||
}));
|
||||
};
|
||||
|
||||
const finishReceipt = () => {
|
||||
if (!client) return;
|
||||
|
||||
ResiduesService.receipt({
|
||||
requestBody: {
|
||||
clientId: client.id,
|
||||
pallets: processPallets(pallets),
|
||||
boxes: processBoxes(boxes),
|
||||
},
|
||||
})
|
||||
.then(({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (!ok) return;
|
||||
reset();
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={finishReceipt}
|
||||
disabled={finishReceiptDisabled}
|
||||
>
|
||||
<Group gap={"sm"}>
|
||||
<IconCheck />
|
||||
Завершить приемку
|
||||
</Group>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default FinishReceiptButton;
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Flex, Stack } from "@mantine/core";
|
||||
import { useReceiptContext } from "../contexts/ReceiptContext.tsx";
|
||||
import { ReceiptBox } from "../types/types.tsx";
|
||||
import AccordionBoxes from "./AccordionBoxes.tsx";
|
||||
import AccordionPallets from "./AccordionPallets.tsx";
|
||||
import InlineButton from "../../../../../components/InlineButton/InlineButton.tsx";
|
||||
import { IconPlus } from "@tabler/icons-react";
|
||||
|
||||
const ReceiptEditor = () => {
|
||||
const { fixed, palletsHandlers, boxesHandlers, nextId } = useReceiptContext();
|
||||
|
||||
if (!fixed) return;
|
||||
|
||||
const createPallet = () => {
|
||||
const pallet = {
|
||||
id: nextId(),
|
||||
boxes: [],
|
||||
residualProducts: [],
|
||||
};
|
||||
palletsHandlers.prepend(pallet);
|
||||
};
|
||||
|
||||
const createBox = () => {
|
||||
const box = {
|
||||
id: nextId(),
|
||||
residualProducts: [],
|
||||
} as ReceiptBox;
|
||||
boxesHandlers.prepend(box);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Flex gap="md">
|
||||
<InlineButton onClick={() => createPallet()} flex={1}>
|
||||
<IconPlus />
|
||||
Паллет
|
||||
</InlineButton>
|
||||
<InlineButton onClick={() => createBox()} flex={1}>
|
||||
<IconPlus />
|
||||
Короб
|
||||
</InlineButton>
|
||||
</Flex>
|
||||
<AccordionBoxes />
|
||||
<AccordionPallets />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReceiptEditor;
|
||||
@@ -0,0 +1,63 @@
|
||||
import { ReceiptBox, ReceiptPallet, ReceiptProduct } from "../types/types.tsx";
|
||||
import { ActionIcon, Divider, Group, NumberInput, rem, Stack, Text } from "@mantine/core";
|
||||
import { IconTrash } from "@tabler/icons-react";
|
||||
|
||||
type Props = {
|
||||
products: ReceiptProduct[];
|
||||
object: ReceiptBox | ReceiptPallet;
|
||||
setObjectData: (object: ReceiptBox | ReceiptPallet) => void;
|
||||
}
|
||||
|
||||
const ReceiptProducts = ({ products, object, setObjectData }: Props) => {
|
||||
if (products.length === 0) return;
|
||||
|
||||
const deleteProduct = (productId: number) => {
|
||||
object.residualProducts = object.residualProducts.filter(product => product.id !== productId);
|
||||
setObjectData({ ...object });
|
||||
};
|
||||
|
||||
const productQuantityInput = (receiptProduct: ReceiptProduct) => {
|
||||
return (
|
||||
<Group>
|
||||
<Text>Количество:</Text>
|
||||
<NumberInput
|
||||
value={receiptProduct.quantity}
|
||||
onChange={value => {
|
||||
const idx = object.residualProducts.findIndex(p => p.id === receiptProduct.id);
|
||||
object.residualProducts[idx].quantity = Number(value);
|
||||
setObjectData({ ...object });
|
||||
}}
|
||||
min={1}
|
||||
allowDecimal={false}
|
||||
allowNegative={false}
|
||||
w={rem(100)}
|
||||
/>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
{products.map((receiptProduct: ReceiptProduct) => (
|
||||
<Stack key={receiptProduct.id}>
|
||||
<Divider />
|
||||
<Group wrap={"nowrap"} justify={"space-between"}>
|
||||
<Stack gap={"sm"} key={`Product-${receiptProduct.id}`}>
|
||||
<Text>Товар: {receiptProduct.product.name}</Text>
|
||||
{productQuantityInput(receiptProduct)}
|
||||
{receiptProduct.product.size && <Text>Размер: {receiptProduct.product.size}</Text>}
|
||||
</Stack>
|
||||
<ActionIcon
|
||||
variant={"default"}
|
||||
onClick={() => deleteProduct(receiptProduct.id)}
|
||||
>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReceiptProducts;
|
||||
@@ -0,0 +1,88 @@
|
||||
import { useReceiptContext } from "../contexts/ReceiptContext.tsx";
|
||||
import ClientSelectNew from "../../../../../components/Selects/ClientSelectNew/ClientSelectNew.tsx";
|
||||
import { Button, Flex, Group, Stack, Text } from "@mantine/core";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { IconArrowLeft, IconX } from "@tabler/icons-react";
|
||||
import FinishReceiptButton from "./FinishReceiptButton.tsx";
|
||||
|
||||
|
||||
const ReceiptUserSelect = () => {
|
||||
const {
|
||||
client,
|
||||
setClient,
|
||||
fixed,
|
||||
setFixed,
|
||||
palletsHandlers,
|
||||
boxesHandlers,
|
||||
} = useReceiptContext();
|
||||
|
||||
const confirmModalText = (
|
||||
<Stack>
|
||||
<Text size="sm">Вы уверены, что хотите отменить приемку?</Text>
|
||||
<Text size="sm">Все загруженные паллеты, короба и товары удалятся.</Text>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
const unfix = () => {
|
||||
modals.openConfirmModal({
|
||||
title: "Отмена приемки",
|
||||
children: confirmModalText,
|
||||
labels: { confirm: "Да", cancel: "Нет" },
|
||||
confirmProps: { color: "red" },
|
||||
onConfirm: () => {
|
||||
palletsHandlers.setState([]);
|
||||
boxesHandlers.setState([]);
|
||||
setFixed(false);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Flex gap={"md"} flex={8}>
|
||||
<Button
|
||||
variant={"default"}
|
||||
onClick={() => window.location.reload()}
|
||||
flex={1}
|
||||
disabled={fixed}
|
||||
>
|
||||
<IconArrowLeft />
|
||||
</Button>
|
||||
<ClientSelectNew
|
||||
placeholder={"Выберите клиента"}
|
||||
value={client}
|
||||
onChange={setClient}
|
||||
disabled={fixed}
|
||||
flex={7}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
{
|
||||
fixed ? (
|
||||
<>
|
||||
<Button
|
||||
variant={"default"}
|
||||
onClick={unfix}
|
||||
>
|
||||
<Group gap={"sm"}>
|
||||
<IconX />
|
||||
Отменить приемку
|
||||
</Group>
|
||||
</Button>
|
||||
<FinishReceiptButton />
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
variant={"default"}
|
||||
onClick={() => setFixed(true)}
|
||||
disabled={!client}
|
||||
>
|
||||
Выбрать
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReceiptUserSelect;
|
||||
@@ -0,0 +1,138 @@
|
||||
import { createContext, Dispatch, FC, SetStateAction, useContext, useState } from "react";
|
||||
import { ClientSchema } from "../../../../../client";
|
||||
import { ReceiptBox, ReceiptPallet } from "../types/types.tsx";
|
||||
import { modals } from "@mantine/modals";
|
||||
import nextId from "../utils/nextId.ts";
|
||||
import { useListState, UseListStateHandlers } from "@mantine/hooks";
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
const ReceiptContext = createContext<ReceiptContextState | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
const useReceiptContextState = () => {
|
||||
const [client, setClient] = useState<ClientSchema>();
|
||||
const [fixed, setFixed] = useState<boolean>(false);
|
||||
|
||||
const [pallets, palletsHandlers] = useListState<ReceiptPallet>([]);
|
||||
const [boxes, boxesHandlers] = useListState<ReceiptBox>([]);
|
||||
|
||||
const setBoxData = (box: ReceiptBox, pallet?: ReceiptPallet) => {
|
||||
if (pallet) {
|
||||
setBoxOnPalletData(box, pallet);
|
||||
} else {
|
||||
setBoxWithoutPalletData(box);
|
||||
}
|
||||
};
|
||||
|
||||
const setBoxWithoutPalletData = (box: ReceiptBox) => {
|
||||
const boxIdx = boxes.findIndex(item => item.id === box.id);
|
||||
if (boxIdx === -1) {
|
||||
boxesHandlers.append(box);
|
||||
} else {
|
||||
boxesHandlers.setItem(boxIdx, box);
|
||||
}
|
||||
};
|
||||
|
||||
const setBoxOnPalletData = (box: ReceiptBox, pallet: ReceiptPallet) => {
|
||||
const palletIdx = pallets.findIndex(p => p.id === pallet.id);
|
||||
const boxIdx = pallets[palletIdx].boxes.findIndex(b => b.id === box.id);
|
||||
if (boxIdx === -1) {
|
||||
palletsHandlers.setItemProp(palletIdx, "boxes", [box, ...boxes]);
|
||||
} else {
|
||||
pallets[palletIdx].boxes[boxIdx] = box;
|
||||
palletsHandlers.setState(pallets);
|
||||
}
|
||||
palletsHandlers.setState([...pallets]);
|
||||
};
|
||||
|
||||
const setPalletData = (pallet: ReceiptPallet) => {
|
||||
const palletIdx = pallets.findIndex(p => p.id === pallet.id);
|
||||
if (palletIdx !== -1) {
|
||||
palletsHandlers.setItem(palletIdx, pallet);
|
||||
palletsHandlers.setState([...pallets]);
|
||||
}
|
||||
};
|
||||
|
||||
const onObjectEditClick = (
|
||||
object: ReceiptBox | ReceiptPallet,
|
||||
isBox: boolean,
|
||||
parentPallet?: ReceiptPallet,
|
||||
) => {
|
||||
if (!client) return;
|
||||
modals.openContextModal({
|
||||
modal: "newReceiptModal",
|
||||
title: "Добавление товара",
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
clientId: client.id,
|
||||
object: object,
|
||||
setObjectData: (object: ReceiptBox | ReceiptPallet) => {
|
||||
if (isBox) setBoxData(object, parentPallet);
|
||||
setPalletData(object as ReceiptPallet);
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
palletsHandlers.setState([]);
|
||||
boxesHandlers.setState([]);
|
||||
setFixed(false);
|
||||
setClient(undefined);
|
||||
};
|
||||
|
||||
return {
|
||||
client,
|
||||
setClient,
|
||||
fixed,
|
||||
setFixed,
|
||||
pallets,
|
||||
palletsHandlers,
|
||||
boxes,
|
||||
boxesHandlers,
|
||||
nextId,
|
||||
onObjectEditClick,
|
||||
reset,
|
||||
setBoxData,
|
||||
setPalletData,
|
||||
};
|
||||
};
|
||||
|
||||
type ReceiptContextProviderProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const ReceiptContextProvider: FC<ReceiptContextProviderProps> = ({ children }) => {
|
||||
const state = useReceiptContextState();
|
||||
return (
|
||||
<ReceiptContext.Provider value={state}>
|
||||
{children}
|
||||
</ReceiptContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useReceiptContext = () => {
|
||||
const context = useContext(ReceiptContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
"useReceiptContext must be used within a ReceiptContextProvider",
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -0,0 +1,126 @@
|
||||
import { useForm } from "@mantine/form";
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import { Button, Divider, Flex, NumberInput, rem, Stack } from "@mantine/core";
|
||||
import ProductSelect from "../../../../../components/ProductSelect/ProductSelect.tsx";
|
||||
import { ReceiptBox, ReceiptPallet } from "../types/types.tsx";
|
||||
import { ResidualModalForm } from "../../../../ResiduesPage/types/ResidualProductData.tsx";
|
||||
import nextId from "../utils/nextId.ts";
|
||||
import useScanning from "../../../hooks/useScanning.tsx";
|
||||
import ScanBarcode from "../../ScanBarcode/ScanBarcode.tsx";
|
||||
import { ProductSchema } from "../../../../../client";
|
||||
|
||||
|
||||
type Props = {
|
||||
clientId: number;
|
||||
object: ReceiptBox | ReceiptPallet;
|
||||
setObjectData: (object: ReceiptBox | ReceiptPallet) => void;
|
||||
}
|
||||
|
||||
const NewReceiptModal = ({
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const {
|
||||
clientId,
|
||||
setObjectData,
|
||||
object,
|
||||
} = innerProps;
|
||||
const { isScanning, setIsScanning, scannedValue, setScannedValue } = useScanning();
|
||||
|
||||
const initialValues = {
|
||||
product: null,
|
||||
quantity: 1,
|
||||
};
|
||||
const form = useForm<ResidualModalForm>({
|
||||
initialValues,
|
||||
validate: {
|
||||
product: product => !product && "Необходимо выбрать товар",
|
||||
quantity: quantity => (!quantity || quantity === 0) && "Слишком мало товара",
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = () => {
|
||||
const productIdx = object.residualProducts.findIndex(p => p.product.id === form.values.product?.id);
|
||||
|
||||
if (productIdx !== -1) {
|
||||
object.residualProducts[productIdx].quantity += form.values.quantity;
|
||||
} else {
|
||||
const id = nextId();
|
||||
const receiptProduct = {
|
||||
id,
|
||||
product: form.values.product!,
|
||||
quantity: form.values.quantity,
|
||||
};
|
||||
object.residualProducts.unshift(receiptProduct);
|
||||
}
|
||||
|
||||
setObjectData(object);
|
||||
form.reset();
|
||||
context.closeContextModal(id);
|
||||
};
|
||||
|
||||
const onProductAfterScanningSelect = (product?: ProductSchema) => {
|
||||
if (!product) {
|
||||
setScannedValue("");
|
||||
return;
|
||||
}
|
||||
|
||||
const productIdx = object.residualProducts.findIndex(p => p.product.id === product.id);
|
||||
if (productIdx === -1) {
|
||||
form.setFieldValue("product", product);
|
||||
} else {
|
||||
object.residualProducts[productIdx].quantity++;
|
||||
setObjectData({ ...object });
|
||||
context.closeContextModal(id);
|
||||
}
|
||||
setScannedValue("");
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex
|
||||
direction={"column"}
|
||||
gap={rem(10)}
|
||||
>
|
||||
<form onSubmit={form.onSubmit(() => onSubmit())}>
|
||||
<Stack gap={rem(10)}>
|
||||
<Divider />
|
||||
<ProductSelect
|
||||
label={"Товар"}
|
||||
placeholder={"Выберите товар"}
|
||||
{...form.getInputProps("product")}
|
||||
clientId={clientId}
|
||||
disabled={isScanning}
|
||||
/>
|
||||
<NumberInput
|
||||
label={"Количество"}
|
||||
hideControls
|
||||
{...form.getInputProps("quantity")}
|
||||
min={1}
|
||||
defaultValue={1}
|
||||
disabled={isScanning}
|
||||
/>
|
||||
<Button
|
||||
variant={"default"}
|
||||
type={"submit"}
|
||||
disabled={isScanning}
|
||||
>
|
||||
Добавить
|
||||
</Button>
|
||||
<Divider />
|
||||
</Stack>
|
||||
</form>
|
||||
|
||||
<ScanBarcode
|
||||
clientId={clientId}
|
||||
isScanning={isScanning}
|
||||
setIsScanning={setIsScanning}
|
||||
onProductSelect={onProductAfterScanningSelect}
|
||||
scannedValue={scannedValue}
|
||||
object={object}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewReceiptModal;
|
||||
18
src/pages/ReceiptPage/components/NewReceipt/types/types.tsx
Normal file
18
src/pages/ReceiptPage/components/NewReceipt/types/types.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { ProductSchema } from "../../../../../client";
|
||||
|
||||
export type ReceiptProduct = {
|
||||
id: number;
|
||||
quantity: number;
|
||||
product: ProductSchema;
|
||||
}
|
||||
|
||||
export type ReceiptBox = {
|
||||
id: number;
|
||||
residualProducts: ReceiptProduct[];
|
||||
}
|
||||
|
||||
export type ReceiptPallet = {
|
||||
id: number;
|
||||
residualProducts: ReceiptProduct[];
|
||||
boxes: ReceiptBox[];
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
let id = 1;
|
||||
|
||||
export default function nextId() {
|
||||
return id++;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { Accordion, ActionIcon, Center } from "@mantine/core";
|
||||
import { IconBox, IconPlus, IconTrash } from "@tabler/icons-react";
|
||||
import ReceiptProducts from "./ReceiptProducts.tsx";
|
||||
import { ResidualBoxSchema, ResidualPalletSchema, ResiduesService } from "../../../../../client";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { notifications } from "../../../../../shared/lib/notifications.ts";
|
||||
|
||||
type Props = {
|
||||
pallet: ResidualPalletSchema;
|
||||
clientId: number;
|
||||
fetchPallet: () => void;
|
||||
}
|
||||
|
||||
const AccordionBoxesOnPallet = ({ pallet, clientId, fetchPallet }: Props) => {
|
||||
const deleteBox = (boxId: number) => {
|
||||
ResiduesService.deleteResidualBox({
|
||||
boxId,
|
||||
})
|
||||
.then(({ ok, message }) => {
|
||||
if (!ok) {
|
||||
notifications.error({ message });
|
||||
}
|
||||
fetchPallet();
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
|
||||
const onCreateProductClick = (box: ResidualBoxSchema) => {
|
||||
if (!(box && clientId)) return;
|
||||
modals.openContextModal({
|
||||
modal: "receiptModal",
|
||||
title: "Добавление товара",
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
clientId,
|
||||
isBox: true,
|
||||
object: box,
|
||||
fetchObject: fetchPallet,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const boxActions = (box: ResidualBoxSchema) => {
|
||||
return (
|
||||
<>
|
||||
<ActionIcon
|
||||
variant={"default"}
|
||||
onClick={() => onCreateProductClick(box)}
|
||||
mr={"sm"}
|
||||
>
|
||||
<IconPlus />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
variant={"default"}
|
||||
onClick={() => deleteBox(box.id)}
|
||||
mr={"sm"}
|
||||
>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
if (pallet.boxes.length === 0) return;
|
||||
|
||||
return (
|
||||
<Accordion multiple={true} bd={"solid 1px gray"}>
|
||||
{pallet.boxes.map(box => (
|
||||
<Accordion.Item key={box.id} value={box.id.toString()}>
|
||||
<Center>
|
||||
<Accordion.Control icon={<IconBox />}>
|
||||
Короб {box.id}
|
||||
</Accordion.Control>
|
||||
{boxActions(box)}
|
||||
</Center>
|
||||
<Accordion.Panel>
|
||||
<ReceiptProducts
|
||||
clientId={clientId}
|
||||
object={box}
|
||||
updateObject={fetchPallet}
|
||||
/>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
))}
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccordionBoxesOnPallet;
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Group, NumberInput, rem, Text } from "@mantine/core";
|
||||
import { ResidualProductSchema, ResiduesService } from "../../../../../client";
|
||||
import { notifications } from "../../../../../shared/lib/notifications.ts";
|
||||
|
||||
type Props = {
|
||||
residualProduct: ResidualProductSchema;
|
||||
updateObject: () => void;
|
||||
}
|
||||
|
||||
const ProductQuantityField = ({ residualProduct, updateObject }: Props) => {
|
||||
const updateProduct = (quantity: number) => {
|
||||
if (residualProduct.quantity === quantity) return;
|
||||
ResiduesService.updateResidualProduct({
|
||||
requestBody: {
|
||||
data: {
|
||||
quantity,
|
||||
productId: residualProduct.product.id,
|
||||
},
|
||||
},
|
||||
residualProductId: residualProduct.id,
|
||||
})
|
||||
.then(({ ok, message }) => {
|
||||
if (!ok) {
|
||||
notifications.error({ message });
|
||||
}
|
||||
updateObject();
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
|
||||
return (
|
||||
<Group>
|
||||
<Text>Количество: </Text>
|
||||
<NumberInput
|
||||
value={residualProduct.quantity}
|
||||
onChange={(value) => updateProduct(Number(value))}
|
||||
min={1}
|
||||
allowDecimal={false}
|
||||
allowNegative={false}
|
||||
w={rem(100)}
|
||||
/>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductQuantityField;
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Button, Flex, Stack, 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";
|
||||
|
||||
type Props = {
|
||||
boxId: number;
|
||||
}
|
||||
|
||||
const ReceiptBoxEditor = ({ boxId }: Props) => {
|
||||
const {
|
||||
box,
|
||||
fetchBox,
|
||||
clientId,
|
||||
onCreateProductClick,
|
||||
} = useReceiptBox({ boxId });
|
||||
|
||||
const backButton = (
|
||||
<Button
|
||||
variant={"default"}
|
||||
onClick={() => window.location.reload()}
|
||||
flex={1}
|
||||
>
|
||||
<IconArrowLeft />
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Flex align={"center"} flex={9} gap={"md"}>
|
||||
{backButton}
|
||||
<Title flex={8} order={3}>
|
||||
Короб ID: К{box?.id}
|
||||
</Title>
|
||||
</Flex>
|
||||
<InlineButton onClick={onCreateProductClick}>
|
||||
<IconPlus />
|
||||
Товар
|
||||
</InlineButton>
|
||||
<ReceiptProducts
|
||||
object={box}
|
||||
clientId={clientId}
|
||||
updateObject={fetchBox}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReceiptBoxEditor;
|
||||
@@ -0,0 +1,92 @@
|
||||
import useReceiptPallet from "../hooks/useReceiptPallet.tsx";
|
||||
import { Button, Flex, Group, Stack, Text, Title } from "@mantine/core";
|
||||
import { IconArrowLeft, IconPlus } from "@tabler/icons-react";
|
||||
import InlineButton from "../../../../../components/InlineButton/InlineButton.tsx";
|
||||
import ReceiptProducts from "./ReceiptProducts.tsx";
|
||||
import AccordionBoxesOnPallet from "./AccordionBoxes.tsx";
|
||||
|
||||
type Props = {
|
||||
palletId: number;
|
||||
}
|
||||
|
||||
const ReceiptPalletEditor = ({ palletId }: Props) => {
|
||||
const {
|
||||
pallet,
|
||||
fetchPallet,
|
||||
clientId,
|
||||
onCreateProductClick,
|
||||
onCreateBoxClick,
|
||||
} = useReceiptPallet({ palletId });
|
||||
|
||||
if (!pallet) return <Text>Паллет c ID П{palletId} не найден</Text>;
|
||||
|
||||
const createButtons = () => {
|
||||
const isBoxes = pallet.boxes.length > 0;
|
||||
const isProducts = pallet.residualProducts.length > 0;
|
||||
const isBoth = !(isBoxes || isProducts);
|
||||
return (
|
||||
<Group>
|
||||
{(isBoxes || isBoth) && (
|
||||
<InlineButton onClick={onCreateBoxClick} flex={1}>
|
||||
<IconPlus />
|
||||
Короб
|
||||
</InlineButton>
|
||||
)}
|
||||
{(isProducts || isBoth) && (
|
||||
<InlineButton onClick={onCreateProductClick} flex={1}>
|
||||
<IconPlus />
|
||||
Товар
|
||||
</InlineButton>
|
||||
)}
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
const renderPalletData = () => {
|
||||
if (!clientId) return;
|
||||
if (pallet?.boxes.length !== 0) {
|
||||
return (
|
||||
<AccordionBoxesOnPallet
|
||||
pallet={pallet}
|
||||
clientId={clientId}
|
||||
fetchPallet={fetchPallet}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (pallet?.residualProducts.length !== 0) {
|
||||
return (
|
||||
<ReceiptProducts
|
||||
clientId={clientId}
|
||||
object={pallet}
|
||||
updateObject={fetchPallet}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const backButton = (
|
||||
<Button
|
||||
variant={"default"}
|
||||
onClick={() => window.location.reload()}
|
||||
flex={1}
|
||||
>
|
||||
<IconArrowLeft />
|
||||
</Button>
|
||||
);
|
||||
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Flex align={"center"} flex={9} gap={"md"}>
|
||||
{backButton}
|
||||
<Title flex={8} order={3}>
|
||||
Паллет ID: П{pallet?.id}
|
||||
</Title>
|
||||
</Flex>
|
||||
{createButtons()}
|
||||
{renderPalletData()}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReceiptPalletEditor;
|
||||
@@ -0,0 +1,64 @@
|
||||
import { ActionIcon, Divider, Flex, Group, rem, Stack, Text } from "@mantine/core";
|
||||
import { ResidualBoxSchema, ResidualPalletSchema, ResidualProductSchema, ResiduesService } from "../../../../../client";
|
||||
import { IconTrash } from "@tabler/icons-react";
|
||||
import { notifications } from "../../../../../shared/lib/notifications.ts";
|
||||
import ProductQuantityField from "./ProductQuantityField.tsx";
|
||||
|
||||
|
||||
type Props = {
|
||||
clientId: number | null;
|
||||
object: ResidualBoxSchema | ResidualPalletSchema | null;
|
||||
updateObject: () => void;
|
||||
}
|
||||
|
||||
const ReceiptProducts = ({ clientId, object, updateObject }: Props) => {
|
||||
if (!object || !clientId) return;
|
||||
|
||||
const deleteProduct = (residualProductId: number) => {
|
||||
ResiduesService.deleteResidualProduct({
|
||||
residualProductId,
|
||||
})
|
||||
.then(({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
updateObject();
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
|
||||
const renderProducts = () => {
|
||||
return object.residualProducts
|
||||
.sort((a, b) => a.id - b.id)
|
||||
.map((residualProduct: ResidualProductSchema) => (
|
||||
<Stack key={residualProduct.id} gap={"sm"}>
|
||||
<Divider />
|
||||
<Group wrap={"nowrap"} justify={"space-between"}>
|
||||
<Stack>
|
||||
<Text>Товар: {residualProduct.product.name}</Text>
|
||||
<ProductQuantityField
|
||||
residualProduct={residualProduct}
|
||||
updateObject={updateObject}
|
||||
/>
|
||||
</Stack>
|
||||
<ActionIcon
|
||||
variant={"default"}
|
||||
onClick={() => deleteProduct(residualProduct.id)}
|
||||
>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Stack>
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex
|
||||
direction={"column"}
|
||||
gap={rem(10)}
|
||||
>
|
||||
{renderProducts()}
|
||||
<Divider />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReceiptProducts;
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Button, Center, Stack, Text } from "@mantine/core";
|
||||
import useScanning from "../../../hooks/useScanning.tsx";
|
||||
import { useEffect } from "react";
|
||||
import { notifications } from "../../../../../shared/lib/notifications.ts";
|
||||
import ReceiptPalletEditor from "./ReceiptPalletEditor.tsx";
|
||||
import ReceiptBoxEditor from "./ReceiptBoxEditor.tsx";
|
||||
import { IconArrowLeft } from "@tabler/icons-react";
|
||||
|
||||
const ReceiptQrCodeScan = () => {
|
||||
const {
|
||||
setIsScanning,
|
||||
scannedValue,
|
||||
setScannedValue,
|
||||
} = useScanning();
|
||||
|
||||
useEffect(() => {
|
||||
setIsScanning(true);
|
||||
}, []);
|
||||
|
||||
const getId = () => Number.parseInt(scannedValue.substring(1));
|
||||
|
||||
const checkQrScannedCorrectly = () => {
|
||||
if (
|
||||
scannedValue.length < 2
|
||||
|| (scannedValue[0] !== "П" && scannedValue[0] !== "К")
|
||||
|| Number.isNaN(getId())
|
||||
) {
|
||||
notifications.error({ message: `Считанный ID ${scannedValue} некорректный` });
|
||||
setScannedValue("");
|
||||
setIsScanning(true);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (scannedValue) {
|
||||
checkQrScannedCorrectly();
|
||||
}
|
||||
}, [scannedValue]);
|
||||
|
||||
if (!scannedValue) {
|
||||
return (
|
||||
<Stack>
|
||||
<Button
|
||||
variant={"default"}
|
||||
onClick={() => window.location.reload()}
|
||||
>
|
||||
<IconArrowLeft />
|
||||
</Button>
|
||||
<Center>
|
||||
<Text>
|
||||
Отсканируйте QR код
|
||||
</Text>
|
||||
</Center>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
if (scannedValue.length > 0 && scannedValue[0] === "К") {
|
||||
return <ReceiptBoxEditor boxId={getId()} />;
|
||||
}
|
||||
return <ReceiptPalletEditor palletId={getId()} />;
|
||||
};
|
||||
|
||||
export default ReceiptQrCodeScan;
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { ResidualBoxSchema, ResiduesService } from "../../../../../client";
|
||||
import { modals } from "@mantine/modals";
|
||||
|
||||
|
||||
type Props = {
|
||||
boxId: number;
|
||||
}
|
||||
|
||||
const useReceiptBox = ({ boxId }: Props) => {
|
||||
const [box, setBox] = useState<ResidualBoxSchema | null>(null);
|
||||
const [clientId, setClientId] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchBox(boxId);
|
||||
}, []);
|
||||
|
||||
const fetchBox = (boxId?: number) => {
|
||||
const id = boxId ?? box?.id;
|
||||
if (!id) return;
|
||||
|
||||
ResiduesService.getResidualBox({ boxId: id })
|
||||
.then(res => {
|
||||
setBox(res.box);
|
||||
setClientId(res.clientId);
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
|
||||
const onCreateProductClick = () => {
|
||||
if (!(box && clientId)) return;
|
||||
modals.openContextModal({
|
||||
modal: "receiptModal",
|
||||
title: "Добавление товара",
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
clientId,
|
||||
isBox: true,
|
||||
object: box,
|
||||
fetchObject: fetchBox,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
box,
|
||||
fetchBox,
|
||||
clientId,
|
||||
onCreateProductClick,
|
||||
};
|
||||
};
|
||||
|
||||
export default useReceiptBox;
|
||||
@@ -0,0 +1,70 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { ResidualPalletSchema, ResiduesService } from "../../../../../client";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { notifications } from "../../../../../shared/lib/notifications.ts";
|
||||
|
||||
type Props = {
|
||||
palletId: number;
|
||||
}
|
||||
|
||||
const useReceiptPallet = ({ palletId }: Props) => {
|
||||
const [pallet, setPallet] = useState<ResidualPalletSchema | null>(null);
|
||||
const [clientId, setClientId] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPallet(palletId);
|
||||
}, []);
|
||||
|
||||
const fetchPallet = (palletId?: number) => {
|
||||
const id = palletId ?? pallet?.id;
|
||||
if (!id) return;
|
||||
|
||||
ResiduesService.getResidualPallet({ palletId: id })
|
||||
.then(res => {
|
||||
setPallet(res.pallet);
|
||||
setClientId(res.clientId);
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
|
||||
const onCreateProductClick = () => {
|
||||
if (!(pallet && clientId)) return;
|
||||
modals.openContextModal({
|
||||
modal: "receiptModal",
|
||||
title: "Добавление товара",
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
clientId,
|
||||
isBox: false,
|
||||
object: pallet,
|
||||
fetchObject: fetchPallet,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onCreateBoxClick = () => {
|
||||
ResiduesService.createResidualBox({
|
||||
requestBody: {
|
||||
palletId,
|
||||
clientId: null,
|
||||
},
|
||||
})
|
||||
.then(({ ok, message }) => {
|
||||
if (!ok) {
|
||||
notifications.error({ message });
|
||||
}
|
||||
fetchPallet();
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
|
||||
return {
|
||||
pallet,
|
||||
fetchPallet,
|
||||
clientId,
|
||||
onCreateProductClick,
|
||||
onCreateBoxClick,
|
||||
};
|
||||
};
|
||||
|
||||
export default useReceiptPallet;
|
||||
@@ -0,0 +1,129 @@
|
||||
import { useForm } from "@mantine/form";
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import { Button, Divider, Flex, NumberInput, rem, Stack } from "@mantine/core";
|
||||
import ProductSelect from "../../../../../components/ProductSelect/ProductSelect.tsx";
|
||||
import useScanning from "../../../hooks/useScanning.tsx";
|
||||
import ScanBarcode from "../../ScanBarcode/ScanBarcode.tsx";
|
||||
import { ProductSchema, ResidualBoxSchema, ResidualPalletSchema, ResiduesService } from "../../../../../client";
|
||||
import { notifications } from "../../../../../shared/lib/notifications.ts";
|
||||
import { ResidualModalForm } from "../../../../ResiduesPage/types/ResidualProductData.tsx";
|
||||
|
||||
|
||||
type Props = {
|
||||
clientId: number;
|
||||
object: ResidualBoxSchema | ResidualPalletSchema;
|
||||
fetchObject: () => void;
|
||||
isBox: boolean;
|
||||
}
|
||||
|
||||
const ReceiptModal = ({
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const {
|
||||
clientId,
|
||||
object,
|
||||
fetchObject,
|
||||
} = innerProps;
|
||||
const { isScanning, setIsScanning, scannedValue, setScannedValue } = useScanning();
|
||||
const initialValues = {
|
||||
product: null,
|
||||
quantity: 1,
|
||||
};
|
||||
const form = useForm<ResidualModalForm>({
|
||||
initialValues,
|
||||
validate: {
|
||||
product: product => !product && "Необходимо выбрать товар",
|
||||
quantity: quantity => (!quantity || quantity === 0) && "Слишком мало товара",
|
||||
},
|
||||
});
|
||||
|
||||
if (!object || !clientId) return;
|
||||
|
||||
const postProduct = (product: ProductSchema, quantity: number) => {
|
||||
ResiduesService.createResidualProduct({
|
||||
requestBody: {
|
||||
data: {
|
||||
productId: product.id,
|
||||
quantity,
|
||||
palletId: innerProps.isBox ? null : object.id,
|
||||
boxId: innerProps.isBox ? object.id : null,
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
fetchObject();
|
||||
if (ok) context.closeContextModal(id);
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
|
||||
const onProductAfterScanningSelect = (product?: ProductSchema) => {
|
||||
if (!product) {
|
||||
setScannedValue("");
|
||||
return;
|
||||
}
|
||||
|
||||
const productIdx = object.residualProducts.findIndex(p => p.product.id === product.id);
|
||||
if (productIdx === -1) {
|
||||
form.setFieldValue("product", product);
|
||||
} else {
|
||||
postProduct(product, 1);
|
||||
}
|
||||
setScannedValue("");
|
||||
};
|
||||
|
||||
const onSubmit = () => {
|
||||
postProduct(form.values.product!, form.values.quantity!);
|
||||
form.reset();
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex
|
||||
direction={"column"}
|
||||
gap={rem(10)}
|
||||
>
|
||||
<form onSubmit={form.onSubmit(() => onSubmit())}>
|
||||
<Stack gap={rem(10)}>
|
||||
<Divider />
|
||||
<ProductSelect
|
||||
label={"Товар"}
|
||||
placeholder={"Выберите товар"}
|
||||
{...form.getInputProps("product")}
|
||||
clientId={clientId}
|
||||
disabled={isScanning}
|
||||
/>
|
||||
<NumberInput
|
||||
label={"Количество"}
|
||||
hideControls
|
||||
{...form.getInputProps("quantity")}
|
||||
min={1}
|
||||
defaultValue={1}
|
||||
disabled={isScanning}
|
||||
/>
|
||||
<Button
|
||||
variant={"default"}
|
||||
type={"submit"}
|
||||
disabled={isScanning}
|
||||
>
|
||||
Добавить
|
||||
</Button>
|
||||
<Divider />
|
||||
</Stack>
|
||||
</form>
|
||||
|
||||
<ScanBarcode
|
||||
clientId={clientId}
|
||||
isScanning={isScanning}
|
||||
setIsScanning={setIsScanning}
|
||||
onProductSelect={onProductAfterScanningSelect}
|
||||
scannedValue={scannedValue}
|
||||
object={object}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReceiptModal;
|
||||
135
src/pages/ReceiptPage/components/ScanBarcode/ScanBarcode.tsx
Normal file
135
src/pages/ReceiptPage/components/ScanBarcode/ScanBarcode.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import { ProductSchema, ResidualBoxSchema, ResidualPalletSchema } from "../../../../client";
|
||||
import { useEffect, useState } from "react";
|
||||
import useProductsList from "../../../ProductsPage/hooks/useProductsList.tsx";
|
||||
import { Button, Group, Radio, Stack, Text } from "@mantine/core";
|
||||
import { notifications } from "../../../../shared/lib/notifications.ts";
|
||||
import { ReceiptBox, ReceiptPallet } from "../NewReceipt/types/types.tsx";
|
||||
|
||||
type Props = {
|
||||
onProductSelect: (product?: ProductSchema) => void;
|
||||
clientId: number;
|
||||
isScanning: boolean;
|
||||
setIsScanning: (isScanning: boolean) => void;
|
||||
scannedValue: string;
|
||||
object: ReceiptBox | ReceiptPallet | ResidualPalletSchema | ResidualBoxSchema;
|
||||
}
|
||||
|
||||
const ScanBarcode = ({
|
||||
onProductSelect,
|
||||
clientId,
|
||||
isScanning,
|
||||
setIsScanning,
|
||||
scannedValue,
|
||||
object,
|
||||
}: Props) => {
|
||||
const productsData = useProductsList({ clientId, searchInput: "" });
|
||||
const [barcodesProducts, setBarcodesProducts] = useState(new Map<string, ProductSchema[]>());
|
||||
let productsToSelect: ProductSchema[] = [];
|
||||
const [selectedProduct, setSelectedProduct] = useState<ProductSchema>();
|
||||
|
||||
const toggleScanning = () => {
|
||||
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) ?? [];
|
||||
if (productsToSelect?.length === 0) {
|
||||
notifications.error({ message: `Товара с штрихкодом ${scannedValue} не найдено` });
|
||||
onProductSelect();
|
||||
return;
|
||||
}
|
||||
if (productsToSelect?.length === 1) {
|
||||
onProductSelect(productsToSelect[0]);
|
||||
return;
|
||||
}
|
||||
const product = findProductInObject(productsToSelect);
|
||||
if (product) {
|
||||
onProductSelect(product);
|
||||
return;
|
||||
}
|
||||
return renderProductSelect();
|
||||
};
|
||||
|
||||
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>
|
||||
));
|
||||
};
|
||||
|
||||
const renderProductSelect = () => {
|
||||
return (
|
||||
<Stack>
|
||||
<Text>Выберите товар для данного штрихкода</Text>
|
||||
{renderProductsToSelect()}
|
||||
<Button
|
||||
variant={"default"}
|
||||
onClick={() => {
|
||||
if (!selectedProduct) {
|
||||
notifications.error({ message: "Товар не выбран" });
|
||||
} else {
|
||||
onProductSelect(selectedProduct);
|
||||
setSelectedProduct(undefined);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Выбрать
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
return (scannedValue) ? (
|
||||
renderScanningResults()
|
||||
) : (
|
||||
<Stack gap={0}>
|
||||
<Button
|
||||
variant={"default"}
|
||||
type={"button"}
|
||||
onClick={toggleScanning}
|
||||
>
|
||||
{isScanning ? "Отменить сканирование" : "Отсканировать штрихкод"}
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScanBarcode;
|
||||
34
src/pages/ReceiptPage/hooks/useScanning.tsx
Normal file
34
src/pages/ReceiptPage/hooks/useScanning.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { useState } from "react";
|
||||
import { useWindowEvent } from "@mantine/hooks";
|
||||
|
||||
const useScanning = () => {
|
||||
const [scanningValue, setScanningValue] = useState<string>("");
|
||||
const [scannedValue, setScannedValue] = useState<string>("");
|
||||
const [isScanning, setIsScanning] = useState<boolean>(false);
|
||||
|
||||
const setIsScanningValue = (isScanning: boolean) => {
|
||||
if (!isScanning) {
|
||||
setScanningValue("");
|
||||
}
|
||||
setIsScanning(isScanning);
|
||||
};
|
||||
|
||||
useWindowEvent("keydown", (event) => {
|
||||
if (!isScanning) return;
|
||||
event.preventDefault();
|
||||
setScanningValue(prevState => prevState + event.key);
|
||||
if (["\n", "\r", "Enter"].includes(event.key)) {
|
||||
setScannedValue(scanningValue);
|
||||
setIsScanningValue(false);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
isScanning,
|
||||
setIsScanning: setIsScanningValue,
|
||||
scannedValue,
|
||||
setScannedValue,
|
||||
};
|
||||
};
|
||||
|
||||
export default useScanning;
|
||||
0
src/pages/ReceiptPage/index.tsx
Normal file
0
src/pages/ReceiptPage/index.tsx
Normal file
7
src/pages/ReceiptPage/ui/ReceiptPage.module.css
Normal file
7
src/pages/ReceiptPage/ui/ReceiptPage.module.css
Normal file
@@ -0,0 +1,7 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
gap: rem(10);
|
||||
}
|
||||
|
||||
61
src/pages/ReceiptPage/ui/ReceiptPage.tsx
Normal file
61
src/pages/ReceiptPage/ui/ReceiptPage.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { ReceiptContextProvider } from "../components/NewReceipt/contexts/ReceiptContext.tsx";
|
||||
import ReceiptUserSelect from "../components/NewReceipt/components/ReceiptUserSelect.tsx";
|
||||
import ReceiptEditor from "../components/NewReceipt/components/ReceiptEditor.tsx";
|
||||
import { Button, Stack } from "@mantine/core";
|
||||
import { useState } from "react";
|
||||
import ReceiptQrCodeScan from "../components/ReceiptEditing/components/ReceiptQrCodeScan.tsx";
|
||||
import PageBlock from "../../../components/PageBlock/PageBlock.tsx";
|
||||
|
||||
enum ReceiptType {
|
||||
NEW_RECEIPT,
|
||||
RECEIPT_EDITING,
|
||||
UNDEFINED,
|
||||
}
|
||||
|
||||
const ReceiptPage = () => {
|
||||
const [receiptType, setReceiptType] = useState<ReceiptType>(ReceiptType.UNDEFINED);
|
||||
|
||||
const getContent = () => {
|
||||
if (receiptType === ReceiptType.UNDEFINED) {
|
||||
return (
|
||||
<Stack h="95vh">
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => setReceiptType(ReceiptType.NEW_RECEIPT)}
|
||||
h="100%"
|
||||
>
|
||||
Начать приемку
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => setReceiptType(ReceiptType.RECEIPT_EDITING)}
|
||||
h="100%"
|
||||
>
|
||||
Редактировать паллет/короб
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
if (receiptType === ReceiptType.NEW_RECEIPT) {
|
||||
return (
|
||||
<ReceiptContextProvider>
|
||||
<Stack>
|
||||
<ReceiptUserSelect />
|
||||
<ReceiptEditor />
|
||||
</Stack>
|
||||
</ReceiptContextProvider>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ReceiptQrCodeScan />
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<PageBlock noBorderRadius style={{ minHeight: "100vh" }}>
|
||||
{getContent()}
|
||||
</PageBlock>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReceiptPage;
|
||||
Reference in New Issue
Block a user