feat(web+api): redesign ClientsPage/OrdersPage e corrige dados empresa 9001
Web — ClientsPage: - Redesign completo: métricas reais via usePortfolioStats (4 queries count), donut Chart.js com totais reais, tabela sem ellipsis, coluna Cliente com nome fantasia/razão/CNPJ completos, drawer de detalhes e análise comercial, cards mobile, filtros de status/busca em tempo real. - Dados reais: substitui mock por useClientList/useClientDetail/useClientOrders; remove tipos fictícios (prospect/lead, cidade, totalComprado). Web — OrdersPage: - Métricas reais via useOrderStats (contagens por situa, não da página atual). - Coluna Cliente sem truncamento (minWidth: 240). - Cabeçalho, filtros e layout alinhados ao padrão da ClientsPage. API — orders.service.ts: - Normalização situa SIG→SAR: SIG usa 5=Cancelado; SAR usa 3=Cancelado. sigToSar(5→3) no mapper; sarToSig(3→5) no filtro SQL. API — clients.service.ts: - dt_ultima_compra corrigida: JOIN duplo (vw_pedidos_erp + sar.pedidos) com GREATEST() — clientes com histórico ERP mas sem pedido SAR deixam de aparecer todos como Inativo. - Filtro de activityStatus movido para SQL — total e paginação corretos. - findOne() atualizado com o mesmo JOIN duplo. Infra — .env: - DEV_EMPRESA_ID: 1 → 9001 — API aponta para dados reais da empresa SIG. Ex: pedido nº 141022 passa de R$1.765,48 para R$2.454,90. Docs — sarweb_views.sql: - Documenta as views reais em schema sar; remove schema sarweb inexistente. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,11 +13,23 @@ import type {
|
||||
import type { WorkspaceClsStore } from '../workspace/workspace.types';
|
||||
import { NotificationsService } from '../notifications/notifications.service';
|
||||
|
||||
// Situa: 1=Pendente Aprovação, 2=Aprovado, 3=Cancelado, 4=Faturado
|
||||
// Situa SAR: 1=Ag.Aprovação, 2=Aprovado, 3=Cancelado, 4=Faturado
|
||||
// Situa SIG: 1=Pendente, 2=Liberado, 5=Cancelado, 4=Faturado
|
||||
const SITUA_PENDENTE = 1;
|
||||
const SITUA_APROVADO = 2;
|
||||
const SITUA_CANCELADO = 3;
|
||||
|
||||
// Mapeia situa SIG → situa SAR para exibição correta no frontend.
|
||||
// SIG usa 5 para Cancelado; SAR usa 3. Demais valores coincidem.
|
||||
function sigToSar(sigSitua: number): number {
|
||||
return sigSitua === 5 ? 3 : sigSitua;
|
||||
}
|
||||
|
||||
// Mapeia situa SAR → situa SIG para usar nos filtros SQL contra vw_pedidos_erp.
|
||||
function sarToSig(sarSitua: number): number {
|
||||
return sarSitua === 3 ? 5 : sarSitua;
|
||||
}
|
||||
|
||||
function decimalToString(v: Prisma.Decimal | null | undefined): string {
|
||||
return v ? v.toString() : '0';
|
||||
}
|
||||
@@ -40,9 +52,14 @@ export class OrdersService {
|
||||
const { idCliente, situa, numPedSar, from, to, page, limit } = query;
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
// Filtro de vendedor: rep vê apenas seus pedidos
|
||||
const vendedorFilter = role === 'rep' ? `AND e.cod_vendedor = ${codVendedor}` : '';
|
||||
const clienteFilter = idCliente != null ? `AND e.id_cliente = ${idCliente}` : '';
|
||||
const situaFilter = situa != null ? `AND e.situa = ${situa}` : '';
|
||||
|
||||
// Converte situa SAR → SIG para filtrar corretamente contra vw_pedidos_erp
|
||||
const sigSitua = situa != null ? sarToSig(situa) : null;
|
||||
const situaFilter = sigSitua != null ? `AND e.situa = ${sigSitua}` : '';
|
||||
|
||||
const pedSarFilter = numPedSar ? `AND TRIM(e.num_ped_sar) ILIKE '%${numPedSar}%'` : '';
|
||||
const fromFilter = from ? `AND e.dt_pedido >= '${from}'` : '';
|
||||
const toFilter = to ? `AND e.dt_pedido <= '${to}'` : '';
|
||||
@@ -95,7 +112,8 @@ export class OrdersService {
|
||||
nomeCliente: o.nome_cliente ?? null,
|
||||
razaoCliente: o.razao_cliente ?? null,
|
||||
codVendedor: Number(o.cod_vendedor),
|
||||
situa: Number(o.situa),
|
||||
// Normaliza situa SIG → SAR para consistência com pedidos SAR
|
||||
situa: sigToSar(Number(o.situa)),
|
||||
statusDescr: o.status_descr,
|
||||
dtPedido: new Date(o.dt_pedido).toISOString(),
|
||||
total: o.total ?? '0',
|
||||
@@ -159,10 +177,8 @@ export class OrdersService {
|
||||
const limitMap = new Map(limitRows.map((r) => [r.codGrupo, Number(r.limitePerc)]));
|
||||
const getLimit = (codGrupo: number) => limitMap.get(codGrupo) ?? limitMap.get(0) ?? 5;
|
||||
|
||||
// Alçada global (codGrupo=0)
|
||||
const needsApproval = dto.descontoPerc > getLimit(0);
|
||||
|
||||
// Calcula totais dos itens
|
||||
const itemsData = dto.itens.map((it) => {
|
||||
const descontoValor =
|
||||
Math.round(it.qtd * it.precoUnitario * (it.descontoPerc / 100) * 100) / 100;
|
||||
@@ -241,7 +257,6 @@ export class OrdersService {
|
||||
return this.mapDetail(pedido);
|
||||
}
|
||||
|
||||
// Aprova pedido pendente. Supervisor pode ajustar descontoPerc global.
|
||||
async approve(id: string, dto: AprovarPedido): Promise<PedidoDetail> {
|
||||
const prisma = this.cls.get('prisma');
|
||||
if (!prisma) throw new Error('prisma não disponível no CLS');
|
||||
@@ -300,7 +315,6 @@ export class OrdersService {
|
||||
return this.mapDetail(final);
|
||||
}
|
||||
|
||||
// Recusa pedido — muda situa para 3 (Cancelado) com motivo.
|
||||
async reject(id: string, dto: RecusarPedido): Promise<PedidoDetail> {
|
||||
const prisma = this.cls.get('prisma');
|
||||
if (!prisma) throw new Error('prisma não disponível no CLS');
|
||||
|
||||
Reference in New Issue
Block a user