feat(web+api): redesign ClientsPage/OrdersPage e corrige dados empresa 9001
Web — ClientsPage: - Redesign completo: métricas reais via usePortfolioStats (4 queries count), donut Chart.js com totais reais, tabela sem ellipsis, coluna Cliente com nome fantasia/razão/CNPJ completos, drawer de detalhes e análise comercial, cards mobile, filtros de status/busca em tempo real. - Dados reais: substitui mock por useClientList/useClientDetail/useClientOrders; remove tipos fictícios (prospect/lead, cidade, totalComprado). Web — OrdersPage: - Métricas reais via useOrderStats (contagens por situa, não da página atual). - Coluna Cliente sem truncamento (minWidth: 240). - Cabeçalho, filtros e layout alinhados ao padrão da ClientsPage. API — orders.service.ts: - Normalização situa SIG→SAR: SIG usa 5=Cancelado; SAR usa 3=Cancelado. sigToSar(5→3) no mapper; sarToSig(3→5) no filtro SQL. API — clients.service.ts: - dt_ultima_compra corrigida: JOIN duplo (vw_pedidos_erp + sar.pedidos) com GREATEST() — clientes com histórico ERP mas sem pedido SAR deixam de aparecer todos como Inativo. - Filtro de activityStatus movido para SQL — total e paginação corretos. - findOne() atualizado com o mesmo JOIN duplo. Infra — .env: - DEV_EMPRESA_ID: 1 → 9001 — API aponta para dados reais da empresa SIG. Ex: pedido nº 141022 passa de R$1.765,48 para R$2.454,90. Docs — sarweb_views.sql: - Documenta as views reais em schema sar; remove schema sarweb inexistente. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,11 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useMemo } from 'react';
|
||||
import {
|
||||
App,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Drawer,
|
||||
Dropdown,
|
||||
Empty,
|
||||
Grid,
|
||||
Row,
|
||||
Select,
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
Tag,
|
||||
Timeline,
|
||||
Typography,
|
||||
Divider,
|
||||
} from 'antd';
|
||||
import type { TableColumnsType } from 'antd';
|
||||
import type { MenuProps } from 'antd';
|
||||
@@ -29,6 +30,8 @@ import {
|
||||
FilePdfOutlined,
|
||||
PlusOutlined,
|
||||
ShoppingCartOutlined,
|
||||
ClearOutlined,
|
||||
SearchOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Link, useNavigate } from '@tanstack/react-router';
|
||||
import type { PedidoSummary } from '@sar/api-interface';
|
||||
@@ -68,7 +71,7 @@ function periodRange(p: string): { from?: string; to?: string } {
|
||||
return {};
|
||||
}
|
||||
|
||||
// ─── Status ───────────────────────────────────────────────────────────────────
|
||||
// ─── Status Config ────────────────────────────────────────────────────────────
|
||||
|
||||
const STATUS: Record<number, { label: string; color: string; rowBg: string; tagColor: string }> = {
|
||||
1: { label: 'Ag. Aprovação', color: '#d46b08', rowBg: '#fffbe6', tagColor: 'orange' },
|
||||
@@ -77,66 +80,73 @@ const STATUS: Record<number, { label: string; color: string; rowBg: string; tagC
|
||||
4: { label: 'Faturado', color: '#1d39c4', rowBg: '#f0f5ff', tagColor: 'geekblue' },
|
||||
};
|
||||
|
||||
// ─── OrderStatusBadge ─────────────────────────────────────────────────────────
|
||||
// ─── useOrderStats ────────────────────────────────────────────────────────────
|
||||
|
||||
function OrderStatusBadge({ situa, descr }: { situa: number; descr?: string }) {
|
||||
const cfg = STATUS[situa];
|
||||
const label = descr ?? cfg?.label ?? SITUA_LABEL[situa] ?? String(situa);
|
||||
return (
|
||||
<Tag
|
||||
color={cfg?.tagColor ?? 'default'}
|
||||
style={{ borderRadius: 20, fontWeight: 600, fontSize: 11, padding: '1px 10px' }}
|
||||
>
|
||||
{label}
|
||||
</Tag>
|
||||
);
|
||||
function useOrderStats() {
|
||||
const all = useOrderList({ limit: 1 });
|
||||
const pendentes = useOrderList({ limit: 1, situa: 1 });
|
||||
const aprovados = useOrderList({ limit: 1, situa: 2 });
|
||||
const faturados = useOrderList({ limit: 1, situa: 4 });
|
||||
return {
|
||||
total: all.data?.total ?? 0,
|
||||
pendentes: pendentes.data?.total ?? 0,
|
||||
aprovados: aprovados.data?.total ?? 0,
|
||||
faturados: faturados.data?.total ?? 0,
|
||||
loaded: !!all.data,
|
||||
};
|
||||
}
|
||||
|
||||
type OrderStats = ReturnType<typeof useOrderStats>;
|
||||
|
||||
// ─── 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;
|
||||
|
||||
function OrdersMetrics({ stats }: { stats: OrderStats }) {
|
||||
const metrics = [
|
||||
{
|
||||
label: 'Total de Pedidos',
|
||||
value: String(data.length),
|
||||
value: stats.total,
|
||||
icon: <ShoppingCartOutlined />,
|
||||
color: '#003B8E',
|
||||
},
|
||||
{ label: 'Total Vendido', value: fmt(total), icon: <DollarOutlined />, color: '#389e0d' },
|
||||
{
|
||||
label: 'Ag. Aprovação',
|
||||
value: String(pendentes),
|
||||
value: stats.pendentes,
|
||||
icon: <ClockCircleOutlined />,
|
||||
color: '#d46b08',
|
||||
},
|
||||
{
|
||||
label: 'Aprovados',
|
||||
value: String(aprovados),
|
||||
icon: <CheckCircleOutlined />,
|
||||
color: '#389e0d',
|
||||
},
|
||||
{ label: 'Ticket Médio', value: fmt(ticket), icon: <DollarOutlined />, color: '#1d39c4' },
|
||||
{ label: 'Aprovados', value: stats.aprovados, icon: <CheckCircleOutlined />, color: '#389e0d' },
|
||||
{ label: 'Faturados', value: stats.faturados, icon: <DollarOutlined />, color: '#1d39c4' },
|
||||
];
|
||||
|
||||
return (
|
||||
<Row gutter={[12, 12]} style={{ marginBottom: 16 }}>
|
||||
<Row gutter={[12, 12]} style={{ marginBottom: 20 }}>
|
||||
{metrics.map((m) => (
|
||||
<Col key={m.label} xs={12} sm={8} md={6} lg={24 / metrics.length}>
|
||||
<Col key={m.label} xs={12} sm={6}>
|
||||
<Card
|
||||
style={{
|
||||
borderRadius: 10,
|
||||
border: '1px solid #EBF0F5',
|
||||
boxShadow: '0 1px 4px rgba(0,0,0,0.06)',
|
||||
boxShadow: '0 1px 4px rgba(0,0,0,0.05)',
|
||||
}}
|
||||
styles={{ body: { padding: '14px 18px' } }}
|
||||
>
|
||||
<Space size={10} align="center">
|
||||
<span style={{ fontSize: 20, color: m.color }}>{m.icon}</span>
|
||||
<div
|
||||
style={{
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 8,
|
||||
background: `${m.color}15`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: 16,
|
||||
color: m.color,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{m.icon}
|
||||
</div>
|
||||
<div>
|
||||
<Text
|
||||
style={{
|
||||
@@ -150,8 +160,8 @@ function OrdersMetrics({ data }: { data: PedidoSummary[] }) {
|
||||
>
|
||||
{m.label}
|
||||
</Text>
|
||||
<Text strong style={{ fontSize: 18, color: '#1F2937', lineHeight: 1.2 }}>
|
||||
{m.value}
|
||||
<Text strong style={{ fontSize: 20, color: '#1F2937', lineHeight: 1.2 }}>
|
||||
{stats.loaded ? m.value.toLocaleString('pt-BR') : <Spin size="small" />}
|
||||
</Text>
|
||||
</div>
|
||||
</Space>
|
||||
@@ -162,6 +172,21 @@ function OrdersMetrics({ data }: { data: PedidoSummary[] }) {
|
||||
);
|
||||
}
|
||||
|
||||
// ─── OrderStatusBadge ─────────────────────────────────────────────────────────
|
||||
|
||||
function OrderStatusBadge({ situa, descr }: { situa: number; descr?: string }) {
|
||||
const cfg = STATUS[situa];
|
||||
const label = descr ?? cfg?.label ?? SITUA_LABEL[situa] ?? String(situa);
|
||||
return (
|
||||
<Tag
|
||||
color={cfg?.tagColor ?? 'default'}
|
||||
style={{ borderRadius: 20, fontWeight: 600, fontSize: 11, padding: '1px 10px' }}
|
||||
>
|
||||
{label}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── OrderActionsMenu ─────────────────────────────────────────────────────────
|
||||
|
||||
function OrderActionsMenu({
|
||||
@@ -250,7 +275,7 @@ function OrderDetailDrawer({ id, onClose }: { id: string | null; onClose: () =>
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.08em',
|
||||
textTransform: 'uppercase',
|
||||
color: '#64748B',
|
||||
color: '#94A3B8',
|
||||
marginBottom: 2,
|
||||
display: 'block',
|
||||
};
|
||||
@@ -261,6 +286,7 @@ function OrderDetailDrawer({ id, onClose }: { id: string | null; onClose: () =>
|
||||
open={!!id}
|
||||
onClose={onClose}
|
||||
width={520}
|
||||
placement="right"
|
||||
styles={{ body: { padding: '16px 24px' } }}
|
||||
footer={
|
||||
<Space>
|
||||
@@ -296,13 +322,13 @@ function OrderDetailDrawer({ id, onClose }: { id: string | null; onClose: () =>
|
||||
<span style={label}>Data</span>
|
||||
<Text>{fmtDate(data.dtPedido)}</Text>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Col span={24}>
|
||||
<span style={label}>Cliente</span>
|
||||
<Text strong>
|
||||
<Text strong style={{ display: 'block' }}>
|
||||
{data.razaoCliente ?? data.nomeCliente ?? `Cód. ${data.idCliente}`}
|
||||
</Text>
|
||||
{data.nomeCliente && data.razaoCliente && (
|
||||
<Text type="secondary" style={{ fontSize: 12, display: 'block' }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{data.nomeCliente}
|
||||
</Text>
|
||||
)}
|
||||
@@ -313,6 +339,12 @@ function OrderDetailDrawer({ id, onClose }: { id: string | null; onClose: () =>
|
||||
{fmt(data.total)}
|
||||
</Text>
|
||||
</Col>
|
||||
{data.descontoPerc && Number(data.descontoPerc) > 0 && (
|
||||
<Col span={12}>
|
||||
<span style={label}>Desconto</span>
|
||||
<Text>{Number(data.descontoPerc).toLocaleString('pt-BR')}%</Text>
|
||||
</Col>
|
||||
)}
|
||||
{data.obs && (
|
||||
<Col span={24}>
|
||||
<span style={label}>Observações</span>
|
||||
@@ -394,6 +426,7 @@ function MobileOrderCard({
|
||||
}) {
|
||||
const navigate = useNavigate();
|
||||
const cfg = STATUS[order.situa];
|
||||
const nome = order.razaoCliente ?? order.nomeCliente;
|
||||
|
||||
return (
|
||||
<Card
|
||||
@@ -402,18 +435,22 @@ function MobileOrderCard({
|
||||
marginBottom: 10,
|
||||
border: `1px solid ${cfg?.rowBg ?? '#EBF0F5'}`,
|
||||
background: cfg?.rowBg ?? '#fff',
|
||||
boxShadow: '0 1px 4px rgba(0,0,0,0.06)',
|
||||
boxShadow: '0 1px 4px rgba(0,0,0,0.05)',
|
||||
}}
|
||||
styles={{ body: { padding: '14px 16px' } }}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
|
||||
<Text strong style={{ fontSize: 15 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
|
||||
<Text strong style={{ fontSize: 15, color: '#003B8E' }}>
|
||||
{order.numPedSar}
|
||||
</Text>
|
||||
<OrderStatusBadge situa={order.situa} descr={order.statusDescr} />
|
||||
</div>
|
||||
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>
|
||||
{order.razaoCliente ?? order.nomeCliente ?? `Cód. ${order.idCliente}`} ·{' '}
|
||||
{nome && (
|
||||
<Text style={{ fontSize: 13, fontWeight: 500, display: 'block', marginBottom: 2 }}>
|
||||
{nome}
|
||||
</Text>
|
||||
)}
|
||||
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 6 }}>
|
||||
{fmtDate(order.dtPedido)}
|
||||
</Text>
|
||||
<Text strong style={{ fontSize: 16, color: '#003B8E' }}>
|
||||
@@ -425,12 +462,14 @@ function MobileOrderCard({
|
||||
icon={<EyeOutlined />}
|
||||
disabled={order.fonte === 'erp'}
|
||||
onClick={() => onView(order.id)}
|
||||
style={{ borderRadius: 6 }}
|
||||
>
|
||||
Ver
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
style={{ borderRadius: 6 }}
|
||||
onClick={() =>
|
||||
void navigate({ to: '/pedidos/novo', search: { clientId: String(order.idCliente) } })
|
||||
}
|
||||
@@ -442,38 +481,18 @@ function MobileOrderCard({
|
||||
);
|
||||
}
|
||||
|
||||
// ─── EmptyState ───────────────────────────────────────────────────────────────
|
||||
|
||||
function EmptyOrders({ onNew }: { onNew: () => void }) {
|
||||
return (
|
||||
<Empty
|
||||
image={<ShoppingCartOutlined style={{ fontSize: 56, color: '#D9E2EC' }} />}
|
||||
imageStyle={{ height: 64 }}
|
||||
description={
|
||||
<Space direction="vertical" size={4}>
|
||||
<Text strong style={{ fontSize: 15 }}>
|
||||
Nenhum pedido encontrado
|
||||
</Text>
|
||||
<Text type="secondary">Tente alterar os filtros ou crie um novo pedido.</Text>
|
||||
</Space>
|
||||
}
|
||||
style={{ padding: '48px 0' }}
|
||||
>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={onNew}>
|
||||
Novo Pedido
|
||||
</Button>
|
||||
</Empty>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── OrdersPage ───────────────────────────────────────────────────────────────
|
||||
|
||||
export function OrdersPage() {
|
||||
const navigate = useNavigate();
|
||||
const screens = useBreakpoint();
|
||||
const isMobile = !screens.md;
|
||||
const { message: msg } = App.useApp();
|
||||
|
||||
const stats = useOrderStats();
|
||||
|
||||
const [search, setSearch] = useState('');
|
||||
const [query, setQuery] = useState('');
|
||||
const [situaFilter, setSituaFilter] = useState<number | undefined>();
|
||||
const [period, setPeriod] = useState('');
|
||||
const [page, setPage] = useState(1);
|
||||
@@ -482,8 +501,8 @@ export function OrdersPage() {
|
||||
|
||||
const { from, to } = period ? periodRange(period) : {};
|
||||
|
||||
const { data, isLoading } = useOrderList({
|
||||
numPedSar: search || undefined,
|
||||
const { data, isLoading, isFetching } = useOrderList({
|
||||
numPedSar: query || undefined,
|
||||
situa: situaFilter,
|
||||
from,
|
||||
to,
|
||||
@@ -494,28 +513,39 @@ export function OrdersPage() {
|
||||
const rows = data?.data ?? [];
|
||||
const total = data?.total ?? 0;
|
||||
|
||||
const hasFilters = !!query || !!situaFilter || !!period;
|
||||
|
||||
function commitSearch() {
|
||||
setQuery(search.trim());
|
||||
setPage(1);
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
setSearch('');
|
||||
setQuery('');
|
||||
setSituaFilter(undefined);
|
||||
setPeriod('');
|
||||
setPage(1);
|
||||
}
|
||||
|
||||
// ── Tabela desktop ─────────────────────────────────────────────────────────
|
||||
// Valor total da página atual (sem query separada)
|
||||
const valorPagina = useMemo(() => rows.reduce((a, o) => a + Number(o.total), 0), [rows]);
|
||||
|
||||
// ── Colunas desktop ─────────────────────────────────────────────────────────
|
||||
const columns: TableColumnsType<PedidoSummary> = [
|
||||
{
|
||||
title: 'Nº Pedido',
|
||||
dataIndex: 'numPedSar',
|
||||
title: 'Pedido',
|
||||
key: 'pedido',
|
||||
width: 140,
|
||||
render: (_: string, row: PedidoSummary) => {
|
||||
render: (_: unknown, row: PedidoSummary) => {
|
||||
const label = row.numero ? String(row.numero) : row.numPedSar;
|
||||
return row.fonte === 'erp' ? (
|
||||
<Text strong className="tabular-nums">
|
||||
<Text strong className="tabular-nums" style={{ color: '#1F2937' }}>
|
||||
{label}
|
||||
</Text>
|
||||
) : (
|
||||
<Link to="/pedidos/$id" params={{ id: row.id }}>
|
||||
<Text strong className="tabular-nums" style={{ color: '#0057D9' }}>
|
||||
<Text strong className="tabular-nums" style={{ color: '#003B8E' }}>
|
||||
{label}
|
||||
</Text>
|
||||
</Link>
|
||||
@@ -525,28 +555,37 @@ export function OrdersPage() {
|
||||
{
|
||||
title: 'Cliente',
|
||||
key: 'cliente',
|
||||
ellipsis: true,
|
||||
minWidth: 240,
|
||||
render: (_: unknown, row: PedidoSummary) => {
|
||||
const nome = row.razaoCliente ?? row.nomeCliente;
|
||||
const subtit = row.nomeCliente && row.razaoCliente ? row.nomeCliente : null;
|
||||
return (
|
||||
<Space direction="vertical" size={0}>
|
||||
<div>
|
||||
{nome ? (
|
||||
<Text style={{ fontWeight: 500 }}>{nome}</Text>
|
||||
<Text
|
||||
strong
|
||||
style={{ fontSize: 14, color: '#1F2937', display: 'block', lineHeight: 1.3 }}
|
||||
>
|
||||
{nome}
|
||||
</Text>
|
||||
) : (
|
||||
<Text type="secondary">Cód. {row.idCliente}</Text>
|
||||
)}
|
||||
{row.nomeCliente && row.razaoCliente && (
|
||||
<Text type="secondary" style={{ fontSize: 11 }}>
|
||||
{row.nomeCliente}
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>
|
||||
Cód. {row.idCliente}
|
||||
</Text>
|
||||
)}
|
||||
</Space>
|
||||
{subtit && (
|
||||
<Text style={{ fontSize: 12, color: '#64748B', display: 'block', lineHeight: 1.3 }}>
|
||||
{subtit}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
dataIndex: 'situa',
|
||||
key: 'status',
|
||||
width: 140,
|
||||
render: (s: number, row: PedidoSummary) => (
|
||||
<OrderStatusBadge situa={s} descr={row.statusDescr} />
|
||||
@@ -555,10 +594,11 @@ export function OrdersPage() {
|
||||
{
|
||||
title: 'Total',
|
||||
dataIndex: 'total',
|
||||
key: 'total',
|
||||
width: 130,
|
||||
align: 'right',
|
||||
align: 'right' as const,
|
||||
render: (v: string) => (
|
||||
<Text strong className="tabular-nums">
|
||||
<Text strong className="tabular-nums" style={{ color: '#003B8E', fontSize: 14 }}>
|
||||
{fmt(v)}
|
||||
</Text>
|
||||
),
|
||||
@@ -566,22 +606,34 @@ export function OrdersPage() {
|
||||
{
|
||||
title: 'Data',
|
||||
dataIndex: 'dtPedido',
|
||||
key: 'dtPedido',
|
||||
width: 110,
|
||||
render: (v: string) => <Text type="secondary">{fmtDate(v)}</Text>,
|
||||
render: (v: string) => <Text style={{ fontSize: 13, color: '#475569' }}>{fmtDate(v)}</Text>,
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
key: 'actions',
|
||||
width: 48,
|
||||
width: 100,
|
||||
render: (_: unknown, row: PedidoSummary) => (
|
||||
<OrderActionsMenu order={row} onView={(id) => setDrawerOrderId(id)} />
|
||||
<Space size={4}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<EyeOutlined />}
|
||||
type="primary"
|
||||
style={{ borderRadius: 6 }}
|
||||
title="Ver detalhes"
|
||||
disabled={row.fonte === 'erp'}
|
||||
onClick={() => setDrawerOrderId(row.id)}
|
||||
/>
|
||||
<OrderActionsMenu order={row} onView={(id) => setDrawerOrderId(id)} />
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 1200, margin: '0 auto' }}>
|
||||
{/* ── Cabeçalho ───────────────────────────────────────────────── */}
|
||||
{/* ── Cabeçalho ─────────────────────────────────────────────────── */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
@@ -595,7 +647,7 @@ export function OrdersPage() {
|
||||
Pedidos
|
||||
</Title>
|
||||
<p style={{ margin: '4px 0 0', color: '#64748B', fontSize: 14 }}>
|
||||
Acompanhe seus pedidos, status de envio e histórico comercial.
|
||||
Acompanhe seus pedidos, status de aprovação e histórico comercial.
|
||||
</p>
|
||||
</div>
|
||||
{!isMobile && (
|
||||
@@ -603,49 +655,70 @@ export function OrdersPage() {
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
size="large"
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
fontWeight: 600,
|
||||
backgroundColor: '#389e0d',
|
||||
borderColor: '#389e0d',
|
||||
}}
|
||||
onClick={() => void navigate({ to: '/pedidos/novo' })}
|
||||
style={{ borderRadius: 8, fontWeight: 600 }}
|
||||
>
|
||||
Novo Pedido
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── Métricas ────────────────────────────────────────────────── */}
|
||||
<OrdersMetrics data={rows} />
|
||||
{/* ── Métricas ──────────────────────────────────────────────────── */}
|
||||
<OrdersMetrics stats={stats} />
|
||||
|
||||
{/* ── Filtros ─────────────────────────────────────────────────── */}
|
||||
{/* ── Filtros ───────────────────────────────────────────────────── */}
|
||||
<Card
|
||||
style={{
|
||||
borderRadius: 10,
|
||||
border: '1px solid #EBF0F5',
|
||||
boxShadow: '0 1px 4px rgba(0,0,0,0.06)',
|
||||
marginBottom: 16,
|
||||
boxShadow: '0 1px 4px rgba(0,0,0,0.05)',
|
||||
marginBottom: 20,
|
||||
}}
|
||||
styles={{ body: { padding: '14px 20px' } }}
|
||||
>
|
||||
<Row gutter={[12, 12]} align="middle">
|
||||
<Col xs={24} sm={24} md={8}>
|
||||
<input
|
||||
value={search}
|
||||
onChange={(e) => {
|
||||
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',
|
||||
}}
|
||||
/>
|
||||
{/* Busca */}
|
||||
<Col xs={24} md={8}>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<SearchOutlined
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 10,
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
color: '#94A3B8',
|
||||
zIndex: 1,
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') commitSearch();
|
||||
}}
|
||||
onBlur={commitSearch}
|
||||
placeholder="Buscar por nº do pedido..."
|
||||
style={{
|
||||
width: '100%',
|
||||
height: 32,
|
||||
padding: '0 11px 0 32px',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: 6,
|
||||
fontSize: 13,
|
||||
outline: 'none',
|
||||
color: '#1F2937',
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
{/* Status */}
|
||||
<Col xs={12} sm={8} md={5}>
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
@@ -664,6 +737,8 @@ export function OrdersPage() {
|
||||
]}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
{/* Período */}
|
||||
<Col xs={12} sm={8} md={5}>
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
@@ -681,19 +756,29 @@ export function OrdersPage() {
|
||||
]}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={8} md={4}>
|
||||
|
||||
{/* Limpar */}
|
||||
<Col xs={12} sm={8} md={3}>
|
||||
<Button
|
||||
style={{ width: '100%', borderRadius: 6 }}
|
||||
icon={<ClearOutlined />}
|
||||
disabled={!hasFilters}
|
||||
onClick={clearFilters}
|
||||
disabled={!search && !situaFilter && !period}
|
||||
>
|
||||
Limpar filtros
|
||||
Limpar
|
||||
</Button>
|
||||
</Col>
|
||||
|
||||
{/* Contador */}
|
||||
<Col>
|
||||
<Text style={{ fontSize: 12, color: '#94A3B8' }}>
|
||||
{data?.total !== undefined ? `${total.toLocaleString('pt-BR')} pedidos` : '…'}
|
||||
</Text>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
{/* ── Conteúdo principal ──────────────────────────────────────── */}
|
||||
{/* ── Lista / tabela ────────────────────────────────────────────── */}
|
||||
{isLoading ? (
|
||||
<div style={{ textAlign: 'center', padding: 64 }}>
|
||||
<Spin size="large" />
|
||||
@@ -703,27 +788,51 @@ export function OrdersPage() {
|
||||
style={{ borderRadius: 10, border: '1px solid #EBF0F5' }}
|
||||
styles={{ body: { padding: 0 } }}
|
||||
>
|
||||
<EmptyOrders onNew={() => void navigate({ to: '/pedidos/novo' })} />
|
||||
<div style={{ padding: '48px 0', textAlign: 'center' }}>
|
||||
<ShoppingCartOutlined
|
||||
style={{ fontSize: 56, color: '#D9E2EC', display: 'block', marginBottom: 16 }}
|
||||
/>
|
||||
<Text strong style={{ fontSize: 15, display: 'block' }}>
|
||||
Nenhum pedido encontrado
|
||||
</Text>
|
||||
<Text type="secondary">Tente alterar os filtros ou crie um novo pedido.</Text>
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => void navigate({ to: '/pedidos/novo' })}
|
||||
style={{ borderRadius: 8 }}
|
||||
>
|
||||
Novo Pedido
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
) : isMobile ? (
|
||||
/* ── Mobile: cards ─────────────────────────────────────────── */
|
||||
/* ── Mobile ────────────────────────────────────────────────────── */
|
||||
<div>
|
||||
{rows.map((o) => (
|
||||
<MobileOrderCard key={o.id} order={o} onView={(id) => setDrawerOrderId(id)} />
|
||||
))}
|
||||
<div
|
||||
style={{ textAlign: 'center', padding: '8px 0 16px', color: '#64748B', fontSize: 13 }}
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: '#94A3B8',
|
||||
display: 'block',
|
||||
textAlign: 'center',
|
||||
padding: '8px 0 16px',
|
||||
}}
|
||||
>
|
||||
Mostrando {rows.length} de {total} pedidos
|
||||
</div>
|
||||
</Text>
|
||||
</div>
|
||||
) : (
|
||||
/* ── Desktop: tabela ────────────────────────────────────────── */
|
||||
/* ── Desktop ────────────────────────────────────────────────────── */
|
||||
<Card
|
||||
style={{
|
||||
borderRadius: 10,
|
||||
border: '1px solid #EBF0F5',
|
||||
boxShadow: '0 1px 4px rgba(0,0,0,0.06)',
|
||||
boxShadow: '0 1px 6px rgba(0,0,0,0.06)',
|
||||
}}
|
||||
styles={{ body: { padding: 0 } }}
|
||||
>
|
||||
@@ -732,6 +841,8 @@ export function OrdersPage() {
|
||||
columns={columns}
|
||||
dataSource={rows}
|
||||
size="middle"
|
||||
loading={isFetching}
|
||||
scroll={{ x: 900 }}
|
||||
onRow={(row) => ({
|
||||
onClick: () => {
|
||||
if (row.fonte !== 'erp') setDrawerOrderId(row.id);
|
||||
@@ -739,6 +850,7 @@ export function OrdersPage() {
|
||||
style: {
|
||||
background: STATUS[row.situa]?.rowBg ?? '#fff',
|
||||
cursor: row.fonte !== 'erp' ? 'pointer' : 'default',
|
||||
verticalAlign: 'top',
|
||||
},
|
||||
})}
|
||||
pagination={{
|
||||
@@ -751,11 +863,21 @@ export function OrdersPage() {
|
||||
style: { padding: '12px 24px' },
|
||||
}}
|
||||
style={{ borderRadius: 10, overflow: 'hidden' }}
|
||||
footer={() => (
|
||||
<div style={{ textAlign: 'right', padding: '4px 8px' }}>
|
||||
<Text style={{ fontSize: 12, color: '#64748B' }}>
|
||||
Valor nesta página:{' '}
|
||||
<Text strong style={{ color: '#003B8E' }}>
|
||||
{fmt(valorPagina)}
|
||||
</Text>
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* ── Drawer de detalhe ───────────────────────────────────────── */}
|
||||
{/* ── Drawer de detalhe ─────────────────────────────────────────── */}
|
||||
<OrderDetailDrawer id={drawerOrderId} onClose={() => setDrawerOrderId(null)} />
|
||||
|
||||
{/* FAB mobile */}
|
||||
@@ -784,6 +906,16 @@ export function OrdersPage() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Aviso de novo pedido via mensagem se tentativa em mobile */}
|
||||
{isMobile && (
|
||||
<Button
|
||||
style={{ display: 'none' }}
|
||||
onClick={() => void msg.info('Use o botão + para criar um novo pedido.')}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Divider style={{ display: 'none' }} />
|
||||
|
||||
<style>{`
|
||||
.ant-table-row:hover td { background: inherit !important; filter: brightness(0.97); }
|
||||
`}</style>
|
||||
|
||||
Reference in New Issue
Block a user