feat(#195): раскладка режима «Встречные записи» — пары сверху, две таблицы на всю ширину; чистая логика пустых состояний
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
97071b03ad
commit
e2e416282f
@ -93,6 +93,7 @@
|
|||||||
| 27.10.2026 | R-2 (design #10): в тексте модалки «Ещё K строк без ИНН…» слово «строк» не склоняется, M — число разных `missing_id` (по спеке) | Спека дословна; склонение K=1 даёт всё равно неграмматичную фразу («учтены») | Низкая |
|
| 27.10.2026 | R-2 (design #10): в тексте модалки «Ещё K строк без ИНН…» слово «строк» не склоняется, M — число разных `missing_id` (по спеке) | Спека дословна; склонение K=1 даёт всё равно неграмматичную фразу («учтены») | Низкая |
|
||||||
| 27.10.2026 | R-3 (design #11): отдельное ревью Task 6 не проводится — её diff в arm-core (одна фраза в CLAUDE.md) и docs-PR #176 проверяются финальным ревью всей ветки | Задача не содержит кода; отдельный ревьюер дублировал бы финальный | Низкая |
|
| 27.10.2026 | R-3 (design #11): отдельное ревью Task 6 не проводится — её diff в arm-core (одна фраза в CLAUDE.md) и docs-PR #176 проверяются финальным ревью всей ветки | Задача не содержит кода; отдельный ревьюер дублировал бы финальный | Низкая |
|
||||||
| 27.10.2026 | R-4 (design #12): подпись под парой в списке — «записей: N · без ИНН у <Б>: M» вместо служебных «держатель»/«Б» | Аналитик не знает ролей «держатель» и «Б»; название организации понятнее | Низкая |
|
| 27.10.2026 | R-4 (design #12): подпись под парой в списке — «записей: N · без ИНН у <Б>: M» вместо служебных «держатель»/«Б» | Аналитик не знает ролей «держатель» и «Б»; название организации понятнее | Низкая |
|
||||||
|
| 27.10.2026 | R-5 (design #13): раскладка режима «Встречные записи» — пары одной строкой «таблеток» с переносом, под ними две таблицы на всю ширину одна под другой (вместо трёх колонок) | В трёх колонках таблицам не хватало ширины: суммы обрезались, видны были только «Обороты Дт» (находка H-1) | Низкая |
|
||||||
|
|
||||||
Отложено решением владельца 27.10.2026: строки Б с ИНН вне справочника — AURORA-1866 (разбор); подсветка кандидата по сумме — AURORA-1867.
|
Отложено решением владельца 27.10.2026: строки Б с ИНН вне справочника — AURORA-1866 (разбор); подсветка кандидата по сумме — AURORA-1867.
|
||||||
| 27.10.2026 | R-6 (design #14): уход с «ОСВ» — закрытая версия (FINISHED_STATUSES) переходит без запроса сводки; при `summary = null` запрашивается сводка, при ошибке запроса переход идёт без модалки и без своего сообщения; на время догрузки переключатель вкладок блокируется; выбранная пара хранится в адресе и в режиме «Свод» (`?holder=&missing=` без `view`) | Модалка нужна только открытой версии; ошибку сводки показывает общий слой; блокировка исключает двойные клики; адрес — единственное хранилище состояния режима | Низкая |
|
| 27.10.2026 | R-6 (design #14): уход с «ОСВ» — закрытая версия (FINISHED_STATUSES) переходит без запроса сводки; при `summary = null` запрашивается сводка, при ошибке запроса переход идёт без модалки и без своего сообщения; на время догрузки переключатель вкладок блокируется; выбранная пара хранится в адресе и в режиме «Свод» (`?holder=&missing=` без `view`) | Модалка нужна только открытой версии; ошибку сводки показывает общий слой; блокировка исключает двойные клики; адрес — единственное хранилище состояния режима | Низкая |
|
||||||
|
|||||||
@ -22,8 +22,8 @@ AURORA-1746 и замечание заказчика (Циликов А.В., 23.
|
|||||||
- **Ядро.** Чистая функция «пары без встречных записей» в `domain/consolidated_osv.py`: для участников периметра А ≠ Б
|
- **Ядро.** Чистая функция «пары без встречных записей» в `domain/consolidated_osv.py`: для участников периметра А ≠ Б
|
||||||
пара (А, Б) — у А есть строка свода с `counterparty_id = Б`, у Б нет строки с `counterparty_id = А`. `GET …/osv/summary`
|
пара (А, Б) — у А есть строка свода с `counterparty_id = Б`, у Б нет строки с `counterparty_id = А`. `GET …/osv/summary`
|
||||||
отдаёт список `counter_pairs[]`; `GET …/osv` и экспорт получают фильтр `counterparty_id`. Ничего не хранится.
|
отдаёт список `counter_pairs[]`; `GET …/osv` и экспорт получают фильтр `counterparty_id`. Ничего не хранится.
|
||||||
- **Фронт, режим «Встречные записи».** Над сводом — переключатель «Свод | Встречные записи (N)». Во втором режиме: слева
|
- **Фронт, режим «Встречные записи».** Над сводом — переключатель «Свод | Встречные записи (N)». Во втором режиме: сверху
|
||||||
список пар, в центре — строки А с контрагентом Б, справа — строки Б без ИНН и без сопоставления с «Указать» (та же
|
список пар, под ним две таблицы на всю ширину: строки А с контрагентом Б и строки Б без ИНН и без сопоставления с «Указать» (та же
|
||||||
модалка сопоставления, что в своде).
|
модалка сопоставления, что в своде).
|
||||||
- **Фронт, модалка перед шахматкой.** Вместо [31-00] «Остались неуказанные ИНН» — «Нет встречных записей: N пар у M
|
- **Фронт, модалка перед шахматкой.** Вместо [31-00] «Остались неуказанные ИНН» — «Нет встречных записей: N пар у M
|
||||||
участников», кнопки «Показать пары» / «Остаться» / «Перейти». Показывается при уходе с «ОСВ», если пары есть, один раз
|
участников», кнопки «Показать пары» / «Остаться» / «Перейти». Показывается при уходе с «ОСВ», если пары есть, один раз
|
||||||
|
|||||||
@ -17,15 +17,16 @@
|
|||||||
`summary.counter_pairs`. Режим — параметр адреса `?view=pairs` (без параметра — «Свод»); переключение режима MUST NOT
|
`summary.counter_pairs`. Режим — параметр адреса `?view=pairs` (без параметра — «Свод»); переключение режима MUST NOT
|
||||||
добавлять запись в историю браузера. Шапка свода (единицы, знаки после запятой) действует в обоих режимах.
|
добавлять запись в историю браузера. Шапка свода (единицы, знаки после запятой) действует в обоих режимах.
|
||||||
|
|
||||||
В режиме «Встречные записи» экран SHALL показывать три колонки:
|
В режиме «Встречные записи» экран SHALL показывать сверху список пар, под ним на всю ширину две таблицы одна под другой:
|
||||||
- **слева** — список пар в порядке ответа, строка пары «Б ← А» (имена `missing_name` и `holder_name`) с числами
|
- **сверху** — список пар одной строкой «таблеток» с переносом, в порядке ответа, строка пары «Б ← А» (имена `missing_name` и `holder_name`) с числами
|
||||||
`holder_rows` и `missing_unmapped_rows`; выбранная пара подсвечена; при входе выбрана первая пара либо пара из адреса
|
`holder_rows` и `missing_unmapped_rows`; выбранная пара подсвечена; при входе выбрана первая пара либо пара из адреса
|
||||||
(`?view=pairs&holder=<id>&missing=<id>`);
|
(`?view=pairs&holder=<id>&missing=<id>`);
|
||||||
- **в центре** — «У <А> есть записи с <Б>»: строки `GET …/osv?legal_entity_id=А&counterparty_id=Б` — счёт, контрагент,
|
- **первая таблица** — «У <А> есть записи с <Б>»: строки `GET …/osv?legal_entity_id=А&counterparty_id=Б` — счёт, контрагент,
|
||||||
обороты Дт/Кт, сальдо на конец Дт/Кт;
|
обороты Дт/Кт, сальдо на конец Дт/Кт;
|
||||||
- **справа** — «У <Б> — строки без ИНН и без сопоставления»: строки `GET …/osv?legal_entity_id=Б&unmapped=true` — счёт,
|
- **вторая таблица** — «У <Б> — строки без ИНН и без сопоставления»: строки `GET …/osv?legal_entity_id=Б&unmapped=true` — счёт,
|
||||||
контрагент, обороты Дт/Кт, сальдо на конец Дт/Кт и «Указать» в каждой строке; над таблицей — поиск по контрагенту
|
контрагент, обороты Дт/Кт, сальдо на конец Дт/Кт и «Указать» в каждой строке; над таблицей — поиск по контрагенту
|
||||||
(`q`, ≤ 500 символов, как фильтр свода); постранично, как таблица свода. Если
|
(`q`, ≤ 500 символов, как фильтр свода); постранично, как таблица свода. Все четыре колонки
|
||||||
|
сумм MUST быть видны без горизонтальной прокрутки при ширине окна 1440 px. Если
|
||||||
`missing_rows_total = 0` — вместо таблицы текст «У <Б> в своде нет строк: ОСВ не привязаны или по ним пробелы»; если
|
`missing_rows_total = 0` — вместо таблицы текст «У <Б> в своде нет строк: ОСВ не привязаны или по ним пробелы»; если
|
||||||
строки у Б есть, а без ИНН и без сопоставления нет (`missing_unmapped_rows = 0`) — текст «Строк без ИНН нет: возможно,
|
строки у Б есть, а без ИНН и без сопоставления нет (`missing_unmapped_rows = 0`) — текст «Строк без ИНН нет: возможно,
|
||||||
ИНН контрагента из файла не найден в справочнике или это пропуск в учёте».
|
ИНН контрагента из файла не найден в справочнике или это пропуск в учёте».
|
||||||
@ -36,8 +37,8 @@
|
|||||||
|
|
||||||
#### Scenario: Пара Русского бисквита
|
#### Scenario: Пара Русского бисквита
|
||||||
- **WHEN** открыт свод версии РБ 2024 Q3 до сопоставления и выбран режим «Встречные записи»
|
- **WHEN** открыт свод версии РБ 2024 Q3 до сопоставления и выбран режим «Встречные записи»
|
||||||
- **THEN** переключатель «Встречные записи (1)»; слева пара «Русский бисквит ← Пекарня Счастливочная»; в центре строки
|
- **THEN** переключатель «Встречные записи (1)»; сверху пара «Русский бисквит ← Пекарня Счастливочная»; в первой таблице строки
|
||||||
Счастливочной с контрагентом «Русский бисквит»; справа строки РБ без ИНН, среди них «Пекарня Счастливочная ООО» с
|
Счастливочной с контрагентом «Русский бисквит»; во второй таблице строки РБ без ИНН, среди них «Пекарня Счастливочная ООО» с
|
||||||
«Указать»
|
«Указать»
|
||||||
|
|
||||||
#### Scenario: Сопоставление из режима закрывает пару
|
#### Scenario: Сопоставление из режима закрывает пару
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
// Режим «Встречные записи» вкладки «ОСВ»: слева пары, в центре строки держателя с Б, справа строки Б без ИНН и сопоставления.
|
// Режим «Встречные записи» вкладки «ОСВ»: сверху пары «таблетками», под ними на всю ширину две таблицы —
|
||||||
|
// строки держателя с Б и строки Б без ИНН и сопоставления.
|
||||||
// Правило: 05-domain-rules.md §3 п. 9.
|
// Правило: 05-domain-rules.md §3 п. 9.
|
||||||
import { Alert, Button, Input, Table } from 'antd';
|
import { Alert, Button, Input, Table } from 'antd';
|
||||||
import type { TableColumnsType } from 'antd';
|
import type { TableColumnsType } from 'antd';
|
||||||
@ -7,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 { pairLabel } from './counterPairs';
|
import { clampPage, 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';
|
||||||
|
|
||||||
@ -57,7 +58,7 @@ function usePagedRows(versionId: string, filters: OsvFilters | null, reloadKey:
|
|||||||
if (requestId !== requestRef.current) return;
|
if (requestId !== requestRef.current) return;
|
||||||
// страница ушла за конец (последнюю строку сопоставили) — сразу на последнюю существующую
|
// страница ушла за конец (последнюю строку сопоставили) — сразу на последнюю существующую
|
||||||
if (r.items.length === 0 && page > 1) {
|
if (r.items.length === 0 && page > 1) {
|
||||||
setPage(Math.max(1, Math.ceil(r.total / OSV_PAGE_SIZE)));
|
setPage(clampPage(page, r.total, OSV_PAGE_SIZE));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setRows(r.items);
|
setRows(r.items);
|
||||||
@ -99,7 +100,7 @@ export function CounterPairsView({ versionId, pairs, selected, numFormat, onSele
|
|||||||
|
|
||||||
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 && selected.missing_rows_total > 0 && selected.missing_unmapped_rows > 0;
|
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;
|
||||||
|
|
||||||
@ -107,11 +108,11 @@ export function CounterPairsView({ versionId, pairs, selected, numFormat, onSele
|
|||||||
const right = usePagedRows(versionId, rightFilters, reloadKey);
|
const right = usePagedRows(versionId, rightFilters, reloadKey);
|
||||||
|
|
||||||
const baseColumns: TableColumnsType<ConsolidatedRowOut> = [
|
const baseColumns: TableColumnsType<ConsolidatedRowOut> = [
|
||||||
{ key: 'account_code', title: 'Счёт', dataIndex: 'account_code', width: 100 },
|
{ key: 'account_code', title: 'Счёт', dataIndex: 'account_code', width: 90 },
|
||||||
{
|
{
|
||||||
key: 'counterparty',
|
key: 'counterparty',
|
||||||
title: 'Контрагент',
|
title: 'Контрагент',
|
||||||
width: 240,
|
ellipsis: true,
|
||||||
render: (_: unknown, r) => r.counterparty_entity_name ?? r.counterparty_name ?? '—',
|
render: (_: unknown, r) => r.counterparty_entity_name ?? r.counterparty_name ?? '—',
|
||||||
},
|
},
|
||||||
group('Обороты за период', [money('turnover_debit', 'Дебет', numFormat), money('turnover_credit', 'Кредит', numFormat)]),
|
group('Обороты за период', [money('turnover_debit', 'Дебет', numFormat), money('turnover_credit', 'Кредит', numFormat)]),
|
||||||
@ -121,7 +122,6 @@ export function CounterPairsView({ versionId, pairs, selected, numFormat, onSele
|
|||||||
...baseColumns,
|
...baseColumns,
|
||||||
{
|
{
|
||||||
key: 'map',
|
key: 'map',
|
||||||
fixed: 'right',
|
|
||||||
title: 'ИНН',
|
title: 'ИНН',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
width: 100,
|
width: 100,
|
||||||
@ -143,16 +143,11 @@ export function CounterPairsView({ versionId, pairs, selected, numFormat, onSele
|
|||||||
return <div style={{ padding: 32, textAlign: 'center', color: MUTED, fontSize: 14 }}>Все участники периметра видят друг друга</div>;
|
return <div style={{ padding: 32, textAlign: 'center', color: MUTED, fontSize: 14 }}>Все участники периметра видят друг друга</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let rightEmpty: string | null = null;
|
const rightEmpty = missingSideEmptyText(selected);
|
||||||
if (selected.missing_rows_total === 0) {
|
|
||||||
rightEmpty = `У ${selected.missing_name} в своде нет строк: ОСВ не привязаны или по ним пробелы`;
|
|
||||||
} else if (selected.missing_unmapped_rows === 0) {
|
|
||||||
rightEmpty = 'Строк без ИНН нет: возможно, ИНН контрагента из файла не найден в справочнике или это пропуск в учёте';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', gap: 12, flex: '1 1 auto', minHeight: 0, alignItems: 'flex-start' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 16, flex: '1 1 auto', minWidth: 0 }}>
|
||||||
<div style={{ ...PANEL, flex: '0 0 280px', maxHeight: 'calc(100vh - 320px)', overflowY: 'auto' }}>
|
<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;
|
||||||
return (
|
return (
|
||||||
@ -162,6 +157,7 @@ export function CounterPairsView({ versionId, pairs, selected, numFormat, onSele
|
|||||||
onClick={() => onSelect(p.holder_id, p.missing_id)}
|
onClick={() => onSelect(p.holder_id, p.missing_id)}
|
||||||
style={{
|
style={{
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
|
maxWidth: 360,
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
padding: '8px 10px',
|
padding: '8px 10px',
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
@ -177,7 +173,7 @@ export function CounterPairsView({ versionId, pairs, selected, numFormat, onSele
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ ...PANEL, flex: '1 1 0' }}>
|
<div style={PANEL}>
|
||||||
<div style={PANEL_TITLE}>
|
<div style={PANEL_TITLE}>
|
||||||
У {selected.holder_name} есть записи с {selected.missing_name}
|
У {selected.holder_name} есть записи с {selected.missing_name}
|
||||||
</div>
|
</div>
|
||||||
@ -189,7 +185,7 @@ export function CounterPairsView({ versionId, pairs, selected, numFormat, onSele
|
|||||||
loading={center.loading}
|
loading={center.loading}
|
||||||
columns={baseColumns}
|
columns={baseColumns}
|
||||||
dataSource={center.rows}
|
dataSource={center.rows}
|
||||||
scroll={{ x: 'max-content' }}
|
tableLayout='fixed'
|
||||||
pagination={{
|
pagination={{
|
||||||
current: center.page,
|
current: center.page,
|
||||||
pageSize: OSV_PAGE_SIZE,
|
pageSize: OSV_PAGE_SIZE,
|
||||||
@ -201,7 +197,7 @@ export function CounterPairsView({ versionId, pairs, selected, numFormat, onSele
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ ...PANEL, flex: '1 1 0' }}>
|
<div style={PANEL}>
|
||||||
<div style={PANEL_TITLE}>У {selected.missing_name} — строки без ИНН и без сопоставления</div>
|
<div style={PANEL_TITLE}>У {selected.missing_name} — строки без ИНН и без сопоставления</div>
|
||||||
{rightEmpty ? (
|
{rightEmpty ? (
|
||||||
<div style={{ padding: 16, color: MUTED, fontSize: 13, background: '#f9fafb', borderRadius: 10 }}>{rightEmpty}</div>
|
<div style={{ padding: 16, color: MUTED, fontSize: 13, background: '#f9fafb', borderRadius: 10 }}>{rightEmpty}</div>
|
||||||
@ -222,7 +218,7 @@ export function CounterPairsView({ versionId, pairs, selected, numFormat, onSele
|
|||||||
loading={right.loading}
|
loading={right.loading}
|
||||||
columns={rightColumns}
|
columns={rightColumns}
|
||||||
dataSource={right.rows}
|
dataSource={right.rows}
|
||||||
scroll={{ x: 'max-content' }}
|
tableLayout='fixed'
|
||||||
locale={{ emptyText: debouncedQ ? 'По поиску строк нет' : 'Строк нет' }}
|
locale={{ emptyText: debouncedQ ? 'По поиску строк нет' : 'Строк нет' }}
|
||||||
pagination={{
|
pagination={{
|
||||||
current: right.page,
|
current: right.page,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
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 { pairLabel, pairsViewSearch, readPairsView, selectPair, switchPairsView } from './counterPairs';
|
import { clampPage, 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,
|
||||||
@ -54,3 +54,35 @@ describe('pairLabel', () => {
|
|||||||
expect(pairLabel(pair('a', 'b'))).toBe('Mb ← Ha');
|
expect(pairLabel(pair('a', 'b'))).toBe('Mb ← Ha');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('missingSideEmptyText (T5.6)', () => {
|
||||||
|
const p = (total: number, unmapped: number): CounterPairOut => ({
|
||||||
|
...pair('a', 'b'),
|
||||||
|
missing_rows_total: total,
|
||||||
|
missing_unmapped_rows: unmapped,
|
||||||
|
});
|
||||||
|
it('у Б нет строк в своде', () => {
|
||||||
|
expect(missingSideEmptyText(p(0, 0))).toBe('У Mb в своде нет строк: ОСВ не привязаны или по ним пробелы');
|
||||||
|
});
|
||||||
|
it('строки есть, без ИНН нет', () => {
|
||||||
|
expect(missingSideEmptyText(p(5, 0))).toBe(
|
||||||
|
'Строк без ИНН нет: возможно, ИНН контрагента из файла не найден в справочнике или это пропуск в учёте',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('есть строки без ИНН — текста нет, показываем таблицу', () => {
|
||||||
|
expect(missingSideEmptyText(p(5, 2))).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clampPage (T5.6)', () => {
|
||||||
|
it('не выходит за последнюю страницу', () => {
|
||||||
|
expect(clampPage(3, 500, 500)).toBe(1);
|
||||||
|
expect(clampPage(5, 1001, 500)).toBe(3);
|
||||||
|
});
|
||||||
|
it('оставляет страницу в пределах', () => {
|
||||||
|
expect(clampPage(2, 1001, 500)).toBe(2);
|
||||||
|
});
|
||||||
|
it('пустой результат — первая страница', () => {
|
||||||
|
expect(clampPage(4, 0, 500)).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -28,3 +28,15 @@ export const selectPair = (pairs: readonly CounterPairOut[], holder: string | nu
|
|||||||
pairs.find((p) => p.holder_id === holder && p.missing_id === missing) ?? pairs[0] ?? null;
|
pairs.find((p) => p.holder_id === holder && p.missing_id === missing) ?? pairs[0] ?? null;
|
||||||
|
|
||||||
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` — таблицу показываем. */
|
||||||
|
export const missingSideEmptyText = (p: CounterPairOut): string | null => {
|
||||||
|
if (p.missing_rows_total === 0) return `У ${p.missing_name} в своде нет строк: ОСВ не привязаны или по ним пробелы`;
|
||||||
|
if (p.missing_unmapped_rows === 0)
|
||||||
|
return 'Строк без ИНН нет: возможно, ИНН контрагента из файла не найден в справочнике или это пропуск в учёте';
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Страница после ответа: не дальше последней существующей (последнюю строку сопоставили), не меньше первой. */
|
||||||
|
export const clampPage = (page: number, total: number, pageSize: number): number =>
|
||||||
|
Math.min(page, Math.max(1, Math.ceil(total / pageSize)));
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user