AURORA-1541: таблица корректировки — подключена к реальному API (#206) #215
@ -281,6 +281,15 @@ describe('resolveLeaveAfterFetch (T7.3)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('reportTabNavigation: вкладка «Таблица корректировки» (#206, T4.13)', () => {
|
||||
it('шахматка → таблица корректировки: меняется последний сегмент, запись истории заменяется', () => {
|
||||
expect(reportTabNavigation('/groups/g/versions/v/chess', 'chess', 'correction')).toEqual({
|
||||
to: '/groups/g/versions/v/correction',
|
||||
options: { replace: true },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('reportTabNavigation: адрес без search (T7.4)', () => {
|
||||
it('?holder=&missing= не переносятся на другую вкладку', () => {
|
||||
expect(reportTabNavigation('/groups/g/versions/v/osv', 'osv', 'chess').to).toBe('/groups/g/versions/v/chess');
|
||||
|
||||
@ -28,6 +28,9 @@ function readOnlyHint(t: CorrectionTableOut): string | null {
|
||||
return READ_ONLY_HINTS[t.read_only_reason];
|
||||
}
|
||||
|
||||
/** Решения по строкам и окно корректировки подключаются в Task 5–6: тогда флаг становится true (одна правка). */
|
||||
const ROW_ACTIONS_READY = false;
|
||||
|
||||
const LOAD_ERROR_KEY = 'corrections-load-error';
|
||||
|
||||
type Props = { versionId: string; card?: VersionCard | null };
|
||||
@ -61,6 +64,9 @@ export function CorrectionContent({ versionId, card = null }: Props) {
|
||||
});
|
||||
}, [error, notification, reload]);
|
||||
|
||||
// ушли с вкладки — уведомление об ошибке закрываем, оно относится к этому экрану
|
||||
useEffect(() => () => notification.destroy(LOAD_ERROR_KEY), [notification]);
|
||||
|
||||
useEffect(() => {
|
||||
const onChange = () => {
|
||||
if (!document.fullscreenElement) setFullscreen(false);
|
||||
@ -134,7 +140,7 @@ export function CorrectionContent({ versionId, card = null }: Props) {
|
||||
rules={table.rules}
|
||||
editable={table.editable}
|
||||
readOnlyHint={readOnlyHint(table)}
|
||||
actionsEnabled={false}
|
||||
actionsEnabled={ROW_ACTIONS_READY && table.editable}
|
||||
actions={actions}
|
||||
filters={filters}
|
||||
onFiltersChange={setFilters}
|
||||
|
||||
@ -57,6 +57,8 @@ const BG_CHANGED = '#fff6e5';
|
||||
/** «394.27» → «394,27%»; строка не переводится в число. */
|
||||
const percentText = (p: string | null): string => (p === null ? '—' : `${p.replace('.', ',')}%`);
|
||||
|
||||
const UNAVAILABLE_HINT = 'Недоступно в этой версии экрана';
|
||||
|
||||
const showLine = (line: string): string => articleCodes(line).join(' / ');
|
||||
|
||||
const sideSuffix = (row: TableRow) => {
|
||||
@ -106,15 +108,26 @@ export function CorrectionTable(props: Props) {
|
||||
else if (key === 'reset') actions.onAllocate({ ...base, action: 'RESET' });
|
||||
};
|
||||
|
||||
const withHint = (node: ReactNode) => (readOnlyHint ? <Tooltip title={readOnlyHint}><span>{node}</span></Tooltip> : node);
|
||||
// почему контрол неактивен: сервер закрыл правки — его причина; иначе действие ещё не подключено в этой версии экрана
|
||||
const inactiveHint = readOnlyHint ?? (actionsEnabled ? null : UNAVAILABLE_HINT);
|
||||
const withHint = (node: ReactNode, hint: string | null = inactiveHint) =>
|
||||
hint ? (
|
||||
<Tooltip title={hint}>
|
||||
<span>{node}</span>
|
||||
</Tooltip>
|
||||
) : (
|
||||
node
|
||||
);
|
||||
|
||||
const lines = (list: CorrectionLineOut[] | undefined, pick: (l: CorrectionLineOut) => string): ReactNode =>
|
||||
list && list.length > 0 ? list.map((l) => <div key={`${l.line_code}-${l.amount}`}>{pick(l)}</div>) : '—';
|
||||
list && list.length > 0 ? list.map((l, i) => <div key={`${i}-${l.line_code}-${l.amount}`}>{pick(l)}</div>) : '—';
|
||||
|
||||
const cell = (key: ColKey, row: TableRow): ReactNode => {
|
||||
if (row.kind === 'aggregate') {
|
||||
const a = row.aggregate;
|
||||
const red = a.exceeds_threshold ? { color: COLOR_OVER } : undefined;
|
||||
// красным — только значение выше порога; прочерк («нет значения») не красим
|
||||
const redIf = (text: string): ReactNode =>
|
||||
a.exceeds_threshold && text !== '—' ? <span style={{ color: COLOR_OVER }}>{text}</span> : text;
|
||||
switch (key) {
|
||||
case 'include':
|
||||
return withHint(
|
||||
@ -127,6 +140,7 @@ export function CorrectionTable(props: Props) {
|
||||
</Tooltip>
|
||||
)}
|
||||
</span>,
|
||||
readOnlyHint,
|
||||
);
|
||||
case 'd_entity':
|
||||
return a.company_1.name;
|
||||
@ -145,9 +159,9 @@ export function CorrectionTable(props: Props) {
|
||||
case 'k_amount':
|
||||
return format(a.side_2.amount);
|
||||
case 'dev_percent':
|
||||
return <span style={red}>{percentText(a.deviation_percent)}</span>;
|
||||
return redIf(percentText(a.deviation_percent));
|
||||
case 'dev_amount':
|
||||
return <span style={red}>{format(a.deviation_amount)}</span>;
|
||||
return redIf(format(a.deviation_amount));
|
||||
case 'c_d_line':
|
||||
return lines(a.correction?.debit, (l) => l.line_code);
|
||||
case 'c_d_amount':
|
||||
@ -254,7 +268,7 @@ export function CorrectionTable(props: Props) {
|
||||
width,
|
||||
ellipsis,
|
||||
render: (_, row) => cell(key, row),
|
||||
onCell: (row) => ({ style: { background: rowBg(row) ?? tint(key), opacity: row.kind === 'entry' && row.entry.excluded ? 0.5 : 1 } }),
|
||||
onCell: (row) => ({ style: { background: rowBg(row) ?? tint(key), opacity: (row.kind === 'entry' || row.kind === 'unallocated') && row.entry.excluded ? 0.5 : 1 } }),
|
||||
});
|
||||
|
||||
const filtersTitle = (label: string, active: boolean, body: ReactNode) => (
|
||||
@ -323,6 +337,7 @@ export function CorrectionTable(props: Props) {
|
||||
type='text'
|
||||
size='small'
|
||||
aria-label={expanded.has(row.key) ? 'Свернуть' : 'Раскрыть'}
|
||||
aria-expanded={expanded.has(row.key)}
|
||||
icon={expanded.has(row.key) ? <DownOutlined /> : <RightOutlined />}
|
||||
onClick={() => onToggle(row.key)}
|
||||
/>
|
||||
|
||||
@ -3,7 +3,7 @@ import { Input, Popover, Select } from 'antd';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
type Props = {
|
||||
label: ReactNode;
|
||||
label: string;
|
||||
/** Фильтр задан — значок выделяется. */
|
||||
active: boolean;
|
||||
children: ReactNode;
|
||||
@ -18,7 +18,7 @@ export function HeaderFilter({ label, active, children }: Props) {
|
||||
{/* стопим всплытие, чтобы клик не запускал выбор заголовка */}
|
||||
<button
|
||||
type='button'
|
||||
aria-label='Фильтр'
|
||||
aria-label={`Фильтр: ${label}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{ cursor: 'pointer', border: 0, background: 'none', padding: 0 }}>
|
||||
{active ? <FilterFilled style={{ color: '#2a8c3a' }} /> : <FilterOutlined style={{ color: '#98a2b3' }} />}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
// Загрузка таблицы корректировок версии. Применяется только ответ последнего запроса: аналитик может быстро щёлкнуть
|
||||
// два флажка подряд, и ответ на первый клик не должен перетереть свежую таблицу.
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { ApiError } from '../../api/client';
|
||||
import { getCorrectionTable } from '../../services/api/corrections';
|
||||
import type { CorrectionAggregateOut, CorrectionTableOut } from '../../types/corrections';
|
||||
@ -29,8 +29,8 @@ export function useCorrectionTable(versionId: string): {
|
||||
const [table, setTable] = useState<CorrectionTableOut | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
// счётчик живёт всё время жизни компонента и не вызывает перерисовок
|
||||
const requests = useRef(latestOnly()).current;
|
||||
// счётчик живёт всё время жизни компонента и не вызывает перерисовок; useState(fn) создаёт его один раз
|
||||
const [requests] = useState(latestOnly);
|
||||
|
||||
const reload = useCallback(() => {
|
||||
const id = requests.next();
|
||||
@ -50,6 +50,9 @@ export function useCorrectionTable(versionId: string): {
|
||||
}, [versionId, requests]);
|
||||
|
||||
useEffect(() => {
|
||||
// другая версия — старая таблица и ошибка не должны мелькать до прихода новой
|
||||
setTable(null);
|
||||
setError(null);
|
||||
reload();
|
||||
// после размонтирования ответ уже не нужен: делаем любой номер устаревшим
|
||||
return () => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user