363 lines
12 KiB
TypeScript
363 lines
12 KiB
TypeScript
import { EditOutlined, FullscreenExitOutlined, FullscreenOutlined } from '@ant-design/icons';
|
||
import { Alert, Button, ConfigProvider, Select, Space, Tooltip } from 'antd';
|
||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||
import { useParams } from 'react-router-dom';
|
||
import type { GoodwillRow, GoodwillTableResponse } from '../../../types/goodwill/goodwill';
|
||
import { useNotificationApi } from '../../../utils/useNotification';
|
||
import { ConsolidationChessFormatTableFilter } from '../../ConsolidationChess/ConsolidationChessFormatTableFilter/ConsolidationChessFormatTableFilter';
|
||
import { ConsolidationGoodwillTable } from '../ConsolidationGoodwillTable/ConsolidationGoodwillTable';
|
||
import { getGoodwillColumns } from '../ConsolidationGoodwillTable/config';
|
||
import { ColumnSettings, useVisibleColumns } from '../../../components/GenericTable';
|
||
import { ApiError } from '../../../api/client';
|
||
|
||
// --- ТЕСТОВЫЕ ДАННЫЕ ---
|
||
const MOCK_DATA: GoodwillTableResponse = {
|
||
version_id: '30d45049-4dd6-4479-9cf7-33f4e71301ef',
|
||
goodwill_enabled: true,
|
||
editable: true,
|
||
read_only_reason: null,
|
||
rows: [
|
||
{
|
||
head_legal_entity: { id: 'head-1', name: 'Агрохолдинг "Просторы" АО' },
|
||
subsidiary_legal_entity: { id: 'sub-1-1', name: 'ООО "Дон Агро"' },
|
||
ownership_share: '100.0000',
|
||
financial_investment_amount: '4060.00',
|
||
charter_capital_amount: '371.17',
|
||
additional_capital_amount: '0.00',
|
||
participation_amount: '371.14',
|
||
goodwill_amount: '3688.86',
|
||
include_in_kfo: true,
|
||
comment: 'Основная дочерняя компания',
|
||
updated_by: { id: 'user-1', name: 'Иванов И.И.' },
|
||
updated_at: '2026-10-15T10:00:00Z',
|
||
},
|
||
{
|
||
head_legal_entity: { id: 'head-1', name: 'Агрохолдинг "Просторы" АО' },
|
||
subsidiary_legal_entity: { id: 'sub-1-2', name: 'АО "Дон Агро"' },
|
||
ownership_share: '75.0000',
|
||
financial_investment_amount: '260.01',
|
||
charter_capital_amount: '120.00',
|
||
additional_capital_amount: '0.00',
|
||
participation_amount: '119.99',
|
||
goodwill_amount: '-140.02',
|
||
include_in_kfo: false,
|
||
comment: null,
|
||
updated_by: null,
|
||
updated_at: null,
|
||
},
|
||
{
|
||
head_legal_entity: { id: 'head-1', name: 'Агрохолдинг "Просторы" АО' },
|
||
subsidiary_legal_entity: { id: 'sub-1-3', name: 'ООО "Дон Мучнов"' },
|
||
ownership_share: '100.0000',
|
||
financial_investment_amount: '130.00',
|
||
charter_capital_amount: '0.01',
|
||
additional_capital_amount: '0.00',
|
||
participation_amount: '0.01',
|
||
goodwill_amount: '129.99',
|
||
include_in_kfo: true,
|
||
comment: '',
|
||
updated_by: null,
|
||
updated_at: null,
|
||
},
|
||
{
|
||
head_legal_entity: { id: 'head-1', name: 'Агрохолдинг "Просторы" АО' },
|
||
subsidiary_legal_entity: { id: 'sub-1-4', name: 'ООО "Инвестком"' },
|
||
ownership_share: '100.0000',
|
||
financial_investment_amount: '267.00',
|
||
charter_capital_amount: '0.01',
|
||
additional_capital_amount: '0.00',
|
||
participation_amount: '0.01',
|
||
goodwill_amount: '266.99',
|
||
include_in_kfo: false,
|
||
comment: null,
|
||
updated_by: null,
|
||
updated_at: null,
|
||
},
|
||
{
|
||
head_legal_entity: { id: 'head-2', name: 'Инвестком АО' },
|
||
subsidiary_legal_entity: { id: 'sub-2-1', name: 'ООО "Юг-Инвест"' },
|
||
ownership_share: '50.0000',
|
||
financial_investment_amount: '8500.00',
|
||
charter_capital_amount: '1000.00',
|
||
additional_capital_amount: '500.00',
|
||
participation_amount: '500.00',
|
||
goodwill_amount: '8000.00',
|
||
include_in_kfo: true,
|
||
comment: 'Требует пересмотра',
|
||
updated_by: { id: 'user-2', name: 'Петров П.П.' },
|
||
updated_at: '2026-10-20T14:30:00Z',
|
||
},
|
||
],
|
||
totals: {
|
||
financial_investment_amount: '13217.01',
|
||
participation_amount: '991.15',
|
||
goodwill_amount: '12225.86',
|
||
goodwill_in_kfo_amount: '11817.85',
|
||
},
|
||
};
|
||
// ------------------------
|
||
|
||
export const GoodwillContent = () => {
|
||
const { version_id } = useParams<{ version_id: string }>();
|
||
const notification = useNotificationApi();
|
||
|
||
const [data, setData] = useState<GoodwillTableResponse | null>(null);
|
||
const [loading, setLoading] = useState(false);
|
||
const [saving, setSaving] = useState(false);
|
||
const [decimals, setDecimals] = useState(2);
|
||
const [units, setUnits] = useState('thousand');
|
||
|
||
const [isEditMode, setIsEditMode] = useState(false);
|
||
const [editedRows, setEditedRows] = useState<Record<string, Partial<GoodwillRow>>>({});
|
||
|
||
const goodwillTableRef = useRef<HTMLDivElement>(null);
|
||
const [isGoodwillFullscreen, setIsGoodwillFullscreen] = useState(false);
|
||
const [goodwillFilter, setGoodwillFilter] = useState('allValue');
|
||
|
||
const handleRowEdit = useCallback(
|
||
(rowKey: string, field: keyof GoodwillRow, value: any) => {
|
||
const isParent = rowKey.startsWith('head-') && !rowKey.includes('-sub-');
|
||
|
||
if (isParent && field === 'include_in_kfo') {
|
||
const headId = rowKey.replace('head-', '');
|
||
const children = data?.rows.filter((r) => r.head_legal_entity.id === headId) || [];
|
||
|
||
setEditedRows((prev) => {
|
||
const next = { ...prev };
|
||
children.forEach((child) => {
|
||
const childKey = `${headId}-${child.subsidiary_legal_entity.id}`;
|
||
next[childKey] = { ...next[childKey], include_in_kfo: value };
|
||
});
|
||
return next;
|
||
});
|
||
return;
|
||
}
|
||
|
||
setEditedRows((prev) => ({
|
||
...prev,
|
||
[rowKey]: { ...prev[rowKey], [field]: value },
|
||
}));
|
||
},
|
||
[data?.rows],
|
||
);
|
||
const columns = useMemo(() => {
|
||
return getGoodwillColumns({ units, decimals, isEditMode, editedRows, onRowEdit: handleRowEdit });
|
||
}, [units, decimals, isEditMode, editedRows, handleRowEdit]);
|
||
|
||
const groupsConfig = useMemo(() => {
|
||
return { rowKey: 'key' as any, columns: columns as any };
|
||
}, [columns]);
|
||
|
||
const { visibleKeys, setVisibleKeys } = useVisibleColumns(groupsConfig);
|
||
|
||
const toggleFullscreen = async (ref: React.RefObject<HTMLDivElement>, setFullscreen: (val: boolean) => void) => {
|
||
if (!ref.current) return;
|
||
try {
|
||
if (!document.fullscreenElement) {
|
||
await ref.current.requestFullscreen();
|
||
setFullscreen(true);
|
||
} else {
|
||
await document.exitFullscreen();
|
||
setFullscreen(false);
|
||
}
|
||
} catch (e) {
|
||
notification.error({ description: e instanceof Error ? e.message : 'Ошибка' });
|
||
}
|
||
};
|
||
const dropFiltersHandler = () => {
|
||
setGoodwillFilter('allValue');
|
||
};
|
||
|
||
const loadData = useCallback(async () => {
|
||
setLoading(true);
|
||
try {
|
||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||
setData(MOCK_DATA);
|
||
|
||
const initialEdited: Record<string, Partial<GoodwillRow>> = {};
|
||
MOCK_DATA.rows.forEach((row) => {
|
||
if (row.include_in_kfo) {
|
||
const key = `${row.head_legal_entity.id}-${row.subsidiary_legal_entity.id}`;
|
||
initialEdited[key] = {
|
||
include_in_kfo: true,
|
||
ownership_share: row.ownership_share,
|
||
comment: row.comment,
|
||
};
|
||
}
|
||
});
|
||
setEditedRows(initialEdited);
|
||
} catch (e) {
|
||
if (e instanceof ApiError) {
|
||
notification.error({ description: e.message });
|
||
}
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, [notification]);
|
||
|
||
const handleSave = async () => {
|
||
setSaving(true);
|
||
try {
|
||
await new Promise((resolve) => setTimeout(resolve, 800));
|
||
notification.success({ description: 'Изменения сохранены (тестовый режим)' });
|
||
setEditedRows({});
|
||
setIsEditMode(false);
|
||
loadData();
|
||
} catch (e) {
|
||
if (e instanceof ApiError) {
|
||
notification.error({ description: e.message });
|
||
}
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
};
|
||
const filteredData = useMemo(() => {
|
||
if (!data) return null;
|
||
if (goodwillFilter === 'allValue') return data;
|
||
|
||
const filteredRows = data.rows.filter((row) => {
|
||
const amount = Number(row.goodwill_amount);
|
||
|
||
if (goodwillFilter === 'positive') return amount > 0;
|
||
if (goodwillFilter === 'negative') return amount < 0;
|
||
return true;
|
||
});
|
||
|
||
return {
|
||
...data,
|
||
rows: filteredRows,
|
||
};
|
||
}, [data, goodwillFilter]);
|
||
|
||
const canSaveEditData = useMemo(() => {
|
||
if (!isEditMode) {
|
||
return;
|
||
}
|
||
const hasErrors = Object.values(editedRows).some((elem) => {
|
||
if (!elem.include_in_kfo) return false;
|
||
|
||
const isShareEmpty = !elem.ownership_share || String(elem.ownership_share).trim() === '';
|
||
const isCommentEmpty = !elem.comment || String(elem.comment).trim() === '';
|
||
|
||
return isShareEmpty || isCommentEmpty;
|
||
});
|
||
return !hasErrors;
|
||
}, [editedRows, isEditMode]);
|
||
|
||
useEffect(() => {
|
||
loadData();
|
||
}, [loadData]);
|
||
|
||
return (
|
||
<ConfigProvider getPopupContainer={() => (document.fullscreenElement as HTMLElement) || document.body}>
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16, width: '100%' }}>
|
||
{isEditMode && (
|
||
<Alert
|
||
title={<span style={{ color: '#874d00' }}>Доступны для редактирования столбцы 'Доля владения' и 'Комментарий'.</span>}
|
||
type='warning'
|
||
action={
|
||
<Space>
|
||
<Button
|
||
onClick={() => {
|
||
setIsEditMode(false);
|
||
setEditedRows({});
|
||
}}>
|
||
Отменить изменения
|
||
</Button>
|
||
<Tooltip title={!canSaveEditData ? 'Не все данные заполнены' : undefined}>
|
||
<Button type='primary' disabled={!canSaveEditData} loading={saving} onClick={handleSave}>
|
||
Сохранить
|
||
</Button>
|
||
</Tooltip>
|
||
</Space>
|
||
}
|
||
/>
|
||
)}
|
||
|
||
<div
|
||
ref={goodwillTableRef}
|
||
style={{
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
height: isGoodwillFullscreen ? '100vh' : 'auto',
|
||
padding: isGoodwillFullscreen ? 16 : 0,
|
||
backgroundColor: isGoodwillFullscreen ? '#fff' : 'transparent',
|
||
}}>
|
||
<ConsolidationGoodwillTable
|
||
data={filteredData}
|
||
loading={loading}
|
||
decimals={decimals}
|
||
units={units}
|
||
isFullscreen={isGoodwillFullscreen}
|
||
isEditMode={isEditMode}
|
||
editedRows={editedRows}
|
||
onRowEdit={handleRowEdit}
|
||
visibleKeys={visibleKeys}
|
||
toolbar={
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
width: '100%',
|
||
flexWrap: 'wrap',
|
||
gap: 16,
|
||
}}>
|
||
<div style={{ display: 'flex', alignItems: 'flex-start', flexWrap: 'wrap', gap: 16, flex: '1 1 auto' }}>
|
||
<Space>
|
||
<Space orientation='vertical' size={0}>
|
||
<span style={{ fontSize: 12, color: '#667085' }}>Гудвил</span>
|
||
|
||
<Select
|
||
style={{ minWidth: 200 }}
|
||
value={goodwillFilter}
|
||
options={[
|
||
{ value: 'allValue', label: 'Все значения' },
|
||
{ value: 'positive', label: 'Положительные' },
|
||
{ value: 'negative', label: 'Отрицательные' },
|
||
]}
|
||
onChange={(e) => setGoodwillFilter(e)}
|
||
variant='underlined'
|
||
/>
|
||
</Space>
|
||
{goodwillFilter !== 'allValue' ? <Button onClick={() => dropFiltersHandler()}>Сбросить фильтры</Button> : null}
|
||
</Space>
|
||
|
||
<div
|
||
style={{
|
||
width: 1,
|
||
height: 50,
|
||
backgroundColor: '#e5e7eb',
|
||
}}
|
||
/>
|
||
<ConsolidationChessFormatTableFilter
|
||
decimals={decimals}
|
||
units={units}
|
||
onDecimalsChange={setDecimals}
|
||
onUnitsChange={setUnits}
|
||
/>
|
||
</div>
|
||
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
|
||
<ColumnSettings defs={groupsConfig.columns} visibleKeys={visibleKeys} onChange={setVisibleKeys} />
|
||
<Button
|
||
type={isEditMode ? 'default' : 'primary'}
|
||
icon={<EditOutlined />}
|
||
onClick={() => setIsEditMode(true)}
|
||
disabled={isEditMode || !data?.editable}>
|
||
Редактировать
|
||
</Button>
|
||
<Button
|
||
onClick={() => toggleFullscreen(goodwillTableRef, setIsGoodwillFullscreen)}
|
||
icon={isGoodwillFullscreen ? <FullscreenExitOutlined /> : <FullscreenOutlined />}>
|
||
{isGoodwillFullscreen ? 'Свернуть' : 'На весь экран'}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</ConfigProvider>
|
||
);
|
||
};
|