feat: cards, attributes and modules

This commit is contained in:
2025-02-19 14:46:13 +04:00
parent cc3e72bf94
commit dc9455966e
286 changed files with 2355 additions and 2168 deletions

View File

@@ -0,0 +1,309 @@
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 { useForm } from "@mantine/form";
import {
CardSchema,
CardService,
ClientService,
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 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";
import CardAttributeFields from "../../../../components/CardAttributeFields/CardAttributeFields.tsx";
import getAttributesFromCard from "../../../../components/CardAttributeFields/utils/getAttributesFromCard.ts";
import isModuleInProject, { Modules } from "../../utils/isModuleInProject.ts";
import PaymentLinkButton from "./components/PaymentLinkButton.tsx";
import PrintDealBarcodesButton from "./components/PrintDealBarcodesButton.tsx";
type Props = {
card: CardSchema;
};
type Attributes = {
[key: string]: number | boolean | string;
};
export type CardGeneralFormType = Omit<CardSchema, "statusHistory" | "services" | "products">;
const Content: FC<Props> = ({ card }) => {
const { setSelectedCard } = useCardPageContext();
const clipboard = useClipboard();
const queryClient = useQueryClient();
const [project, setProject] = useState<ProjectSchema | null>(card.board.project);
const isServicesAndProductsIncluded = isModuleInProject(Modules.SERVICES_AND_PRODUCTS, card.board.project);
const getInitialValues = (card: CardSchema): CardGeneralFormType => {
return {
...card,
...getAttributesFromCard(card),
};
};
let initialValues = getInitialValues(card);
const form = useForm<CardGeneralFormType>({
initialValues,
validate: {
name: (value: string) =>
value.length > 0
? null
: "Название не может быть пустым",
status: (value: StatusSchema) =>
!value && "Статус не выбран",
},
});
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) => {
return {
...attrs,
[projectAttr.name]: formCardAttrs[projectAttr.name],
};
}, {});
return CardService.updateCardGeneralInfo({
requestBody: {
cardId: card.id,
data: {
...values,
statusId: values.status.id,
boardId: values.board.id,
shippingWarehouse: values.shippingWarehouse?.toString(),
attributes,
},
},
}).then(({ ok, message }) => {
notifications.guess(ok, { message });
if (!ok) return;
CardService.getCardById({ cardId: card.id }).then(data => {
console.log(data);
setSelectedCard(data);
initialValues = getInitialValues(data);
form.setValues(initialValues);
queryClient.invalidateQueries({
queryKey: ["getCardSummaries"],
});
});
});
};
const updateClientInfo = async (values: CardGeneralFormType) => {
return ClientService.updateClient({
requestBody: {
data: values.client,
},
}).then(({ ok, message }) => notifications.guess(ok, { message }));
};
const handleSubmit = async (values: CardGeneralFormType) => {
// Updating client info if there changes
if (!isEqual(values.client, card.client)) {
await updateClientInfo(values);
}
const shippingWarehouse = isShippingWarehouse(values.shippingWarehouse) ? values.shippingWarehouse.name : values.shippingWarehouse;
await updateCardInfo(
{
...values,
shippingWarehouse,
},
);
};
const isShippingWarehouse = (
value: ShippingWarehouseSchema | string | null | undefined,
): value is ShippingWarehouseSchema => {
return !["string", "null", "undefined"].includes(typeof value);
};
const onCopyGuestUrlClick = () => {
CardService.createDealGuestUrl({
requestBody: {
cardId: card.id,
},
}).then(({ ok, message, url }) => {
if (!ok) notifications.guess(ok, { message });
clipboard.copy(`${window.location.origin}/${url}`);
});
};
return (
<form onSubmit={form.onSubmit(values => handleSubmit(values))}>
<Flex
direction={"column"}
justify={"space-between"}
h={"100%"}>
<Fieldset legend={`Общие параметры [ID: ${card.id}]`}>
<Flex
direction={"column"}
gap={rem(10)}
>
<TextInput
placeholder={"Название сделки"}
label={"Название сделки"}
{...form.getInputProps("name")}
/>
<TextInput
disabled
placeholder={"Дата создания"}
label={"Дата создания"}
value={new Date(card.createdAt).toLocaleString(
"ru-RU",
)}
/>
<ProjectSelect
value={project}
onChange={setProject}
label={"Проект"}
disabled
/>
<BoardSelect
project={project}
{...form.getInputProps("board")}
label={"Доска"}
/>
<DealStatusSelect
board={form.values.board}
{...form.getInputProps("status")}
label={"Статус"}
/>
<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);
}}
/>
<ManagerSelect
placeholder={"Укажите менеджера"}
label={"Менеджер"}
{...form.getInputProps("manager")}
/>
{project && (
<CardAttributeFields
project={project}
form={form}
/>
)}
</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"}>
{isServicesAndProductsIncluded && (
<PrintDealBarcodesButton card={card}/>
)}
<Flex gap={rem(10)}>
{isServicesAndProductsIncluded && (
<PaymentLinkButton card={card} />
)}
<ButtonCopyControlled
onCopyClick={onCopyGuestUrlClick}
onCopiedLabel={
"Ссылка скопирована в буфер обмена"
}
copied={clipboard.copied}
>
Ссылка на редактирование
</ButtonCopyControlled>
</Flex>
</Flex>
<Flex gap={rem(10)}>
{isServicesAndProductsIncluded && (
<Checkbox
label={"Оплачен"}
checked={card.billRequest?.paid || card.group?.billRequest?.paid || false}
disabled
/>
)}
<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 GeneralTab: FC = () => {
const { selectedCard } = useCardPageContext();
if (!selectedCard) return <>No card selected</>;
return <Content card={selectedCard} />;
};
export default GeneralTab;