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 BarcodeTemplateFormModal
 | 
			
		||||
    from "../pages/BarcodePage/modals/BarcodeTemplateFormModal/BarcodeTemplateFormModal.tsx";
 | 
			
		||||
import ProductServiceFormModal from "../pages/LeadsPage/modals/ProductServiceFormModal.tsx";
 | 
			
		||||
 | 
			
		||||
export const modals = {
 | 
			
		||||
    enterDeadline: EnterDeadlineModal,
 | 
			
		||||
@@ -18,6 +19,7 @@ export const modals = {
 | 
			
		||||
    productFormModal: ProductFormModal,
 | 
			
		||||
    addDealService: AddDealServiceModal,
 | 
			
		||||
    addDealProduct: AddDealProductModal,
 | 
			
		||||
    productServiceForm: ProductServiceFormModal,
 | 
			
		||||
    printBarcode: PrintBarcodeModal,
 | 
			
		||||
    addBarcode: AddBarcodeModal,
 | 
			
		||||
    barcodeTemplateFormModal: BarcodeTemplateFormModal
 | 
			
		||||
 
 | 
			
		||||
@@ -7,10 +7,11 @@ import {notifications} from "../../../../shared/lib/notifications.ts";
 | 
			
		||||
import {modals} from "@mantine/modals";
 | 
			
		||||
import {BaseTableRef} from "../../../../components/BaseTable/BaseTable.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 DealEditDrawerGeneralTab from "./tabs/DealEditDrawerGeneralTab.tsx";
 | 
			
		||||
import {useQueryClient} from "@tanstack/react-query";
 | 
			
		||||
import ProductAndServiceTab from "../../tabs/ProductAndServiceTab/ProductAndServiceTab.tsx";
 | 
			
		||||
// import styles from './DealEditDrawer.module.css';
 | 
			
		||||
 | 
			
		||||
const useDealServicesTableState = () => {
 | 
			
		||||
@@ -338,12 +339,15 @@ const DealEditDrawer: FC = () => {
 | 
			
		||||
                    <Tabs.Tab value={"history"} leftSection={<IconCalendarUser/>}>
 | 
			
		||||
                        История
 | 
			
		||||
                    </Tabs.Tab>
 | 
			
		||||
                    <Tabs.Tab value={"services"} leftSection={<IconBox/>}>
 | 
			
		||||
                        Услуги
 | 
			
		||||
                    </Tabs.Tab>
 | 
			
		||||
                    <Tabs.Tab value={"products"} leftSection={<IconBarcode/>}>
 | 
			
		||||
                        Товары
 | 
			
		||||
                    <Tabs.Tab value={"servicesAndProducts"} leftSection={<IconBox/>}>
 | 
			
		||||
                        Товары и услуги
 | 
			
		||||
                    </Tabs.Tab>
 | 
			
		||||
                    {/*<Tabs.Tab value={"services"} leftSection={<IconBox/>}>*/}
 | 
			
		||||
                    {/*    Услуги*/}
 | 
			
		||||
                    {/*</Tabs.Tab>*/}
 | 
			
		||||
                    {/*<Tabs.Tab value={"products"} leftSection={<IconBarcode/>}>*/}
 | 
			
		||||
                    {/*    Товары*/}
 | 
			
		||||
                    {/*</Tabs.Tab>*/}
 | 
			
		||||
                </Tabs.List>
 | 
			
		||||
                <Tabs.Panel value={"general"}>
 | 
			
		||||
                    <Box h={"100%"} w={"100%"} p={rem(10)}>
 | 
			
		||||
@@ -355,6 +359,11 @@ const DealEditDrawer: FC = () => {
 | 
			
		||||
                        <DealEditDrawerStatusChangeTable/>
 | 
			
		||||
                    </Box>
 | 
			
		||||
                </Tabs.Panel>
 | 
			
		||||
                <Tabs.Panel value={"servicesAndProducts"}>
 | 
			
		||||
                    <Box p={rem(10)}>
 | 
			
		||||
                        <ProductAndServiceTab/>
 | 
			
		||||
                    </Box>
 | 
			
		||||
                </Tabs.Panel>
 | 
			
		||||
                <Tabs.Panel value={"services"}>
 | 
			
		||||
                    <Box p={rem(10)}>
 | 
			
		||||
                        <DealEditDrawerServicesTable/>
 | 
			
		||||
@@ -362,18 +371,10 @@ const DealEditDrawer: FC = () => {
 | 
			
		||||
                </Tabs.Panel>
 | 
			
		||||
                <Tabs.Panel value={"products"}>
 | 
			
		||||
                    <Box p={rem(10)}>
 | 
			
		||||
 | 
			
		||||
                        <DealEditDrawerProductsTable/>
 | 
			
		||||
                    </Box>
 | 
			
		||||
                </Tabs.Panel>
 | 
			
		||||
            </Tabs>
 | 
			
		||||
            {/*<Flex*/}
 | 
			
		||||
            {/*    h={"10%"}*/}
 | 
			
		||||
            {/*    align={'flex-end'}*/}
 | 
			
		||||
            {/*    justify={"flex-end"}*/}
 | 
			
		||||
            {/*>*/}
 | 
			
		||||
 | 
			
		||||
            {/*</Flex>*/}
 | 
			
		||||
        </Drawer>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,8 @@ import {omit} from "lodash";
 | 
			
		||||
import {BaseFormInputProps} from "../../../types/utils.ts";
 | 
			
		||||
 | 
			
		||||
type RestProps = {
 | 
			
		||||
    clientId: number
 | 
			
		||||
    clientId: number;
 | 
			
		||||
    productIds?: number[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Props = CreateEditFormProps<DealProductSchema> & RestProps;
 | 
			
		||||
@@ -56,6 +57,7 @@ const AddDealProductModal = ({
 | 
			
		||||
                        label={"Товар"}
 | 
			
		||||
                        clientId={innerProps.clientId}
 | 
			
		||||
                        disabled={isEditing}
 | 
			
		||||
                        filterBy={(item) => !(innerProps.productIds || []).includes(item.id)}
 | 
			
		||||
                        {...form.getInputProps('product')}
 | 
			
		||||
                    />
 | 
			
		||||
                    <NumberInput
 | 
			
		||||
 
 | 
			
		||||
@@ -40,7 +40,6 @@ const AddDealServiceModal = ({
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <BaseFormModal
 | 
			
		||||
            {...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) => {
 | 
			
		||||
    const tzoffset = date.getTimezoneOffset() * 60000; //offset in milliseconds
 | 
			
		||||
    const withoutTimezone = new Date(date.valueOf() - tzoffset)
 | 
			
		||||
@@ -45,3 +47,7 @@ export const IMAGE_MIME_TYPES = [
 | 
			
		||||
    "image/svg+xml",
 | 
			
		||||
    "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