365 lines
12 KiB
TypeScript
365 lines
12 KiB
TypeScript
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||
import {
|
||
Modal,
|
||
Form,
|
||
Select,
|
||
Row,
|
||
Col,
|
||
Checkbox,
|
||
Input,
|
||
Empty,
|
||
Spin,
|
||
} from "antd";
|
||
import {
|
||
fetchLegEntsByComGroup,
|
||
fetchGSKByComGroup,
|
||
fetchGroupMembers,
|
||
} from "../../services/api/directories/company_groups";
|
||
import { useAppDispatch, useAppSelector } from "../../store/store";
|
||
import { getCompanyGroupsThunk } from "../../store/slices/companyGroupsSlice/thunks";
|
||
|
||
type FormValues = {
|
||
company_group_id: string;
|
||
related_group_id: string;
|
||
year: number;
|
||
quarter: string; // example of 2nd static field
|
||
legal_entity_ids: string[];
|
||
};
|
||
|
||
type Props = {
|
||
open: boolean;
|
||
onCancel: () => void;
|
||
onOk: (values: FormValues) => void | Promise<void>;
|
||
};
|
||
|
||
export const MyModal: React.FC<Props> = ({ open, onCancel, onOk }) => {
|
||
const [form] = Form.useForm<FormValues>();
|
||
const dispatch = useAppDispatch();
|
||
|
||
const { raw_rows, loading } = useAppSelector((state) => state.company_groups);
|
||
|
||
const [subgroups, setSubgroups] = useState<any[]>([]);
|
||
const [loadingSubgroups, setLoadingSubgroups] = useState(false);
|
||
const subgroupReqRef = useRef(0);
|
||
|
||
const [legEntities, setLegEntities] = useState<any[]>([]);
|
||
const [loadingEntities, setLoadingEntities] = useState(false);
|
||
const legEntReqRef = useRef(0);
|
||
|
||
const [legEntSearch, setLegEntSearch] = useState("");
|
||
const gskEntReqRef = useRef(0);
|
||
|
||
const company_group_id = Form.useWatch("company_group_id", form);
|
||
const related_group_id = Form.useWatch("related_group_id", form);
|
||
const year = Form.useWatch("year", form);
|
||
const semester = Form.useWatch("quarter", form);
|
||
const legEntIds = Form.useWatch("legal_entity_ids", form);
|
||
|
||
const quarterOptions = [
|
||
{ value: "Q1", label: "1 квартал" },
|
||
{ value: "Q2", label: "2 квартал" },
|
||
{ value: "Q3", label: "3 квартал" },
|
||
{ value: "Q4", label: "4 квартал" },
|
||
];
|
||
useEffect(() => {
|
||
if (open && raw_rows.length === 0 && !loading) {
|
||
dispatch(getCompanyGroupsThunk());
|
||
}
|
||
}, [open, raw_rows.length, loading, dispatch]);
|
||
|
||
const isSubmitDisabled =
|
||
!company_group_id ||
|
||
!related_group_id ||
|
||
!year ||
|
||
!semester ||
|
||
!legEntIds?.length;
|
||
|
||
const yearOptions = useMemo(() => {
|
||
const y = new Date().getFullYear();
|
||
return Array.from({ length: 4 }, (_, i) => ({
|
||
value: y - i,
|
||
label: String(y - i),
|
||
}));
|
||
}, []);
|
||
|
||
const clearLegEnts = () => {
|
||
setLegEntities([]);
|
||
setLoadingEntities(false);
|
||
setLegEntSearch("");
|
||
legEntReqRef.current++;
|
||
};
|
||
|
||
// const handleGroupChange = async (value?: string) => {
|
||
// form.setFieldsValue({
|
||
// related_group_id: undefined as any,
|
||
// legal_entity_ids: [],
|
||
// });
|
||
// setSubgroups([]);
|
||
// clearLegEnts();
|
||
// if (!value) return;
|
||
// const reqId = ++subgroupReqRef.current;
|
||
// setLoadingSubgroups(true);
|
||
// try {
|
||
// const data = await fetchGSKByComGroup(value);
|
||
// if (reqId === subgroupReqRef.current) setSubgroups(data);
|
||
// } finally {
|
||
// if (reqId === subgroupReqRef.current) setLoadingSubgroups(false);
|
||
// }
|
||
// };
|
||
|
||
const handleSubGroupChange = async (value?: string) => {
|
||
form.setFieldsValue({ legal_entity_ids: [] });
|
||
if (!value) return;
|
||
const reqId = ++gskEntReqRef.current;
|
||
setLoadingEntities(true);
|
||
try {
|
||
const data = await fetchGroupMembers(value);
|
||
|
||
if (reqId !== gskEntReqRef.current) return;
|
||
const gskEntitiesIds = new Set(data.map((c) => c.legal_entity_id));
|
||
const sorted = [...legEntities].sort((a, b) => {
|
||
const a_gsk = gskEntitiesIds.has(a.id);
|
||
const b_gsk = gskEntitiesIds.has(b.id);
|
||
if (a_gsk !== b_gsk) return a_gsk ? -1 : 1;
|
||
return a.name.localeCompare(b.name);
|
||
});
|
||
setLegEntities(sorted);
|
||
form.setFieldsValue({ legal_entity_ids: [...gskEntitiesIds] });
|
||
} finally {
|
||
if (reqId === gskEntReqRef.current) setLoadingEntities(false);
|
||
}
|
||
};
|
||
const handleGroupChange = async (value?: string) => {
|
||
form.setFieldsValue({
|
||
related_group_id: undefined as any,
|
||
legal_entity_ids: [],
|
||
});
|
||
setSubgroups([]);
|
||
clearLegEnts();
|
||
if (!value) return;
|
||
const reqId = ++subgroupReqRef.current;
|
||
const leReqId = ++legEntReqRef.current;
|
||
setLoadingSubgroups(true);
|
||
setLoadingEntities(true);
|
||
try {
|
||
const dataSubgroups = await fetchGSKByComGroup(value);
|
||
const dataLegEnts = await fetchLegEntsByComGroup(value);
|
||
if (reqId === subgroupReqRef.current) setSubgroups(dataSubgroups);
|
||
if (leReqId === legEntReqRef.current) setLegEntities(dataLegEnts);
|
||
} finally {
|
||
if (reqId === subgroupReqRef.current) setLoadingSubgroups(false);
|
||
if (leReqId === legEntReqRef.current) setLoadingEntities(false);
|
||
}
|
||
};
|
||
|
||
const resetLocal = () => {
|
||
setSubgroups([]);
|
||
setLoadingSubgroups(false);
|
||
subgroupReqRef.current++;
|
||
clearLegEnts();
|
||
setLegEntSearch("");
|
||
};
|
||
|
||
const handleOk = async () => {
|
||
try {
|
||
const values = await form.validateFields();
|
||
await onOk(values);
|
||
form.resetFields();
|
||
resetLocal();
|
||
} catch {}
|
||
};
|
||
|
||
const handleCancel = () => {
|
||
form.resetFields();
|
||
resetLocal();
|
||
onCancel();
|
||
};
|
||
|
||
const filteredLegEnts = useMemo(() => {
|
||
if (!legEntSearch) return legEntities;
|
||
const q = legEntSearch.toLowerCase();
|
||
return legEntities.filter((s) => s.name.toLowerCase().includes(q));
|
||
}, [legEntities, legEntSearch]);
|
||
|
||
return (
|
||
<Modal
|
||
title="Новая консолидация"
|
||
open={open}
|
||
onCancel={handleCancel}
|
||
onOk={handleOk}
|
||
okButtonProps={{ disabled: isSubmitDisabled }}
|
||
okText="Submit"
|
||
destroyOnHidden
|
||
width={640}
|
||
>
|
||
<Form form={form} layout="vertical" preserve={false}>
|
||
<Row gutter={16}>
|
||
<Col span={12}>
|
||
<Form.Item
|
||
name="company_group_id"
|
||
label="Группа компаний"
|
||
rules={[{ required: true, message: "Укажите ГК" }]}
|
||
>
|
||
<Select
|
||
placeholder="Выберите ГК"
|
||
loading={loading}
|
||
showSearch
|
||
allowClear
|
||
filterOption={(input, option) =>
|
||
(option?.label as string)
|
||
?.toLowerCase()
|
||
.includes(input.toLowerCase())
|
||
}
|
||
options={raw_rows.map((g) => ({ value: g.id, label: g.name }))}
|
||
onChange={handleGroupChange}
|
||
/>
|
||
</Form.Item>
|
||
</Col>
|
||
<Col span={12}>
|
||
<Form.Item
|
||
name="related_group_id"
|
||
label="ГСК"
|
||
rules={[{ required: true, message: "Укажите ГСК" }]}
|
||
>
|
||
<Select
|
||
placeholder={
|
||
company_group_id ? "Выберите ГСК" : "Сначала выберите ГК"
|
||
}
|
||
disabled={!company_group_id}
|
||
loading={loadingSubgroups}
|
||
showSearch
|
||
allowClear
|
||
filterOption={(input, option) =>
|
||
(option?.label as string)
|
||
?.toLowerCase()
|
||
.includes(input.toLowerCase())
|
||
}
|
||
options={subgroups.map((s) => ({ value: s.id, label: s.name }))}
|
||
onChange={handleSubGroupChange}
|
||
/>
|
||
</Form.Item>
|
||
</Col>
|
||
|
||
<Col span={12}>
|
||
<Form.Item
|
||
name="year"
|
||
label="Год"
|
||
rules={[{ required: true, message: "Укажите год" }]}
|
||
>
|
||
<Select placeholder="Выберите год" options={yearOptions} />
|
||
</Form.Item>
|
||
</Col>
|
||
<Col span={12}>
|
||
<Form.Item
|
||
name="quarter"
|
||
label="Квартал"
|
||
rules={[{ required: true, message: "Укажите квартал" }]}
|
||
>
|
||
<Select placeholder="Выберите квартал" options={quarterOptions} />
|
||
</Form.Item>
|
||
</Col>
|
||
|
||
{/* ROW 3 - full width block, only after subgroup is selected */}
|
||
{related_group_id && (
|
||
<Col span={24}>
|
||
<Form.Item
|
||
name="legal_entity_ids"
|
||
label="ЮЛ консолидации"
|
||
rules={[
|
||
{ required: true, message: "Выберите хотя бы одно ЮЛ" },
|
||
]}
|
||
>
|
||
<Checkbox.Group style={{ width: "100%" }}>
|
||
<div
|
||
style={{
|
||
border: "1px solid #d9d9d9",
|
||
borderRadius: 8,
|
||
padding: 12,
|
||
width: "100%",
|
||
}}
|
||
>
|
||
<Input.Search
|
||
placeholder="Поиск ЮЛ..."
|
||
allowClear
|
||
value={legEntSearch}
|
||
onChange={(e) => setLegEntSearch(e.target.value)}
|
||
style={{ marginBottom: 12 }}
|
||
/>
|
||
|
||
{loadingEntities ? (
|
||
<div style={{ textAlign: "center", padding: 24 }}>
|
||
<Spin />
|
||
</div>
|
||
) : filteredLegEnts.length === 0 ? (
|
||
<Empty
|
||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||
description={
|
||
legEntities.length === 0
|
||
? "Нет строк"
|
||
: "Нет совпадений"
|
||
}
|
||
/>
|
||
) : (
|
||
<div
|
||
style={{
|
||
maxHeight: 200,
|
||
overflowY: "auto",
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
width: "100%",
|
||
gap: 8,
|
||
}}
|
||
>
|
||
{filteredLegEnts.map((s) => (
|
||
<Checkbox
|
||
key={s.id}
|
||
value={s.id}
|
||
style={{ width: "100%", display: "flex" }}
|
||
>
|
||
<div
|
||
style={{
|
||
flex: 1,
|
||
justifySelf: "flex-start",
|
||
display: "flex",
|
||
justifyContent: "space-between",
|
||
width: "100%",
|
||
gap: "30px",
|
||
}}
|
||
>
|
||
<span style={{ display: "inline-block" }}>
|
||
{s.name}
|
||
</span>
|
||
<span
|
||
style={{
|
||
display: "inline-block",
|
||
color: "rgb(102, 112, 133)",
|
||
fontSize: 11,
|
||
}}
|
||
>
|
||
{s.inn}
|
||
</span>
|
||
</div>
|
||
</Checkbox>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{!loadingEntities && legEntities.length > 0 && (
|
||
<div style={{ marginTop: 8, display: "flex", gap: 12 }}>
|
||
<span style={{ marginLeft: "auto", color: "#8c8c8c" }}>
|
||
Выбрано: {legEntIds?.length ?? 0} /{" "}
|
||
{legEntities.length}
|
||
</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</Checkbox.Group>
|
||
</Form.Item>
|
||
</Col>
|
||
)}
|
||
</Row>
|
||
</Form>
|
||
</Modal>
|
||
);
|
||
};
|