Formas de pagamento: - Endpoint GET /catalog/payment-methods lendo vw_formas_pagamento filtrado por ativa=1 e integrar_sar=1 - FormaPagamento schema/type no shared api-interface - Hook useFormasPagamento (staleTime 1h) substituindo lista hardcoded Offline (FR-4.2 / NFR-2.1–2.4): - IndexedDB queue: lib/offline/idb.ts + order-queue.ts sem deps externos - NewOrderPage detecta !navigator.onLine → enqueueOrder() → toast + reset - useOfflineSync: auto-sync ao reconectar (POST orders + PATCH transmit) - usePendingOrders: fila reativa via CustomEvents - AppShell: banner offline + useOfflineSync() global - OrdersPage: seção de pedidos pendentes com retry/descartar - sw.js: network-first para API GETs cacheáveis + stale-while-revalidate para assets + app shell navigate fallback Docs: - architecture.md: documento de decisões de arquitetura do SAR MVP Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
109 lines
2.9 KiB
TypeScript
109 lines
2.9 KiB
TypeScript
import { Table, Tag, Typography, Badge, Space } from 'antd';
|
|
import type { TableColumnsType } from 'antd';
|
|
import { Link } from '@tanstack/react-router';
|
|
import type { PedidoSummary } from '@sar/api-interface';
|
|
import { useOrderList } from '../../lib/queries/orders';
|
|
|
|
const { Title } = Typography;
|
|
|
|
function hoursWaiting(createdAt: string): number {
|
|
return Math.floor((Date.now() - new Date(createdAt).getTime()) / 3_600_000);
|
|
}
|
|
|
|
const columns: TableColumnsType<PedidoSummary> = [
|
|
{
|
|
title: 'Nº',
|
|
dataIndex: 'numPedSar',
|
|
width: 120,
|
|
render: (num: string, row: PedidoSummary) => (
|
|
<Link to="/pedidos/$id" params={{ id: row.id }}>
|
|
{num}
|
|
</Link>
|
|
),
|
|
},
|
|
{
|
|
title: 'Representante',
|
|
key: 'rep',
|
|
width: 160,
|
|
render: (_: unknown, row: PedidoSummary) => row.nomeVendedor ?? `Cód. ${row.codVendedor}`,
|
|
},
|
|
{
|
|
title: 'Cliente',
|
|
key: 'cliente',
|
|
width: 200,
|
|
render: (_: unknown, row: PedidoSummary) =>
|
|
row.razaoCliente ?? row.nomeCliente ?? `Cód. ${row.idCliente}`,
|
|
},
|
|
{
|
|
title: 'Total',
|
|
dataIndex: 'total',
|
|
width: 130,
|
|
align: 'right',
|
|
render: (v: string) =>
|
|
Number(v).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' }),
|
|
},
|
|
{
|
|
title: 'Desc. %',
|
|
dataIndex: 'descontoPerc',
|
|
width: 90,
|
|
align: 'right',
|
|
render: (v: string) => `${v}%`,
|
|
},
|
|
{
|
|
title: 'Aguardando',
|
|
dataIndex: 'createdAt',
|
|
width: 130,
|
|
render: (v: string) => {
|
|
const h = hoursWaiting(v);
|
|
return <Tag color={h > 2 ? 'red' : 'orange'}>{h}h</Tag>;
|
|
},
|
|
},
|
|
{
|
|
title: '',
|
|
width: 100,
|
|
render: (_: unknown, row: PedidoSummary) => (
|
|
<Link to="/pedidos/$id" params={{ id: row.id }}>
|
|
<Tag color="blue" style={{ cursor: 'pointer' }}>
|
|
Analisar
|
|
</Tag>
|
|
</Link>
|
|
),
|
|
},
|
|
];
|
|
|
|
export function ApprovalQueuePage() {
|
|
// situa=1 = Pendente de Aprovação
|
|
const { data, isLoading } = useOrderList({ situa: 1, limit: 200 });
|
|
|
|
const urgentCount = data?.data.filter((o) => hoursWaiting(o.createdAt) > 2).length ?? 0;
|
|
|
|
return (
|
|
<div style={{ padding: 24 }}>
|
|
<Space align="center" style={{ marginBottom: 16 }}>
|
|
<Title level={3} style={{ margin: 0 }}>
|
|
Fila de Aprovações
|
|
</Title>
|
|
{urgentCount > 0 && (
|
|
<Badge
|
|
count={urgentCount}
|
|
style={{ backgroundColor: '#cf1322' }}
|
|
title={`${urgentCount} urgente(s) — mais de 2h aguardando`}
|
|
/>
|
|
)}
|
|
</Space>
|
|
|
|
<Table<PedidoSummary>
|
|
rowKey="id"
|
|
columns={columns}
|
|
dataSource={data?.data ?? []}
|
|
loading={isLoading}
|
|
rowClassName={(row) => (hoursWaiting(row.createdAt) > 2 ? 'row-urgent' : '')}
|
|
pagination={false}
|
|
locale={{ emptyText: 'Nenhum pedido aguardando aprovação.' }}
|
|
/>
|
|
|
|
<style>{`.row-urgent td { background: #fff1f0 !important; }`}</style>
|
|
</div>
|
|
);
|
|
}
|