NewOrderPage: - Layout de página única com cards (remove wizard em steps) - AutoComplete de cliente com busca na API - Badge de confirmação ao selecionar cliente - Select de Pauta (API real) e Condição de Pagamento (mock) - Campos Contato e Nº OC - AutoComplete de produto por catálogo com pauta aplicada - Soma qty automaticamente se produto já está no carrinho - Tabela de itens com qty/desconto editáveis inline - Rodapé fixo com total e botão Finalizar verde OrdersPage: - Cards de métricas (total, vendido, pendentes, aprovados, ticket médio) - Filtros por status e período (hoje / 7d / 30d) - Tabela com row-click colorido por status - Drawer lateral com detalhes, itens e timeline de histórico - Menu de ações por linha (ver, duplicar, PDF, cancelar) - Cards mobile responsivos Layout global: - Botão Novo Pedido na Topbar (sempre visível) - FAB verde fixo (bottom-right) no AppShell Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
777 lines
25 KiB
TypeScript
777 lines
25 KiB
TypeScript
import { useState } from 'react';
|
||
import {
|
||
Button,
|
||
Card,
|
||
Col,
|
||
Drawer,
|
||
Dropdown,
|
||
Empty,
|
||
Grid,
|
||
Row,
|
||
Select,
|
||
Space,
|
||
Spin,
|
||
Table,
|
||
Tag,
|
||
Timeline,
|
||
Typography,
|
||
} from 'antd';
|
||
import type { TableColumnsType } from 'antd';
|
||
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, useOrderDetail } from '../../lib/queries/orders';
|
||
|
||
const { Title, Text } = Typography;
|
||
const { useBreakpoint } = Grid;
|
||
|
||
// ─── 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<number, { label: string; color: string; rowBg: string; tagColor: string }> = {
|
||
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' },
|
||
};
|
||
|
||
// ─── 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>
|
||
);
|
||
}
|
||
|
||
// ─── 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: <ShoppingCartOutlined />,
|
||
color: '#003B8E',
|
||
},
|
||
{ label: 'Total Vendido', value: fmt(total), icon: <DollarOutlined />, color: '#389e0d' },
|
||
{
|
||
label: 'Ag. Aprovação',
|
||
value: String(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' },
|
||
];
|
||
|
||
return (
|
||
<Row gutter={[12, 12]} style={{ marginBottom: 16 }}>
|
||
{metrics.map((m) => (
|
||
<Col key={m.label} xs={12} sm={8} md={6} lg={24 / metrics.length}>
|
||
<Card
|
||
style={{
|
||
borderRadius: 10,
|
||
border: '1px solid #EBF0F5',
|
||
boxShadow: '0 1px 4px rgba(0,0,0,0.06)',
|
||
}}
|
||
styles={{ body: { padding: '14px 18px' } }}
|
||
>
|
||
<Space size={10} align="center">
|
||
<span style={{ fontSize: 20, color: m.color }}>{m.icon}</span>
|
||
<div>
|
||
<Text
|
||
style={{
|
||
fontSize: 11,
|
||
color: '#64748B',
|
||
fontWeight: 600,
|
||
textTransform: 'uppercase',
|
||
letterSpacing: '0.06em',
|
||
display: 'block',
|
||
}}
|
||
>
|
||
{m.label}
|
||
</Text>
|
||
<Text strong style={{ fontSize: 18, color: '#1F2937', lineHeight: 1.2 }}>
|
||
{m.value}
|
||
</Text>
|
||
</div>
|
||
</Space>
|
||
</Card>
|
||
</Col>
|
||
))}
|
||
</Row>
|
||
);
|
||
}
|
||
|
||
// ─── 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: <EyeOutlined />,
|
||
label: 'Ver detalhes',
|
||
onClick: () => onView(order.id),
|
||
}
|
||
: { key: 'view', icon: <EyeOutlined />, label: 'Ver detalhes', disabled: true },
|
||
{
|
||
key: 'duplicate',
|
||
icon: <CopyOutlined style={{ color: '#0057D9' }} />,
|
||
label: <span style={{ color: '#0057D9', fontWeight: 600 }}>Duplicar pedido</span>,
|
||
onClick: () =>
|
||
void navigate({ to: '/pedidos/novo', search: { clientId: String(order.idCliente) } }),
|
||
},
|
||
{ type: 'divider' },
|
||
{
|
||
key: 'pdf',
|
||
icon: <FilePdfOutlined />,
|
||
label: 'Gerar PDF',
|
||
onClick: () => alert('PDF em breve'),
|
||
},
|
||
{
|
||
key: 'cancel',
|
||
icon: <CloseCircleOutlined />,
|
||
label: 'Cancelar pedido',
|
||
danger: true,
|
||
disabled: order.situa === 3,
|
||
onClick: () => alert('Cancelamento em breve'),
|
||
},
|
||
];
|
||
|
||
return (
|
||
<Dropdown menu={{ items }} trigger={['click']} placement="bottomRight">
|
||
<Button type="text" icon={<EllipsisOutlined />} size="small" />
|
||
</Dropdown>
|
||
);
|
||
}
|
||
|
||
// ─── 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 ? (
|
||
<CheckCircleOutlined style={{ color: '#389e0d' }} />
|
||
) : h.situaNova === 3 ? (
|
||
<CloseCircleOutlined style={{ color: '#cf1322' }} />
|
||
) : (
|
||
<ClockCircleOutlined style={{ color: '#d46b08' }} />
|
||
),
|
||
children: (
|
||
<span style={{ fontSize: 13 }}>
|
||
<Text type="secondary">{new Date(h.changedAt).toLocaleString('pt-BR')}</Text>
|
||
{' — '}
|
||
{h.situaAnterior === null ? (
|
||
<strong>Pedido criado</strong>
|
||
) : (
|
||
<>
|
||
Status alterado para{' '}
|
||
<strong>{STATUS[h.situaNova]?.label ?? SITUA_LABEL[h.situaNova] ?? h.situaNova}</strong>
|
||
</>
|
||
)}
|
||
{h.nota && <Text type="secondary"> · {h.nota}</Text>}
|
||
</span>
|
||
),
|
||
}));
|
||
|
||
const label: React.CSSProperties = {
|
||
fontSize: 11,
|
||
fontWeight: 700,
|
||
letterSpacing: '0.08em',
|
||
textTransform: 'uppercase',
|
||
color: '#64748B',
|
||
marginBottom: 2,
|
||
display: 'block',
|
||
};
|
||
|
||
return (
|
||
<Drawer
|
||
title={data ? `Pedido ${data.numPedSar}` : 'Detalhes do Pedido'}
|
||
open={!!id}
|
||
onClose={onClose}
|
||
width={520}
|
||
styles={{ body: { padding: '16px 24px' } }}
|
||
footer={
|
||
<Space>
|
||
<Button onClick={onClose}>Fechar</Button>
|
||
{data && (
|
||
<Button type="primary" onClick={() => alert('PDF em breve')}>
|
||
Gerar PDF
|
||
</Button>
|
||
)}
|
||
</Space>
|
||
}
|
||
>
|
||
{isLoading && <Spin style={{ display: 'block', marginTop: 48, textAlign: 'center' }} />}
|
||
|
||
{data && (
|
||
<Space direction="vertical" size={20} style={{ width: '100%' }}>
|
||
{/* Status */}
|
||
<div>
|
||
<OrderStatusBadge situa={data.situa} descr={data.statusDescr} />
|
||
</div>
|
||
|
||
{/* Dados principais */}
|
||
<Card
|
||
styles={{ body: { padding: '14px 16px' } }}
|
||
style={{ borderRadius: 8, background: '#F8FAFC', border: '1px solid #EBF0F5' }}
|
||
>
|
||
<Row gutter={[16, 10]}>
|
||
<Col span={12}>
|
||
<span style={label}>Pedido</span>
|
||
<Text strong>{data.numPedSar}</Text>
|
||
</Col>
|
||
<Col span={12}>
|
||
<span style={label}>Data</span>
|
||
<Text>{fmtDate(data.dtPedido)}</Text>
|
||
</Col>
|
||
<Col span={12}>
|
||
<span style={label}>Cód. Cliente</span>
|
||
<Text>{data.idCliente}</Text>
|
||
</Col>
|
||
<Col span={12}>
|
||
<span style={label}>Total</span>
|
||
<Text strong style={{ color: '#003B8E', fontSize: 16 }}>
|
||
{fmt(data.total)}
|
||
</Text>
|
||
</Col>
|
||
{data.obs && (
|
||
<Col span={24}>
|
||
<span style={label}>Observações</span>
|
||
<Text type="secondary">{data.obs}</Text>
|
||
</Col>
|
||
)}
|
||
</Row>
|
||
</Card>
|
||
|
||
{/* Itens */}
|
||
{data.itens?.length > 0 && (
|
||
<div>
|
||
<Text style={{ ...label, marginBottom: 8 }}>
|
||
Itens do Pedido ({data.itens.length})
|
||
</Text>
|
||
{data.itens.map((item) => (
|
||
<div
|
||
key={item.id}
|
||
style={{
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
padding: '8px 12px',
|
||
borderRadius: 8,
|
||
background: '#F8FAFC',
|
||
border: '1px solid #EBF0F5',
|
||
marginBottom: 6,
|
||
}}
|
||
>
|
||
<Space direction="vertical" size={0}>
|
||
<Text style={{ fontSize: 12, color: '#64748B' }}>{item.codProduto}</Text>
|
||
<Text style={{ fontWeight: 500 }}>{item.descProduto}</Text>
|
||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||
{Number(item.qtd)} un × {fmt(Number(item.precoUnitario))}
|
||
</Text>
|
||
</Space>
|
||
<Text strong className="tabular-nums">
|
||
{fmt(Number(item.total))}
|
||
</Text>
|
||
</div>
|
||
))}
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
justifyContent: 'flex-end',
|
||
marginTop: 8,
|
||
paddingTop: 8,
|
||
borderTop: '1px solid #EBF0F5',
|
||
}}
|
||
>
|
||
<Text strong style={{ fontSize: 15, color: '#003B8E' }}>
|
||
Total: {fmt(data.total)}
|
||
</Text>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Histórico */}
|
||
{timelineItems.length > 0 && (
|
||
<div>
|
||
<Text style={{ ...label, marginBottom: 10 }}>Histórico</Text>
|
||
<Timeline items={timelineItems} />
|
||
</div>
|
||
)}
|
||
</Space>
|
||
)}
|
||
</Drawer>
|
||
);
|
||
}
|
||
|
||
// ─── MobileOrderCard ──────────────────────────────────────────────────────────
|
||
|
||
function MobileOrderCard({
|
||
order,
|
||
onView,
|
||
}: {
|
||
order: PedidoSummary;
|
||
onView: (id: string) => void;
|
||
}) {
|
||
const navigate = useNavigate();
|
||
const cfg = STATUS[order.situa];
|
||
|
||
return (
|
||
<Card
|
||
style={{
|
||
borderRadius: 10,
|
||
marginBottom: 10,
|
||
border: `1px solid ${cfg?.rowBg ?? '#EBF0F5'}`,
|
||
background: cfg?.rowBg ?? '#fff',
|
||
boxShadow: '0 1px 4px rgba(0,0,0,0.06)',
|
||
}}
|
||
styles={{ body: { padding: '14px 16px' } }}
|
||
>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
|
||
<Text strong style={{ fontSize: 15 }}>
|
||
{order.numPedSar}
|
||
</Text>
|
||
<OrderStatusBadge situa={order.situa} descr={order.statusDescr} />
|
||
</div>
|
||
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>
|
||
Cód. cliente {order.idCliente} · {fmtDate(order.dtPedido)}
|
||
</Text>
|
||
<Text strong style={{ fontSize: 16, color: '#003B8E' }}>
|
||
{fmt(order.total)}
|
||
</Text>
|
||
<div style={{ marginTop: 10, display: 'flex', gap: 8 }}>
|
||
<Button
|
||
size="small"
|
||
icon={<EyeOutlined />}
|
||
disabled={order.fonte === 'erp'}
|
||
onClick={() => onView(order.id)}
|
||
>
|
||
Ver
|
||
</Button>
|
||
<Button
|
||
size="small"
|
||
icon={<CopyOutlined />}
|
||
onClick={() =>
|
||
void navigate({ to: '/pedidos/novo', search: { clientId: String(order.idCliente) } })
|
||
}
|
||
>
|
||
Duplicar
|
||
</Button>
|
||
</div>
|
||
</Card>
|
||
);
|
||
}
|
||
|
||
// ─── 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 [search, setSearch] = useState('');
|
||
const [situaFilter, setSituaFilter] = useState<number | undefined>();
|
||
const [period, setPeriod] = useState('');
|
||
const [page, setPage] = useState(1);
|
||
const [drawerOrderId, setDrawerOrderId] = useState<string | null>(null);
|
||
const limit = 20;
|
||
|
||
const { from, to } = period ? periodRange(period) : {};
|
||
|
||
const { data, isLoading } = useOrderList({
|
||
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<PedidoSummary> = [
|
||
{
|
||
title: 'Nº Pedido',
|
||
dataIndex: 'numPedSar',
|
||
width: 140,
|
||
render: (_: string, row: PedidoSummary) => {
|
||
const label = row.numero ? String(row.numero) : row.numPedSar;
|
||
return row.fonte === 'erp' ? (
|
||
<Text strong className="tabular-nums">
|
||
{label}
|
||
</Text>
|
||
) : (
|
||
<Link to="/pedidos/$id" params={{ id: row.id }}>
|
||
<Text strong className="tabular-nums" style={{ color: '#0057D9' }}>
|
||
{label}
|
||
</Text>
|
||
</Link>
|
||
);
|
||
},
|
||
},
|
||
{
|
||
title: 'Cliente',
|
||
key: 'cliente',
|
||
render: (_: unknown, row: PedidoSummary) => (
|
||
<Space direction="vertical" size={0}>
|
||
<Text style={{ fontWeight: 500 }}>Cód. {row.idCliente}</Text>
|
||
{row.obs && (
|
||
<Text type="secondary" style={{ fontSize: 11 }} ellipsis={{ tooltip: row.obs }}>
|
||
{row.obs.slice(0, 40)}
|
||
</Text>
|
||
)}
|
||
</Space>
|
||
),
|
||
},
|
||
{
|
||
title: 'Status',
|
||
dataIndex: 'situa',
|
||
width: 140,
|
||
render: (s: number, row: PedidoSummary) => (
|
||
<OrderStatusBadge situa={s} descr={row.statusDescr} />
|
||
),
|
||
},
|
||
{
|
||
title: 'Total',
|
||
dataIndex: 'total',
|
||
width: 130,
|
||
align: 'right',
|
||
render: (v: string) => (
|
||
<Text strong className="tabular-nums">
|
||
{fmt(v)}
|
||
</Text>
|
||
),
|
||
},
|
||
{
|
||
title: 'Data',
|
||
dataIndex: 'dtPedido',
|
||
width: 110,
|
||
render: (v: string) => <Text type="secondary">{fmtDate(v)}</Text>,
|
||
},
|
||
{
|
||
title: '',
|
||
key: 'actions',
|
||
width: 48,
|
||
render: (_: unknown, row: PedidoSummary) => (
|
||
<OrderActionsMenu order={row} onView={(id) => setDrawerOrderId(id)} />
|
||
),
|
||
},
|
||
];
|
||
|
||
return (
|
||
<div style={{ maxWidth: 1200, margin: '0 auto' }}>
|
||
{/* ── Cabeçalho ───────────────────────────────────────────────── */}
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'flex-start',
|
||
justifyContent: 'space-between',
|
||
marginBottom: 20,
|
||
}}
|
||
>
|
||
<div>
|
||
<Title level={3} style={{ margin: 0, color: '#003B8E' }}>
|
||
Pedidos
|
||
</Title>
|
||
<p style={{ margin: '4px 0 0', color: '#64748B', fontSize: 14 }}>
|
||
Acompanhe seus pedidos, status de envio e histórico comercial.
|
||
</p>
|
||
</div>
|
||
{!isMobile && (
|
||
<Button
|
||
type="primary"
|
||
icon={<PlusOutlined />}
|
||
size="large"
|
||
onClick={() => void navigate({ to: '/pedidos/novo' })}
|
||
style={{ borderRadius: 8, fontWeight: 600 }}
|
||
>
|
||
Novo Pedido
|
||
</Button>
|
||
)}
|
||
</div>
|
||
|
||
{/* ── Métricas ────────────────────────────────────────────────── */}
|
||
<OrdersMetrics data={rows} />
|
||
|
||
{/* ── Filtros ─────────────────────────────────────────────────── */}
|
||
<Card
|
||
style={{
|
||
borderRadius: 10,
|
||
border: '1px solid #EBF0F5',
|
||
boxShadow: '0 1px 4px rgba(0,0,0,0.06)',
|
||
marginBottom: 16,
|
||
}}
|
||
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',
|
||
}}
|
||
/>
|
||
</Col>
|
||
<Col xs={12} sm={8} md={5}>
|
||
<Select
|
||
style={{ width: '100%' }}
|
||
placeholder="Status"
|
||
allowClear
|
||
value={situaFilter}
|
||
onChange={(v) => {
|
||
setSituaFilter(v);
|
||
setPage(1);
|
||
}}
|
||
options={[
|
||
{ value: 1, label: 'Ag. Aprovação' },
|
||
{ value: 2, label: 'Aprovado' },
|
||
{ value: 3, label: 'Cancelado' },
|
||
{ value: 4, label: 'Faturado' },
|
||
]}
|
||
/>
|
||
</Col>
|
||
<Col xs={12} sm={8} md={5}>
|
||
<Select
|
||
style={{ width: '100%' }}
|
||
placeholder="Período"
|
||
allowClear
|
||
value={period || undefined}
|
||
onChange={(v) => {
|
||
setPeriod(v ?? '');
|
||
setPage(1);
|
||
}}
|
||
options={[
|
||
{ value: 'today', label: 'Hoje' },
|
||
{ value: '7d', label: 'Últimos 7 dias' },
|
||
{ value: '30d', label: 'Últimos 30 dias' },
|
||
]}
|
||
/>
|
||
</Col>
|
||
<Col xs={24} sm={8} md={4}>
|
||
<Button
|
||
style={{ width: '100%', borderRadius: 6 }}
|
||
onClick={clearFilters}
|
||
disabled={!search && !situaFilter && !period}
|
||
>
|
||
Limpar filtros
|
||
</Button>
|
||
</Col>
|
||
</Row>
|
||
</Card>
|
||
|
||
{/* ── Conteúdo principal ──────────────────────────────────────── */}
|
||
{isLoading ? (
|
||
<div style={{ textAlign: 'center', padding: 64 }}>
|
||
<Spin size="large" />
|
||
</div>
|
||
) : rows.length === 0 ? (
|
||
<Card
|
||
style={{ borderRadius: 10, border: '1px solid #EBF0F5' }}
|
||
styles={{ body: { padding: 0 } }}
|
||
>
|
||
<EmptyOrders onNew={() => void navigate({ to: '/pedidos/novo' })} />
|
||
</Card>
|
||
) : isMobile ? (
|
||
/* ── Mobile: cards ─────────────────────────────────────────── */
|
||
<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 }}
|
||
>
|
||
Mostrando {rows.length} de {total} pedidos
|
||
</div>
|
||
</div>
|
||
) : (
|
||
/* ── Desktop: tabela ────────────────────────────────────────── */
|
||
<Card
|
||
style={{
|
||
borderRadius: 10,
|
||
border: '1px solid #EBF0F5',
|
||
boxShadow: '0 1px 4px rgba(0,0,0,0.06)',
|
||
}}
|
||
styles={{ body: { padding: 0 } }}
|
||
>
|
||
<Table<PedidoSummary>
|
||
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' }}
|
||
/>
|
||
</Card>
|
||
)}
|
||
|
||
{/* ── Drawer de detalhe ───────────────────────────────────────── */}
|
||
<OrderDetailDrawer id={drawerOrderId} onClose={() => setDrawerOrderId(null)} />
|
||
|
||
{/* FAB mobile */}
|
||
{isMobile && (
|
||
<Button
|
||
type="primary"
|
||
shape="circle"
|
||
icon={<PlusOutlined />}
|
||
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',
|
||
}}
|
||
/>
|
||
)}
|
||
|
||
<style>{`
|
||
.ant-table-row:hover td { background: inherit !important; filter: brightness(0.97); }
|
||
`}</style>
|
||
</div>
|
||
);
|
||
}
|