feat: prettier

This commit is contained in:
2024-09-27 04:47:04 +03:00
parent c5f839d9ef
commit de4fe450ab
253 changed files with 11322 additions and 10004 deletions

View File

@@ -1,89 +1,132 @@
import {ActionIcon, Button, ComboboxItem, ComboboxItemGroup, Flex, Input, OptionsFilter, rem} from "@mantine/core";
import {BaseFormInputProps} from "../../../../types/utils.ts";
import {DealProductServiceSchema, ServiceSchema} from "../../../../client";
import {FC, useEffect, useState} from "react";
import {
ActionIcon,
Button,
ComboboxItem,
ComboboxItemGroup,
Flex,
Input,
OptionsFilter,
rem,
} from "@mantine/core";
import { BaseFormInputProps } from "../../../../types/utils.ts";
import { DealProductServiceSchema, ServiceSchema } from "../../../../client";
import { FC, useEffect, useState } from "react";
import ServiceWithPriceInput from "../../../../components/ServiceWithPriceInput/ServiceWithPriceInput.tsx";
import {isNumber} from "lodash";
import {notifications} from "../../../../shared/lib/notifications.ts";
import {IconTrash} from "@tabler/icons-react";
import {useSelector} from "react-redux";
import {RootState} from "../../../../redux/store.ts";
import { isNumber } from "lodash";
import { notifications } from "../../../../shared/lib/notifications.ts";
import { IconTrash } from "@tabler/icons-react";
import { useSelector } from "react-redux";
import { RootState } from "../../../../redux/store.ts";
type RestProps = {
quantity: number;
}
};
type Props = BaseFormInputProps<DealProductServiceSchema[]> & RestProps;
const DealProductServiceTable: FC<Props> = (props: Props) => {
const {value, onChange, quantity, error} = props;
const { value, onChange, quantity, error } = props;
const authState = useSelector((state: RootState) => state.auth);
const [innerValue, setInnerValue] = useState<Partial<DealProductServiceSchema>[]>(value || []);
const [innerValue, setInnerValue] = useState<
Partial<DealProductServiceSchema>[]
>(value || []);
const onServiceChange = (idx: number, value: ServiceSchema) => {
setInnerValue(oldValue => oldValue.map((item, i) => i === idx ? {...item, service: value} : item));
}
setInnerValue(oldValue =>
oldValue.map((item, i) =>
i === idx ? { ...item, service: value } : item
)
);
};
const onQuantityChange = (idx: number, value: string | number) => {
if (!isNumber(value)) return;
setInnerValue(oldValue => oldValue.map((item, i) => i === idx ? {...item, price: value} : item));
}
setInnerValue(oldValue =>
oldValue.map((item, i) =>
i === idx ? { ...item, price: value } : item
)
);
};
const onCreate = () => {
if (innerValue.length > 0 && !innerValue.at(-1)?.service) {
notifications.error({message: "Заполните последнюю услугу"})
notifications.error({ message: "Заполните последнюю услугу" });
return;
}
setInnerValue(prevState => [...prevState, {service: undefined, quantity: 1}])
}
setInnerValue(prevState => [
...prevState,
{ service: undefined, quantity: 1 },
]);
};
const onDelete = (idx: number) => {
setInnerValue(oldValue => oldValue.filter((_, i) => i !== idx));
};
}
const serviceOptionsFilter = ({options}: { options: ComboboxItemGroup[] }) => {
const productServiceIds = innerValue.map(service => service.service?.id);
return (options as ComboboxItemGroup[]).map(({items, group}) => {
const serviceOptionsFilter = ({
options,
}: {
options: ComboboxItemGroup[];
}) => {
const productServiceIds = innerValue.map(
service => service.service?.id
);
return (options as ComboboxItemGroup[]).map(({ items, group }) => {
return {
group,
items: items.filter(item => !productServiceIds.includes(parseInt((item as ComboboxItem).value)))
}
})
items: items.filter(
item =>
!productServiceIds.includes(
parseInt((item as ComboboxItem).value)
)
),
};
});
};
useEffect(() => {
onChange(innerValue as DealProductServiceSchema[]);
}, [innerValue]);
return (
<Input.Wrapper error={error}>
<Flex direction={"column"} gap={rem(10)}>
<Flex
direction={"column"}
gap={rem(10)}>
{innerValue.map((service, idx) => (
<Flex key={service.service?.name || idx} direction={"row"} gap={rem(10)} align={"center"}
justify={"stretch"}>
<ActionIcon onClick={() => onDelete(idx)} variant={"default"}>
<IconTrash/>
<Flex
key={service.service?.name || idx}
direction={"row"}
gap={rem(10)}
align={"center"}
justify={"stretch"}>
<ActionIcon
onClick={() => onDelete(idx)}
variant={"default"}>
<IconTrash />
</ActionIcon>
<ServiceWithPriceInput
serviceProps={{
onChange: (event) => onServiceChange(idx, event),
onChange: event => onServiceChange(idx, event),
value: service.service,
placeholder: "Выберите услугу",
style: {width: "100%"},
filter: serviceOptionsFilter as OptionsFilter
style: { width: "100%" },
filter: serviceOptionsFilter as OptionsFilter,
}}
priceProps={{
onChange: (event) => onQuantityChange(idx, event),
onChange: event => onQuantityChange(idx, event),
value: service.price,
placeholder: "Введите стоимость",
hideControls: true,
style: {width: "100%"},
style: { width: "100%" },
suffix: "₽",
disabled: authState.isGuest
disabled: authState.isGuest,
}}
containerProps={{w: "100%"}}
containerProps={{ w: "100%" }}
quantity={quantity}
/>
</Flex>
))}
<Button onClick={onCreate} variant={"default"}>Добавить услугу</Button>
<Button
onClick={onCreate}
variant={"default"}>
Добавить услугу
</Button>
</Flex>
</Input.Wrapper>
)
}
export default DealProductServiceTable;
);
};
export default DealProductServiceTable;

View File

@@ -1,22 +1,23 @@
import {BaseTable} from "../../../../components/BaseTable/BaseTable.tsx";
import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
import useDealProductsTableColumns from "./columns.tsx";
import {FC} from "react";
import {CRUDTableProps} from "../../../../types/CRUDTable.tsx";
import {DealProductSchema, ProductService} from "../../../../client";
import {ActionIcon, Button, Flex, rem, Tooltip} from "@mantine/core";
import {MRT_TableOptions} from "mantine-react-table";
import {modals} from "@mantine/modals";
import {IconBarcode, IconEdit, IconTrash} from "@tabler/icons-react";
import {notifications} from "../../../../shared/lib/notifications.ts";
import {CreateProductRequest} from "../../../ProductsPage/types.ts";
import { FC } from "react";
import { CRUDTableProps } from "../../../../types/CRUDTable.tsx";
import { DealProductSchema, ProductService } from "../../../../client";
import { ActionIcon, Button, Flex, rem, Tooltip } from "@mantine/core";
import { MRT_TableOptions } from "mantine-react-table";
import { modals } from "@mantine/modals";
import { IconBarcode, IconEdit, IconTrash } from "@tabler/icons-react";
import { notifications } from "../../../../shared/lib/notifications.ts";
import { CreateProductRequest } from "../../../ProductsPage/types.ts";
type RestProps = {
clientId: number;
onMultipleDelete?: (items: DealProductSchema[]) => void;
}
};
type Props = CRUDTableProps<DealProductSchema> & RestProps;
const DealProductsTable: FC<Props> = (props: Props) => {
const {items, clientId, onChange, onCreate, onDelete, onMultipleDelete} = props;
const { items, clientId, onChange, onCreate, onDelete, onMultipleDelete } =
props;
const columns = useDealProductsTableColumns({
onChange: (product, quantity) => {
@@ -25,10 +26,9 @@ const DealProductsTable: FC<Props> = (props: Props) => {
onDelete(product);
return;
}
onChange({...product, quantity})
onChange({ ...product, quantity });
},
data: items
data: items,
});
const onCreateClick = () => {
@@ -37,112 +37,132 @@ const DealProductsTable: FC<Props> = (props: Props) => {
modal: "addDealProduct",
title: "Добавление товара",
innerProps: {
onCreate: (product) => onCreate(product as DealProductSchema),
clientId
onCreate: product => onCreate(product as DealProductSchema),
clientId,
},
size: "lg"
})
}
size: "lg",
});
};
const onPrintBarcodeClick = (product: DealProductSchema) => {
modals.openContextModal({
modal: "printBarcode",
title: 'Печать штрихкода',
title: "Печать штрихкода",
withCloseButton: true,
innerProps: {
productId: product.product.id,
defaultQuantity: product.quantity
}
})
}
defaultQuantity: product.quantity,
},
});
};
const onCreateProduct = (newProduct: CreateProductRequest) => {
ProductService.createProduct({
requestBody: newProduct
}).then(({ok, message}) => {
notifications.guess(ok, {message: message});
})
}
requestBody: newProduct,
}).then(({ ok, message }) => {
notifications.guess(ok, { message: message });
});
};
const onCreateProductClick = () => {
modals.openContextModal({
modal: "createProduct",
title: 'Создание товара',
title: "Создание товара",
withCloseButton: false,
innerProps: {
clientId: clientId,
onCreate: onCreateProduct
}
})
}
onCreate: onCreateProduct,
},
});
};
const onEditClick = (product: DealProductSchema) => {
if (!onChange) return;
modals.openContextModal({
modal: "addDealProduct",
title: 'Создание товара',
title: "Создание товара",
withCloseButton: false,
innerProps: {
clientId: clientId,
element: product,
onChange: onChange
onChange: onChange,
},
size: "lg"
})
}
size: "lg",
});
};
return (
<BaseTable
data={items}
columns={columns}
restProps={{
enableBottomToolbar: true,
enableRowActions: true,
enableRowSelection: true,
renderRowActions: ({row}) => (
<Flex gap="md">
<Tooltip label="Удалить">
<ActionIcon onClick={() => onDelete && onDelete(row.original)} variant={"default"}>
<IconTrash/>
</ActionIcon>
</Tooltip>
<Tooltip label="Печать штрихкода">
<ActionIcon onClick={() => onPrintBarcodeClick(row.original)} variant={"default"}>
<IconBarcode/>
</ActionIcon>
</Tooltip>
<Tooltip label="Редактировать">
<ActionIcon onClick={() => onEditClick(row.original)} variant={"default"}>
<IconEdit/>
</ActionIcon>
</Tooltip>
</Flex>
),
renderBottomToolbar: ({table}) => (
<Flex justify={"flex-end"} gap={rem(10)} p={rem(10)}>
{(onMultipleDelete && table.getSelectedRowModel().rows.length > 0) && (
restProps={
{
enableBottomToolbar: true,
enableRowActions: true,
enableRowSelection: true,
renderRowActions: ({ row }) => (
<Flex gap="md">
<Tooltip label="Удалить">
<ActionIcon
onClick={() =>
onDelete && onDelete(row.original)
}
variant={"default"}>
<IconTrash />
</ActionIcon>
</Tooltip>
<Tooltip label="Печать штрихкода">
<ActionIcon
onClick={() =>
onPrintBarcodeClick(row.original)
}
variant={"default"}>
<IconBarcode />
</ActionIcon>
</Tooltip>
<Tooltip label="Редактировать">
<ActionIcon
onClick={() => onEditClick(row.original)}
variant={"default"}>
<IconEdit />
</ActionIcon>
</Tooltip>
</Flex>
),
renderBottomToolbar: ({ table }) => (
<Flex
justify={"flex-end"}
gap={rem(10)}
p={rem(10)}>
{onMultipleDelete &&
table.getSelectedRowModel().rows.length > 0 && (
<Button
onClick={() => {
onMultipleDelete(
table
.getSelectedRowModel()
.rows.map(
row => row.original
)
);
}}
variant={"filled"}
color={"red"}>
Удалить выбранные
</Button>
)}
<Button
onClick={() => {
onMultipleDelete(table.getSelectedRowModel().rows.map(row => row.original))
}}
variant={"filled"}
color={"red"}
>
Удалить выбранные
variant={"default"}
onClick={onCreateProductClick}>
Создать товар
</Button>
)}
<Button
variant={"default"}
onClick={onCreateProductClick}>
Создать товар
</Button>
<Button
onClick={onCreateClick}
variant={"default"}>
Добавить товар в сделку
</Button>
</Flex>
),
} as MRT_TableOptions<DealProductSchema>}
<Button
onClick={onCreateClick}
variant={"default"}>
Добавить товар в сделку
</Button>
</Flex>
),
} as MRT_TableOptions<DealProductSchema>
}
/>
)
}
);
};
export default DealProductsTable;
export default DealProductsTable;

View File

@@ -1,74 +1,107 @@
import {useMemo} from "react";
import {MRT_ColumnDef} from "mantine-react-table";
import {DealProductSchema} from "../../../../client";
import {List} from "@mantine/core";
import { useMemo } from "react";
import { MRT_ColumnDef } from "mantine-react-table";
import { DealProductSchema } from "../../../../client";
import { List } from "@mantine/core";
type Props = {
onChange: (product: DealProductSchema, quantity: number) => void;
data: DealProductSchema[];
}
};
const useDealProductsTableColumns = (props: Props) => {
const {onChange, data} = props;
const totalQuantity = useMemo(() => data.reduce((acc, row) => acc + row.quantity, 0), [data]);
const totalPrice = useMemo(() => data.reduce((totalAcc, row) => totalAcc + row.services.reduce((singleAcc, service) => singleAcc + service.price * row.quantity, 0), 0), [data]);
return useMemo<MRT_ColumnDef<DealProductSchema>[]>(() => [
{
accessorKey: "product.article",
header: "Артикул",
enableSorting: false,
enableColumnActions: false,
},
{
accessorKey: "product.name",
header: "Название",
enableSorting: false,
enableColumnActions: false,
},
{
accessorKey: "product.barcodes",
header: "Штрихкоды",
Cell: ({cell}) => {
return (
<List size={"sm"}>
{cell.getValue<string[]>()?.map(barcode => (
<List.Item key={barcode}>
{barcode}
</List.Item>
))}
</List>
)
const { onChange, data } = props;
const totalQuantity = useMemo(
() => data.reduce((acc, row) => acc + row.quantity, 0),
[data]
);
const totalPrice = useMemo(
() =>
data.reduce(
(totalAcc, row) =>
totalAcc +
row.services.reduce(
(singleAcc, service) =>
singleAcc + service.price * row.quantity,
0
),
0
),
[data]
);
return useMemo<MRT_ColumnDef<DealProductSchema>[]>(
() => [
{
accessorKey: "product.article",
header: "Артикул",
enableSorting: false,
enableColumnActions: false,
},
enableSorting: false,
enableColumnActions: false,
},
{
accessorKey: "quantity",
header: "Количество",
enableSorting: false,
enableColumnActions: false,
Footer: <>Всего товаров: {totalQuantity} </>,
},
{
header: "Услуги",
Cell: ({row}) => <List size={"sm"}>{
row.original.services.map(service => `${service.service.name} (${service.price}₽ за шт)`)
.map(serviceText => <List.Item key={serviceText}>
{serviceText}
</List.Item>
)}
</List>,
enableColumnActions: false,
},
{
header: "Итоговая стоимость услуг",
Cell: ({row}) => <>
{row.original.services.reduce((acc, service) => acc + row.original.quantity * service.price, 0)}</>,
enableColumnActions: false,
Footer: <>Всего стоимость услуг: {totalPrice}</>
}
], [onChange, data])
}
{
accessorKey: "product.name",
header: "Название",
enableSorting: false,
enableColumnActions: false,
},
{
accessorKey: "product.barcodes",
header: "Штрихкоды",
Cell: ({ cell }) => {
return (
<List size={"sm"}>
{cell
.getValue<string[]>()
?.map(barcode => (
<List.Item key={barcode}>
{barcode}
</List.Item>
))}
</List>
);
},
enableSorting: false,
enableColumnActions: false,
},
{
accessorKey: "quantity",
header: "Количество",
enableSorting: false,
enableColumnActions: false,
Footer: <>Всего товаров: {totalQuantity} </>,
},
{
header: "Услуги",
Cell: ({ row }) => (
<List size={"sm"}>
{row.original.services
.map(
service =>
`${service.service.name} (${service.price}₽ за шт)`
)
.map(serviceText => (
<List.Item key={serviceText}>
{serviceText}
</List.Item>
))}
</List>
),
enableColumnActions: false,
},
{
header: "Итоговая стоимость услуг",
Cell: ({ row }) => (
<>
{row.original.services.reduce(
(acc, service) =>
acc + row.original.quantity * service.price,
0
)}
</>
),
enableColumnActions: false,
Footer: <>Всего стоимость услуг: {totalPrice}</>,
},
],
[onChange, data]
);
};
export default useDealProductsTableColumns;
export default useDealProductsTableColumns;

View File

@@ -1,32 +1,30 @@
import {FC} from "react";
import {useDealServicesTableColumns} from "./columns.tsx";
import {BaseTable} from "../../../../components/BaseTable/BaseTable.tsx";
import {DealServiceSchema} from "../../../../client";
import {CRUDTableProps} from "../../../../types/CRUDTable.tsx";
import {MRT_TableOptions} from "mantine-react-table";
import {ActionIcon, Button, Flex, rem, Tooltip} from "@mantine/core";
import {openContextModal} from "@mantine/modals";
import {IconEdit, IconTrash} from "@tabler/icons-react";
import { FC } from "react";
import { useDealServicesTableColumns } from "./columns.tsx";
import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
import { DealServiceSchema } from "../../../../client";
import { CRUDTableProps } from "../../../../types/CRUDTable.tsx";
import { MRT_TableOptions } from "mantine-react-table";
import { ActionIcon, Button, Flex, rem, Tooltip } from "@mantine/core";
import { openContextModal } from "@mantine/modals";
import { IconEdit, IconTrash } from "@tabler/icons-react";
type RestProps = {
onMultipleDelete?: (items: DealServiceSchema[]) => void;
}
};
type Props = CRUDTableProps<DealServiceSchema> & RestProps;
const DealServicesTable: FC<Props> = (
{
items,
onChange,
onDelete,
onCreate,
onSelectionChange,
onMultipleDelete,
tableRef
}) => {
const DealServicesTable: FC<Props> = ({
items,
onChange,
onDelete,
onCreate,
onSelectionChange,
onMultipleDelete,
tableRef,
}) => {
const serviceIds = items.map(item => item.service.id);
const columns = useDealServicesTableColumns({
data: items
data: items,
});
const onCreateClick = () => {
if (!onCreate) return;
@@ -34,12 +32,11 @@ const DealServicesTable: FC<Props> = (
title: "Добавление услуги",
modal: "addDealService",
innerProps: {
onCreate: (event) => onCreate(event as DealServiceSchema),
serviceIds
}
})
}
onCreate: event => onCreate(event as DealServiceSchema),
serviceIds,
},
});
};
const onEditClick = (service: DealServiceSchema) => {
if (!onChange) return;
openContextModal({
@@ -48,12 +45,10 @@ const DealServicesTable: FC<Props> = (
innerProps: {
element: service,
onChange,
serviceIds
}
})
}
serviceIds,
},
});
};
return (
<>
@@ -62,58 +57,73 @@ const DealServicesTable: FC<Props> = (
data={items}
columns={columns}
onSelectionChange={onSelectionChange}
restProps={{
enableGrouping: true,
initialState: {grouping: ["service.category"]},
enableColumnActions: false,
enableSorting: false,
enableBottomToolbar: true,
enableRowActions: true,
enableRowSelection: true,
renderBottomToolbar: ({table}) => (
<Flex justify={"flex-end"} gap={rem(10)} p={rem(10)}>
{(onMultipleDelete && table.getSelectedRowModel().rows.length > 0) && (
restProps={
{
enableGrouping: true,
initialState: { grouping: ["service.category"] },
enableColumnActions: false,
enableSorting: false,
enableBottomToolbar: true,
enableRowActions: true,
enableRowSelection: true,
renderBottomToolbar: ({ table }) => (
<Flex
justify={"flex-end"}
gap={rem(10)}
p={rem(10)}>
{onMultipleDelete &&
table.getSelectedRowModel().rows.length >
0 && (
<Button
onClick={() => {
onMultipleDelete(
table
.getSelectedRowModel()
.rows.map(
row => row.original
)
);
}}
variant={"filled"}
color={"red"}>
Удалить выбранные
</Button>
)}
<Button
onClick={() => {
onMultipleDelete(table.getSelectedRowModel().rows.map(row => row.original))
}}
variant={"filled"}
color={"red"}
>
Удалить выбранные
onClick={onCreateClick}
variant={"default"}>
Добавить услугу
</Button>
)}
<Button onClick={onCreateClick} variant={"default"}>
Добавить услугу
</Button>
</Flex>
),
renderRowActions: ({row}) => (
<Flex gap="md">
<Tooltip label="Удалить">
<ActionIcon onClick={() => {
if (onDelete) onDelete(row.original);
}} variant={"default"}>
<IconTrash/>
</ActionIcon>
</Tooltip>
<Tooltip label="Редактировать">
<ActionIcon onClick={() => {
onEditClick(row.original);
}} variant={"default"}>
<IconEdit/>
</ActionIcon>
</Tooltip>
</Flex>
)
} as MRT_TableOptions<DealServiceSchema>}
</Flex>
),
renderRowActions: ({ row }) => (
<Flex gap="md">
<Tooltip label="Удалить">
<ActionIcon
onClick={() => {
if (onDelete)
onDelete(row.original);
}}
variant={"default"}>
<IconTrash />
</ActionIcon>
</Tooltip>
<Tooltip label="Редактировать">
<ActionIcon
onClick={() => {
onEditClick(row.original);
}}
variant={"default"}>
<IconEdit />
</ActionIcon>
</Tooltip>
</Flex>
),
} as MRT_TableOptions<DealServiceSchema>
}
/>
</>
);
};
)
}
export default DealServicesTable;
export default DealServicesTable;

View File

@@ -1,62 +1,69 @@
import {MRT_ColumnDef} from "mantine-react-table";
import {useMemo} from "react";
import {DealServiceSchema} from "../../../../client";
import { MRT_ColumnDef } from "mantine-react-table";
import { useMemo } from "react";
import { DealServiceSchema } from "../../../../client";
type Props = {
// onChange: (service: DealServiceSchema, quantity: number) => void;
data: DealServiceSchema[];
}
};
export const useDealServicesTableColumns = (props: Props) => {
const {data} = props;
const totalPrice = useMemo(() =>
data.reduce((acc, row) => acc + row.quantity * row.price, 0)
,
[data]);
const { data } = props;
const totalPrice = useMemo(
() => data.reduce((acc, row) => acc + row.quantity * row.price, 0),
[data]
);
return useMemo<MRT_ColumnDef<DealServiceSchema>[]>(() => [
{
accessorKey: "service.category",
header: "Категория",
accessorFn: (row) => row.service.category.name,
},
{
enableGrouping: false,
accessorKey: "service.name",
header: "Услуга",
},
{
enableGrouping: false,
accessorKey: "price",
header: "Цена",
},
{
enableGrouping: false,
accessorKey: "service.cost",
header: "Себестоимость"
},
{
enableGrouping: false,
accessorKey: "quantity",
header: "Количество",
},
{
enableGrouping: false,
header: "Сумма",
Cell: ({row}) => {
return row.original.quantity * row.original.price;
return useMemo<MRT_ColumnDef<DealServiceSchema>[]>(
() => [
{
accessorKey: "service.category",
header: "Категория",
accessorFn: row => row.service.category.name,
},
aggregationFn: "sum",
AggregatedCell: ({cell}) => {
return <>Итоговая сумма по категории: {" "}
{
cell.row.subRows?.reduce((acc, row) =>
acc + row.original.quantity * row.original.price, 0)
}
</>;
{
enableGrouping: false,
accessorKey: "service.name",
header: "Услуга",
},
Footer: <>Итоговая сумма по услугам: {totalPrice}</>
}
], []);
}
{
enableGrouping: false,
accessorKey: "price",
header: "Цена",
},
{
enableGrouping: false,
accessorKey: "service.cost",
header: "Себестоимость",
},
{
enableGrouping: false,
accessorKey: "quantity",
header: "Количество",
},
{
enableGrouping: false,
header: "Сумма",
Cell: ({ row }) => {
return row.original.quantity * row.original.price;
},
aggregationFn: "sum",
AggregatedCell: ({ cell }) => {
return (
<>
Итоговая сумма по категории:{" "}
{cell.row.subRows?.reduce(
(acc, row) =>
acc +
row.original.quantity * row.original.price,
0
)}
</>
);
},
Footer: <>Итоговая сумма по услугам: {totalPrice}</>,
},
],
[]
);
};

View File

@@ -1,14 +1,14 @@
import {DealStatusHistorySchema} from "../../../../client";
import {useDealStatusChangeTableColumns} from "./columns.tsx";
import {BaseTable} from "../../../../components/BaseTable/BaseTable.tsx";
import {FC} from "react";
import { DealStatusHistorySchema } from "../../../../client";
import { useDealStatusChangeTableColumns } from "./columns.tsx";
import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
import { FC } from "react";
type Props = {
items: DealStatusHistorySchema[];
}
};
const DealStatusChangeTable: FC<Props> = (props: Props) => {
const {items} = props;
const { items } = props;
return (
<BaseTable
@@ -21,9 +21,9 @@ const DealStatusChangeTable: FC<Props> = (props: Props) => {
enableBottomToolbar: false,
enableColumnFilters: false,
enableColumnVisibilityToggle: false,
layoutMode:"grid"
layoutMode: "grid",
}}
/>
);
}
export default DealStatusChangeTable;
};
export default DealStatusChangeTable;

View File

@@ -1,41 +1,60 @@
import {useMemo} from "react";
import {MRT_ColumnDef} from "mantine-react-table";
import {DealStatusHistorySchema} from "../../../../client";
import {DealStatus, DealStatusDictionary} from "../../../../shared/enums/DealStatus.ts";
import {Spoiler, Text} from "@mantine/core";
import { useMemo } from "react";
import { MRT_ColumnDef } from "mantine-react-table";
import { DealStatusHistorySchema } from "../../../../client";
import {
DealStatus,
DealStatusDictionary,
} from "../../../../shared/enums/DealStatus.ts";
import { Spoiler, Text } from "@mantine/core";
export const useDealStatusChangeTableColumns = () => {
return useMemo<MRT_ColumnDef<DealStatusHistorySchema>[]>(() => [
{
accessorKey: "changedAt",
header: "Дата",
accessorFn: (row) => new Date(row.changedAt).toLocaleString('ru-RU'),
},
{
header: "Пользователь",
accessorFn: (row) => `${row.user.firstName} ${row.user.secondName}`
},
{
accessorKey: "fromStatus",
header: "Из статуса",
accessorFn: (row) => DealStatusDictionary[row.fromStatus as DealStatus],
},
{
accessorKey: "toStatus",
header: "В статус",
accessorFn: (row) => DealStatusDictionary[row.toStatus as DealStatus],
},
{
accessorKey: "comment",
header: "Комментарий",
Cell: ({row}) =>
<Spoiler maxHeight={80} showLabel={"Показать весь"} hideLabel={"Скрыть"}>
<Text style={{wordWrap: "break-word", wordBreak: "break-all", whiteSpace: "normal"}} span>
{row.original.comment}<br/>
</Text>
</Spoiler>
,
}
], []);
}
return useMemo<MRT_ColumnDef<DealStatusHistorySchema>[]>(
() => [
{
accessorKey: "changedAt",
header: "Дата",
accessorFn: row =>
new Date(row.changedAt).toLocaleString("ru-RU"),
},
{
header: "Пользователь",
accessorFn: row =>
`${row.user.firstName} ${row.user.secondName}`,
},
{
accessorKey: "fromStatus",
header: "Из статуса",
accessorFn: row =>
DealStatusDictionary[row.fromStatus as DealStatus],
},
{
accessorKey: "toStatus",
header: "В статус",
accessorFn: row =>
DealStatusDictionary[row.toStatus as DealStatus],
},
{
accessorKey: "comment",
header: "Комментарий",
Cell: ({ row }) => (
<Spoiler
maxHeight={80}
showLabel={"Показать весь"}
hideLabel={"Скрыть"}>
<Text
style={{
wordWrap: "break-word",
wordBreak: "break-all",
whiteSpace: "normal",
}}
span>
{row.original.comment}
<br />
</Text>
</Spoiler>
),
},
],
[]
);
};

View File

@@ -1,19 +1,19 @@
import {UserSchema} from "../../../../client";
import {FC} from "react";
import {BaseTable} from "../../../../components/BaseTable/BaseTable.tsx";
import {useSimpleUsersTableColumns} from "./columns.tsx";
import {MRT_TableOptions} from "mantine-react-table";
import {ActionIcon, Button, Flex, rem, Tooltip} from "@mantine/core";
import {modals} from "@mantine/modals";
import {IconTrash} from "@tabler/icons-react";
import {UserRoleEnum} from "../../../../shared/enums/UserRole.ts";
import { UserSchema } from "../../../../client";
import { FC } from "react";
import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
import { useSimpleUsersTableColumns } from "./columns.tsx";
import { MRT_TableOptions } from "mantine-react-table";
import { ActionIcon, Button, Flex, rem, Tooltip } from "@mantine/core";
import { modals } from "@mantine/modals";
import { IconTrash } from "@tabler/icons-react";
import { UserRoleEnum } from "../../../../shared/enums/UserRole.ts";
export type SimpleUsersTableProps = {
items: UserSchema[],
onChange: (items: UserSchema[]) => void
}
items: UserSchema[];
onChange: (items: UserSchema[]) => void;
};
const SimpleUsersTable: FC<SimpleUsersTableProps> = ({items, onChange}) => {
const SimpleUsersTable: FC<SimpleUsersTableProps> = ({ items, onChange }) => {
const columns = useSimpleUsersTableColumns();
const onAddClick = () => {
@@ -24,40 +24,56 @@ const SimpleUsersTable: FC<SimpleUsersTableProps> = ({items, onChange}) => {
withCloseButton: false,
innerProps: {
selectProps: {
filterBy: (user) => !userIds.includes(user.id) && user.roleKey === UserRoleEnum.EMPLOYEE
filterBy: user =>
!userIds.includes(user.id) &&
user.roleKey === UserRoleEnum.EMPLOYEE,
},
onSelect: (user) => onChange([...items, user])
}
})
}
onSelect: user => onChange([...items, user]),
},
});
};
return (
<BaseTable
data={items}
columns={columns}
restProps={{
enableColumnActions: false,
enableSorting: false,
enableBottomToolbar: true,
renderBottomToolbar: (
<Flex justify={"flex-end"} gap={rem(10)} p={rem(10)}>
<Button onClick={() => onAddClick()} variant={"default"}>
Добавить сотрудника к услуге
</Button>
</Flex>
),
enableRowActions: true,
renderRowActions: ({row}) => (
<Tooltip label="Удалить">
<ActionIcon onClick={() => {
onChange(items.filter(item => item.id !== row.original.id));
}} variant={"default"}>
<IconTrash/>
</ActionIcon>
</Tooltip>),
} as MRT_TableOptions<UserSchema>}
restProps={
{
enableColumnActions: false,
enableSorting: false,
enableBottomToolbar: true,
renderBottomToolbar: (
<Flex
justify={"flex-end"}
gap={rem(10)}
p={rem(10)}>
<Button
onClick={() => onAddClick()}
variant={"default"}>
Добавить сотрудника к услуге
</Button>
</Flex>
),
enableRowActions: true,
renderRowActions: ({ row }) => (
<Tooltip label="Удалить">
<ActionIcon
onClick={() => {
onChange(
items.filter(
item => item.id !== row.original.id
)
);
}}
variant={"default"}>
<IconTrash />
</ActionIcon>
</Tooltip>
),
} as MRT_TableOptions<UserSchema>
}
/>
)
}
);
};
export default SimpleUsersTable;
export default SimpleUsersTable;

View File

@@ -1,20 +1,23 @@
import {useMemo} from "react";
import {MRT_ColumnDef} from "mantine-react-table";
import {UserSchema} from "../../../../client";
import { useMemo } from "react";
import { MRT_ColumnDef } from "mantine-react-table";
import { UserSchema } from "../../../../client";
export const useSimpleUsersTableColumns = () => {
return useMemo<MRT_ColumnDef<UserSchema>[]>(() => [
{
accessorKey: "firstName",
header: "Имя"
},
{
accessorKey: "secondName",
header: "Фамилия"
},
{
accessorKey: "position.name",
header: "Должность"
},
], []);
}
return useMemo<MRT_ColumnDef<UserSchema>[]>(
() => [
{
accessorKey: "firstName",
header: "Имя",
},
{
accessorKey: "secondName",
header: "Фамилия",
},
{
accessorKey: "position.name",
header: "Должность",
},
],
[]
);
};

View File

@@ -1,34 +1,42 @@
import {createContext, FC, useContext, useState} from "react";
import {DealSchema} from "../../../client";
import { createContext, FC, useContext, useState } from "react";
import { DealSchema } from "../../../client";
type DealPageContextState = {
selectedDeal?: DealSchema;
setSelectedDeal: (deal: DealSchema | undefined) => void;
}
};
const DealPageContext = createContext<DealPageContextState | undefined>(undefined);
const DealPageContext = createContext<DealPageContextState | undefined>(
undefined
);
const useDealPageContextState = () => {
const [selectedDeal, setSelectedDeal] = useState<DealSchema | undefined>(undefined);
return {selectedDeal, setSelectedDeal};
}
const [selectedDeal, setSelectedDeal] = useState<DealSchema | undefined>(
undefined
);
return { selectedDeal, setSelectedDeal };
};
type DealPageContextProviderProps = {
children: React.ReactNode;
}
};
export const DealPageContextProvider: FC<DealPageContextProviderProps> = ({children}) => {
export const DealPageContextProvider: FC<DealPageContextProviderProps> = ({
children,
}) => {
const state = useDealPageContextState();
return (
<DealPageContext.Provider value={state}>
{children}
</DealPageContext.Provider>
);
}
};
export const useDealPageContext = () => {
const context = useContext(DealPageContext);
if (!context) {
throw new Error('useDealPageContext must be used within a DealPageContextProvider');
throw new Error(
"useDealPageContext must be used within a DealPageContextProvider"
);
}
return context;
}
};

View File

@@ -7,4 +7,4 @@
@mixin dark {
background-color: var(--mantine-color-dark-5);
}
}
}

View File

@@ -2,7 +2,11 @@ import { Box, Drawer, rem, Tabs, Text } from "@mantine/core";
import { FC, useEffect, useRef } from "react";
import DealServicesTable from "../../components/DealServicesTable/DealServicesTable.tsx";
import { useDealPageContext } from "../../contexts/DealPageContext.tsx";
import { DealProductSchema, DealService, DealServiceSchema } from "../../../../client";
import {
DealProductSchema,
DealService,
DealServiceSchema,
} from "../../../../client";
import { notifications } from "../../../../shared/lib/notifications.ts";
import { modals } from "@mantine/modals";
import { BaseTableRef } from "../../../../components/BaseTable/BaseTable.tsx";
@@ -27,13 +31,13 @@ const useDealServicesTableState = () => {
service,
},
}).then(async ({ ok, message }) => {
if (!ok) {
notifications.guess(ok, { message });
return;
}
await DealService.getDealById({ dealId: selectedDeal.id })
.then(setSelectedDeal);
await DealService.getDealById({ dealId: selectedDeal.id }).then(
setSelectedDeal
);
});
};
const onServiceDelete = (service: DealServiceSchema) => {
@@ -42,15 +46,9 @@ const useDealServicesTableState = () => {
title: "Удаление услуги",
children: (
<>
<Text>
Вы уверены, что хотите удалить услугу:
</Text>
<Text>
{service.service.name}?
</Text>
<Text>Вы уверены, что хотите удалить услугу:</Text>
<Text>{service.service.name}?</Text>
</>
),
onConfirm: () => {
DealService.deleteDealService({
@@ -63,8 +61,9 @@ const useDealServicesTableState = () => {
notifications.guess(ok, { message });
return;
}
await DealService.getDealById({ dealId: selectedDeal.id })
.then(setSelectedDeal);
await DealService.getDealById({
dealId: selectedDeal.id,
}).then(setSelectedDeal);
});
},
labels: {
@@ -87,8 +86,9 @@ const useDealServicesTableState = () => {
notifications.guess(ok, { message });
return;
}
await DealService.getDealById({ dealId: selectedDeal.id })
.then(setSelectedDeal);
await DealService.getDealById({ dealId: selectedDeal.id }).then(
setSelectedDeal
);
});
};
const onsServiceMultipleDelete = (items: DealServiceSchema[]) => {
@@ -113,8 +113,9 @@ const useDealServicesTableState = () => {
notifications.guess(ok, { message });
return;
}
await DealService.getDealById({ dealId: selectedDeal.id })
.then(setSelectedDeal);
await DealService.getDealById({
dealId: selectedDeal.id,
}).then(setSelectedDeal);
});
},
labels: {
@@ -143,14 +144,16 @@ const DealEditDrawerServicesTable = () => {
onsServiceMultipleDelete,
} = useDealServicesTableState();
return (<DealServicesTable
tableRef={tableRef}
items={services}
onChange={onServiceUpdate}
onDelete={onServiceDelete}
onCreate={onServiceCreate}
onMultipleDelete={onsServiceMultipleDelete}
/>);
return (
<DealServicesTable
tableRef={tableRef}
items={services}
onChange={onServiceUpdate}
onDelete={onServiceDelete}
onCreate={onServiceCreate}
onMultipleDelete={onsServiceMultipleDelete}
/>
);
};
const useDealProductTableState = () => {
@@ -166,8 +169,9 @@ const useDealProductTableState = () => {
}).then(async ({ ok, message }) => {
notifications.guess(ok, { message });
if (!ok) return;
await DealService.getDealById({ dealId: selectedDeal.id })
.then(setSelectedDeal);
await DealService.getDealById({ dealId: selectedDeal.id }).then(
setSelectedDeal
);
});
};
const onProductDelete = (product: DealProductSchema) => {
@@ -176,15 +180,9 @@ const useDealProductTableState = () => {
title: "Удаление товара",
children: (
<>
<Text>
Вы уверены, что хотите удалить товар:
</Text>
<Text>
{product.product.name}?
</Text>
<Text>Вы уверены, что хотите удалить товар:</Text>
<Text>{product.product.name}?</Text>
</>
),
onConfirm: () => {
DealService.deleteDealProduct({
@@ -197,8 +195,9 @@ const useDealProductTableState = () => {
notifications.guess(ok, { message });
return;
}
await DealService.getDealById({ dealId: selectedDeal.id })
.then(setSelectedDeal);
await DealService.getDealById({
dealId: selectedDeal.id,
}).then(setSelectedDeal);
});
},
labels: {
@@ -219,8 +218,9 @@ const useDealProductTableState = () => {
notifications.guess(ok, { message });
return;
}
await DealService.getDealById({ dealId: selectedDeal.id })
.then(setSelectedDeal);
await DealService.getDealById({ dealId: selectedDeal.id }).then(
setSelectedDeal
);
});
};
const onProductMultipleDelete = (items: DealProductSchema[]) => {
@@ -245,8 +245,9 @@ const useDealProductTableState = () => {
notifications.guess(ok, { message });
return;
}
await DealService.getDealById({ dealId: selectedDeal.id })
.then(setSelectedDeal);
await DealService.getDealById({
dealId: selectedDeal.id,
}).then(setSelectedDeal);
});
},
labels: {
@@ -281,7 +282,6 @@ const DealEditDrawerProductsTable = () => {
onMultipleDelete={onProductMultipleDelete}
onDelete={onProductDelete}
onCreate={onProductCreate}
/>
);
};
@@ -295,10 +295,7 @@ const useDealStatusChangeState = () => {
const DealEditDrawerStatusChangeTable = () => {
const { statusHistory } = useDealStatusChangeState();
return (
<DealStatusChangeTable
items={statusHistory}
/>);
return <DealStatusChangeTable items={statusHistory} />;
};
const useDealEditDrawerState = () => {
@@ -328,53 +325,51 @@ const DealEditDrawer: FC = () => {
body: {
height: "100%",
display: "flex",
flexDirection: "column", gap: rem(10),
flexDirection: "column",
gap: rem(10),
},
}}
>
}}>
<Tabs
defaultValue={"general"}
flex={1}
variant={"outline"}
orientation={"vertical"}
keepMounted={false}
>
<Tabs.List
>
<Tabs.Tab value={"general"} leftSection={<IconSettings />}>
keepMounted={false}>
<Tabs.List>
<Tabs.Tab
value={"general"}
leftSection={<IconSettings />}>
Общее
</Tabs.Tab>
<Tabs.Tab value={"history"} leftSection={<IconCalendarUser />}>
<Tabs.Tab
value={"history"}
leftSection={<IconCalendarUser />}>
История
</Tabs.Tab>
<Tabs.Tab value={"servicesAndProducts"} leftSection={<IconBox />}>
<Tabs.Tab
value={"servicesAndProducts"}
leftSection={<IconBox />}>
Товары и услуги
</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value={"general"}>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2 }}
>
<Box h={"100%"} w={"100%"} p={rem(10)}>
transition={{ duration: 0.2 }}>
<Box
h={"100%"}
w={"100%"}
p={rem(10)}>
<DealEditDrawerGeneralTab />
</Box>
</motion.div>
</Tabs.Panel>
<Tabs.Panel value={"history"}>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2 }}
>
transition={{ duration: 0.2 }}>
<Box p={rem(10)}>
<DealEditDrawerStatusChangeTable />
</Box>
@@ -382,11 +377,9 @@ const DealEditDrawer: FC = () => {
</Tabs.Panel>
<Tabs.Panel value={"servicesAndProducts"}>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2 }}
>
transition={{ duration: 0.2 }}>
<Box p={rem(10)}>
<ProductAndServiceTab />
</Box>
@@ -407,4 +400,4 @@ const DealEditDrawer: FC = () => {
);
};
export default DealEditDrawer;
export default DealEditDrawer;

View File

@@ -14,13 +14,20 @@ import {
Tooltip,
} from "@mantine/core";
import { useForm } from "@mantine/form";
import { ClientService, DealSchema, DealService, ShippingWarehouseSchema } from "../../../../../client";
import { DealStatus, DealStatusDictionary } from "../../../../../shared/enums/DealStatus.ts";
import {
ClientService,
DealSchema,
DealService,
ShippingWarehouseSchema,
} from "../../../../../client";
import {
DealStatus,
DealStatusDictionary,
} from "../../../../../shared/enums/DealStatus.ts";
import { isEqual } from "lodash";
import { notifications } from "../../../../../shared/lib/notifications.ts";
import { useQueryClient } from "@tanstack/react-query";
import ShippingWarehouseAutocomplete
from "../../../../../components/Selects/ShippingWarehouseAutocomplete/ShippingWarehouseAutocomplete.tsx";
import ShippingWarehouseAutocomplete from "../../../../../components/Selects/ShippingWarehouseAutocomplete/ShippingWarehouseAutocomplete.tsx";
import { ButtonCopyControlled } from "../../../../../components/ButtonCopyControlled/ButtonCopyControlled.tsx";
import { useClipboard } from "@mantine/hooks";
import ButtonCopy from "../../../../../components/ButtonCopy/ButtonCopy.tsx";
@@ -29,10 +36,10 @@ import { getCurrentDateTimeForFilename } from "../../../../../shared/lib/date.ts
import { IconPrinter } from "@tabler/icons-react";
type Props = {
deal: DealSchema
}
deal: DealSchema;
};
type FormType = Omit<DealSchema, "statusHistory" | "services" | "products">
type FormType = Omit<DealSchema, "statusHistory" | "services" | "products">;
const Content: FC<Props> = ({ deal }) => {
const { setSelectedDeal } = useDealPageContext();
@@ -40,31 +47,34 @@ const Content: FC<Props> = ({ deal }) => {
const queryClient = useQueryClient();
const initialValues: FormType = deal;
const form = useForm<FormType>(
{
initialValues: initialValues,
validate: {
name: (value: string) => value.length > 0 ? null : "Название сделки не может быть пустым",
},
const form = useForm<FormType>({
initialValues: initialValues,
validate: {
name: (value: string) =>
value.length > 0
? null
: "Название сделки не может быть пустым",
},
);
});
const updateDealInfo = async (values: FormType) => {
return DealService.updateDealGeneralInfo({
requestBody: {
dealId: deal.id,
data: { ...values, shippingWarehouse: values.shippingWarehouse?.toString() },
data: {
...values,
shippingWarehouse: values.shippingWarehouse?.toString(),
},
},
}).then(({ ok, message }) => {
notifications.guess(ok, { message });
if (!ok) return;
DealService.getDealById({ dealId: deal.id })
.then((data) => {
setSelectedDeal(data);
form.setInitialValues(data);
queryClient.invalidateQueries({
queryKey: ["getDealSummaries"],
});
DealService.getDealById({ dealId: deal.id }).then(data => {
setSelectedDeal(data);
form.setInitialValues(data);
queryClient.invalidateQueries({
queryKey: ["getDealSummaries"],
});
});
});
};
const updateClientInfo = async (values: FormType) => {
@@ -72,8 +82,7 @@ const Content: FC<Props> = ({ deal }) => {
requestBody: {
data: values.client,
},
}).then(({ ok, message }) =>
notifications.guess(ok, { message }));
}).then(({ ok, message }) => notifications.guess(ok, { message }));
};
const handleSubmit = async (values: FormType) => {
// Updating client info if there changes
@@ -83,9 +92,10 @@ const Content: FC<Props> = ({ deal }) => {
// updating deal info
await updateDealInfo(values);
};
const isShippingWarehouse = (value: (ShippingWarehouseSchema | string | null | undefined)): value is ShippingWarehouseSchema => {
return !["string", "null", "undefined"].includes((typeof value));
const isShippingWarehouse = (
value: ShippingWarehouseSchema | string | null | undefined
): value is ShippingWarehouseSchema => {
return !["string", "null", "undefined"].includes(typeof value);
};
const onCopyGuestUrlClick = () => {
@@ -94,18 +104,20 @@ const Content: FC<Props> = ({ deal }) => {
dealId: deal.id,
},
}).then(({ ok, message, url }) => {
if (!ok)
notifications.guess(ok, { message });
clipboard.copy(
`${window.location.origin}/${url}`,
);
if (!ok) notifications.guess(ok, { message });
clipboard.copy(`${window.location.origin}/${url}`);
});
};
return (
<form onSubmit={form.onSubmit((values) => handleSubmit(values))}>
<Flex direction={"column"} justify={"space-between"} h={"100%"}>
<form onSubmit={form.onSubmit(values => handleSubmit(values))}>
<Flex
direction={"column"}
justify={"space-between"}
h={"100%"}>
<Fieldset legend={`Общие параметры [ID: ${deal.id}]`}>
<Flex direction={"column"} gap={rem(10)}>
<Flex
direction={"column"}
gap={rem(10)}>
<TextInput
placeholder={"Название сделки"}
label={"Название сделки"}
@@ -115,20 +127,26 @@ const Content: FC<Props> = ({ deal }) => {
disabled
placeholder={"Дата создания"}
label={"Дата создания"}
value={new Date(deal.createdAt).toLocaleString("ru-RU")}
value={new Date(deal.createdAt).toLocaleString(
"ru-RU"
)}
/>
<TextInput
disabled
placeholder={"Текущий статус"}
label={"Текущий статус"}
value={DealStatusDictionary[deal.currentStatus as DealStatus]} />
value={
DealStatusDictionary[
deal.currentStatus as DealStatus
]
}
/>
{deal.category && (
<TextInput
disabled
placeholder={"Категория"}
label={"Категория"}
value={deal.category.name}
/>
)}
<Textarea
@@ -139,27 +157,46 @@ const Content: FC<Props> = ({ deal }) => {
<ShippingWarehouseAutocomplete
placeholder={"Введите склад отгрузки"}
label={"Склад отгрузки"}
value={isShippingWarehouse(form.values.shippingWarehouse) ? form.values.shippingWarehouse : undefined}
value={
isShippingWarehouse(
form.values.shippingWarehouse
)
? form.values.shippingWarehouse
: undefined
}
onChange={event => {
if (isShippingWarehouse(event)) {
form.getInputProps("shippingWarehouse").onChange(event.name);
form.getInputProps(
"shippingWarehouse"
).onChange(event.name);
return;
}
form.getInputProps("shippingWarehouse").onChange(event);
form.getInputProps(
"shippingWarehouse"
).onChange(event);
}}
/>
</Flex>
</Fieldset>
<Flex mt={"md"} gap={rem(10)} align={"center"} justify={"flex-end"}>
<Flex align={"center"} gap={rem(10)} justify={"center"}>
<Flex gap={rem(10)} align={"center"} justify={"space-between"}>
<Tooltip
label={"Распечатать сделку"}
>
<Flex
mt={"md"}
gap={rem(10)}
align={"center"}
justify={"flex-end"}>
<Flex
align={"center"}
gap={rem(10)}
justify={"center"}>
<Flex
gap={rem(10)}
align={"center"}
justify={"space-between"}>
<Tooltip label={"Распечатать сделку"}>
<ActionIcon
onClick={() => {
const pdfWindow = window.open(`${import.meta.env.VITE_API_URL}/deal/detailedDocument/${deal.id}`);
const pdfWindow = window.open(
`${import.meta.env.VITE_API_URL}/deal/detailedDocument/${deal.id}`
);
if (!pdfWindow) return;
pdfWindow.print();
}}
@@ -169,38 +206,42 @@ const Content: FC<Props> = ({ deal }) => {
</Tooltip>
<Flex gap={rem(10)}>
{(deal.billRequest && deal.billRequest.pdfUrl) ?
{deal.billRequest && deal.billRequest.pdfUrl ? (
<ButtonCopy
onCopiedLabel={"Ссылка скопирована в буфер обмена"}
value={deal.billRequest.pdfUrl}
>
onCopiedLabel={
"Ссылка скопирована в буфер обмена"
}
value={deal.billRequest.pdfUrl}>
Ссылка на оплату
</ButtonCopy>
:
) : (
<ButtonCopyControlled
onCopyClick={() => {
const date = getCurrentDateTimeForFilename();
FileSaver.saveAs(`${import.meta.env.VITE_API_URL}/deal/document/${deal.id}`,
`bill_${deal.id}_${date}.pdf`);
const date =
getCurrentDateTimeForFilename();
FileSaver.saveAs(
`${import.meta.env.VITE_API_URL}/deal/document/${deal.id}`,
`bill_${deal.id}_${date}.pdf`
);
}}
copied={false}
onCopiedLabel={"Ссылка скопирована в буфер обмена"}
>
onCopiedLabel={
"Ссылка скопирована в буфер обмена"
}>
Ссылка на оплату (PDF)
</ButtonCopyControlled>
}
)}
<ButtonCopyControlled
onCopyClick={onCopyGuestUrlClick}
onCopiedLabel={"Ссылка скопирована в буфер обмена"}
onCopiedLabel={
"Ссылка скопирована в буфер обмена"
}
copied={clipboard.copied}>
Ссылка на редактирование
</ButtonCopyControlled>
</Flex>
</Flex>
<Flex gap={rem(10)}>
<Checkbox
label={"Оплачен"}
checked={deal.billRequest?.paid || false}
@@ -216,36 +257,33 @@ const Content: FC<Props> = ({ deal }) => {
{...form.getInputProps("isDeleted")}
/>
</Flex>
</Flex>
<Divider
orientation={"vertical"}
/>
<Group align={"center"} justify={"center"}>
<Divider orientation={"vertical"} />
<Group
align={"center"}
justify={"center"}>
<Button
color={"red"}
type={"reset"}
disabled={isEqual(initialValues, form.values)}
onClick={() => form.reset()}
>Отменить изменения</Button>
onClick={() => form.reset()}>
Отменить изменения
</Button>
<Button
variant={"default"}
type={"submit"}
disabled={isEqual(initialValues, form.values)}
>Сохранить изменения</Button>
disabled={isEqual(initialValues, form.values)}>
Сохранить изменения
</Button>
</Group>
</Flex>
</Flex>
</form>
);
};
const DealEditDrawerGeneralTab: FC = () => {
const { selectedDeal } = useDealPageContext();
if (!selectedDeal) return <>No deal selected</>;
return (
<Content deal={selectedDeal} />
);
return <Content deal={selectedDeal} />;
};
export default DealEditDrawerGeneralTab;
export default DealEditDrawerGeneralTab;

View File

@@ -1,21 +1,22 @@
import {useQuery} from "@tanstack/react-query";
import {DealService} from "../../../client";
import { useQuery } from "@tanstack/react-query";
import { DealService } from "../../../client";
import ObjectList from "../../../hooks/objectList.tsx";
export const useDealSummaries = () => {
const {data: summariesRaw = [], refetch} = useQuery({
queryKey: ['getDealSummaries'],
queryFn: () => DealService.getDealSummaries({full: false}),
select: data => data.summaries || [] // Трансформируем полученные данные
const { data: summariesRaw = [], refetch } = useQuery({
queryKey: ["getDealSummaries"],
queryFn: () => DealService.getDealSummaries({ full: false }),
select: data => data.summaries || [], // Трансформируем полученные данные
});
// Теперь summaries будет содержать либо трансформированные данные, либо пустой массив по умолчанию
// isLoading и isError могут быть использованы для отображения индикаторов загрузки или ошибки
return {summariesRaw, refetch};
}
export const useDealSummariesFull = () => ObjectList({
queryFn: () => DealService.getDealSummaries({full: true}),
queryKey: "getDealSummariesFull",
getObjectsFn: response => response.summaries
});
return { summariesRaw, refetch };
};
export const useDealSummariesFull = () =>
ObjectList({
queryFn: () => DealService.getDealSummaries({ full: true }),
queryKey: "getDealSummariesFull",
getObjectsFn: response => response.summaries,
});

View File

@@ -1 +1 @@
export {LeadsPage} from './ui/LeadsPage.tsx';
export { LeadsPage } from "./ui/LeadsPage.tsx";

View File

@@ -1,5 +1,7 @@
import { ContextModalProps } from "@mantine/modals";
import BaseFormModal, { CreateEditFormProps } from "../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
import BaseFormModal, {
CreateEditFormProps,
} from "../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
import { DealProductSchema, DealProductServiceSchema } from "../../../client";
import { useForm } from "@mantine/form";
import { NumberInput } from "@mantine/core";
@@ -9,41 +11,48 @@ import { omit } from "lodash";
type RestProps = {
clientId: number;
productIds?: number[];
}
};
type Props = CreateEditFormProps<DealProductSchema> & RestProps;
const AddDealProductModal = ({
context,
id,
innerProps
}: ContextModalProps<Props>) => {
const isEditing = 'element' in innerProps;
const restProps = omit(innerProps, ['clientId']);
context,
id,
innerProps,
}: ContextModalProps<Props>) => {
const isEditing = "element" in innerProps;
const restProps = omit(innerProps, ["clientId"]);
const validateServices = (services?: DealProductServiceSchema[]) => {
if (!services || services.length == 0) return null;
return services.find(service => service.service === undefined) ? "Удалите пустые услуги" : null;
}
return services.find(service => service.service === undefined)
? "Удалите пустые услуги"
: null;
};
const form = useForm<Partial<DealProductSchema>>({
initialValues: isEditing ? innerProps.element : {
product: undefined,
services: [],
quantity: 1
},
initialValues: isEditing
? innerProps.element
: {
product: undefined,
services: [],
quantity: 1,
},
validate: {
product: (product?: DealProductSchema['product']) => product !== undefined ? null : "Необходимо выбрать товар",
quantity: (quantity?: number) => (quantity && quantity > 0) ? null : "Количество должно быть больше 0",
services: validateServices
}
product: (product?: DealProductSchema["product"]) =>
product !== undefined ? null : "Необходимо выбрать товар",
quantity: (quantity?: number) =>
quantity && quantity > 0
? null
: "Количество должно быть больше 0",
services: validateServices,
},
});
const onClose = () => {
context.closeContextModal(id);
}
};
return (
<BaseFormModal
{...restProps as CreateEditFormProps<DealProductSchema>}
{...(restProps as CreateEditFormProps<DealProductSchema>)}
form={form}
closeOnSubmit
onClose={onClose}>
@@ -54,14 +63,16 @@ const AddDealProductModal = ({
label={"Товар"}
clientId={innerProps.clientId}
disabled={isEditing}
filterBy={(item) => !(innerProps.productIds || []).includes(item.id)}
{...form.getInputProps('product')}
filterBy={item =>
!(innerProps.productIds || []).includes(item.id)
}
{...form.getInputProps("product")}
/>
<NumberInput
placeholder={"Введите количество"}
label={"Количество"}
min={1}
{...form.getInputProps('quantity')}
{...form.getInputProps("quantity")}
/>
{/*<Fieldset legend={'Услуги'}>*/}
{/* <DealProductServiceTable*/}
@@ -70,13 +81,10 @@ const AddDealProductModal = ({
{/* BaseFormInputProps<DealProductServiceSchema[]>}*/}
{/* />*/}
{/*</Fieldset>*/}
</>
</BaseFormModal.Body>
</BaseFormModal>
);
};
)
}
export default AddDealProductModal;
export default AddDealProductModal;

View File

@@ -1,50 +1,72 @@
import {ContextModalProps} from "@mantine/modals";
import BaseFormModal, {CreateEditFormProps} from "../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
import { ContextModalProps } from "@mantine/modals";
import BaseFormModal, {
CreateEditFormProps,
} from "../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
import { DealServiceSchema, ServicePriceCategorySchema } from "../../../client";
import {useForm} from "@mantine/form";
import {ComboboxItem, ComboboxItemGroup, NumberInput, OptionsFilter} from "@mantine/core";
import { useForm } from "@mantine/form";
import {
ComboboxItem,
ComboboxItemGroup,
NumberInput,
OptionsFilter,
} from "@mantine/core";
import ServiceWithPriceInput from "../../../components/ServiceWithPriceInput/ServiceWithPriceInput.tsx";
import {ServiceType} from "../../../shared/enums/ServiceType.ts";
import {useSelector} from "react-redux";
import {RootState} from "../../../redux/store.ts";
import { ServiceType } from "../../../shared/enums/ServiceType.ts";
import { useSelector } from "react-redux";
import { RootState } from "../../../redux/store.ts";
type RestProps = {
serviceIds?: number[];
category?: ServicePriceCategorySchema;
}
};
type Props = CreateEditFormProps<Partial<DealServiceSchema>> & RestProps;
const AddDealServiceModal = ({
context,
id,
innerProps
}: ContextModalProps<Props>) => {
context,
id,
innerProps,
}: ContextModalProps<Props>) => {
const authState = useSelector((state: RootState) => state.auth);
console.log(innerProps.category)
const isEditing = 'element' in innerProps;
console.log(innerProps.category);
const isEditing = "element" in innerProps;
const form = useForm<Partial<DealServiceSchema>>({
initialValues: isEditing ? innerProps.element : {
service: undefined,
quantity: 1,
employees: []
},
initialValues: isEditing
? innerProps.element
: {
service: undefined,
quantity: 1,
employees: [],
},
validate: {
service: (service?: DealServiceSchema['service']) => service !== undefined ? null : "Необходимо выбрать услугу",
quantity: (quantity?: number) => (quantity && quantity > 0) ? null : "Количество должно быть больше 0"
}
service: (service?: DealServiceSchema["service"]) =>
service !== undefined ? null : "Необходимо выбрать услугу",
quantity: (quantity?: number) =>
quantity && quantity > 0
? null
: "Количество должно быть больше 0",
},
});
const onClose = () => {
context.closeContextModal(id);
}
};
const serviceOptionsFilter = ({options}: { options: ComboboxItemGroup[] }) => {
const serviceOptionsFilter = ({
options,
}: {
options: ComboboxItemGroup[];
}) => {
if (!innerProps.serviceIds) return options;
const productServiceIds = innerProps.serviceIds;
return (options as ComboboxItemGroup[]).map(({items, group}) => {
return (options as ComboboxItemGroup[]).map(({ items, group }) => {
return {
group,
items: items.filter(item => !productServiceIds.includes(parseInt((item as ComboboxItem).value)))
}
})
items: items.filter(
item =>
!productServiceIds.includes(
parseInt((item as ComboboxItem).value)
)
),
};
});
};
return (
<BaseFormModal
@@ -57,24 +79,24 @@ const AddDealServiceModal = ({
<ServiceWithPriceInput
category={innerProps.category}
serviceProps={{
...form.getInputProps('service'),
...form.getInputProps("service"),
label: "Услуга",
placeholder: "Выберите услугу",
style: {width: '100%'},
style: { width: "100%" },
disabled: isEditing,
filter: serviceOptionsFilter as OptionsFilter
filter: serviceOptionsFilter as OptionsFilter,
}}
priceProps={{
...form.getInputProps('price'),
...form.getInputProps("price"),
label: "Цена",
placeholder: "Введите цену",
style: {width: '100%'},
disabled: authState.isGuest
style: { width: "100%" },
disabled: authState.isGuest,
}}
quantity={form.values.quantity || 1}
containerProps={{
direction: "column",
style: {width: "100%"}
style: { width: "100%" },
}}
filterType={ServiceType.DEAL_SERVICE}
lockOnEdit={isEditing}
@@ -83,13 +105,12 @@ const AddDealServiceModal = ({
placeholder={"Введите количество"}
label={"Количество"}
min={1}
{...form.getInputProps('quantity')}
{...form.getInputProps("quantity")}
/>
</>
</BaseFormModal.Body>
</BaseFormModal>
)
}
);
};
export default AddDealServiceModal;
export default AddDealServiceModal;

View File

@@ -1,5 +1,11 @@
import BaseFormModal, { CreateEditFormProps } from "../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
import { DealProductServiceSchema, ServicePriceCategorySchema, ServiceSchema } from "../../../client";
import BaseFormModal, {
CreateEditFormProps,
} from "../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
import {
DealProductServiceSchema,
ServicePriceCategorySchema,
ServiceSchema,
} from "../../../client";
import { ContextModalProps } from "@mantine/modals";
import { useForm, UseFormReturnType } from "@mantine/form";
import { isNil, isNumber } from "lodash";
@@ -13,26 +19,31 @@ type RestProps = {
quantity: number;
serviceIds: number[];
category?: ServicePriceCategorySchema;
}
};
type Props = CreateEditFormProps<DealProductServiceSchema> & RestProps;
const ProductServiceFormModal = ({
context,
id, innerProps,
}: ContextModalProps<Props>) => {
context,
id,
innerProps,
}: ContextModalProps<Props>) => {
const authState = useSelector((state: RootState) => state.auth);
const isEditing = "onChange" in innerProps;
const initialValues: Partial<DealProductServiceSchema> = isEditing ? innerProps.element : {
service: undefined,
price: undefined,
employees: [],
};
const initialValues: Partial<DealProductServiceSchema> = isEditing
? innerProps.element
: {
service: undefined,
price: undefined,
employees: [],
};
const form = useForm<Partial<DealProductServiceSchema>>({
initialValues,
validate: {
service: (service?: ServiceSchema) => isNil(service) || service.id < 0 ? "Укажите услугу" : null,
price: (price?: number) => !isNumber(price) || price < 0 ? "Укажите цену" : null,
service: (service?: ServiceSchema) =>
isNil(service) || service.id < 0 ? "Укажите услугу" : null,
price: (price?: number) =>
!isNumber(price) || price < 0 ? "Укажите цену" : null,
},
});
const onClose = () => {
@@ -43,12 +54,10 @@ const ProductServiceFormModal = ({
{...innerProps}
form={form as UseFormReturnType<DealProductServiceSchema>}
onClose={onClose}
closeOnSubmit
>
closeOnSubmit>
<BaseFormModal.Body>
<>
<Flex w={"100%"}>
<ServiceWithPriceInput
category={innerProps.category}
serviceProps={{
@@ -56,7 +65,9 @@ const ProductServiceFormModal = ({
label: "Услуга",
placeholder: "Выберите услугу",
disabled: isEditing,
filterBy: (item) => !innerProps.serviceIds.includes(item.id) || isEditing,
filterBy: item =>
!innerProps.serviceIds.includes(item.id) ||
isEditing,
style: { width: "100%" },
}}
priceProps={{
@@ -70,17 +81,14 @@ const ProductServiceFormModal = ({
containerProps={{
direction: "column",
style: { width: "100%" },
}}
lockOnEdit={isEditing}
quantity={innerProps.quantity}
/>
</Flex>
</>
</BaseFormModal.Body>
</BaseFormModal>
);
};
export default ProductServiceFormModal;
export default ProductServiceFormModal;

View File

@@ -1,9 +1,9 @@
import {DealProductSchema} from "../../../client";
import {ContextModalProps} from "@mantine/modals";
import {Button, Flex, rem} from "@mantine/core";
import {useState} from "react";
import { DealProductSchema } from "../../../client";
import { ContextModalProps } from "@mantine/modals";
import { Button, Flex, rem } from "@mantine/core";
import { useState } from "react";
import ObjectMultiSelect from "../../../components/ObjectMultiSelect/ObjectMultiSelect.tsx";
import {notifications} from "../../../shared/lib/notifications.ts";
import { notifications } from "../../../shared/lib/notifications.ts";
type Props = {
dealProducts: DealProductSchema[];
@@ -11,37 +11,46 @@ type Props = {
onSelect: (
sourceProduct: DealProductSchema,
destinationProducts: DealProductSchema[]
) => void
}
) => void;
};
const SelectDealProductsModal = ({
context,
id,
innerProps
}: ContextModalProps<Props>) => {
context,
id,
innerProps,
}: ContextModalProps<Props>) => {
const [dealProducts, setDealProducts] = useState<DealProductSchema[]>([]);
const onSelectClick = () => {
if (!dealProducts) {
notifications.error({message: "Выберите товары на которые необходимо продублировать услуги"});
notifications.error({
message:
"Выберите товары на которые необходимо продублировать услуги",
});
return;
}
innerProps.onSelect(innerProps.dealProduct, dealProducts);
context.closeContextModal(id);
}
};
const onDuplicateAllClick = () => {
innerProps.onSelect(
innerProps.dealProduct,
innerProps.dealProducts.filter(item => item !== innerProps.dealProduct)
innerProps.dealProducts.filter(
item => item !== innerProps.dealProduct
)
);
context.closeContextModal(id);
}
};
return (
<Flex direction={"column"} gap={rem(10)}>
<Flex
direction={"column"}
gap={rem(10)}>
<Flex>
<ObjectMultiSelect<DealProductSchema>
w={"100%"}
label={"Товары"}
placeholder={"Выберите товары на которые нужно продублировать услуги"}
placeholder={
"Выберите товары на которые нужно продублировать услуги"
}
onChange={setDealProducts}
value={dealProducts}
data={innerProps.dealProducts}
@@ -50,27 +59,25 @@ const SelectDealProductsModal = ({
filterBy={item => item !== innerProps.dealProduct}
/>
</Flex>
<Flex gap={rem(10)} justify={"flex-end"}>
<Flex
gap={rem(10)}
justify={"flex-end"}>
<Button
variant={"subtle"}
onClick={() => context.closeContextModal(id)}
>
onClick={() => context.closeContextModal(id)}>
Отменить
</Button>
<Button
onClick={onDuplicateAllClick}
>
<Button onClick={onDuplicateAllClick}>
Продублировать на все товары
</Button>
<Button
onClick={onSelectClick}
variant={"default"}
>
variant={"default"}>
Продублировать
</Button>
</Flex>
</Flex>
)
}
);
};
export default SelectDealProductsModal;
export default SelectDealProductsModal;

View File

@@ -8,7 +8,6 @@
.container-disabled {
}
.products-list {
width: 60%;
display: flex;
@@ -28,7 +27,6 @@
border: dashed var(--item-border-size) var(--mantine-color-default-border);
border-radius: var(--item-border-radius);
padding: rem(10);
}
.deal-container-buttons {
@@ -37,4 +35,4 @@
flex-direction: column;
margin-top: auto;
width: 100%;
}
}

View File

@@ -1,44 +1,66 @@
import {FC} from "react";
import styles from './ProductAndServiceTab.module.css';
import { FC } from "react";
import styles from "./ProductAndServiceTab.module.css";
import ProductView from "./components/ProductView/ProductView.tsx";
import {Button, Divider, Flex, rem, ScrollArea, Text, Title} from "@mantine/core";
import {
Button,
Divider,
Flex,
rem,
ScrollArea,
Text,
Title,
} from "@mantine/core";
import DealServicesTable from "./components/DealServicesTable/DealServicesTable.tsx";
import useDealProductAndServiceTabState from "./hooks/useProductAndServiceTabState.tsx";
import {modals} from "@mantine/modals";
import { modals } from "@mantine/modals";
import {
BillingService,
DealProductSchema,
DealService,
GetServiceKitSchema,
ProductSchema,
ProductService
ProductService,
} from "../../../../client";
import {notifications} from "../../../../shared/lib/notifications.ts";
import {CreateProductRequest} from "../../../ProductsPage/types.ts";
import { notifications } from "../../../../shared/lib/notifications.ts";
import { CreateProductRequest } from "../../../ProductsPage/types.ts";
import classNames from "classnames";
const ProductAndServiceTab: FC = () => {
const {dealState, dealServicesState, dealProductsState} = useDealProductAndServiceTabState();
const { dealState, dealServicesState, dealProductsState } =
useDealProductAndServiceTabState();
const isLocked = Boolean(dealState.deal?.billRequest);
const onAddProductClick = () => {
if (!dealProductsState.onCreate || !dealState.deal) return;
const productIds = dealState.deal.products.map(product => product.product.id);
const productIds = dealState.deal.products.map(
product => product.product.id
);
modals.openContextModal({
modal: "addDealProduct",
innerProps: {
onCreate: dealProductsState.onCreate,
clientId: dealState.deal.clientId,
productIds: productIds
productIds: productIds,
},
withCloseButton: false
})
}
withCloseButton: false,
});
};
const getTotalPrice = () => {
if (!dealState.deal) return 0
const productServicesPrice = dealState.deal.products.reduce((acc, row) => acc + row.services.reduce((acc2, row2) => acc2 + row2.price * row.quantity, 0), 0);
const dealServicesPrice = dealState.deal.services.reduce((acc, row) => acc + row.price * row.quantity, 0);
if (!dealState.deal) return 0;
const productServicesPrice = dealState.deal.products.reduce(
(acc, row) =>
acc +
row.services.reduce(
(acc2, row2) => acc2 + row2.price * row.quantity,
0
),
0
);
const dealServicesPrice = dealState.deal.services.reduce(
(acc, row) => acc + row.price * row.quantity,
0
);
return dealServicesPrice + productServicesPrice;
}
};
const onCopyServices = (
sourceProduct: DealProductSchema,
destinationProducts: DealProductSchema[]
@@ -47,15 +69,17 @@ const ProductAndServiceTab: FC = () => {
DealService.copyProductServices({
requestBody: {
dealId: dealState.deal.id,
destinationProductIds: destinationProducts.map(product => product.product.id),
sourceProductId: sourceProduct.product.id
}
}).then(async ({ok, message}) => {
notifications.guess(ok, {message});
destinationProductIds: destinationProducts.map(
product => product.product.id
),
sourceProductId: sourceProduct.product.id,
},
}).then(async ({ ok, message }) => {
notifications.guess(ok, { message });
if (!ok) return;
await dealState.refetch()
})
}
await dealState.refetch();
});
};
const onCopyServicesClick = (product: DealProductSchema) => {
modals.openContextModal({
modal: "selectDealProductsModal",
@@ -64,11 +88,11 @@ const ProductAndServiceTab: FC = () => {
innerProps: {
dealProducts: dealState.deal?.products || [],
dealProduct: product,
onSelect: onCopyServices
onSelect: onCopyServices,
},
withCloseButton: false
})
}
withCloseButton: false,
});
};
const onKitAdd = (item: DealProductSchema, kit: GetServiceKitSchema) => {
if (!dealState.deal) return;
@@ -76,55 +100,56 @@ const ProductAndServiceTab: FC = () => {
requestBody: {
dealId: dealState.deal.id,
kitId: kit.id,
productId: item.product.id
}
}).then(async ({ok, message}) => {
notifications.guess(ok, {message});
productId: item.product.id,
},
}).then(async ({ ok, message }) => {
notifications.guess(ok, { message });
if (!ok) return;
await dealState.refetch();
});
}
};
const onDealKitAdd = (kit: GetServiceKitSchema) => {
if (!dealState.deal) return;
DealService.addKitToDeal({
requestBody: {
dealId: dealState.deal.id,
kitId: kit.id,
}
}).then(async ({ok, message}) => {
notifications.guess(ok, {message});
},
}).then(async ({ ok, message }) => {
notifications.guess(ok, { message });
if (!ok) return;
await dealState.refetch();
});
}
};
const onCreateProduct = (newProduct: CreateProductRequest) => {
ProductService.createProduct({
requestBody: newProduct
}).then(({ok, message}) => {
notifications.guess(ok, {message: message});
})
}
requestBody: newProduct,
}).then(({ ok, message }) => {
notifications.guess(ok, { message: message });
});
};
const onCreateProductClick = () => {
if (!dealState.deal) return;
modals.openContextModal({
modal: "createProduct",
title: 'Создание товара',
title: "Создание товара",
withCloseButton: false,
innerProps: {
clientId: dealState.deal.clientId,
onCreate: onCreateProduct
}
})
}
onCreate: onCreateProduct,
},
});
};
const onProductEdit = (product: ProductSchema) => {
ProductService.updateProduct({requestBody: {product}})
.then(async ({ok, message}) => {
notifications.guess(ok, {message});
ProductService.updateProduct({ requestBody: { product } }).then(
async ({ ok, message }) => {
notifications.guess(ok, { message });
if (!ok) return;
await dealState.refetch();
})
}
}
);
};
const onCreateBillClick = () => {
if (!dealState.deal) return;
@@ -132,67 +157,69 @@ const ProductAndServiceTab: FC = () => {
modals.openConfirmModal({
withCloseButton: false,
size: "xl",
children:
<Text style={{textAlign: "justify"}}>
Создание заявки на выставление счета, подтвержденное нажатием кнопки "Выставить", заблокирует
возможность
редактирования товаров и услуг сделки. Пожалуйста, проверьте всю информацию на точность и полноту
перед подтверждением.
</Text>,
children: (
<Text style={{ textAlign: "justify" }}>
Создание заявки на выставление счета, подтвержденное
нажатием кнопки "Выставить", заблокирует возможность
редактирования товаров и услуг сделки. Пожалуйста, проверьте
всю информацию на точность и полноту перед подтверждением.
</Text>
),
onConfirm: () => {
BillingService.createDealBill({
requestBody: {
dealId
}
}).then(async ({ok, message}) => {
notifications.guess(ok, {message});
if (ok) notifications.success({message: "Ссылка на оплату доступна во вкладе общее"});
dealId,
},
}).then(async ({ ok, message }) => {
notifications.guess(ok, { message });
if (ok)
notifications.success({
message:
"Ссылка на оплату доступна во вкладе общее",
});
await dealState.refetch();
})
});
},
labels: {
confirm: "Выставить",
cancel: "Отмена"
}
cancel: "Отмена",
},
});
}
};
const onCancelBillClick = () => {
if (!dealState.deal) return;
const dealId = dealState.deal.id;
modals.openConfirmModal({
withCloseButton: false,
children:
<Text style={{textAlign: "justify"}}>
children: (
<Text style={{ textAlign: "justify" }}>
Вы уверены что хотите отозвать заявку на оплату?
</Text>,
</Text>
),
onConfirm: () => {
BillingService.cancelDealBill({
requestBody: {
dealId
}
}).then(async ({ok, message}) => {
notifications.guess(ok, {message});
dealId,
},
}).then(async ({ ok, message }) => {
notifications.guess(ok, { message });
await dealState.refetch();
})
});
},
labels: {
confirm: "Отозвать",
cancel: "Отмена"
cancel: "Отмена",
},
});
}
};
return (
<div
className={
classNames(styles['container'],
dealState.deal?.billRequest && styles['container-disabled']
)
}>
<div className={styles['products-list']}>
className={classNames(
styles["container"],
dealState.deal?.billRequest && styles["container-disabled"]
)}>
<div className={styles["products-list"]}>
<ScrollArea offsetScrollbars>
{dealState.deal?.products.map(product => (
<ProductView
onProductEdit={onProductEdit}
@@ -205,61 +232,66 @@ const ProductAndServiceTab: FC = () => {
/>
))}
</ScrollArea>
</div>
<div className={styles['deal-container']}>
<div className={styles["deal-container"]}>
<ScrollArea offsetScrollbars>
<Flex direction={"column"} className={styles['deal-container-wrapper']}>
<Flex
direction={"column"}
className={styles["deal-container-wrapper"]}>
<DealServicesTable
onKitAdd={onDealKitAdd}
{...dealServicesState}
/>
<Divider my={rem(15)}/>
<div className={styles['deal-container-buttons']}>
<Divider my={rem(15)} />
<div className={styles["deal-container-buttons"]}>
<Button
disabled={isLocked}
variant={"default"}
fullWidth
onClick={onCreateProductClick}
>Создать товар</Button>
onClick={onCreateProductClick}>
Создать товар
</Button>
<Button
disabled={isLocked}
onClick={onAddProductClick}
variant={"default"}
fullWidth>Добавить товар</Button>
fullWidth>
Добавить товар
</Button>
</div>
<Divider my={rem(15)}/>
<div className={styles['deal-container-buttons']}>
{isLocked ? <Button
<Divider my={rem(15)} />
<div className={styles["deal-container-buttons"]}>
{isLocked ? (
<Button
onClick={onCancelBillClick}
color={"red"}
>
color={"red"}>
Отозвать счет
</Button> :
</Button>
) : (
<Button
disabled={isLocked}
onClick={onCreateBillClick}
variant={"default"}
fullWidth>Выставить счет</Button>
}
fullWidth>
Выставить счет
</Button>
)}
</div>
</Flex>
<Flex direction={"column"} className={styles['deal-container-wrapper']}>
<Title order={3}>Общая стоимость всех услуг: {getTotalPrice().toLocaleString("ru")}</Title>
<Flex
direction={"column"}
className={styles["deal-container-wrapper"]}>
<Title order={3}>
Общая стоимость всех услуг:{" "}
{getTotalPrice().toLocaleString("ru")}
</Title>
</Flex>
</ScrollArea>
</div>
</div>
)
}
);
};
export default ProductAndServiceTab;
export default ProductAndServiceTab;

View File

@@ -1,7 +1,21 @@
import { CRUDTableProps } from "../../../../../../types/CRUDTable.tsx";
import { DealServiceSchema, GetServiceKitSchema, UserSchema } from "../../../../../../client";
import {
DealServiceSchema,
GetServiceKitSchema,
UserSchema,
} from "../../../../../../client";
import { FC, useState } from "react";
import { ActionIcon, Button, Flex, Modal, NumberInput, rem, Text, Title, Tooltip } from "@mantine/core";
import {
ActionIcon,
Button,
Flex,
Modal,
NumberInput,
rem,
Text,
Title,
Tooltip,
} from "@mantine/core";
import { IconTrash, IconUsersGroup } from "@tabler/icons-react";
import { modals } from "@mantine/modals";
import { isNumber } from "lodash";
@@ -12,16 +26,24 @@ import { RootState } from "../../../../../../redux/store.ts";
import useDealProductAndServiceTabState from "../../hooks/useProductAndServiceTabState.tsx";
type RestProps = {
onKitAdd?: (kit: GetServiceKitSchema) => void
onKitAdd?: (kit: GetServiceKitSchema) => void;
};
type Props = CRUDTableProps<DealServiceSchema> & RestProps;
const DealServicesTable: FC<Props> = ({ items, onDelete, onCreate, onChange, onKitAdd }) => {
const DealServicesTable: FC<Props> = ({
items,
onDelete,
onCreate,
onChange,
onKitAdd,
}) => {
const authState = useSelector((state: RootState) => state.auth);
const { dealState } = useDealProductAndServiceTabState();
const isLocked = Boolean(dealState.deal?.billRequest);
const [currentService, setCurrentService] = useState<DealServiceSchema | undefined>();
const [currentService, setCurrentService] = useState<
DealServiceSchema | undefined
>();
const [employeesModalVisible, setEmployeesModalVisible] = useState(false);
const onDeleteClick = (item: DealServiceSchema) => {
@@ -30,7 +52,7 @@ const DealServicesTable: FC<Props> = ({ items, onDelete, onCreate, onChange, onK
};
const onCreateClick = () => {
if (!onCreate) return;
console.log("228")
console.log("228");
const serviceIds = items.map(service => service.service.id);
modals.openContextModal({
modal: "addDealService",
@@ -68,7 +90,9 @@ const DealServicesTable: FC<Props> = ({ items, onDelete, onCreate, onChange, onK
};
const getCurrentEmployees = (): UserSchema[] => {
if (!currentService) return [];
const item = items.find(i => i.service.id === currentService.service.id);
const item = items.find(
i => i.service.id === currentService.service.id
);
if (!item) return [];
return item.employees;
};
@@ -88,7 +112,6 @@ const DealServicesTable: FC<Props> = ({ items, onDelete, onCreate, onChange, onK
serviceType: ServiceType.DEAL_SERVICE,
},
withCloseButton: false,
});
};
return (
@@ -96,9 +119,7 @@ const DealServicesTable: FC<Props> = ({ items, onDelete, onCreate, onChange, onK
<Flex
direction={"column"}
gap={rem(10)}
h={"100%"}
>
h={"100%"}>
<Flex
h={"100%"}
direction={"column"}>
@@ -106,22 +127,20 @@ const DealServicesTable: FC<Props> = ({ items, onDelete, onCreate, onChange, onK
order={3}
w={"100%"}
style={{ textAlign: "center" }}
mb={rem(10)}
>Общие услуги</Title>
mb={rem(10)}>
Общие услуги
</Title>
<Flex
direction={"column"}
gap={rem(10)}
>
gap={rem(10)}>
{items.map(service => (
<Flex
key={service.service.id}
w={"100%"}
gap={rem(10)}
align={"center"}
>
align={"center"}>
<Tooltip
onClick={() => onDeleteClick(service)}
label="Удалить услугу">
<ActionIcon
@@ -130,26 +149,34 @@ const DealServicesTable: FC<Props> = ({ items, onDelete, onCreate, onChange, onK
<IconTrash />
</ActionIcon>
</Tooltip>
{!authState.isGuest &&
{!authState.isGuest && (
<Tooltip label="Сотрудники">
<ActionIcon onClick={() => onEmployeeClick(service)} variant={"default"}>
<ActionIcon
onClick={() =>
onEmployeeClick(service)
}
variant={"default"}>
<IconUsersGroup />
</ActionIcon>
</Tooltip>
}
<Text
flex={1}
>{service.service.name}</Text>
)}
<Text flex={1}>{service.service.name}</Text>
<NumberInput
disabled={isLocked}
flex={1}
suffix={" шт."}
onChange={event => isNumber(event) && onQuantityChange(service, event)}
onChange={event =>
isNumber(event) &&
onQuantityChange(service, event)
}
value={service.quantity}
/>
<NumberInput
flex={1}
onChange={event => isNumber(event) && onPriceChange(service, event)}
onChange={event =>
isNumber(event) &&
onPriceChange(service, event)
}
suffix={"₽"}
value={service.price}
disabled={authState.isGuest || isLocked}
@@ -161,22 +188,31 @@ const DealServicesTable: FC<Props> = ({ items, onDelete, onCreate, onChange, onK
<Title
style={{ textAlign: "end" }}
mt={rem(10)}
order={3}
>Итог: {items.reduce((acc, item) => acc + (item.price * item.quantity), 0)}</Title>
order={3}>
Итог:{" "}
{items.reduce(
(acc, item) => acc + item.price * item.quantity,
0
)}
</Title>
</Flex>
<Flex direction={"column"} gap={rem(10)} mt={"auto"}>
<Flex
direction={"column"}
gap={rem(10)}
mt={"auto"}>
<Button
disabled={isLocked}
onClick={onCreateClick}
fullWidth
variant={"default"}
>Добавить услугу</Button>
variant={"default"}>
Добавить услугу
</Button>
<Button
disabled={isLocked}
onClick={onAddKitClick}
fullWidth
variant={"default"}
>
variant={"default"}>
Добавить набор услуг
</Button>
</Flex>
@@ -185,19 +221,17 @@ const DealServicesTable: FC<Props> = ({ items, onDelete, onCreate, onChange, onK
title={"Добавление сотрудника к услуге"}
opened={employeesModalVisible}
onClose={onEmployeeModalClose}
size={"xl"}
>
<Flex direction={"column"} gap={rem(10)}>
size={"xl"}>
<Flex
direction={"column"}
gap={rem(10)}>
<SimpleUsersTable
items={getCurrentEmployees()}
onChange={onEmployeesChange}
/>
</Flex>
</Modal>
</>
);
};
export default DealServicesTable;
export default DealServicesTable;

View File

@@ -16,18 +16,18 @@ type RestProps = {
quantity: number;
onCopyServices?: () => void;
onKitAdd?: () => void;
}
};
type Props = CRUDTableProps<DealProductServiceSchema> & RestProps;
const ProductServicesTable: FC<Props> = ({
items,
quantity,
onCreate,
onDelete,
onChange,
onCopyServices,
onKitAdd,
}) => {
items,
quantity,
onCreate,
onDelete,
onChange,
onCopyServices,
onKitAdd,
}) => {
const { dealState } = useDealProductAndServiceTabState();
const isLocked = Boolean(dealState.deal?.billRequest);
const authState = useSelector((state: RootState) => state.auth);
@@ -35,7 +35,9 @@ const ProductServicesTable: FC<Props> = ({
const columns = useProductServicesTableColumns({ data: items, quantity });
const serviceIds = items.map(service => service.service.id);
const [currentService, setCurrentService] = useState<DealProductServiceSchema | undefined>();
const [currentService, setCurrentService] = useState<
DealProductServiceSchema | undefined
>();
const [employeesModalVisible, setEmployeesModalVisible] = useState(false);
const onCreateClick = () => {
@@ -62,7 +64,6 @@ const ProductServicesTable: FC<Props> = ({
serviceIds,
quantity,
category: dealState.deal?.category || undefined,
},
withCloseButton: false,
});
@@ -78,7 +79,9 @@ const ProductServicesTable: FC<Props> = ({
};
const getCurrentEmployees = (): UserSchema[] => {
if (!currentService) return [];
const item = items.find(i => i.service.id === currentService.service.id);
const item = items.find(
i => i.service.id === currentService.service.id
);
if (!item) return [];
return item.employees;
};
@@ -93,93 +96,99 @@ const ProductServicesTable: FC<Props> = ({
<>
<Flex
direction={"column"}
gap={rem(10)}
>
gap={rem(10)}>
<BaseTable
data={items}
columns={columns}
restProps={{
enableColumnActions: false,
enableSorting: false,
enableRowActions: true,
enableBottomToolbar: true,
renderBottomToolbar: (
<Flex justify={"flex-end"} gap={rem(10)} p={rem(10)}>
<Button
disabled={isLocked}
onClick={() => onKitAdd && onKitAdd()}
variant={"default"}
>
Добавить набор услуг
</Button>
<Button
disabled={isLocked}
onClick={() => onCopyServices && onCopyServices()}
variant={"default"}
>
Продублировать услуги
</Button>
<Button
disabled={isLocked}
onClick={onCreateClick}
variant={"default"}
>
Добавить услугу
</Button>
</Flex>
),
renderRowActions: ({ row }) => (
<Flex gap="md">
<Tooltip label="Удалить">
<ActionIcon
onClick={() => {
if (onDelete) onDelete(row.original);
}} variant={"default"}>
<IconTrash />
</ActionIcon>
</Tooltip>
<Tooltip
label="Редактировать">
<ActionIcon
onClick={() => onChangeClick(row.original)}
restProps={
{
enableColumnActions: false,
enableSorting: false,
enableRowActions: true,
enableBottomToolbar: true,
renderBottomToolbar: (
<Flex
justify={"flex-end"}
gap={rem(10)}
p={rem(10)}>
<Button
disabled={isLocked}
onClick={() => onKitAdd && onKitAdd()}
variant={"default"}>
<IconEdit />
</ActionIcon>
</Tooltip>
{!authState.isGuest &&
<Tooltip label="Сотрудники">
<ActionIcon onClick={() => onEmployeeClick(row.original)} variant={"default"}>
<IconUsersGroup />
Добавить набор услуг
</Button>
<Button
disabled={isLocked}
onClick={() =>
onCopyServices && onCopyServices()
}
variant={"default"}>
Продублировать услуги
</Button>
<Button
disabled={isLocked}
onClick={onCreateClick}
variant={"default"}>
Добавить услугу
</Button>
</Flex>
),
renderRowActions: ({ row }) => (
<Flex gap="md">
<Tooltip label="Удалить">
<ActionIcon
onClick={() => {
if (onDelete)
onDelete(row.original);
}}
variant={"default"}>
<IconTrash />
</ActionIcon>
</Tooltip>
}
</Flex>
),
} as MRT_TableOptions<DealProductServiceSchema>}
<Tooltip label="Редактировать">
<ActionIcon
onClick={() =>
onChangeClick(row.original)
}
variant={"default"}>
<IconEdit />
</ActionIcon>
</Tooltip>
{!authState.isGuest && (
<Tooltip label="Сотрудники">
<ActionIcon
onClick={() =>
onEmployeeClick(
row.original
)
}
variant={"default"}>
<IconUsersGroup />
</ActionIcon>
</Tooltip>
)}
</Flex>
),
} as MRT_TableOptions<DealProductServiceSchema>
}
/>
</Flex>
<Modal
title={"Добавление сотрудника к услуге"}
opened={employeesModalVisible}
onClose={onEmployeeModalClose}
size={"xl"}
>
<Flex direction={"column"} gap={rem(10)}>
size={"xl"}>
<Flex
direction={"column"}
gap={rem(10)}>
<SimpleUsersTable
items={getCurrentEmployees()}
onChange={onEmployeesChange}
/>
</Flex>
</Modal>
</>
);
};
export default ProductServicesTable;
export default ProductServicesTable;

View File

@@ -1,38 +1,60 @@
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";
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 { 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 totalCost = useMemo(() => data.reduce((acc, row) => acc + ((row.service.cost || 0) * quantity), 0), [data, quantity]);
const hideGuestColumns = [
"service.cost"
]
return useMemo<MRT_ColumnDef<DealProductServiceSchema>[]>(() => [
{
accessorKey: "service.name",
header: "Услуга",
},
{
enableHiding: true,
accessorKey: "service.cost",
header: "Себестоимость",
Footer: () => <>Итоговая себестоимость: {totalCost.toLocaleString("ru")}</>,
},
{
accessorKey: "price",
header: "Цена",
Footer: () => <>Итог: {totalPrice.toLocaleString("ru")}</>,
}
], [totalPrice]).filter(columnDef => !(hideGuestColumns.includes(columnDef.accessorKey || "") && authState.isGuest));
}
const totalPrice = useMemo(
() => data.reduce((acc, row) => acc + row.price * quantity, 0),
[data, quantity]
);
const totalCost = useMemo(
() =>
data.reduce(
(acc, row) => acc + (row.service.cost || 0) * quantity,
0
),
[data, quantity]
);
const hideGuestColumns = ["service.cost"];
return useMemo<MRT_ColumnDef<DealProductServiceSchema>[]>(
() => [
{
accessorKey: "service.name",
header: "Услуга",
},
{
enableHiding: true,
accessorKey: "service.cost",
header: "Себестоимость",
Footer: () => (
<>
Итоговая себестоимость: {totalCost.toLocaleString("ru")}
</>
),
},
{
accessorKey: "price",
header: "Цена",
Footer: () => <>Итог: {totalPrice.toLocaleString("ru")}</>,
},
],
[totalPrice]
).filter(
columnDef =>
!(
hideGuestColumns.includes(columnDef.accessorKey || "") &&
authState.isGuest
)
);
};
export default useProductServicesTableColumns;
export default useProductServicesTableColumns;

View File

@@ -6,7 +6,6 @@
padding: rem(10);
margin-bottom: rem(10);
flex: 1;
}
.image-container {
@@ -21,7 +20,7 @@
display: flex;
flex-direction: column;
gap: rem(10);
flex: 1
flex: 1;
}
.data-container {
@@ -34,4 +33,4 @@
.attributes-container {
overflow-wrap: break-word;
}
}

View File

@@ -1,30 +1,40 @@
import {FC} from "react";
import { FC } from "react";
import {
DealProductSchema,
DealProductServiceSchema,
GetServiceKitSchema,
ProductSchema
ProductSchema,
} from "../../../../../../client";
import styles from './ProductView.module.css';
import {ActionIcon, Box, Flex, Image, NumberInput, rem, Text, Title, Tooltip} from '@mantine/core';
import styles from "./ProductView.module.css";
import {
ActionIcon,
Box,
Flex,
Image,
NumberInput,
rem,
Text,
Title,
Tooltip,
} from "@mantine/core";
import ProductServicesTable from "../ProductServicesTable/ProductServicesTable.tsx";
import {isNil, isNumber} from "lodash";
import {IconBarcode, IconEdit, IconTrash} from "@tabler/icons-react";
import {modals} from "@mantine/modals";
import {ServiceType} from "../../../../../../shared/enums/ServiceType.ts";
import { isNil, isNumber } from "lodash";
import { IconBarcode, IconEdit, IconTrash } from "@tabler/icons-react";
import { modals } from "@mantine/modals";
import { ServiceType } from "../../../../../../shared/enums/ServiceType.ts";
import useDealProductAndServiceTabState from "../../hooks/useProductAndServiceTabState.tsx";
type Props = {
product: DealProductSchema;
onChange?: (item: DealProductSchema) => void;
onDelete?: (item: DealProductSchema) => void
onDelete?: (item: DealProductSchema) => void;
onCopyServices?: (item: DealProductSchema) => void;
onKitAdd?: (item: DealProductSchema, kit: GetServiceKitSchema) => void;
onProductEdit: (product: ProductSchema) => void;
}
};
type ProductFieldNames = {
[K in keyof ProductSchema]: string
}
[K in keyof ProductSchema]: string;
};
export const ProductFieldNames: Partial<ProductFieldNames> = {
color: "Цвет",
article: "Артикул",
@@ -32,95 +42,94 @@ export const ProductFieldNames: Partial<ProductFieldNames> = {
brand: "Бренд",
composition: "Состав",
additionalInfo: "Доп. информация",
}
};
const ProductView: FC<Props> = ({
product,
onDelete,
onChange,
onCopyServices,
onKitAdd,
onProductEdit
}) => {
const {dealState} = useDealProductAndServiceTabState();
product,
onDelete,
onChange,
onCopyServices,
onKitAdd,
onProductEdit,
}) => {
const { dealState } = useDealProductAndServiceTabState();
const isLocked = Boolean(dealState.deal?.billRequest);
const onDeleteClick = () => {
if (!onDelete) return;
onDelete(product);
}
};
const onServiceDelete = (item: DealProductServiceSchema) => {
if (!onChange) return;
onChange({
...product,
services: product.services.filter(service => service.service.id !== item.service.id)
services: product.services.filter(
service => service.service.id !== item.service.id
),
});
}
};
const onServiceCreate = (item: DealProductServiceSchema) => {
if (!onChange) return;
onChange({
...product,
services: [...product.services, item]
})
}
services: [...product.services, item],
});
};
const onServiceChange = (item: DealProductServiceSchema) => {
if (!onChange) return;
onChange({
...product,
services: product.services.map(service => service.service.id === item.service.id ? item : service)
})
}
services: product.services.map(service =>
service.service.id === item.service.id ? item : service
),
});
};
const onQuantityChange = (quantity: number) => {
if (!onChange) return;
onChange({
...product,
quantity
})
}
quantity,
});
};
const onPrintBarcodeClick = () => {
modals.openContextModal({
modal: "printBarcode",
innerProps: {
productId: product.product.id,
defaultQuantity: product.quantity
defaultQuantity: product.quantity,
},
title: 'Печать штрихкода',
})
}
title: "Печать штрихкода",
});
};
const onKitAddClick = () => {
if (!onKitAdd) return;
modals.openContextModal({
modal: "servicesKitSelectModal",
innerProps: {
onSelect: (kit) => onKitAdd(product, kit),
serviceType: ServiceType.PRODUCT_SERVICE
onSelect: kit => onKitAdd(product, kit),
serviceType: ServiceType.PRODUCT_SERVICE,
},
withCloseButton: false
})
}
withCloseButton: false,
});
};
const onProductEditClick = () => {
modals.openContextModal({
modal: "createProduct",
title: 'Редактирование товара',
title: "Редактирование товара",
withCloseButton: false,
innerProps: {
onChange: (newProduct) => onProductEdit(newProduct),
onChange: newProduct => onProductEdit(newProduct),
product: product.product,
}
})
}
},
});
};
return (
<div className={styles['container']}>
<div className={styles['data-container']}>
<div
className={styles['image-container']}>
<div className={styles["container"]}>
<div className={styles["data-container"]}>
<div className={styles["image-container"]}>
<Image
flex={1}
radius={rem(10)}
@@ -128,34 +137,44 @@ const ProductView: FC<Props> = ({
src={product.product.imageUrl}
/>
</div>
<div className={styles['attributes-container']}>
<div className={styles["attributes-container"]}>
<Title order={3}>{product.product.name}</Title>
{/*<Spoiler maxHeight={0} showLabel={"Показать характеристики"} hideLabel={"Скрыть"}>*/}
{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>
{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>
{/*</Spoiler>*/}
</div>
<Box/>
<Box />
<NumberInput
mt={rem(10)}
disabled={isLocked}
suffix={" шт."}
value={product.quantity}
onChange={event => isNumber(event) && onQuantityChange(event)}
onChange={event =>
isNumber(event) && onQuantityChange(event)
}
placeholder={"Введите количество товара"}
/>
</div>
<div className={styles['services-container']}>
<div className={styles["services-container"]}>
<ProductServicesTable
onKitAdd={onKitAddClick}
onCopyServices={() => onCopyServices && onCopyServices(product)}
onCopyServices={() =>
onCopyServices && onCopyServices(product)
}
items={product.services}
quantity={product.quantity}
onCreate={onServiceCreate}
@@ -165,39 +184,34 @@ const ProductView: FC<Props> = ({
<Flex
mt={"auto"}
ml={"auto"}
gap={rem(10)}
>
gap={rem(10)}>
<Tooltip
onClick={onPrintBarcodeClick}
label="Печать штрихкода">
<ActionIcon
variant={"default"}>
<IconBarcode/>
<ActionIcon variant={"default"}>
<IconBarcode />
</ActionIcon>
</Tooltip>
<Tooltip
onClick={onProductEditClick}
label="Редактировать товар">
<ActionIcon
variant={"default"}>
<IconEdit/>
<ActionIcon variant={"default"}>
<IconEdit />
</ActionIcon>
</Tooltip>
<Tooltip onClick={onDeleteClick} label="Удалить товар">
<Tooltip
onClick={onDeleteClick}
label="Удалить товар">
<ActionIcon
disabled={isLocked}
variant={"default"}
>
<IconTrash/>
variant={"default"}>
<IconTrash />
</ActionIcon>
</Tooltip>
</Flex>
</div>
</div>
)
}
);
};
export default ProductView;
export default ProductView;

View File

@@ -1,24 +1,30 @@
import {CRUDTableProps} from "../../../../../types/CRUDTable.tsx";
import {DealProductSchema, DealService, DealServiceSchema} from "../../../../../client";
import {useDealPageContext} from "../../../contexts/DealPageContext.tsx";
import {notifications} from "../../../../../shared/lib/notifications.ts";
import { CRUDTableProps } from "../../../../../types/CRUDTable.tsx";
import {
DealProductSchema,
DealService,
DealServiceSchema,
} from "../../../../../client";
import { useDealPageContext } from "../../../contexts/DealPageContext.tsx";
import { notifications } from "../../../../../shared/lib/notifications.ts";
const useDealState = () => {
const {selectedDeal, setSelectedDeal} = useDealPageContext();
const { selectedDeal, setSelectedDeal } = useDealPageContext();
const refetch = async () => {
if (!selectedDeal) return
return DealService.getDealById({dealId: selectedDeal.id}).then((deal) => {
setSelectedDeal(deal);
})
}
if (!selectedDeal) return;
return DealService.getDealById({ dealId: selectedDeal.id }).then(
deal => {
setSelectedDeal(deal);
}
);
};
return {
deal: selectedDeal,
refetch
}
}
refetch,
};
};
const useDealServicesState = (): CRUDTableProps<DealServiceSchema> => {
const {deal, refetch} = useDealState();
const { deal, refetch } = useDealState();
const onCreate = (item: DealServiceSchema) => {
if (!deal) return;
DealService.addDealService({
@@ -26,92 +32,90 @@ const useDealServicesState = (): CRUDTableProps<DealServiceSchema> => {
dealId: deal.id,
serviceId: item.service.id,
quantity: item.quantity,
price: item.price
}
}).then(async ({ok, message}) => {
if (!ok) notifications.guess(ok, {message});
price: item.price,
},
}).then(async ({ ok, message }) => {
if (!ok) notifications.guess(ok, { message });
if (ok) await refetch();
})
}
});
};
const onDelete = (item: DealServiceSchema) => {
if (!deal) return;
DealService.deleteDealService({
requestBody:
{
dealId: deal.id,
serviceId: item.service.id
}
}).then(async ({ok, message}) => {
if (!ok) notifications.guess(ok, {message});
requestBody: {
dealId: deal.id,
serviceId: item.service.id,
},
}).then(async ({ ok, message }) => {
if (!ok) notifications.guess(ok, { message });
if (ok) await refetch();
})
}
});
};
const onChange = (item: DealServiceSchema) => {
if (!deal) return;
DealService.updateDealService({
requestBody:
{
dealId: deal.id,
service: item
}
}).then(async ({ok, message}) => {
if (!ok) notifications.guess(ok, {message});
requestBody: {
dealId: deal.id,
service: item,
},
}).then(async ({ ok, message }) => {
if (!ok) notifications.guess(ok, { message });
if (ok) await refetch();
})
}
});
};
return {
items: deal?.services || [],
onCreate,
onDelete,
onChange
}
}
onChange,
};
};
const useDealProductsState = (): CRUDTableProps<DealProductSchema> => {
const {deal, refetch} = useDealState();
const { deal, refetch } = useDealState();
const onCreate = (item: DealProductSchema) => {
if (!deal) return;
DealService.addDealProduct({
requestBody: {
dealId: deal.id,
product: item
}
}).then(async ({ok, message}) => {
if (!ok) notifications.guess(ok, {message});
product: item,
},
}).then(async ({ ok, message }) => {
if (!ok) notifications.guess(ok, { message });
if (ok) await refetch();
})
});
};
const onDelete = (item: DealProductSchema) => {
if (!deal) return;
DealService.deleteDealProduct({
requestBody: {
dealId: deal.id,
productId: item.product.id
}
}).then(async ({ok, message}) => {
if (!ok) notifications.guess(ok, {message});
productId: item.product.id,
},
}).then(async ({ ok, message }) => {
if (!ok) notifications.guess(ok, { message });
if (ok) await refetch();
})
});
};
const onChange = (item: DealProductSchema) => {
if (!deal) return;
DealService.updateDealProduct({
requestBody: {
dealId: deal.id,
product: item
}
}).then(async ({ok, message}) => {
if (!ok) notifications.guess(ok, {message});
product: item,
},
}).then(async ({ ok, message }) => {
if (!ok) notifications.guess(ok, { message });
if (ok) await refetch();
})
});
};
return {
items: deal?.products || [],
onCreate,
onDelete,
onChange
}
}
onChange,
};
};
const useDealProductAndServiceTabState = () => {
const dealState = useDealState();
const dealProductsState = useDealProductsState();
@@ -119,7 +123,7 @@ const useDealProductAndServiceTabState = () => {
return {
dealState,
dealProductsState,
dealServicesState
}
}
export default useDealProductAndServiceTabState;
dealServicesState,
};
};
export default useDealProductAndServiceTabState;

View File

@@ -6,7 +6,6 @@
}
.search-input {
}
.boards {
@@ -21,7 +20,6 @@
}
.delete {
@mixin light {
border-color: var(--mantine-color-gray-1);
}

View File

@@ -3,7 +3,10 @@ import styles from "./LeadsPage.module.css";
import Board from "../../../components/Dnd/Board/Board.tsx";
import { DragDropContext, Droppable, DropResult } from "@hello-pangea/dnd";
import { useDealSummaries } from "../hooks/useDealSummaries.tsx";
import { DealStatus, getDealStatusByName } from "../../../shared/enums/DealStatus.ts";
import {
DealStatus,
getDealStatusByName,
} from "../../../shared/enums/DealStatus.ts";
import PageBlock from "../../../components/PageBlock/PageBlock.tsx";
import DealEditDrawer from "../drawers/DealEditDrawer/DealEditDrawer.tsx";
import { DealPageContextProvider } from "../contexts/DealPageContext.tsx";
@@ -22,7 +25,7 @@ import { motion } from "framer-motion";
enum DisplayMode {
BOARD,
TABLE
TABLE,
}
export const LeadsPage: FC = () => {
@@ -30,7 +33,9 @@ export const LeadsPage: FC = () => {
const { summariesRaw, refetch } = useDealSummaries();
const [summaries, setSummaries] = useState(summariesRaw);
const [displayMode, setDisplayMode] = useState<DisplayMode>(DisplayMode.BOARD);
const [displayMode, setDisplayMode] = useState<DisplayMode>(
DisplayMode.BOARD
);
const [isDragEnded, setIsDragEnded] = useState(true);
useEffect(() => {
setSummaries(summariesRaw);
@@ -41,17 +46,19 @@ export const LeadsPage: FC = () => {
if (!summary) return;
modals.openConfirmModal({
title: "Удаление сделки",
children:
children: (
<Flex>
Вы действительно хотите удалить сделку {summary.name}?
</Flex>,
</Flex>
),
onConfirm: () => {
DealService.deleteDeal({ requestBody: { dealId: dealId } })
.then(async ({ ok, message }) => {
notifications.guess(ok, { message });
if (!ok) return;
await refetch();
});
DealService.deleteDeal({
requestBody: { dealId: dealId },
}).then(async ({ ok, message }) => {
notifications.guess(ok, { message });
if (!ok) return;
await refetch();
});
},
labels: {
confirm: "Удалить",
@@ -64,17 +71,19 @@ export const LeadsPage: FC = () => {
if (!summary) return;
modals.openConfirmModal({
title: "Завершение сделки",
children:
children: (
<Flex>
Вы действительно хотите завершить сделку {summary.name}?
</Flex>,
</Flex>
),
onConfirm: () => {
DealService.completeDeal({ requestBody: { dealId: dealId } })
.then(async ({ ok, message }) => {
notifications.guess(ok, { message });
if (!ok) return;
await refetch();
});
DealService.completeDeal({
requestBody: { dealId: dealId },
}).then(async ({ ok, message }) => {
notifications.guess(ok, { message });
if (!ok) return;
await refetch();
});
},
labels: {
confirm: "Завершить",
@@ -112,44 +121,42 @@ export const LeadsPage: FC = () => {
status: status,
};
if (status == summary.status) {
DealService.reorderDealSummaries({ requestBody: request as DealSummaryReorderRequest })
.then(async response => {
setSummaries(response.summaries);
await refetch();
});
DealService.reorderDealSummaries({
requestBody: request as DealSummaryReorderRequest,
}).then(async response => {
setSummaries(response.summaries);
await refetch();
});
return;
}
modals.openContextModal({
modal: "enterDeadline",
title: "Необходимо указать дедлайн",
innerProps: {
onSubmit: (event) => DealService.reorderDealSummaries({ requestBody: event })
.then(async response => {
onSubmit: event =>
DealService.reorderDealSummaries({
requestBody: event,
}).then(async response => {
setSummaries(response.summaries);
await refetch();
}),
request: request,
},
});
};
const getTableBody = () => {
return (
<motion.div
key={displayMode}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2 }}
>
transition={{ duration: 0.2 }}>
<DealsTable items={data} />
</motion.div>
);
};
const getBoardBody = () => {
return (
<motion.div
style={{
display: "flex",
@@ -159,9 +166,7 @@ export const LeadsPage: FC = () => {
key={displayMode}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2 }}
>
transition={{ duration: 0.2 }}>
<DragDropContext
onDragStart={() => {
setIsDragEnded(false);
@@ -170,124 +175,115 @@ export const LeadsPage: FC = () => {
<Flex
justify={"space-between"}
direction={"column"}
style={{ flex: 1 }}
>
style={{ flex: 1 }}>
<div className={styles["boards"]}>
<Board
withCreateButton
summaries={summaries
.filter(summary => summary.status == DealStatus.AWAITING_ACCEPTANCE)}
summaries={summaries.filter(
summary =>
summary.status ==
DealStatus.AWAITING_ACCEPTANCE
)}
title={"Ожидает приемки"}
droppableId={"AWAITING_ACCEPTANCE"}
color={"#4A90E2"}
/>
<Board
summaries={summaries
.filter(summary => summary.status == DealStatus.PACKAGING)}
summaries={summaries.filter(
summary =>
summary.status == DealStatus.PACKAGING
)}
title={"Упаковка"}
droppableId={"PACKAGING"}
color={"#F5A623"}
/>
<Board
summaries={summaries
.filter(summary => summary.status == DealStatus.AWAITING_SHIPMENT)}
summaries={summaries.filter(
summary =>
summary.status ==
DealStatus.AWAITING_SHIPMENT
)}
title={"Ожидает отгрузки"}
droppableId={"AWAITING_SHIPMENT"}
color={"#7ED321"}
/>
<Board
summaries={summaries
.filter(summary => summary.status == DealStatus.AWAITING_PAYMENT)}
summaries={summaries.filter(
summary =>
summary.status ==
DealStatus.AWAITING_PAYMENT
)}
title={"Ожидает оплаты"}
droppableId={"AWAITING_PAYMENT"}
color={"#D0021B"}
/>
<Board
summaries={summaries
.filter(summary => summary.status == DealStatus.COMPLETED)}
summaries={summaries.filter(
summary =>
summary.status == DealStatus.COMPLETED
)}
title={"Завершена"}
droppableId={"COMPLETED"}
color={"#417505"}
/>
</div>
<Flex justify={"space-between"} gap={rem(10)}>
<Flex
justify={"space-between"}
gap={rem(10)}>
<div
className={
classNames(
styles["delete"],
isDragEnded && styles["delete-hidden"],
)
}
>
className={classNames(
styles["delete"],
isDragEnded && styles["delete-hidden"]
)}>
<Droppable droppableId={"DELETE"}>
{(provided, snapshot) => (
<>
<div
{...provided.droppableProps}
ref={provided.innerRef}
>
{
!isDragEnded
&&
!snapshot.isDraggingOver &&
<span>
Удалить
</span>
}
ref={provided.innerRef}>
{!isDragEnded &&
!snapshot.isDraggingOver && (
<span>Удалить</span>
)}
</div>
{provided.placeholder}
</>
)}
</Droppable>
</div>
<div
className={
classNames(
styles["delete"],
isDragEnded && styles["delete-hidden"],
)
}
>
className={classNames(
styles["delete"],
isDragEnded && styles["delete-hidden"]
)}>
<Droppable droppableId={"SUCCESS"}>
{(provided, snapshot) => (
<>
<div
{...provided.droppableProps}
ref={provided.innerRef}
>
{
!isDragEnded
&&
!snapshot.isDraggingOver &&
<span>
Успешно завершена
</span>
}
ref={provided.innerRef}>
{!isDragEnded &&
!snapshot.isDraggingOver && (
<span>
Успешно завершена
</span>
)}
</div>
{provided.placeholder}
</>
)}
</Droppable>
</div>
</Flex>
</Flex>
</DragDropContext>
</motion.div>
);
};
const getBody = () => {
return displayMode === DisplayMode.TABLE ? getTableBody() : getBoardBody();
return displayMode === DisplayMode.TABLE
? getTableBody()
: getBoardBody();
};
return (
<PageBlock
@@ -298,78 +294,77 @@ export const LeadsPage: FC = () => {
flexDirection: "column",
backgroundColor: "transparent",
boxShadow: "none",
}}
>
}}>
<DealPageContextProvider>
<PageBlock
style={{ flex: 0 }}
>
<PageBlock style={{ flex: 0 }}>
<Flex
align={"center"}
justify={"space-between"}
>
justify={"space-between"}>
<Flex
gap={rem(10)}
direction={"column"}
align={"center"}
>
align={"center"}>
<Text size={"xs"}>Вид</Text>
<Flex gap={rem(10)}>
<ActionIcon
onClick={() => setDisplayMode(DisplayMode.BOARD)}
onClick={() =>
setDisplayMode(DisplayMode.BOARD)
}
variant={
displayMode === DisplayMode.BOARD ?
"filled"
:
"default"
displayMode === DisplayMode.BOARD
? "filled"
: "default"
}>
<IconMenuDeep
style={{ rotate: "-90deg" }}
/>
</ActionIcon>
<ActionIcon
onClick={() => setDisplayMode(DisplayMode.TABLE)}
variant={
displayMode === DisplayMode.TABLE ?
"filled"
:
"default"
onClick={() =>
setDisplayMode(DisplayMode.TABLE)
}
>
<IconMenu2
/>
variant={
displayMode === DisplayMode.TABLE
? "filled"
: "default"
}>
<IconMenu2 />
</ActionIcon>
</Flex>
</Flex>
<motion.div
key={displayMode}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2 }}
>
transition={{ duration: 0.2 }}>
<div
className={styles["top-panel"]}
style={{ display: displayMode === DisplayMode.TABLE ? "flex" : "none" }}
>
style={{
display:
displayMode === DisplayMode.TABLE
? "flex"
: "none",
}}>
<DealStatusSelect
onClear={() => form.setFieldValue("dealStatus", null)}
onClear={() =>
form.setFieldValue("dealStatus", null)
}
clearable
placeholder={"Выберите статус "}
{...form.getInputProps("dealStatus")}
/>
<BaseMarketplaceSelect
onClear={() => form.setFieldValue("marketplace", null)}
onClear={() =>
form.setFieldValue("marketplace", null)
}
clearable
placeholder={"Выберите маркетплейс"}
{...form.getInputProps("marketplace")}
/>
<ClientSelectNew
onClear={() => form.setFieldValue("client", null)}
onClear={() =>
form.setFieldValue("client", null)
}
clearable
searchable
placeholder={"Выберите клиента"}
@@ -388,11 +383,8 @@ export const LeadsPage: FC = () => {
}}>
{getBody()}
</PageBlock>
<DealEditDrawer
/>
<DealEditDrawer />
</DealPageContextProvider>
</PageBlock>
);
};