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,25 +1,25 @@
import { useQuery } from '@tanstack/react-query';
import {
ProductListResponseSchema,
type ProductListQuery,
type ProductListResponse,
ProdutoListResponseSchema,
type ProdutoListQuery,
type ProdutoListResponse,
} from '@sar/api-interface';
import { apiFetch } from '../api-client';
export function useCatalog(params: Partial<ProductListQuery> = {}) {
export function useCatalog(params: Partial<ProdutoListQuery> = {}) {
const search = new URLSearchParams();
if (params.q) search.set('q', params.q);
if (params.category) search.set('category', params.category);
if (params.codGrupo) search.set('codGrupo', String(params.codGrupo));
if (params.page) search.set('page', String(params.page));
if (params.limit) search.set('limit', String(params.limit));
const qs = search.toString();
return useQuery<ProductListResponse>({
return useQuery<ProdutoListResponse>({
queryKey: ['catalog', params],
queryFn: async () => {
const res = await apiFetch(`/catalog${qs ? `?${qs}` : ''}`);
if (!res.ok) throw new Error(`catalog error ${res.status}`);
return ProductListResponseSchema.parse(await res.json());
return ProdutoListResponseSchema.parse(await res.json());
},
staleTime: 4 * 60 * 60 * 1000, // TTL 4h — FR-4.4
});

View File

@@ -11,14 +11,13 @@ import { apiFetch } from '../api-client';
export const CLIENT_KEYS = {
all: ['clients'] as const,
list: (params: Partial<ClientListQuery>) => ['clients', 'list', params] as const,
detail: (id: string) => ['clients', 'detail', id] as const,
detail: (id: number) => ['clients', 'detail', id] as const,
};
export function useClientList(params: Partial<ClientListQuery> = {}) {
const qs = new URLSearchParams();
if (params.q) qs.set('q', params.q);
if (params.status) qs.set('status', params.status);
if (params.financialStatus) qs.set('financialStatus', params.financialStatus);
if (params.page) qs.set('page', String(params.page));
if (params.limit) qs.set('limit', String(params.limit));
@@ -27,18 +26,20 @@ export function useClientList(params: Partial<ClientListQuery> = {}) {
return useQuery<ClientListResponse, Error>({
queryKey: CLIENT_KEYS.list(params),
queryFn: async () => {
const raw = await apiFetch(`/clients${query ? `?${query}` : ''}`);
return ClientListResponseSchema.parse(raw);
const res = await apiFetch(`/clients${query ? `?${query}` : ''}`);
if (!res.ok) throw new Error(`clients list error ${res.status}`);
return ClientListResponseSchema.parse(await res.json());
},
});
}
export function useClientDetail(id: string) {
export function useClientDetail(id: number | string | undefined) {
return useQuery<ClientDetail, Error>({
queryKey: CLIENT_KEYS.detail(id),
queryKey: CLIENT_KEYS.detail(Number(id)),
queryFn: async () => {
const raw = await apiFetch(`/clients/${id}`);
return ClientDetailSchema.parse(raw);
const res = await apiFetch(`/clients/${id}`);
if (!res.ok) throw new Error(`client detail error ${res.status}`);
return ClientDetailSchema.parse(await res.json());
},
enabled: !!id,
});

View File

@@ -1,56 +1,56 @@
import { useQuery } from '@tanstack/react-query';
import {
OrderListResponseSchema,
OrderDetailSchema,
type OrderListQuery,
type OrderListResponse,
type OrderDetail,
type OrderSummary,
PedidoListResponseSchema,
PedidoDetailSchema,
type PedidoListQuery,
type PedidoListResponse,
type PedidoDetail,
type PedidoSummary,
} from '@sar/api-interface';
import { apiFetch } from '../api-client';
export function useOrderList(params: Partial<OrderListQuery> = {}) {
export function useOrderList(params: Partial<PedidoListQuery> = {}) {
const search = new URLSearchParams();
if (params.clientId) search.set('clientId', params.clientId);
if (params.status) search.set('status', params.status);
if (params.number) search.set('number', params.number);
if (params.idCliente) search.set('idCliente', String(params.idCliente));
if (params.situa) search.set('situa', String(params.situa));
if (params.numPedSar) search.set('numPedSar', params.numPedSar);
if (params.from) search.set('from', params.from);
if (params.to) search.set('to', params.to);
if (params.page) search.set('page', String(params.page));
if (params.limit) search.set('limit', String(params.limit));
const qs = search.toString();
return useQuery<OrderListResponse>({
return useQuery<PedidoListResponse>({
queryKey: ['orders', params],
queryFn: async () => {
const res = await apiFetch(`/orders${qs ? `?${qs}` : ''}`);
if (!res.ok) throw new Error(`orders list error ${res.status}`);
return OrderListResponseSchema.parse(await res.json());
return PedidoListResponseSchema.parse(await res.json());
},
});
}
export function useOrderDetail(id: string | undefined) {
return useQuery<OrderDetail>({
return useQuery<PedidoDetail>({
queryKey: ['orders', id],
enabled: !!id,
queryFn: async () => {
const res = await apiFetch(`/orders/${id}`);
if (!res.ok) throw new Error(`order detail error ${res.status}`);
return OrderDetailSchema.parse(await res.json());
return PedidoDetailSchema.parse(await res.json());
},
});
}
export function useClientOrders(clientId: string | undefined) {
return useQuery<OrderSummary[]>({
queryKey: ['clients', clientId, 'orders'],
enabled: !!clientId,
export function useClientOrders(idCliente: number | undefined) {
return useQuery<PedidoSummary[]>({
queryKey: ['clients', idCliente, 'orders'],
enabled: idCliente != null,
queryFn: async () => {
const res = await apiFetch(`/clients/${clientId}/orders`);
const res = await apiFetch(`/orders?idCliente=${idCliente}&limit=10`);
if (!res.ok) throw new Error(`client orders error ${res.status}`);
const data = await res.json();
return data as OrderSummary[];
const data = PedidoListResponseSchema.parse(await res.json());
return data.data;
},
});
}