182 lines
5.1 KiB
TypeScript
182 lines
5.1 KiB
TypeScript
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 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: 'ОСВ' },
|
||
{ key: 'chess', label: 'Шахматка' },
|
||
{
|
||
key: 'correction',
|
||
label: 'Таблица корректировки',
|
||
disabled: true,
|
||
tooltip: 'Будет в следующих срезах',
|
||
},
|
||
{
|
||
key: 'goodwill',
|
||
label: 'Гудвилл',
|
||
disabled: true,
|
||
tooltip: 'Будет в следующих срезах',
|
||
},
|
||
{
|
||
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<string | null>(null);
|
||
|
||
const activeKey = tab || 'osv';
|
||
|
||
const go = (newKey: string) => {
|
||
const { to, options } = reportTabNavigation(location.pathname, activeKey, newKey);
|
||
navigate(to, options);
|
||
};
|
||
|
||
const handleTabChange = (value: string | number) => {
|
||
const newKey = String(value);
|
||
if (newKey === activeKey) return;
|
||
const pairs = summary?.counter_pairs ?? [];
|
||
if (activeKey === 'osv' && newKey === 'chess' && shouldWarnCounterPairs(versionId, status, pairs, safeSessionStorage())) {
|
||
rememberCounterPairsShown(versionId, pairs, safeSessionStorage());
|
||
setPending(newKey);
|
||
} else {
|
||
go(newKey);
|
||
}
|
||
};
|
||
|
||
const options = TAB_CONFIG.map((tabItem) => {
|
||
const isActive = tabItem.key === activeKey;
|
||
|
||
const label = (
|
||
<span
|
||
key={tabItem.key}
|
||
style={{
|
||
color: isActive ? '#2a8c3a' : undefined,
|
||
fontWeight: isActive ? 600 : 400,
|
||
}}>
|
||
{tabItem.label}
|
||
</span>
|
||
);
|
||
|
||
return {
|
||
value: tabItem.key,
|
||
disabled: tabItem.disabled,
|
||
label: tabItem.tooltip ? <Tooltip title={tabItem.tooltip}>{label}</Tooltip> : label,
|
||
};
|
||
});
|
||
|
||
return (
|
||
<>
|
||
<Segmented
|
||
value={activeKey}
|
||
onChange={handleTabChange}
|
||
style={{ alignSelf: 'flex-start', maxWidth: '100%', overflowX: 'auto' }}
|
||
options={options}
|
||
/>
|
||
<CounterPairsModal
|
||
open={pending !== null}
|
||
pairs={summary?.counter_pairs ?? []}
|
||
unmappedTotal={summary?.unmapped_total ?? 0}
|
||
onShow={() => {
|
||
setPending(null);
|
||
navigate(
|
||
{ pathname: location.pathname, search: pairsViewSearch({ view: 'pairs', holder: null, missing: null }) },
|
||
{ replace: true },
|
||
);
|
||
}}
|
||
onStay={() => setPending(null)}
|
||
onGo={() => {
|
||
const next = pending;
|
||
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 (
|
||
<Modal
|
||
open={open}
|
||
title='Проверьте встречные записи участников'
|
||
centered
|
||
onCancel={onStay}
|
||
footer={[
|
||
<Button key='show' onClick={onShow}>
|
||
Показать пары
|
||
</Button>,
|
||
<Button key='stay' onClick={onStay}>
|
||
Остаться
|
||
</Button>,
|
||
<Button key='go' type='primary' onClick={onGo}>
|
||
Перейти
|
||
</Button>,
|
||
]}>
|
||
<div style={{ display: 'flex', gap: 12, alignItems: 'flex-start' }}>
|
||
<WarningOutlined style={{ color: '#dc6803', fontSize: 20, marginTop: 2 }} />
|
||
<div>
|
||
<div style={{ fontWeight: 600 }}>{headline}</div>
|
||
<div style={{ color: '#667085' }}>{details}</div>
|
||
</div>
|
||
</div>
|
||
</Modal>
|
||
);
|
||
}
|