feat: products and services on same page
This commit is contained in:
		@@ -9,6 +9,7 @@ import PrintBarcodeModal from "./PrintBarcodeModal/PrintBarcodeModal.tsx";
 | 
				
			|||||||
import AddBarcodeModal from "./AddBarcodeModal/AddBarcodeModal.tsx";
 | 
					import AddBarcodeModal from "./AddBarcodeModal/AddBarcodeModal.tsx";
 | 
				
			||||||
import BarcodeTemplateFormModal
 | 
					import BarcodeTemplateFormModal
 | 
				
			||||||
    from "../pages/BarcodePage/modals/BarcodeTemplateFormModal/BarcodeTemplateFormModal.tsx";
 | 
					    from "../pages/BarcodePage/modals/BarcodeTemplateFormModal/BarcodeTemplateFormModal.tsx";
 | 
				
			||||||
 | 
					import ProductServiceFormModal from "../pages/LeadsPage/modals/ProductServiceFormModal.tsx";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const modals = {
 | 
					export const modals = {
 | 
				
			||||||
    enterDeadline: EnterDeadlineModal,
 | 
					    enterDeadline: EnterDeadlineModal,
 | 
				
			||||||
@@ -18,6 +19,7 @@ export const modals = {
 | 
				
			|||||||
    productFormModal: ProductFormModal,
 | 
					    productFormModal: ProductFormModal,
 | 
				
			||||||
    addDealService: AddDealServiceModal,
 | 
					    addDealService: AddDealServiceModal,
 | 
				
			||||||
    addDealProduct: AddDealProductModal,
 | 
					    addDealProduct: AddDealProductModal,
 | 
				
			||||||
 | 
					    productServiceForm: ProductServiceFormModal,
 | 
				
			||||||
    printBarcode: PrintBarcodeModal,
 | 
					    printBarcode: PrintBarcodeModal,
 | 
				
			||||||
    addBarcode: AddBarcodeModal,
 | 
					    addBarcode: AddBarcodeModal,
 | 
				
			||||||
    barcodeTemplateFormModal: BarcodeTemplateFormModal
 | 
					    barcodeTemplateFormModal: BarcodeTemplateFormModal
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,10 +7,11 @@ import {notifications} from "../../../../shared/lib/notifications.ts";
 | 
				
			|||||||
import {modals} from "@mantine/modals";
 | 
					import {modals} from "@mantine/modals";
 | 
				
			||||||
import {BaseTableRef} from "../../../../components/BaseTable/BaseTable.tsx";
 | 
					import {BaseTableRef} from "../../../../components/BaseTable/BaseTable.tsx";
 | 
				
			||||||
import DealProductsTable from "../../components/DealProductsTable/DealProductsTable.tsx";
 | 
					import DealProductsTable from "../../components/DealProductsTable/DealProductsTable.tsx";
 | 
				
			||||||
import {IconBarcode, IconBox, IconCalendarUser, IconSettings} from "@tabler/icons-react";
 | 
					import {IconBox, IconCalendarUser, IconSettings} from "@tabler/icons-react";
 | 
				
			||||||
import DealStatusChangeTable from "../../components/DealStatusChangeTable/DealStatusChangeTable.tsx";
 | 
					import DealStatusChangeTable from "../../components/DealStatusChangeTable/DealStatusChangeTable.tsx";
 | 
				
			||||||
import DealEditDrawerGeneralTab from "./tabs/DealEditDrawerGeneralTab.tsx";
 | 
					import DealEditDrawerGeneralTab from "./tabs/DealEditDrawerGeneralTab.tsx";
 | 
				
			||||||
import {useQueryClient} from "@tanstack/react-query";
 | 
					import {useQueryClient} from "@tanstack/react-query";
 | 
				
			||||||
 | 
					import ProductAndServiceTab from "../../tabs/ProductAndServiceTab/ProductAndServiceTab.tsx";
 | 
				
			||||||
// import styles from './DealEditDrawer.module.css';
 | 
					// import styles from './DealEditDrawer.module.css';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const useDealServicesTableState = () => {
 | 
					const useDealServicesTableState = () => {
 | 
				
			||||||
@@ -338,12 +339,15 @@ const DealEditDrawer: FC = () => {
 | 
				
			|||||||
                    <Tabs.Tab value={"history"} leftSection={<IconCalendarUser/>}>
 | 
					                    <Tabs.Tab value={"history"} leftSection={<IconCalendarUser/>}>
 | 
				
			||||||
                        История
 | 
					                        История
 | 
				
			||||||
                    </Tabs.Tab>
 | 
					                    </Tabs.Tab>
 | 
				
			||||||
                    <Tabs.Tab value={"services"} leftSection={<IconBox/>}>
 | 
					                    <Tabs.Tab value={"servicesAndProducts"} leftSection={<IconBox/>}>
 | 
				
			||||||
                        Услуги
 | 
					                        Товары и услуги
 | 
				
			||||||
                    </Tabs.Tab>
 | 
					 | 
				
			||||||
                    <Tabs.Tab value={"products"} leftSection={<IconBarcode/>}>
 | 
					 | 
				
			||||||
                        Товары
 | 
					 | 
				
			||||||
                    </Tabs.Tab>
 | 
					                    </Tabs.Tab>
 | 
				
			||||||
 | 
					                    {/*<Tabs.Tab value={"services"} leftSection={<IconBox/>}>*/}
 | 
				
			||||||
 | 
					                    {/*    Услуги*/}
 | 
				
			||||||
 | 
					                    {/*</Tabs.Tab>*/}
 | 
				
			||||||
 | 
					                    {/*<Tabs.Tab value={"products"} leftSection={<IconBarcode/>}>*/}
 | 
				
			||||||
 | 
					                    {/*    Товары*/}
 | 
				
			||||||
 | 
					                    {/*</Tabs.Tab>*/}
 | 
				
			||||||
                </Tabs.List>
 | 
					                </Tabs.List>
 | 
				
			||||||
                <Tabs.Panel value={"general"}>
 | 
					                <Tabs.Panel value={"general"}>
 | 
				
			||||||
                    <Box h={"100%"} w={"100%"} p={rem(10)}>
 | 
					                    <Box h={"100%"} w={"100%"} p={rem(10)}>
 | 
				
			||||||
@@ -355,6 +359,11 @@ const DealEditDrawer: FC = () => {
 | 
				
			|||||||
                        <DealEditDrawerStatusChangeTable/>
 | 
					                        <DealEditDrawerStatusChangeTable/>
 | 
				
			||||||
                    </Box>
 | 
					                    </Box>
 | 
				
			||||||
                </Tabs.Panel>
 | 
					                </Tabs.Panel>
 | 
				
			||||||
 | 
					                <Tabs.Panel value={"servicesAndProducts"}>
 | 
				
			||||||
 | 
					                    <Box p={rem(10)}>
 | 
				
			||||||
 | 
					                        <ProductAndServiceTab/>
 | 
				
			||||||
 | 
					                    </Box>
 | 
				
			||||||
 | 
					                </Tabs.Panel>
 | 
				
			||||||
                <Tabs.Panel value={"services"}>
 | 
					                <Tabs.Panel value={"services"}>
 | 
				
			||||||
                    <Box p={rem(10)}>
 | 
					                    <Box p={rem(10)}>
 | 
				
			||||||
                        <DealEditDrawerServicesTable/>
 | 
					                        <DealEditDrawerServicesTable/>
 | 
				
			||||||
@@ -362,18 +371,10 @@ const DealEditDrawer: FC = () => {
 | 
				
			|||||||
                </Tabs.Panel>
 | 
					                </Tabs.Panel>
 | 
				
			||||||
                <Tabs.Panel value={"products"}>
 | 
					                <Tabs.Panel value={"products"}>
 | 
				
			||||||
                    <Box p={rem(10)}>
 | 
					                    <Box p={rem(10)}>
 | 
				
			||||||
 | 
					 | 
				
			||||||
                        <DealEditDrawerProductsTable/>
 | 
					                        <DealEditDrawerProductsTable/>
 | 
				
			||||||
                    </Box>
 | 
					                    </Box>
 | 
				
			||||||
                </Tabs.Panel>
 | 
					                </Tabs.Panel>
 | 
				
			||||||
            </Tabs>
 | 
					            </Tabs>
 | 
				
			||||||
            {/*<Flex*/}
 | 
					 | 
				
			||||||
            {/*    h={"10%"}*/}
 | 
					 | 
				
			||||||
            {/*    align={'flex-end'}*/}
 | 
					 | 
				
			||||||
            {/*    justify={"flex-end"}*/}
 | 
					 | 
				
			||||||
            {/*>*/}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            {/*</Flex>*/}
 | 
					 | 
				
			||||||
        </Drawer>
 | 
					        </Drawer>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,7 +9,8 @@ import {omit} from "lodash";
 | 
				
			|||||||
import {BaseFormInputProps} from "../../../types/utils.ts";
 | 
					import {BaseFormInputProps} from "../../../types/utils.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type RestProps = {
 | 
					type RestProps = {
 | 
				
			||||||
    clientId: number
 | 
					    clientId: number;
 | 
				
			||||||
 | 
					    productIds?: number[];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Props = CreateEditFormProps<DealProductSchema> & RestProps;
 | 
					type Props = CreateEditFormProps<DealProductSchema> & RestProps;
 | 
				
			||||||
@@ -56,6 +57,7 @@ const AddDealProductModal = ({
 | 
				
			|||||||
                        label={"Товар"}
 | 
					                        label={"Товар"}
 | 
				
			||||||
                        clientId={innerProps.clientId}
 | 
					                        clientId={innerProps.clientId}
 | 
				
			||||||
                        disabled={isEditing}
 | 
					                        disabled={isEditing}
 | 
				
			||||||
 | 
					                        filterBy={(item) => !(innerProps.productIds || []).includes(item.id)}
 | 
				
			||||||
                        {...form.getInputProps('product')}
 | 
					                        {...form.getInputProps('product')}
 | 
				
			||||||
                    />
 | 
					                    />
 | 
				
			||||||
                    <NumberInput
 | 
					                    <NumberInput
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -40,7 +40,6 @@ const AddDealServiceModal = ({
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
        <BaseFormModal
 | 
					        <BaseFormModal
 | 
				
			||||||
            {...innerProps}
 | 
					            {...innerProps}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										80
									
								
								src/pages/LeadsPage/modals/ProductServiceFormModal.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								src/pages/LeadsPage/modals/ProductServiceFormModal.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,80 @@
 | 
				
			|||||||
 | 
					import BaseFormModal, {CreateEditFormProps} from "../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
 | 
				
			||||||
 | 
					import {DealProductServiceSchema, ServiceSchema} from "../../../client";
 | 
				
			||||||
 | 
					import {ContextModalProps} from "@mantine/modals";
 | 
				
			||||||
 | 
					import {useForm, UseFormReturnType} from "@mantine/form";
 | 
				
			||||||
 | 
					import {isNil, isNumber} from "lodash";
 | 
				
			||||||
 | 
					import ServiceWithPriceInput from "../../../components/ServiceWithPriceInput/ServiceWithPriceInput.tsx";
 | 
				
			||||||
 | 
					import {Flex} from "@mantine/core";
 | 
				
			||||||
 | 
					import {ServiceType} from "../../../shared/enums/ServiceType.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type RestProps = {
 | 
				
			||||||
 | 
					    quantity: number;
 | 
				
			||||||
 | 
					    serviceIds: number[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					type Props = CreateEditFormProps<DealProductServiceSchema> & RestProps;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ProductServiceFormModal = ({
 | 
				
			||||||
 | 
					                                     context,
 | 
				
			||||||
 | 
					                                     id, innerProps
 | 
				
			||||||
 | 
					                                 }: ContextModalProps<Props>) => {
 | 
				
			||||||
 | 
					    const isEditing = 'onChange' in innerProps;
 | 
				
			||||||
 | 
					    const initialValues: Partial<DealProductServiceSchema> = isEditing ? innerProps.element : {
 | 
				
			||||||
 | 
					        service: undefined,
 | 
				
			||||||
 | 
					        price: undefined
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const form = useForm<Partial<DealProductServiceSchema>>({
 | 
				
			||||||
 | 
					        initialValues,
 | 
				
			||||||
 | 
					        validate: {
 | 
				
			||||||
 | 
					            service: (service?: ServiceSchema) => isNil(service) || service.id < 0 ? 'Укажите услугу' : null,
 | 
				
			||||||
 | 
					            price: (price?: number) => !isNumber(price) || price < 0 ? 'Укажите цену' : null
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    const onClose = () => {
 | 
				
			||||||
 | 
					        context.closeContextModal(id);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    console.log(innerProps)
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <BaseFormModal
 | 
				
			||||||
 | 
					            {...innerProps}
 | 
				
			||||||
 | 
					            form={form as UseFormReturnType<DealProductServiceSchema>}
 | 
				
			||||||
 | 
					            onClose={onClose}
 | 
				
			||||||
 | 
					            closeOnSubmit
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					            <BaseFormModal.Body>
 | 
				
			||||||
 | 
					                <>
 | 
				
			||||||
 | 
					                    <Flex w={"100%"}>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        <ServiceWithPriceInput
 | 
				
			||||||
 | 
					                            serviceProps={{
 | 
				
			||||||
 | 
					                                ...form.getInputProps('service'),
 | 
				
			||||||
 | 
					                                label: "Услуга",
 | 
				
			||||||
 | 
					                                placeholder: "Выберите услугу",
 | 
				
			||||||
 | 
					                                disabled: isEditing,
 | 
				
			||||||
 | 
					                                filterBy: (item) => !innerProps.serviceIds.includes(item.id) || isEditing,
 | 
				
			||||||
 | 
					                                style: {width: "100%"}
 | 
				
			||||||
 | 
					                            }}
 | 
				
			||||||
 | 
					                            priceProps={{
 | 
				
			||||||
 | 
					                                ...form.getInputProps('price'),
 | 
				
			||||||
 | 
					                                label: "Цена",
 | 
				
			||||||
 | 
					                                placeholder: "Введите цену",
 | 
				
			||||||
 | 
					                                style: {width: "100%"}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            }}
 | 
				
			||||||
 | 
					                            filterType={ServiceType.PRODUCT_SERVICE}
 | 
				
			||||||
 | 
					                            containerProps={{
 | 
				
			||||||
 | 
					                                direction: "column",
 | 
				
			||||||
 | 
					                                style: {width: "100%"}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            }}
 | 
				
			||||||
 | 
					                            lockOnEdit={isEditing}
 | 
				
			||||||
 | 
					                            quantity={innerProps.quantity}
 | 
				
			||||||
 | 
					                        />
 | 
				
			||||||
 | 
					                    </Flex>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                </>
 | 
				
			||||||
 | 
					            </BaseFormModal.Body>
 | 
				
			||||||
 | 
					        </BaseFormModal>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export default ProductServiceFormModal;
 | 
				
			||||||
@@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					.container {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    //flex-direction: column;
 | 
				
			||||||
 | 
					    gap: rem(10);
 | 
				
			||||||
 | 
					    max-height: 95vh;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.products-list {
 | 
				
			||||||
 | 
					    width: 60%;
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    gap: rem(10);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.deal-container {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    height: 100%;
 | 
				
			||||||
 | 
					    gap: rem(10);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.deal-container-wrapper {
 | 
				
			||||||
 | 
					    border: dashed var(--item-border-size) var(--mantine-color-default-border);
 | 
				
			||||||
 | 
					    border-radius: var(--item-border-radius);
 | 
				
			||||||
 | 
					    padding: rem(10);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.deal-container-buttons {
 | 
				
			||||||
 | 
					    gap: rem(10);
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    margin-top: auto;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,69 @@
 | 
				
			|||||||
 | 
					import {FC} from "react";
 | 
				
			||||||
 | 
					import styles from './ProductAndServiceTab.module.css';
 | 
				
			||||||
 | 
					import ProductView from "./components/ProductView/ProductView.tsx";
 | 
				
			||||||
 | 
					import {Button, Flex, ScrollArea, Title} from "@mantine/core";
 | 
				
			||||||
 | 
					import DealServicesTable from "./components/DealServicesTable/DealServicesTable.tsx";
 | 
				
			||||||
 | 
					import useDealProductAndServiceTabState from "./hooks/useProductAndServiceTabState.tsx";
 | 
				
			||||||
 | 
					import {modals} from "@mantine/modals";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ProductAndServiceTab: FC = () => {
 | 
				
			||||||
 | 
					    const {dealState, dealServicesState, dealProductsState} = useDealProductAndServiceTabState();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const onCreateProductClick = () => {
 | 
				
			||||||
 | 
					        if (!dealProductsState.onCreate || !dealState.deal) return;
 | 
				
			||||||
 | 
					        const productIds = dealState.deal.products.map(product => product.product.id);
 | 
				
			||||||
 | 
					        modals.openContextModal({
 | 
				
			||||||
 | 
					            modal: "addDealProduct",
 | 
				
			||||||
 | 
					            innerProps: {
 | 
				
			||||||
 | 
					                onCreate: dealProductsState.onCreate,
 | 
				
			||||||
 | 
					                clientId: dealState.deal.clientId,
 | 
				
			||||||
 | 
					                productIds: productIds
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            withCloseButton: false
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const getTotalPrice = () => {
 | 
				
			||||||
 | 
					        if (!dealState.deal) return 0
 | 
				
			||||||
 | 
					        const productServicesPrice = dealState.deal.products.reduce((acc, row) => acc + row.services.reduce((acc2, row2) => acc2 + row2.price * row.quantity, 0), 0);
 | 
				
			||||||
 | 
					        const dealServicesPrice = dealState.deal.services.reduce((acc, row) => acc + row.price * row.quantity, 0);
 | 
				
			||||||
 | 
					        return dealServicesPrice + productServicesPrice;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <div className={styles['container']}>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <div className={styles['products-list']}>
 | 
				
			||||||
 | 
					                <ScrollArea offsetScrollbars>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    {dealState.deal?.products.map(product => (
 | 
				
			||||||
 | 
					                        <ProductView
 | 
				
			||||||
 | 
					                            key={product.product.id}
 | 
				
			||||||
 | 
					                            product={product}
 | 
				
			||||||
 | 
					                            onChange={dealProductsState.onChange}
 | 
				
			||||||
 | 
					                            onDelete={dealProductsState.onDelete}
 | 
				
			||||||
 | 
					                        />
 | 
				
			||||||
 | 
					                    ))}
 | 
				
			||||||
 | 
					                </ScrollArea>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div className={styles['deal-container']}>
 | 
				
			||||||
 | 
					                <Flex direction={"column"} className={styles['deal-container-wrapper']}>
 | 
				
			||||||
 | 
					                    <DealServicesTable
 | 
				
			||||||
 | 
					                        {...dealServicesState}
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                    <div className={styles['deal-container-buttons']}>
 | 
				
			||||||
 | 
					                        <Button
 | 
				
			||||||
 | 
					                            onClick={onCreateProductClick}
 | 
				
			||||||
 | 
					                            variant={"default"}
 | 
				
			||||||
 | 
					                            fullWidth>Добавить товар</Button>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                </Flex>
 | 
				
			||||||
 | 
					                <Flex direction={"column"} className={styles['deal-container-wrapper']}>
 | 
				
			||||||
 | 
					                    <Title order={3}>Общая стоимость всех услуг: {getTotalPrice()}</Title>
 | 
				
			||||||
 | 
					                </Flex>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default ProductAndServiceTab;
 | 
				
			||||||
@@ -0,0 +1,110 @@
 | 
				
			|||||||
 | 
					import {CRUDTableProps} from "../../../../../../types/CRUDTable.tsx";
 | 
				
			||||||
 | 
					import {DealServiceSchema} from "../../../../../../client";
 | 
				
			||||||
 | 
					import {FC} from "react";
 | 
				
			||||||
 | 
					import {ActionIcon, Button, Flex, NumberInput, rem, Text, Title, Tooltip} from "@mantine/core";
 | 
				
			||||||
 | 
					import {IconTrash} from "@tabler/icons-react";
 | 
				
			||||||
 | 
					import {modals} from "@mantine/modals";
 | 
				
			||||||
 | 
					import {isNumber} from "lodash";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Props = CRUDTableProps<DealServiceSchema>;
 | 
				
			||||||
 | 
					const DealServicesTable: FC<Props> = ({items, onDelete, onCreate, onChange}) => {
 | 
				
			||||||
 | 
					    const onDeleteClick = (item: DealServiceSchema) => {
 | 
				
			||||||
 | 
					        if (!onDelete) return;
 | 
				
			||||||
 | 
					        onDelete(item);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const onCreateClick = () => {
 | 
				
			||||||
 | 
					        if (!onCreate) return;
 | 
				
			||||||
 | 
					        const serviceIds = items.map(service => service.service.id);
 | 
				
			||||||
 | 
					        modals.openContextModal({
 | 
				
			||||||
 | 
					            modal: "addDealService",
 | 
				
			||||||
 | 
					            innerProps: {
 | 
				
			||||||
 | 
					                onCreate: onCreate,
 | 
				
			||||||
 | 
					                serviceIds
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            withCloseButton: false
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const onQuantityChange = (item: DealServiceSchema, quantity: number) => {
 | 
				
			||||||
 | 
					        if (!onChange) return;
 | 
				
			||||||
 | 
					        onChange({
 | 
				
			||||||
 | 
					            ...item,
 | 
				
			||||||
 | 
					            quantity
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const onPriceChange = (item: DealServiceSchema, price: number) => {
 | 
				
			||||||
 | 
					        if (!onChange) return;
 | 
				
			||||||
 | 
					        onChange({
 | 
				
			||||||
 | 
					            ...item,
 | 
				
			||||||
 | 
					            price
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <Flex
 | 
				
			||||||
 | 
					            direction={"column"}
 | 
				
			||||||
 | 
					            gap={rem(10)}
 | 
				
			||||||
 | 
					            h={"100%"}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					            <Flex
 | 
				
			||||||
 | 
					                h={"100%"}
 | 
				
			||||||
 | 
					                direction={"column"}>
 | 
				
			||||||
 | 
					                <Title
 | 
				
			||||||
 | 
					                    order={3}
 | 
				
			||||||
 | 
					                    w={"100%"}
 | 
				
			||||||
 | 
					                    style={{textAlign: "center"}}
 | 
				
			||||||
 | 
					                    mb={rem(10)}
 | 
				
			||||||
 | 
					                >Общие услуги</Title>
 | 
				
			||||||
 | 
					                <Flex
 | 
				
			||||||
 | 
					                    direction={"column"}
 | 
				
			||||||
 | 
					                    gap={rem(10)}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    {items.map(service => (
 | 
				
			||||||
 | 
					                        <Flex
 | 
				
			||||||
 | 
					                            key={service.service.id}
 | 
				
			||||||
 | 
					                            w={"100%"}
 | 
				
			||||||
 | 
					                            gap={rem(10)}
 | 
				
			||||||
 | 
					                            align={"center"}
 | 
				
			||||||
 | 
					                        >
 | 
				
			||||||
 | 
					                            <Tooltip
 | 
				
			||||||
 | 
					                                onClick={() => onDeleteClick(service)}
 | 
				
			||||||
 | 
					                                label="Удалить услугу">
 | 
				
			||||||
 | 
					                                <ActionIcon
 | 
				
			||||||
 | 
					                                    variant={"default"}>
 | 
				
			||||||
 | 
					                                    <IconTrash/>
 | 
				
			||||||
 | 
					                                </ActionIcon>
 | 
				
			||||||
 | 
					                            </Tooltip>
 | 
				
			||||||
 | 
					                            <Text
 | 
				
			||||||
 | 
					                                flex={1}
 | 
				
			||||||
 | 
					                            >{service.service.name}</Text>
 | 
				
			||||||
 | 
					                            <NumberInput
 | 
				
			||||||
 | 
					                                suffix={" шт."}
 | 
				
			||||||
 | 
					                                onChange={event => isNumber(event) && onQuantityChange(service, event)}
 | 
				
			||||||
 | 
					                                value={service.quantity}
 | 
				
			||||||
 | 
					                            />
 | 
				
			||||||
 | 
					                            <NumberInput
 | 
				
			||||||
 | 
					                                onChange={event => isNumber(event) && onPriceChange(service, event)}
 | 
				
			||||||
 | 
					                                suffix={"₽"}
 | 
				
			||||||
 | 
					                                value={service.price}
 | 
				
			||||||
 | 
					                            />
 | 
				
			||||||
 | 
					                        </Flex>
 | 
				
			||||||
 | 
					                    ))}
 | 
				
			||||||
 | 
					                </Flex>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <Title
 | 
				
			||||||
 | 
					                    style={{textAlign: "end"}}
 | 
				
			||||||
 | 
					                    mt={rem(10)}
 | 
				
			||||||
 | 
					                    order={3}
 | 
				
			||||||
 | 
					                >Итог: {items.reduce((acc, item) => acc + (item.price * item.quantity), 0)}₽</Title>
 | 
				
			||||||
 | 
					            </Flex>
 | 
				
			||||||
 | 
					            <Flex pb={rem(10)} mt={"auto"}>
 | 
				
			||||||
 | 
					                <Button
 | 
				
			||||||
 | 
					                    onClick={onCreateClick}
 | 
				
			||||||
 | 
					                    fullWidth
 | 
				
			||||||
 | 
					                    variant={"default"}
 | 
				
			||||||
 | 
					                >Добавить услугу</Button>
 | 
				
			||||||
 | 
					            </Flex>
 | 
				
			||||||
 | 
					        </Flex>
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export default DealServicesTable;
 | 
				
			||||||
@@ -0,0 +1,97 @@
 | 
				
			|||||||
 | 
					import {CRUDTableProps} from "../../../../../../types/CRUDTable.tsx";
 | 
				
			||||||
 | 
					import {DealProductServiceSchema} from "../../../../../../client";
 | 
				
			||||||
 | 
					import {FC} from "react";
 | 
				
			||||||
 | 
					import useProductServicesTableColumns from "./columns.tsx";
 | 
				
			||||||
 | 
					import {BaseTable} from "../../../../../../components/BaseTable/BaseTable.tsx";
 | 
				
			||||||
 | 
					import {MRT_TableOptions} from "mantine-react-table";
 | 
				
			||||||
 | 
					import {ActionIcon, Button, Flex, rem, Tooltip} from "@mantine/core";
 | 
				
			||||||
 | 
					import {IconEdit, IconTrash} from "@tabler/icons-react";
 | 
				
			||||||
 | 
					import {modals} from "@mantine/modals";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type RestProps = {
 | 
				
			||||||
 | 
					    quantity: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					type Props = CRUDTableProps<DealProductServiceSchema> & RestProps;
 | 
				
			||||||
 | 
					const ProductServicesTable: FC<Props> = ({items, quantity, onCreate, onDelete, onChange}) => {
 | 
				
			||||||
 | 
					    const columns = useProductServicesTableColumns({data: items, quantity});
 | 
				
			||||||
 | 
					    const serviceIds = items.map(service => service.service.id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const onCreateClick = () => {
 | 
				
			||||||
 | 
					        if (!onCreate) return;
 | 
				
			||||||
 | 
					        modals.openContextModal({
 | 
				
			||||||
 | 
					            modal: "productServiceForm",
 | 
				
			||||||
 | 
					            innerProps: {
 | 
				
			||||||
 | 
					                onCreate: onCreate,
 | 
				
			||||||
 | 
					                serviceIds,
 | 
				
			||||||
 | 
					                quantity
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            withCloseButton: false
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const onChangeClick = (item: DealProductServiceSchema) => {
 | 
				
			||||||
 | 
					        console.log('change click')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!onChange) return;
 | 
				
			||||||
 | 
					        console.log('change click1')
 | 
				
			||||||
 | 
					        modals.openContextModal({
 | 
				
			||||||
 | 
					            modal: "productServiceForm",
 | 
				
			||||||
 | 
					            innerProps: {
 | 
				
			||||||
 | 
					                element: item,
 | 
				
			||||||
 | 
					                onChange,
 | 
				
			||||||
 | 
					                serviceIds,
 | 
				
			||||||
 | 
					                quantity
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            withCloseButton: false
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <Flex
 | 
				
			||||||
 | 
					            direction={"column"}
 | 
				
			||||||
 | 
					            gap={rem(10)}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					            <BaseTable
 | 
				
			||||||
 | 
					                data={items}
 | 
				
			||||||
 | 
					                columns={columns}
 | 
				
			||||||
 | 
					                restProps={{
 | 
				
			||||||
 | 
					                    enableColumnActions: false,
 | 
				
			||||||
 | 
					                    enableSorting: false,
 | 
				
			||||||
 | 
					                    enableRowActions: true,
 | 
				
			||||||
 | 
					                    enableBottomToolbar: true,
 | 
				
			||||||
 | 
					                    renderBottomToolbar: (
 | 
				
			||||||
 | 
					                        <Flex justify={"flex-end"} gap={rem(10)} p={rem(10)}>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            <Button onClick={onCreateClick} variant={"default"}>
 | 
				
			||||||
 | 
					                                Добавить услугу
 | 
				
			||||||
 | 
					                            </Button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        </Flex>
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    renderRowActions: ({row}) => (
 | 
				
			||||||
 | 
					                        <Flex gap="md">
 | 
				
			||||||
 | 
					                            <Tooltip label="Редактировать">
 | 
				
			||||||
 | 
					                                <ActionIcon
 | 
				
			||||||
 | 
					                                    onClick={() => onChangeClick(row.original)}
 | 
				
			||||||
 | 
					                                    variant={"default"}>
 | 
				
			||||||
 | 
					                                    <IconEdit/>
 | 
				
			||||||
 | 
					                                </ActionIcon>
 | 
				
			||||||
 | 
					                            </Tooltip>
 | 
				
			||||||
 | 
					                            <Tooltip label="Удалить">
 | 
				
			||||||
 | 
					                                <ActionIcon onClick={() => {
 | 
				
			||||||
 | 
					                                    if (onDelete) onDelete(row.original);
 | 
				
			||||||
 | 
					                                    // console.log(row);
 | 
				
			||||||
 | 
					                                }} variant={"default"}>
 | 
				
			||||||
 | 
					                                    <IconTrash/>
 | 
				
			||||||
 | 
					                                </ActionIcon>
 | 
				
			||||||
 | 
					                            </Tooltip>
 | 
				
			||||||
 | 
					                        </Flex>
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                } as MRT_TableOptions<DealProductServiceSchema>}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					            {/*<Button variant={"default"}>Добавить услугу</Button>*/}
 | 
				
			||||||
 | 
					        </Flex>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export default ProductServicesTable;
 | 
				
			||||||
@@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					import {useMemo} from "react";
 | 
				
			||||||
 | 
					import {MRT_ColumnDef} from "mantine-react-table";
 | 
				
			||||||
 | 
					import {DealProductServiceSchema} from "../../../../../../client";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Props = {
 | 
				
			||||||
 | 
					    data: DealProductServiceSchema[];
 | 
				
			||||||
 | 
					    quantity: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					const useProductServicesTableColumns = (props: Props) => {
 | 
				
			||||||
 | 
					    const {data, quantity} = props;
 | 
				
			||||||
 | 
					    const totalPrice = useMemo(() => data.reduce((acc, row) => acc + (row.price * quantity), 0), [data, quantity]);
 | 
				
			||||||
 | 
					    return useMemo<MRT_ColumnDef<DealProductServiceSchema>[]>(() => [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            accessorKey: "service.name",
 | 
				
			||||||
 | 
					            header: "Услуга",
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            accessorKey: "price",
 | 
				
			||||||
 | 
					            header: "Цена",
 | 
				
			||||||
 | 
					            Footer: () => <>Итог: {totalPrice}₽</>,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    ], [totalPrice]);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default useProductServicesTableColumns;
 | 
				
			||||||
@@ -0,0 +1,33 @@
 | 
				
			|||||||
 | 
					.container {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    border: dashed var(--item-border-size) var(--mantine-color-default-border);
 | 
				
			||||||
 | 
					    border-radius: var(--item-border-radius);
 | 
				
			||||||
 | 
					    gap: rem(20);
 | 
				
			||||||
 | 
					    padding: rem(10);
 | 
				
			||||||
 | 
					    margin-bottom: rem(10);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.image-container {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    max-height: rem(250);
 | 
				
			||||||
 | 
					    max-width: rem(250);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.services-container {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    gap: rem(10);
 | 
				
			||||||
 | 
					    flex: 1
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.data-container {
 | 
				
			||||||
 | 
					    max-width: rem(250);
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    gap: rem(10);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.attributes-container {
 | 
				
			||||||
 | 
					    overflow-wrap: break-word;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,140 @@
 | 
				
			|||||||
 | 
					import {FC} from "react";
 | 
				
			||||||
 | 
					import {DealProductSchema, DealProductServiceSchema, ProductSchema} from "../../../../../../client";
 | 
				
			||||||
 | 
					import styles from './ProductView.module.css';
 | 
				
			||||||
 | 
					import {ActionIcon, Flex, Image, NumberInput, rem, Spoiler, Text, Title, Tooltip} from '@mantine/core';
 | 
				
			||||||
 | 
					import ProductServicesTable from "../ProductServicesTable/ProductServicesTable.tsx";
 | 
				
			||||||
 | 
					import {isNil, isNumber} from "lodash";
 | 
				
			||||||
 | 
					import {IconBarcode, IconTrash} from "@tabler/icons-react";
 | 
				
			||||||
 | 
					import {modals} from "@mantine/modals";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Props = {
 | 
				
			||||||
 | 
					    product: DealProductSchema;
 | 
				
			||||||
 | 
					    onChange?: (item: DealProductSchema) => void;
 | 
				
			||||||
 | 
					    onDelete?: (item: DealProductSchema) => void
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					type ProductFieldNames = {
 | 
				
			||||||
 | 
					    [K in keyof ProductSchema]: string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					const ProductFieldNames: Partial<ProductFieldNames> = {
 | 
				
			||||||
 | 
					    color: "Цвет",
 | 
				
			||||||
 | 
					    article: "Артикул",
 | 
				
			||||||
 | 
					    size: "Размер",
 | 
				
			||||||
 | 
					    brand: "Бренд",
 | 
				
			||||||
 | 
					    composition: "Состав",
 | 
				
			||||||
 | 
					    additionalInfo: "Доп. информация",
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					const ProductView: FC<Props> = ({product, onDelete, onChange}) => {
 | 
				
			||||||
 | 
					    const onDeleteClick = () => {
 | 
				
			||||||
 | 
					        if (!onDelete) return;
 | 
				
			||||||
 | 
					        onDelete(product);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const onServiceDelete = (item: DealProductServiceSchema) => {
 | 
				
			||||||
 | 
					        if (!onChange) return;
 | 
				
			||||||
 | 
					        onChange({
 | 
				
			||||||
 | 
					            ...product,
 | 
				
			||||||
 | 
					            services: product.services.filter(service => service.service.id !== item.service.id)
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const onServiceCreate = (item: DealProductServiceSchema) => {
 | 
				
			||||||
 | 
					        if (!onChange) return;
 | 
				
			||||||
 | 
					        onChange({
 | 
				
			||||||
 | 
					            ...product,
 | 
				
			||||||
 | 
					            services: [...product.services, item]
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const onServiceChange = (item: DealProductServiceSchema) => {
 | 
				
			||||||
 | 
					        if (!onChange) return;
 | 
				
			||||||
 | 
					        onChange({
 | 
				
			||||||
 | 
					            ...product,
 | 
				
			||||||
 | 
					            services: product.services.map(service => service.service.id === item.service.id ? item : service)
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const onQuantityChange = (quantity: number) => {
 | 
				
			||||||
 | 
					        if (!onChange) return;
 | 
				
			||||||
 | 
					        onChange({
 | 
				
			||||||
 | 
					            ...product,
 | 
				
			||||||
 | 
					            quantity
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const onPrintBarcodeClick = () => {
 | 
				
			||||||
 | 
					        modals.openContextModal({
 | 
				
			||||||
 | 
					            modal: "printBarcode",
 | 
				
			||||||
 | 
					            innerProps: {
 | 
				
			||||||
 | 
					                productId: product.product.id,
 | 
				
			||||||
 | 
					                defaultQuantity: product.quantity
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            title: 'Печать штрихкода',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <div className={styles['container']}>
 | 
				
			||||||
 | 
					            <div className={styles['data-container']}>
 | 
				
			||||||
 | 
					                <div className={styles['image-container']}>
 | 
				
			||||||
 | 
					                    <Image
 | 
				
			||||||
 | 
					                        radius={rem(10)}
 | 
				
			||||||
 | 
					                        fit={"cover"}
 | 
				
			||||||
 | 
					                        src={product.product.imageUrl}
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <div className={styles['attributes-container']}>
 | 
				
			||||||
 | 
					                    <Title>{product.product.name}</Title>
 | 
				
			||||||
 | 
					                    <Spoiler maxHeight={0} showLabel={"Показать характеристики"} hideLabel={"Скрыть"}>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        {Object.entries(product.product).map(([key, value]) => {
 | 
				
			||||||
 | 
					                            const fieldName = ProductFieldNames[key as keyof ProductSchema];
 | 
				
			||||||
 | 
					                            if (!fieldName || isNil(value) || value === '') return;
 | 
				
			||||||
 | 
					                            return (<Text>{fieldName}: {value.toString()} </Text>)
 | 
				
			||||||
 | 
					                        })}
 | 
				
			||||||
 | 
					                        <Text>Штрихкоды: {product.product.barcodes.join(', ')}</Text>
 | 
				
			||||||
 | 
					                    </Spoiler>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <NumberInput
 | 
				
			||||||
 | 
					                    suffix={" шт."}
 | 
				
			||||||
 | 
					                    value={product.quantity}
 | 
				
			||||||
 | 
					                    onChange={event => isNumber(event) && onQuantityChange(event)}
 | 
				
			||||||
 | 
					                    placeholder={"Введите количество товара"}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <div className={styles['services-container']}>
 | 
				
			||||||
 | 
					                <ProductServicesTable
 | 
				
			||||||
 | 
					                    items={product.services}
 | 
				
			||||||
 | 
					                    quantity={product.quantity}
 | 
				
			||||||
 | 
					                    onCreate={onServiceCreate}
 | 
				
			||||||
 | 
					                    onDelete={onServiceDelete}
 | 
				
			||||||
 | 
					                    onChange={onServiceChange}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					                <Flex
 | 
				
			||||||
 | 
					                    mt={"auto"}
 | 
				
			||||||
 | 
					                    ml={"auto"}
 | 
				
			||||||
 | 
					                    gap={rem(10)}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                    <Tooltip
 | 
				
			||||||
 | 
					                        onClick={onPrintBarcodeClick}
 | 
				
			||||||
 | 
					                        label="Печать штрихкода">
 | 
				
			||||||
 | 
					                        <ActionIcon
 | 
				
			||||||
 | 
					                            variant={"default"}>
 | 
				
			||||||
 | 
					                            <IconBarcode/>
 | 
				
			||||||
 | 
					                        </ActionIcon>
 | 
				
			||||||
 | 
					                    </Tooltip>
 | 
				
			||||||
 | 
					                    <Tooltip onClick={onDeleteClick} label="Удалить товар">
 | 
				
			||||||
 | 
					                        <ActionIcon
 | 
				
			||||||
 | 
					                            variant={"default"}>
 | 
				
			||||||
 | 
					                            <IconTrash/>
 | 
				
			||||||
 | 
					                        </ActionIcon>
 | 
				
			||||||
 | 
					                    </Tooltip>
 | 
				
			||||||
 | 
					                </Flex>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default ProductView;
 | 
				
			||||||
@@ -0,0 +1,125 @@
 | 
				
			|||||||
 | 
					import {CRUDTableProps} from "../../../../../types/CRUDTable.tsx";
 | 
				
			||||||
 | 
					import {DealProductSchema, DealService, DealServiceSchema} from "../../../../../client";
 | 
				
			||||||
 | 
					import {useDealPageContext} from "../../../contexts/DealPageContext.tsx";
 | 
				
			||||||
 | 
					import {notifications} from "../../../../../shared/lib/notifications.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const useDealState = () => {
 | 
				
			||||||
 | 
					    const {selectedDeal, setSelectedDeal} = useDealPageContext();
 | 
				
			||||||
 | 
					    const refetch = async () => {
 | 
				
			||||||
 | 
					        if (!selectedDeal) return
 | 
				
			||||||
 | 
					        return DealService.getDealById({dealId: selectedDeal.id}).then((deal) => {
 | 
				
			||||||
 | 
					            setSelectedDeal(deal);
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					        deal: selectedDeal,
 | 
				
			||||||
 | 
					        refetch
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const useDealServicesState = (): CRUDTableProps<DealServiceSchema> => {
 | 
				
			||||||
 | 
					    const {deal, refetch} = useDealState();
 | 
				
			||||||
 | 
					    const onCreate = (item: DealServiceSchema) => {
 | 
				
			||||||
 | 
					        if (!deal) return;
 | 
				
			||||||
 | 
					        DealService.addDealService({
 | 
				
			||||||
 | 
					            requestBody: {
 | 
				
			||||||
 | 
					                dealId: deal.id,
 | 
				
			||||||
 | 
					                serviceId: item.service.id,
 | 
				
			||||||
 | 
					                quantity: item.quantity,
 | 
				
			||||||
 | 
					                price: item.price
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }).then(async ({ok, message}) => {
 | 
				
			||||||
 | 
					            if (!ok) notifications.guess(ok, {message});
 | 
				
			||||||
 | 
					            if (ok) await refetch();
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const onDelete = (item: DealServiceSchema) => {
 | 
				
			||||||
 | 
					        if (!deal) return;
 | 
				
			||||||
 | 
					        DealService.deleteDealService({
 | 
				
			||||||
 | 
					            requestBody:
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    dealId: deal.id,
 | 
				
			||||||
 | 
					                    serviceId: item.service.id
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					        }).then(async ({ok, message}) => {
 | 
				
			||||||
 | 
					            if (!ok) notifications.guess(ok, {message});
 | 
				
			||||||
 | 
					            if (ok) await refetch();
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const onChange = (item: DealServiceSchema) => {
 | 
				
			||||||
 | 
					        if (!deal) return;
 | 
				
			||||||
 | 
					        DealService.updateDealService({
 | 
				
			||||||
 | 
					            requestBody:
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    dealId: deal.id,
 | 
				
			||||||
 | 
					                    service: item
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					        }).then(async ({ok, message}) => {
 | 
				
			||||||
 | 
					            if (!ok) notifications.guess(ok, {message});
 | 
				
			||||||
 | 
					            if (ok) await refetch();
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					        items: deal?.services || [],
 | 
				
			||||||
 | 
					        onCreate,
 | 
				
			||||||
 | 
					        onDelete,
 | 
				
			||||||
 | 
					        onChange
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const useDealProductsState = (): CRUDTableProps<DealProductSchema> => {
 | 
				
			||||||
 | 
					    const {deal, refetch} = useDealState();
 | 
				
			||||||
 | 
					    const onCreate = (item: DealProductSchema) => {
 | 
				
			||||||
 | 
					        if (!deal) return;
 | 
				
			||||||
 | 
					        DealService.addDealProduct({
 | 
				
			||||||
 | 
					            requestBody: {
 | 
				
			||||||
 | 
					                dealId: deal.id,
 | 
				
			||||||
 | 
					                product: item
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }).then(async ({ok, message}) => {
 | 
				
			||||||
 | 
					            if (!ok) notifications.guess(ok, {message});
 | 
				
			||||||
 | 
					            if (ok) await refetch();
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    const onDelete = (item: DealProductSchema) => {
 | 
				
			||||||
 | 
					        if (!deal) return;
 | 
				
			||||||
 | 
					        DealService.deleteDealProduct({
 | 
				
			||||||
 | 
					            requestBody: {
 | 
				
			||||||
 | 
					                dealId: deal.id,
 | 
				
			||||||
 | 
					                productId: item.product.id
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }).then(async ({ok, message}) => {
 | 
				
			||||||
 | 
					            if (!ok) notifications.guess(ok, {message});
 | 
				
			||||||
 | 
					            if (ok) await refetch();
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    const onChange = (item: DealProductSchema) => {
 | 
				
			||||||
 | 
					        if (!deal) return;
 | 
				
			||||||
 | 
					        DealService.updateDealProduct({
 | 
				
			||||||
 | 
					            requestBody: {
 | 
				
			||||||
 | 
					                dealId: deal.id,
 | 
				
			||||||
 | 
					                product: item
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }).then(async ({ok, message}) => {
 | 
				
			||||||
 | 
					            if (!ok) notifications.guess(ok, {message});
 | 
				
			||||||
 | 
					            if (ok) await refetch();
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					        items: deal?.products || [],
 | 
				
			||||||
 | 
					        onCreate,
 | 
				
			||||||
 | 
					        onDelete,
 | 
				
			||||||
 | 
					        onChange
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					const useDealProductAndServiceTabState = () => {
 | 
				
			||||||
 | 
					    const dealState = useDealState();
 | 
				
			||||||
 | 
					    const dealProductsState = useDealProductsState();
 | 
				
			||||||
 | 
					    const dealServicesState = useDealServicesState();
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					        dealState,
 | 
				
			||||||
 | 
					        dealProductsState,
 | 
				
			||||||
 | 
					        dealServicesState
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export default useDealProductAndServiceTabState;
 | 
				
			||||||
@@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					import {isNil} from "lodash";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const dateWithoutTimezone = (date: Date) => {
 | 
					export const dateWithoutTimezone = (date: Date) => {
 | 
				
			||||||
    const tzoffset = date.getTimezoneOffset() * 60000; //offset in milliseconds
 | 
					    const tzoffset = date.getTimezoneOffset() * 60000; //offset in milliseconds
 | 
				
			||||||
    const withoutTimezone = new Date(date.valueOf() - tzoffset)
 | 
					    const withoutTimezone = new Date(date.valueOf() - tzoffset)
 | 
				
			||||||
@@ -45,3 +47,7 @@ export const IMAGE_MIME_TYPES = [
 | 
				
			|||||||
    "image/svg+xml",
 | 
					    "image/svg+xml",
 | 
				
			||||||
    "image/heic"
 | 
					    "image/heic"
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const isNilOrEmptyString = (value: unknown): value is null | undefined | '' => {
 | 
				
			||||||
 | 
					    return isNil(value) || (typeof value === 'string' && value.trim() === '');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user