import { useQueryClient } from '@tanstack/react-query' import { useMemo, useState } from 'react' import { Link } from 'react-router-dom' import { deleteOrder } from '@/api/orders' import { cancelReceipt, createReceipts } from '@/api/receipts' import '@/pages/DashboardPage.css' import { useAuth } from '@/features/auth/useAuth' import { OrderDetailModal } from '@/features/orders/OrderDetailModal' import type { Order, OrderTab } from '@/features/orders/types' import { useOrders, useOrdersSummary } from '@/features/orders/useOrders' import { defaultPrepayment, prepaymentMatches, toKopecks } from '@/features/receipts/money' import { CANCELLABLE, RECEIPT_STATUS } from '@/features/receipts/types' import type { ReceiptRequestItem } from '@/features/receipts/types' const ROLE_LABEL: Record = { admin: 'Адміністратор', cashier: 'Касир', viewer: 'Спостерігач', } const TABS: { key: OrderTab; label: string }[] = [ { key: 'no_receipt', label: 'Без чека' }, { key: 'has_receipt', label: 'Виписані чеки' }, { key: 'received', label: 'Отримані' }, { key: 'refused', label: 'Відмови' }, ] function npStatusTone(order: Order): 'delivered' | 'processing' | 'danger' | 'new' { const text = (order.np_status ?? '').toLowerCase() if (text.includes('видано') || text.includes('отримано')) return 'delivered' if (text.includes('відмова') || text.includes('не забра') || text.includes('поверн')) return 'danger' if (text) return 'processing' return 'new' } /** Заказ можно отправить в Checkbox: есть ТТН и сумма контроля оплаты. */ function canCreateReceipt(order: Order): boolean { return Boolean(order.waybill_number && order.np_cod_amount) } function paymentBadge(order: Order): { label: string; tone: 'delivered' | 'processing' } | null { if (order.np_payment_status === 'Payed') return { label: 'Оплачено', tone: 'delivered' } if (order.np_payment_status === 'NeedPayment') return { label: 'Не оплачено', tone: 'processing' } return null } const MONEY_FORMAT = new Intl.NumberFormat('uk-UA', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) function formatMoney(amount: string): string { return MONEY_FORMAT.format(Number(amount)) } export function DashboardPage() { const { user, logout } = useAuth() const [tab, setTab] = useState('no_receipt') const { data: orders, isLoading, isError } = useOrders(tab) const { data: summary } = useOrdersSummary() const queryClient = useQueryClient() const [selected, setSelected] = useState>(new Set()) const [search, setSearch] = useState('') const [viewingOrder, setViewingOrder] = useState(null) const [deletingId, setDeletingId] = useState(null) // Введённая кассиром предоплата (₴) по заказу; нет ключа — значение по умолчанию. const [prepayments, setPrepayments] = useState>({}) const [submitting, setSubmitting] = useState(false) const [cancellingId, setCancellingId] = useState(null) const [notice, setNotice] = useState<{ tone: 'ok' | 'error'; lines: string[] } | null>(null) const canDelete = user?.role !== 'viewer' const canFiscalize = user?.role === 'admin' || user?.role === 'cashier' function prepaymentOf(order: Order): string { return prepayments[order.id] ?? defaultPrepayment(order.total_amount, order.np_cod_amount) } async function submitReceipts(targets: Order[]) { const items: ReceiptRequestItem[] = [] const localErrors: string[] = [] for (const order of targets) { const prepayment = toKopecks(prepaymentOf(order)) if (prepayment === null) { localErrors.push(`${order.id}: некоректна сума передоплати`) continue } items.push({ order_id: order.id, prepayment_kopecks: prepayment }) } if (items.length === 0) { setNotice({ tone: 'error', lines: localErrors }) return } setSubmitting(true) try { const result = await createReceipts(items) const errors = [...localErrors, ...Object.entries(result.errors).map(([id, msg]) => `${id}: ${msg}`)] const lines = [ ...(result.created.length ? [`Надіслано в Checkbox: ${result.created.length}`] : []), ...errors, ] setNotice({ tone: errors.length ? 'error' : 'ok', lines }) setSelected(new Set()) await queryClient.invalidateQueries({ queryKey: ['orders'] }) } catch (err) { setNotice({ tone: 'error', lines: [err instanceof Error ? err.message : 'Не вдалося створити чеки'] }) } finally { setSubmitting(false) } } async function handleCancel(order: Order) { if (!order.receipt_id) return const outcome = { no_receipt: 'Замовлення повернеться в чергу.', has_receipt: 'Замовлення повернеться в чергу.', received: 'Замовлення залишиться в отриманих.', refused: 'Замовлення залишиться у відмовах.', }[tab] if (!window.confirm(`Скасувати ЕТТН-чек за замовленням ${order.id}? ${outcome}`)) return setCancellingId(order.id) try { await cancelReceipt(order.receipt_id) setNotice({ tone: 'ok', lines: [`Чек за замовленням ${order.id} скасовано`] }) await queryClient.invalidateQueries({ queryKey: ['orders'] }) } catch (err) { setNotice({ tone: 'error', lines: [err instanceof Error ? err.message : 'Не вдалося скасувати чек'] }) } finally { setCancellingId(null) } } const filtered = useMemo(() => { const query = search.trim().toLowerCase() const list = orders ?? [] if (!query) return list return list.filter( (order) => order.id.toLowerCase().includes(query) || (order.waybill_number ?? '').includes(query) || (order.recipient_name ?? '').toLowerCase().includes(query), ) }, [orders, search]) const allSelected = filtered.length > 0 && filtered.every((order) => selected.has(order.id)) function toggleAll() { setSelected(allSelected ? new Set() : new Set(filtered.map((order) => order.id))) } function toggleOne(id: string) { setSelected((prev) => { const next = new Set(prev) if (next.has(id)) next.delete(id) else next.add(id) return next }) } async function handleDelete(order: Order) { setDeletingId(order.id) try { await deleteOrder(order.id) setSelected((prev) => { const next = new Set(prev) next.delete(order.id) return next }) await queryClient.invalidateQueries({ queryKey: ['orders'] }) } finally { setDeletingId(null) } } return (
Assistant System
{user?.full_name} {user ? (ROLE_LABEL[user.role] ?? user.role) : ''}

Замовлення

{user?.role === 'admin' && ( Каси )} {tab === 'no_receipt' && canFiscalize && ( )}
Післяплата в дорозі {summary ? `${formatMoney(summary.cod_in_transit_amount)} ₴` : '—'} {summary ? `${summary.cod_in_transit_count} посилок` : ' '}
{notice && (
    {notice.lines.map((line) => (
  • {line}
  • ))}
)}
{TABS.map((t) => ( ))}
Вибрано: {selected.size} замовлень setSearch(e.target.value)} />
{isLoading && ( )} {isError && ( )} {!isLoading && !isError && filtered.map((order) => { const badge = paymentBadge(order) return ( ) })} {!isLoading && !isError && filtered.length === 0 && ( )}
ID замовлення Номер ТТН Клієнт Сума Післяплата Оплачено Передоплата, ₴ Статус ТТН Дії
Завантаження…
Не вдалося завантажити замовлення з CRM
toggleOne(order.id)} aria-label={`Вибрати замовлення ${order.id}`} /> {order.id} {order.waybill_number || '—'} {order.recipient_name || '—'} {order.total_amount} ₴ {order.np_cod_amount ? `${order.np_cod_amount} ₴` : '—'} {badge ? ( {badge.label} ) : ( '—' )} {tab === 'no_receipt' ? ( setPrepayments((prev) => ({ ...prev, [order.id]: e.target.value }))} /> ) : ( (order.receipt_prepayment ?? '—') )} {order.np_status || 'Немає даних'} {tab === 'no_receipt' && canFiscalize && ( )} {order.receipt_status && (tab !== 'no_receipt' || !CANCELLABLE.has(order.receipt_status)) && ( {RECEIPT_STATUS[order.receipt_status].label} )} {tab !== 'no_receipt' && canFiscalize && order.receipt_status && CANCELLABLE.has(order.receipt_status) && ( )} {canDelete && (tab === 'no_receipt' || ((tab === 'refused' || tab === 'received') && !order.has_receipt)) && ( )}
Нічого не знайдено
{viewingOrder && ( order.id === viewingOrder.id) ?? viewingOrder} canEdit={canFiscalize && tab === 'no_receipt'} onClose={() => setViewingOrder(null)} /> )}
) }