fix(web): режим пар — прокрутка внутри карточки, один запрос при смене фильтра (I-6, M-8, M-12)
Тело режима прокручивается внутри карточки; ключ и страница — одно состояние (currentPage); запасная горизонтальная прокрутка таблиц на узком окне. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
7c523b93d1
commit
e4c030d3e9
@ -8,7 +8,7 @@ import { getConsolidatedOsv } from '../../services/api/osv';
|
|||||||
import type { ConsolidatedRowOut, CounterPairOut, OsvFilters } from '../../types/api';
|
import type { ConsolidatedRowOut, CounterPairOut, OsvFilters } from '../../types/api';
|
||||||
import { CounterpartyMapModal } from './CounterpartyMapModal';
|
import { CounterpartyMapModal } from './CounterpartyMapModal';
|
||||||
import { group, money } from './OsvTable';
|
import { group, money } from './OsvTable';
|
||||||
import { clampPage, missingSideEmptyText, pairLabel } from './counterPairs';
|
import { clampPage, currentPage, missingSideEmptyText, pairLabel } from './counterPairs';
|
||||||
import { mapEntryId } from './counterpartyMapping';
|
import { mapEntryId } from './counterpartyMapping';
|
||||||
import { OSV_PAGE_SIZE, osvErrorText } from './useConsolidatedOsv';
|
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) {
|
function usePagedRows(versionId: string, filters: OsvFilters | null, reloadKey: number) {
|
||||||
const [rows, setRows] = useState<ConsolidatedRowOut[]>([]);
|
const [rows, setRows] = useState<ConsolidatedRowOut[]>([]);
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
const [page, setPage] = useState(1);
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const requestRef = useRef(0);
|
const requestRef = useRef(0);
|
||||||
const key = JSON.stringify(filters);
|
const key = JSON.stringify(filters);
|
||||||
|
// ключ и страница — одно состояние: смена фильтра даёт первую страницу и один запрос, а не два
|
||||||
// смена фильтра — снова первая страница
|
const [pageState, setPageState] = useState({ key, page: 1 });
|
||||||
// biome-ignore lint/correctness/useExhaustiveDependencies: key — сериализованный filters
|
const page = currentPage(pageState, key);
|
||||||
useEffect(() => setPage(1), [key]);
|
const setPage = useCallback((p: number) => setPageState({ key, page: p }), [key]);
|
||||||
|
|
||||||
// biome-ignore lint/correctness/useExhaustiveDependencies: key — сериализованный filters; reloadKey — перечитать после сопоставления
|
// biome-ignore lint/correctness/useExhaustiveDependencies: key — сериализованный filters; reloadKey — перечитать после сопоставления
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -99,7 +98,7 @@ export function CounterPairsView({ versionId, pairs, selected, numFormat, onSele
|
|||||||
}, [selectedKey]);
|
}, [selectedKey]);
|
||||||
|
|
||||||
const centerFilters: OsvFilters | null = selected ? { legal_entity_id: selected.holder_id, counterparty_id: selected.missing_id } : null;
|
const centerFilters: OsvFilters | null = selected ? { legal_entity_id: selected.holder_id, counterparty_id: selected.missing_id } : null;
|
||||||
// таблицу справа не запрашиваем, если у Б нет строк или нет строк без ИНН: показываем текст
|
// нижнюю таблицу не запрашиваем, если у Б нет строк или нет строк без ИНН: показываем текст
|
||||||
const rightHasRows = !!selected && missingSideEmptyText(selected) === null;
|
const rightHasRows = !!selected && missingSideEmptyText(selected) === null;
|
||||||
const rightFilters: OsvFilters | null =
|
const rightFilters: OsvFilters | null =
|
||||||
selected && rightHasRows ? { legal_entity_id: selected.missing_id, unmapped: true, ...(debouncedQ ? { q: debouncedQ } : {}) } : 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);
|
const rightEmpty = missingSideEmptyText(selected);
|
||||||
|
|
||||||
return (
|
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 }}>
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
||||||
{pairs.map((p) => {
|
{pairs.map((p) => {
|
||||||
const active = p.holder_id === selected.holder_id && p.missing_id === selected.missing_id;
|
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}
|
columns={baseColumns}
|
||||||
dataSource={center.rows}
|
dataSource={center.rows}
|
||||||
tableLayout='fixed'
|
tableLayout='fixed'
|
||||||
|
scroll={{ x: 750 }}
|
||||||
pagination={{
|
pagination={{
|
||||||
current: center.page,
|
current: center.page,
|
||||||
pageSize: OSV_PAGE_SIZE,
|
pageSize: OSV_PAGE_SIZE,
|
||||||
@ -219,6 +219,7 @@ export function CounterPairsView({ versionId, pairs, selected, numFormat, onSele
|
|||||||
columns={rightColumns}
|
columns={rightColumns}
|
||||||
dataSource={right.rows}
|
dataSource={right.rows}
|
||||||
tableLayout='fixed'
|
tableLayout='fixed'
|
||||||
|
scroll={{ x: 850 }}
|
||||||
locale={{ emptyText: debouncedQ ? 'По поиску строк нет' : 'Строк нет' }}
|
locale={{ emptyText: debouncedQ ? 'По поиску строк нет' : 'Строк нет' }}
|
||||||
pagination={{
|
pagination={{
|
||||||
current: right.page,
|
current: right.page,
|
||||||
|
|||||||
@ -1,6 +1,15 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import type { CounterPairOut } from '../../types/api';
|
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 => ({
|
const pair = (holder: string, missing: string): CounterPairOut => ({
|
||||||
holder_id: holder,
|
holder_id: holder,
|
||||||
@ -86,3 +95,15 @@ describe('clampPage (T5.6)', () => {
|
|||||||
expect(clampPage(4, 0, 500)).toBe(1);
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -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}`;
|
export const pairLabel = (p: CounterPairOut): string => `${p.missing_name} ← ${p.holder_name}`;
|
||||||
|
|
||||||
/** Текст вместо правой таблицы (строки Б без ИНН); `null` — таблицу показываем. */
|
/** Текст вместо нижней таблицы (строки Б без ИНН); `null` — таблицу показываем. */
|
||||||
export const missingSideEmptyText = (p: CounterPairOut): string | null => {
|
export const missingSideEmptyText = (p: CounterPairOut): string | null => {
|
||||||
if (p.missing_rows_total === 0) return `У ${p.missing_name} в своде нет строк: ОСВ не привязаны или по ним пробелы`;
|
if (p.missing_rows_total === 0) return `У ${p.missing_name} в своде нет строк: ОСВ не привязаны или по ним пробелы`;
|
||||||
if (p.missing_unmapped_rows === 0)
|
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 =>
|
export const clampPage = (page: number, total: number, pageSize: number): number =>
|
||||||
Math.min(page, Math.max(1, Math.ceil(total / pageSize)));
|
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);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user