feat: projects and boards
This commit is contained in:
@@ -0,0 +1,381 @@
|
||||
import { FC, useState } 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,
|
||||
ProjectSchema,
|
||||
ShippingWarehouseSchema,
|
||||
StatusSchema,
|
||||
} from "../../../../../client";
|
||||
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/DealsPage.module.css";
|
||||
import { base64ToBlob } from "../../../../../shared/lib/utils.ts";
|
||||
import { DatePickerInput } from "@mantine/dates";
|
||||
import ManagerSelect from "../../../../../components/ManagerSelect/ManagerSelect.tsx";
|
||||
import ProjectSelect from "../../../../../components/ProjectSelect/ProjectSelect.tsx";
|
||||
import BoardSelect from "../../../../../components/BoardSelect/BoardSelect.tsx";
|
||||
import DealStatusSelect from "../../../../../components/DealStatusSelect/DealStatusSelect.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();
|
||||
const [project, setProject] = useState<ProjectSchema | null>(deal.board.project);
|
||||
|
||||
// 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
|
||||
: "Название сделки не может быть пустым",
|
||||
status: (value: StatusSchema) =>
|
||||
!value && "Статус для сделки не выбран",
|
||||
},
|
||||
});
|
||||
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",
|
||||
)}
|
||||
/>
|
||||
<ProjectSelect
|
||||
value={project}
|
||||
onChange={setProject}
|
||||
label={"Проект"}
|
||||
/>
|
||||
<BoardSelect
|
||||
project={project}
|
||||
{...form.getInputProps("board")}
|
||||
label={"Доска"}
|
||||
/>
|
||||
<DealStatusSelect
|
||||
board={form.values.board}
|
||||
{...form.getInputProps("status")}
|
||||
label={"Статус"}
|
||||
/>
|
||||
{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;
|
||||
Reference in New Issue
Block a user