[AURORA-2006] - Гудвилл (вкладка): мелкие дефекты после ревью

- Ответы 404/409 при «Сохранить» перечитывают таблицу
- Исправлено поведение Стрелки раскрытия блока
- Вывод сообщения об ошибке
- Убран лишние лишний CSS из шахматки
- hooks/useFullScreen.tsx => hooks/useFullScreen.ts, убрано удблирование типов
This commit is contained in:
Veronika Tsygankova 2026-11-12 09:07:20 +03:00
parent ab46262bd9
commit 64284a2917
5 changed files with 126 additions and 105 deletions

View File

@ -1,8 +1,5 @@
/** DTO таблицы гудвилла — по `api/src/schemas/goodwill.py` и `../docs/dev/api-contract-goodwill.md`. Деньги и доля — строки. */ /** DTO таблицы гудвилла — по `api/src/schemas/goodwill.py` и `../docs/dev/api-contract-goodwill.md`. Деньги и доля — строки. */
export interface Ref { import type { ReadOnlyReason, Ref } from '../corrections';
id: string;
name: string;
}
export interface GoodwillWarning { export interface GoodwillWarning {
code: 'CAPITAL_SOURCE_MISSING' | 'CAPITAL_SOURCE_AMBIGUOUS' | 'OWNERSHIP_TOTAL_EXCEEDED' | string; code: 'CAPITAL_SOURCE_MISSING' | 'CAPITAL_SOURCE_AMBIGUOUS' | 'OWNERSHIP_TOTAL_EXCEEDED' | string;
@ -42,13 +39,11 @@ export interface GoodwillGroup {
totals: GoodwillTotals; totals: GoodwillTotals;
} }
export type GoodwillReadOnlyReason = 'VERSION_NOT_EDITABLE' | 'LOCK_REQUIRED' | 'LOCKED_BY_OTHER';
export interface GoodwillTableResponse { export interface GoodwillTableResponse {
version_id: string; version_id: string;
goodwill_enabled: boolean; goodwill_enabled: boolean;
editable: boolean; editable: boolean;
read_only_reason: GoodwillReadOnlyReason | null; read_only_reason: ReadOnlyReason | null;
rows: GoodwillRow[]; rows: GoodwillRow[];
groups: GoodwillGroup[]; groups: GoodwillGroup[];
totals: GoodwillTotals; totals: GoodwillTotals;

View File

@ -1,18 +1,18 @@
import { DownloadOutlined, EditOutlined, FullscreenExitOutlined, FullscreenOutlined } from '@ant-design/icons'; import { DownloadOutlined, EditOutlined, FullscreenExitOutlined, FullscreenOutlined } from '@ant-design/icons';
import { Alert, Button, ConfigProvider, Select, Space, Switch, Tooltip, Typography } from 'antd'; import { Alert, Button, ConfigProvider, Flex, Select, Space, Switch, Tooltip, Typography } from 'antd';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { ApiError } from '../../../api/client'; import { ApiError } from '../../../api/client';
import { ConsolidationFormatTableFilter } from '../../../components/ConsolidationFormatTableFilter/ConsolidationFormatTableFilter'; import { ConsolidationFormatTableFilter } from '../../../components/ConsolidationFormatTableFilter/ConsolidationFormatTableFilter';
import { type ColumnDef, ColumnSettings, useVisibleColumns } from '../../../components/GenericTable'; import { type ColumnDef, ColumnSettings, useVisibleColumns } from '../../../components/GenericTable';
import { useFullscreen } from '../../../hooks/useFullScreen'; import { useFullscreen } from '../../../hooks/useFullscreen';
import { exportGoodwillTable, fetchGoodwillTable, saveGoodwillRows, saveGoodwillSettings } from '../../../services/api/goodwill'; import { exportGoodwillTable, fetchGoodwillTable, saveGoodwillRows, saveGoodwillSettings } from '../../../services/api/goodwill';
import type { VersionCard } from '../../../types/consolidations/version_members'; import type { VersionCard } from '../../../types/consolidations/version_members';
import type { GoodwillEdit, GoodwillSign, GoodwillTableResponse } from '../../../types/goodwill/goodwill'; import type { GoodwillEdit, GoodwillSign, GoodwillTableResponse } from '../../../types/goodwill/goodwill';
import { saveBlob, xlsxFilename } from '../../../utils/download'; import { saveBlob, xlsxFilename } from '../../../utils/download';
import { changedRows, effectiveRow, matchesSign, parseShare, rowKey } from '../../../utils/goodwill'; import { changedRows, effectiveRow, matchesSign, parseShare, rowKey } from '../../../utils/goodwill';
import { useNotificationApi } from '../../../utils/useNotification'; import { useNotificationApi } from '../../../utils/useNotification';
import { readOnlyHint } from '../../CorrectionTable/readOnly'; import { errorOutcome, readOnlyHint } from '../../CorrectionTable/readOnly';
import { ConsolidationGoodwillTable } from '../ConsolidationGoodwillTable/ConsolidationGoodwillTable'; import { ConsolidationGoodwillTable } from '../ConsolidationGoodwillTable/ConsolidationGoodwillTable';
import { type GoodwillTreeRow, type RowIssues, formatMoney, getGoodwillColumns } from '../ConsolidationGoodwillTable/config'; import { type GoodwillTreeRow, type RowIssues, formatMoney, getGoodwillColumns } from '../ConsolidationGoodwillTable/config';
@ -45,6 +45,7 @@ export const GoodwillContent = ({ card }: { card: VersionCard | null }) => {
const [data, setData] = useState<GoodwillTableResponse | null>(null); const [data, setData] = useState<GoodwillTableResponse | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [loadError, setLoadError] = useState<string | null>(null);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [settingsSaving, setSettingsSaving] = useState(false); const [settingsSaving, setSettingsSaving] = useState(false);
const [decimals, setDecimals] = useState(2); const [decimals, setDecimals] = useState(2);
@ -60,10 +61,14 @@ export const GoodwillContent = ({ card }: { card: VersionCard | null }) => {
const loadData = useCallback( const loadData = useCallback(
async (vId: string) => { async (vId: string) => {
setLoading(true); setLoading(true);
setLoadError(null);
try { try {
setData(await fetchGoodwillTable(vId)); setData(await fetchGoodwillTable(vId));
} catch (e) { } catch (e) {
if (e instanceof ApiError) notification.error({ description: e.message }); const message = e instanceof ApiError ? e.message : 'Не удалось загрузить таблицу гудвилла';
setData(null);
setLoadError(message);
notification.error({ description: message });
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -158,6 +163,16 @@ export const GoodwillContent = ({ card }: { card: VersionCard | null }) => {
setIsEditMode(false); setIsEditMode(false);
}; };
// сервер закрыл правки (перечитали после 409, версию забрали или закрыли) — выходим из редактирования:
// иначе экран остаётся редактируемым и каждое «Сохранить» снова получает 409
useEffect(() => {
if (data && !data.editable) {
setEdits({});
setServerIssues({});
setIsEditMode(false);
}
}, [data]);
const handleSave = async () => { const handleSave = async () => {
if (!version_id || !data) return; if (!version_id || !data) return;
if (pending.length === 0) { if (pending.length === 0) {
@ -170,13 +185,21 @@ export const GoodwillContent = ({ card }: { card: VersionCard | null }) => {
closeEditing(); closeEditing();
notification.success({ description: 'Изменения сохранены' }); notification.success({ description: 'Изменения сохранены' });
} catch (e) { } catch (e) {
// 409 (версию закрыли, блокировку забрали) и 404 (пары уже нет) — перечитываем
// таблицу, строку не подсвечиваем; правки в несуществующих парах при пересчёте cбрасываем
const out = errorOutcome(e, 'Не удалось сохранить изменения');
notification.error({ description: out.message });
if (out.reload) {
setServerIssues({});
loadData(version_id);
return;
}
const detail = rowErrorDetail(e); const detail = rowErrorDetail(e);
if (detail) { if (detail) {
const key = `${detail.head_legal_entity_id}:${detail.subsidiary_legal_entity_id}`; const key = `${detail.head_legal_entity_id}:${detail.subsidiary_legal_entity_id}`;
const field = ERROR_FIELD[detail.code ?? ''] ?? 'ownership_share'; const field = ERROR_FIELD[detail.code ?? ''] ?? 'ownership_share';
setServerIssues((prev) => ({ ...prev, [key]: { ...prev[key], [field]: detail.message } })); setServerIssues((prev) => ({ ...prev, [key]: { ...prev[key], [field]: detail.message } }));
} }
if (e instanceof ApiError) notification.error({ description: e.message });
} finally { } finally {
setSaving(false); setSaving(false);
} }
@ -188,7 +211,9 @@ export const GoodwillContent = ({ card }: { card: VersionCard | null }) => {
try { try {
setData(await saveGoodwillSettings(version_id, { goodwill_enabled: enabled })); setData(await saveGoodwillSettings(version_id, { goodwill_enabled: enabled }));
} catch (e) { } catch (e) {
if (e instanceof ApiError) notification.error({ description: e.message }); const out = errorOutcome(e, 'Не удалось изменить настройку гудвилла');
notification.error({ description: out.message });
if (out.reload) loadData(version_id);
} finally { } finally {
setSettingsSaving(false); setSettingsSaving(false);
} }
@ -229,91 +254,98 @@ export const GoodwillContent = ({ card }: { card: VersionCard | null }) => {
/> />
)} )}
<div {loadError && !loading ? (
ref={tableRef} <Flex vertical align='center' gap={8} style={{ padding: 48 }}>
style={{ <span>{loadError}</span>
display: 'flex', <Button onClick={() => version_id && loadData(version_id)}>Повторить</Button>
flexDirection: 'column', </Flex>
height: isFullscreen ? '100vh' : 'auto', ) : (
padding: isFullscreen ? 16 : 0, <div
backgroundColor: isFullscreen ? '#fff' : 'transparent', ref={tableRef}
}}> style={{
<ConsolidationGoodwillTable display: 'flex',
data={data} flexDirection: 'column',
rows={filteredRows} height: isFullscreen ? '100vh' : 'auto',
columns={visibleColumns} padding: isFullscreen ? 16 : 0,
loading={loading} backgroundColor: isFullscreen ? '#fff' : 'transparent',
decimals={decimals} }}>
units={units} <ConsolidationGoodwillTable
isFullscreen={isFullscreen} data={data}
toolbar={ rows={filteredRows}
<div columns={visibleColumns}
style={{ loading={loading}
display: 'flex', decimals={decimals}
justifyContent: 'space-between', units={units}
alignItems: 'center', isFullscreen={isFullscreen}
width: '100%', toolbar={
flexWrap: 'wrap', <div
gap: 16, style={{
}}> display: 'flex',
<div style={{ display: 'flex', alignItems: 'flex-start', flexWrap: 'wrap', gap: 16, flex: '1 1 auto' }}> justifyContent: 'space-between',
<Space> alignItems: 'center',
<Space orientation='vertical' size={0}> width: '100%',
<span style={{ fontSize: 12, color: '#667085' }}>Гудвилл</span> flexWrap: 'wrap',
<Select style={{ minWidth: 200 }} value={sign} options={SIGN_OPTIONS} onChange={setSign} variant='underlined' /> gap: 16,
</Space> }}>
{sign !== 'all' ? <Button onClick={() => setSign('all')}>Сбросить фильтры</Button> : null} <div style={{ display: 'flex', alignItems: 'flex-start', flexWrap: 'wrap', gap: 16, flex: '1 1 auto' }}>
</Space>
<div style={{ width: 1, height: 50, backgroundColor: '#e5e7eb' }} />
<ConsolidationFormatTableFilter
decimals={decimals}
units={units}
onDecimalsChange={setDecimals}
onUnitsChange={setUnits}
/>
<div style={{ width: 1, height: 50, backgroundColor: '#e5e7eb' }} />
<Space orientation='vertical' size={0}>
<Space> <Space>
<Tooltip title={readOnly}> <Space orientation='vertical' size={0}>
<Switch <span style={{ fontSize: 12, color: '#667085' }}>Гудвилл</span>
checked={data?.goodwill_enabled ?? false} <Select style={{ minWidth: 200 }} value={sign} options={SIGN_OPTIONS} onChange={setSign} variant='underlined' />
disabled={!data?.editable || isEditMode} </Space>
loading={settingsSaving} {sign !== 'all' ? <Button onClick={() => setSign('all')}>Сбросить фильтры</Button> : null}
onChange={handleSettings}
aria-label='Учитывать гудвилл в КФО'
/>
</Tooltip>
<span>Учитывать гудвилл в КФО</span>
</Space> </Space>
<Text type='secondary' style={{ fontSize: 12 }}> <div style={{ width: 1, height: 50, backgroundColor: '#e5e7eb' }} />
{data?.goodwill_enabled <ConsolidationFormatTableFilter
? `В КФО: ${formatMoney(data.totals.goodwill_in_kfo_amount, units, decimals)}` decimals={decimals}
: 'Гудвилл не учитывается в КФО'} units={units}
</Text> onDecimalsChange={setDecimals}
</Space> onUnitsChange={setUnits}
</div> />
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}> <div style={{ width: 1, height: 50, backgroundColor: '#e5e7eb' }} />
<ColumnSettings defs={columnsConfig.columns} visibleKeys={visibleKeys} onChange={setVisibleKeys} /> <Space orientation='vertical' size={0}>
<Tooltip title={readOnly}> <Space>
<Button <Tooltip title={readOnly}>
type={isEditMode ? 'default' : 'primary'} <Switch
icon={<EditOutlined />} checked={data?.goodwill_enabled ?? false}
onClick={() => setIsEditMode(true)} disabled={!data?.editable || isEditMode}
disabled={isEditMode || !data?.editable}> loading={settingsSaving}
Редактировать onChange={handleSettings}
aria-label='Учитывать гудвилл в КФО'
/>
</Tooltip>
<span>Учитывать гудвилл в КФО</span>
</Space>
<Text type='secondary' style={{ fontSize: 12 }}>
{data?.goodwill_enabled
? `В КФО: ${formatMoney(data.totals.goodwill_in_kfo_amount, units, decimals)}`
: 'Гудвилл не учитывается в КФО'}
</Text>
</Space>
</div>
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
<ColumnSettings defs={columnsConfig.columns} visibleKeys={visibleKeys} onChange={setVisibleKeys} />
<Tooltip title={readOnly}>
<Button
type={isEditMode ? 'default' : 'primary'}
icon={<EditOutlined />}
onClick={() => setIsEditMode(true)}
disabled={isEditMode || !data?.editable}>
Редактировать
</Button>
</Tooltip>
<Button loading={exportLoading} onClick={download} icon={<DownloadOutlined />}>
Экспорт
</Button> </Button>
</Tooltip> <Button onClick={toggleFullscreen} icon={isFullscreen ? <FullscreenExitOutlined /> : <FullscreenOutlined />}>
<Button loading={exportLoading} onClick={download} icon={<DownloadOutlined />}> {isFullscreen ? 'Свернуть' : 'На весь экран'}
Экспорт </Button>
</Button> </div>
<Button onClick={toggleFullscreen} icon={isFullscreen ? <FullscreenExitOutlined /> : <FullscreenOutlined />}>
{isFullscreen ? 'Свернуть' : 'На весь экран'}
</Button>
</div> </div>
</div> }
} />
/> </div>
</div> )}
</div> </div>
</ConfigProvider> </ConfigProvider>
); );

View File

@ -1,18 +1,8 @@
.goodwillTable .empty-cell {
background-color: #f2f4f7;
}
.goodwillTable .ant-table-tbody > tr:hover > td.empty-cell {
background-color: #f2f4f7 !important;
}
.goodwillTable .ant-table-summary { .goodwillTable .ant-table-summary {
border-end-end-radius: 8px !important; border-end-end-radius: 8px !important;
border-end-start-radius: 8px !important; border-end-start-radius: 8px !important;
} }
.goodwillTable .ant-table-cell-fix {
background-color: #f8fafc !important;
}
.goodwillTable .resRow { .goodwillTable .resRow {
background-color: #e6f4ea !important ; background-color: #e6f4ea !important ;
} }

View File

@ -57,6 +57,10 @@ export const ConsolidationGoodwillTable: React.FC<ConsolidationGoodwillTableI> =
isFullscreen = false, isFullscreen = false,
}) => { }) => {
const treeData = useMemo(() => buildTree(data, rows), [data, rows]); const treeData = useMemo(() => buildTree(data, rows), [data, rows]);
const expandIconColumnIndex = Math.max(
0,
columns.findIndex((col) => col.key === 'name'),
);
return ( return (
<Table<GoodwillTreeRow> <Table<GoodwillTreeRow>
@ -72,7 +76,7 @@ export const ConsolidationGoodwillTable: React.FC<ConsolidationGoodwillTableI> =
bordered bordered
title={() => toolbar} title={() => toolbar}
key={treeData.map((g) => g.key).join('|')} key={treeData.map((g) => g.key).join('|')}
expandable={{ defaultExpandAllRows: true, expandIconColumnIndex: 1 }} expandable={{ defaultExpandAllRows: true, expandIconColumnIndex }}
summary={() => <ConsolidationGoodwillSummary totals={data?.totals ?? null} columns={columns} units={units} decimals={decimals} />} summary={() => <ConsolidationGoodwillSummary totals={data?.totals ?? null} columns={columns} units={units} decimals={decimals} />}
rowClassName={rowClassName} rowClassName={rowClassName}
/> />