fix(DocumentReview): виртуализация таблицы ОСВ (#38, AURORA-1665) #41

Merged
aep merged 2 commits from feat/38-osv-virtual-table into test 2026-10-17 13:46:20 +03:00
4 changed files with 58 additions and 1 deletions
Showing only changes of commit a3e53c8cfc - Show all commits

View File

@ -79,3 +79,15 @@ column_id, amount}`, ячейка достроенной строки (`id = nul
#### Scenario: Подтверждение ОСВ
- **WHEN** аналитик подтверждает ОСВ-документ `NEEDS_CHECK`
- **THEN** документ становится «Загружен», версия, к которой он подходит, переходит в `DATA_CORRECTION`
### Requirement: Виртуализация таблицы ОСВ-документа
Таблица строк ОСВ-документа SHALL быть виртуализирована: в DOM находятся только видимые строки, остальные не
отрисовываются. Строки по-прежнему загружаются целиком, поиск по контрагенту/ИНН и фильтр по субсчёту — клиентские;
серверной пагинации нет. Высота области прокрутки считается числом по высоте окна и пересчитывается при его изменении.
Строка «Итого по документу» закреплена внизу и выровнена по колонкам, двухъярусная шапка и колонка «Субсчёт»
закреплены, горизонтальная прокрутка сохраняется. ОСВ любого размера (десятки тысяч строк) SHALL открываться без
падения вкладки.
#### Scenario: Большая ОСВ
- **WHEN** аналитик открывает ОСВ-документ на ~29 000 строк
- **THEN** документ открывается, число строк в DOM не зависит от размера документа, прокрутка до конца, поиск и фильтр по субсчёту работают, «Итого по документу» совпадает с суммой по всему документу

View File

@ -9,6 +9,7 @@ import { formatAmount } from '../../utils/money';
import { confirmButtonStyle } from './ReviewCard';
import { confirmErrorInfo } from './confirmError';
import { AMOUNT_KEYS, type AmountKey, type OsvRow, flattenEntries } from './osvRows';
import { osvBodyHeight } from './osvTableSize';
const money = (key: AmountKey, title: string) => ({
key,
@ -48,6 +49,17 @@ interface ConfirmProblem {
failures: FailedCheckOut[];
}
/** Высота области прокрутки числом, пересчитывается при изменении размера окна. */
function useBodyHeight(): number {
const [height, setHeight] = useState(() => osvBodyHeight(window.innerHeight));
useEffect(() => {
const onResize = () => setHeight(osvBodyHeight(window.innerHeight));
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []);
return height;
}
export function OsvDocument({ document, onChanged }: { document: DocumentOut; onChanged: () => void }) {
const [entries, setEntries] = useState<EntryOut[]>([]);
const [loading, setLoading] = useState(true);
@ -66,6 +78,7 @@ export function OsvDocument({ document, onChanged }: { document: DocumentOut; on
};
}, [document.id]);
const bodyHeight = useBodyHeight();
const [query, setQuery] = useState('');
const [account, setAccount] = useState<string | undefined>();
const table = useMemo(() => flattenEntries(entries), [entries]);
@ -143,6 +156,8 @@ export function OsvDocument({ document, onChanged }: { document: DocumentOut; on
/>
)}
<Table<OsvRow>
// virtual: в DOM только видимые строки (ОСВ на десятки тысяч строк иначе роняет вкладку); нужны числовые scroll.x и scroll.y
virtual
bordered
size='small'
rowKey='key'
@ -151,7 +166,7 @@ export function OsvDocument({ document, onChanged }: { document: DocumentOut; on
dataSource={rows}
pagination={false}
locale={{ emptyText: entries.length > 0 ? 'По заданным фильтрам строк нет' : 'В документе нет строк' }}
scroll={{ y: 'calc(100vh - 340px)', x: table.hasDepartments ? 1540 : 1380 }}
scroll={{ y: bodyHeight, x: table.hasDepartments ? 1540 : 1380 }}
summary={() =>
entries.length > 0 && (
<Table.Summary fixed>

View File

@ -0,0 +1,17 @@
import { describe, expect, it } from 'vitest';
import { OSV_MIN_BODY_HEIGHT, osvBodyHeight } from './osvTableSize';
describe('osvBodyHeight', () => {
it('вычитает высоту обвязки из высоты окна', () => {
expect(osvBodyHeight(1000)).toBe(660);
});
it('округляет вниз до целого', () => {
expect(osvBodyHeight(1000.7)).toBe(660);
});
it('в низком окне не опускается ниже минимума', () => {
expect(osvBodyHeight(400)).toBe(OSV_MIN_BODY_HEIGHT);
});
it('на нечисловом значении возвращает минимум', () => {
expect(osvBodyHeight(Number.NaN)).toBe(OSV_MIN_BODY_HEIGHT);
});
});

View File

@ -0,0 +1,13 @@
/** Высота тела таблицы не меньше этой, даже в низком окне. */
export const OSV_MIN_BODY_HEIGHT = 240;
/** Сколько высоты окна занимает всё над и под телом таблицы: шапка страницы, кнопки, поиск, шапка и «Итого» таблицы. */
export const OSV_CHROME_HEIGHT = 340;
/**
* Высота области прокрутки таблицы ОСВ числом (виртуальной таблице antd нужен числовой scroll.y, calc() не подходит).
* Повторяет прежнее `calc(100vh - 340px)`, но не ниже минимума; мусорное значение окна даёт минимум.
*/
export function osvBodyHeight(viewportHeight: number): number {
if (!Number.isFinite(viewportHeight)) return OSV_MIN_BODY_HEIGHT;
return Math.max(OSV_MIN_BODY_HEIGHT, Math.floor(viewportHeight - OSV_CHROME_HEIGHT));
}