feat: client tab in deal editor

This commit is contained in:
2025-10-05 12:05:23 +04:00
parent d14920df7d
commit 0fcf086861
21 changed files with 511 additions and 35 deletions

View File

@ -0,0 +1,29 @@
import { FC } from "react";
import { Stack } from "@mantine/core";
import { DealSchema } from "@/lib/client";
import ClientDataForm from "@/modules/dealModularEditorTabs/Clients/shared/components/ClientDataForm";
import ClientSelectForm from "@/modules/dealModularEditorTabs/Clients/shared/components/ClientSelectForm";
import { ClientTabContextProvider } from "@/modules/dealModularEditorTabs/Clients/shared/contexts/ClientTabContext";
type Props = {
value: DealSchema;
onChange: (value: DealSchema) => void;
};
const ClientTab: FC<Props> = ({ value, onChange }) => {
return (
<Stack
flex={1}
mt={"xs"}
mx={"xs"}>
<ClientTabContextProvider
value={value}
onChange={onChange}>
<ClientSelectForm />
<ClientDataForm />
</ClientTabContextProvider>
</Stack>
);
};
export default ClientTab;

View File

@ -0,0 +1,119 @@
import { FC } from "react";
import { IMaskInput } from "react-imask";
import {
Button,
Fieldset,
Group,
Input,
Stack,
Textarea,
TextInput,
} from "@mantine/core";
import { useClientsCrud } from "@/app/clients/hooks/useClientsCrud";
import FormFlexRow from "@/components/ui/FormFlexRow/FormFlexRow";
import useIsMobile from "@/hooks/utils/useIsMobile";
import { ClientSchema } from "@/lib/client";
import { getClientsQueryKey } from "@/lib/client/@tanstack/react-query.gen";
import { useClientTabContext } from "../contexts/ClientTabContext";
const ClientDataForm: FC = () => {
const { client, form, isEditorDisabled } = useClientTabContext();
const isMobile = useIsMobile();
const clientsQueryKey = getClientsQueryKey();
const clientsCrud = useClientsCrud({ queryKey: clientsQueryKey });
const handleSubmitClientInfo = (values: ClientSchema) => {
if (!client) return;
if (values.details?.phoneNumber === "+7 ")
values.details!.phoneNumber = "";
clientsCrud.onUpdate(client.id, values, () => form.resetDirty());
};
return (
<Fieldset
radius={"md"}
legend={"Данные клиента"}
flex={1}>
<form
onSubmit={form.onSubmit(values =>
handleSubmitClientInfo(values as ClientSchema)
)}>
<Stack gap={"xs"}>
<FormFlexRow>
<TextInput
disabled
placeholder={"Название"}
label={"Название"}
{...form.getInputProps("name")}
flex={1}
/>
<TextInput
disabled
placeholder={"Наименование компании"}
label={"Наименование компании"}
{...form.getInputProps("companyName")}
flex={1}
/>
</FormFlexRow>
<FormFlexRow>
<Input.Wrapper
label={"Номер телефона"}
flex={1}
error={
form.getInputProps("details.phoneNumber").error
}>
<Input
component={IMaskInput}
mask="+7 000 000-00-00"
placeholder={"Введите номер телефона"}
{...form.getInputProps("details.phoneNumber")}
disabled={isEditorDisabled}
/>
</Input.Wrapper>
<TextInput
disabled={isEditorDisabled}
placeholder={"Введите email"}
label={"Email"}
{...form.getInputProps("details.email")}
flex={1}
/>
</FormFlexRow>
<FormFlexRow>
<TextInput
disabled={isEditorDisabled}
placeholder={"Введите телеграм"}
label={"Телеграм"}
{...form.getInputProps("details.telegram")}
flex={1}
/>
<TextInput
disabled={isEditorDisabled}
placeholder={"Введите ИНН"}
label={"ИНН"}
{...form.getInputProps("details.inn")}
flex={1}
/>
</FormFlexRow>
<Textarea
disabled={isEditorDisabled}
placeholder={"Введите комментарий"}
label={"Комментарий"}
{...form.getInputProps("comment")}
/>
<Group>
<Button
variant={"default"}
disabled={isEditorDisabled || !form.isDirty()}
w={isMobile ? "100%" : "auto"}
type={"submit"}>
Сохранить
</Button>
</Group>
</Stack>
</form>
</Fieldset>
);
};
export default ClientDataForm;

View File

@ -0,0 +1,68 @@
import { FC, useEffect, useMemo, useState } from "react";
import { Text } from "@mantine/core";
import useClientsList from "@/app/clients/hooks/useClientsList";
import ObjectSelect, {
ObjectSelectProps,
} from "@/components/selects/ObjectSelect/ObjectSelect";
import { ClientSchema, DealSchema } from "@/lib/client";
type RestProps = {
includeDeleted?: boolean;
deal?: DealSchema;
};
type Props = Omit<ObjectSelectProps<ClientSchema>, "data"> & RestProps;
const ClientSelect: FC<Props> = ({ includeDeleted = false, ...props }) => {
const { clients } = useClientsList({ includeDeleted });
const filteredClients = useMemo(
() =>
clients.filter(
client =>
!client.isDeleted ||
(includeDeleted && client.id === props.deal?.client?.id)
),
[clients]
);
const [deletedClientIds, setDeletedClientIds] = useState<Set<number>>(
new Set()
);
const [warning, setWarning] = useState<string>("");
useEffect(() => {
const deletedClientIds = new Set(
clients.filter(client => client.isDeleted).map(client => client.id)
);
setDeletedClientIds(deletedClientIds);
}, [clients]);
useEffect(() => {
if (props.value && deletedClientIds.has(props.value.id)) {
setWarning("Выбран удаленный клиент");
} else {
setWarning("");
}
}, [props.value, deletedClientIds]);
return (
<>
<ObjectSelect
searchable
data={filteredClients}
placeholder={"Выберите клиента"}
{...props}
/>
{warning && (
<Text
size={"sm"}
c={"yellow"}>
{warning}
</Text>
)}
</>
);
};
export default ClientSelect;

View File

@ -0,0 +1,44 @@
import { Fieldset, Group, Stack } from "@mantine/core";
import InlineButton from "@/components/ui/InlineButton/InlineButton";
import useIsMobile from "@/hooks/utils/useIsMobile";
import ClientSelect from "@/modules/dealModularEditorTabs/Clients/shared/components/ClientSelect";
import { useClientTabContext } from "../contexts/ClientTabContext";
const ClientSelectForm = () => {
const {
client,
setClient,
submitClientSelection,
form,
isEditorDisabled,
deal,
} = useClientTabContext();
const isMobile = useIsMobile();
return (
<Fieldset
legend={"Выбор клиента"}
radius={"md"}
flex={1}>
<Stack gap={"xs"}>
<ClientSelect
value={client}
onChange={setClient}
disabled={form.isDirty()}
deal={deal}
includeDeleted
/>
<Group>
<InlineButton
onClick={submitClientSelection}
disabled={!isEditorDisabled}
w={isMobile ? "100%" : "auto"}>
Сохранить
</InlineButton>
</Group>
</Stack>
</Fieldset>
);
};
export default ClientSelectForm;

View File

@ -0,0 +1,92 @@
"use client";
import { Dispatch, SetStateAction, useEffect, useMemo, useState } from "react";
import phone from "phone";
import { useForm, UseFormReturnType } from "@mantine/form";
import isValidInn from "@/app/clients/utils/isValidInn";
import { ClientSchema, DealSchema } from "@/lib/client";
import makeContext from "@/lib/contextFactory/contextFactory";
type ClientTabContextState = {
form: UseFormReturnType<Partial<ClientSchema>>;
client: ClientSchema | undefined;
setClient: Dispatch<SetStateAction<ClientSchema | undefined>>;
isEditorDisabled: boolean;
submitClientSelection: () => void;
deal: DealSchema;
};
type Props = {
value: DealSchema;
onChange: (value: DealSchema) => void;
};
const useClientTabContextState = ({
value,
onChange,
}: Props): ClientTabContextState => {
const initialValues = value?.client ?? {
name: "",
companyName: "",
comment: "",
isDeleted: false,
details: { email: "", inn: "", phoneNumber: "", telegram: "" },
};
const [client, setClient] = useState<ClientSchema | undefined>(
value?.client ?? undefined
);
const form = useForm<Partial<ClientSchema>>({
initialValues,
validate: {
details: {
phoneNumber: phoneNumber => {
if (!phoneNumber || phoneNumber === "+7 ") return false;
return (
!phone(phoneNumber || "", {
country: "",
strictDetection: false,
validateMobilePrefix: false,
}).isValid && "Неверно указан номер телефона"
);
},
inn: (inn: string | undefined | null) =>
inn && !isValidInn(inn) ? "Некорректный ИНН" : null,
},
},
});
useEffect(() => {
const data = value?.client ?? {};
form.setValues(data);
form.resetDirty();
}, [value]);
const isEditorDisabled = useMemo(
() =>
!client ||
client?.id !== value?.client?.id ||
client?.isDeleted === true,
[client, value.client?.id]
);
const submitClientSelection = () => {
if (!(value && client)) return;
onChange({ ...value, client });
};
return {
form,
client,
setClient,
isEditorDisabled,
submitClientSelection,
deal: value,
};
};
export const [ClientTabContextProvider, useClientTabContext] = makeContext<
ClientTabContextState,
Props
>(useClientTabContextState, "ClientTab");

View File

@ -1,3 +1,4 @@
export { default as CommonServicesTab } from "@/modules/dealModularEditorTabs/FulfillmentBase/mobile/CommonServicesTab";
export { default as FulfillmentBaseTab } from "@/modules/dealModularEditorTabs/FulfillmentBase/desktop/FulfillmentBaseTab";
export { default as ProductsTab } from "@/modules/dealModularEditorTabs/FulfillmentBase/mobile/ProductsTab";
export { default as ClientsTab } from "@/modules/dealModularEditorTabs/Clients/shared/ClientsTab";

View File

@ -1,16 +1,19 @@
import {
IconColumns,
IconBox,
IconColumns,
IconUsers,
} from "@tabler/icons-react";
import {
CommonServicesTab,
ProductsTab,
CommonServicesTab,
FulfillmentBaseTab,
ClientsTab,
} from "./dealModularEditorTabs";
import ModulesType from "./types";
export enum ModuleNames {
FULFILLMENT_BASE = "fulfillment_base",
CLIENTS = "clients",
}
const MODULES: ModulesType = {
@ -20,13 +23,6 @@ const MODULES: ModulesType = {
label: "Фулфиллмент",
},
tabs: [
{
label: "Услуги",
key: "common_services",
icon: <IconColumns />,
device: "mobile",
tab: (props: any) => <CommonServicesTab {...props} key={"common_services"} />,
},
{
label: "Товары",
key: "products",
@ -34,6 +30,13 @@ const MODULES: ModulesType = {
device: "mobile",
tab: (props: any) => <ProductsTab {...props} key={"products"} />,
},
{
label: "Услуги",
key: "common_services",
icon: <IconColumns />,
device: "mobile",
tab: (props: any) => <CommonServicesTab {...props} key={"common_services"} />,
},
{
label: "Фулфиллмент",
key: "fulfillment_base",
@ -43,6 +46,21 @@ const MODULES: ModulesType = {
},
]
},
[ModuleNames.CLIENTS]: {
info: {
key: "clients",
label: "Клиенты",
},
tabs: [
{
label: "Клиенты",
key: "clients",
icon: <IconUsers />,
device: "both",
tab: (props: any) => <ClientsTab {...props} key={"clients"} />,
},
]
},
};
export default MODULES;