Merge remote-tracking branch 'origin/fillingDealsFromExcelFile' into excelMerge

This commit is contained in:
2025-01-10 14:39:04 +03:00
29 changed files with 878 additions and 101 deletions

View File

@@ -30,12 +30,14 @@ export type { BaseShippingWarehouseSchema } from './models/BaseShippingWarehouse
export type { BaseTransactionTagSchema } from './models/BaseTransactionTagSchema'; export type { BaseTransactionTagSchema } from './models/BaseTransactionTagSchema';
export type { BillPaymentStatus } from './models/BillPaymentStatus'; export type { BillPaymentStatus } from './models/BillPaymentStatus';
export type { BillStatusUpdateRequest } from './models/BillStatusUpdateRequest'; 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_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_barcode_image } from './models/Body_upload_product_barcode_image';
export type { Body_upload_product_image } from './models/Body_upload_product_image'; export type { Body_upload_product_image } from './models/Body_upload_product_image';
export type { BoxSchema } from './models/BoxSchema'; export type { BoxSchema } from './models/BoxSchema';
export type { CancelDealBillRequest } from './models/CancelDealBillRequest'; export type { CancelDealBillRequest } from './models/CancelDealBillRequest';
export type { CancelDealBillResponse } from './models/CancelDealBillResponse'; export type { CancelDealBillResponse } from './models/CancelDealBillResponse';
export type { CityBreakdownFromExcelSchema } from './models/CityBreakdownFromExcelSchema';
export type { ClientCreateRequest } from './models/ClientCreateRequest'; export type { ClientCreateRequest } from './models/ClientCreateRequest';
export type { ClientCreateResponse } from './models/ClientCreateResponse'; export type { ClientCreateResponse } from './models/ClientCreateResponse';
export type { ClientDeleteRequest } from './models/ClientDeleteRequest'; export type { ClientDeleteRequest } from './models/ClientDeleteRequest';
@@ -52,6 +54,8 @@ export type { CreateBoxInDealSchema } from './models/CreateBoxInDealSchema';
export type { CreateBoxInPalletSchema } from './models/CreateBoxInPalletSchema'; export type { CreateBoxInPalletSchema } from './models/CreateBoxInPalletSchema';
export type { CreateDealBillRequest } from './models/CreateDealBillRequest'; export type { CreateDealBillRequest } from './models/CreateDealBillRequest';
export type { CreateDealBillResponse } from './models/CreateDealBillResponse'; 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 { CreateDepartmentRequest } from './models/CreateDepartmentRequest';
export type { CreateDepartmentResponse } from './models/CreateDepartmentResponse'; export type { CreateDepartmentResponse } from './models/CreateDepartmentResponse';
export type { CreateDepartmentSectionRequest } from './models/CreateDepartmentSectionRequest'; export type { CreateDepartmentSectionRequest } from './models/CreateDepartmentSectionRequest';
@@ -222,8 +226,12 @@ export type { ManageEmployeeResponse } from './models/ManageEmployeeResponse';
export type { MarketplaceCreateSchema } from './models/MarketplaceCreateSchema'; export type { MarketplaceCreateSchema } from './models/MarketplaceCreateSchema';
export type { MarketplaceSchema } from './models/MarketplaceSchema'; export type { MarketplaceSchema } from './models/MarketplaceSchema';
export type { NotificationChannel } from './models/NotificationChannel'; export type { NotificationChannel } from './models/NotificationChannel';
export type { OptionalShippingWarehouseSchema } from './models/OptionalShippingWarehouseSchema';
export type { PaginationInfoSchema } from './models/PaginationInfoSchema'; export type { PaginationInfoSchema } from './models/PaginationInfoSchema';
export type { PalletSchema } from './models/PalletSchema'; 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 { PassportImageSchema } from './models/PassportImageSchema';
export type { PaymentRecordCreateSchema } from './models/PaymentRecordCreateSchema'; export type { PaymentRecordCreateSchema } from './models/PaymentRecordCreateSchema';
export type { PaymentRecordGetSchema } from './models/PaymentRecordGetSchema'; export type { PaymentRecordGetSchema } from './models/PaymentRecordGetSchema';
@@ -240,6 +248,7 @@ export type { ProductDeleteBarcodeImageResponse } from './models/ProductDeleteBa
export type { ProductDeleteRequest } from './models/ProductDeleteRequest'; export type { ProductDeleteRequest } from './models/ProductDeleteRequest';
export type { ProductDeleteResponse } from './models/ProductDeleteResponse'; export type { ProductDeleteResponse } from './models/ProductDeleteResponse';
export type { ProductExistsBarcodeResponse } from './models/ProductExistsBarcodeResponse'; export type { ProductExistsBarcodeResponse } from './models/ProductExistsBarcodeResponse';
export type { ProductFromExcelSchema } from './models/ProductFromExcelSchema';
export type { ProductGenerateBarcodeRequest } from './models/ProductGenerateBarcodeRequest'; export type { ProductGenerateBarcodeRequest } from './models/ProductGenerateBarcodeRequest';
export type { ProductGenerateBarcodeResponse } from './models/ProductGenerateBarcodeResponse'; export type { ProductGenerateBarcodeResponse } from './models/ProductGenerateBarcodeResponse';
export type { ProductGetBarcodeImageResponse } from './models/ProductGetBarcodeImageResponse'; export type { ProductGetBarcodeImageResponse } from './models/ProductGetBarcodeImageResponse';

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -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<ProductFromExcelSchema>;
};

View File

@@ -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;
};

View File

@@ -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);
};

View File

@@ -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<ParsedProductRowSchema>;
errors: Array<string>;
};

View File

@@ -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;
};

View File

@@ -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<ProductSchema>;
breakdowns: Array<ParsedCityBreakdownSchema>;
};

View File

@@ -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<CityBreakdownFromExcelSchema>;
};

View File

@@ -2,6 +2,9 @@
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-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 { DealAddKitRequest } from '../models/DealAddKitRequest';
import type { DealAddKitResponse } from '../models/DealAddKitResponse'; import type { DealAddKitResponse } from '../models/DealAddKitResponse';
import type { DealAddProductRequest } from '../models/DealAddProductRequest'; import type { DealAddProductRequest } from '../models/DealAddProductRequest';
@@ -66,6 +69,7 @@ import type { GetDealProductsBarcodesPdfRequest } from '../models/GetDealProduct
import type { GetDealProductsBarcodesPdfResponse } from '../models/GetDealProductsBarcodesPdfResponse'; import type { GetDealProductsBarcodesPdfResponse } from '../models/GetDealProductsBarcodesPdfResponse';
import type { ManageEmployeeRequest } from '../models/ManageEmployeeRequest'; import type { ManageEmployeeRequest } from '../models/ManageEmployeeRequest';
import type { ManageEmployeeResponse } from '../models/ManageEmployeeResponse'; import type { ManageEmployeeResponse } from '../models/ManageEmployeeResponse';
import type { ParseDealsExcelResponse } from '../models/ParseDealsExcelResponse';
import type { CancelablePromise } from '../core/CancelablePromise'; import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI'; import { OpenAPI } from '../core/OpenAPI';
import { request as __request } from '../core/request'; 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<ParseDealsExcelResponse> {
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<CreateDealsFromExcelResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/deal/prefill/excel/create',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/** /**
* Services Add * Services Add
* @returns DealAddServicesResponse Successful Response * @returns DealAddServicesResponse Successful Response

View File

@@ -9,6 +9,7 @@ import classNames from "classnames";
import { getPluralForm } from "../../../shared/lib/utils.ts"; import { getPluralForm } from "../../../shared/lib/utils.ts";
import { groupBy, has, sum, uniq } from "lodash"; import { groupBy, has, sum, uniq } from "lodash";
import { DealGroupView } from "../DealGroupView/DealGroupView.tsx"; import { DealGroupView } from "../DealGroupView/DealGroupView.tsx";
import CreateDealsFromFileButton from "../CreateDealsFromFileButton/CreateDealsFromFileButton.tsx";
type Props = { type Props = {
droppableId: string; droppableId: string;
@@ -181,8 +182,10 @@ export const Board: FC<Props> = ({
)} )}
{...provided.droppableProps}> {...provided.droppableProps}>
{withCreateButton && ( {withCreateButton && (
<CreateDealButton onClick={() => { <>
}} /> <CreateDealButton />
<CreateDealsFromFileButton />
</>
)} )}
{getDealsAndGroups().map(obj => { {getDealsAndGroups().map(obj => {
if (isGroup(obj)) { if (isGroup(obj)) {

View File

@@ -1,4 +1,4 @@
import { FC, useState } from "react"; import { useState } from "react";
import styles from "./CreateDealButton.module.css"; import styles from "./CreateDealButton.module.css";
import { Text, Transition } from "@mantine/core"; import { Text, Transition } from "@mantine/core";
@@ -8,10 +8,7 @@ import { useQueryClient } from "@tanstack/react-query";
import { dateWithoutTimezone } from "../../../shared/lib/date.ts"; import { dateWithoutTimezone } from "../../../shared/lib/date.ts";
import { usePrefillDealContext } from "../../../pages/LeadsPage/contexts/PrefillDealContext.tsx"; import { usePrefillDealContext } from "../../../pages/LeadsPage/contexts/PrefillDealContext.tsx";
type Props = { const CreateDealButton = () => {
onClick: () => void;
};
const CreateDealButton: FC<Props> = () => {
const [isCreating, setIsCreating] = useState(false); const [isCreating, setIsCreating] = useState(false);
const [isTransitionEnded, setIsTransitionEnded] = useState(true); const [isTransitionEnded, setIsTransitionEnded] = useState(true);
const queryClient = useQueryClient(); const queryClient = useQueryClient();

View File

@@ -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)
);
}

View File

@@ -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 (
<div
className={styles["container"]}
onClick={prefillWithExcelOnOpen}>
<Text>Добавление из файла</Text>
</div>
);
};
export default CreateDealsFromFileButton;

View File

@@ -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<DropzoneProps, "onDrop">;
const ExcelDropzone: FC<Props> = (props: Props) => {
const { isLoading } = props.dropzone;
const getBody = () => {
return (
<Dropzone
{...props}
accept={[
MIME_TYPES.xlsx,
]}
multiple={false}
onDrop={props.onDrop}>
<Group
justify="center"
gap="xl"
style={{ pointerEvents: "none" }}>
<Dropzone.Accept>
<IconUpload
style={{
width: rem(52),
height: rem(52),
color: "var(--mantine-color-blue-6)",
}}
stroke={1.5}
/>
</Dropzone.Accept>
<Dropzone.Reject>
<IconX
style={{
width: rem(52),
height: rem(52),
color: "var(--mantine-color-red-6)",
}}
stroke={1.5}
/>
</Dropzone.Reject>
<Dropzone.Idle>
<IconFileExcel
style={{
width: rem(52),
height: rem(52),
color: "var(--mantine-color-dimmed)",
}}
stroke={1.5}
/>
</Dropzone.Idle>
<div style={{ textAlign: "center" }}>
<Text
size="xl"
inline>
Перенесите или нажмите чтоб выбрать файл формата xlsx
</Text>
</div>
</Group>
</Dropzone>
);
};
return (
<Flex
gap={rem(10)}
direction={"column"}>
<Fieldset legend={"Файл"}>
<Flex justify={"center"}>
{isLoading ? <Loader /> : getBody()}
</Flex>
</Fieldset>
</Flex>
);
};
export default ExcelDropzone;

View File

@@ -1,4 +1,4 @@
import { FC } from "react"; import { FC, ReactNode } from "react";
import { Select } from "@mantine/core"; import { Select } from "@mantine/core";
import { ClientSchema } from "../../../client"; import { ClientSchema } from "../../../client";
import useClientsList from "../../../pages/ClientsPage/hooks/useClientsList.tsx"; import useClientsList from "../../../pages/ClientsPage/hooks/useClientsList.tsx";
@@ -7,8 +7,10 @@ type Props = {
value?: ClientSchema; value?: ClientSchema;
onChange: (client: ClientSchema) => void; onChange: (client: ClientSchema) => void;
withLabel?: boolean; withLabel?: boolean;
error?: string;
inputContainer?: (children: ReactNode) => ReactNode;
}; };
const ClientSelect: FC<Props> = ({ value, onChange, withLabel = false }) => { const ClientSelect: FC<Props> = ({ value, onChange, error, inputContainer, withLabel = false }) => {
const { clients } = useClientsList(); const { clients } = useClientsList();
const options = clients.map(client => ({ const options = clients.map(client => ({
label: client.name, label: client.name,
@@ -33,6 +35,8 @@ const ClientSelect: FC<Props> = ({ value, onChange, withLabel = false }) => {
}} }}
data={options} data={options}
label={withLabel && "Клиент"} label={withLabel && "Клиент"}
error={error}
inputContainer={inputContainer}
/> />
); );
}; };

View File

@@ -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<string, ProductExcelData>,
onProductSelectChange: (barcode: string, selectedProduct: ProductSchema) => void,
onDrop: (files: FileWithPath[]) => void;
excelDropzone: UseExcelDropzone;
createDeals: (values: DealsWithExcelForm) => void;
form: UseFormReturnType<DealsWithExcelForm>;
errors: string[];
};
const PrefillDealsWithExcelContext = createContext<PrefillDealsWithExcelContextState | undefined>(
undefined,
);
const usePrefillDealsWithExcelContextState = () => {
const [prefillWithExcelOpened, { open, close }] = useDisclosure(false);
const { refetchDeals } = useDealPageContext();
const [isLoading, setIsLoading] = useState(false);
const [errors, setErrors] = useState<string[]>([]);
const excelDropzone: UseExcelDropzone = {
isLoading,
setIsLoading,
};
const form = useForm<DealsWithExcelForm>({
validate: {
client: client => !client && "Выберите клиента",
},
});
const [barcodeProductsMap, setBarcodeProductsMap] = useState<Map<string, ProductExcelData>>(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<PrefillDealsWithExcelContextProviderProps> = ({ children }) => {
const state = usePrefillDealsWithExcelContextState();
return (
<PrefillDealsWithExcelContext.Provider value={state}>
{children}
</PrefillDealsWithExcelContext.Provider>
);
};
export const usePrefillDealsWithExcelContext = () => {
const context = useContext(PrefillDealsWithExcelContext);
if (!context) {
throw new Error(
"usePrefillDealsWithExcelContext must be used within a PrefillDealsWithExcelContextProvider",
);
}
return context;
};

View File

@@ -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;
}

View File

@@ -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 <ExcelDropzone dropzone={excelDropzone} onDrop={onDrop} />;
}
return <ProductsPreview />;
};
return (
<Drawer
size={"calc(77vw)"}
position={"right"}
onClose={prefillWithExcelOnClose}
removeScrollProps={{ allowPinchZoom: true }}
withCloseButton={false}
opened={prefillWithExcelOpened}
styles={{
body: {
height: "100%",
display: "flex",
flexDirection: "row",
gap: rem(20),
},
}}
>
<div className={styles["deal-container"]}>
{getBody()}
</div>
</Drawer>
);
};
export default PrefillDealsWithExcelDrawer;

View File

@@ -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 (
<BaseTable
data={breakdowns}
columns={columns}
w={"30%"}
restProps={{
enableSorting: false,
enableRowActions: false,
enableTopToolbar: false,
enableColumnActions: false,
}}
/>
);
};
export default BreakdownByCityTable;

View File

@@ -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) => <Text key={i}>{error}</Text>);
const tooltipData = isError ? errorLines : "Ошибок при обработке нет";
const color = isError ? "red" : "grey";
return (
<Tooltip
label={tooltipData}
multiline
w={350}
withArrow
color={color}
>
{isError ? (
<IconAlertCircle color={"red"}/>
) : (
<IconCircleCheck color={"green"}/>
)}
</Tooltip>
);
};
export default ParsingResultsTooltip;

View File

@@ -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]) => (
<div key={barcode} className={styles["deal-container-wrapper"]}>
<Stack>
<Title order={5}>
{getTitle(barcode, productsData)}
</Title>
<Flex direction={"row"} gap={"md"} flex={10}>
<Box flex={7}>
<ProductsTable barcode={barcode} productsData={productsData} />
</Box>
<Box flex={3}>
<BreakdownByCityTable breakdowns={productsData.breakdowns} />
</Box>
</Flex>
</Stack>
</div>
),
).toArray();
};
return (
<Stack gap={"md"}>
<Title order={3}>Предпросмотр</Title>
<form onSubmit={form.onSubmit((values) => createDeals(values))}>
<ClientSelect
{...form.getInputProps("client")}
inputContainer={(children) => (
<Group align={"flex-start"}>
{children}
<Group>
<Button
variant="outline"
type="submit"
>
Создать сделки
</Button>
<ParsingResultsTooltip />
</Group>
</Group>
)}
/>
</form>
{getProductsData()}
</Stack>
);
};
export default ProductsPreview;

View File

@@ -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 (
<BaseTable
data={productsData.products}
columns={columns}
w={"100%"}
restProps={{
enableSorting: false,
enableRowActions: false,
enableTopToolbar: false,
enableColumnActions: false,
}}
/>
);
};
export default ProductsTable;

View File

@@ -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<MRT_ColumnDef<ParsedCityBreakdownSchema>[]>(
() => [
{
accessorKey: "baseMarketplace.iconUrl",
header: "Маркетплейс",
Cell: ({ cell }) => (
<ActionIcon
radius={"md"}
variant={"transparent"}>
<Image src={cell.getValue()} />
</ActionIcon>
),
size: 10,
},
{
accessorKey: "shippingWarehouse.name",
header: "Склад отгрузки",
size: 10,
},
{
accessorKey: "quantity",
header: "Количество",
size: 10,
},
],
[],
);
};

View File

@@ -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<ProductExcelData>();
useEffect(() => {
setProductData(barcodeProductsMap.get(barcode));
}, [barcodeProductsMap]);
return useMemo<MRT_ColumnDef<ProductSchema>[]>(
() => [
{
header: "Выбор",
size: 10,
Cell: ({ row }) => {
return (
<Radio
checked={productData?.selectedProduct?.id === row.original.id}
onChange={() => 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)
));
};

View File

@@ -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;
}

View File

@@ -23,6 +23,8 @@ import { dateWithoutTimezone } from "../../../shared/lib/date.ts";
import DealPrefillDrawer from "../drawers/DealPrefillDrawer/DealPrefillDrawer.tsx"; import DealPrefillDrawer from "../drawers/DealPrefillDrawer/DealPrefillDrawer.tsx";
import { PrefillDealContextProvider } from "../contexts/PrefillDealContext.tsx"; import { PrefillDealContextProvider } from "../contexts/PrefillDealContext.tsx";
import { useParams } from "@tanstack/react-router"; import { useParams } from "@tanstack/react-router";
import { PrefillDealsWithExcelContextProvider } from "../contexts/PrefillDealsWithExcelContext.tsx";
import PrefillDealsWithExcelDrawer from "../drawers/PrefillDealWithExcelDrawer/PrefillDealsWithExcelDrawer.tsx";
enum DisplayMode { enum DisplayMode {
BOARD, BOARD,
@@ -401,101 +403,104 @@ export const LeadsPage: FC = () => {
await refetch(); await refetch();
}}> }}>
<PrefillDealContextProvider> <PrefillDealContextProvider>
<PageBlock style={{ flex: 0 }}> <PrefillDealsWithExcelContextProvider>
<Flex <PageBlock style={{ flex: 0 }}>
align={"center"}
justify={"space-between"}>
<Flex <Flex
gap={rem(10)} align={"center"}
direction={"column"} justify={"space-between"}>
align={"center"}> <Flex
<Text size={"xs"}>Вид</Text> gap={rem(10)}
<Flex gap={rem(10)}> direction={"column"}
<ActionIcon align={"center"}>
onClick={() => <Text size={"xs"}>Вид</Text>
setDisplayMode(DisplayMode.BOARD) <Flex gap={rem(10)}>
} <ActionIcon
variant={ onClick={() =>
displayMode === DisplayMode.BOARD setDisplayMode(DisplayMode.BOARD)
? "filled" }
: "default" variant={
}> displayMode === DisplayMode.BOARD
<IconMenuDeep ? "filled"
style={{ rotate: "-90deg" }} : "default"
/> }>
</ActionIcon> <IconMenuDeep
<ActionIcon style={{ rotate: "-90deg" }}
onClick={() => />
setDisplayMode(DisplayMode.TABLE) </ActionIcon>
} <ActionIcon
variant={ onClick={() =>
displayMode === DisplayMode.TABLE setDisplayMode(DisplayMode.TABLE)
? "filled" }
: "default" variant={
}> displayMode === DisplayMode.TABLE
<IconMenu2 /> ? "filled"
</ActionIcon> : "default"
}>
<IconMenu2 />
</ActionIcon>
</Flex>
</Flex> </Flex>
<motion.div
key={displayMode}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2 }}>
<div
className={styles["top-panel"]}
style={{
display:
displayMode === DisplayMode.TABLE
? "flex"
: "none",
}}>
<NumberInput
min={1}
placeholder={"Введите номер"}
{...form.getInputProps("id")}
hideControls
/>
<DealStatusSelect
onClear={() =>
form.setFieldValue("dealStatus", null)
}
clearable
placeholder={"Выберите статус "}
{...form.getInputProps("dealStatus")}
/>
<BaseMarketplaceSelect
onClear={() =>
form.setFieldValue("marketplace", null)
}
clearable
placeholder={"Выберите маркетплейс"}
{...form.getInputProps("marketplace")}
/>
<ClientSelectNew
onClear={() =>
form.setFieldValue("client", null)
}
clearable
searchable
placeholder={"Выберите клиента"}
{...form.getInputProps("client")}
/>
</div>
</motion.div>
</Flex> </Flex>
<motion.div </PageBlock>
key={displayMode} <PageBlock
initial={{ opacity: 0 }} style={{
animate={{ opacity: 1 }} display: "flex",
transition={{ duration: 0.2 }}> flexDirection: "column",
<div flex: 1,
className={styles["top-panel"]} height: "100%",
style={{ }}>
display: {getBody()}
displayMode === DisplayMode.TABLE </PageBlock>
? "flex" <DealEditDrawer />
: "none", <DealPrefillDrawer />
}}> <PrefillDealsWithExcelDrawer />
<NumberInput </PrefillDealsWithExcelContextProvider>
min={1}
placeholder={"Введите номер"}
{...form.getInputProps("id")}
hideControls
/>
<DealStatusSelect
onClear={() =>
form.setFieldValue("dealStatus", null)
}
clearable
placeholder={"Выберите статус "}
{...form.getInputProps("dealStatus")}
/>
<BaseMarketplaceSelect
onClear={() =>
form.setFieldValue("marketplace", null)
}
clearable
placeholder={"Выберите маркетплейс"}
{...form.getInputProps("marketplace")}
/>
<ClientSelectNew
onClear={() =>
form.setFieldValue("client", null)
}
clearable
searchable
placeholder={"Выберите клиента"}
{...form.getInputProps("client")}
/>
</div>
</motion.div>
</Flex>
</PageBlock>
<PageBlock
style={{
display: "flex",
flexDirection: "column",
flex: 1,
height: "100%",
}}>
{getBody()}
</PageBlock>
<DealEditDrawer />
<DealPrefillDrawer />
</PrefillDealContextProvider> </PrefillDealContextProvider>
</DealPageContextProvider> </DealPageContextProvider>
</PageBlock> </PageBlock>

View File

@@ -0,0 +1,8 @@
import { Dispatch, SetStateAction } from "react";
type UseExcelDropzone = {
isLoading: boolean;
setIsLoading: Dispatch<SetStateAction<boolean>>;
}
export default UseExcelDropzone;