feat: processing of modules in card, renaming

This commit is contained in:
2025-02-20 20:21:08 +04:00
parent dc9455966e
commit 8083bdf3d0
29 changed files with 240 additions and 768 deletions

View File

@@ -13,5 +13,6 @@ export type CardGeneralInfoSchema = {
manager?: (UserSchema | null); manager?: (UserSchema | null);
boardId: number; boardId: number;
statusId: number; statusId: number;
isServicesProfitAccounted: boolean;
}; };

View File

@@ -5,11 +5,11 @@
import type { BaseMarketplaceSchema } from './BaseMarketplaceSchema'; import type { BaseMarketplaceSchema } from './BaseMarketplaceSchema';
export type CardQuickCreateRequest = { export type CardQuickCreateRequest = {
name: string; name: string;
clientName: string; clientName: (string | null);
comment: string; comment: string;
acceptanceDate: string; acceptanceDate: string;
shippingWarehouse: string; shippingWarehouse: (string | null);
baseMarketplace: BaseMarketplaceSchema; baseMarketplace: (BaseMarketplaceSchema | null);
statusId: number; statusId: number;
}; };

View File

@@ -26,11 +26,12 @@ export type CardSchema = {
statusHistory: Array<CardStatusHistorySchema>; statusHistory: Array<CardStatusHistorySchema>;
isDeleted: boolean; isDeleted: boolean;
isCompleted: boolean; isCompleted: boolean;
isServicesProfitAccounted: boolean;
isLocked: boolean; isLocked: boolean;
services: Array<CardServiceSchema>; services: Array<CardServiceSchema>;
products: Array<CardProductSchema>; products: Array<CardProductSchema>;
clientId: number; clientId: (number | null);
client: ClientSchema; client: (ClientSchema | null);
shippingWarehouse?: (ShippingWarehouseSchema | string | null); shippingWarehouse?: (ShippingWarehouseSchema | string | null);
billRequest?: (CardBillRequestSchema | null); billRequest?: (CardBillRequestSchema | null);
group?: (CardGroupSchema | null); group?: (CardGroupSchema | null);

View File

@@ -10,7 +10,7 @@ import type { StatusSchema } from './StatusSchema';
export type CardSummary = { export type CardSummary = {
id: number; id: number;
name: string; name: string;
clientName: string; clientName: (string | null);
createdAt: string; createdAt: string;
status: StatusSchema; status: StatusSchema;
board: BoardSchema; board: BoardSchema;

View File

@@ -1,4 +1,4 @@
import { ProjectSchema } from "../../client"; import { AttributeSchema, ProjectSchema } from "../../client";
import { UseFormReturnType } from "@mantine/form"; import { UseFormReturnType } from "@mantine/form";
import { rem, Stack } from "@mantine/core"; import { rem, Stack } from "@mantine/core";
import { ReactNode } from "react"; import { ReactNode } from "react";
@@ -11,7 +11,17 @@ type Props = {
} }
const CardAttributeFields = ({ project, form }: Props) => { const CardAttributeFields = ({ project, form }: Props) => {
const fields: ReactNode[] = project.attributes.map(attribute => { const attributes: AttributeSchema[] = [];
project.attributes.forEach(attribute => {
if (attribute.type.type === "boolean") {
attributes.push(attribute);
} else {
attributes.unshift(attribute);
}
});
const fields: ReactNode[] = attributes.map(attribute => {
return ( return (
<CardAttributeField <CardAttributeField
key={attribute.id} key={attribute.id}

View File

@@ -22,6 +22,7 @@ const CardSummaryItem: FC<Props> = ({ cardSummary, color }) => {
const { onDelete, onComplete, onDeleteFromGroup } = useCardSummaryState(); const { onDelete, onComplete, onDeleteFromGroup } = useCardSummaryState();
const isServicesAndProductsIncluded = isModuleInProject(Modules.SERVICES_AND_PRODUCTS, selectedProject); const isServicesAndProductsIncluded = isModuleInProject(Modules.SERVICES_AND_PRODUCTS, selectedProject);
const isClientIncluded = isModuleInProject(Modules.CLIENTS, selectedProject);
const onDealSummaryClick = () => { const onDealSummaryClick = () => {
CardService.getCardById({ cardId: cardSummary.id }).then(card => { CardService.getCardById({ cardId: cardSummary.id }).then(card => {
@@ -61,16 +62,13 @@ const CardSummaryItem: FC<Props> = ({ cardSummary, color }) => {
style={{ backgroundColor: color }} style={{ backgroundColor: color }}
> >
<Flex direction={"column"} flex={1} gap={rem(3)}> <Flex direction={"column"} flex={1} gap={rem(3)}>
<Flex justify={"space-between"}> {isClientIncluded && (
<Flex justify={"space-between"}>
<Text <Text c={"gray.6"} size={"xs"}>
c={"gray.6"} {cardSummary.clientName}
size={"xs"} </Text>
</Flex>
> )}
{cardSummary.clientName}
</Text>
</Flex>
<Text <Text
c={"blue.5"} c={"blue.5"}

View File

@@ -36,15 +36,14 @@ export const CardsDndColumn: FC<Props> = ({
return has(obj, "cards"); return has(obj, "cards");
}; };
const getDealGroups = (): GroupWithCards[] => { const getCardGroups = (): GroupWithCards[] => {
const groups = uniq<CardGroupSchema>(summaries.filter(s => s.group).map(summary => summary.group) as CardGroupSchema[]); const groups = uniq<CardGroupSchema>(summaries.filter(s => s.group).map(summary => summary.group) as CardGroupSchema[]);
if (groups.length === 0) return []; if (groups.length === 0) return [];
const groupedSummaries = groupBy(summaries, "group.id"); const groupedSummaries = groupBy(summaries, "group.id");
const groupDict = groups.reduce((acc, group) => { const groupDict = groups.reduce((acc, group) => {
acc[group.id] = group; acc[group.id] = group;
return acc; return acc;
} }, {} as { [key: number]: CardGroupSchema });
, {} as { [key: number]: CardGroupSchema });
return Object.entries(groupedSummaries).reduce((acc, [groupId, cards]) => { return Object.entries(groupedSummaries).reduce((acc, [groupId, cards]) => {
if (!groupId) return acc; if (!groupId) return acc;
const group = groupDict[parseInt(groupId)]; const group = groupDict[parseInt(groupId)];
@@ -57,8 +56,8 @@ export const CardsDndColumn: FC<Props> = ({
}, [] as { group: CardGroupSchema; cards: CardSummary[] }[]); }, [] as { group: CardGroupSchema; cards: CardSummary[] }[]);
}; };
const getDealsAndGroups = (): (GroupWithCards | CardSummary)[] => { const getCardsAndGroups = (): (GroupWithCards | CardSummary)[] => {
const groups = getDealGroups(); const groups = getCardGroups();
const cards = summaries.filter(s => !s.group).sort((a, b) => { const cards = summaries.filter(s => !s.group).sort((a, b) => {
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
}); });
@@ -144,7 +143,7 @@ export const CardsDndColumn: FC<Props> = ({
} }
</> </>
)} )}
{getDealsAndGroups().map(obj => { {getCardsAndGroups().map(obj => {
if (isGroup(obj)) { if (isGroup(obj)) {
return renderGroup(obj); return renderGroup(obj);
} }

View File

@@ -2,7 +2,7 @@ import { Button, rem, Textarea, TextInput } from "@mantine/core";
import { QuickCard } from "../../../../types/QuickCard.ts"; import { QuickCard } from "../../../../types/QuickCard.ts";
import { FC } from "react"; import { FC } from "react";
import { useForm } from "@mantine/form"; import { useForm } from "@mantine/form";
import styles from "./CreateDealForm.module.css"; import styles from "./CreateCardForm.module.css";
import ClientAutocomplete from "../../../Selects/ClientAutocomplete/ClientAutocomplete.tsx"; import ClientAutocomplete from "../../../Selects/ClientAutocomplete/ClientAutocomplete.tsx";
import { DateTimePicker } from "@mantine/dates"; import { DateTimePicker } from "@mantine/dates";
import ShippingWarehouseAutocomplete import ShippingWarehouseAutocomplete
@@ -19,22 +19,28 @@ type Props = {
const CreateCardForm: FC<Props> = ({ onSubmit, onCancel }) => { const CreateCardForm: FC<Props> = ({ onSubmit, onCancel }) => {
const { selectedProject } = useCardPageContext(); const { selectedProject } = useCardPageContext();
const isPrefillingDealEnabled = isModuleInProject(Modules.SERVICES_AND_PRODUCTS, selectedProject); const isPrefillingEnabled = isModuleInProject(Modules.SERVICES_AND_PRODUCTS, selectedProject);
const { prefillOnOpen, prefillCard } = usePrefillCardContext(); const { prefillOnOpen, prefillCard } = usePrefillCardContext();
const isServicesAndProductsIncluded = isModuleInProject(Modules.SERVICES_AND_PRODUCTS, selectedProject);
const isClientsIncluded = isModuleInProject(Modules.CLIENTS, selectedProject);
const form = useForm<QuickCard>({ const form = useForm<QuickCard>({
initialValues: { initialValues: {
name: "", name: "",
clientName: "", clientName: null,
clientAddress: "",
comment: "", comment: "",
acceptanceDate: new Date(), acceptanceDate: new Date(),
shippingWarehouse: "", shippingWarehouse: null,
baseMarketplace: { baseMarketplace: null,
key: "", },
iconUrl: "", validate: {
name: "", baseMarketplace: baseMarketplace =>
}, isServicesAndProductsIncluded && !baseMarketplace && "МП не выбран",
shippingWarehouse: shippingWarehouse =>
isServicesAndProductsIncluded && !shippingWarehouse && "Склад отгрузки не выбран",
clientName: clientName =>
isClientsIncluded && !clientName && "Клиент не выбран",
}, },
}); });
@@ -54,26 +60,34 @@ const CreateCardForm: FC<Props> = ({ onSubmit, onCancel }) => {
}}> }}>
<div className={styles["inputs"]}> <div className={styles["inputs"]}>
<TextInput <TextInput
placeholder={"Название сделки"} placeholder={"Название"}
{...form.getInputProps("name")} {...form.getInputProps("name")}
/> />
</div> </div>
<div className={styles["inputs"]}> {isClientsIncluded && (
<ClientAutocomplete <div className={styles["inputs"]}>
nameRestProps={form.getInputProps("clientName")} <ClientAutocomplete
/> nameRestProps={form.getInputProps("clientName")}
</div> />
<div className={styles["inputs"]}> </div>
<BaseMarketplaceSelect )}
rightSection={<></>} {isServicesAndProductsIncluded && (
placeholder={"Базовый маркетплейс"} <>
{...form.getInputProps("baseMarketplace")} <div className={styles["inputs"]}>
/> <BaseMarketplaceSelect
<ShippingWarehouseAutocomplete rightSection={<></>}
{...form.getInputProps("shippingWarehouse")} placeholder={"Базовый маркетплейс"}
placeholder={"Склад отгрузки"} {...form.getInputProps("baseMarketplace")}
/> />
</div> </div>
<div className={styles["inputs"]}>
<ShippingWarehouseAutocomplete
{...form.getInputProps("shippingWarehouse")}
placeholder={"Склад отгрузки"}
/>
</div>
</>
)}
<div className={styles["inputs"]}> <div className={styles["inputs"]}>
<Textarea <Textarea
autosize autosize
@@ -89,7 +103,7 @@ const CreateCardForm: FC<Props> = ({ onSubmit, onCancel }) => {
{...form.getInputProps("acceptanceDate")} {...form.getInputProps("acceptanceDate")}
/> />
</div> </div>
{isPrefillingDealEnabled && ( {isPrefillingEnabled && (
<div className={styles["button-prefill"]}> <div className={styles["button-prefill"]}>
<Button <Button
style={{ whiteSpace: "wrap" }} style={{ whiteSpace: "wrap" }}

View File

@@ -20,7 +20,7 @@ type Props = {
refetchProjects: () => void; refetchProjects: () => void;
} }
const LeadsPageHeader = ({ const CardsPageHeader = ({
displayMode, displayMode,
setDisplayMode, setDisplayMode,
form, form,
@@ -124,4 +124,4 @@ const LeadsPageHeader = ({
); );
}; };
export default LeadsPageHeader; export default CardsPageHeader;

View File

@@ -1,132 +0,0 @@
import {
ActionIcon,
Button,
ComboboxItem,
ComboboxItemGroup,
Flex,
Input,
OptionsFilter,
rem,
} from "@mantine/core";
import { BaseFormInputProps } from "../../../../types/utils.ts";
import { CardProductServiceSchema, 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";
type RestProps = {
quantity: number;
};
type Props = BaseFormInputProps<CardProductServiceSchema[]> & RestProps;
const DealProductServiceTable: FC<Props> = (props: Props) => {
const { value, onChange, quantity, error } = props;
const authState = useSelector((state: RootState) => state.auth);
const [innerValue, setInnerValue] = useState<
Partial<CardProductServiceSchema>[]
>(value || []);
const onServiceChange = (idx: number, value: ServiceSchema) => {
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
)
);
};
const onCreate = () => {
if (innerValue.length > 0 && !innerValue.at(-1)?.service) {
notifications.error({ message: "Заполните последнюю услугу" });
return;
}
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 }) => {
return {
group,
items: items.filter(
item =>
!productServiceIds.includes(
parseInt((item as ComboboxItem).value)
)
),
};
});
};
useEffect(() => {
onChange(innerValue as CardProductServiceSchema[]);
}, [innerValue]);
return (
<Input.Wrapper error={error}>
<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 />
</ActionIcon>
<ServiceWithPriceInput
serviceProps={{
onChange: event => onServiceChange(idx, event),
value: service.service,
placeholder: "Выберите услугу",
style: { width: "100%" },
filter: serviceOptionsFilter as OptionsFilter,
}}
priceProps={{
onChange: event => onQuantityChange(idx, event),
value: service.price,
placeholder: "Введите стоимость",
hideControls: true,
style: { width: "100%" },
suffix: "₽",
disabled: authState.isGuest,
}}
containerProps={{ w: "100%" }}
quantity={quantity}
/>
</Flex>
))}
<Button
onClick={onCreate}
variant={"default"}>
Добавить услугу
</Button>
</Flex>
</Input.Wrapper>
);
};
export default DealProductServiceTable;

View File

@@ -1,168 +0,0 @@
import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
import useDealProductsTableColumns from "./columns.tsx";
import { FC } from "react";
import { CRUDTableProps } from "../../../../types/CRUDTable.tsx";
import { CardProductSchema, 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: CardProductSchema[]) => void;
};
type Props = CRUDTableProps<CardProductSchema> & RestProps;
const DealProductsTable: FC<Props> = (props: Props) => {
const { items, clientId, onChange, onCreate, onDelete, onMultipleDelete } =
props;
const columns = useDealProductsTableColumns({
onChange: (product, quantity) => {
if (!onChange) return;
if (quantity <= 0 && onDelete) {
onDelete(product);
return;
}
onChange({ ...product, quantity });
},
data: items,
});
const onCreateClick = () => {
if (!onCreate) return;
modals.openContextModal({
modal: "addCardProduct",
title: "Добавление товара",
innerProps: {
onCreate: product => onCreate(product as CardProductSchema),
clientId,
},
size: "lg",
});
};
const onPrintBarcodeClick = (product: CardProductSchema) => {
modals.openContextModal({
modal: "printBarcode",
title: "Печать штрихкода",
withCloseButton: true,
innerProps: {
productId: product.product.id,
defaultQuantity: product.quantity,
},
});
};
const onCreateProduct = (newProduct: CreateProductRequest) => {
ProductService.createProduct({
requestBody: newProduct,
}).then(({ ok, message }) => {
notifications.guess(ok, { message: message });
});
};
const onCreateProductClick = () => {
modals.openContextModal({
modal: "createProduct",
title: "Создание товара",
withCloseButton: false,
innerProps: {
clientId: clientId,
onCreate: onCreateProduct,
},
});
};
const onEditClick = (product: CardProductSchema) => {
if (!onChange) return;
modals.openContextModal({
modal: "addCardProduct",
title: "Создание товара",
withCloseButton: false,
innerProps: {
clientId: clientId,
element: product,
onChange: onChange,
},
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 && (
<Button
onClick={() => {
onMultipleDelete(
table
.getSelectedRowModel()
.rows.map(
row => row.original
)
);
}}
variant={"filled"}
color={"red"}>
Удалить выбранные
</Button>
)}
<Button
variant={"default"}
onClick={onCreateProductClick}>
Создать товар
</Button>
<Button
onClick={onCreateClick}
variant={"default"}>
Добавить товар в сделку
</Button>
</Flex>
),
} as MRT_TableOptions<CardProductSchema>
}
/>
);
};
export default DealProductsTable;

View File

@@ -1,107 +0,0 @@
import { useMemo } from "react";
import { MRT_ColumnDef } from "mantine-react-table";
import { CardProductSchema } from "../../../../client";
import { List } from "@mantine/core";
type Props = {
onChange: (product: CardProductSchema, quantity: number) => void;
data: CardProductSchema[];
};
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<CardProductSchema>[]>(
() => [
{
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>
);
},
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;

View File

@@ -1,129 +0,0 @@
import { FC } from "react";
import { useDealServicesTableColumns } from "./columns.tsx";
import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
import { CardServiceSchema } 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: CardServiceSchema[]) => void;
};
type Props = CRUDTableProps<CardServiceSchema> & RestProps;
const DealServicesTable: FC<Props> = ({
items,
onChange,
onDelete,
onCreate,
onSelectionChange,
onMultipleDelete,
tableRef,
}) => {
const serviceIds = items.map(item => item.service.id);
const columns = useDealServicesTableColumns({
data: items,
});
const onCreateClick = () => {
if (!onCreate) return;
openContextModal({
title: "Добавление услуги",
modal: "addCardService",
innerProps: {
onCreate: event => onCreate(event as CardServiceSchema),
serviceIds,
},
});
};
const onEditClick = (service: CardServiceSchema) => {
if (!onChange) return;
openContextModal({
title: "Добавление услуги",
modal: "addCardService",
innerProps: {
element: service,
onChange,
serviceIds,
},
});
};
return (
<>
<BaseTable
ref={tableRef}
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 && (
<Button
onClick={() => {
onMultipleDelete(
table
.getSelectedRowModel()
.rows.map(
row => row.original
)
);
}}
variant={"filled"}
color={"red"}>
Удалить выбранные
</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<CardServiceSchema>
}
/>
</>
);
};
export default DealServicesTable;

View File

@@ -1,63 +0,0 @@
import { MRT_ColumnDef } from "mantine-react-table";
import { useMemo } from "react";
import { CardServiceSchema } from "../../../../client";
type Props = {
data: CardServiceSchema[];
};
export const useDealServicesTableColumns = (props: Props) => {
const { data } = props;
const totalPrice = useMemo(
() => data.reduce((acc, row) => acc + row.quantity * row.price, 0),
[data]
);
return useMemo<MRT_ColumnDef<CardServiceSchema>[]>(
() => [
{
accessorKey: "service.category",
header: "Категория",
accessorFn: row => row.service.category.name,
},
{
enableGrouping: false,
accessorKey: "service.name",
header: "Услуга",
},
{
enableGrouping: false,
accessorKey: "price",
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

@@ -2,7 +2,7 @@ import { Box, Drawer, rem, Tabs } from "@mantine/core";
import { FC, ReactNode, useEffect } from "react"; import { FC, ReactNode, useEffect } from "react";
import { useCardPageContext } from "../../contexts/CardPageContext.tsx"; import { useCardPageContext } from "../../contexts/CardPageContext.tsx";
import { IconBox, IconCalendarUser, IconCubeSend, IconSettings, IconUser, IconUsersGroup } from "@tabler/icons-react"; import { IconBox, IconCalendarUser, IconCubeSend, IconSettings, IconUser, IconUsersGroup } from "@tabler/icons-react";
import CardStatusChangeTable from "../../components/DealStatusChangeTable/CardStatusChangeTable.tsx"; import CardStatusChangeTable from "../../components/CardStatusChangeTable/CardStatusChangeTable.tsx";
import GeneralTab from "../../tabs/GeneralTab/GeneralTab.tsx"; import GeneralTab from "../../tabs/GeneralTab/GeneralTab.tsx";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import ProductAndServiceTab from "../../tabs/ProductAndServiceTab/ProductAndServiceTab.tsx"; import ProductAndServiceTab from "../../tabs/ProductAndServiceTab/ProductAndServiceTab.tsx";
@@ -62,8 +62,16 @@ const CardEditDrawer: FC = () => {
); );
}; };
const getTab = (key: string, icon: ReactNode, label: string) => { const getTab = (
if (!modules.has(key)) return; key: string,
icon: ReactNode,
label: string,
enablingModules: string[] | null = null, // Show if at least one of modules is in project
) => {
if (!enablingModules) {
enablingModules = [key];
}
if (!enablingModules.some(key => modules.has(key))) return;
return ( return (
<Tabs.Tab <Tabs.Tab
@@ -102,23 +110,19 @@ const CardEditDrawer: FC = () => {
leftSection={<IconSettings />}> leftSection={<IconSettings />}>
Общее Общее
</Tabs.Tab> </Tabs.Tab>
<Tabs.Tab
value={"client"}
leftSection={<IconUser />}>
Клиент
</Tabs.Tab>
<Tabs.Tab <Tabs.Tab
value={"history"} value={"history"}
leftSection={<IconCalendarUser />}> leftSection={<IconCalendarUser />}>
История История
</Tabs.Tab> </Tabs.Tab>
{getTab("clients", <IconUser />, "Клиент", ["servicesAndProducts", "clients"])}
{getTab("servicesAndProducts", <IconBox />, "Товары и услуги")} {getTab("servicesAndProducts", <IconBox />, "Товары и услуги")}
{getTab("shipment", <IconCubeSend />, "Отгрузка")} {getTab("shipment", <IconCubeSend />, "Отгрузка")}
{getTab("employees", <IconUsersGroup />, "Исполнители")} {getTab("employees", <IconUsersGroup />, "Исполнители")}
</Tabs.List> </Tabs.List>
{getTabPanel("general", <GeneralTab />)} {getTabPanel("general", <GeneralTab />)}
{getTabPanel("client", <ClientTab />)} {getTabPanel("clients", <ClientTab />)}
{getTabPanel("history", <CardEditDrawerStatusChangeTable />)} {getTabPanel("history", <CardEditDrawerStatusChangeTable />)}
{getTabPanel("servicesAndProducts", <ProductAndServiceTab />)} {getTabPanel("servicesAndProducts", <ProductAndServiceTab />)}
{getTabPanel("shipment", <ShippingTab />)} {getTabPanel("shipment", <ShippingTab />)}

View File

@@ -12,6 +12,8 @@ const ClientTab = () => {
const initialValues: CardGeneralFormType = card as CardSchema; const initialValues: CardGeneralFormType = card as CardSchema;
const queryClient = useQueryClient(); const queryClient = useQueryClient();
if (!card?.client) return;
const form = useForm<CardGeneralFormType>( const form = useForm<CardGeneralFormType>(
{ {
initialValues: initialValues, initialValues: initialValues,
@@ -22,6 +24,8 @@ const ClientTab = () => {
); );
const hasChanges = !isEqual(form.values, initialValues); const hasChanges = !isEqual(form.values, initialValues);
const updateClientInfo = async (values: CardGeneralFormType) => { const updateClientInfo = async (values: CardGeneralFormType) => {
if (!values.client) return;
return ClientService.updateClient({ return ClientService.updateClient({
requestBody: { requestBody: {
data: values.client, data: values.client,

View File

@@ -1,6 +1,18 @@
import { FC, useState } from "react"; import { FC, useState } from "react";
import { useCardPageContext } from "../../contexts/CardPageContext.tsx"; import { useCardPageContext } from "../../contexts/CardPageContext.tsx";
import { Button, Checkbox, Divider, Fieldset, Flex, Group, rem, Textarea, TextInput } from "@mantine/core"; import {
Button,
Checkbox,
Divider,
Fieldset,
Flex,
Group,
rem,
ScrollArea,
Stack,
Textarea,
TextInput,
} from "@mantine/core";
import { useForm } from "@mantine/form"; import { useForm } from "@mantine/form";
import { import {
CardSchema, CardSchema,
@@ -44,6 +56,7 @@ const Content: FC<Props> = ({ card }) => {
const [project, setProject] = useState<ProjectSchema | null>(card.board.project); const [project, setProject] = useState<ProjectSchema | null>(card.board.project);
const isServicesAndProductsIncluded = isModuleInProject(Modules.SERVICES_AND_PRODUCTS, card.board.project); const isServicesAndProductsIncluded = isModuleInProject(Modules.SERVICES_AND_PRODUCTS, card.board.project);
const isManagerIncluded = isModuleInProject(Modules.MANAGERS, card.board.project);
const getInitialValues = (card: CardSchema): CardGeneralFormType => { const getInitialValues = (card: CardSchema): CardGeneralFormType => {
return { return {
@@ -67,8 +80,6 @@ const Content: FC<Props> = ({ card }) => {
}); });
const updateCardInfo = async (values: CardGeneralFormType) => { const updateCardInfo = async (values: CardGeneralFormType) => {
console.log("Updated attributes:");
console.log(values);
const formCardAttrs = values as unknown as Attributes; const formCardAttrs = values as unknown as Attributes;
const attributes = project?.attributes.reduce((attrs, projectAttr) => { const attributes = project?.attributes.reduce((attrs, projectAttr) => {
@@ -104,6 +115,7 @@ const Content: FC<Props> = ({ card }) => {
}); });
}; };
const updateClientInfo = async (values: CardGeneralFormType) => { const updateClientInfo = async (values: CardGeneralFormType) => {
if (!values.client) return;
return ClientService.updateClient({ return ClientService.updateClient({
requestBody: { requestBody: {
data: values.client, data: values.client,
@@ -116,7 +128,9 @@ const Content: FC<Props> = ({ card }) => {
await updateClientInfo(values); await updateClientInfo(values);
} }
const shippingWarehouse = isShippingWarehouse(values.shippingWarehouse) ? values.shippingWarehouse.name : values.shippingWarehouse; const shippingWarehouse = isShippingWarehouse(values.shippingWarehouse)
? values.shippingWarehouse.name
: values.shippingWarehouse;
await updateCardInfo( await updateCardInfo(
{ {
...values, ...values,
@@ -128,7 +142,7 @@ const Content: FC<Props> = ({ card }) => {
const isShippingWarehouse = ( const isShippingWarehouse = (
value: ShippingWarehouseSchema | string | null | undefined, value: ShippingWarehouseSchema | string | null | undefined,
): value is ShippingWarehouseSchema => { ): value is ShippingWarehouseSchema => {
return !["string", "null", "undefined"].includes(typeof value); return !!value && !["string"].includes(typeof value);
}; };
const onCopyGuestUrlClick = () => { const onCopyGuestUrlClick = () => {
@@ -146,87 +160,101 @@ const Content: FC<Props> = ({ card }) => {
<form onSubmit={form.onSubmit(values => handleSubmit(values))}> <form onSubmit={form.onSubmit(values => handleSubmit(values))}>
<Flex <Flex
direction={"column"} direction={"column"}
justify={"space-between"} h={"95vh"}
h={"100%"}> >
<Fieldset legend={`Общие параметры [ID: ${card.id}]`}> <ScrollArea>
<Flex <Stack>
direction={"column"} <Fieldset legend={`Общие параметры [ID: ${card.id}]`}>
gap={rem(10)} <Flex
> direction={"column"}
<TextInput gap={rem(10)}
placeholder={"Название сделки"} >
label={"Название сделки"} <TextInput
{...form.getInputProps("name")} placeholder={"Название сделки"}
/> label={"Название сделки"}
<TextInput {...form.getInputProps("name")}
disabled />
placeholder={"Дата создания"} <TextInput
label={"Дата создания"} disabled
value={new Date(card.createdAt).toLocaleString( placeholder={"Дата создания"}
"ru-RU", label={"Дата создания"}
)} value={new Date(card.createdAt).toLocaleString(
/> "ru-RU",
<ProjectSelect )}
value={project} />
onChange={setProject} <ProjectSelect
label={"Проект"} value={project}
disabled onChange={setProject}
/> label={"Проект"}
<BoardSelect disabled
project={project} />
{...form.getInputProps("board")} <BoardSelect
label={"Доска"} project={project}
/> {...form.getInputProps("board")}
<DealStatusSelect label={"Доска"}
board={form.values.board} />
{...form.getInputProps("status")} <DealStatusSelect
label={"Статус"} board={form.values.board}
/> {...form.getInputProps("status")}
<Textarea label={"Статус"}
h={rem(150)} />
styles={{ <Textarea
wrapper: { height: "90%" }, h={rem(150)}
input: { height: "90%" }, styles={{
}} wrapper: { height: "90%" },
label={"Коментарий"} input: { height: "90%" },
placeholder={"Введите коментарий"} }}
{...form.getInputProps("comment")} label={"Коментарий"}
/> placeholder={"Введите коментарий"}
<ShippingWarehouseAutocomplete {...form.getInputProps("comment")}
placeholder={"Введите склад отгрузки"} />
label={"Склад отгрузки"} {isServicesAndProductsIncluded && (
value={ <ShippingWarehouseAutocomplete
isShippingWarehouse( placeholder={"Введите склад отгрузки"}
form.values.shippingWarehouse, label={"Склад отгрузки"}
) value={
? form.values.shippingWarehouse isShippingWarehouse(
: undefined form.values.shippingWarehouse,
} )
onChange={event => { ? form.values.shippingWarehouse
if (isShippingWarehouse(event)) { : undefined
form.getInputProps( }
"shippingWarehouse", onChange={event => {
).onChange(event.name); if (isShippingWarehouse(event)) {
return; form.getInputProps(
} "shippingWarehouse",
form.getInputProps( ).onChange(event.name);
"shippingWarehouse", return;
).onChange(event); }
}} form.getInputProps(
/> "shippingWarehouse",
<ManagerSelect ).onChange(event);
placeholder={"Укажите менеджера"} }}
label={"Менеджер"} />
{...form.getInputProps("manager")} )}
/> {isManagerIncluded && (
{project && ( <ManagerSelect
<CardAttributeFields placeholder={"Укажите менеджера"}
project={project} label={"Менеджер"}
form={form} {...form.getInputProps("manager")}
/> />
)} )}
</Flex> {isServicesAndProductsIncluded && (
</Fieldset> <Checkbox
label={"Учет выручки с услуг в статистике"}
{...form.getInputProps("isServicesProfitAccounted", { type: "checkbox" })}
/>
)}
{project && (
<CardAttributeFields
project={project}
form={form}
/>
)}
</Flex>
</Fieldset>
</Stack>
</ScrollArea>
<Flex <Flex
mt={"md"} mt={"md"}
gap={rem(10)} gap={rem(10)}
@@ -241,7 +269,7 @@ const Content: FC<Props> = ({ card }) => {
align={"center"} align={"center"}
justify={"space-between"}> justify={"space-between"}>
{isServicesAndProductsIncluded && ( {isServicesAndProductsIncluded && (
<PrintDealBarcodesButton card={card}/> <PrintDealBarcodesButton card={card} />
)} )}
<Flex gap={rem(10)}> <Flex gap={rem(10)}>
{isServicesAndProductsIncluded && ( {isServicesAndProductsIncluded && (
@@ -303,7 +331,9 @@ const Content: FC<Props> = ({ card }) => {
const GeneralTab: FC = () => { const GeneralTab: FC = () => {
const { selectedCard } = useCardPageContext(); const { selectedCard } = useCardPageContext();
if (!selectedCard) return <>No card selected</>; if (!selectedCard) return <>No card selected</>;
return <Content card={selectedCard} />; return (
<Content card={selectedCard} />
);
}; };
export default GeneralTab; export default GeneralTab;

View File

@@ -30,7 +30,7 @@ const ProductAndServiceTab: FC = () => {
useCardProductAndServiceTabState(); useCardProductAndServiceTabState();
const isLocked = Boolean(cardState.card?.billRequest || cardState.card?.group?.billRequest); const isLocked = Boolean(cardState.card?.billRequest || cardState.card?.group?.billRequest);
const onAddProductClick = () => { const onAddProductClick = () => {
if (!cardProductsState.onCreate || !cardState.card) return; if (!cardProductsState.onCreate || !cardState.card || !cardState.card.clientId) return;
const productIds = cardState.card.products.map( const productIds = cardState.card.products.map(
product => product.product.id product => product.product.id
); );
@@ -108,7 +108,7 @@ const ProductAndServiceTab: FC = () => {
await cardState.refetch(); await cardState.refetch();
}); });
}; };
const onDealKitAdd = (kit: GetServiceKitSchema) => { const onCardKitAdd = (kit: GetServiceKitSchema) => {
if (!cardState.card) return; if (!cardState.card) return;
CardService.addKitToCard({ CardService.addKitToCard({
requestBody: { requestBody: {
@@ -130,7 +130,7 @@ const ProductAndServiceTab: FC = () => {
}); });
}; };
const onCreateProductClick = () => { const onCreateProductClick = () => {
if (!cardState.card) return; if (!cardState.card || !cardState.card.clientId) return;
modals.openContextModal({ modals.openContextModal({
modal: "createProduct", modal: "createProduct",
title: "Создание товара", title: "Создание товара",
@@ -240,7 +240,7 @@ const ProductAndServiceTab: FC = () => {
direction={"column"} direction={"column"}
className={styles["deal-container-wrapper"]}> className={styles["deal-container-wrapper"]}>
<CardServicesTable <CardServicesTable
onKitAdd={onDealKitAdd} onKitAdd={onCardKitAdd}
{...cardServicesState} {...cardServicesState}
/> />

View File

@@ -5,14 +5,14 @@ import CardEditDrawer from "../drawers/CardEditDrawer/CardEditDrawer.tsx";
import { CardPageContextProvider } from "../contexts/CardPageContext.tsx"; import { CardPageContextProvider } from "../contexts/CardPageContext.tsx";
import { rem } from "@mantine/core"; import { rem } from "@mantine/core";
import useCardsPageState from "../hooks/useCardsPageState.tsx"; import useCardsPageState from "../hooks/useCardsPageState.tsx";
import CardsTable from "../components/DealsTable/CardsTable.tsx"; import CardsTable from "../components/CardsTable/CardsTable.tsx";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import CardPrefillDrawer from "../drawers/CardPrefillDrawer/CardPrefillDrawer.tsx"; import CardPrefillDrawer from "../drawers/CardPrefillDrawer/CardPrefillDrawer.tsx";
import { PrefillCardContextProvider } from "../contexts/PrefillCardContext.tsx"; import { PrefillCardContextProvider } from "../contexts/PrefillCardContext.tsx";
import { useParams } from "@tanstack/react-router"; import { useParams } from "@tanstack/react-router";
import { PrefillCardsWithExcelContextProvider } from "../contexts/PrefillDealsWithExcelContext.tsx"; import { PrefillCardsWithExcelContextProvider } from "../contexts/PrefillDealsWithExcelContext.tsx";
import DisplayMode from "../enums/DisplayMode.ts"; import DisplayMode from "../enums/DisplayMode.ts";
import LeadsPageHeader from "../components/LeadsPageHeader/LeadsPageHeader.tsx"; import CardsPageHeader from "../components/CardsPageHeader/CardsPageHeader.tsx";
import useProjects from "../hooks/useProjects.tsx"; import useProjects from "../hooks/useProjects.tsx";
import Boards from "../../../components/Dnd/Boards/Boards/Boards.tsx"; import Boards from "../../../components/Dnd/Boards/Boards/Boards.tsx";
import useBoards from "../hooks/useBoards.tsx"; import useBoards from "../hooks/useBoards.tsx";
@@ -81,7 +81,7 @@ export const CardsPage: FC = () => {
> >
<PrefillCardContextProvider> <PrefillCardContextProvider>
<PrefillCardsWithExcelContextProvider> <PrefillCardsWithExcelContextProvider>
<LeadsPageHeader <CardsPageHeader
form={form} form={form}
displayMode={displayMode} displayMode={displayMode}
setDisplayMode={setDisplayMode} setDisplayMode={setDisplayMode}

View File

@@ -2,10 +2,20 @@ import { ProjectSchema } from "../../../client";
export enum Modules { export enum Modules {
SERVICES_AND_PRODUCTS = "servicesAndProducts", SERVICES_AND_PRODUCTS = "servicesAndProducts",
SHIPMENT = "shipment",
EMPLOYEES = "employees",
CLIENTS = "clients",
MANAGERS = "managers",
} }
const isModuleInProject = (module: Modules, project?: ProjectSchema | null) => { const isModuleInProject = (module: Modules, project?: ProjectSchema | null) => {
console.log(module.toString()); // if servicesAndProducts included, then clients also included
if (module === Modules.CLIENTS) {
if (project?.modules.findIndex(m => m.key === Modules.SERVICES_AND_PRODUCTS.toString()) !== -1) {
return true;
}
}
return project?.modules.findIndex(m => m.key === module.toString()) !== -1; return project?.modules.findIndex(m => m.key === module.toString()) !== -1;
}; };

View File

@@ -3,7 +3,7 @@ import { CardSummary, ShippingWarehouseSchema } from "../../../client";
import { ContextModalProps } from "@mantine/modals"; import { ContextModalProps } from "@mantine/modals";
import { useForm } from "@mantine/form"; import { useForm } from "@mantine/form";
import { Input, TextInput } from "@mantine/core"; import { Input, TextInput } from "@mantine/core";
import CardsTable from "../../CardsPage/components/DealsTable/CardsTable.tsx"; import CardsTable from "../../CardsPage/components/CardsTable/CardsTable.tsx";
import { CardPageContextProvider } from "../../CardsPage/contexts/CardPageContext.tsx"; import { CardPageContextProvider } from "../../CardsPage/contexts/CardPageContext.tsx";
import CardEditDrawer from "../../CardsPage/drawers/CardEditDrawer/CardEditDrawer.tsx"; import CardEditDrawer from "../../CardsPage/drawers/CardEditDrawer/CardEditDrawer.tsx";

View File

@@ -27,7 +27,7 @@ export const ProfitChart = () => {
{ name: "expenses", label: "Расходы", color: "red.6" }, { name: "expenses", label: "Расходы", color: "red.6" },
], ],
[ [
{ name: "dealsCount", label: "Количество сделок", color: "indigo.6" }, { name: "cardsCount", label: "Количество сделок", color: "indigo.6" },
], ],
]; ];
@@ -54,6 +54,7 @@ export const ProfitChart = () => {
valueFormatter={(value) => new Intl.NumberFormat("ru-RU").format(value)} valueFormatter={(value) => new Intl.NumberFormat("ru-RU").format(value)}
series={series} series={series}
fillOpacity={0.5} fillOpacity={0.5}
key={idx}
/> />
); );
})} })}

View File

@@ -62,7 +62,7 @@ export const useProfitTableColumns = ({ groupTableBy, statuses }: Props) => {
size: groupTableBy === GroupStatisticsTable.BY_DATES ? 40 : 80, size: groupTableBy === GroupStatisticsTable.BY_DATES ? 40 : 80,
}, },
{ {
accessorKey: "dealsCount", accessorKey: "cardsCount",
header: "Кол-во", header: "Кол-во",
size: 40, size: 40,
}, },

View File

@@ -2,10 +2,9 @@ import { BaseMarketplaceSchema } from "../client";
export type QuickCard = { export type QuickCard = {
name: string; name: string;
clientName: string; clientName: string | null;
clientAddress: string;
comment: string; comment: string;
acceptanceDate: Date; acceptanceDate: Date;
shippingWarehouse: string; shippingWarehouse: string | null;
baseMarketplace: BaseMarketplaceSchema; baseMarketplace: BaseMarketplaceSchema | null;
}; };