fix(web): режим пар — прокрутка внутри карточки, один запрос при смене фильтра (I-6, M-8, M-12)

Тело режима прокручивается внутри карточки; ключ и страница — одно состояние (currentPage);
запасная горизонтальная прокрутка таблиц на узком окне.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-27 22:24:39 +04:00
parent 7c523b93d1
commit e4c030d3e9
3 changed files with 35 additions and 10 deletions

View File

@ -8,7 +8,7 @@ import { getConsolidatedOsv } from '../../services/api/osv';
import type { ConsolidatedRowOut, CounterPairOut, OsvFilters } from '../../types/api';
import { CounterpartyMapModal } from './CounterpartyMapModal';
import { group, money } from './OsvTable';
import { clampPage, missingSideEmptyText, pairLabel } from './counterPairs';
import { clampPage, currentPage, missingSideEmptyText, pairLabel } from './counterPairs';
import { mapEntryId } from './counterpartyMapping';
import { OSV_PAGE_SIZE, osvErrorText } from './useConsolidatedOsv';
@ -33,15 +33,14 @@ const PANEL_TITLE: React.CSSProperties = { fontWeight: 700, fontSize: 13, color:
function usePagedRows(versionId: string, filters: OsvFilters | null, reloadKey: number) {
const [rows, setRows] = useState<ConsolidatedRowOut[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const requestRef = useRef(0);
const key = JSON.stringify(filters);
// смена фильтра — снова первая страница
// biome-ignore lint/correctness/useExhaustiveDependencies: key — сериализованный filters
useEffect(() => setPage(1), [key]);
// ключ и страница — одно состояние: смена фильтра даёт первую страницу и один запрос, а не два
const [pageState, setPageState] = useState({ key, page: 1 });
const page = currentPage(pageState, key);
const setPage = useCallback((p: number) => setPageState({ key, page: p }), [key]);
// biome-ignore lint/correctness/useExhaustiveDependencies: key — сериализованный filters; reloadKey — перечитать после сопоставления
useEffect(() => {
@ -99,7 +98,7 @@ export function CounterPairsView({ versionId, pairs, selected, numFormat, onSele
}, [selectedKey]);
const centerFilters: OsvFilters | null = selected ? { legal_entity_id: selected.holder_id, counterparty_id: selected.missing_id } : null;
// таблицу справа не запрашиваем, если у Б нет строк или нет строк без ИНН: показываем текст
// нижнюю таблицу не запрашиваем, если у Б нет строк или нет строк без ИНН: показываем текст
const rightHasRows = !!selected && missingSideEmptyText(selected) === null;
const rightFilters: OsvFilters | null =
selected && rightHasRows ? { legal_entity_id: selected.missing_id, unmapped: true, ...(debouncedQ ? { q: debouncedQ } : {}) } : null;
@ -146,7 +145,7 @@ export function CounterPairsView({ versionId, pairs, selected, numFormat, onSele
const rightEmpty = missingSideEmptyText(selected);
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16, flex: '1 1 auto', minWidth: 0 }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16, flex: '1 1 auto', minWidth: 0, minHeight: 0, overflowY: 'auto' }}>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
{pairs.map((p) => {
const active = p.holder_id === selected.holder_id && p.missing_id === selected.missing_id;
@ -186,6 +185,7 @@ export function CounterPairsView({ versionId, pairs, selected, numFormat, onSele
columns={baseColumns}
dataSource={center.rows}
tableLayout='fixed'
scroll={{ x: 750 }}
pagination={{
current: center.page,
pageSize: OSV_PAGE_SIZE,
@ -219,6 +219,7 @@ export function CounterPairsView({ versionId, pairs, selected, numFormat, onSele
columns={rightColumns}
dataSource={right.rows}
tableLayout='fixed'
scroll={{ x: 850 }}
locale={{ emptyText: debouncedQ ? 'По поиску строк нет' : 'Строк нет' }}
pagination={{
current: right.page,

View File

@ -1,6 +1,15 @@
import { describe, expect, it } from 'vitest';
import type { CounterPairOut } from '../../types/api';
import { clampPage, missingSideEmptyText, pairLabel, pairsViewSearch, readPairsView, selectPair, switchPairsView } from './counterPairs';
import {
clampPage,
currentPage,
missingSideEmptyText,
pairLabel,
pairsViewSearch,
readPairsView,
selectPair,
switchPairsView,
} from './counterPairs';
const pair = (holder: string, missing: string): CounterPairOut => ({
holder_id: holder,
@ -86,3 +95,15 @@ describe('clampPage (T5.6)', () => {
expect(clampPage(4, 0, 500)).toBe(1);
});
});
describe('currentPage (M-8)', () => {
it('ключ тот же — сохранённая страница', () => {
expect(currentPage({ key: 'a', page: 3 }, 'a')).toBe(3);
});
it('ключ сменился — первая страница без отдельного обновления состояния', () => {
expect(currentPage({ key: 'a', page: 3 }, 'b')).toBe(1);
});
it('вернулись к прежнему ключу после смены страницы под новым — страница нового', () => {
expect(currentPage({ key: 'b', page: 2 }, 'b')).toBe(2);
});
});

View File

@ -29,7 +29,7 @@ export const selectPair = (pairs: readonly CounterPairOut[], holder: string | nu
export const pairLabel = (p: CounterPairOut): string => `${p.missing_name} ← ${p.holder_name}`;
/** Текст вместо правой таблицы (строки Б без ИНН); `null` — таблицу показываем. */
/** Текст вместо нижней таблицы (строки Б без ИНН); `null` — таблицу показываем. */
export const missingSideEmptyText = (p: CounterPairOut): string | null => {
if (p.missing_rows_total === 0) return `У ${p.missing_name} в своде нет строк: ОСВ не привязаны или по ним пробелы`;
if (p.missing_unmapped_rows === 0)
@ -40,3 +40,6 @@ export const missingSideEmptyText = (p: CounterPairOut): string | null => {
/** Страница после ответа: не дальше последней существующей (последнюю строку сопоставили), не меньше первой. */
export const clampPage = (page: number, total: number, pageSize: number): number =>
Math.min(page, Math.max(1, Math.ceil(total / pageSize)));
/** Страница, привязанная к ключу фильтра: другой ключ — первая страница сразу, без отдельного обновления состояния (один запрос). */
export const currentPage = (state: { key: string; page: number }, key: string): number => (state.key === key ? state.page : 1);