Merge remote-tracking branch 'origin/dealPrefilling'

This commit is contained in:
2024-10-18 05:46:22 +03:00
22 changed files with 736 additions and 7 deletions

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,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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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