arm-core/web/src/widgets/NewConsoleModal/NewConsoleModal.tsx

223 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { PlusOutlined } from '@ant-design/icons';
import { Col, Form, Row, Select, message } from 'antd';
import type React from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { ApiError } from '../../api/client';
import { useBeforeUnload } from '../../hooks/useBeforeUnload';
import { fetchAllLegalEntities, fetchRelatedGroup, fetchRelatedGroups } from '../../services/api/directories/company_groups';
import { getCompanyGroupsThunk } from '../../store/slices/companyGroupsSlice/thunks';
import { useAppDispatch, useAppSelector } from '../../store/store';
import type { ConsolidationCreate, LegalEntityBrief, Quarter, RelatedGroupOut } from '../../types/api';
import { MIN_PERIMETER } from '../../utils/consolidations';
import { CompanyMultiSelect } from './CompanyOptionRow';
import { membersFirst, preselectMembers } from './preselect';
import { AppModal } from '../../components/AppModal/AppModal';
type Props = {
open: boolean;
onCancel: () => void;
onOk: (values: ConsolidationCreate) => Promise<void>;
};
export const NewConsolidationModal: React.FC<Props> = ({ open, onCancel, onOk }) => {
const [form] = Form.useForm<ConsolidationCreate>();
const dispatch = useAppDispatch();
const { raw_rows, loading } = useAppSelector((state) => state.company_groups);
const [subgroups, setSubgroups] = useState<RelatedGroupOut[]>([]);
const [loadingSubgroups, setLoadingSubgroups] = useState(false);
const subgroupReqRef = useRef(0);
const [legEntities, setLegEntities] = useState<LegalEntityBrief[]>([]);
const [loadingEntities, setLoadingEntities] = useState(false);
const legEntReqRef = useRef(0);
const relatedReqRef = useRef(0);
const [submitting, setSubmitting] = useState(false);
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: Quarter; label: string }[] = [
{ 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]);
// не меньше двух ЮЛ: иначе сервер ответит 422 PERIMETER_TOO_SMALL
const isSubmitDisabled = !company_group_id || !related_group_id || !year || !semester || legEntIds.length < MIN_PERIMETER;
// начатая форма — несохранённые правки: браузер предупредит при закрытии вкладки
useBeforeUnload(open && !!(company_group_id || related_group_id || year || semester));
const yearOptions = useMemo(() => {
const y = new Date().getFullYear();
return Array.from({ length: 4 }, (_, i) => ({
value: y - i,
label: String(y - i),
}));
}, []);
const clearLegEnts = () => {
relatedReqRef.current++; // сменили ГК: ответ по старой ГСК больше не нужен
setLegEntities([]);
setLoadingEntities(false);
legEntReqRef.current++;
};
const handleGroupChange = async (value?: string) => {
form.setFieldsValue({
related_group_id: undefined,
legal_entity_ids: [],
});
setSubgroups([]);
clearLegEnts();
if (!value) return;
const reqId = ++subgroupReqRef.current;
const leReqId = ++legEntReqRef.current;
setLoadingSubgroups(true);
setLoadingEntities(true);
try {
const [dataSubgroups, dataLegEnts] = await Promise.all([fetchRelatedGroups(value), fetchAllLegalEntities(value)]);
if (reqId === subgroupReqRef.current) setSubgroups(dataSubgroups.filter((g) => g.kind === 'gsk'));
if (leReqId === legEntReqRef.current) setLegEntities(dataLegEnts);
} catch (e: unknown) {
message.error(e instanceof ApiError ? e.message : 'Не удалось загрузить данные ГК');
} finally {
if (reqId === subgroupReqRef.current) setLoadingSubgroups(false);
if (leReqId === legEntReqRef.current) setLoadingEntities(false);
}
};
const handleRelatedGroupChange = async (value?: string) => {
form.setFieldsValue({ legal_entity_ids: [] });
if (!value) return;
const reqId = ++relatedReqRef.current;
try {
const detail = await fetchRelatedGroup(value);
if (reqId === relatedReqRef.current) {
const memberIds = detail.members.map((m) => m.legal_entity_id);
setLegEntities((prev) => membersFirst(prev, memberIds));
form.setFieldsValue({ legal_entity_ids: preselectMembers(memberIds, legEntities) });
}
} catch (e: unknown) {
message.error(e instanceof ApiError ? e.message : 'Не удалось загрузить состав ГСК');
}
};
const resetLocal = () => {
relatedReqRef.current++;
setSubgroups([]);
setLoadingSubgroups(false);
subgroupReqRef.current++;
clearLegEnts();
};
const handleOk = async () => {
let values: ConsolidationCreate;
try {
values = await form.validateFields();
} catch {
return; // ошибки полей antd показывает сам
}
setSubmitting(true);
try {
await onOk(values);
form.resetFields();
resetLocal();
} catch (e: unknown) {
message.error(e instanceof ApiError ? e.message : 'Не удалось создать консолидацию');
} finally {
setSubmitting(false);
}
};
const handleCancel = () => {
form.resetFields();
resetLocal();
onCancel();
};
return (
<AppModal
title='Новая консолидация'
open={open}
onCancel={handleCancel}
onOk={handleOk}
okButtonProps={{ disabled: isSubmitDisabled, icon: <PlusOutlined /> }}
okText='Создать'
cancelText='Отмена'
confirmLoading={submitting}
destroyOnHidden
width={640}>
<Form form={form} requiredMark={false} layout='vertical' preserve={false} initialValues={{ legal_entity_ids: [] }}>
<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.filter((g) => !g.is_archived).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={handleRelatedGroupChange}
/>
</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={[
{
validator: (_, v: string[] | undefined) =>
(v?.length ?? 0) >= MIN_PERIMETER ? Promise.resolve() : Promise.reject(new Error('Выберите не меньше двух ЮЛ')),
},
]}>
<CompanyMultiSelect loading={loadingEntities} companies={legEntities} />
</Form.Item>
</Col>
)}
</Row>
</Form>
</AppModal>
);
};