fix(DocumentReview): виртуализация таблицы ОСВ (#38, AURORA-1665) #41
@ -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 не зависит от размера документа, прокрутка до конца, поиск и фильтр по субсчёту работают, «Итого по документу» совпадает с суммой по всему документу
|
||||
|
||||
@ -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>
|
||||
|
||||
17
src/widgets/DocumentReview/osvTableSize.test.ts
Normal file
17
src/widgets/DocumentReview/osvTableSize.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
13
src/widgets/DocumentReview/osvTableSize.ts
Normal file
13
src/widgets/DocumentReview/osvTableSize.ts
Normal 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));
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user