refactor(erp): integração direta com banco ERP — schema sar

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>
This commit is contained in:
2026-05-28 21:51:16 +00:00
parent 246eb28bb1
commit b0b60d7a14
39 changed files with 1433 additions and 1544 deletions

View File

@@ -1,8 +1,8 @@
import { useState } from 'react';
import { Badge, Input, Select, Space, Table, Tag, Tooltip, Typography } from 'antd';
import { Badge, Input, Select, Space, Table, Typography } from 'antd';
import type { TableColumnsType } from 'antd';
import { useNavigate } from '@tanstack/react-router';
import type { ActivityStatus, ClientSummary, FinancialStatus } from '@sar/api-interface';
import type { ActivityStatus, ClientSummary } from '@sar/api-interface';
import { useClientList } from '../../lib/queries/clients';
const { Title } = Typography;
@@ -16,31 +16,27 @@ const ACTIVITY_CONFIG: Record<ActivityStatus, { color: string; label: string }>
inactive: { color: 'error', label: 'Inativo' },
};
const FINANCIAL_CONFIG: Record<FinancialStatus, { color: string; label: string }> = {
regular: { color: 'success', label: 'Regular' },
attention: { color: 'warning', label: 'Atenção' },
blocked: { color: 'error', label: 'Bloqueado' },
};
// ─── Columns ──────────────────────────────────────────────────────────────────
function buildColumns(navigate: ReturnType<typeof useNavigate>): TableColumnsType<ClientSummary> {
return [
{
title: 'Cliente',
dataIndex: 'name',
key: 'name',
render: (name: string, record: ClientSummary) => (
dataIndex: 'nome',
key: 'nome',
render: (nome: string, record: ClientSummary) => (
<Space direction="vertical" size={0}>
<Typography.Link
strong
onClick={() => navigate({ to: '/clientes/$id', params: { id: record.id } })}
onClick={() =>
navigate({ to: '/clientes/$id', params: { id: String(record.idCliente) } })
}
>
{name}
{nome}
</Typography.Link>
{record.tradeName && (
{record.razao && (
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{record.tradeName}
{record.razao}
</Typography.Text>
)}
</Space>
@@ -49,12 +45,12 @@ function buildColumns(navigate: ReturnType<typeof useNavigate>): TableColumnsTyp
},
{
title: 'CNPJ / CPF',
dataIndex: 'taxId',
key: 'taxId',
dataIndex: 'cgcpf',
key: 'cgcpf',
width: 160,
render: (v: string) => (
render: (v: string | null) => (
<Typography.Text className="tabular-nums" style={{ fontSize: 13 }}>
{v}
{v ?? '—'}
</Typography.Text>
),
},
@@ -68,49 +64,20 @@ function buildColumns(navigate: ReturnType<typeof useNavigate>): TableColumnsTyp
return <Badge status={cfg.color as 'success' | 'warning' | 'error'} text={cfg.label} />;
},
},
{
title: 'Situação',
dataIndex: 'financialStatus',
key: 'financialStatus',
width: 110,
render: (v: FinancialStatus) => {
const cfg = FINANCIAL_CONFIG[v];
return <Tag color={cfg.color}>{cfg.label}</Tag>;
},
},
{
title: 'Última compra',
dataIndex: 'lastOrderAt',
key: 'lastOrderAt',
dataIndex: 'dtUltimaCompra',
key: 'dtUltimaCompra',
width: 140,
render: (v: string | null, record: ClientSummary) => {
render: (v: string | null) => {
if (!v) return <Typography.Text type="secondary"></Typography.Text>;
const date = new Date(v).toLocaleDateString('pt-BR');
const value = record.lastOrderValue
? `R$ ${Number(record.lastOrderValue).toLocaleString('pt-BR', { minimumFractionDigits: 2 })}`
: '';
return (
<Tooltip title={value}>
<Typography.Text className="tabular-nums">{date}</Typography.Text>
</Tooltip>
<Typography.Text className="tabular-nums">
{new Date(v).toLocaleDateString('pt-BR')}
</Typography.Text>
);
},
},
{
title: 'Pedidos abertos',
dataIndex: 'openOrdersCount',
key: 'openOrdersCount',
width: 120,
align: 'center',
render: (v: number) =>
v > 0 ? (
<Tag color="processing" className="tabular-nums">
{v}
</Tag>
) : (
<Typography.Text type="secondary"></Typography.Text>
),
},
];
}
@@ -141,7 +108,7 @@ export function ClientsPage() {
Carteira de Clientes
</Title>
<Typography.Text type="secondary">
{data ? `${data.total} cliente${data.total !== 1 ? 's' : ''} na sua carteira` : ' '}
{data ? `${data.total} cliente${data.total !== 1 ? 's' : ''} na sua carteira` : ' '}
</Typography.Text>
</Space>
@@ -179,7 +146,7 @@ export function ClientsPage() {
<Table<ClientSummary>
columns={columns}
dataSource={data?.data ?? []}
rowKey="id"
rowKey="idCliente"
loading={isLoading || isFetching}
pagination={{
current: page,
@@ -189,11 +156,12 @@ export function ClientsPage() {
showTotal: (total) => `${total} clientes`,
onChange: (p) => setPage(p),
}}
scroll={{ x: 900 }}
scroll={{ x: 700 }}
size="middle"
onRow={(record) => ({
style: { cursor: 'pointer' },
onClick: () => navigate({ to: '/clientes/$id', params: { id: record.id } }),
onClick: () =>
navigate({ to: '/clientes/$id', params: { id: String(record.idCliente) } }),
})}
/>
</Space>