Revoga ADR 0006 (BD-por-workspace separado). O SAR agora conecta ao banco PostgreSQL do ERP (módulo SIG) e usa o schema `sar` para tudo. PRISMA - Remove: Client, Product, Order, OrderItem, OrderStatusHistory, RepTarget, RepDiscountLimit, PushSubscription (modelos isolados) - Adiciona: Pedido, PedidoItem, HistoricoPedido, AlcadaDesconto, MetaRepresentante, PushSubscription (mapeados para sar.*) - IDs: id_cliente/cod_vendedor/id_empresa são INTEGER (ERP) - situa: Int (1=Pendente 2=Aprovado 3=Cancelado 4=Faturado) - JWT: workspace_id:string → id_empresa:number - URL: inclui ?schema=sar para Prisma rotear ao schema ERP SERVICES - ClientsService: $queryRawUnsafe contra sar.vw_clientes + sar.pedidos - CatalogService: $queryRawUnsafe contra sar.vw_produtos + sar.vw_estoque - OrdersService: Prisma models Pedido/PedidoItem/HistoricoPedido/AlcadaDesconto - DashboardService: MetaRepresentante + queries raw para inativos - NotificationsService: PushSubscription com codVendedor + idEmpresa CONTRATOS (api-interface) - client.contract: campos ERP (idCliente, nome, cgcpf, cod_vendedor…) - order.contract: PedidoSummary/PedidoDetail/CreatePedido + SITUA_LABEL - product.contract: ProdutoSummary/ProdutoDetail (vw_produtos) - auth.contract: workspaceId:string → idEmpresa:number WEB - Todos os cockpits e queries atualizados para os novos tipos Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
138 lines
4.5 KiB
TypeScript
138 lines
4.5 KiB
TypeScript
import { Button, Descriptions, Tag, Table, Typography, Spin, Alert, Space, Divider } from 'antd';
|
|
import type { TableColumnsType } from 'antd';
|
|
import { Link, useNavigate, useParams } from '@tanstack/react-router';
|
|
import type { PedidoSummary } from '@sar/api-interface';
|
|
import { SITUA_LABEL } from '@sar/api-interface';
|
|
import { useClientDetail } from '../../lib/queries/clients';
|
|
import { useClientOrders } from '../../lib/queries/orders';
|
|
|
|
const { Title } = Typography;
|
|
|
|
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 orderColumns: TableColumnsType<PedidoSummary> = [
|
|
{
|
|
title: 'Nº',
|
|
dataIndex: 'numPedSar',
|
|
width: 120,
|
|
render: (num: string, row: PedidoSummary) => (
|
|
<Link to="/pedidos/$id" params={{ id: row.id }}>
|
|
{num}
|
|
</Link>
|
|
),
|
|
},
|
|
{
|
|
title: 'Status',
|
|
dataIndex: 'situa',
|
|
width: 140,
|
|
render: (s: number) => {
|
|
const colorMap: Record<number, string> = {
|
|
1: 'warning',
|
|
2: 'processing',
|
|
3: 'error',
|
|
4: 'success',
|
|
};
|
|
return <Tag color={colorMap[s] ?? 'default'}>{SITUA_LABEL[s] ?? String(s)}</Tag>;
|
|
},
|
|
},
|
|
{
|
|
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'),
|
|
},
|
|
];
|
|
|
|
export function ClientDetailPage() {
|
|
const { id } = useParams({ from: '/clientes/$id' });
|
|
const idNum = Number(id);
|
|
const navigate = useNavigate();
|
|
const { data: client, isLoading: clientLoading, error: clientError } = useClientDetail(idNum);
|
|
const { data: orders, isLoading: ordersLoading } = useClientOrders(idNum);
|
|
|
|
if (clientLoading) return <Spin style={{ display: 'block', marginTop: 64 }} />;
|
|
if (clientError || !client)
|
|
return <Alert type="error" message="Cliente não encontrado." style={{ margin: 24 }} />;
|
|
|
|
return (
|
|
<div style={{ padding: 24 }}>
|
|
<Space align="center" style={{ marginBottom: 16 }} wrap>
|
|
<Link to="/clientes">← Clientes</Link>
|
|
<Title level={3} style={{ margin: 0 }}>
|
|
{client.razao ?? client.nome}
|
|
</Title>
|
|
<Tag color={ACTIVITY_COLOR[client.activityStatus]}>
|
|
{ACTIVITY_LABEL[client.activityStatus]}
|
|
</Tag>
|
|
<Button
|
|
type="primary"
|
|
onClick={() => void navigate({ to: '/pedidos/novo', search: { clientId: id } })}
|
|
>
|
|
Novo Pedido
|
|
</Button>
|
|
</Space>
|
|
|
|
<Descriptions bordered size="small" column={2} style={{ marginBottom: 24 }}>
|
|
<Descriptions.Item label="Razão Social">{client.nome}</Descriptions.Item>
|
|
<Descriptions.Item label="CNPJ / CPF">{client.cgcpf ?? '—'}</Descriptions.Item>
|
|
<Descriptions.Item label="E-mail">{client.email ?? '—'}</Descriptions.Item>
|
|
<Descriptions.Item label="Telefone">
|
|
{client.ddd ? `(${client.ddd}) ` : ''}
|
|
{client.telefone ?? '—'}
|
|
</Descriptions.Item>
|
|
{client.endereco && (
|
|
<Descriptions.Item label="Endereço" span={2}>
|
|
{client.endereco}
|
|
{client.numEndereco ? `, ${client.numEndereco}` : ''}
|
|
{client.bairro ? ` — ${client.bairro}` : ''}
|
|
{client.cep ? ` — CEP ${client.cep}` : ''}
|
|
</Descriptions.Item>
|
|
)}
|
|
<Descriptions.Item label="Limite de Crédito">
|
|
{client.limiteCreditoStr
|
|
? Number(client.limiteCreditoStr).toLocaleString('pt-BR', {
|
|
style: 'currency',
|
|
currency: 'BRL',
|
|
})
|
|
: '—'}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label="Última Compra">
|
|
{client.dtUltimaCompra
|
|
? new Date(client.dtUltimaCompra).toLocaleDateString('pt-BR')
|
|
: '—'}
|
|
</Descriptions.Item>
|
|
</Descriptions>
|
|
|
|
<Divider orientation="left">Últimos Pedidos</Divider>
|
|
|
|
<Table<PedidoSummary>
|
|
rowKey="id"
|
|
columns={orderColumns}
|
|
dataSource={orders ?? []}
|
|
loading={ordersLoading}
|
|
pagination={false}
|
|
size="small"
|
|
rowClassName={(row) => (row.situa === 1 ? 'row-pending' : '')}
|
|
/>
|
|
<style>{`.row-pending td { background: #fffbe6 !important; }`}</style>
|
|
</div>
|
|
);
|
|
}
|