feat: deal prefilling
This commit is contained in:
		@@ -25,6 +25,7 @@ export type { BaseEnumListSchema } from './models/BaseEnumListSchema';
 | 
			
		||||
export type { BaseEnumSchema } from './models/BaseEnumSchema';
 | 
			
		||||
export type { BaseMarketplaceSchema } from './models/BaseMarketplaceSchema';
 | 
			
		||||
export type { BaseShippingWarehouseSchema } from './models/BaseShippingWarehouseSchema';
 | 
			
		||||
export type { BillPaymentInfo } from './models/BillPaymentInfo';
 | 
			
		||||
export type { BillPaymentStatus } from './models/BillPaymentStatus';
 | 
			
		||||
export type { BillStatusUpdateRequest } from './models/BillStatusUpdateRequest';
 | 
			
		||||
export type { Body_upload_product_image } from './models/Body_upload_product_image';
 | 
			
		||||
@@ -90,6 +91,8 @@ export type { DealDeleteServicesRequest } from './models/DealDeleteServicesReque
 | 
			
		||||
export type { DealDeleteServicesResponse } from './models/DealDeleteServicesResponse';
 | 
			
		||||
export type { DealGeneralInfoSchema } from './models/DealGeneralInfoSchema';
 | 
			
		||||
export type { DealGetAllResponse } from './models/DealGetAllResponse';
 | 
			
		||||
export type { DealPrefillRequest } from './models/DealPrefillRequest';
 | 
			
		||||
export type { DealPrefillResponse } from './models/DealPrefillResponse';
 | 
			
		||||
export type { DealProductAddKitRequest } from './models/DealProductAddKitRequest';
 | 
			
		||||
export type { DealProductAddKitResponse } from './models/DealProductAddKitResponse';
 | 
			
		||||
export type { DealProductSchema } from './models/DealProductSchema';
 | 
			
		||||
 
 | 
			
		||||
@@ -2,11 +2,12 @@
 | 
			
		||||
/* istanbul ignore file */
 | 
			
		||||
/* tslint:disable */
 | 
			
		||||
/* eslint-disable */
 | 
			
		||||
import type { BillPaymentInfo } from './BillPaymentInfo';
 | 
			
		||||
import type { BillPaymentStatus } from './BillPaymentStatus';
 | 
			
		||||
import type { NotificationChannel } from './NotificationChannel';
 | 
			
		||||
export type BillStatusUpdateRequest = {
 | 
			
		||||
    listenerTransactionId: number;
 | 
			
		||||
    channel: NotificationChannel;
 | 
			
		||||
    info: BillPaymentStatus;
 | 
			
		||||
    info: (BillPaymentInfo | BillPaymentStatus);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										9
									
								
								src/client/models/DealPrefillRequest.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/client/models/DealPrefillRequest.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
/* generated using openapi-typescript-codegen -- do not edit */
 | 
			
		||||
/* istanbul ignore file */
 | 
			
		||||
/* tslint:disable */
 | 
			
		||||
/* eslint-disable */
 | 
			
		||||
export type DealPrefillRequest = {
 | 
			
		||||
    oldDealId: number;
 | 
			
		||||
    newDealId: number;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -28,6 +28,8 @@ import type { DealDeleteServiceResponse } from '../models/DealDeleteServiceRespo
 | 
			
		||||
import type { DealDeleteServicesRequest } from '../models/DealDeleteServicesRequest';
 | 
			
		||||
import type { DealDeleteServicesResponse } from '../models/DealDeleteServicesResponse';
 | 
			
		||||
import type { DealGetAllResponse } from '../models/DealGetAllResponse';
 | 
			
		||||
import type { DealPrefillRequest } from '../models/DealPrefillRequest';
 | 
			
		||||
import type { DealPrefillResponse } from '../models/DealPrefillResponse';
 | 
			
		||||
import type { DealProductAddKitRequest } from '../models/DealProductAddKitRequest';
 | 
			
		||||
import type { DealProductAddKitResponse } from '../models/DealProductAddKitResponse';
 | 
			
		||||
import type { DealQuickCreateRequest } from '../models/DealQuickCreateRequest';
 | 
			
		||||
@@ -328,6 +330,26 @@ export class DealService {
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    /**
 | 
			
		||||
     * Post Prefill Deal
 | 
			
		||||
     * @returns DealPrefillResponse Successful Response
 | 
			
		||||
     * @throws ApiError
 | 
			
		||||
     */
 | 
			
		||||
    public static prefillDeal({
 | 
			
		||||
        requestBody,
 | 
			
		||||
    }: {
 | 
			
		||||
        requestBody: DealPrefillRequest,
 | 
			
		||||
    }): CancelablePromise<DealPrefillResponse> {
 | 
			
		||||
        return __request(OpenAPI, {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            url: '/deal/prefill',
 | 
			
		||||
            body: requestBody,
 | 
			
		||||
            mediaType: 'application/json',
 | 
			
		||||
            errors: {
 | 
			
		||||
                422: `Validation Error`,
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    /**
 | 
			
		||||
     * Services Add
 | 
			
		||||
     * @returns DealAddServicesResponse Successful Response
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ import CreateDealFrom from "../CreateDealForm/CreateDealFrom.tsx";
 | 
			
		||||
import { DealService } from "../../../client";
 | 
			
		||||
import { useQueryClient } from "@tanstack/react-query";
 | 
			
		||||
import { dateWithoutTimezone } from "../../../shared/lib/date.ts";
 | 
			
		||||
import { useDealPageContext } from "../../../pages/LeadsPage/contexts/DealPageContext.tsx";
 | 
			
		||||
 | 
			
		||||
type Props = {
 | 
			
		||||
    onClick: () => void;
 | 
			
		||||
@@ -14,6 +15,7 @@ const CreateDealButton: FC<Props> = () => {
 | 
			
		||||
    const [isCreating, setIsCreating] = useState(false);
 | 
			
		||||
    const [isTransitionEnded, setIsTransitionEnded] = useState(true);
 | 
			
		||||
    const queryClient = useQueryClient();
 | 
			
		||||
    const { prefillDeal, setPrefillDeal } = useDealPageContext();
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <div
 | 
			
		||||
@@ -34,6 +36,7 @@ const CreateDealButton: FC<Props> = () => {
 | 
			
		||||
                    <div style={styles}>
 | 
			
		||||
                        <CreateDealFrom
 | 
			
		||||
                            onCancel={() => {
 | 
			
		||||
                                setPrefillDeal(undefined);
 | 
			
		||||
                                setIsCreating(false);
 | 
			
		||||
                            }}
 | 
			
		||||
                            onSubmit={quickDeal => {
 | 
			
		||||
@@ -41,10 +44,18 @@ const CreateDealButton: FC<Props> = () => {
 | 
			
		||||
                                    requestBody: {
 | 
			
		||||
                                        ...quickDeal,
 | 
			
		||||
                                        acceptanceDate: dateWithoutTimezone(
 | 
			
		||||
                                            quickDeal.acceptanceDate
 | 
			
		||||
                                            quickDeal.acceptanceDate,
 | 
			
		||||
                                        ),
 | 
			
		||||
                                    },
 | 
			
		||||
                                }).then(async () => {
 | 
			
		||||
                                }).then(async (result) => {
 | 
			
		||||
                                    if (prefillDeal) {
 | 
			
		||||
                                        DealService.prefillDeal({
 | 
			
		||||
                                            requestBody: {
 | 
			
		||||
                                                oldDealId: prefillDeal.id,
 | 
			
		||||
                                                newDealId: result.dealId,
 | 
			
		||||
                                            },
 | 
			
		||||
                                        });
 | 
			
		||||
                                    }
 | 
			
		||||
                                    await queryClient.invalidateQueries({
 | 
			
		||||
                                        queryKey: ["getDealSummaries"],
 | 
			
		||||
                                    });
 | 
			
		||||
 
 | 
			
		||||
@@ -12,3 +12,8 @@
 | 
			
		||||
    display: flex;
 | 
			
		||||
    gap: rem(10);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.button-prefill {
 | 
			
		||||
    display: grid;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
}
 | 
			
		||||
@@ -8,12 +8,14 @@ import { DateTimePicker } from "@mantine/dates";
 | 
			
		||||
import ShippingWarehouseAutocomplete from "../../Selects/ShippingWarehouseAutocomplete/ShippingWarehouseAutocomplete.tsx";
 | 
			
		||||
import BaseMarketplaceSelect from "../../Selects/BaseMarketplaceSelect/BaseMarketplaceSelect.tsx";
 | 
			
		||||
import ServicePriceCategorySelect from "../../Selects/ServicePriceCategorySelect/ServicePriceCategorySelect.tsx";
 | 
			
		||||
import { useDealPageContext } from "../../../pages/LeadsPage/contexts/DealPageContext.tsx";
 | 
			
		||||
 | 
			
		||||
type Props = {
 | 
			
		||||
    onSubmit: (quickDeal: QuickDeal) => void;
 | 
			
		||||
    onCancel: () => void;
 | 
			
		||||
};
 | 
			
		||||
const CreateDealFrom: FC<Props> = ({ onSubmit, onCancel }) => {
 | 
			
		||||
    const { prefillOnOpen, prefillDeal } = useDealPageContext();
 | 
			
		||||
    const form = useForm<QuickDeal>({
 | 
			
		||||
        initialValues: {
 | 
			
		||||
            name: "",
 | 
			
		||||
@@ -29,6 +31,9 @@ const CreateDealFrom: FC<Props> = ({ onSubmit, onCancel }) => {
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const prefillButtonLabel = prefillDeal ? `Предзаполнено [ID: ${prefillDeal.id}]` : "Предзаполнить";
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <form
 | 
			
		||||
            style={{ width: "100%" }}
 | 
			
		||||
@@ -86,7 +91,14 @@ const CreateDealFrom: FC<Props> = ({ onSubmit, onCancel }) => {
 | 
			
		||||
                        {...form.getInputProps("acceptanceDate")}
 | 
			
		||||
                    />
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div className={styles["button-prefill"]}>
 | 
			
		||||
                    <Button
 | 
			
		||||
                        style={{ whiteSpace: "wrap" }}
 | 
			
		||||
                        variant={"outline"}
 | 
			
		||||
                        onClick={() => prefillOnOpen()}>
 | 
			
		||||
                        {prefillButtonLabel}
 | 
			
		||||
                    </Button>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div className={styles["buttons"]}>
 | 
			
		||||
                    <Button type={"submit"}>Добавить</Button>
 | 
			
		||||
                    <Button
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,19 @@
 | 
			
		||||
import { createContext, FC, useContext, useState } from "react";
 | 
			
		||||
import { DealSchema } from "../../../client";
 | 
			
		||||
import { createContext, Dispatch, FC, SetStateAction, useContext, useState } from "react";
 | 
			
		||||
import { DealSchema, DealService } from "../../../client";
 | 
			
		||||
import { useDisclosure } from "@mantine/hooks";
 | 
			
		||||
 | 
			
		||||
type DealPageContextState = {
 | 
			
		||||
    selectedDeal?: DealSchema;
 | 
			
		||||
    setSelectedDeal: (deal: DealSchema | undefined) => void;
 | 
			
		||||
 | 
			
		||||
    prefillOpened: boolean;
 | 
			
		||||
    prefillOnClose: () => void;
 | 
			
		||||
    prefillOnOpen: () => void;
 | 
			
		||||
 | 
			
		||||
    selectedPrefillDeal?: DealSchema;
 | 
			
		||||
    selectPrefillDeal: (dealId: number) => void;
 | 
			
		||||
    prefillDeal?: DealSchema;
 | 
			
		||||
    setPrefillDeal: Dispatch<SetStateAction<DealSchema | undefined>>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const DealPageContext = createContext<DealPageContextState | undefined>(
 | 
			
		||||
@@ -13,7 +23,35 @@ const useDealPageContextState = () => {
 | 
			
		||||
    const [selectedDeal, setSelectedDeal] = useState<DealSchema | undefined>(
 | 
			
		||||
        undefined
 | 
			
		||||
    );
 | 
			
		||||
    return { selectedDeal, setSelectedDeal };
 | 
			
		||||
    const [selectedPrefillDeal, setSelectedPrefillDeal] = useState<DealSchema | undefined>(
 | 
			
		||||
        undefined,
 | 
			
		||||
    );
 | 
			
		||||
    const [prefillDeal, setPrefillDeal] = useState<DealSchema | undefined>(
 | 
			
		||||
        undefined,
 | 
			
		||||
    );
 | 
			
		||||
    const [prefillOpened, { open, close }] = useDisclosure(false);
 | 
			
		||||
    const prefillOnClose = close;
 | 
			
		||||
    const prefillOnOpen = open;
 | 
			
		||||
 | 
			
		||||
    const selectPrefillDeal = (dealId: number) => {
 | 
			
		||||
        DealService.getDealById({ dealId }).then(deal => {
 | 
			
		||||
            setSelectedPrefillDeal(deal);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        selectedDeal,
 | 
			
		||||
        setSelectedDeal,
 | 
			
		||||
 | 
			
		||||
        prefillOpened,
 | 
			
		||||
        prefillOnClose,
 | 
			
		||||
        prefillOnOpen,
 | 
			
		||||
 | 
			
		||||
        selectedPrefillDeal,
 | 
			
		||||
        selectPrefillDeal,
 | 
			
		||||
        prefillDeal,
 | 
			
		||||
        setPrefillDeal,
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type DealPageContextProviderProps = {
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,33 @@
 | 
			
		||||
.container {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
    gap: rem(10);
 | 
			
		||||
    max-height: 95vh;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.deal-container {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    gap: rem(10);
 | 
			
		||||
    flex: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.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%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.top-panel {
 | 
			
		||||
    padding-bottom: rem(9);
 | 
			
		||||
    gap: rem(10);
 | 
			
		||||
    display: flex;
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,82 @@
 | 
			
		||||
import { FC, useEffect } from "react";
 | 
			
		||||
import { Button, Drawer, Flex, rem, TextInput } from "@mantine/core";
 | 
			
		||||
import { useDealPageContext } from "../../contexts/DealPageContext.tsx";
 | 
			
		||||
import DealsTable from "./components/tables/DealsTable/DealsTable.tsx";
 | 
			
		||||
import Preview from "./components/Preview/Preview.tsx";
 | 
			
		||||
import styles from "./DealPrefillDrawer.module.css";
 | 
			
		||||
import BaseMarketplaceSelect from "../../../../components/Selects/BaseMarketplaceSelect/BaseMarketplaceSelect.tsx";
 | 
			
		||||
import usePrefillDeal from "./hooks/usePrefillDeal.tsx";
 | 
			
		||||
import { notifications } from "../../../../shared/lib/notifications.ts";
 | 
			
		||||
 | 
			
		||||
const DealPrefillDrawer: FC = () => {
 | 
			
		||||
    const { prefillOpened, prefillOnClose, selectedPrefillDeal, setPrefillDeal, prefillDeal } = useDealPageContext();
 | 
			
		||||
    const { data, form } = usePrefillDeal();
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (prefillOpened) return;
 | 
			
		||||
    }, [prefillOpened]);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <Drawer
 | 
			
		||||
            size={"calc(77vw)"}
 | 
			
		||||
            position={"right"}
 | 
			
		||||
            onClose={prefillOnClose}
 | 
			
		||||
            removeScrollProps={{ allowPinchZoom: true }}
 | 
			
		||||
            withCloseButton={false}
 | 
			
		||||
            opened={prefillOpened}
 | 
			
		||||
            styles={{
 | 
			
		||||
                body: {
 | 
			
		||||
                    height: "100%",
 | 
			
		||||
                    display: "flex",
 | 
			
		||||
                    flexDirection: "row",
 | 
			
		||||
                    gap: rem(20),
 | 
			
		||||
                },
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
            <div className={styles["deal-container"]}>
 | 
			
		||||
                <div className={styles["deal-container-wrapper"]}>
 | 
			
		||||
                    <div className={styles["top-panel"]}>
 | 
			
		||||
                        <TextInput
 | 
			
		||||
                            placeholder={"Введите название / id"}
 | 
			
		||||
                            {...form.getInputProps("idOrName")}
 | 
			
		||||
                        />
 | 
			
		||||
                        <BaseMarketplaceSelect
 | 
			
		||||
                            onClear={() =>
 | 
			
		||||
                                form.setFieldValue("marketplace", null)
 | 
			
		||||
                            }
 | 
			
		||||
                            clearable
 | 
			
		||||
                            placeholder={"Выберите маркетплейс"}
 | 
			
		||||
                            {...form.getInputProps("marketplace")}
 | 
			
		||||
                        />
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <DealsTable items={data} />
 | 
			
		||||
                    <Flex direction={"row"} gap="sm">
 | 
			
		||||
                        <Button mt={10} w={"100%"} onClick={() => {
 | 
			
		||||
                            if (!selectedPrefillDeal) {
 | 
			
		||||
                                notifications.error({ message: "Сделка не выбрана." });
 | 
			
		||||
                                return;
 | 
			
		||||
                            }
 | 
			
		||||
                            setPrefillDeal(selectedPrefillDeal);
 | 
			
		||||
                            prefillOnClose();
 | 
			
		||||
                        }}>
 | 
			
		||||
                            Предзаполнить
 | 
			
		||||
                        </Button>
 | 
			
		||||
                        {
 | 
			
		||||
                            prefillDeal &&
 | 
			
		||||
                            <Button mt={10} w={"100%"} variant={"outline"} onClick={() => {
 | 
			
		||||
                                setPrefillDeal(undefined);
 | 
			
		||||
                                notifications.success({ message: "Предзаполнение отменено." });
 | 
			
		||||
                                prefillOnClose();
 | 
			
		||||
                            }}>
 | 
			
		||||
                                Отменить предзаполнение
 | 
			
		||||
                            </Button>
 | 
			
		||||
                        }
 | 
			
		||||
                    </Flex>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <Preview />
 | 
			
		||||
        </Drawer>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default DealPrefillDrawer;
 | 
			
		||||
@@ -0,0 +1,34 @@
 | 
			
		||||
.container {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    gap: rem(10);
 | 
			
		||||
    max-height: 95vh;
 | 
			
		||||
    width: 50%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.products-list {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    gap: rem(10);
 | 
			
		||||
    flex: 2;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.deal-container {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    gap: rem(10);
 | 
			
		||||
    flex: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.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,56 @@
 | 
			
		||||
import { FC } from "react";
 | 
			
		||||
import styles from "./Preview.module.css";
 | 
			
		||||
import { ScrollArea, Skeleton, Title } from "@mantine/core";
 | 
			
		||||
import { useDealPageContext } from "../../../../contexts/DealPageContext.tsx";
 | 
			
		||||
import DealServicesTable from "../tables/DealServicesTable/DealServicesTable.tsx";
 | 
			
		||||
import ProductPreview from "../ProductPreview/ProductPreview.tsx";
 | 
			
		||||
 | 
			
		||||
const Preview: FC = () => {
 | 
			
		||||
    const { selectedPrefillDeal } = useDealPageContext();
 | 
			
		||||
 | 
			
		||||
    const getTotalPrice = () => {
 | 
			
		||||
        if (!selectedPrefillDeal) return 0;
 | 
			
		||||
        const productServicesPrice = selectedPrefillDeal.products.reduce(
 | 
			
		||||
            (acc, row) =>
 | 
			
		||||
                acc +
 | 
			
		||||
                row.services.reduce(
 | 
			
		||||
                    (acc2, row2) => acc2 + row2.price * row.quantity,
 | 
			
		||||
                    0,
 | 
			
		||||
                ),
 | 
			
		||||
            0,
 | 
			
		||||
        );
 | 
			
		||||
        const dealServicesPrice = selectedPrefillDeal.services.reduce(
 | 
			
		||||
            (acc, row) => acc + row.price * row.quantity,
 | 
			
		||||
            0,
 | 
			
		||||
        );
 | 
			
		||||
        return dealServicesPrice + productServicesPrice;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <div className={styles["container"]}>
 | 
			
		||||
            <ScrollArea offsetScrollbars={"y"} w={"100%"}>
 | 
			
		||||
                <Skeleton visible={!selectedPrefillDeal}>
 | 
			
		||||
                    <div className={styles["deal-container-wrapper"]}>
 | 
			
		||||
                        <Title order={4} mb={18}>
 | 
			
		||||
                            Общая стоимость всех услуг:{" "}
 | 
			
		||||
                            {getTotalPrice().toLocaleString("ru")}₽
 | 
			
		||||
                        </Title>
 | 
			
		||||
 | 
			
		||||
                        <DealServicesTable items={selectedPrefillDeal?.services} />
 | 
			
		||||
 | 
			
		||||
                        <div className={styles["products-list"]}>
 | 
			
		||||
                            {selectedPrefillDeal?.products.map(product => (
 | 
			
		||||
                                <ProductPreview
 | 
			
		||||
                                    key={product.product.id}
 | 
			
		||||
                                    product={product}
 | 
			
		||||
                                />
 | 
			
		||||
                            ))}
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </Skeleton>
 | 
			
		||||
            </ScrollArea>
 | 
			
		||||
        </div>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Preview;
 | 
			
		||||
@@ -0,0 +1,33 @@
 | 
			
		||||
.container {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    gap: rem(20);
 | 
			
		||||
    margin-bottom: rem(10);
 | 
			
		||||
    flex: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.image-container {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    max-height: rem(250);
 | 
			
		||||
    max-width: rem(250);
 | 
			
		||||
    height: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.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);
 | 
			
		||||
    flex: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.attributes-container {
 | 
			
		||||
    overflow-wrap: break-word;
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,55 @@
 | 
			
		||||
import { FC } from "react";
 | 
			
		||||
import { DealProductSchema, ProductSchema } from "../../../../../../client";
 | 
			
		||||
import { Image, rem, Text, Title } from "@mantine/core";
 | 
			
		||||
import { isNil } from "lodash";
 | 
			
		||||
import { ProductFieldNames } from "../../../../tabs/ProductAndServiceTab/components/ProductView/ProductView.tsx";
 | 
			
		||||
import ProductServicesTable from "../tables/ProductServicesTable/ProductServicesTable.tsx";
 | 
			
		||||
import styles from "./ProductPreview.module.css";
 | 
			
		||||
 | 
			
		||||
type Props = {
 | 
			
		||||
    product: DealProductSchema;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const ProductPreview: FC<Props> = ({ product }) => {
 | 
			
		||||
    return (
 | 
			
		||||
        <div className={styles["container"]}>
 | 
			
		||||
            <div className={styles["data-container"]}>
 | 
			
		||||
                <div className={styles["image-container"]}>
 | 
			
		||||
                    <Image
 | 
			
		||||
                        flex={1}
 | 
			
		||||
                        radius={rem(10)}
 | 
			
		||||
                        fit={"cover"}
 | 
			
		||||
                        src={product.product.imageUrl}
 | 
			
		||||
                    />
 | 
			
		||||
                </div>
 | 
			
		||||
                <div className={styles["attributes-container"]}>
 | 
			
		||||
                    <Title order={3}>{product.product.name}</Title>
 | 
			
		||||
 | 
			
		||||
                    {Object.entries(product.product).map(([key, value]) => {
 | 
			
		||||
                        const fieldName =
 | 
			
		||||
                            ProductFieldNames[key as keyof ProductSchema];
 | 
			
		||||
                        if (!fieldName || isNil(value) || value === "") return;
 | 
			
		||||
                        return (
 | 
			
		||||
                            <Text key={fieldName}>
 | 
			
		||||
                                {fieldName}: {value.toString()}{" "}
 | 
			
		||||
                            </Text>
 | 
			
		||||
                        );
 | 
			
		||||
                    })}
 | 
			
		||||
                    <Text>
 | 
			
		||||
                        Штрихкоды: {product.product.barcodes.join(", ")}
 | 
			
		||||
                    </Text>
 | 
			
		||||
                    <Text>Количество товара: {product.quantity}</Text>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div className={styles["services-container"]}>
 | 
			
		||||
                <ProductServicesTable
 | 
			
		||||
                    items={product.services}
 | 
			
		||||
                    quantity={product.quantity}
 | 
			
		||||
                />
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default ProductPreview;
 | 
			
		||||
@@ -0,0 +1,59 @@
 | 
			
		||||
import { FC } from "react";
 | 
			
		||||
import { Flex, rem, Title } from "@mantine/core";
 | 
			
		||||
import { DealServiceSchema, DealSummary } from "../../../../../../../client";
 | 
			
		||||
import useDealServicesTableColumns from "./columns.tsx";
 | 
			
		||||
import { BaseTable } from "../../../../../../../components/BaseTable/BaseTable.tsx";
 | 
			
		||||
import { MRT_TableOptions } from "mantine-react-table";
 | 
			
		||||
 | 
			
		||||
type Props = {
 | 
			
		||||
    items?: DealServiceSchema[];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const DealServicesTable: FC<Props> = ({ items }) => {
 | 
			
		||||
    const columns = useDealServicesTableColumns();
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <Flex
 | 
			
		||||
            direction={"column"}
 | 
			
		||||
            gap={rem(10)}
 | 
			
		||||
            h={"100%"}
 | 
			
		||||
            mb={10}>
 | 
			
		||||
            <Flex
 | 
			
		||||
                h={"100%"}
 | 
			
		||||
                direction={"column"}>
 | 
			
		||||
                {
 | 
			
		||||
                    items && items.length > 0 &&
 | 
			
		||||
                    <>
 | 
			
		||||
                        <BaseTable
 | 
			
		||||
                            data={items}
 | 
			
		||||
                            columns={columns}
 | 
			
		||||
 | 
			
		||||
                            restProps={
 | 
			
		||||
                                {
 | 
			
		||||
                                    enableSorting: false,
 | 
			
		||||
                                    enableColumnActions: false,
 | 
			
		||||
                                    enablePagination: false,
 | 
			
		||||
                                    enableBottomToolbar: false,
 | 
			
		||||
                                } as MRT_TableOptions<DealSummary>
 | 
			
		||||
                            }
 | 
			
		||||
                        />
 | 
			
		||||
 | 
			
		||||
                        <Title
 | 
			
		||||
                            style={{ textAlign: "end" }}
 | 
			
		||||
                            mt={rem(10)}
 | 
			
		||||
                            mb={rem(6)}
 | 
			
		||||
                            order={4}>
 | 
			
		||||
                            Итог:{" "}
 | 
			
		||||
                            {items.reduce(
 | 
			
		||||
                                (acc, item) => acc + item.price * item.quantity,
 | 
			
		||||
                                0,
 | 
			
		||||
                            )}
 | 
			
		||||
                            ₽
 | 
			
		||||
                        </Title>
 | 
			
		||||
                    </>
 | 
			
		||||
                }
 | 
			
		||||
            </Flex>
 | 
			
		||||
        </Flex>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
export default DealServicesTable;
 | 
			
		||||
@@ -0,0 +1,30 @@
 | 
			
		||||
import { useMemo } from "react";
 | 
			
		||||
import { MRT_ColumnDef } from "mantine-react-table";
 | 
			
		||||
import { DealServiceSchema } from "../../../../../../../client";
 | 
			
		||||
 | 
			
		||||
const useDealServicesTableColumns = () => {
 | 
			
		||||
    return useMemo<MRT_ColumnDef<DealServiceSchema>[]>(
 | 
			
		||||
        () => [
 | 
			
		||||
            {
 | 
			
		||||
                header: "Название",
 | 
			
		||||
                accessorKey: "service.name",
 | 
			
		||||
                size: 450,
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                header: "Количество",
 | 
			
		||||
                accessorKey: "quantity",
 | 
			
		||||
                size: 50,
 | 
			
		||||
                Cell: ({ cell }) => cell.getValue() + " шт.",
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                accessorKey: "price",
 | 
			
		||||
                header: "Цена",
 | 
			
		||||
                size: 50,
 | 
			
		||||
                Cell: ({ cell }) => cell.getValue() + " ₽",
 | 
			
		||||
            },
 | 
			
		||||
        ],
 | 
			
		||||
        []
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default useDealServicesTableColumns;
 | 
			
		||||
@@ -0,0 +1,45 @@
 | 
			
		||||
import { FC, useEffect } from "react";
 | 
			
		||||
import useDealsTableColumns from "./columns.tsx";
 | 
			
		||||
import { DealSummary } from "../../../../../../../client";
 | 
			
		||||
import { useDealPageContext } from "../../../../../contexts/DealPageContext.tsx";
 | 
			
		||||
import { BaseTable } from "../../../../../../../components/BaseTable/BaseTable.tsx";
 | 
			
		||||
 | 
			
		||||
type Props = {
 | 
			
		||||
    items: DealSummary[];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const DealsTable: FC<Props> = ({ items }) => {
 | 
			
		||||
    const { selectPrefillDeal } = useDealPageContext();
 | 
			
		||||
    const columns = useDealsTableColumns();
 | 
			
		||||
    const defaultSorting = [{ id: "createdAt", desc: false }];
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (items.length < 1) return;
 | 
			
		||||
        selectPrefillDeal(items[0].id);
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <BaseTable
 | 
			
		||||
            data={items}
 | 
			
		||||
            columns={columns}
 | 
			
		||||
 | 
			
		||||
            restProps={
 | 
			
		||||
                {
 | 
			
		||||
                    enableSorting: true,
 | 
			
		||||
                    enableColumnActions: false,
 | 
			
		||||
                    enablePagination: true,
 | 
			
		||||
                    enableBottomToolbar: true,
 | 
			
		||||
                    paginationDisplayMode: "pages",
 | 
			
		||||
                    initialState: {
 | 
			
		||||
                        sorting: defaultSorting,
 | 
			
		||||
                    },
 | 
			
		||||
                    mantinePaginationProps: {
 | 
			
		||||
                        showRowsPerPage: false,
 | 
			
		||||
                    },
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        />
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default DealsTable;
 | 
			
		||||
@@ -0,0 +1,72 @@
 | 
			
		||||
import { useMemo } from "react";
 | 
			
		||||
import { MRT_ColumnDef } from "mantine-react-table";
 | 
			
		||||
import { ActionIcon, Image, Radio } from "@mantine/core";
 | 
			
		||||
import { DealSummary } from "../../../../../../../client";
 | 
			
		||||
import { useDealPageContext } from "../../../../../contexts/DealPageContext.tsx";
 | 
			
		||||
 | 
			
		||||
const useDealsTableColumns = () => {
 | 
			
		||||
    return useMemo<MRT_ColumnDef<DealSummary>[]>(
 | 
			
		||||
        () => [
 | 
			
		||||
            {
 | 
			
		||||
                accessorKey: "select",
 | 
			
		||||
                header: "",
 | 
			
		||||
                size: 5,
 | 
			
		||||
                enableSorting: false,
 | 
			
		||||
                Cell: ({ row }) => {
 | 
			
		||||
                    const { selectPrefillDeal, selectedPrefillDeal } = useDealPageContext();
 | 
			
		||||
                    const checked = row.original.id === selectedPrefillDeal?.id;
 | 
			
		||||
                    return (
 | 
			
		||||
                        <Radio
 | 
			
		||||
                            checked={checked}
 | 
			
		||||
                            onChange={() => {
 | 
			
		||||
                                selectPrefillDeal(row.original.id);
 | 
			
		||||
                            }}
 | 
			
		||||
                        />
 | 
			
		||||
                    );
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                accessorKey: "id",
 | 
			
		||||
                header: "ID",
 | 
			
		||||
                size: 20,
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                accessorKey: "clientName",
 | 
			
		||||
                header: "Клиент",
 | 
			
		||||
                size: 60,
 | 
			
		||||
                enableSorting: false,
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                accessorKey: "name",
 | 
			
		||||
                header: "Название",
 | 
			
		||||
                enableSorting: false,
 | 
			
		||||
                size: 60,
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                header: "Дата создания",
 | 
			
		||||
                accessorKey: "createdAt",
 | 
			
		||||
                size: 10,
 | 
			
		||||
                Cell: ({ row }) =>
 | 
			
		||||
                    new Date(row.original.createdAt).toLocaleString("ru-RU").substring(0, 17),
 | 
			
		||||
                enableSorting: true,
 | 
			
		||||
                sortingFn: (rowA, rowB) =>
 | 
			
		||||
                    new Date(rowB.original.createdAt).getTime() -
 | 
			
		||||
                    new Date(rowA.original.createdAt).getTime(),
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                header: "МП",
 | 
			
		||||
                size: 5,
 | 
			
		||||
                Cell: ({ row }) => (
 | 
			
		||||
                    <ActionIcon variant={"transparent"}>
 | 
			
		||||
                        <Image
 | 
			
		||||
                            src={row.original.baseMarketplace?.iconUrl || ""}
 | 
			
		||||
                        />
 | 
			
		||||
                    </ActionIcon>
 | 
			
		||||
                ),
 | 
			
		||||
            },
 | 
			
		||||
        ],
 | 
			
		||||
        [],
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default useDealsTableColumns;
 | 
			
		||||
@@ -0,0 +1,31 @@
 | 
			
		||||
import { FC } from "react";
 | 
			
		||||
import { MRT_TableOptions } from "mantine-react-table";
 | 
			
		||||
import { DealProductServiceSchema } from "../../../../../../../client";
 | 
			
		||||
import { BaseTable } from "../../../../../../../components/BaseTable/BaseTable.tsx";
 | 
			
		||||
import useProductServicesTableColumns from "./columns.tsx";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
type Props = {
 | 
			
		||||
    items: DealProductServiceSchema[];
 | 
			
		||||
    quantity: number;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const ProductServicesTable: FC<Props> = ({ items, quantity }) => {
 | 
			
		||||
    const columns = useProductServicesTableColumns({ data: items, quantity });
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <BaseTable
 | 
			
		||||
            data={items}
 | 
			
		||||
            columns={columns}
 | 
			
		||||
            restProps={
 | 
			
		||||
                {
 | 
			
		||||
                    enableColumnActions: false,
 | 
			
		||||
                    enableSorting: false,
 | 
			
		||||
                    enableRowActions: false,
 | 
			
		||||
                    enableBottomToolbar: false,
 | 
			
		||||
                } as MRT_TableOptions<DealProductServiceSchema>
 | 
			
		||||
            }
 | 
			
		||||
        />
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
export default ProductServicesTable;
 | 
			
		||||
@@ -0,0 +1,44 @@
 | 
			
		||||
import { useMemo } from "react";
 | 
			
		||||
import { MRT_ColumnDef } from "mantine-react-table";
 | 
			
		||||
import { DealProductServiceSchema } from "../../../../../../../client";
 | 
			
		||||
import { useSelector } from "react-redux";
 | 
			
		||||
import { RootState } from "../../../../../../../redux/store.ts";
 | 
			
		||||
 | 
			
		||||
type Props = {
 | 
			
		||||
    data: DealProductServiceSchema[];
 | 
			
		||||
    quantity: number;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const useProductServicesTableColumns = (props: Props) => {
 | 
			
		||||
    const { data, quantity } = props;
 | 
			
		||||
    const authState = useSelector((state: RootState) => state.auth);
 | 
			
		||||
    const totalPrice = useMemo(
 | 
			
		||||
        () => data.reduce((acc, row) => acc + row.price * quantity, 0),
 | 
			
		||||
        [data, quantity]
 | 
			
		||||
    );
 | 
			
		||||
    const hideGuestColumns = ["service.cost"];
 | 
			
		||||
    return useMemo<MRT_ColumnDef<DealProductServiceSchema>[]>(
 | 
			
		||||
        () => [
 | 
			
		||||
            {
 | 
			
		||||
                accessorKey: "service.name",
 | 
			
		||||
                header: "Услуга",
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                accessorKey: "price",
 | 
			
		||||
                header: "Цена",
 | 
			
		||||
                size: 5,
 | 
			
		||||
                Cell: ({ cell }) => cell.getValue() + " ₽",
 | 
			
		||||
                Footer: () => <>Итог: {totalPrice.toLocaleString("ru")}₽</>,
 | 
			
		||||
            },
 | 
			
		||||
        ],
 | 
			
		||||
        [totalPrice]
 | 
			
		||||
    ).filter(
 | 
			
		||||
        columnDef =>
 | 
			
		||||
            !(
 | 
			
		||||
                hideGuestColumns.includes(columnDef.accessorKey || "") &&
 | 
			
		||||
                authState.isGuest
 | 
			
		||||
            )
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default useProductServicesTableColumns;
 | 
			
		||||
@@ -0,0 +1,52 @@
 | 
			
		||||
import { useForm } from "@mantine/form";
 | 
			
		||||
import { useEffect, useState } from "react";
 | 
			
		||||
import { BaseMarketplaceSchema } from "../../../../../client";
 | 
			
		||||
import { useDealSummariesFull } from "../../../hooks/useDealSummaries.tsx";
 | 
			
		||||
 | 
			
		||||
type State = {
 | 
			
		||||
    idOrName: string | null;
 | 
			
		||||
    marketplace: BaseMarketplaceSchema | null;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const usePrefillDeal = () => {
 | 
			
		||||
    const { objects } = useDealSummariesFull();
 | 
			
		||||
    const form = useForm<State>({
 | 
			
		||||
        initialValues: {
 | 
			
		||||
            idOrName: null,
 | 
			
		||||
            marketplace: null,
 | 
			
		||||
        },
 | 
			
		||||
    });
 | 
			
		||||
    const [data, setData] = useState(objects);
 | 
			
		||||
 | 
			
		||||
    const applyFilters = () => {
 | 
			
		||||
        let result = objects;
 | 
			
		||||
        if (form.values.idOrName) {
 | 
			
		||||
            if (isNaN(parseInt(form.values.idOrName))) {
 | 
			
		||||
                const name: string = form.values.idOrName.toLowerCase();
 | 
			
		||||
                result = result.filter(
 | 
			
		||||
                    obj => obj.name.toLowerCase().search(name) !== -1,
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
            else {
 | 
			
		||||
                const id = parseInt(form.values.idOrName);
 | 
			
		||||
                result = result.filter(
 | 
			
		||||
                    obj => obj.id === id,
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if (form.values.marketplace) {
 | 
			
		||||
            result = result.filter(
 | 
			
		||||
                obj => obj.baseMarketplace?.key === form.values.marketplace?.key,
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
        setData(result);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        applyFilters();
 | 
			
		||||
    }, [form.values, objects]);
 | 
			
		||||
 | 
			
		||||
    return { data, form };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default usePrefillDeal;
 | 
			
		||||
@@ -20,6 +20,7 @@ import ClientSelectNew from "../../../components/Selects/ClientSelectNew/ClientS
 | 
			
		||||
import DealsTable from "../../DealsPage/components/DealsTable/DealsTable.tsx";
 | 
			
		||||
import { motion } from "framer-motion";
 | 
			
		||||
import { dateWithoutTimezone } from "../../../shared/lib/date.ts";
 | 
			
		||||
import DealPrefillDrawer from "../drawers/DealPrefillDrawer/DealPrefillDrawer.tsx";
 | 
			
		||||
 | 
			
		||||
enum DisplayMode {
 | 
			
		||||
    BOARD,
 | 
			
		||||
@@ -410,6 +411,7 @@ export const LeadsPage: FC = () => {
 | 
			
		||||
                    {getBody()}
 | 
			
		||||
                </PageBlock>
 | 
			
		||||
                <DealEditDrawer />
 | 
			
		||||
                <DealPrefillDrawer />
 | 
			
		||||
            </DealPageContextProvider>
 | 
			
		||||
        </PageBlock>
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user