Merge remote-tracking branch 'origin/dealPrefilling'
This commit is contained in:
@@ -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;
|
||||
Reference in New Issue
Block a user