import { CheckOutlined, SearchOutlined } from '@ant-design/icons'; import { Alert, Button, Input, Select, Table, message } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import { useEffect, useMemo, useState } from 'react'; import { ApiError } from '../../api/client'; import { confirmDocument, getAllEntries } from '../../services/api/documents'; import type { DocumentOut, EntryOut, FailedCheckOut } from '../../types/api'; import { formatAmount } from '../../utils/money'; import { confirmButtonStyle } from './ReviewCard'; import { confirmErrorInfo } from './confirmError'; import { AMOUNT_KEYS, type AmountKey, type OsvRow, flattenEntries } from './osvRows'; const money = (key: AmountKey, title: string) => ({ key, title, dataIndex: key, align: 'right' as const, width: 140, render: (v: string | null) => formatAmount(v, { zeroDash: true }), }); const group = (title: string, children: ColumnsType): ColumnsType[number] => ({ title, align: 'center', children }); function buildColumns(hasDepartments: boolean): ColumnsType { return [ { key: 'account', title: 'Субсчёт', dataIndex: 'account', width: 100, fixed: 'left' }, ...(hasDepartments ? [{ key: 'department', title: 'Подразделение', dataIndex: 'department', width: 160 }] : []), { key: 'analytics', title: 'Контрагент / аналитика', dataIndex: 'analytics', width: 320, render: (v: string | null) => v ?? '—' }, { key: 'inn', title: 'ИНН', dataIndex: 'inn', width: 120 }, group('Сальдо на начало периода', [money('opening_debit', 'Дебет'), money('opening_credit', 'Кредит')]), group('Обороты за период', [money('turnover_debit', 'Дебет'), money('turnover_credit', 'Кредит')]), group('Сальдо на конец периода', [money('closing_debit', 'Дебет'), money('closing_credit', 'Кредит')]), ]; } /** Поиск по контрагенту/ИНН без учёта регистра и фильтр по субсчёту — на клиенте: строки документа уже загружены целиком. */ function filterRows(rows: OsvRow[], query: string, account: string | undefined): OsvRow[] { const q = query.trim().toLowerCase(); return rows.filter( (row) => (!account || row.account === account) && (!q || (row.analytics ?? '').toLowerCase().includes(q) || (row.inn ?? '').includes(q)), ); } interface ConfirmProblem { code: string | undefined; message: string; failures: FailedCheckOut[]; } export function OsvDocument({ document, onChanged }: { document: DocumentOut; onChanged: () => void }) { const [entries, setEntries] = useState([]); const [loading, setLoading] = useState(true); const [confirming, setConfirming] = useState(false); const [problem, setProblem] = useState(null); useEffect(() => { let cancelled = false; setLoading(true); getAllEntries(document.id) .then((items) => !cancelled && setEntries(items)) .catch((e: unknown) => message.error(e instanceof ApiError ? e.message : 'Не удалось получить строки ОСВ')) .finally(() => !cancelled && setLoading(false)); return () => { cancelled = true; }; }, [document.id]); const [query, setQuery] = useState(''); const [account, setAccount] = useState(); const table = useMemo(() => flattenEntries(entries), [entries]); const columns = useMemo(() => buildColumns(table.hasDepartments), [table.hasDepartments]); const rows = useMemo(() => filterRows(table.rows, query, account), [table.rows, query, account]); // колонки до сумм: Субсчёт, [Подразделение], Контрагент, ИНН — под ними подпись «Итого по документу» const labelSpan = table.hasDepartments ? 4 : 3; const canConfirm = document.status === 'RECOGNIZED' || document.status === 'NEEDS_CHECK'; const confirm = async () => { setConfirming(true); setProblem(null); try { await confirmDocument(document.id); message.success('Документ подтверждён'); onChanged(); } catch (e: unknown) { setProblem(confirmErrorInfo(e)); } finally { setConfirming(false); } }; return (
Суммы в рублях; строки ОСВ только для чтения
} placeholder='Контрагент или ИНН' style={{ width: 320 }} value={query} onChange={(e) => setQuery(e.target.value)} />