[AURORA-2006] - Гудвилл (вкладка): мелкие дефекты после ревью #244
@ -1,8 +1,5 @@
|
||||
/** DTO таблицы гудвилла — по `api/src/schemas/goodwill.py` и `../docs/dev/api-contract-goodwill.md`. Деньги и доля — строки. */
|
||||
export interface Ref {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
import type { ReadOnlyReason, Ref } from '../corrections';
|
||||
|
||||
export interface GoodwillWarning {
|
||||
code: 'CAPITAL_SOURCE_MISSING' | 'CAPITAL_SOURCE_AMBIGUOUS' | 'OWNERSHIP_TOTAL_EXCEEDED' | string;
|
||||
@ -42,13 +39,11 @@ export interface GoodwillGroup {
|
||||
totals: GoodwillTotals;
|
||||
}
|
||||
|
||||
export type GoodwillReadOnlyReason = 'VERSION_NOT_EDITABLE' | 'LOCK_REQUIRED' | 'LOCKED_BY_OTHER';
|
||||
|
||||
export interface GoodwillTableResponse {
|
||||
version_id: string;
|
||||
goodwill_enabled: boolean;
|
||||
editable: boolean;
|
||||
read_only_reason: GoodwillReadOnlyReason | null;
|
||||
read_only_reason: ReadOnlyReason | null;
|
||||
rows: GoodwillRow[];
|
||||
groups: GoodwillGroup[];
|
||||
totals: GoodwillTotals;
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
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 { useParams } from 'react-router-dom';
|
||||
import { ApiError } from '../../../api/client';
|
||||
import { ConsolidationFormatTableFilter } from '../../../components/ConsolidationFormatTableFilter/ConsolidationFormatTableFilter';
|
||||
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 type { VersionCard } from '../../../types/consolidations/version_members';
|
||||
import type { GoodwillEdit, GoodwillSign, GoodwillTableResponse } from '../../../types/goodwill/goodwill';
|
||||
import { saveBlob, xlsxFilename } from '../../../utils/download';
|
||||
import { changedRows, effectiveRow, matchesSign, parseShare, rowKey } from '../../../utils/goodwill';
|
||||
import { useNotificationApi } from '../../../utils/useNotification';
|
||||
import { readOnlyHint } from '../../CorrectionTable/readOnly';
|
||||
import { errorOutcome, readOnlyHint } from '../../CorrectionTable/readOnly';
|
||||
import { ConsolidationGoodwillTable } from '../ConsolidationGoodwillTable/ConsolidationGoodwillTable';
|
||||
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 [loading, setLoading] = useState(false);
|
||||
const [loadError, setLoadError] = useState<string | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [settingsSaving, setSettingsSaving] = useState(false);
|
||||
const [decimals, setDecimals] = useState(2);
|
||||
@ -60,10 +61,14 @@ export const GoodwillContent = ({ card }: { card: VersionCard | null }) => {
|
||||
const loadData = useCallback(
|
||||
async (vId: string) => {
|
||||
setLoading(true);
|
||||
setLoadError(null);
|
||||
try {
|
||||
setData(await fetchGoodwillTable(vId));
|
||||
} 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 {
|
||||
setLoading(false);
|
||||
}
|
||||
@ -158,6 +163,16 @@ export const GoodwillContent = ({ card }: { card: VersionCard | null }) => {
|
||||
setIsEditMode(false);
|
||||
};
|
||||
|
||||
// сервер закрыл правки (перечитали после 409, версию забрали или закрыли) — выходим из редактирования:
|
||||
// иначе экран остаётся редактируемым и каждое «Сохранить» снова получает 409
|
||||
useEffect(() => {
|
||||
if (data && !data.editable) {
|
||||
setEdits({});
|
||||
setServerIssues({});
|
||||
setIsEditMode(false);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!version_id || !data) return;
|
||||
if (pending.length === 0) {
|
||||
@ -170,13 +185,21 @@ export const GoodwillContent = ({ card }: { card: VersionCard | null }) => {
|
||||
closeEditing();
|
||||
notification.success({ description: 'Изменения сохранены' });
|
||||
} 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);
|
||||
if (detail) {
|
||||
const key = `${detail.head_legal_entity_id}:${detail.subsidiary_legal_entity_id}`;
|
||||
const field = ERROR_FIELD[detail.code ?? ''] ?? 'ownership_share';
|
||||
setServerIssues((prev) => ({ ...prev, [key]: { ...prev[key], [field]: detail.message } }));
|
||||
}
|
||||
if (e instanceof ApiError) notification.error({ description: e.message });
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@ -188,7 +211,9 @@ export const GoodwillContent = ({ card }: { card: VersionCard | null }) => {
|
||||
try {
|
||||
setData(await saveGoodwillSettings(version_id, { goodwill_enabled: enabled }));
|
||||
} 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 {
|
||||
setSettingsSaving(false);
|
||||
}
|
||||
@ -229,91 +254,98 @@ export const GoodwillContent = ({ card }: { card: VersionCard | null }) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
ref={tableRef}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: isFullscreen ? '100vh' : 'auto',
|
||||
padding: isFullscreen ? 16 : 0,
|
||||
backgroundColor: isFullscreen ? '#fff' : 'transparent',
|
||||
}}>
|
||||
<ConsolidationGoodwillTable
|
||||
data={data}
|
||||
rows={filteredRows}
|
||||
columns={visibleColumns}
|
||||
loading={loading}
|
||||
decimals={decimals}
|
||||
units={units}
|
||||
isFullscreen={isFullscreen}
|
||||
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={sign} options={SIGN_OPTIONS} onChange={setSign} variant='underlined' />
|
||||
</Space>
|
||||
{sign !== 'all' ? <Button onClick={() => setSign('all')}>Сбросить фильтры</Button> : null}
|
||||
</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}>
|
||||
{loadError && !loading ? (
|
||||
<Flex vertical align='center' gap={8} style={{ padding: 48 }}>
|
||||
<span>{loadError}</span>
|
||||
<Button onClick={() => version_id && loadData(version_id)}>Повторить</Button>
|
||||
</Flex>
|
||||
) : (
|
||||
<div
|
||||
ref={tableRef}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: isFullscreen ? '100vh' : 'auto',
|
||||
padding: isFullscreen ? 16 : 0,
|
||||
backgroundColor: isFullscreen ? '#fff' : 'transparent',
|
||||
}}>
|
||||
<ConsolidationGoodwillTable
|
||||
data={data}
|
||||
rows={filteredRows}
|
||||
columns={visibleColumns}
|
||||
loading={loading}
|
||||
decimals={decimals}
|
||||
units={units}
|
||||
isFullscreen={isFullscreen}
|
||||
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>
|
||||
<Tooltip title={readOnly}>
|
||||
<Switch
|
||||
checked={data?.goodwill_enabled ?? false}
|
||||
disabled={!data?.editable || isEditMode}
|
||||
loading={settingsSaving}
|
||||
onChange={handleSettings}
|
||||
aria-label='Учитывать гудвилл в КФО'
|
||||
/>
|
||||
</Tooltip>
|
||||
<span>Учитывать гудвилл в КФО</span>
|
||||
<Space orientation='vertical' size={0}>
|
||||
<span style={{ fontSize: 12, color: '#667085' }}>Гудвилл</span>
|
||||
<Select style={{ minWidth: 200 }} value={sign} options={SIGN_OPTIONS} onChange={setSign} variant='underlined' />
|
||||
</Space>
|
||||
{sign !== 'all' ? <Button onClick={() => setSign('all')}>Сбросить фильтры</Button> : null}
|
||||
</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}>
|
||||
Редактировать
|
||||
<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>
|
||||
<Tooltip title={readOnly}>
|
||||
<Switch
|
||||
checked={data?.goodwill_enabled ?? false}
|
||||
disabled={!data?.editable || isEditMode}
|
||||
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>
|
||||
</Tooltip>
|
||||
<Button loading={exportLoading} onClick={download} icon={<DownloadOutlined />}>
|
||||
Экспорт
|
||||
</Button>
|
||||
<Button onClick={toggleFullscreen} icon={isFullscreen ? <FullscreenExitOutlined /> : <FullscreenOutlined />}>
|
||||
{isFullscreen ? 'Свернуть' : 'На весь экран'}
|
||||
</Button>
|
||||
<Button onClick={toggleFullscreen} icon={isFullscreen ? <FullscreenExitOutlined /> : <FullscreenOutlined />}>
|
||||
{isFullscreen ? 'Свернуть' : 'На весь экран'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ConfigProvider>
|
||||
);
|
||||
|
||||
@ -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 {
|
||||
border-end-end-radius: 8px !important;
|
||||
border-end-start-radius: 8px !important;
|
||||
}
|
||||
|
||||
.goodwillTable .ant-table-cell-fix {
|
||||
background-color: #f8fafc !important;
|
||||
}
|
||||
.goodwillTable .resRow {
|
||||
background-color: #e6f4ea !important ;
|
||||
}
|
||||
|
||||
@ -57,6 +57,10 @@ export const ConsolidationGoodwillTable: React.FC<ConsolidationGoodwillTableI> =
|
||||
isFullscreen = false,
|
||||
}) => {
|
||||
const treeData = useMemo(() => buildTree(data, rows), [data, rows]);
|
||||
const expandIconColumnIndex = Math.max(
|
||||
0,
|
||||
columns.findIndex((col) => col.key === 'name'),
|
||||
);
|
||||
|
||||
return (
|
||||
<Table<GoodwillTreeRow>
|
||||
@ -72,7 +76,7 @@ export const ConsolidationGoodwillTable: React.FC<ConsolidationGoodwillTableI> =
|
||||
bordered
|
||||
title={() => toolbar}
|
||||
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} />}
|
||||
rowClassName={rowClassName}
|
||||
/>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user