feat: processing of modules in card, renaming
This commit is contained in:
@@ -13,5 +13,6 @@ export type CardGeneralInfoSchema = {
|
||||
manager?: (UserSchema | null);
|
||||
boardId: number;
|
||||
statusId: number;
|
||||
isServicesProfitAccounted: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
import type { BaseMarketplaceSchema } from './BaseMarketplaceSchema';
|
||||
export type CardQuickCreateRequest = {
|
||||
name: string;
|
||||
clientName: string;
|
||||
clientName: (string | null);
|
||||
comment: string;
|
||||
acceptanceDate: string;
|
||||
shippingWarehouse: string;
|
||||
baseMarketplace: BaseMarketplaceSchema;
|
||||
shippingWarehouse: (string | null);
|
||||
baseMarketplace: (BaseMarketplaceSchema | null);
|
||||
statusId: number;
|
||||
};
|
||||
|
||||
|
||||
@@ -26,11 +26,12 @@ export type CardSchema = {
|
||||
statusHistory: Array<CardStatusHistorySchema>;
|
||||
isDeleted: boolean;
|
||||
isCompleted: boolean;
|
||||
isServicesProfitAccounted: boolean;
|
||||
isLocked: boolean;
|
||||
services: Array<CardServiceSchema>;
|
||||
products: Array<CardProductSchema>;
|
||||
clientId: number;
|
||||
client: ClientSchema;
|
||||
clientId: (number | null);
|
||||
client: (ClientSchema | null);
|
||||
shippingWarehouse?: (ShippingWarehouseSchema | string | null);
|
||||
billRequest?: (CardBillRequestSchema | null);
|
||||
group?: (CardGroupSchema | null);
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { StatusSchema } from './StatusSchema';
|
||||
export type CardSummary = {
|
||||
id: number;
|
||||
name: string;
|
||||
clientName: string;
|
||||
clientName: (string | null);
|
||||
createdAt: string;
|
||||
status: StatusSchema;
|
||||
board: BoardSchema;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ProjectSchema } from "../../client";
|
||||
import { AttributeSchema, ProjectSchema } from "../../client";
|
||||
import { UseFormReturnType } from "@mantine/form";
|
||||
import { rem, Stack } from "@mantine/core";
|
||||
import { ReactNode } from "react";
|
||||
@@ -11,7 +11,17 @@ type 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 (
|
||||
<CardAttributeField
|
||||
key={attribute.id}
|
||||
|
||||
@@ -22,6 +22,7 @@ const CardSummaryItem: FC<Props> = ({ cardSummary, color }) => {
|
||||
const { onDelete, onComplete, onDeleteFromGroup } = useCardSummaryState();
|
||||
|
||||
const isServicesAndProductsIncluded = isModuleInProject(Modules.SERVICES_AND_PRODUCTS, selectedProject);
|
||||
const isClientIncluded = isModuleInProject(Modules.CLIENTS, selectedProject);
|
||||
|
||||
const onDealSummaryClick = () => {
|
||||
CardService.getCardById({ cardId: cardSummary.id }).then(card => {
|
||||
@@ -61,16 +62,13 @@ const CardSummaryItem: FC<Props> = ({ cardSummary, color }) => {
|
||||
style={{ backgroundColor: color }}
|
||||
>
|
||||
<Flex direction={"column"} flex={1} gap={rem(3)}>
|
||||
{isClientIncluded && (
|
||||
<Flex justify={"space-between"}>
|
||||
|
||||
<Text
|
||||
c={"gray.6"}
|
||||
size={"xs"}
|
||||
|
||||
>
|
||||
<Text c={"gray.6"} size={"xs"}>
|
||||
{cardSummary.clientName}
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
<Text
|
||||
c={"blue.5"}
|
||||
|
||||
@@ -36,15 +36,14 @@ export const CardsDndColumn: FC<Props> = ({
|
||||
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[]);
|
||||
if (groups.length === 0) return [];
|
||||
const groupedSummaries = groupBy(summaries, "group.id");
|
||||
const groupDict = groups.reduce((acc, group) => {
|
||||
acc[group.id] = group;
|
||||
return acc;
|
||||
}
|
||||
, {} as { [key: number]: CardGroupSchema });
|
||||
}, {} as { [key: number]: CardGroupSchema });
|
||||
return Object.entries(groupedSummaries).reduce((acc, [groupId, cards]) => {
|
||||
if (!groupId) return acc;
|
||||
const group = groupDict[parseInt(groupId)];
|
||||
@@ -57,8 +56,8 @@ export const CardsDndColumn: FC<Props> = ({
|
||||
}, [] as { group: CardGroupSchema; cards: CardSummary[] }[]);
|
||||
};
|
||||
|
||||
const getDealsAndGroups = (): (GroupWithCards | CardSummary)[] => {
|
||||
const groups = getDealGroups();
|
||||
const getCardsAndGroups = (): (GroupWithCards | CardSummary)[] => {
|
||||
const groups = getCardGroups();
|
||||
const cards = summaries.filter(s => !s.group).sort((a, b) => {
|
||||
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)) {
|
||||
return renderGroup(obj);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Button, rem, Textarea, TextInput } from "@mantine/core";
|
||||
import { QuickCard } from "../../../../types/QuickCard.ts";
|
||||
import { FC } from "react";
|
||||
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 { DateTimePicker } from "@mantine/dates";
|
||||
import ShippingWarehouseAutocomplete
|
||||
@@ -19,22 +19,28 @@ type Props = {
|
||||
|
||||
const CreateCardForm: FC<Props> = ({ onSubmit, onCancel }) => {
|
||||
const { selectedProject } = useCardPageContext();
|
||||
const isPrefillingDealEnabled = isModuleInProject(Modules.SERVICES_AND_PRODUCTS, selectedProject);
|
||||
|
||||
const isPrefillingEnabled = isModuleInProject(Modules.SERVICES_AND_PRODUCTS, selectedProject);
|
||||
const { prefillOnOpen, prefillCard } = usePrefillCardContext();
|
||||
|
||||
const isServicesAndProductsIncluded = isModuleInProject(Modules.SERVICES_AND_PRODUCTS, selectedProject);
|
||||
const isClientsIncluded = isModuleInProject(Modules.CLIENTS, selectedProject);
|
||||
|
||||
const form = useForm<QuickCard>({
|
||||
initialValues: {
|
||||
name: "",
|
||||
clientName: "",
|
||||
clientAddress: "",
|
||||
clientName: null,
|
||||
comment: "",
|
||||
acceptanceDate: new Date(),
|
||||
shippingWarehouse: "",
|
||||
baseMarketplace: {
|
||||
key: "",
|
||||
iconUrl: "",
|
||||
name: "",
|
||||
shippingWarehouse: null,
|
||||
baseMarketplace: null,
|
||||
},
|
||||
validate: {
|
||||
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"]}>
|
||||
<TextInput
|
||||
placeholder={"Название сделки"}
|
||||
placeholder={"Название"}
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
</div>
|
||||
{isClientsIncluded && (
|
||||
<div className={styles["inputs"]}>
|
||||
<ClientAutocomplete
|
||||
nameRestProps={form.getInputProps("clientName")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isServicesAndProductsIncluded && (
|
||||
<>
|
||||
<div className={styles["inputs"]}>
|
||||
<BaseMarketplaceSelect
|
||||
rightSection={<></>}
|
||||
placeholder={"Базовый маркетплейс"}
|
||||
{...form.getInputProps("baseMarketplace")}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles["inputs"]}>
|
||||
<ShippingWarehouseAutocomplete
|
||||
{...form.getInputProps("shippingWarehouse")}
|
||||
placeholder={"Склад отгрузки"}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className={styles["inputs"]}>
|
||||
<Textarea
|
||||
autosize
|
||||
@@ -89,7 +103,7 @@ const CreateCardForm: FC<Props> = ({ onSubmit, onCancel }) => {
|
||||
{...form.getInputProps("acceptanceDate")}
|
||||
/>
|
||||
</div>
|
||||
{isPrefillingDealEnabled && (
|
||||
{isPrefillingEnabled && (
|
||||
<div className={styles["button-prefill"]}>
|
||||
<Button
|
||||
style={{ whiteSpace: "wrap" }}
|
||||
|
||||
@@ -20,7 +20,7 @@ type Props = {
|
||||
refetchProjects: () => void;
|
||||
}
|
||||
|
||||
const LeadsPageHeader = ({
|
||||
const CardsPageHeader = ({
|
||||
displayMode,
|
||||
setDisplayMode,
|
||||
form,
|
||||
@@ -124,4 +124,4 @@ const LeadsPageHeader = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default LeadsPageHeader;
|
||||
export default CardsPageHeader;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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}</>,
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
};
|
||||
@@ -2,7 +2,7 @@ import { Box, Drawer, rem, Tabs } from "@mantine/core";
|
||||
import { FC, ReactNode, useEffect } from "react";
|
||||
import { useCardPageContext } from "../../contexts/CardPageContext.tsx";
|
||||
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 { useQueryClient } from "@tanstack/react-query";
|
||||
import ProductAndServiceTab from "../../tabs/ProductAndServiceTab/ProductAndServiceTab.tsx";
|
||||
@@ -62,8 +62,16 @@ const CardEditDrawer: FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const getTab = (key: string, icon: ReactNode, label: string) => {
|
||||
if (!modules.has(key)) return;
|
||||
const getTab = (
|
||||
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 (
|
||||
<Tabs.Tab
|
||||
@@ -102,23 +110,19 @@ const CardEditDrawer: FC = () => {
|
||||
leftSection={<IconSettings />}>
|
||||
Общее
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
value={"client"}
|
||||
leftSection={<IconUser />}>
|
||||
Клиент
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
value={"history"}
|
||||
leftSection={<IconCalendarUser />}>
|
||||
История
|
||||
</Tabs.Tab>
|
||||
{getTab("clients", <IconUser />, "Клиент", ["servicesAndProducts", "clients"])}
|
||||
{getTab("servicesAndProducts", <IconBox />, "Товары и услуги")}
|
||||
{getTab("shipment", <IconCubeSend />, "Отгрузка")}
|
||||
{getTab("employees", <IconUsersGroup />, "Исполнители")}
|
||||
</Tabs.List>
|
||||
|
||||
{getTabPanel("general", <GeneralTab />)}
|
||||
{getTabPanel("client", <ClientTab />)}
|
||||
{getTabPanel("clients", <ClientTab />)}
|
||||
{getTabPanel("history", <CardEditDrawerStatusChangeTable />)}
|
||||
{getTabPanel("servicesAndProducts", <ProductAndServiceTab />)}
|
||||
{getTabPanel("shipment", <ShippingTab />)}
|
||||
|
||||
@@ -12,6 +12,8 @@ const ClientTab = () => {
|
||||
const initialValues: CardGeneralFormType = card as CardSchema;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
if (!card?.client) return;
|
||||
|
||||
const form = useForm<CardGeneralFormType>(
|
||||
{
|
||||
initialValues: initialValues,
|
||||
@@ -22,6 +24,8 @@ const ClientTab = () => {
|
||||
);
|
||||
const hasChanges = !isEqual(form.values, initialValues);
|
||||
const updateClientInfo = async (values: CardGeneralFormType) => {
|
||||
if (!values.client) return;
|
||||
|
||||
return ClientService.updateClient({
|
||||
requestBody: {
|
||||
data: values.client,
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
import { FC, useState } from "react";
|
||||
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 {
|
||||
CardSchema,
|
||||
@@ -44,6 +56,7 @@ const Content: FC<Props> = ({ card }) => {
|
||||
const [project, setProject] = useState<ProjectSchema | null>(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 => {
|
||||
return {
|
||||
@@ -67,8 +80,6 @@ const Content: FC<Props> = ({ card }) => {
|
||||
});
|
||||
|
||||
const updateCardInfo = async (values: CardGeneralFormType) => {
|
||||
console.log("Updated attributes:");
|
||||
console.log(values);
|
||||
const formCardAttrs = values as unknown as Attributes;
|
||||
|
||||
const attributes = project?.attributes.reduce((attrs, projectAttr) => {
|
||||
@@ -104,6 +115,7 @@ const Content: FC<Props> = ({ card }) => {
|
||||
});
|
||||
};
|
||||
const updateClientInfo = async (values: CardGeneralFormType) => {
|
||||
if (!values.client) return;
|
||||
return ClientService.updateClient({
|
||||
requestBody: {
|
||||
data: values.client,
|
||||
@@ -116,7 +128,9 @@ const Content: FC<Props> = ({ card }) => {
|
||||
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(
|
||||
{
|
||||
...values,
|
||||
@@ -128,7 +142,7 @@ const Content: FC<Props> = ({ card }) => {
|
||||
const isShippingWarehouse = (
|
||||
value: ShippingWarehouseSchema | string | null | undefined,
|
||||
): value is ShippingWarehouseSchema => {
|
||||
return !["string", "null", "undefined"].includes(typeof value);
|
||||
return !!value && !["string"].includes(typeof value);
|
||||
};
|
||||
|
||||
const onCopyGuestUrlClick = () => {
|
||||
@@ -146,8 +160,10 @@ const Content: FC<Props> = ({ card }) => {
|
||||
<form onSubmit={form.onSubmit(values => handleSubmit(values))}>
|
||||
<Flex
|
||||
direction={"column"}
|
||||
justify={"space-between"}
|
||||
h={"100%"}>
|
||||
h={"95vh"}
|
||||
>
|
||||
<ScrollArea>
|
||||
<Stack>
|
||||
<Fieldset legend={`Общие параметры [ID: ${card.id}]`}>
|
||||
<Flex
|
||||
direction={"column"}
|
||||
@@ -192,6 +208,7 @@ const Content: FC<Props> = ({ card }) => {
|
||||
placeholder={"Введите коментарий"}
|
||||
{...form.getInputProps("comment")}
|
||||
/>
|
||||
{isServicesAndProductsIncluded && (
|
||||
<ShippingWarehouseAutocomplete
|
||||
placeholder={"Введите склад отгрузки"}
|
||||
label={"Склад отгрузки"}
|
||||
@@ -214,11 +231,20 @@ const Content: FC<Props> = ({ card }) => {
|
||||
).onChange(event);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isManagerIncluded && (
|
||||
<ManagerSelect
|
||||
placeholder={"Укажите менеджера"}
|
||||
label={"Менеджер"}
|
||||
{...form.getInputProps("manager")}
|
||||
/>
|
||||
)}
|
||||
{isServicesAndProductsIncluded && (
|
||||
<Checkbox
|
||||
label={"Учет выручки с услуг в статистике"}
|
||||
{...form.getInputProps("isServicesProfitAccounted", { type: "checkbox" })}
|
||||
/>
|
||||
)}
|
||||
{project && (
|
||||
<CardAttributeFields
|
||||
project={project}
|
||||
@@ -227,6 +253,8 @@ const Content: FC<Props> = ({ card }) => {
|
||||
)}
|
||||
</Flex>
|
||||
</Fieldset>
|
||||
</Stack>
|
||||
</ScrollArea>
|
||||
<Flex
|
||||
mt={"md"}
|
||||
gap={rem(10)}
|
||||
@@ -303,7 +331,9 @@ const Content: FC<Props> = ({ card }) => {
|
||||
const GeneralTab: FC = () => {
|
||||
const { selectedCard } = useCardPageContext();
|
||||
if (!selectedCard) return <>No card selected</>;
|
||||
return <Content card={selectedCard} />;
|
||||
return (
|
||||
<Content card={selectedCard} />
|
||||
);
|
||||
};
|
||||
|
||||
export default GeneralTab;
|
||||
|
||||
@@ -30,7 +30,7 @@ const ProductAndServiceTab: FC = () => {
|
||||
useCardProductAndServiceTabState();
|
||||
const isLocked = Boolean(cardState.card?.billRequest || cardState.card?.group?.billRequest);
|
||||
const onAddProductClick = () => {
|
||||
if (!cardProductsState.onCreate || !cardState.card) return;
|
||||
if (!cardProductsState.onCreate || !cardState.card || !cardState.card.clientId) return;
|
||||
const productIds = cardState.card.products.map(
|
||||
product => product.product.id
|
||||
);
|
||||
@@ -108,7 +108,7 @@ const ProductAndServiceTab: FC = () => {
|
||||
await cardState.refetch();
|
||||
});
|
||||
};
|
||||
const onDealKitAdd = (kit: GetServiceKitSchema) => {
|
||||
const onCardKitAdd = (kit: GetServiceKitSchema) => {
|
||||
if (!cardState.card) return;
|
||||
CardService.addKitToCard({
|
||||
requestBody: {
|
||||
@@ -130,7 +130,7 @@ const ProductAndServiceTab: FC = () => {
|
||||
});
|
||||
};
|
||||
const onCreateProductClick = () => {
|
||||
if (!cardState.card) return;
|
||||
if (!cardState.card || !cardState.card.clientId) return;
|
||||
modals.openContextModal({
|
||||
modal: "createProduct",
|
||||
title: "Создание товара",
|
||||
@@ -240,7 +240,7 @@ const ProductAndServiceTab: FC = () => {
|
||||
direction={"column"}
|
||||
className={styles["deal-container-wrapper"]}>
|
||||
<CardServicesTable
|
||||
onKitAdd={onDealKitAdd}
|
||||
onKitAdd={onCardKitAdd}
|
||||
{...cardServicesState}
|
||||
/>
|
||||
|
||||
|
||||
@@ -5,14 +5,14 @@ import CardEditDrawer from "../drawers/CardEditDrawer/CardEditDrawer.tsx";
|
||||
import { CardPageContextProvider } from "../contexts/CardPageContext.tsx";
|
||||
import { rem } from "@mantine/core";
|
||||
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 CardPrefillDrawer from "../drawers/CardPrefillDrawer/CardPrefillDrawer.tsx";
|
||||
import { PrefillCardContextProvider } from "../contexts/PrefillCardContext.tsx";
|
||||
import { useParams } from "@tanstack/react-router";
|
||||
import { PrefillCardsWithExcelContextProvider } from "../contexts/PrefillDealsWithExcelContext.tsx";
|
||||
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 Boards from "../../../components/Dnd/Boards/Boards/Boards.tsx";
|
||||
import useBoards from "../hooks/useBoards.tsx";
|
||||
@@ -81,7 +81,7 @@ export const CardsPage: FC = () => {
|
||||
>
|
||||
<PrefillCardContextProvider>
|
||||
<PrefillCardsWithExcelContextProvider>
|
||||
<LeadsPageHeader
|
||||
<CardsPageHeader
|
||||
form={form}
|
||||
displayMode={displayMode}
|
||||
setDisplayMode={setDisplayMode}
|
||||
|
||||
@@ -2,10 +2,20 @@ import { ProjectSchema } from "../../../client";
|
||||
|
||||
export enum Modules {
|
||||
SERVICES_AND_PRODUCTS = "servicesAndProducts",
|
||||
SHIPMENT = "shipment",
|
||||
EMPLOYEES = "employees",
|
||||
CLIENTS = "clients",
|
||||
MANAGERS = "managers",
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { CardSummary, ShippingWarehouseSchema } from "../../../client";
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import { useForm } from "@mantine/form";
|
||||
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 CardEditDrawer from "../../CardsPage/drawers/CardEditDrawer/CardEditDrawer.tsx";
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ export const ProfitChart = () => {
|
||||
{ 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)}
|
||||
series={series}
|
||||
fillOpacity={0.5}
|
||||
key={idx}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -62,7 +62,7 @@ export const useProfitTableColumns = ({ groupTableBy, statuses }: Props) => {
|
||||
size: groupTableBy === GroupStatisticsTable.BY_DATES ? 40 : 80,
|
||||
},
|
||||
{
|
||||
accessorKey: "dealsCount",
|
||||
accessorKey: "cardsCount",
|
||||
header: "Кол-во",
|
||||
size: 40,
|
||||
},
|
||||
|
||||
@@ -2,10 +2,9 @@ import { BaseMarketplaceSchema } from "../client";
|
||||
|
||||
export type QuickCard = {
|
||||
name: string;
|
||||
clientName: string;
|
||||
clientAddress: string;
|
||||
clientName: string | null;
|
||||
comment: string;
|
||||
acceptanceDate: Date;
|
||||
shippingWarehouse: string;
|
||||
baseMarketplace: BaseMarketplaceSchema;
|
||||
shippingWarehouse: string | null;
|
||||
baseMarketplace: BaseMarketplaceSchema | null;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user