import { WarningOutlined } from '@ant-design/icons'; import { Button, Segmented, Tooltip } from 'antd'; import { useEffect, useRef, useState } from 'react'; import { useLocation, useNavigate, useParams } from 'react-router-dom'; import { AppModal } from '../../components/AppModal/AppModal'; import { getOsvSummary } from '../../services/api/osv'; import type { CounterPairOut, OsvSummaryOut } from '../../types/api'; import type { ConsStatus } from '../../types/consolidations/common_types'; import { counterPairsWarning, decideOsvLeave, rememberCounterPairsShown, reportTabNavigation, resolveLeaveAfterFetch, } from '../../utils/consolidations'; import { pairsViewSearch } from './counterPairs'; const TAB_CONFIG = [ { key: 'osv', label: 'ОСВ' }, { key: 'chess', label: 'Шахматка' }, { key: 'correction', label: 'Таблица корректировки', disabled: true, tooltip: 'Будет в следующих срезах', }, { key: 'goodwill', label: 'Гудвилл', }, { key: 'kfo', label: 'КФО', disabled: true, tooltip: 'Будет в следующих срезах', }, { key: 'kfo_common', label: 'КФО Общий', disabled: true, tooltip: 'Будет в следующих срезах', }, { key: 'kfo_confirm', label: 'Подтверждение КФО клиента', disabled: true, tooltip: 'Будет в следующих срезах', }, ]; type Props = { /** Сводка ОСВ: есть пары без встречных записей — переход «ОСВ» → «Шахматка» через модалку. */ 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({ summary, versionId, status }: Props) { const navigate = useNavigate(); const location = useLocation(); const { tab } = useParams<{ tab?: string }>(); // переход, ожидающий решения в модалке; сводка — та, по которой модалка показана (только на время модалки) const [pending, setPending] = useState<{ key: string; summary: OsvSummaryOut | null } | null>(null); const activeKey = tab || 'osv'; const go = (newKey: string) => { const { to, options } = reportTabNavigation(location.pathname, activeKey, newKey); navigate(to, options); }; const [checking, setChecking] = useState(false); // актуальные версия и признак «компонент жив» — ответ запроса, пришедший после смены версии/размонтирования, игнорируется const liveRef = useRef({ versionId, mounted: true }); liveRef.current.versionId = versionId; useEffect(() => { liveRef.current.mounted = true; return () => { liveRef.current.mounted = false; }; }, []); const handleTabChange = async (value: string | number) => { const newKey = String(value); if (newKey === activeKey || checking) return; if (activeKey !== 'osv') { go(newKey); return; } const storage = safeSessionStorage(); let found: OsvSummaryOut | null = summary; let decision: 'warn' | 'go' | 'perform' = decideOsvLeave({ summary: found, versionId, status, storage }); if (decision === 'perform') { setChecking(true); const requestedFor = versionId; let result: OsvSummaryOut | null = null; try { result = await getOsvSummary(requestedFor); } catch { // не удалось проверить — не держим пользователя на вкладке } if (!liveRef.current.mounted) return; setChecking(false); if (liveRef.current.versionId !== requestedFor) return; found = result; decision = resolveLeaveAfterFetch({ summary: result, versionId, status, storage }); } if (decision === 'warn' && found) { rememberCounterPairsShown(versionId, found.counter_pairs, storage); setPending({ key: newKey, summary: found }); } else { go(newKey); } }; const options = TAB_CONFIG.map((tabItem) => { const isActive = tabItem.key === activeKey; const label = ( {tabItem.label} ); return { value: tabItem.key, disabled: tabItem.disabled, label: tabItem.tooltip ? {label} : label, }; }); return ( <> { setPending(null); navigate( { pathname: location.pathname, search: pairsViewSearch({ view: 'pairs', holder: null, missing: null }) }, { replace: true }, ); }} onStay={() => setPending(null)} onGo={() => { const next = pending?.key; setPending(null); if (next) go(next); }} /> ); } /** Модалка перед шахматкой: у части участников нет встречных записей (`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 ( Показать пары , , , ]}>
{headline}
{details}
); }