Files
Fulfillment-Frontend/src/pages/LeadsPage/drawers/DealEditDrawer/tabs/DealEditDrawerGeneralTab.tsx

365 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { FC } from "react";
import { useDealPageContext } from "../../../contexts/DealPageContext.tsx";
import {
ActionIcon,
Button,
Checkbox,
Divider,
Fieldset,
Flex,
Group,
rem,
Textarea,
TextInput,
Tooltip,
} from "@mantine/core";
import { useForm } from "@mantine/form";
import { ClientService, DealSchema, DealService, ShippingWarehouseSchema } from "../../../../../client";
import { DealStatus, DealStatusDictionary } from "../../../../../shared/enums/DealStatus.ts";
import { isEqual } from "lodash";
import { notifications } from "../../../../../shared/lib/notifications.ts";
import { useQueryClient } from "@tanstack/react-query";
import ShippingWarehouseAutocomplete
from "../../../../../components/Selects/ShippingWarehouseAutocomplete/ShippingWarehouseAutocomplete.tsx";
import { ButtonCopyControlled } from "../../../../../components/ButtonCopyControlled/ButtonCopyControlled.tsx";
import { useClipboard } from "@mantine/hooks";
import ButtonCopy from "../../../../../components/ButtonCopy/ButtonCopy.tsx";
import FileSaver from "file-saver";
import { dateWithoutTimezone, getCurrentDateTimeForFilename } from "../../../../../shared/lib/date.ts";
import { IconBarcode, IconPrinter } from "@tabler/icons-react";
import styles from "../../../ui/LeadsPage.module.css";
import { base64ToBlob } from "../../../../../shared/lib/utils.ts";
import { DatePickerInput } from "@mantine/dates";
import ManagerSelect from "../../../../../components/ManagerSelect/ManagerSelect.tsx";
type Props = {
deal: DealSchema;
};
export type DealGeneralFormType = Omit<DealSchema, "statusHistory" | "services" | "products">;
const Content: FC<Props> = ({ deal }) => {
const { setSelectedDeal } = useDealPageContext();
const clipboard = useClipboard();
const queryClient = useQueryClient();
// ignore typescript
const initialValues: DealGeneralFormType = {
...deal,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
deliveryDate: deal.deliveryDate ? new Date(deal.deliveryDate) : null,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
receivingSlotDate: deal.receivingSlotDate ? new Date(deal.receivingSlotDate) : null,
};
const form = useForm<DealGeneralFormType>({
initialValues: initialValues,
validate: {
name: (value: string) =>
value.length > 0
? null
: "Название сделки не может быть пустым",
},
});
const updateDealInfo = async (values: DealGeneralFormType) => {
return DealService.updateDealGeneralInfo({
requestBody: {
dealId: deal.id,
data: {
...values,
shippingWarehouse: values.shippingWarehouse?.toString(),
},
},
}).then(({ ok, message }) => {
notifications.guess(ok, { message });
if (!ok) return;
DealService.getDealById({ dealId: deal.id }).then(data => {
setSelectedDeal(data);
form.setInitialValues(data);
queryClient.invalidateQueries({
queryKey: ["getDealSummaries"],
});
});
});
};
const updateClientInfo = async (values: DealGeneralFormType) => {
return ClientService.updateClient({
requestBody: {
data: values.client,
},
}).then(({ ok, message }) => notifications.guess(ok, { message }));
};
const handleSubmit = async (values: DealGeneralFormType) => {
// Updating client info if there changes
if (!isEqual(values.client, deal.client)) {
await updateClientInfo(values);
}
// updating deal info
// get shimpent warehouse name from object if its object, otherwise just pass it
const shippingWarehouse = isShippingWarehouse(values.shippingWarehouse) ? values.shippingWarehouse.name : values.shippingWarehouse;
await updateDealInfo({
...values,
deliveryDate: values.deliveryDate ? dateWithoutTimezone(new Date(values.deliveryDate)) : null,
receivingSlotDate: values.receivingSlotDate ? dateWithoutTimezone(new Date(values.receivingSlotDate)) : null,
shippingWarehouse: shippingWarehouse,
});
};
const isShippingWarehouse = (
value: ShippingWarehouseSchema | string | null | undefined,
): value is ShippingWarehouseSchema => {
return !["string", "null", "undefined"].includes(typeof value);
};
const onCopyGuestUrlClick = () => {
DealService.createDealGuestUrl({
requestBody: {
dealId: deal.id,
},
}).then(({ ok, message, url }) => {
if (!ok) notifications.guess(ok, { message });
clipboard.copy(`${window.location.origin}/${url}`);
});
};
const billRequestPdfUrl = deal?.billRequest?.pdfUrl || deal?.group?.billRequest?.pdfUrl;
return (
<form onSubmit={form.onSubmit(values => handleSubmit(values))}>
<Flex
direction={"column"}
justify={"space-between"}
h={"100%"}>
<Fieldset legend={`Общие параметры [ID: ${deal.id}]`}>
<Flex
direction={"column"}
gap={rem(10)}>
<TextInput
placeholder={"Название сделки"}
label={"Название сделки"}
{...form.getInputProps("name")}
/>
<TextInput
disabled
placeholder={"Дата создания"}
label={"Дата создания"}
value={new Date(deal.createdAt).toLocaleString(
"ru-RU",
)}
/>
<TextInput
disabled
placeholder={"Текущий статус"}
label={"Текущий статус"}
value={
DealStatusDictionary[
deal.currentStatus as DealStatus
]
}
/>
{deal.category && (
<TextInput
disabled
placeholder={"Категория"}
label={"Категория"}
value={deal.category.name}
/>
)}
<Textarea
h={rem(150)}
styles={{
wrapper: {height:'90%'},
input: {height:'90%'}
}}
label={"Коментарий к сделке"}
placeholder={"Введите коментарий к сделке"}
{...form.getInputProps("comment")}
/>
<ShippingWarehouseAutocomplete
placeholder={"Введите склад отгрузки"}
label={"Склад отгрузки"}
value={
isShippingWarehouse(
form.values.shippingWarehouse,
)
? form.values.shippingWarehouse
: undefined
}
onChange={event => {
if (isShippingWarehouse(event)) {
form.getInputProps(
"shippingWarehouse",
).onChange(event.name);
return;
}
form.getInputProps(
"shippingWarehouse",
).onChange(event);
}}
/>
<DatePickerInput
minDate={new Date()}
placeholder={"Укажите дату передачи в доставку"}
label={"Дата передачи в доставку"}
{...form.getInputProps("deliveryDate")}
/>
<DatePickerInput
minDate={new Date()}
placeholder={"Укажите слот приемки"}
label={"Слот приемки"}
{...form.getInputProps("receivingSlotDate")}
/>
<ManagerSelect
placeholder={"Укажите менеджера"}
label={"Менеджер"}
{...form.getInputProps("manager")}
/>
</Flex>
</Fieldset>
<Flex
mt={"md"}
gap={rem(10)}
align={"center"}
justify={"flex-end"}>
<Flex
align={"center"}
gap={rem(10)}
justify={"center"}>
<Flex
gap={rem(10)}
align={"center"}
justify={"space-between"}>
<Tooltip
className={styles["print-deals-button"]}
label={"Распечатать штрихкоды сделки"}
>
<ActionIcon
onClick={async () => {
const response =
await DealService.getDealProductsBarcodesPdf({
requestBody: {
dealId: deal.id,
},
});
const pdfBlob = base64ToBlob(
response.base64String,
response.mimeType,
);
const pdfUrl = URL.createObjectURL(pdfBlob);
const pdfWindow = window.open(pdfUrl);
if (!pdfWindow) {
notifications.error({ message: "Ошибка" });
return;
}
pdfWindow.onload = () => {
pdfWindow.print();
};
}}
variant={"default"}>
<IconBarcode />
</ActionIcon>
</Tooltip>
<Tooltip label={"Распечатать сделку"}>
<ActionIcon
onClick={() => {
const pdfWindow = window.open(
`${import.meta.env.VITE_API_URL}/deal/tech-spec/${deal.id}`,
);
if (!pdfWindow) return;
pdfWindow.print();
}}
variant={"default"}>
<IconPrinter />
</ActionIcon>
</Tooltip>
<Flex gap={rem(10)}>
{billRequestPdfUrl ? (
<ButtonCopy
onCopiedLabel={
"Ссылка скопирована в буфер обмена"
}
value={billRequestPdfUrl}>
Ссылка на оплату
</ButtonCopy>
) : (
<ButtonCopyControlled
onCopyClick={() => {
const date =
getCurrentDateTimeForFilename();
FileSaver.saveAs(
`${import.meta.env.VITE_API_URL}/deal/billing-document/${deal.id}`,
`bill_${deal.id}_${date}.pdf`,
);
}}
copied={false}
onCopiedLabel={
"Ссылка скопирована в буфер обмена"
}>
Ссылка на оплату (PDF)
</ButtonCopyControlled>
)}
<ButtonCopyControlled
onCopyClick={onCopyGuestUrlClick}
onCopiedLabel={
"Ссылка скопирована в буфер обмена"
}
copied={clipboard.copied}>
Ссылка на редактирование
</ButtonCopyControlled>
</Flex>
</Flex>
<Flex gap={rem(10)}>
<Checkbox
label={"Оплачен"}
checked={deal.billRequest?.paid || deal.group?.billRequest?.paid || false}
disabled
/>
<Checkbox
label={"Учет выручки"}
{...form.getInputProps("isAccounted", { type: "checkbox" })}
/>
<Checkbox
label={"Завершена"}
{...form.getInputProps("isCompleted", { type: "checkbox" })}
/>
<Checkbox
label={"Удалена"}
{...form.getInputProps("isDeleted", { type: "checkbox" })}
/>
</Flex>
</Flex>
<Divider orientation={"vertical"} />
<Group
align={"center"}
justify={"center"}>
<Button
color={"red"}
type={"reset"}
disabled={isEqual(initialValues, form.values)}
onClick={() => form.reset()}>
Отменить изменения
</Button>
<Button
variant={"default"}
type={"submit"}
disabled={isEqual(initialValues, form.values)}>
Сохранить изменения
</Button>
</Group>
</Flex>
</Flex>
</form>
);
};
const DealEditDrawerGeneralTab: FC = () => {
const { selectedDeal } = useDealPageContext();
if (!selectedDeal) return <>No deal selected</>;
return <Content deal={selectedDeal} />;
};
export default DealEditDrawerGeneralTab;