diff --git a/web/src/layouts/ConsolidationReportPage.tsx b/web/src/layouts/ConsolidationReportPage.tsx index 187a12a..ea92e89 100644 --- a/web/src/layouts/ConsolidationReportPage.tsx +++ b/web/src/layouts/ConsolidationReportPage.tsx @@ -7,6 +7,7 @@ import PageTitle from '../components/PageTitle'; import { TableTag } from '../components/TableTag/TableTag'; import { TAG_PRESETS } from '../components/TableTag/presets'; import { getVersionCard } from '../services/api/consolidations'; +import type { OsvSummaryOut } from '../types/api'; import type { ConsRussianStatus } from '../types/consolidations/common_types'; import type { VersionCard } from '../types/consolidations/version_members'; import { CONS_STATUS_LABEL, canChangeStatus, consStatusDescription, isLockedBy } from '../utils/consolidations'; @@ -20,14 +21,14 @@ import ChangeStatusModal from '../widgets/StatusChangeModal/ChangeStatusModal'; function ReportContent() { const [card, setCard] = useState(null); const [statusModalOpen, setModalOpen] = useState(false); - // unmapped_total сводки ОСВ — для [31-00] при уходе с вкладки «ОСВ» - const [unmappedTotal, setUnmappedTotal] = useState(0); + // сводка ОСВ (пары без встречных записей) — для модалки при уходе с вкладки «ОСВ» + const [osvSummary, setOsvSummary] = useState(null); const currentUser = useCurrentUser(); const { version_id, tab } = useParams<{ tab?: string; version_id: string }>(); const navigation = useNavigate(); const { pathname } = useLocation(); const TAB_COMPONENTS: Record = { - osv: , + osv: , chess: , }; @@ -108,7 +109,7 @@ function ReportContent() { ) } /> - + {ActiveTab} {card && setModalOpen(false)} />} diff --git a/web/src/utils/consolidations.test.ts b/web/src/utils/consolidations.test.ts index 1c74617..2b8bd77 100644 --- a/web/src/utils/consolidations.test.ts +++ b/web/src/utils/consolidations.test.ts @@ -15,7 +15,6 @@ import { isLockedBy, isValidReason, manualTransitions, - needsUnmappedWarning, parseInWorkOnly, rememberCounterPairsShown, reportTabNavigation, @@ -133,14 +132,6 @@ describe('endSentence', () => { }); }); -describe('модалка [31-00]', () => { - it('только при несопоставленных строках', () => { - expect(needsUnmappedWarning(2)).toBe(true); - expect(needsUnmappedWarning(0)).toBe(false); - expect(needsUnmappedWarning(undefined)).toBe(false); - }); -}); - describe('вкладки отчёта (AURORA-1851)', () => { it('смена вкладки заменяет запись истории: «Назад» ведёт со страницы отчёта, а не по вкладкам', () => { expect(reportTabNavigation('/groups/g1/versions/v1/osv', 'osv', 'chess')).toEqual({ diff --git a/web/src/utils/consolidations.ts b/web/src/utils/consolidations.ts index 8a23293..7124db0 100644 --- a/web/src/utils/consolidations.ts +++ b/web/src/utils/consolidations.ts @@ -91,9 +91,6 @@ export const endSentence = (text: string): string => (text.endsWith('.') ? text /** «+ Создать» в модалке «Новая консолидация»: в консолидации не меньше двух ЮЛ (иначе 422 `PERIMETER_TOO_SMALL`). */ export const MIN_PERIMETER = 2; -/** Модалка [31-00] нужна при уходе со вкладки «ОСВ», пока есть несопоставленные строки. */ -export const needsUnmappedWarning = (unmappedTotal: number | null | undefined): boolean => (unmappedTotal ?? 0) > 0; - /** * Переход на другую вкладку отчёта (`/versions/:version_id/:tab`): сегмент вкладки заменяется, запись истории — тоже, * чтобы «Назад» вёл со страницы отчёта, а не по переключениям вкладок (AURORA-1851, как переключатель документа в AURORA-1692). diff --git a/web/src/widgets/ConsolidatedOsv/OsvTabs.tsx b/web/src/widgets/ConsolidatedOsv/OsvTabs.tsx index 5fa4433..9523035 100644 --- a/web/src/widgets/ConsolidatedOsv/OsvTabs.tsx +++ b/web/src/widgets/ConsolidatedOsv/OsvTabs.tsx @@ -2,7 +2,10 @@ import { WarningOutlined } from '@ant-design/icons'; import { Button, Modal, Segmented, Tooltip } from 'antd'; import { useState } from 'react'; import { useLocation, useNavigate, useParams } from 'react-router-dom'; -import { needsUnmappedWarning, reportTabNavigation } from '../../utils/consolidations'; +import type { CounterPairOut, OsvSummaryOut } from '../../types/api'; +import type { ConsStatus } from '../../types/consolidations/common_types'; +import { counterPairsWarning, rememberCounterPairsShown, reportTabNavigation, shouldWarnCounterPairs } from '../../utils/consolidations'; +import { pairsViewSearch } from './counterPairs'; const TAB_CONFIG = [ { key: 'osv', label: 'ОСВ' }, @@ -40,12 +43,23 @@ const TAB_CONFIG = [ ]; type Props = { - /** `unmapped_total` сводки ОСВ: больше нуля — уход с «ОСВ» через модалку [31-00]. */ - unmappedTotal?: number; + /** Сводка ОСВ: есть пары без встречных записей — переход «ОСВ» → «Шахматка» через модалку. */ + summary: OsvSummaryOut | null; + versionId: string; + status: ConsStatus | undefined; +}; + +/** `sessionStorage` может бросать (приватный режим) — тогда `null`. */ +const safeSessionStorage = (): Storage | null => { + try { + return window.sessionStorage; + } catch { + return null; + } }; /** Ряд вкладок шагов консолидации (макет [30]); вкладка — сегмент адреса `/versions/:version_id/:tab`. */ -export function OsvTabs({ unmappedTotal = 0 }: Props) { +export function OsvTabs({ summary, versionId, status }: Props) { const navigate = useNavigate(); const location = useLocation(); const { tab } = useParams<{ tab?: string }>(); @@ -61,7 +75,9 @@ export function OsvTabs({ unmappedTotal = 0 }: Props) { const handleTabChange = (value: string | number) => { const newKey = String(value); if (newKey === activeKey) return; - if (activeKey === 'osv' && needsUnmappedWarning(unmappedTotal)) { + const pairs = summary?.counter_pairs ?? []; + if (activeKey === 'osv' && newKey === 'chess' && shouldWarnCounterPairs(versionId, status, pairs, safeSessionStorage())) { + rememberCounterPairsShown(versionId, pairs, safeSessionStorage()); setPending(newKey); } else { go(newKey); @@ -97,9 +113,17 @@ export function OsvTabs({ unmappedTotal = 0 }: Props) { style={{ alignSelf: 'flex-start', maxWidth: '100%', overflowX: 'auto' }} options={options} /> - { + setPending(null); + navigate( + { pathname: location.pathname, search: pairsViewSearch({ view: 'pairs', holder: null, missing: null }) }, + { replace: true }, + ); + }} onStay={() => setPending(null)} onGo={() => { const next = pending; @@ -111,25 +135,33 @@ export function OsvTabs({ unmappedTotal = 0 }: Props) { ); } -/** [31-00] «Остались неуказанные ИНН»: «Остаться» — без перехода, «Перейти» — переход. */ -export function UnmappedInnModal({ +/** Модалка перед шахматкой: у части участников нет встречных записей (`05-domain-rules.md` §3 п. 9). */ +export function CounterPairsModal({ open, + pairs, unmappedTotal, + onShow, onStay, onGo, }: { open: boolean; + pairs: readonly CounterPairOut[]; unmappedTotal: number; + onShow: () => void; onStay: () => void; onGo: () => void; }) { + const { headline, details } = counterPairsWarning(pairs, unmappedTotal); return ( + Показать пары + , , @@ -138,11 +170,10 @@ export function UnmappedInnModal({ , ]}>
- {/* треугольник с «!», как в макете [31-00] */}
-
В ОСВ остались неуказанные ИНН
-
Не заполнено ИНН: {unmappedTotal}. Точно хотите перейти в другой раздел?
+
{headline}
+
{details}
diff --git a/web/src/widgets/ConsolidatedOsvContent/ConsolidatedOsvContent.tsx b/web/src/widgets/ConsolidatedOsvContent/ConsolidatedOsvContent.tsx index bafdf06..a5d2460 100644 --- a/web/src/widgets/ConsolidatedOsvContent/ConsolidatedOsvContent.tsx +++ b/web/src/widgets/ConsolidatedOsvContent/ConsolidatedOsvContent.tsx @@ -4,7 +4,7 @@ import { useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; import { ApiError } from '../../api/client'; import { exportOsv } from '../../services/api/osv'; -import type { ConsolidatedRowOut } from '../../types/api'; +import type { ConsolidatedRowOut, OsvSummaryOut } from '../../types/api'; import type { VersionCard } from '../../types/consolidations/version_members'; import { exportFilename, saveBlob } from '../../utils/download'; import { CounterpartyMapModal } from '../ConsolidatedOsv/CounterpartyMapModal'; @@ -16,11 +16,11 @@ import { PRECISIONS, UNITS, useNumberDisplay } from '../ConsolidatedOsv/useNumbe type Props = { card: VersionCard | null; - /** Сообщает странице отчёта `unmapped_total` сводки — для модалки [31-00] при уходе с вкладки «ОСВ». */ - onUnmappedChange?: (total: number) => void; + /** Сообщает странице отчёта сводку ОСВ (пары без встречных записей — для модалки при уходе с «ОСВ»); при размонтировании — `null`. */ + onSummaryChange?: (summary: OsvSummaryOut | null) => void; }; -export function OsvContent({ card, onUnmappedChange }: Props) { +export function OsvContent({ card, onSummaryChange }: Props) { const [exporting, setExporting] = useState(false); const { version_id } = useParams<{ version_id: string }>(); @@ -49,10 +49,10 @@ export function OsvContent({ card, onUnmappedChange }: Props) { const onlyUnmapped = osv.filters.unmapped === true; useEffect(() => { - onUnmappedChange?.(unmapped); - }, [unmapped, onUnmappedChange]); + onSummaryChange?.(s); + }, [s, onSummaryChange]); // вне вкладки «ОСВ» предупреждать не о чем - useEffect(() => () => onUnmappedChange?.(0), [onUnmappedChange]); + useEffect(() => () => onSummaryChange?.(null), [onSummaryChange]); return ( <> @@ -104,8 +104,10 @@ export function OsvContent({ card, onUnmappedChange }: Props) { color: '#93370d', fontSize: 13, }}> - Требуется указать соответствия - Для перехода к следующему шагу сопоставьте все внешние организации без ИНН. Осталось: {unmapped}. + Есть организации без ИНН и без сопоставления + + Осталось: {unmapped}. Несопоставленные учитываются как внешние; при необходимости укажите организацию кнопкой «Указать». + {/* макет оставляет кнопку как есть, снимать фильтр — воронкой ИНН или «Сбросить фильтры» (панель — позже) */}