diff --git a/src/client/index.ts b/src/client/index.ts index e1c1970..927dbcb 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -30,12 +30,14 @@ export type { BaseShippingWarehouseSchema } from './models/BaseShippingWarehouse export type { BaseTransactionTagSchema } from './models/BaseTransactionTagSchema'; export type { BillPaymentStatus } from './models/BillPaymentStatus'; export type { BillStatusUpdateRequest } from './models/BillStatusUpdateRequest'; +export type { Body_parse_deals_excel } from './models/Body_parse_deals_excel'; export type { Body_upload_passport_image } from './models/Body_upload_passport_image'; export type { Body_upload_product_barcode_image } from './models/Body_upload_product_barcode_image'; export type { Body_upload_product_image } from './models/Body_upload_product_image'; export type { BoxSchema } from './models/BoxSchema'; export type { CancelDealBillRequest } from './models/CancelDealBillRequest'; export type { CancelDealBillResponse } from './models/CancelDealBillResponse'; +export type { CityBreakdownFromExcelSchema } from './models/CityBreakdownFromExcelSchema'; export type { ClientCreateRequest } from './models/ClientCreateRequest'; export type { ClientCreateResponse } from './models/ClientCreateResponse'; export type { ClientDeleteRequest } from './models/ClientDeleteRequest'; @@ -52,6 +54,8 @@ export type { CreateBoxInDealSchema } from './models/CreateBoxInDealSchema'; export type { CreateBoxInPalletSchema } from './models/CreateBoxInPalletSchema'; export type { CreateDealBillRequest } from './models/CreateDealBillRequest'; export type { CreateDealBillResponse } from './models/CreateDealBillResponse'; +export type { CreateDealsFromExcelRequest } from './models/CreateDealsFromExcelRequest'; +export type { CreateDealsFromExcelResponse } from './models/CreateDealsFromExcelResponse'; export type { CreateDepartmentRequest } from './models/CreateDepartmentRequest'; export type { CreateDepartmentResponse } from './models/CreateDepartmentResponse'; export type { CreateDepartmentSectionRequest } from './models/CreateDepartmentSectionRequest'; @@ -222,8 +226,12 @@ export type { ManageEmployeeResponse } from './models/ManageEmployeeResponse'; export type { MarketplaceCreateSchema } from './models/MarketplaceCreateSchema'; export type { MarketplaceSchema } from './models/MarketplaceSchema'; export type { NotificationChannel } from './models/NotificationChannel'; +export type { OptionalShippingWarehouseSchema } from './models/OptionalShippingWarehouseSchema'; export type { PaginationInfoSchema } from './models/PaginationInfoSchema'; export type { PalletSchema } from './models/PalletSchema'; +export type { ParsedCityBreakdownSchema } from './models/ParsedCityBreakdownSchema'; +export type { ParseDealsExcelResponse } from './models/ParseDealsExcelResponse'; +export type { ParsedProductRowSchema } from './models/ParsedProductRowSchema'; export type { PassportImageSchema } from './models/PassportImageSchema'; export type { PaymentRecordCreateSchema } from './models/PaymentRecordCreateSchema'; export type { PaymentRecordGetSchema } from './models/PaymentRecordGetSchema'; @@ -240,6 +248,7 @@ export type { ProductDeleteBarcodeImageResponse } from './models/ProductDeleteBa export type { ProductDeleteRequest } from './models/ProductDeleteRequest'; export type { ProductDeleteResponse } from './models/ProductDeleteResponse'; export type { ProductExistsBarcodeResponse } from './models/ProductExistsBarcodeResponse'; +export type { ProductFromExcelSchema } from './models/ProductFromExcelSchema'; export type { ProductGenerateBarcodeRequest } from './models/ProductGenerateBarcodeRequest'; export type { ProductGenerateBarcodeResponse } from './models/ProductGenerateBarcodeResponse'; export type { ProductGetBarcodeImageResponse } from './models/ProductGetBarcodeImageResponse'; diff --git a/src/client/models/Body_parse_deals_excel.ts b/src/client/models/Body_parse_deals_excel.ts new file mode 100644 index 0000000..ecfa04c --- /dev/null +++ b/src/client/models/Body_parse_deals_excel.ts @@ -0,0 +1,8 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type Body_parse_deals_excel = { + upload_file: Blob; +}; + diff --git a/src/client/models/CityBreakdownFromExcelSchema.ts b/src/client/models/CityBreakdownFromExcelSchema.ts new file mode 100644 index 0000000..e2a7fd2 --- /dev/null +++ b/src/client/models/CityBreakdownFromExcelSchema.ts @@ -0,0 +1,12 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { BaseMarketplaceSchema } from './BaseMarketplaceSchema'; +import type { OptionalShippingWarehouseSchema } from './OptionalShippingWarehouseSchema'; +export type CityBreakdownFromExcelSchema = { + baseMarketplace: BaseMarketplaceSchema; + shippingWarehouse: OptionalShippingWarehouseSchema; + quantity: number; +}; + diff --git a/src/client/models/CreateDealsFromExcelRequest.ts b/src/client/models/CreateDealsFromExcelRequest.ts new file mode 100644 index 0000000..cbfe3fe --- /dev/null +++ b/src/client/models/CreateDealsFromExcelRequest.ts @@ -0,0 +1,10 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { ProductFromExcelSchema } from './ProductFromExcelSchema'; +export type CreateDealsFromExcelRequest = { + clientId: number; + products: Array; +}; + diff --git a/src/client/models/CreateDealsFromExcelResponse.ts b/src/client/models/CreateDealsFromExcelResponse.ts new file mode 100644 index 0000000..5718e1d --- /dev/null +++ b/src/client/models/CreateDealsFromExcelResponse.ts @@ -0,0 +1,9 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type CreateDealsFromExcelResponse = { + ok: boolean; + message: string; +}; + diff --git a/src/client/models/OptionalShippingWarehouseSchema.ts b/src/client/models/OptionalShippingWarehouseSchema.ts new file mode 100644 index 0000000..f6d6bc4 --- /dev/null +++ b/src/client/models/OptionalShippingWarehouseSchema.ts @@ -0,0 +1,9 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type OptionalShippingWarehouseSchema = { + name: string; + id?: (number | null); +}; + diff --git a/src/client/models/ParseDealsExcelResponse.ts b/src/client/models/ParseDealsExcelResponse.ts new file mode 100644 index 0000000..0615eac --- /dev/null +++ b/src/client/models/ParseDealsExcelResponse.ts @@ -0,0 +1,10 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { ParsedProductRowSchema } from './ParsedProductRowSchema'; +export type ParseDealsExcelResponse = { + rows: Array; + errors: Array; +}; + diff --git a/src/client/models/ParsedCityBreakdownSchema.ts b/src/client/models/ParsedCityBreakdownSchema.ts new file mode 100644 index 0000000..9936241 --- /dev/null +++ b/src/client/models/ParsedCityBreakdownSchema.ts @@ -0,0 +1,12 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { BaseMarketplaceSchema } from './BaseMarketplaceSchema'; +import type { OptionalShippingWarehouseSchema } from './OptionalShippingWarehouseSchema'; +export type ParsedCityBreakdownSchema = { + baseMarketplace: BaseMarketplaceSchema; + shippingWarehouse: OptionalShippingWarehouseSchema; + quantity: number; +}; + diff --git a/src/client/models/ParsedProductRowSchema.ts b/src/client/models/ParsedProductRowSchema.ts new file mode 100644 index 0000000..ba08aae --- /dev/null +++ b/src/client/models/ParsedProductRowSchema.ts @@ -0,0 +1,12 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { ParsedCityBreakdownSchema } from './ParsedCityBreakdownSchema'; +import type { ProductSchema } from './ProductSchema'; +export type ParsedProductRowSchema = { + barcode: string; + products: Array; + breakdowns: Array; +}; + diff --git a/src/client/models/ProductFromExcelSchema.ts b/src/client/models/ProductFromExcelSchema.ts new file mode 100644 index 0000000..9bd1901 --- /dev/null +++ b/src/client/models/ProductFromExcelSchema.ts @@ -0,0 +1,10 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { CityBreakdownFromExcelSchema } from './CityBreakdownFromExcelSchema'; +export type ProductFromExcelSchema = { + productId: number; + citiesBreakdown: Array; +}; + diff --git a/src/client/services/DealService.ts b/src/client/services/DealService.ts index c7adbfc..6687887 100644 --- a/src/client/services/DealService.ts +++ b/src/client/services/DealService.ts @@ -2,6 +2,9 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ +import type { Body_parse_deals_excel } from '../models/Body_parse_deals_excel'; +import type { CreateDealsFromExcelRequest } from '../models/CreateDealsFromExcelRequest'; +import type { CreateDealsFromExcelResponse } from '../models/CreateDealsFromExcelResponse'; import type { DealAddKitRequest } from '../models/DealAddKitRequest'; import type { DealAddKitResponse } from '../models/DealAddKitResponse'; import type { DealAddProductRequest } from '../models/DealAddProductRequest'; @@ -66,6 +69,7 @@ import type { GetDealProductsBarcodesPdfRequest } from '../models/GetDealProduct import type { GetDealProductsBarcodesPdfResponse } from '../models/GetDealProductsBarcodesPdfResponse'; import type { ManageEmployeeRequest } from '../models/ManageEmployeeRequest'; import type { ManageEmployeeResponse } from '../models/ManageEmployeeResponse'; +import type { ParseDealsExcelResponse } from '../models/ParseDealsExcelResponse'; import type { CancelablePromise } from '../core/CancelablePromise'; import { OpenAPI } from '../core/OpenAPI'; import { request as __request } from '../core/request'; @@ -426,6 +430,46 @@ export class DealService { }, }); } + /** + * Parse Deals Excel + * @returns ParseDealsExcelResponse Successful Response + * @throws ApiError + */ + public static parseDealsExcel({ + formData, + }: { + formData: Body_parse_deals_excel, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/deal/prefill/excel/parse', + formData: formData, + mediaType: 'multipart/form-data', + errors: { + 422: `Validation Error`, + }, + }); + } + /** + * Create Deals From Excel + * @returns CreateDealsFromExcelResponse Successful Response + * @throws ApiError + */ + public static createDealsExcel({ + requestBody, + }: { + requestBody: CreateDealsFromExcelRequest, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/deal/prefill/excel/create', + body: requestBody, + mediaType: 'application/json', + errors: { + 422: `Validation Error`, + }, + }); + } /** * Services Add * @returns DealAddServicesResponse Successful Response diff --git a/src/components/Dnd/Board/Board.tsx b/src/components/Dnd/Board/Board.tsx index 7fa9a85..dce463d 100644 --- a/src/components/Dnd/Board/Board.tsx +++ b/src/components/Dnd/Board/Board.tsx @@ -9,6 +9,7 @@ import classNames from "classnames"; import { getPluralForm } from "../../../shared/lib/utils.ts"; import { groupBy, has, sum, uniq } from "lodash"; import { DealGroupView } from "../DealGroupView/DealGroupView.tsx"; +import CreateDealsFromFileButton from "../CreateDealsFromFileButton/CreateDealsFromFileButton.tsx"; type Props = { droppableId: string; @@ -181,8 +182,10 @@ export const Board: FC = ({ )} {...provided.droppableProps}> {withCreateButton && ( - { - }} /> + <> + + + )} {getDealsAndGroups().map(obj => { if (isGroup(obj)) { diff --git a/src/components/Dnd/CreateDealButton/CreateDealButton.tsx b/src/components/Dnd/CreateDealButton/CreateDealButton.tsx index 73f2795..7a29c6e 100644 --- a/src/components/Dnd/CreateDealButton/CreateDealButton.tsx +++ b/src/components/Dnd/CreateDealButton/CreateDealButton.tsx @@ -1,4 +1,4 @@ -import { FC, useState } from "react"; +import { useState } from "react"; import styles from "./CreateDealButton.module.css"; import { Text, Transition } from "@mantine/core"; @@ -8,10 +8,7 @@ import { useQueryClient } from "@tanstack/react-query"; import { dateWithoutTimezone } from "../../../shared/lib/date.ts"; import { usePrefillDealContext } from "../../../pages/LeadsPage/contexts/PrefillDealContext.tsx"; -type Props = { - onClick: () => void; -}; -const CreateDealButton: FC = () => { +const CreateDealButton = () => { const [isCreating, setIsCreating] = useState(false); const [isTransitionEnded, setIsTransitionEnded] = useState(true); const queryClient = useQueryClient(); diff --git a/src/components/Dnd/CreateDealsFromFileButton/CreateDealsFromFileButton.module.css b/src/components/Dnd/CreateDealsFromFileButton/CreateDealsFromFileButton.module.css new file mode 100644 index 0000000..d8688d2 --- /dev/null +++ b/src/components/Dnd/CreateDealsFromFileButton/CreateDealsFromFileButton.module.css @@ -0,0 +1,19 @@ +.container { + /*background-color: red;*/ + min-height: 5rem; + display: flex; + justify-content: center; + align-items: stretch; + text-align: center; + flex-direction: column; + border: dashed var(--item-border-size) var(--mantine-color-default-border); + border-radius: var(--item-border-radius); + cursor: pointer; +} + +.container:hover { + background-color: light-dark( + var(--mantine-color-default-hover), + var(--mantine-color-gray-filled-hover) + ); +} diff --git a/src/components/Dnd/CreateDealsFromFileButton/CreateDealsFromFileButton.tsx b/src/components/Dnd/CreateDealsFromFileButton/CreateDealsFromFileButton.tsx new file mode 100644 index 0000000..d623427 --- /dev/null +++ b/src/components/Dnd/CreateDealsFromFileButton/CreateDealsFromFileButton.tsx @@ -0,0 +1,17 @@ +import styles from "./CreateDealsFromFileButton.module.css"; +import { Text } from "@mantine/core"; +import { usePrefillDealsWithExcelContext } from "../../../pages/LeadsPage/contexts/PrefillDealsWithExcelContext.tsx"; + +const CreateDealsFromFileButton = () => { + const { prefillWithExcelOnOpen } = usePrefillDealsWithExcelContext(); + + return ( +
+ Добавление из файла +
+ ); +}; + +export default CreateDealsFromFileButton; diff --git a/src/components/ExcelDropzone/ExcelDropzone.tsx b/src/components/ExcelDropzone/ExcelDropzone.tsx new file mode 100644 index 0000000..a184bec --- /dev/null +++ b/src/components/ExcelDropzone/ExcelDropzone.tsx @@ -0,0 +1,86 @@ +import { Dropzone, DropzoneProps, FileWithPath, MIME_TYPES } from "@mantine/dropzone"; +import { FC } from "react"; +import { Fieldset, Flex, Group, Loader, rem, Text } from "@mantine/core"; +import { IconFileExcel, IconUpload, IconX } from "@tabler/icons-react"; +import UseExcelDropzone from "../../types/UseExcelDropzone.tsx"; + +interface RestProps { + dropzone: UseExcelDropzone; + onDrop: (files: FileWithPath[]) => void; +} + +type Props = RestProps & Omit; + +const ExcelDropzone: FC = (props: Props) => { + const { isLoading } = props.dropzone; + + const getBody = () => { + return ( + + + + + + + + + + + + +
+ + Перенесите или нажмите чтоб выбрать файл формата xlsx + +
+
+
+ ); + }; + + return ( + +
+ + {isLoading ? : getBody()} + +
+
+ ); +}; + +export default ExcelDropzone; diff --git a/src/components/Selects/ClientSelect/ClientSelect.tsx b/src/components/Selects/ClientSelect/ClientSelect.tsx index 4aa1c7c..0f3d5cf 100644 --- a/src/components/Selects/ClientSelect/ClientSelect.tsx +++ b/src/components/Selects/ClientSelect/ClientSelect.tsx @@ -1,4 +1,4 @@ -import { FC } from "react"; +import { FC, ReactNode } from "react"; import { Select } from "@mantine/core"; import { ClientSchema } from "../../../client"; import useClientsList from "../../../pages/ClientsPage/hooks/useClientsList.tsx"; @@ -7,8 +7,10 @@ type Props = { value?: ClientSchema; onChange: (client: ClientSchema) => void; withLabel?: boolean; + error?: string; + inputContainer?: (children: ReactNode) => ReactNode; }; -const ClientSelect: FC = ({ value, onChange, withLabel = false }) => { +const ClientSelect: FC = ({ value, onChange, error, inputContainer, withLabel = false }) => { const { clients } = useClientsList(); const options = clients.map(client => ({ label: client.name, @@ -33,6 +35,8 @@ const ClientSelect: FC = ({ value, onChange, withLabel = false }) => { }} data={options} label={withLabel && "Клиент"} + error={error} + inputContainer={inputContainer} /> ); }; diff --git a/src/pages/LeadsPage/contexts/PrefillDealsWithExcelContext.tsx b/src/pages/LeadsPage/contexts/PrefillDealsWithExcelContext.tsx new file mode 100644 index 0000000..af9682b --- /dev/null +++ b/src/pages/LeadsPage/contexts/PrefillDealsWithExcelContext.tsx @@ -0,0 +1,150 @@ +import { createContext, FC, useContext, useState } from "react"; +import { useDisclosure } from "@mantine/hooks"; +import { DealsWithExcelForm, ProductExcelData } from "../drawers/PrefillDealWithExcelDrawer/types.tsx"; +import { FileWithPath } from "@mantine/dropzone"; +import { notifications } from "../../../shared/lib/notifications.ts"; +import { DealService, type ProductFromExcelSchema, ProductSchema } from "../../../client"; +import UseExcelDropzone from "../../../types/UseExcelDropzone.tsx"; +import { useForm, UseFormReturnType } from "@mantine/form"; +import { useDealPageContext } from "./DealPageContext.tsx"; + +type PrefillDealsWithExcelContextState = { + prefillWithExcelOpened: boolean; + prefillWithExcelOnClose: () => void; + prefillWithExcelOnOpen: () => void; + barcodeProductsMap: Map, + onProductSelectChange: (barcode: string, selectedProduct: ProductSchema) => void, + onDrop: (files: FileWithPath[]) => void; + excelDropzone: UseExcelDropzone; + createDeals: (values: DealsWithExcelForm) => void; + form: UseFormReturnType; + errors: string[]; +}; + +const PrefillDealsWithExcelContext = createContext( + undefined, +); +const usePrefillDealsWithExcelContextState = () => { + const [prefillWithExcelOpened, { open, close }] = useDisclosure(false); + const { refetchDeals } = useDealPageContext(); + const [isLoading, setIsLoading] = useState(false); + const [errors, setErrors] = useState([]); + const excelDropzone: UseExcelDropzone = { + isLoading, + setIsLoading, + }; + + const form = useForm({ + validate: { + client: client => !client && "Выберите клиента", + }, + }); + + const [barcodeProductsMap, setBarcodeProductsMap] = useState>(new Map()); + + const onDrop = (files: FileWithPath[]) => { + if (files.length > 1) { + notifications.error({ message: "Прикрепите одно изображение" }); + return; + } + const file = files[0]; + + setIsLoading(true); + DealService.parseDealsExcel({ + formData: { + upload_file: file, + }, + }) + .then(res => { + const barcodeProductsMap = new Map(); + res.rows.forEach((row) => { + const productsData: ProductExcelData = row; + productsData.selectedProduct = productsData.products[0]; + barcodeProductsMap.set(row.barcode, row); + }); + setBarcodeProductsMap(barcodeProductsMap); + setErrors(res.errors); + }) + .catch(error => { + notifications.error({ message: error.toString() }); + }) + .finally(() => setIsLoading(false)); + }; + + const onProductSelectChange = (barcode: string, selectedProduct: ProductSchema) => { + const newBarcodeProductsMap = new Map(barcodeProductsMap); + const productsData = newBarcodeProductsMap.get(barcode); + if (!productsData) return; + productsData.selectedProduct = selectedProduct; + + newBarcodeProductsMap.set( + barcode, + productsData, + ); + setBarcodeProductsMap(newBarcodeProductsMap); + }; + + const prefillWithExcelOnClose = () => { + close(); + setBarcodeProductsMap(new Map()); + form.reset(); + }; + + const createDeals = (values: DealsWithExcelForm) => { + const products: ProductFromExcelSchema[] = barcodeProductsMap.entries().map(([, productData]) => { + return { + productId: productData.selectedProduct!.id, + citiesBreakdown: productData.breakdowns, + }; + }).toArray(); + + DealService.createDealsExcel({ + requestBody: { + products, + clientId: values.client?.id ?? -1, + }, + }) + .then(({ ok, message }) => { + notifications.guess(ok, { message }); + if (ok) prefillWithExcelOnClose(); + refetchDeals(); + }) + .catch(err => console.log(err)); + }; + + return { + prefillWithExcelOpened, + prefillWithExcelOnClose, + prefillWithExcelOnOpen: open, + barcodeProductsMap, + onProductSelectChange, + onDrop, + excelDropzone, + createDeals, + form, + errors, + }; +}; + +type PrefillDealsWithExcelContextProviderProps = { + children: React.ReactNode; +}; + +export const PrefillDealsWithExcelContextProvider: FC = ({ children }) => { + const state = usePrefillDealsWithExcelContextState(); + return ( + + {children} + + ); +}; + +export const usePrefillDealsWithExcelContext = () => { + const context = useContext(PrefillDealsWithExcelContext); + if (!context) { + throw new Error( + "usePrefillDealsWithExcelContext must be used within a PrefillDealsWithExcelContextProvider", + ); + } + return context; +}; diff --git a/src/pages/LeadsPage/drawers/PrefillDealWithExcelDrawer/PrefillDealsWithExcelDrawer.module.css b/src/pages/LeadsPage/drawers/PrefillDealWithExcelDrawer/PrefillDealsWithExcelDrawer.module.css new file mode 100644 index 0000000..30050e0 --- /dev/null +++ b/src/pages/LeadsPage/drawers/PrefillDealWithExcelDrawer/PrefillDealsWithExcelDrawer.module.css @@ -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; +} \ No newline at end of file diff --git a/src/pages/LeadsPage/drawers/PrefillDealWithExcelDrawer/PrefillDealsWithExcelDrawer.tsx b/src/pages/LeadsPage/drawers/PrefillDealWithExcelDrawer/PrefillDealsWithExcelDrawer.tsx new file mode 100644 index 0000000..5fd61bc --- /dev/null +++ b/src/pages/LeadsPage/drawers/PrefillDealWithExcelDrawer/PrefillDealsWithExcelDrawer.tsx @@ -0,0 +1,47 @@ +import { Drawer, rem } from "@mantine/core"; +import ExcelDropzone from "../../../../components/ExcelDropzone/ExcelDropzone.tsx"; +import styles from "../PrefillDealWithExcelDrawer/PrefillDealsWithExcelDrawer.module.css"; +import { usePrefillDealsWithExcelContext } from "../../contexts/PrefillDealsWithExcelContext.tsx"; +import ProductsPreview from "./components/ProductsPreview.tsx"; + +const PrefillDealsWithExcelDrawer = () => { + const { + prefillWithExcelOpened, + prefillWithExcelOnClose, + barcodeProductsMap, + onDrop, + excelDropzone, + } = usePrefillDealsWithExcelContext(); + + const getBody = () => { + if (barcodeProductsMap?.size === 0) { + return ; + } + return ; + }; + + return ( + +
+ {getBody()} +
+
+ ); +}; + +export default PrefillDealsWithExcelDrawer; diff --git a/src/pages/LeadsPage/drawers/PrefillDealWithExcelDrawer/components/BreakdownByCityTable.tsx b/src/pages/LeadsPage/drawers/PrefillDealWithExcelDrawer/components/BreakdownByCityTable.tsx new file mode 100644 index 0000000..22d1274 --- /dev/null +++ b/src/pages/LeadsPage/drawers/PrefillDealWithExcelDrawer/components/BreakdownByCityTable.tsx @@ -0,0 +1,28 @@ +import { BaseTable } from "../../../../../components/BaseTable/BaseTable.tsx"; +import { ParsedCityBreakdownSchema } from "../../../../../client"; +import { useBreakdownByCityTableColumns } from "../hooks/useBreakdownByCityTableColumns.tsx"; + +type Props = { + breakdowns: ParsedCityBreakdownSchema[]; +} + +const BreakdownByCityTable = ({ breakdowns }: Props) => { + const columns = useBreakdownByCityTableColumns(); + + return ( + + ); +}; + +export default BreakdownByCityTable; diff --git a/src/pages/LeadsPage/drawers/PrefillDealWithExcelDrawer/components/ParsingResultsTooltip.tsx b/src/pages/LeadsPage/drawers/PrefillDealWithExcelDrawer/components/ParsingResultsTooltip.tsx new file mode 100644 index 0000000..f9477ef --- /dev/null +++ b/src/pages/LeadsPage/drawers/PrefillDealWithExcelDrawer/components/ParsingResultsTooltip.tsx @@ -0,0 +1,30 @@ +import { usePrefillDealsWithExcelContext } from "../../../contexts/PrefillDealsWithExcelContext.tsx"; +import { Text, Tooltip } from "@mantine/core"; +import { IconAlertCircle, IconCircleCheck } from "@tabler/icons-react"; + +const ParsingResultsTooltip = () => { + const { errors } = usePrefillDealsWithExcelContext(); + const isError = errors.length !== 0; + + const errorLines = errors.map((error, i) => {error}); + const tooltipData = isError ? errorLines : "Ошибок при обработке нет"; + const color = isError ? "red" : "grey"; + + return ( + + {isError ? ( + + ) : ( + + )} + + ); +}; + +export default ParsingResultsTooltip; diff --git a/src/pages/LeadsPage/drawers/PrefillDealWithExcelDrawer/components/ProductsPreview.tsx b/src/pages/LeadsPage/drawers/PrefillDealWithExcelDrawer/components/ProductsPreview.tsx new file mode 100644 index 0000000..3b10d60 --- /dev/null +++ b/src/pages/LeadsPage/drawers/PrefillDealWithExcelDrawer/components/ProductsPreview.tsx @@ -0,0 +1,68 @@ +import styles from "../PrefillDealsWithExcelDrawer.module.css"; +import ProductsTable from "./ProductsTable.tsx"; +import { usePrefillDealsWithExcelContext } from "../../../contexts/PrefillDealsWithExcelContext.tsx"; +import { Box, Button, Flex, Group, Stack, Title } from "@mantine/core"; +import { ProductExcelData } from "../types.tsx"; +import BreakdownByCityTable from "./BreakdownByCityTable.tsx"; +import ClientSelect from "../../../../../components/Selects/ClientSelect/ClientSelect.tsx"; +import ParsingResultsTooltip from "./ParsingResultsTooltip.tsx"; + +const ProductsPreview = () => { + const { barcodeProductsMap, createDeals, form } = usePrefillDealsWithExcelContext(); + + const getTitle = (barcode: string, productsData: ProductExcelData) => { + if (productsData.products.length === 1) { + return `Товар со штрихкодом ${barcode}`; + } + return `Товары со штрихкодом ${barcode}`; + }; + + const getProductsData = () => { + return barcodeProductsMap.entries().map(([barcode, productsData]) => ( +
+ + + {getTitle(barcode, productsData)} + + + + + + + + + + +
+ ), + ).toArray(); + }; + + return ( + + Предпросмотр +
createDeals(values))}> + ( + + {children} + + + + + + )} + /> + + {getProductsData()} +
+ ); +}; + +export default ProductsPreview; \ No newline at end of file diff --git a/src/pages/LeadsPage/drawers/PrefillDealWithExcelDrawer/components/ProductsTable.tsx b/src/pages/LeadsPage/drawers/PrefillDealWithExcelDrawer/components/ProductsTable.tsx new file mode 100644 index 0000000..72e4558 --- /dev/null +++ b/src/pages/LeadsPage/drawers/PrefillDealWithExcelDrawer/components/ProductsTable.tsx @@ -0,0 +1,29 @@ +import { BaseTable } from "../../../../../components/BaseTable/BaseTable.tsx"; +import { useProductsTableColumns } from "../hooks/useProductsTableColumns.tsx"; +import { ProductExcelData } from "../types.tsx"; + +type Props = { + barcode: string; + productsData: ProductExcelData; +} + +const ProductsTable = ({ barcode, productsData }: Props) => { + const columns = useProductsTableColumns({ barcode }); + + return ( + + ); +}; + +export default ProductsTable; diff --git a/src/pages/LeadsPage/drawers/PrefillDealWithExcelDrawer/hooks/useBreakdownByCityTableColumns.tsx b/src/pages/LeadsPage/drawers/PrefillDealWithExcelDrawer/hooks/useBreakdownByCityTableColumns.tsx new file mode 100644 index 0000000..774ab8c --- /dev/null +++ b/src/pages/LeadsPage/drawers/PrefillDealWithExcelDrawer/hooks/useBreakdownByCityTableColumns.tsx @@ -0,0 +1,34 @@ +import { useMemo } from "react"; +import { MRT_ColumnDef } from "mantine-react-table"; +import { ParsedCityBreakdownSchema } from "../../../../../client"; +import { ActionIcon, Image } from "@mantine/core"; + +export const useBreakdownByCityTableColumns = () => { + return useMemo[]>( + () => [ + { + accessorKey: "baseMarketplace.iconUrl", + header: "Маркетплейс", + Cell: ({ cell }) => ( + + + + ), + size: 10, + }, + { + accessorKey: "shippingWarehouse.name", + header: "Склад отгрузки", + size: 10, + }, + { + accessorKey: "quantity", + header: "Количество", + size: 10, + }, + ], + [], + ); +}; diff --git a/src/pages/LeadsPage/drawers/PrefillDealWithExcelDrawer/hooks/useProductsTableColumns.tsx b/src/pages/LeadsPage/drawers/PrefillDealWithExcelDrawer/hooks/useProductsTableColumns.tsx new file mode 100644 index 0000000..01fd1ce --- /dev/null +++ b/src/pages/LeadsPage/drawers/PrefillDealWithExcelDrawer/hooks/useProductsTableColumns.tsx @@ -0,0 +1,63 @@ +import { useEffect, useMemo, useState } from "react"; +import { MRT_ColumnDef } from "mantine-react-table"; +import { ProductSchema } from "../../../../../client"; +import { Radio } from "@mantine/core"; +import { usePrefillDealsWithExcelContext } from "../../../contexts/PrefillDealsWithExcelContext.tsx"; +import { ProductExcelData } from "../types.tsx"; + +type Props = { + barcode: string; +} + +export const useProductsTableColumns = ({ barcode }: Props) => { + const { onProductSelectChange, barcodeProductsMap } = usePrefillDealsWithExcelContext(); + const [productData, setProductData] = useState(); + + useEffect(() => { + setProductData(barcodeProductsMap.get(barcode)); + }, [barcodeProductsMap]); + + return useMemo[]>( + () => [ + { + header: "Выбор", + size: 10, + Cell: ({ row }) => { + return ( + onProductSelectChange(barcode, row.original)} + /> + ); + }, + }, + { + accessorKey: "article", + header: "Артикул", + size: 20, + }, + { + accessorKey: "name", + header: "Название", + }, + { + accessorKey: "brand", + header: "Бренд", + size: 30, + }, + { + accessorKey: "color", + header: "Цвет", + size: 30, + }, + { + accessorKey: "size", + header: "Размер", + size: 10, + }, + ], + [productData], + ).filter(columnDef => ( + !(columnDef.header === "Выбор" && (productData?.products.length ?? 0) === 1) + )); +}; diff --git a/src/pages/LeadsPage/drawers/PrefillDealWithExcelDrawer/types.tsx b/src/pages/LeadsPage/drawers/PrefillDealWithExcelDrawer/types.tsx new file mode 100644 index 0000000..1fbb389 --- /dev/null +++ b/src/pages/LeadsPage/drawers/PrefillDealWithExcelDrawer/types.tsx @@ -0,0 +1,11 @@ +import { ClientSchema, type ParsedCityBreakdownSchema, ProductSchema } from "../../../../client"; + +export type ProductExcelData = { + products: ProductSchema[]; + breakdowns: ParsedCityBreakdownSchema[]; + selectedProduct?: ProductSchema; +} + +export type DealsWithExcelForm = { + client?: ClientSchema; +} diff --git a/src/pages/LeadsPage/ui/LeadsPage.tsx b/src/pages/LeadsPage/ui/LeadsPage.tsx index e6188ca..540a907 100644 --- a/src/pages/LeadsPage/ui/LeadsPage.tsx +++ b/src/pages/LeadsPage/ui/LeadsPage.tsx @@ -23,6 +23,8 @@ import { dateWithoutTimezone } from "../../../shared/lib/date.ts"; import DealPrefillDrawer from "../drawers/DealPrefillDrawer/DealPrefillDrawer.tsx"; import { PrefillDealContextProvider } from "../contexts/PrefillDealContext.tsx"; import { useParams } from "@tanstack/react-router"; +import { PrefillDealsWithExcelContextProvider } from "../contexts/PrefillDealsWithExcelContext.tsx"; +import PrefillDealsWithExcelDrawer from "../drawers/PrefillDealWithExcelDrawer/PrefillDealsWithExcelDrawer.tsx"; enum DisplayMode { BOARD, @@ -401,101 +403,104 @@ export const LeadsPage: FC = () => { await refetch(); }}> - - + + - Вид - - - setDisplayMode(DisplayMode.BOARD) - } - variant={ - displayMode === DisplayMode.BOARD - ? "filled" - : "default" - }> - - - - setDisplayMode(DisplayMode.TABLE) - } - variant={ - displayMode === DisplayMode.TABLE - ? "filled" - : "default" - }> - - + align={"center"} + justify={"space-between"}> + + Вид + + + setDisplayMode(DisplayMode.BOARD) + } + variant={ + displayMode === DisplayMode.BOARD + ? "filled" + : "default" + }> + + + + setDisplayMode(DisplayMode.TABLE) + } + variant={ + displayMode === DisplayMode.TABLE + ? "filled" + : "default" + }> + + + + +
+ + + form.setFieldValue("dealStatus", null) + } + clearable + placeholder={"Выберите статус "} + {...form.getInputProps("dealStatus")} + /> + + form.setFieldValue("marketplace", null) + } + clearable + placeholder={"Выберите маркетплейс"} + {...form.getInputProps("marketplace")} + /> + + form.setFieldValue("client", null) + } + clearable + searchable + placeholder={"Выберите клиента"} + {...form.getInputProps("client")} + /> +
+
- -
- - - form.setFieldValue("dealStatus", null) - } - clearable - placeholder={"Выберите статус "} - {...form.getInputProps("dealStatus")} - /> - - form.setFieldValue("marketplace", null) - } - clearable - placeholder={"Выберите маркетплейс"} - {...form.getInputProps("marketplace")} - /> - - form.setFieldValue("client", null) - } - clearable - searchable - placeholder={"Выберите клиента"} - {...form.getInputProps("client")} - /> -
-
-
-
- - {getBody()} - - - +
+ + {getBody()} + + + + +
diff --git a/src/types/UseExcelDropzone.tsx b/src/types/UseExcelDropzone.tsx new file mode 100644 index 0000000..0993a97 --- /dev/null +++ b/src/types/UseExcelDropzone.tsx @@ -0,0 +1,8 @@ +import { Dispatch, SetStateAction } from "react"; + +type UseExcelDropzone = { + isLoading: boolean; + setIsLoading: Dispatch>; +} + +export default UseExcelDropzone;