feat(c3): consulta de pedidos — schema, api, web (OrdersModule + ClientDetailPage)
- Prisma: Order, OrderItem, OrderStatusHistory + migration - Seed: 17 pedidos em 7 clientes com itens, histórico e desnorm de clientes - @sar/api-interface: contratos Zod (OrderSummary, OrderDetail, OrderListQuery, etc.) - API: GET /orders, GET /orders/:id, GET /clients/:id/orders (últimos 10) - Web: OrdersPage (lista + filtro status/número + pending_approval highlighted) - Web: ClientDetailPage (ficha completa + últimos 10 pedidos) - Web: /pedidos e /pedidos/$id adicionados ao router; ClientDetailPage substitui placeholder Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
155
apps/web/src/cockpits/rafael/ClientDetailPage.tsx
Normal file
155
apps/web/src/cockpits/rafael/ClientDetailPage.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import { Descriptions, Tag, Table, Typography, Spin, Alert, Space, Divider } from 'antd';
|
||||
import type { TableColumnsType } from 'antd';
|
||||
import { Link, useParams } from '@tanstack/react-router';
|
||||
import type { OrderSummary, OrderStatus } from '@sar/api-interface';
|
||||
import { useClientDetail } from '../../lib/queries/clients';
|
||||
import { useClientOrders } from '../../lib/queries/orders';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
const FINANCIAL_COLOR: Record<string, string> = {
|
||||
regular: 'success',
|
||||
attention: 'warning',
|
||||
blocked: 'error',
|
||||
};
|
||||
const FINANCIAL_LABEL: Record<string, string> = {
|
||||
regular: 'Regular',
|
||||
attention: 'Atenção',
|
||||
blocked: 'Bloqueado',
|
||||
};
|
||||
const ACTIVITY_COLOR: Record<string, string> = {
|
||||
active: 'success',
|
||||
alert: 'warning',
|
||||
inactive: 'default',
|
||||
};
|
||||
const ACTIVITY_LABEL: Record<string, string> = {
|
||||
active: 'Ativo',
|
||||
alert: 'Alerta',
|
||||
inactive: 'Inativo',
|
||||
};
|
||||
const STATUS_LABEL: Record<OrderStatus, string> = {
|
||||
budget: 'Orçamento',
|
||||
pending_approval: 'Ag. Aprovação',
|
||||
approved: 'Aprovado',
|
||||
invoiced: 'Faturado',
|
||||
cancelled: 'Cancelado',
|
||||
};
|
||||
const STATUS_COLOR: Record<OrderStatus, string> = {
|
||||
budget: 'default',
|
||||
pending_approval: 'warning',
|
||||
approved: 'processing',
|
||||
invoiced: 'success',
|
||||
cancelled: 'error',
|
||||
};
|
||||
|
||||
const orderColumns: TableColumnsType<OrderSummary> = [
|
||||
{
|
||||
title: 'Nº',
|
||||
dataIndex: 'number',
|
||||
width: 120,
|
||||
render: (num: string, row: OrderSummary) => (
|
||||
<Link to="/pedidos/$id" params={{ id: row.id }}>
|
||||
{num}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
dataIndex: 'status',
|
||||
width: 140,
|
||||
render: (s: OrderStatus) => <Tag color={STATUS_COLOR[s]}>{STATUS_LABEL[s]}</Tag>,
|
||||
},
|
||||
{
|
||||
title: 'Total',
|
||||
dataIndex: 'total',
|
||||
width: 130,
|
||||
align: 'right',
|
||||
render: (v: string) =>
|
||||
Number(v).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' }),
|
||||
},
|
||||
{
|
||||
title: 'Emitido em',
|
||||
dataIndex: 'issuedAt',
|
||||
width: 130,
|
||||
render: (v: string) => new Date(v).toLocaleDateString('pt-BR'),
|
||||
},
|
||||
];
|
||||
|
||||
export function ClientDetailPage() {
|
||||
const { id } = useParams({ from: '/clientes/$id' });
|
||||
const { data: client, isLoading: clientLoading, error: clientError } = useClientDetail(id);
|
||||
const { data: orders, isLoading: ordersLoading } = useClientOrders(id);
|
||||
|
||||
if (clientLoading) return <Spin style={{ display: 'block', marginTop: 64 }} />;
|
||||
if (clientError || !client)
|
||||
return <Alert type="error" message="Cliente não encontrado." style={{ margin: 24 }} />;
|
||||
|
||||
const addr = client.address;
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24 }}>
|
||||
<Space align="center" style={{ marginBottom: 16 }}>
|
||||
<Link to="/clientes">← Clientes</Link>
|
||||
<Title level={3} style={{ margin: 0 }}>
|
||||
{client.tradeName ?? client.name}
|
||||
</Title>
|
||||
<Tag color={FINANCIAL_COLOR[client.financialStatus]}>
|
||||
{FINANCIAL_LABEL[client.financialStatus]}
|
||||
</Tag>
|
||||
<Tag color={ACTIVITY_COLOR[client.activityStatus]}>
|
||||
{ACTIVITY_LABEL[client.activityStatus]}
|
||||
</Tag>
|
||||
</Space>
|
||||
|
||||
<Descriptions bordered size="small" column={2} style={{ marginBottom: 24 }}>
|
||||
<Descriptions.Item label="Razão Social">{client.name}</Descriptions.Item>
|
||||
<Descriptions.Item label="CNPJ">{client.taxId}</Descriptions.Item>
|
||||
<Descriptions.Item label="E-mail">{client.email ?? '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="Telefone">{client.phone ?? '—'}</Descriptions.Item>
|
||||
{addr && (
|
||||
<Descriptions.Item label="Endereço" span={2}>
|
||||
{addr.street}, {addr.number}
|
||||
{addr.complement ? `, ${addr.complement}` : ''} — {addr.district}, {addr.city}/
|
||||
{addr.state} — CEP {addr.zip}
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
<Descriptions.Item label="Limite de Crédito">
|
||||
{client.creditLimit
|
||||
? Number(client.creditLimit).toLocaleString('pt-BR', {
|
||||
style: 'currency',
|
||||
currency: 'BRL',
|
||||
})
|
||||
: '—'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Pedidos em Aberto">{client.openOrdersCount}</Descriptions.Item>
|
||||
<Descriptions.Item label="Último Pedido">
|
||||
{client.lastOrderAt ? new Date(client.lastOrderAt).toLocaleDateString('pt-BR') : '—'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Valor Último Pedido">
|
||||
{client.lastOrderValue
|
||||
? Number(client.lastOrderValue).toLocaleString('pt-BR', {
|
||||
style: 'currency',
|
||||
currency: 'BRL',
|
||||
})
|
||||
: '—'}
|
||||
</Descriptions.Item>
|
||||
{client.erpCode && (
|
||||
<Descriptions.Item label="Código ERP">{client.erpCode}</Descriptions.Item>
|
||||
)}
|
||||
</Descriptions>
|
||||
|
||||
<Divider orientation="left">Últimos 10 Pedidos</Divider>
|
||||
|
||||
<Table<OrderSummary>
|
||||
rowKey="id"
|
||||
columns={orderColumns}
|
||||
dataSource={orders ?? []}
|
||||
loading={ordersLoading}
|
||||
pagination={false}
|
||||
size="small"
|
||||
rowClassName={(row) => (row.status === 'pending_approval' ? 'row-pending' : '')}
|
||||
/>
|
||||
<style>{`.row-pending td { background: #fffbe6 !important; }`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user