feat: projects and boards

This commit is contained in:
2025-02-07 20:07:10 +04:00
parent 58d397ed0b
commit 580552bd47
185 changed files with 3352 additions and 1284 deletions

View File

@@ -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;