aurora-1544 добавление таблицы гудвил #220

Merged
aep merged 18 commits from feat/goodWill into main 2026-11-06 18:08:01 +03:00
9 changed files with 161 additions and 49 deletions
Showing only changes of commit a1054f08c9 - Show all commits

View File

@ -1,14 +1,14 @@
import { Select, Space } from 'antd';
import type React from 'react';
interface ConsolidationChessFormatTableFilterI {
interface ConsolidationFormatTableFilterI {
decimals: number;
onDecimalsChange: (val: number) => void;
units: string;
onUnitsChange: (val: string) => void;
}
export const ConsolidationChessFormatTableFilter: React.FC<ConsolidationChessFormatTableFilterI> = ({
export const ConsolidationFormatTableFilter: React.FC<ConsolidationFormatTableFilterI> = ({
decimals,
onDecimalsChange,
units,

View File

@ -32,7 +32,7 @@ function ReportContent() {
osv: <OsvContent card={card} onSummaryChange={setOsvSummary} />,
chess: <ChessContent card={card} />,
goodwill: <GoodwillContent />,
goodwill: <GoodwillContent card={card} />,
};
const ActiveTab = TAB_COMPONENTS[tab as string] ?? null;

View File

@ -1,5 +1,5 @@
import type { GoodwillTableResponse, GoodwillRow, GoodwillRowPayload, GoodwillSettingsPayload } from '../types/goodwill/goodwill';
import { api, apiFetchBlob } from '../api/client';
import type { GoodwillTableResponse, GoodwillRow, GoodwillRowPayload, GoodwillSettingsPayload } from '../../types/goodwill/goodwill';
import { api, apiFetchBlob } from '../../api/client';
export function fetchGoodwillTable(versionId: string): Promise<GoodwillTableResponse> {
return api.get<GoodwillTableResponse>(`/consolidations/versions/${versionId}/goodwill`);

View File

@ -34,10 +34,20 @@ export interface GoodwillTableResponse {
totals: GoodwillTotals;
}
// Для дерева Ant Design (с добавлением группировки)
export type GoodwillTreeRow = GoodwillRow & {
key: string;
children?: GoodwillTreeRow[];
isGroup?: boolean;
isTotal?: boolean;
};
isGroupTotal?:boolean
};
export interface GoodwillRowPayload {
ownership_share: string;
include_in_kfo: boolean;
comment: string | null;
}
export interface GoodwillSettingsPayload {
goodwill_enabled: boolean;
}

View File

@ -16,7 +16,7 @@ import type { VersionCard } from '../../../types/consolidations/version_members'
import { chessMatrixMode } from '../../../utils/chessMatrixMode';
import { saveBlob, xlsxFilename } from '../../../utils/download';
import { useNotificationApi } from '../../../utils/useNotification';
import { ConsolidationChessFormatTableFilter } from '../ConsolidationChessFormatTableFilter/ConsolidationChessFormatTableFilter';
import { ConsolidationFormatTableFilter } from '../../../components/ConsolidationFormatTableFilter/ConsolidationFormatTableFilter';
import { ConsolidationChessResTable } from '../ConsolidationChessResTable/ConsolidationChessResTable';
import { ConsolidationChessTable } from '../ConsolidationChessTable/ConsolidationChessTable';
import { ConsolidationChessTableFilter } from '../ConsolidationChessTableFilter/ConsolidationChessTableFilter';
@ -245,7 +245,7 @@ export const ChessContent = ({ card }: { card: VersionCard | null }) => {
backgroundColor: '#e5e7eb',
}}
/>
<ConsolidationChessFormatTableFilter
<ConsolidationFormatTableFilter
decimals={decimals}
units={units}
onDecimalsChange={setDecimals}
@ -294,7 +294,7 @@ export const ChessContent = ({ card }: { card: VersionCard | null }) => {
gap: 16,
}}>
<div style={{ display: 'flex', alignItems: 'flex-start', flexWrap: 'wrap', gap: 16, flex: '1 1 auto' }}>
<ConsolidationChessFormatTableFilter
<ConsolidationFormatTableFilter
decimals={chessResDecimals}
units={chessResUnits}
onDecimalsChange={setChessResDecimals}

View File

@ -1,16 +1,19 @@
import { EditOutlined, FullscreenExitOutlined, FullscreenOutlined } from '@ant-design/icons';
import { DownloadOutlined, 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 type { GoodwillRow, GoodwillTableResponse, GoodwillTreeRow, GoodwillRowPayload } from '../../../types/goodwill/goodwill';
import { useNotificationApi } from '../../../utils/useNotification';
import { ConsolidationChessFormatTableFilter } from '../../ConsolidationChess/ConsolidationChessFormatTableFilter/ConsolidationChessFormatTableFilter';
import { ConsolidationFormatTableFilter } from '../../../components/ConsolidationFormatTableFilter/ConsolidationFormatTableFilter';
import { ConsolidationGoodwillTable } from '../ConsolidationGoodwillTable/ConsolidationGoodwillTable';
import { getGoodwillColumns } from '../ConsolidationGoodwillTable/config';
import { ColumnSettings, useVisibleColumns } from '../../../components/GenericTable';
import { ColumnDef, ColumnSettings, useVisibleColumns } from '../../../components/GenericTable';
import { ApiError } from '../../../api/client';
import { saveBlob, xlsxFilename } from '../../../utils/download';
import { VersionCard } from '../../../types/consolidations/version_members';
import { exportGoodwillTable, fetchGoodwillTable, saveGoodwillRow } from '../../../services/api/goodwill';
// --- ТЕСТОВЫЕ ДАННЫЕ ---
// #TODO --- ТЕСТОВЫЕ ДАННЫЕ ---
const MOCK_DATA: GoodwillTableResponse = {
version_id: '30d45049-4dd6-4479-9cf7-33f4e71301ef',
goodwill_enabled: true,
@ -95,9 +98,8 @@ const MOCK_DATA: GoodwillTableResponse = {
goodwill_in_kfo_amount: '11817.85',
},
};
// ------------------------
export const GoodwillContent = () => {
export const GoodwillContent = ({ card }: { card: VersionCard | null }) => {
const { version_id } = useParams<{ version_id: string }>();
const notification = useNotificationApi();
@ -113,9 +115,11 @@ export const GoodwillContent = () => {
const goodwillTableRef = useRef<HTMLDivElement>(null);
const [isGoodwillFullscreen, setIsGoodwillFullscreen] = useState(false);
const [goodwillFilter, setGoodwillFilter] = useState('allValue');
const [exportLoading, setExportLoading] = useState(false);
const versionInfo = card ? { groupName: card.company_group.name, year: card.year, quarter: card.quarter } : null;
const handleRowEdit = useCallback(
(rowKey: string, field: keyof GoodwillRow, value: any) => {
(rowKey: string, field: keyof GoodwillRow, value: boolean | string) => {
const isParent = rowKey.startsWith('head-') && !rowKey.includes('-sub-');
if (isParent && field === 'include_in_kfo') {
@ -126,7 +130,7 @@ export const GoodwillContent = () => {
const next = { ...prev };
children.forEach((child) => {
const childKey = `${headId}-${child.subsidiary_legal_entity.id}`;
next[childKey] = { ...next[childKey], include_in_kfo: value };
next[childKey] = { ...next[childKey], include_in_kfo: value as boolean };
});
return next;
});
@ -140,12 +144,13 @@ export const GoodwillContent = () => {
},
[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 };
return { rowKey: 'key' as const, columns: columns as ColumnDef<GoodwillTreeRow>[] };
}, [columns]);
const { visibleKeys, setVisibleKeys } = useVisibleColumns(groupsConfig);
@ -164,18 +169,19 @@ export const GoodwillContent = () => {
notification.error({ description: e instanceof Error ? e.message : 'Ошибка' });
}
};
const dropFiltersHandler = () => {
setGoodwillFilter('allValue');
};
const loadData = useCallback(async () => {
const loadData = useCallback(async (vId: string) => {
setLoading(true);
try {
await new Promise((resolve) => setTimeout(resolve, 500));
setData(MOCK_DATA);
const res = await fetchGoodwillTable(vId);
setData(res);
const initialEdited: Record<string, Partial<GoodwillRow>> = {};
MOCK_DATA.rows.forEach((row) => {
res.rows.forEach((row) => {
if (row.include_in_kfo) {
const key = `${row.head_legal_entity.id}-${row.subsidiary_legal_entity.id}`;
initialEdited[key] = {
@ -196,13 +202,35 @@ export const GoodwillContent = () => {
}, [notification]);
const handleSave = async () => {
if (!version_id || !data) return;
setSaving(true);
try {
await new Promise((resolve) => setTimeout(resolve, 800));
notification.success({ description: 'Изменения сохранены (тестовый режим)' });
const promises = Object.keys(editedRows).map((rowKey) => {
const changes = editedRows[rowKey];
const originalRow = data.rows.find(
(r) => `${r.head_legal_entity.id}-${r.subsidiary_legal_entity.id}` === rowKey
);
if (!originalRow) return;
const payload: GoodwillRowPayload = {
ownership_share: String(changes.ownership_share ?? originalRow.ownership_share ?? ''),
include_in_kfo: changes.include_in_kfo ?? originalRow.include_in_kfo,
comment: changes.comment !== undefined ? changes.comment : (originalRow.comment ?? null),
};
return saveGoodwillRow(
version_id,
originalRow.head_legal_entity.id,
originalRow.subsidiary_legal_entity.id,
payload
);
});
await Promise.all(promises);
notification.success({ description: 'Изменения сохранены' });
setEditedRows({});
setIsEditMode(false);
loadData();
loadData(version_id);
} catch (e) {
if (e instanceof ApiError) {
notification.error({ description: e.message });
@ -211,6 +239,24 @@ export const GoodwillContent = () => {
setSaving(false);
}
};
const download = async () => {
if (!version_id) {
return;
}
setExportLoading(true);
try {
const { blob, filename } = await exportGoodwillTable(version_id);
saveBlob(blob, filename ?? xlsxFilename('Гудвил', versionInfo));
} catch (e) {
if (e instanceof ApiError) {
notification.error({ description: e.message });
}
} finally {
setExportLoading(false);
}
};
const filteredData = useMemo(() => {
if (!data) return null;
if (goodwillFilter === 'allValue') return data;
@ -245,8 +291,10 @@ export const GoodwillContent = () => {
}, [editedRows, isEditMode]);
useEffect(() => {
loadData();
}, [loadData]);
if (version_id) {
loadData(version_id);
}
}, [version_id, loadData]);
return (
<ConfigProvider getPopupContainer={() => (document.fullscreenElement as HTMLElement) || document.body}>
@ -330,7 +378,7 @@ export const GoodwillContent = () => {
backgroundColor: '#e5e7eb',
}}
/>
<ConsolidationChessFormatTableFilter
<ConsolidationFormatTableFilter
decimals={decimals}
units={units}
onDecimalsChange={setDecimals}
@ -346,6 +394,9 @@ export const GoodwillContent = () => {
disabled={isEditMode || !data?.editable}>
Редактировать
</Button>
<Button loading={exportLoading} onClick={() => download()} icon={<DownloadOutlined />}>
Экспорт
</Button>
<Button
onClick={() => toggleFullscreen(goodwillTableRef, setIsGoodwillFullscreen)}
icon={isGoodwillFullscreen ? <FullscreenExitOutlined /> : <FullscreenOutlined />}>
@ -359,4 +410,4 @@ export const GoodwillContent = () => {
</div>
</ConfigProvider>
);
};
};

View File

@ -16,3 +16,8 @@
.goodwillTable .resRow {
background-color: #e6f4ea !important ;
}
.goodwillTable .group-total-row td {
background-color: #ecfdf3 !important;
font-weight: 600;
}

View File

@ -18,7 +18,7 @@ interface ConsolidationGoodwillTableI {
isFullscreen?: boolean;
isEditMode: boolean;
editedRows: Record<string, Partial<GoodwillRow>>;
onRowEdit: (rowId: string, field: keyof GoodwillRow, value: any) => void;
onRowEdit: (rowId: string, field: keyof GoodwillRow, value: boolean | string) => void;
visibleKeys: string[];
}
@ -42,10 +42,10 @@ export const ConsolidationGoodwillTable: React.FC<ConsolidationGoodwillTableI> =
data.rows.forEach((row) => {
const headId = row.head_legal_entity.id;
if (!heads.has(headId)) {
const groupNode: GoodwillTreeRow = {
...row,
...row,
key: `head-${headId}`,
isGroup: true,
head_legal_entity: row.head_legal_entity,
@ -73,12 +73,44 @@ export const ConsolidationGoodwillTable: React.FC<ConsolidationGoodwillTableI> =
});
});
tree.forEach(parent => {
tree.forEach((parent) => {
if (parent.children && parent.children.length > 0) {
const allChecked = parent.children.every(child =>
editedRows[child.key]?.include_in_kfo ?? child.include_in_kfo
);
const allChecked = parent.children.every((child) => editedRows[child.key]?.include_in_kfo ?? child.include_in_kfo);
parent.include_in_kfo = allChecked;
let totalFinInv = 0;
let totalParticipation = 0;
let totalCharter = 0;
let totalGoodwill = 0;
parent.children.forEach((child) => {
totalFinInv += Number(child.financial_investment_amount || 0);

Подсчёт строки «Всего» в группах лучше перевести на sumAmounts из utils/money. В функции уже учитывается, что суммы приходят строчками и позволяет избежать ошибок округления. также не нужно будет использовать +=.

код сведется к:

// вместо четырёх let и forEach:
const children = parent.children;

const groupTotalRow: GoodwillTreeRow = {
	...
	financial_investment_amount: sumAmounts(children.map((c) => c.financial_investment_amount)) ?? '0.00',
	participation_amount: sumAmounts(children.map((c) => c.participation_amount)) ?? '0.00',
	charter_capital_amount: sumAmounts(children.map((c) => c.charter_capital_amount)) ?? '0.00',
	goodwill_amount: sumAmounts(children.map((c) => c.goodwill_amount)) ?? '0.00',

Подсчёт строки «Всего» в группах лучше перевести на `sumAmounts` из `utils/money`. В функции уже учитывается, что суммы приходят строчками и позволяет избежать ошибок округления. также не нужно будет использовать `+=`. код сведется к: ``` // вместо четырёх let и forEach: const children = parent.children; const groupTotalRow: GoodwillTreeRow = { ... financial_investment_amount: sumAmounts(children.map((c) => c.financial_investment_amount)) ?? '0.00', participation_amount: sumAmounts(children.map((c) => c.participation_amount)) ?? '0.00', charter_capital_amount: sumAmounts(children.map((c) => c.charter_capital_amount)) ?? '0.00', goodwill_amount: sumAmounts(children.map((c) => c.goodwill_amount)) ?? '0.00', ```
totalParticipation += Number(child.participation_amount || 0);
totalCharter += Number(child.charter_capital_amount || 0);
totalGoodwill += Number(child.goodwill_amount || 0);
});
const groupTotalRow: GoodwillTreeRow = {
key: `total-${parent.key}`,
head_legal_entity: parent.head_legal_entity,
subsidiary_legal_entity: { id: '', name: 'Всего' },
isGroupTotal: true,
isGroup: false,
financial_investment_amount: String(totalFinInv),
participation_amount: String(totalParticipation),
charter_capital_amount: String(totalCharter),
goodwill_amount: String(totalGoodwill),
ownership_share: null,
additional_capital_amount: '0',
include_in_kfo: false,
comment: null,
updated_at: null,
updated_by: null,
};
parent.children.push(groupTotalRow);
}
});
@ -90,7 +122,7 @@ export const ConsolidationGoodwillTable: React.FC<ConsolidationGoodwillTableI> =
}, [units, decimals, isEditMode, editedRows, onRowEdit]);
const visibleColumns = useMemo(() => {
return columns.filter(col => visibleKeys.includes(col.key as string));
return columns.filter((col) => visibleKeys.includes(col.key as string));
}, [columns, visibleKeys]);
const scrollY = isFullscreen ? 'calc(100vh - 200px)' : 'auto';
@ -98,7 +130,7 @@ export const ConsolidationGoodwillTable: React.FC<ConsolidationGoodwillTableI> =
const renderSummary = () => {
if (!data?.totals) return null;
const ownershipIndex = visibleColumns.findIndex(c => c.key === 'ownership_share');
const ownershipIndex = visibleColumns.findIndex((c) => c.key === 'ownership_share');
return (
<Table.Summary fixed>
@ -106,7 +138,7 @@ export const ConsolidationGoodwillTable: React.FC<ConsolidationGoodwillTableI> =
{visibleColumns.map((col, index) => {
if (index === 0) {
return (
<Table.Summary.Cell key="total-label" index={0} colSpan={ownershipIndex > 0 ? ownershipIndex : 1} align='left'>
<Table.Summary.Cell key='total-label' index={0} colSpan={ownershipIndex > 0 ? ownershipIndex : 1} align='left'>
<div style={{ backgroundColor: '#e6f4ea', width: '100%', height: '100%' }}>
<Text strong>Итого по листу</Text>
</div>
@ -117,7 +149,11 @@ export const ConsolidationGoodwillTable: React.FC<ConsolidationGoodwillTableI> =
if (index < ownershipIndex) return null;
if (col.key === 'ownership_share') {
return <Table.Summary.Cell key={col.key} index={index} align='right'><Text strong>—</Text></Table.Summary.Cell>;
return (
<Table.Summary.Cell key={col.key} index={index} align='right'>
<Text strong>—</Text>
</Table.Summary.Cell>
);
}
if (col.key === 'financial_investment_amount') {
@ -141,7 +177,11 @@ export const ConsolidationGoodwillTable: React.FC<ConsolidationGoodwillTableI> =
}
if (col.key === 'charter_capital_amount') {
return <Table.Summary.Cell key={col.key} index={index} align='right'><Text strong>—</Text></Table.Summary.Cell>;
return (
<Table.Summary.Cell key={col.key} index={index} align='right'>
<Text strong>—</Text>
</Table.Summary.Cell>
);
}
if (col.key === 'goodwill_amount') {
@ -155,7 +195,11 @@ export const ConsolidationGoodwillTable: React.FC<ConsolidationGoodwillTableI> =
}
if (col.key === 'comment') {
return <Table.Summary.Cell key={col.key} index={index} align='right'><Text strong>—</Text></Table.Summary.Cell>;
return (
<Table.Summary.Cell key={col.key} index={index} align='right'>
<Text strong>—</Text>
</Table.Summary.Cell>
);
}
return null;
@ -179,6 +223,7 @@ export const ConsolidationGoodwillTable: React.FC<ConsolidationGoodwillTableI> =
title={() => toolbar}
expandable={{ defaultExpandAllRows: true }}
summary={renderSummary}
rowClassName={(record) => (record.isGroupTotal ? 'group-total-row' : '')}
/>
);
};
};

View File

@ -10,7 +10,7 @@ interface GetColumnsParams {
decimals: number;
isEditMode: boolean;
editedRows: Record<string, Partial<GoodwillRow>>;
onRowEdit: (rowId: string, field: keyof GoodwillRow, value: any) => void;
onRowEdit: (rowId: string, field: keyof GoodwillRow, value: boolean | string) => void;
}
export const getGoodwillColumns = ({
@ -32,6 +32,8 @@ export const getGoodwillColumns = ({
width: 120,
align: 'center',
render: (_, row) => {
if (row.isGroupTotal) return null;
const isChecked = row.isGroup ? row.include_in_kfo : (editedRows[row.key]?.include_in_kfo ?? row.include_in_kfo);
return (
@ -63,7 +65,7 @@ export const getGoodwillColumns = ({
return {
style: {
backgroundColor: isInvalid ? '#fffbe6' : '#f6ffed',
backgroundColor: isInvalid ? '#fffbe6' : '#f6ffed',
},
};
},
@ -80,7 +82,7 @@ export const getGoodwillColumns = ({
min={0}
max={100}
step={1}
style={{ width: '100%' }}
style={{ width: '100%' }}
value={val ? Math.floor(Number(val)) : ''}
onChange={(e) => {
const intValue = e.target.value.replace(/\D/g, '');
@ -131,7 +133,6 @@ export const getGoodwillColumns = ({
const formatted = formatMoney(val);
if (formatted === '—') return <Text type='secondary'>—</Text>;
const isNegative = String(val).trim().startsWith('-');
return (