feat: processing of modules in card, renaming
This commit is contained in:
@@ -13,5 +13,6 @@ export type CardGeneralInfoSchema = {
|
|||||||
manager?: (UserSchema | null);
|
manager?: (UserSchema | null);
|
||||||
boardId: number;
|
boardId: number;
|
||||||
statusId: number;
|
statusId: number;
|
||||||
|
isServicesProfitAccounted: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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"}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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" }}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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 { 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 />)}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user