-
-
- ← {client.razao ?? client.nome}
-
-
- Novo Pedido
+
+ {/* ── Cabeçalho ─────────────────────────────────────────────────── */}
+
+
+ Lançamento de Pedido
-
-
-
-
- {step === 0 && (
-
- )}
- {step === 1 && (
-
- )}
- {step === 2 && (
-
- )}
+
}
+ onClick={() =>
+ void (clientIdParam
+ ? navigate({ to: '/clientes/$id', params: { id: clientIdParam } })
+ : navigate({ to: '/pedidos' }))
+ }
+ style={{ borderRadius: 8 }}
+ >
+ Voltar
+
+
{error && (
setError(null)}
+ style={{ marginBottom: 16, borderRadius: 8 }}
/>
)}
-
-
- {step > 0 && }
- {step < 2 && (
-
+ {/* ── Card 1: Dados do Cliente e Comercial ───────────────────────── */}
+
+ Dados do Cliente e Comercial
+
+ {
+ setSelectedClient(c);
+ setClientSearch('');
+ }}
+ />
+
+ {effectiveClient && (
+
+
+
+ {effectiveClient.razao ?? effectiveClient.nome}
+
+ {effectiveClient.cgcpf && (
+
+ {effectiveClient.cgcpf}
+
+ )}
+ {effectiveClient.limiteCreditoStr && (
+
+ Limite: {fmt(Number(effectiveClient.limiteCreditoStr))}
+
+ )}
+
)}
- {step === 2 && (
-
+
+
+
+
+ Pauta de Preço
+
+
+
+
+ {/* ── Card 2: Informações Adicionais ─────────────────────────────── */}
+
+ Informações Adicionais
+
+
+
+ Nº Ordem de Compra (Cliente)
+
+ setNumOC(e.target.value)}
+ placeholder="Ex: OC-98765"
+ style={{
+ width: '100%',
+ height: 32,
+ padding: '0 11px',
+ border: '1px solid #d9d9d9',
+ borderRadius: 6,
+ fontSize: 14,
+ outline: 'none',
+ boxSizing: 'border-box',
+ color: '#1F2937',
+ }}
+ />
+
+
+
+ Observações do Pedido
+
+
+
+
+ {/* ── Card 3: Produtos ────────────────────────────────────────────── */}
+
+ Pesquisar e Adicionar Produtos
+
+
+
+
+
+ {cart.length > 0 && (
+
+
+ {cart.length} item(ns) · {cart.reduce((a, i) => a + i.qtd, 0).toLocaleString('pt-BR')}{' '}
+ unidades
+
+
)}
-
+
+
+ {/* ── Rodapé fixo ─────────────────────────────────────────────────── */}
+
+ void navigate({ to: clientIdParam ? `/clientes/${clientIdParam}` : '/pedidos' })
+ }
+ onSubmit={() => mutation.mutate()}
+ />
);
}
diff --git a/apps/web/src/cockpits/rep/OrdersPage.tsx b/apps/web/src/cockpits/rep/OrdersPage.tsx
index b4d747e..428a5df 100644
--- a/apps/web/src/cockpits/rep/OrdersPage.tsx
+++ b/apps/web/src/cockpits/rep/OrdersPage.tsx
@@ -1,142 +1,776 @@
import { useState } from 'react';
-import { Table, Tag, Input, Select, Space, Typography, Badge } from 'antd';
+import {
+ Button,
+ Card,
+ Col,
+ Drawer,
+ Dropdown,
+ Empty,
+ Grid,
+ Row,
+ Select,
+ Space,
+ Spin,
+ Table,
+ Tag,
+ Timeline,
+ Typography,
+} from 'antd';
import type { TableColumnsType } from 'antd';
-import { Link } from '@tanstack/react-router';
+import type { MenuProps } from 'antd';
+import {
+ CheckCircleOutlined,
+ ClockCircleOutlined,
+ CloseCircleOutlined,
+ CopyOutlined,
+ DollarOutlined,
+ EllipsisOutlined,
+ EyeOutlined,
+ FilePdfOutlined,
+ PlusOutlined,
+ ShoppingCartOutlined,
+} from '@ant-design/icons';
+import { Link, useNavigate } from '@tanstack/react-router';
import type { PedidoSummary } from '@sar/api-interface';
import { SITUA_LABEL } from '@sar/api-interface';
-import { useOrderList } from '../../lib/queries/orders';
+import { useOrderList, useOrderDetail } from '../../lib/queries/orders';
-const { Title } = Typography;
-const { Search } = Input;
+const { Title, Text } = Typography;
+const { useBreakpoint } = Grid;
-const SITUA_COLOR: Record = {
- 1: 'warning',
- 2: 'processing',
- 3: 'error',
- 4: 'success',
+// ─── Helpers ──────────────────────────────────────────────────────────────────
+
+function fmt(v: number | string) {
+ return Number(v).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' });
+}
+
+function fmtDate(v: string) {
+ return new Date(v).toLocaleDateString('pt-BR');
+}
+
+function toISO(d: Date) {
+ return d.toISOString().split('T')[0];
+}
+
+function periodRange(p: string): { from?: string; to?: string } {
+ const today = new Date();
+ if (p === 'today') return { from: toISO(today), to: toISO(today) };
+ if (p === '7d') {
+ const d = new Date(today);
+ d.setDate(d.getDate() - 7);
+ return { from: toISO(d), to: toISO(today) };
+ }
+ if (p === '30d') {
+ const d = new Date(today);
+ d.setDate(d.getDate() - 30);
+ return { from: toISO(d), to: toISO(today) };
+ }
+ return {};
+}
+
+// ─── Status ───────────────────────────────────────────────────────────────────
+
+const STATUS: Record = {
+ 1: { label: 'Ag. Aprovação', color: '#d46b08', rowBg: '#fffbe6', tagColor: 'orange' },
+ 2: { label: 'Aprovado', color: '#389e0d', rowBg: '#f6ffed', tagColor: 'green' },
+ 3: { label: 'Cancelado', color: '#cf1322', rowBg: '#fff1f0', tagColor: 'red' },
+ 4: { label: 'Faturado', color: '#1d39c4', rowBg: '#f0f5ff', tagColor: 'geekblue' },
};
-const columns: TableColumnsType = [
- {
- title: 'Nº',
- dataIndex: 'numero',
- width: 120,
- render: (_: number, row: PedidoSummary) => {
- const label = row.numero ? String(row.numero) : row.numPedSar || row.id;
- return row.fonte === 'erp' ? (
- {label}
+// ─── OrderStatusBadge ─────────────────────────────────────────────────────────
+
+function OrderStatusBadge({ situa, descr }: { situa: number; descr?: string }) {
+ const cfg = STATUS[situa];
+ const label = descr ?? cfg?.label ?? SITUA_LABEL[situa] ?? String(situa);
+ return (
+
+ {label}
+
+ );
+}
+
+// ─── OrdersMetrics ────────────────────────────────────────────────────────────
+
+function OrdersMetrics({ data }: { data: PedidoSummary[] }) {
+ const total = data.reduce((a, o) => a + Number(o.total), 0);
+ const pendentes = data.filter((o) => o.situa === 1).length;
+ const aprovados = data.filter((o) => o.situa === 2).length;
+ const ticket = data.length > 0 ? total / data.length : 0;
+
+ const metrics = [
+ {
+ label: 'Total de Pedidos',
+ value: String(data.length),
+ icon: ,
+ color: '#003B8E',
+ },
+ { label: 'Total Vendido', value: fmt(total), icon: , color: '#389e0d' },
+ {
+ label: 'Ag. Aprovação',
+ value: String(pendentes),
+ icon: ,
+ color: '#d46b08',
+ },
+ {
+ label: 'Aprovados',
+ value: String(aprovados),
+ icon: ,
+ color: '#389e0d',
+ },
+ { label: 'Ticket Médio', value: fmt(ticket), icon: , color: '#1d39c4' },
+ ];
+
+ return (
+
+ {metrics.map((m) => (
+
+
+
+ {m.icon}
+
+
+ {m.label}
+
+
+ {m.value}
+
+
+
+
+
+ ))}
+
+ );
+}
+
+// ─── OrderActionsMenu ─────────────────────────────────────────────────────────
+
+function OrderActionsMenu({
+ order,
+ onView,
+}: {
+ order: PedidoSummary;
+ onView: (id: string) => void;
+}) {
+ const navigate = useNavigate();
+ const canDetail = order.fonte !== 'erp';
+
+ const items: MenuProps['items'] = [
+ canDetail
+ ? {
+ key: 'view',
+ icon: ,
+ label: 'Ver detalhes',
+ onClick: () => onView(order.id),
+ }
+ : { key: 'view', icon: , label: 'Ver detalhes', disabled: true },
+ {
+ key: 'duplicate',
+ icon: ,
+ label: Duplicar pedido,
+ onClick: () =>
+ void navigate({ to: '/pedidos/novo', search: { clientId: String(order.idCliente) } }),
+ },
+ { type: 'divider' },
+ {
+ key: 'pdf',
+ icon: ,
+ label: 'Gerar PDF',
+ onClick: () => alert('PDF em breve'),
+ },
+ {
+ key: 'cancel',
+ icon: ,
+ label: 'Cancelar pedido',
+ danger: true,
+ disabled: order.situa === 3,
+ onClick: () => alert('Cancelamento em breve'),
+ },
+ ];
+
+ return (
+
+ } size="small" />
+
+ );
+}
+
+// ─── OrderDetailDrawer ────────────────────────────────────────────────────────
+
+function OrderDetailDrawer({ id, onClose }: { id: string | null; onClose: () => void }) {
+ const { data, isLoading } = useOrderDetail(id ?? undefined);
+
+ const timelineItems = (data?.historico ?? []).map((h) => ({
+ dot:
+ h.situaNova === 2 ? (
+
+ ) : h.situaNova === 3 ? (
+
) : (
-
- {label}
-
- );
- },
- },
- {
- title: 'Status',
- dataIndex: 'situa',
- width: 150,
- render: (s: number, row: PedidoSummary) => {
- const label = row.statusDescr ?? SITUA_LABEL[s] ?? String(s);
- return (
-
+ ),
+ children: (
+
+ {new Date(h.changedAt).toLocaleString('pt-BR')}
+ {' — '}
+ {h.situaAnterior === null ? (
+ Pedido criado
+ ) : (
+ <>
+ Status alterado para{' '}
+ {STATUS[h.situaNova]?.label ?? SITUA_LABEL[h.situaNova] ?? h.situaNova}
+ >
+ )}
+ {h.nota && · {h.nota}}
+
+ ),
+ }));
+
+ const label: React.CSSProperties = {
+ fontSize: 11,
+ fontWeight: 700,
+ letterSpacing: '0.08em',
+ textTransform: 'uppercase',
+ color: '#64748B',
+ marginBottom: 2,
+ display: 'block',
+ };
+
+ return (
+
+
+ {data && (
+
+ )}
+
+ }
+ >
+ {isLoading &&
}
+
+ {data && (
+
+ {/* Status */}
+
+
+
+
+ {/* Dados principais */}
+
+
+
+ Pedido
+ {data.numPedSar}
+
+
+ Data
+ {fmtDate(data.dtPedido)}
+
+
+ Cód. Cliente
+ {data.idCliente}
+
+
+ Total
+
+ {fmt(data.total)}
+
+
+ {data.obs && (
+
+ Observações
+ {data.obs}
+
+ )}
+
+
+
+ {/* Itens */}
+ {data.itens?.length > 0 && (
+
+
+ Itens do Pedido ({data.itens.length})
+
+ {data.itens.map((item) => (
+
+
+ {item.codProduto}
+ {item.descProduto}
+
+ {Number(item.qtd)} un × {fmt(Number(item.precoUnitario))}
+
+
+
+ {fmt(Number(item.total))}
+
+
+ ))}
+
+
+ Total: {fmt(data.total)}
+
+
+
+ )}
+
+ {/* Histórico */}
+ {timelineItems.length > 0 && (
+
+ Histórico
+
+
+ )}
+
+ )}
+
+ );
+}
+
+// ─── MobileOrderCard ──────────────────────────────────────────────────────────
+
+function MobileOrderCard({
+ order,
+ onView,
+}: {
+ order: PedidoSummary;
+ onView: (id: string) => void;
+}) {
+ const navigate = useNavigate();
+ const cfg = STATUS[order.situa];
+
+ return (
+
+
+
+ {order.numPedSar}
+
+
+
+
+ Cód. cliente {order.idCliente} · {fmtDate(order.dtPedido)}
+
+
+ {fmt(order.total)}
+
+
+ }
+ disabled={order.fonte === 'erp'}
+ onClick={() => onView(order.id)}
+ >
+ Ver
+
+ }
+ onClick={() =>
+ void navigate({ to: '/pedidos/novo', search: { clientId: String(order.idCliente) } })
}
- text={{label}}
- />
- );
- },
- },
- {
- title: 'Total',
- dataIndex: 'total',
- width: 130,
- align: 'right',
- render: (v: string) =>
- Number(v).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' }),
- },
- {
- title: 'Data',
- dataIndex: 'dtPedido',
- width: 130,
- render: (v: string) => new Date(v).toLocaleDateString('pt-BR'),
- },
-];
+ >
+ Duplicar
+
+
+
+ );
+}
+
+// ─── EmptyState ───────────────────────────────────────────────────────────────
+
+function EmptyOrders({ onNew }: { onNew: () => void }) {
+ return (
+
}
+ imageStyle={{ height: 64 }}
+ description={
+
+
+ Nenhum pedido encontrado
+
+ Tente alterar os filtros ou crie um novo pedido.
+
+ }
+ style={{ padding: '48px 0' }}
+ >
+
} onClick={onNew}>
+ Novo Pedido
+
+
+ );
+}
+
+// ─── OrdersPage ───────────────────────────────────────────────────────────────
export function OrdersPage() {
- const [numFilter, setNumFilter] = useState('');
+ const navigate = useNavigate();
+ const screens = useBreakpoint();
+ const isMobile = !screens.md;
+
+ const [search, setSearch] = useState('');
const [situaFilter, setSituaFilter] = useState
();
+ const [period, setPeriod] = useState('');
const [page, setPage] = useState(1);
- const limit = 50;
+ const [drawerOrderId, setDrawerOrderId] = useState(null);
+ const limit = 20;
+
+ const { from, to } = period ? periodRange(period) : {};
const { data, isLoading } = useOrderList({
- numPedSar: numFilter || undefined,
+ numPedSar: search || undefined,
situa: situaFilter,
+ from,
+ to,
page,
limit,
});
+ const rows = data?.data ?? [];
+ const total = data?.total ?? 0;
+
+ function clearFilters() {
+ setSearch('');
+ setSituaFilter(undefined);
+ setPeriod('');
+ setPage(1);
+ }
+
+ // ── Tabela desktop ─────────────────────────────────────────────────────────
+ const columns: TableColumnsType = [
+ {
+ title: 'Nº Pedido',
+ dataIndex: 'numPedSar',
+ width: 140,
+ render: (_: string, row: PedidoSummary) => {
+ const label = row.numero ? String(row.numero) : row.numPedSar;
+ return row.fonte === 'erp' ? (
+
+ {label}
+
+ ) : (
+
+
+ {label}
+
+
+ );
+ },
+ },
+ {
+ title: 'Cliente',
+ key: 'cliente',
+ render: (_: unknown, row: PedidoSummary) => (
+
+ Cód. {row.idCliente}
+ {row.obs && (
+
+ {row.obs.slice(0, 40)}
+
+ )}
+
+ ),
+ },
+ {
+ title: 'Status',
+ dataIndex: 'situa',
+ width: 140,
+ render: (s: number, row: PedidoSummary) => (
+
+ ),
+ },
+ {
+ title: 'Total',
+ dataIndex: 'total',
+ width: 130,
+ align: 'right',
+ render: (v: string) => (
+
+ {fmt(v)}
+
+ ),
+ },
+ {
+ title: 'Data',
+ dataIndex: 'dtPedido',
+ width: 110,
+ render: (v: string) => {fmtDate(v)},
+ },
+ {
+ title: '',
+ key: 'actions',
+ width: 48,
+ render: (_: unknown, row: PedidoSummary) => (
+ setDrawerOrderId(id)} />
+ ),
+ },
+ ];
+
return (
-
-
- Pedidos
-
-
-
- {
- setNumFilter(v);
- setPage(1);
- }}
- onChange={(e) => {
- if (!e.target.value) {
- setNumFilter('');
- setPage(1);
- }
- }}
- />
- {
- setSituaFilter(v as number | undefined);
- setPage(1);
- }}
- options={[
- { value: 1, label: 'Ag. Aprovação' },
- { value: 2, label: 'Aprovado' },
- { value: 3, label: 'Cancelado' },
- { value: 4, label: 'Faturado' },
- ]}
- />
-
-
-
- rowKey="id"
- columns={columns}
- dataSource={data?.data ?? []}
- loading={isLoading}
- rowClassName={(row) => (row.situa === 1 ? 'row-pending' : '')}
- pagination={{
- current: page,
- pageSize: limit,
- total: data?.total ?? 0,
- showSizeChanger: false,
- onChange: (p) => setPage(p),
+
+ {/* ── Cabeçalho ───────────────────────────────────────────────── */}
+
+ >
+
+
+ Pedidos
+
+
+ Acompanhe seus pedidos, status de envio e histórico comercial.
+
+
+ {!isMobile && (
+
}
+ size="large"
+ onClick={() => void navigate({ to: '/pedidos/novo' })}
+ style={{ borderRadius: 8, fontWeight: 600 }}
+ >
+ Novo Pedido
+
+ )}
+
-
+ {/* ── Métricas ────────────────────────────────────────────────── */}
+
+
+ {/* ── Filtros ─────────────────────────────────────────────────── */}
+
+
+
+ {
+ setSearch(e.target.value);
+ setPage(1);
+ }}
+ placeholder="Buscar por nº do pedido..."
+ style={{
+ width: '100%',
+ height: 32,
+ padding: '0 11px',
+ border: '1px solid #d9d9d9',
+ borderRadius: 6,
+ fontSize: 14,
+ outline: 'none',
+ color: '#1F2937',
+ boxSizing: 'border-box',
+ }}
+ />
+
+
+ {
+ setSituaFilter(v);
+ setPage(1);
+ }}
+ options={[
+ { value: 1, label: 'Ag. Aprovação' },
+ { value: 2, label: 'Aprovado' },
+ { value: 3, label: 'Cancelado' },
+ { value: 4, label: 'Faturado' },
+ ]}
+ />
+
+
+ {
+ setPeriod(v ?? '');
+ setPage(1);
+ }}
+ options={[
+ { value: 'today', label: 'Hoje' },
+ { value: '7d', label: 'Últimos 7 dias' },
+ { value: '30d', label: 'Últimos 30 dias' },
+ ]}
+ />
+
+
+
+
+
+
+
+ {/* ── Conteúdo principal ──────────────────────────────────────── */}
+ {isLoading ? (
+
+
+
+ ) : rows.length === 0 ? (
+
+ void navigate({ to: '/pedidos/novo' })} />
+
+ ) : isMobile ? (
+ /* ── Mobile: cards ─────────────────────────────────────────── */
+
+ {rows.map((o) => (
+
setDrawerOrderId(id)} />
+ ))}
+
+ Mostrando {rows.length} de {total} pedidos
+
+
+ ) : (
+ /* ── Desktop: tabela ────────────────────────────────────────── */
+
+
+ rowKey="id"
+ columns={columns}
+ dataSource={rows}
+ size="middle"
+ onRow={(row) => ({
+ onClick: () => {
+ if (row.fonte !== 'erp') setDrawerOrderId(row.id);
+ },
+ style: {
+ background: STATUS[row.situa]?.rowBg ?? '#fff',
+ cursor: row.fonte !== 'erp' ? 'pointer' : 'default',
+ },
+ })}
+ pagination={{
+ current: page,
+ pageSize: limit,
+ total,
+ showSizeChanger: false,
+ showTotal: (t, [s, e]) => `Mostrando ${s}–${e} de ${t} pedidos`,
+ onChange: (p) => setPage(p),
+ style: { padding: '12px 24px' },
+ }}
+ style={{ borderRadius: 10, overflow: 'hidden' }}
+ />
+
+ )}
+
+ {/* ── Drawer de detalhe ───────────────────────────────────────── */}
+ setDrawerOrderId(null)} />
+
+ {/* FAB mobile */}
+ {isMobile && (
+ }
+ size="large"
+ onClick={() => void navigate({ to: '/pedidos/novo' })}
+ style={{
+ position: 'fixed',
+ bottom: 24,
+ right: 24,
+ width: 52,
+ height: 52,
+ fontSize: 22,
+ backgroundColor: '#389e0d',
+ borderColor: '#389e0d',
+ boxShadow: '0 4px 16px rgba(56,158,13,0.45)',
+ zIndex: 1000,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ }}
+ />
+ )}
+
+
);
}
diff --git a/apps/web/src/components/layout/AppShell.tsx b/apps/web/src/components/layout/AppShell.tsx
index 4d03e6b..43c15b0 100644
--- a/apps/web/src/components/layout/AppShell.tsx
+++ b/apps/web/src/components/layout/AppShell.tsx
@@ -1,5 +1,7 @@
import { useState, type ReactNode } from 'react';
-import { Flex } from 'antd';
+import { Button, Flex, Tooltip } from 'antd';
+import { PlusOutlined } from '@ant-design/icons';
+import { useNavigate } from '@tanstack/react-router';
import { Topbar } from './Topbar';
import { Sidebar } from './Sidebar';
@@ -15,6 +17,7 @@ interface AppShellProps {
export function AppShell({ children }: AppShellProps) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_sidebarOpen, setSidebarOpen] = useState(true);
+ const navigate = useNavigate();
return (
@@ -32,6 +35,31 @@ export function AppShell({ children }: AppShellProps) {
{children}
+
+ {/* FAB — Novo Pedido */}
+
+ }
+ onClick={() => void navigate({ to: '/pedidos/novo' })}
+ style={{
+ position: 'fixed',
+ bottom: 32,
+ right: 32,
+ width: 52,
+ height: 52,
+ fontSize: 22,
+ backgroundColor: '#389e0d',
+ borderColor: '#389e0d',
+ boxShadow: '0 4px 16px rgba(56,158,13,0.45)',
+ zIndex: 1000,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ }}
+ />
+
);
}
diff --git a/apps/web/src/components/layout/Topbar.tsx b/apps/web/src/components/layout/Topbar.tsx
index 249f2b3..009c983 100644
--- a/apps/web/src/components/layout/Topbar.tsx
+++ b/apps/web/src/components/layout/Topbar.tsx
@@ -1,4 +1,5 @@
import { Avatar, Badge, Button, Dropdown, Flex, Input, Typography } from 'antd';
+import { PlusOutlined } from '@ant-design/icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
faBell,
@@ -6,6 +7,7 @@ import {
faBars,
faRightFromBracket,
} from '@fortawesome/free-solid-svg-icons';
+import { useNavigate } from '@tanstack/react-router';
import { brandTokens } from '../../lib/theme';
import { FoundationStatus } from './FoundationStatus';
import { usePendingCount } from '../../lib/queries/notifications';
@@ -27,6 +29,7 @@ function logout() {
}
export function Topbar({ onToggleSidebar }: TopbarProps) {
+ const navigate = useNavigate();
const { data: pendingData } = usePendingCount();
const pendingCount = pendingData?.count ?? 0;
const { data: user } = useCurrentUser();
@@ -131,8 +134,16 @@ export function Topbar({ onToggleSidebar }: TopbarProps) {
/>
- {/* Lado direito: status fundação + notificações + perfil */}
+ {/* Lado direito: novo pedido + status fundação + notificações + perfil */}
+ }
+ onClick={() => void navigate({ to: '/pedidos/novo' })}
+ style={{ borderRadius: 8, fontWeight: 'var(--font-weight-semibold)' }}
+ >
+ Novo Pedido
+