feat(mvp-rep): formas de pagamento do ERP + suporte offline completo
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>
This commit is contained in:
@@ -20,8 +20,9 @@ import {
|
||||
} from 'antd';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faShareNodes } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FilePdfOutlined } from '@ant-design/icons';
|
||||
import type { TableColumnsType } from 'antd';
|
||||
import { Link, useParams } from '@tanstack/react-router';
|
||||
import { Link, useParams, useNavigate } from '@tanstack/react-router';
|
||||
import type { PedidoItem, HistoricoPedido } from '@sar/api-interface';
|
||||
import { SITUA_LABEL } from '@sar/api-interface';
|
||||
import { useOrderDetail } from '../../lib/queries/orders';
|
||||
@@ -35,6 +36,7 @@ const { TextArea } = Input;
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
const SITUA_COLOR: Record<number, string> = {
|
||||
0: 'default',
|
||||
1: 'warning',
|
||||
2: 'processing',
|
||||
3: 'error',
|
||||
@@ -244,12 +246,14 @@ function RejectModal({
|
||||
|
||||
export function OrderDetailPage() {
|
||||
const { id } = useParams({ from: '/pedidos/$id' });
|
||||
const navigate = useNavigate();
|
||||
const qc = useQueryClient();
|
||||
const { data: order, isLoading, error } = useOrderDetail(id);
|
||||
const { data: clientOrders } = useClientOrders(order?.idCliente);
|
||||
|
||||
const role = getRoleFromToken();
|
||||
const canAct = role !== 'rep' && order?.situa === 1;
|
||||
const canTransmit = role === 'rep' && order?.situa === 0;
|
||||
const canShare =
|
||||
role === 'rep' &&
|
||||
(order?.situa === 2 || order?.situa === 4) &&
|
||||
@@ -288,6 +292,16 @@ export function OrderDetailPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const transmitMutation = useMutation({
|
||||
mutationFn: () => apiFetch(`/orders/${id}/transmit`, { method: 'PATCH' }),
|
||||
onSuccess: () => {
|
||||
void qc.invalidateQueries({ queryKey: ['orders', id] });
|
||||
void qc.invalidateQueries({ queryKey: ['orders'] });
|
||||
},
|
||||
// Mensagem de bloqueio de alçada (desconto acima do máximo) vem aqui.
|
||||
onError: (e: unknown) => setActionError(e instanceof Error ? e.message : 'Erro ao transmitir'),
|
||||
});
|
||||
|
||||
if (isLoading) return <Spin style={{ display: 'block', marginTop: 64 }} />;
|
||||
if (error || !order)
|
||||
return <Alert type="error" message="Pedido não encontrado." style={{ margin: 24 }} />;
|
||||
@@ -297,8 +311,11 @@ export function OrderDetailPage() {
|
||||
? Math.floor((Date.now() - new Date(order.createdAt).getTime()) / 3_600_000)
|
||||
: null;
|
||||
|
||||
// Orçamento: tela mais larga para consulta/revisão com o cliente.
|
||||
const isOrcamento = order.situa === 0;
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24, maxWidth: 960 }}>
|
||||
<div style={{ padding: 24, maxWidth: isOrcamento ? 1320 : 960, margin: '0 auto' }}>
|
||||
<Space align="center" style={{ marginBottom: 16 }} wrap>
|
||||
<Link to="/pedidos">← Pedidos</Link>
|
||||
<Title level={3} style={{ margin: 0 }}>
|
||||
@@ -322,6 +339,25 @@ export function OrderDetailPage() {
|
||||
{timeWaiting !== null && timeWaiting > 2 && (
|
||||
<Tag color="red">Urgente — {timeWaiting}h aguardando</Tag>
|
||||
)}
|
||||
<Button
|
||||
icon={<FilePdfOutlined />}
|
||||
onClick={() => navigate({ to: '/pedidos/$id/imprimir', params: { id } })}
|
||||
>
|
||||
Gerar PDF
|
||||
</Button>
|
||||
{canTransmit && (
|
||||
<Button
|
||||
type="primary"
|
||||
loading={transmitMutation.isPending}
|
||||
onClick={() => {
|
||||
setActionError(null);
|
||||
transmitMutation.mutate();
|
||||
}}
|
||||
style={{ backgroundColor: '#389e0d', borderColor: '#389e0d' }}
|
||||
>
|
||||
Transmitir pedido
|
||||
</Button>
|
||||
)}
|
||||
{canAct && (
|
||||
<Space>
|
||||
<Button type="primary" onClick={() => setApproveOpen(true)}>
|
||||
@@ -359,13 +395,20 @@ export function OrderDetailPage() {
|
||||
/>
|
||||
)}
|
||||
|
||||
<Descriptions bordered size="small" column={2} style={{ marginBottom: 24 }}>
|
||||
<Descriptions
|
||||
bordered
|
||||
size={isOrcamento ? 'middle' : 'small'}
|
||||
column={isOrcamento ? 3 : 2}
|
||||
style={{ marginBottom: 24 }}
|
||||
>
|
||||
<Descriptions.Item label="Cliente">
|
||||
<Link to="/clientes/$id" params={{ id: String(order.idCliente) }}>
|
||||
Cód. {order.idCliente}
|
||||
{order.razaoCliente ?? order.nomeCliente ?? `Cód. ${order.idCliente}`}
|
||||
</Link>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Rep (cód)">{order.codVendedor}</Descriptions.Item>
|
||||
<Descriptions.Item label="Representante">
|
||||
{order.nomeVendedor ?? `Cód. ${order.codVendedor}`}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Data">
|
||||
{new Date(order.dtPedido).toLocaleDateString('pt-BR')}
|
||||
</Descriptions.Item>
|
||||
@@ -399,7 +442,7 @@ export function OrderDetailPage() {
|
||||
columns={itemColumns}
|
||||
dataSource={order.itens}
|
||||
pagination={false}
|
||||
size="small"
|
||||
size={isOrcamento ? 'middle' : 'small'}
|
||||
style={{ marginBottom: 24 }}
|
||||
/>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user