feat(web+api): cockpit rep — novo pedido v2, endereço de entrega e cancelamento de orçamento

- NewOrderPage: etapas 1-5 — merge de cards, busca de produto melhorada
  (autocomplete com estoque + modal catálogo), responsividade mobile (tabela →
  cards, footer compacto), histórico de compras do cliente e campo de
  endereço de entrega (cadastrado vs livre)
- endEntrega: campo TEXT adicionado em sar.pedidos; contrato CreatePedidoSchema
  e PedidoDetailSchema atualizados; aparece no detalhe (OrderDetailPage,
  OrderDetailModal) e no PDF (OrderPrintPage)
- Cancelamento de orçamento: PATCH /orders/:id/cancel — rep cancela seu
  próprio orçamento (situa 0); histórico registrado; UI com Modal.confirm
  substituindo alert() em OrderActionsMenu
- Fix: useClientDetail chamado para cliente selecionado manualmente, garantindo
  que clientEndStr seja preenchido mesmo sem clientIdParam na URL

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-29 14:25:24 +00:00
parent 5bda00009f
commit a6cb1df2aa
10 changed files with 1181 additions and 202 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -425,18 +425,23 @@ export function OrderDetailPage() {
)}
<Descriptions.Item label="Total produtos">{fmt(order.totalProdutos)}</Descriptions.Item>
<Descriptions.Item label="Desc. Global">{order.descontoPerc}%</Descriptions.Item>
<Descriptions.Item label="Total">
<Descriptions.Item label="Total" span={isOrcamento ? 1 : 2}>
<Text strong style={{ fontSize: 16 }}>
{fmt(order.total)}
</Text>
</Descriptions.Item>
{order.endEntrega && (
<Descriptions.Item label="End. Entrega" span={isOrcamento ? 3 : 2}>
{order.endEntrega}
</Descriptions.Item>
)}
{order.obs && (
<Descriptions.Item label="Observações" span={2}>
<Descriptions.Item label="Observações" span={isOrcamento ? 3 : 2}>
{order.obs}
</Descriptions.Item>
)}
{order.motivoRecusa && (
<Descriptions.Item label="Motivo Recusa" span={2}>
<Descriptions.Item label="Motivo Recusa" span={isOrcamento ? 3 : 2}>
<Text type="danger">{order.motivoRecusa}</Text>
</Descriptions.Item>
)}

View File

@@ -399,11 +399,23 @@ export function OrderPrintPage() {
</div>
</div>
{/* ── Observações + rodapé ────────────────────────────────────────── */}
{order.obs && (
<div style={{ padding: '10px 28px 0' }}>
<span style={label}>Observações</span>
<div style={{ fontSize: 11, color: '#475569', lineHeight: 1.5 }}>{order.obs}</div>
{/* ── Endereço de entrega + Observações ───────────────────────────── */}
{(order.endEntrega || order.obs) && (
<div style={{ padding: '10px 28px 0', display: 'flex', gap: 24, flexWrap: 'wrap' }}>
{order.endEntrega && (
<div style={{ flex: 1, minWidth: 220 }}>
<span style={label}>Endereço de entrega</span>
<div style={{ fontSize: 11, color: '#475569', lineHeight: 1.5 }}>
{order.endEntrega}
</div>
</div>
)}
{order.obs && (
<div style={{ flex: 1, minWidth: 220 }}>
<span style={label}>Observações</span>
<div style={{ fontSize: 11, color: '#475569', lineHeight: 1.5 }}>{order.obs}</div>
</div>
)}
</div>
)}
<div

View File

@@ -1,4 +1,5 @@
import { useState, useMemo } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import {
App,
Button,
@@ -43,6 +44,7 @@ import { SITUA_LABEL } from '@sar/api-interface';
import { useOrderList, useOrderDetail } from '../../lib/queries/orders';
import { usePendingOrders } from '../../lib/hooks/usePendingOrders';
import { removePendingOrder, retryPendingOrder } from '../../lib/offline/order-queue';
import { apiFetch } from '../../lib/api-client';
const { Title, Text } = Typography;
const { useBreakpoint } = Grid;
@@ -250,6 +252,19 @@ function OrderActionsMenu({
onView: (id: string) => void;
}) {
const navigate = useNavigate();
const qc = useQueryClient();
const { modal, message: msg } = App.useApp();
const cancelMutation = useMutation({
mutationFn: () => apiFetch(`/orders/${order.id}/cancel`, { method: 'PATCH' }),
onSuccess: () => {
void msg.success('Orçamento cancelado.');
void qc.invalidateQueries({ queryKey: ['orders'] });
},
onError: (e: unknown) => void msg.error(e instanceof Error ? e.message : 'Erro ao cancelar'),
});
const canCancel = order.situa === 0 && order.fonte !== 'erp';
const items: MenuProps['items'] = [
{
@@ -287,8 +302,18 @@ function OrderActionsMenu({
icon: <CloseCircleOutlined />,
label: 'Cancelar pedido',
danger: true,
disabled: order.situa === 3,
onClick: () => alert('Cancelamento em breve'),
disabled: !canCancel,
onClick: () => {
if (!canCancel) return;
void modal.confirm({
title: 'Cancelar orçamento?',
content: `O pedido ${order.numPedSar} será marcado como cancelado. Esta ação não pode ser desfeita.`,
okText: 'Cancelar pedido',
okButtonProps: { danger: true },
cancelText: 'Voltar',
onOk: () => cancelMutation.mutate(),
});
},
},
];
@@ -387,7 +412,7 @@ function OrderDetailModal({ id, onClose }: { id: string | null; onClose: () => v
{isLoading && <Spin style={{ display: 'block', margin: '48px auto' }} />}
{data && (
<Space direction="vertical" size={20} style={{ width: '100%' }}>
<Space orientation="vertical" size={20} style={{ width: '100%' }}>
{/* Status */}
<div>
<OrderStatusBadge situa={data.situa} descr={data.statusDescr} />
@@ -434,6 +459,12 @@ function OrderDetailModal({ id, onClose }: { id: string | null; onClose: () => v
<span style={label}>Representante</span>
<Text>{data.nomeVendedor ?? `Cód. ${data.codVendedor}`}</Text>
</Col>
{data.endEntrega && (
<Col span={24}>
<span style={label}>End. Entrega</span>
<Text type="secondary">{data.endEntrega}</Text>
</Col>
)}
{data.obs && (
<Col span={24}>
<span style={label}>Observações</span>
@@ -460,7 +491,7 @@ function OrderDetailModal({ id, onClose }: { id: string | null; onClose: () => v
title: 'Produto',
key: 'produto',
render: (_: unknown, item) => (
<Space direction="vertical" size={0}>
<Space orientation="vertical" size={0}>
<Text style={{ fontSize: 11, color: '#94A3B8' }}>{item.codProduto}</Text>
<Text style={{ fontWeight: 500 }}>{item.descProduto}</Text>
</Space>