arm-web/src/widgets/NewConsoleModal/NewConsoleModal.tsx
2026-10-08 01:07:46 +04:00

291 lines
9.9 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 { Checkbox, Col, Empty, Form, Input, Modal, Row, Select, Spin, message } from 'antd';
import type React from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { ApiError } from '../../api/client';
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 { preselectMembers } from './preselect';
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 [legEntSearch, setLegEntSearch] = useState('');
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]);
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 = () => {
relatedReqRef.current++; // сменили ГК: ответ по старой ГСК больше не нужен
setLegEntities([]);
setLoadingEntities(false);
setLegEntSearch('');
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) {
form.setFieldsValue({
legal_entity_ids: preselectMembers(
detail.members.map((m) => m.legal_entity_id),
legEntities,
),
});
}
} catch (e: unknown) {
message.error(e instanceof ApiError ? e.message : 'Не удалось загрузить состав ГСК');
}
};
const resetLocal = () => {
relatedReqRef.current++;
setSubgroups([]);
setLoadingSubgroups(false);
subgroupReqRef.current++;
clearLegEnts();
setLegEntSearch('');
};
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();
};
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='Создать'
cancelText='Отмена'
confirmLoading={submitting}
destroyOnHidden
width={640}>
<Form form={form} 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.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={[{ 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>
) : null}
{!loadingEntities && filteredLegEnts.length === 0 && (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={legEntities.length === 0 ? 'Нет ЮЛ в ГК' : 'Ничего не найдено'}
/>
)}
{!loadingEntities && filteredLegEnts.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>
);
};