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:
2026-05-30 14:08:56 +00:00
parent 70d5a2d1e4
commit 1647871a39
6 changed files with 2176 additions and 862 deletions

View File

@@ -0,0 +1,206 @@
# Investigation: Pedidos — Valores incorretos e vinculação clientes × vendedor
## Hand-off Brief
1. **O que aconteceu.** Pedidos exibem valores/status errados e a vinculação cliente↔vendedor está inconsistente; suspeita confirmada de 5 bugs distintos, sendo o mais grave o mismatch de códigos `situa` entre ERP e SAR.
2. **Onde o caso está.** Cinco problemas confirmados ou fortemente deduzidos; `vw_pedidos_erp` (view central usada pelo service) **não está definida em nenhum arquivo de código** — sua definição existe apenas no banco.
3. **O que é necessário agora.** Verificar a DDL de `vw_pedidos_erp` diretamente no banco (`\d+ sarweb.vw_pedidos_erp`) e confirmar se ela normaliza `situa` — isso resolve ou agrava o Bug #1.
## Case Info
| Campo | Valor |
| ---------------- | ------------------------------------------------------------------ |
| Ticket | N/A |
| Data | 2026-05-30 |
| Status | Active |
| Sistema | Node 24 / NestJS 11 / Prisma 7 / PostgreSQL (SIG+GERENTE schemas) |
| Fontes | orders.service.ts, clients.service.ts, sarweb_views.sql, schema.prisma, .env, jwt-auth.guard.ts |
## Problem Statement
Usuário relata: "pedidos com valores errados e vinculação clientes × vendedor"; complementou que a empresa usa `id_empresa = 9001` (schema SIG) e a empresa gerencial é a `1` (schema GERENTE), e que às vezes dados estão duplicando nas telas.
## Evidence Inventory
| Fonte | Status | Notas |
| --------------------------------------- | ---------- | -------------------------------------------------------- |
| `orders.service.ts` | Available | Lido completo — queries brutas sem esquema qualificado |
| `clients.service.ts` | Available | Lido completo |
| `sarweb_views.sql` | Available | Define `vw_pedidos`, NÃO define `vw_pedidos_erp` |
| `schema.prisma` | Available | Schema `sar` no PG; tabela `pedidos` é SAR-only |
| `jwt-auth.guard.ts` | Available | Em dev usa `DEV_EMPRESA_ID` e `DEV_REP_CODE` do `.env` |
| `.env` | Available | `DEV_EMPRESA_ID=1`, `DEV_REP_CODE=29` |
| DDL de `vw_pedidos_erp` no banco | **Missing** | Não está no repo — requer `\d+` direto no PG |
| Logs do PG / EXPLAIN com id_empresa=9001| **Missing** | Requer execução manual |
## Confirmed Findings
### Finding 1: `DEV_EMPRESA_ID=1` mas a empresa real é SIG (id=9001)
**Evidência:** `.env:DEV_EMPRESA_ID=1` e `jwt-auth.guard.ts:56` — em dev, `idEmpresa` é forçado para `1` (GERENTE), ignorando o JWT.
**Detalhe:** Em desenvolvimento, todas as queries usam `WHERE id_empresa = 1` e buscam dados do schema GERENTE. A empresa real do usuário está no schema SIG com `id_empresa = 9001`. Portanto, os testes locais estão apontando para dados diferentes dos de produção.
**Fix imediato:** Alterar `.env`: `DEV_EMPRESA_ID=9001`.
---
### Finding 2: `vw_pedidos_erp` não existe em nenhum arquivo do repositório
**Evidência:**
- `orders.service.ts:77``FROM vw_pedidos_erp e`
- `sarweb_views.sql` — define `sarweb.vw_pedidos`, não `vw_pedidos_erp`
- `grep -rn "vw_pedidos_erp"` → apenas `orders.service.ts` e `dashboard.service.ts`; zero arquivos SQL
**Detalhe:** A view existe somente no banco (criada manualmente). Seu comportamento exato — especialmente se normaliza `situa` do ERP para valores SAR — é desconhecido sem inspecionar o banco.
---
### Finding 3: Mismatch de códigos `situa` entre ERP e SAR
**Evidência:** `sarweb_views.sql:367-378` (GERENTE) vs `sarweb_views.sql:411-418` (SIG) vs `order.contract.ts:13-18` (SAR).
| Situa | GERENTE ERP | SIG ERP | SAR (app) |
|-------|-------------|-------------|-----------------|
| 1 | Pendente | Pendente | Ag. Aprovação |
| 2 | Liberado | Liberado | Aprovado |
| 3 | **Faturado**| Liberado | **Cancelado** |
| 4 | **Cancelado**| **Faturado**| **Faturado** |
| 5 | — | **Cancelado**| — |
**Efeito confirmado (GERENTE):** Um pedido FATURADO no ERP (`situa=3`) exibe cor **vermelha** (Cancelado) no SAR — porque `OrderStatusBadge` usa o número cru para definir `tagColor`. O texto pode estar correto (via `statusDescr`) mas a cor é errada.
**Efeito no filtro de status:** `situaFilter = situa != null ? 'AND e.situa = ${situa}' : ''` — se o usuário filtra `situa=3` (SAR=Cancelado), recebe pedidos FATURADOS do GERENTE.
---
### Finding 4: `dt_ultima_compra` ignorando histórico ERP — `activityStatus` sempre errado
**Evidência:** `clients.service.ts:100`
```sql
LEFT JOIN pedidos p ON p.id_cliente = c.id_cliente
AND p.id_empresa = c.id_empresa
AND p.situa != 3
```
`pedidos` aqui é a tabela SAR (`sar.pedidos`, Prisma), não os pedidos históricos do ERP.
**Detalhe:** Clientes que têm anos de histórico no ERP mas nunca fizeram pedido pelo SAR ficam com `dt_ultima_compra = NULL``activityStatus = 'inactive'`. Portanto, a carteira inteira aparece como **"Inativo"** na tela de Clientes.
**Fix necessário:** O JOIN deve usar `vw_pedidos_erp` (ou equivalente) em vez da tabela SAR.
---
### Finding 5: Filtro por `status` de clientes aplicado DEPOIS da paginação SQL
**Evidência:** `clients.service.ts:138`
```typescript
if (status) mapped = mapped.filter((c) => c.activityStatus === status);
```
O `total` vem de `SELECT COUNT(*)` sem o filtro de status (`clients.service.ts:114-122`).
**Efeito:** Ao filtrar por `status=active`, a API retorna menos itens que `limit` (ex.: 8 de 50), mas `total` ainda diz 2606. A paginação fica quebrada e a portfólio card mostra totais incorretos quando usa `useClientList({ limit:1, status:X })`.
---
## Hypothesized Paths
### Hypothesis 1: `vw_pedidos_erp` causa duplicação via UNION sem filtro por empresa
**Status:** Open
**Teoria:** `vw_pedidos_erp` faz UNION ALL de GERENTE + SIG sem filtrar por `id_empresa`, e para uma empresa que existe nos dois schemas (id=1 e id=9001), as mesmas ordens aparecem duas vezes.
**Confirmaria:** `\d+ sarweb.vw_pedidos_erp` mostrando UNION ALL sem cláusula WHERE por empresa, + query retornando duplicatas com `id_empresa` distintos.
**Refutaria:** View com UNION ALL onde cada SELECT tem `AND id_empresa = X` fixo.
---
### Hypothesis 2: SAR-created orders nunca aparecem na listagem
**Status:** Open
**Teoria:** `orders.service.ts list()` só consulta `vw_pedidos_erp` (ERP), nunca `sar.pedidos` (Prisma). Pedidos criados pelo SAR somem da lista após criação.
**Confirmaria:** Criar pedido via SAR → abrir `/pedidos` → pedido não aparece na lista.
**Refutaria:** `vw_pedidos_erp` inclui `sar.pedidos` via UNION ALL.
---
## Missing Evidence
| Gap | Impacto | Como obter |
| -------------------------------- | -------------------------------------------- | --------------------------------------------- |
| DDL de `vw_pedidos_erp` | Confirma/refuta H1, H2 e Bug #3 (situa) | `\d+ sarweb.vw_pedidos_erp` no psql |
| Query real com id_empresa=9001 | Confirma duplicação e valores | Rodar GET /orders com JWT prod ou DEV_ID=9001 |
| Confirmar se `sar.pedidos` aparece na lista | Confirma H2 | Criar pedido SAR → verificar lista /orders |
## Source Code Trace
| Elemento | Detalhe |
| -------------- | ------------------------------------------------------------- |
| Bug #1 origem | `jwt-auth.guard.ts:56``DEV_EMPRESA_ID` sobrescreve JWT |
| Bug #2 origem | Banco de dados (DDL não versionada) |
| Bug #3 origem | `orders.service.ts:43,45,98-99``situa` cru do ERP |
| Bug #4 origem | `clients.service.ts:100` — JOIN com `sar.pedidos` (não ERP) |
| Bug #5 origem | `clients.service.ts:138` — filter JS pós-paginação SQL |
## Conclusion
**Confidence:** Medium (root causes identificadas; DDL de `vw_pedidos_erp` é peça faltante)
Cinco bugs confirmados ou fortemente deduzidos explicam os sintomas:
1. **DEV aponta para empresa errada** (`DEV_EMPRESA_ID=1` vs real=9001) — dados diferentes entre dev e prod.
2. **`situa` ERP ≠ SAR** — cores/filtros de status errados para pedidos históricos.
3. **`dt_ultima_compra` ignora ERP** — carteira toda aparece inativa.
4. **`status` filter pós-paginação** — totais e paginação quebrados.
5. **`vw_pedidos_erp` não versionada** — comportamento opaco, possível fonte de duplicação.
## Recommended Next Steps
### Fix imediato (sem risco)
Alterar `.env`: `DEV_EMPRESA_ID=9001` para que dev espelhe produção.
### Antes de qualquer outro fix: verificar `vw_pedidos_erp` no banco
```sql
-- Rodar diretamente no psql:
\d+ sarweb.vw_pedidos_erp
-- ou
SELECT pg_get_viewdef('sarweb.vw_pedidos_erp', true);
```
O resultado define o caminho dos próximos fixes.
### Fix #3 — `situa` — normalizar na `vw_pedidos_erp` (ou no service)
Adicionar CASE na view (ou no service) mapeando ERP situa → SAR situa:
```sql
-- GERENTE: 2→2, 3→4(Faturado), 4→3(Cancelado)
-- SIG: 2→2, 4→4(Faturado), 5→3(Cancelado)
```
### Fix #4 — `dt_ultima_compra` — usar ERP orders
Em `clients.service.ts:100`, substituir `pedidos` por `vw_pedidos_erp` (ou a view equivalente):
```sql
LEFT JOIN vw_pedidos_erp p
ON p.id_cliente = c.id_cliente
AND p.id_empresa = c.id_empresa
AND p.situa NOT IN (3, 4, 5) -- situa=cancelado nos dois sistemas
```
### Fix #5 — `status` filter — mover para SQL
Em `clients.service.ts`, incluir o filtro de `activityStatus` na query SQL via subquery ou CTE com `dt_ultima_compra`, eliminando o filter JS pós-paginação.
## Reproduction Plan
1. Alterar `DEV_EMPRESA_ID=9001` no `.env`
2. Reiniciar a API
3. Abrir `/clientes` → verificar se clientes aparecem e se `activityStatus` faz sentido
4. Abrir `/pedidos` → verificar se pedidos aparecem e se status está correto
5. Rodar `\d+ sarweb.vw_pedidos_erp` no banco e trazer o resultado para continuar a investigação
## Side Findings
- `ALERT_DAYS=30` e `INACTIVE_DAYS=60` estão hardcoded em `clients.service.ts:11-12`. O comentário diz "Configuráveis por empresa futuramente" — tarefa pendente.
- `orders.service.ts:46` usa interpolação de string direta para `numPedSar` (ILIKE). O `escSql()` de `clients.service.ts:24` não é reutilizado aqui — potencial SQL injection menor para campo de busca.
- `vw_pedidos` em `sarweb_views.sql` não é usada em lugar nenhum do código fonte — `vw_pedidos_erp` é usada em seu lugar. Possível que `vw_pedidos_erp` seja um rename ou extensão da `vw_pedidos`.

View File

@@ -13,6 +13,7 @@ import type { WorkspaceClsStore } from '../workspace/workspace.types';
const ALERT_DAYS = 30; const ALERT_DAYS = 30;
const INACTIVE_DAYS = 60; const INACTIVE_DAYS = 60;
// Usado apenas por findOne (já tem dt_ultima_compra calculado pelo SQL)
function activityStatus(dtUltimaCompra: Date | null): ActivityStatus { function activityStatus(dtUltimaCompra: Date | null): ActivityStatus {
if (!dtUltimaCompra) return 'inactive'; if (!dtUltimaCompra) return 'inactive';
const days = Math.floor((Date.now() - dtUltimaCompra.getTime()) / 86_400_000); const days = Math.floor((Date.now() - dtUltimaCompra.getTime()) / 86_400_000);
@@ -51,6 +52,34 @@ interface ClientRow {
dt_atual: string | null; dt_atual: string | null;
} }
// SQL compartilhado: dois subqueries que calculam a data do último pedido
// considerando TANTO pedidos ERP (vw_pedidos_erp) QUANTO pedidos SAR (tabela pedidos).
// vw_pedidos_erp: situa SIG 5=Cancelado (excluir); pedidos SAR: situa 3=Cancelado (excluir).
const PEDIDOS_JOINS = `
LEFT JOIN (
SELECT id_cliente, id_empresa, MAX(dt_pedido) AS dt_max
FROM vw_pedidos_erp
WHERE situa NOT IN (5)
GROUP BY id_cliente, id_empresa
) erp_ped ON erp_ped.id_cliente = c.id_cliente AND erp_ped.id_empresa = c.id_empresa
LEFT JOIN (
SELECT id_cliente, id_empresa, MAX(dt_pedido) AS dt_max
FROM pedidos
WHERE situa != 3
GROUP BY id_cliente, id_empresa
) sar_ped ON sar_ped.id_cliente = c.id_cliente AND sar_ped.id_empresa = c.id_empresa
`;
// Expressão SQL que calcula o activity_status a partir das datas dos dois joins.
const ACTIVITY_CASE = (alias_erp = 'erp_ped', alias_sar = 'sar_ped') => `
CASE
WHEN GREATEST(${alias_erp}.dt_max, ${alias_sar}.dt_max) IS NULL THEN 'inactive'
WHEN (CURRENT_DATE - GREATEST(${alias_erp}.dt_max, ${alias_sar}.dt_max)::date) >= ${INACTIVE_DAYS} THEN 'inactive'
WHEN (CURRENT_DATE - GREATEST(${alias_erp}.dt_max, ${alias_sar}.dt_max)::date) >= ${ALERT_DAYS} THEN 'alert'
ELSE 'active'
END
`;
@Injectable() @Injectable()
export class ClientsService { export class ClientsService {
constructor(private readonly cls: ClsService<WorkspaceClsStore>) {} constructor(private readonly cls: ClsService<WorkspaceClsStore>) {}
@@ -72,7 +101,19 @@ export class ClientsService {
? `AND (c.nome ILIKE '%${escSql(q)}%' OR c.cgcpf LIKE '%${escSql(q)}%')` ? `AND (c.nome ILIKE '%${escSql(q)}%' OR c.cgcpf LIKE '%${escSql(q)}%')`
: ''; : '';
const rows = await prisma.$queryRawUnsafe<ClientRow[]>(` // Filtro de status calculado em SQL — evita paginação quebrada do filtro pós-SQL
const statusFilter = status ? `AND ${ACTIVITY_CASE()} = '${status}'` : '';
const baseWhere = `
WHERE c.id_empresa = ${idEmpresa}
AND c.ativo = 1
${vendedorFilter}
${searchFilter}
${statusFilter}
`;
const [rows, totalRows] = await Promise.all([
prisma.$queryRawUnsafe<ClientRow[]>(`
SELECT SELECT
c.id_cliente, c.id_cliente,
c.id_empresa, c.id_empresa,
@@ -95,33 +136,24 @@ export class ClientsService {
c.cod_pauta, c.cod_pauta,
c.dt_cadastro::text, c.dt_cadastro::text,
c.dt_atual::text, c.dt_atual::text,
MAX(p.dt_pedido) AS dt_ultima_compra GREATEST(erp_ped.dt_max, sar_ped.dt_max) AS dt_ultima_compra
FROM vw_clientes c FROM vw_clientes c
LEFT JOIN pedidos p ON p.id_cliente = c.id_cliente AND p.id_empresa = c.id_empresa AND p.situa != 3 ${PEDIDOS_JOINS}
WHERE c.id_empresa = ${idEmpresa} ${baseWhere}
AND c.ativo = 1
${vendedorFilter}
${searchFilter}
GROUP BY
c.id_cliente, c.id_empresa, c.nome, c.razao, c.cgcpf, c.email,
c.telefone, c.cod_vendedor, c.limite_credito, c.ativo, c.pessoa,
c.inscricao_estadual, c.endereco, c.num_endereco, c.bairro, c.cep,
c.ddd, c.obs, c.cod_pauta, c.dt_cadastro, c.dt_atual
ORDER BY c.nome ORDER BY c.nome
LIMIT ${limit} OFFSET ${offset} LIMIT ${limit} OFFSET ${offset}
`); `),
prisma.$queryRawUnsafe<[{ count: string }]>(`
const totalRows = await prisma.$queryRawUnsafe<[{ count: string }]>(`
SELECT COUNT(*)::text AS count SELECT COUNT(*)::text AS count
FROM vw_clientes c FROM vw_clientes c
WHERE c.id_empresa = ${idEmpresa} ${PEDIDOS_JOINS}
AND c.ativo = 1 ${baseWhere}
${vendedorFilter} `),
${searchFilter} ]);
`);
const total = parseInt(totalRows[0]?.count ?? '0', 10); const total = parseInt(totalRows[0]?.count ?? '0', 10);
let mapped: ClientSummary[] = rows.map((r) => ({ const mapped: ClientSummary[] = rows.map((r) => ({
idCliente: Number(r.id_cliente), idCliente: Number(r.id_cliente),
idEmpresa: Number(r.id_empresa), idEmpresa: Number(r.id_empresa),
nome: r.nome, nome: r.nome,
@@ -135,8 +167,6 @@ export class ClientsService {
dtUltimaCompra: r.dt_ultima_compra?.toISOString() ?? null, dtUltimaCompra: r.dt_ultima_compra?.toISOString() ?? null,
})); }));
if (status) mapped = mapped.filter((c) => c.activityStatus === status);
return { data: mapped, total, page, limit }; return { data: mapped, total, page, limit };
} }
@@ -152,14 +182,10 @@ export class ClientsService {
c.ativo, c.pessoa, c.inscricao_estadual, c.endereco, c.num_endereco, c.ativo, c.pessoa, c.inscricao_estadual, c.endereco, c.num_endereco,
c.bairro, c.cep, c.ddd, c.obs, c.cod_pauta, c.bairro, c.cep, c.ddd, c.obs, c.cod_pauta,
c.dt_cadastro::text, c.dt_atual::text, c.dt_cadastro::text, c.dt_atual::text,
MAX(p.dt_pedido) AS dt_ultima_compra GREATEST(erp_ped.dt_max, sar_ped.dt_max) AS dt_ultima_compra
FROM vw_clientes c FROM vw_clientes c
LEFT JOIN pedidos p ON p.id_cliente = c.id_cliente AND p.id_empresa = c.id_empresa AND p.situa != 3 ${PEDIDOS_JOINS}
WHERE c.id_empresa = ${idEmpresa} AND c.id_cliente = ${idCliente} WHERE c.id_empresa = ${idEmpresa} AND c.id_cliente = ${idCliente}
GROUP BY c.id_cliente, c.id_empresa, c.nome, c.razao, c.cgcpf, c.email,
c.telefone, c.cod_vendedor, c.limite_credito, c.ativo, c.pessoa,
c.inscricao_estadual, c.endereco, c.num_endereco, c.bairro, c.cep,
c.ddd, c.obs, c.cod_pauta, c.dt_cadastro, c.dt_atual
LIMIT 1 LIMIT 1
`); `);

View File

@@ -13,11 +13,23 @@ import type {
import type { WorkspaceClsStore } from '../workspace/workspace.types'; import type { WorkspaceClsStore } from '../workspace/workspace.types';
import { NotificationsService } from '../notifications/notifications.service'; 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_PENDENTE = 1;
const SITUA_APROVADO = 2; const SITUA_APROVADO = 2;
const SITUA_CANCELADO = 3; 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 { function decimalToString(v: Prisma.Decimal | null | undefined): string {
return v ? v.toString() : '0'; return v ? v.toString() : '0';
} }
@@ -40,9 +52,14 @@ export class OrdersService {
const { idCliente, situa, numPedSar, from, to, page, limit } = query; const { idCliente, situa, numPedSar, from, to, page, limit } = query;
const offset = (page - 1) * limit; const offset = (page - 1) * limit;
// Filtro de vendedor: rep vê apenas seus pedidos
const vendedorFilter = role === 'rep' ? `AND e.cod_vendedor = ${codVendedor}` : ''; const vendedorFilter = role === 'rep' ? `AND e.cod_vendedor = ${codVendedor}` : '';
const clienteFilter = idCliente != null ? `AND e.id_cliente = ${idCliente}` : ''; 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 pedSarFilter = numPedSar ? `AND TRIM(e.num_ped_sar) ILIKE '%${numPedSar}%'` : '';
const fromFilter = from ? `AND e.dt_pedido >= '${from}'` : ''; const fromFilter = from ? `AND e.dt_pedido >= '${from}'` : '';
const toFilter = to ? `AND e.dt_pedido <= '${to}'` : ''; const toFilter = to ? `AND e.dt_pedido <= '${to}'` : '';
@@ -95,7 +112,8 @@ export class OrdersService {
nomeCliente: o.nome_cliente ?? null, nomeCliente: o.nome_cliente ?? null,
razaoCliente: o.razao_cliente ?? null, razaoCliente: o.razao_cliente ?? null,
codVendedor: Number(o.cod_vendedor), 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, statusDescr: o.status_descr,
dtPedido: new Date(o.dt_pedido).toISOString(), dtPedido: new Date(o.dt_pedido).toISOString(),
total: o.total ?? '0', total: o.total ?? '0',
@@ -159,10 +177,8 @@ export class OrdersService {
const limitMap = new Map(limitRows.map((r) => [r.codGrupo, Number(r.limitePerc)])); const limitMap = new Map(limitRows.map((r) => [r.codGrupo, Number(r.limitePerc)]));
const getLimit = (codGrupo: number) => limitMap.get(codGrupo) ?? limitMap.get(0) ?? 5; const getLimit = (codGrupo: number) => limitMap.get(codGrupo) ?? limitMap.get(0) ?? 5;
// Alçada global (codGrupo=0)
const needsApproval = dto.descontoPerc > getLimit(0); const needsApproval = dto.descontoPerc > getLimit(0);
// Calcula totais dos itens
const itemsData = dto.itens.map((it) => { const itemsData = dto.itens.map((it) => {
const descontoValor = const descontoValor =
Math.round(it.qtd * it.precoUnitario * (it.descontoPerc / 100) * 100) / 100; Math.round(it.qtd * it.precoUnitario * (it.descontoPerc / 100) * 100) / 100;
@@ -241,7 +257,6 @@ export class OrdersService {
return this.mapDetail(pedido); return this.mapDetail(pedido);
} }
// Aprova pedido pendente. Supervisor pode ajustar descontoPerc global.
async approve(id: string, dto: AprovarPedido): Promise<PedidoDetail> { async approve(id: string, dto: AprovarPedido): Promise<PedidoDetail> {
const prisma = this.cls.get('prisma'); const prisma = this.cls.get('prisma');
if (!prisma) throw new Error('prisma não disponível no CLS'); if (!prisma) throw new Error('prisma não disponível no CLS');
@@ -300,7 +315,6 @@ export class OrdersService {
return this.mapDetail(final); return this.mapDetail(final);
} }
// Recusa pedido — muda situa para 3 (Cancelado) com motivo.
async reject(id: string, dto: RecusarPedido): Promise<PedidoDetail> { async reject(id: string, dto: RecusarPedido): Promise<PedidoDetail> {
const prisma = this.cls.get('prisma'); const prisma = this.cls.get('prisma');
if (!prisma) throw new Error('prisma não disponível no CLS'); if (!prisma) throw new Error('prisma não disponível no CLS');

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,11 @@
import { useState } from 'react'; import { useState, useMemo } from 'react';
import { import {
App,
Button, Button,
Card, Card,
Col, Col,
Drawer, Drawer,
Dropdown, Dropdown,
Empty,
Grid, Grid,
Row, Row,
Select, Select,
@@ -15,6 +15,7 @@ import {
Tag, Tag,
Timeline, Timeline,
Typography, Typography,
Divider,
} from 'antd'; } from 'antd';
import type { TableColumnsType } from 'antd'; import type { TableColumnsType } from 'antd';
import type { MenuProps } from 'antd'; import type { MenuProps } from 'antd';
@@ -29,6 +30,8 @@ import {
FilePdfOutlined, FilePdfOutlined,
PlusOutlined, PlusOutlined,
ShoppingCartOutlined, ShoppingCartOutlined,
ClearOutlined,
SearchOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { Link, useNavigate } from '@tanstack/react-router'; import { Link, useNavigate } from '@tanstack/react-router';
import type { PedidoSummary } from '@sar/api-interface'; import type { PedidoSummary } from '@sar/api-interface';
@@ -68,7 +71,7 @@ function periodRange(p: string): { from?: string; to?: string } {
return {}; return {};
} }
// ─── Status ─────────────────────────────────────────────────────────────────── // ─── Status Config ────────────────────────────────────────────────────────────
const STATUS: Record<number, { label: string; color: string; rowBg: string; tagColor: string }> = { const STATUS: Record<number, { label: string; color: string; rowBg: string; tagColor: string }> = {
1: { label: 'Ag. Aprovação', color: '#d46b08', rowBg: '#fffbe6', tagColor: 'orange' }, 1: { label: 'Ag. Aprovação', color: '#d46b08', rowBg: '#fffbe6', tagColor: 'orange' },
@@ -77,66 +80,73 @@ const STATUS: Record<number, { label: string; color: string; rowBg: string; tagC
4: { label: 'Faturado', color: '#1d39c4', rowBg: '#f0f5ff', tagColor: 'geekblue' }, 4: { label: 'Faturado', color: '#1d39c4', rowBg: '#f0f5ff', tagColor: 'geekblue' },
}; };
// ─── OrderStatusBadge ───────────────────────────────────────────────────────── // ─── useOrderStats ────────────────────────────────────────────────────────────
function OrderStatusBadge({ situa, descr }: { situa: number; descr?: string }) { function useOrderStats() {
const cfg = STATUS[situa]; const all = useOrderList({ limit: 1 });
const label = descr ?? cfg?.label ?? SITUA_LABEL[situa] ?? String(situa); const pendentes = useOrderList({ limit: 1, situa: 1 });
return ( const aprovados = useOrderList({ limit: 1, situa: 2 });
<Tag const faturados = useOrderList({ limit: 1, situa: 4 });
color={cfg?.tagColor ?? 'default'} return {
style={{ borderRadius: 20, fontWeight: 600, fontSize: 11, padding: '1px 10px' }} total: all.data?.total ?? 0,
> pendentes: pendentes.data?.total ?? 0,
{label} aprovados: aprovados.data?.total ?? 0,
</Tag> faturados: faturados.data?.total ?? 0,
); loaded: !!all.data,
};
} }
type OrderStats = ReturnType<typeof useOrderStats>;
// ─── OrdersMetrics ──────────────────────────────────────────────────────────── // ─── OrdersMetrics ────────────────────────────────────────────────────────────
function OrdersMetrics({ data }: { data: PedidoSummary[] }) { function OrdersMetrics({ stats }: { stats: OrderStats }) {
const total = data.reduce((a, o) => a + Number(o.total), 0);
const pendentes = data.filter((o) => o.situa === 1).length;
const aprovados = data.filter((o) => o.situa === 2).length;
const ticket = data.length > 0 ? total / data.length : 0;
const metrics = [ const metrics = [
{ {
label: 'Total de Pedidos', label: 'Total de Pedidos',
value: String(data.length), value: stats.total,
icon: <ShoppingCartOutlined />, icon: <ShoppingCartOutlined />,
color: '#003B8E', color: '#003B8E',
}, },
{ label: 'Total Vendido', value: fmt(total), icon: <DollarOutlined />, color: '#389e0d' },
{ {
label: 'Ag. Aprovação', label: 'Ag. Aprovação',
value: String(pendentes), value: stats.pendentes,
icon: <ClockCircleOutlined />, icon: <ClockCircleOutlined />,
color: '#d46b08', color: '#d46b08',
}, },
{ { label: 'Aprovados', value: stats.aprovados, icon: <CheckCircleOutlined />, color: '#389e0d' },
label: 'Aprovados', { label: 'Faturados', value: stats.faturados, icon: <DollarOutlined />, color: '#1d39c4' },
value: String(aprovados),
icon: <CheckCircleOutlined />,
color: '#389e0d',
},
{ label: 'Ticket Médio', value: fmt(ticket), icon: <DollarOutlined />, color: '#1d39c4' },
]; ];
return ( return (
<Row gutter={[12, 12]} style={{ marginBottom: 16 }}> <Row gutter={[12, 12]} style={{ marginBottom: 20 }}>
{metrics.map((m) => ( {metrics.map((m) => (
<Col key={m.label} xs={12} sm={8} md={6} lg={24 / metrics.length}> <Col key={m.label} xs={12} sm={6}>
<Card <Card
style={{ style={{
borderRadius: 10, borderRadius: 10,
border: '1px solid #EBF0F5', border: '1px solid #EBF0F5',
boxShadow: '0 1px 4px rgba(0,0,0,0.06)', boxShadow: '0 1px 4px rgba(0,0,0,0.05)',
}} }}
styles={{ body: { padding: '14px 18px' } }} styles={{ body: { padding: '14px 18px' } }}
> >
<Space size={10} align="center"> <Space size={10} align="center">
<span style={{ fontSize: 20, color: m.color }}>{m.icon}</span> <div
style={{
width: 36,
height: 36,
borderRadius: 8,
background: `${m.color}15`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 16,
color: m.color,
flexShrink: 0,
}}
>
{m.icon}
</div>
<div> <div>
<Text <Text
style={{ style={{
@@ -150,8 +160,8 @@ function OrdersMetrics({ data }: { data: PedidoSummary[] }) {
> >
{m.label} {m.label}
</Text> </Text>
<Text strong style={{ fontSize: 18, color: '#1F2937', lineHeight: 1.2 }}> <Text strong style={{ fontSize: 20, color: '#1F2937', lineHeight: 1.2 }}>
{m.value} {stats.loaded ? m.value.toLocaleString('pt-BR') : <Spin size="small" />}
</Text> </Text>
</div> </div>
</Space> </Space>
@@ -162,6 +172,21 @@ function OrdersMetrics({ data }: { data: PedidoSummary[] }) {
); );
} }
// ─── OrderStatusBadge ─────────────────────────────────────────────────────────
function OrderStatusBadge({ situa, descr }: { situa: number; descr?: string }) {
const cfg = STATUS[situa];
const label = descr ?? cfg?.label ?? SITUA_LABEL[situa] ?? String(situa);
return (
<Tag
color={cfg?.tagColor ?? 'default'}
style={{ borderRadius: 20, fontWeight: 600, fontSize: 11, padding: '1px 10px' }}
>
{label}
</Tag>
);
}
// ─── OrderActionsMenu ───────────────────────────────────────────────────────── // ─── OrderActionsMenu ─────────────────────────────────────────────────────────
function OrderActionsMenu({ function OrderActionsMenu({
@@ -250,7 +275,7 @@ function OrderDetailDrawer({ id, onClose }: { id: string | null; onClose: () =>
fontWeight: 700, fontWeight: 700,
letterSpacing: '0.08em', letterSpacing: '0.08em',
textTransform: 'uppercase', textTransform: 'uppercase',
color: '#64748B', color: '#94A3B8',
marginBottom: 2, marginBottom: 2,
display: 'block', display: 'block',
}; };
@@ -261,6 +286,7 @@ function OrderDetailDrawer({ id, onClose }: { id: string | null; onClose: () =>
open={!!id} open={!!id}
onClose={onClose} onClose={onClose}
width={520} width={520}
placement="right"
styles={{ body: { padding: '16px 24px' } }} styles={{ body: { padding: '16px 24px' } }}
footer={ footer={
<Space> <Space>
@@ -296,13 +322,13 @@ function OrderDetailDrawer({ id, onClose }: { id: string | null; onClose: () =>
<span style={label}>Data</span> <span style={label}>Data</span>
<Text>{fmtDate(data.dtPedido)}</Text> <Text>{fmtDate(data.dtPedido)}</Text>
</Col> </Col>
<Col span={12}> <Col span={24}>
<span style={label}>Cliente</span> <span style={label}>Cliente</span>
<Text strong> <Text strong style={{ display: 'block' }}>
{data.razaoCliente ?? data.nomeCliente ?? `Cód. ${data.idCliente}`} {data.razaoCliente ?? data.nomeCliente ?? `Cód. ${data.idCliente}`}
</Text> </Text>
{data.nomeCliente && data.razaoCliente && ( {data.nomeCliente && data.razaoCliente && (
<Text type="secondary" style={{ fontSize: 12, display: 'block' }}> <Text type="secondary" style={{ fontSize: 12 }}>
{data.nomeCliente} {data.nomeCliente}
</Text> </Text>
)} )}
@@ -313,6 +339,12 @@ function OrderDetailDrawer({ id, onClose }: { id: string | null; onClose: () =>
{fmt(data.total)} {fmt(data.total)}
</Text> </Text>
</Col> </Col>
{data.descontoPerc && Number(data.descontoPerc) > 0 && (
<Col span={12}>
<span style={label}>Desconto</span>
<Text>{Number(data.descontoPerc).toLocaleString('pt-BR')}%</Text>
</Col>
)}
{data.obs && ( {data.obs && (
<Col span={24}> <Col span={24}>
<span style={label}>Observações</span> <span style={label}>Observações</span>
@@ -394,6 +426,7 @@ function MobileOrderCard({
}) { }) {
const navigate = useNavigate(); const navigate = useNavigate();
const cfg = STATUS[order.situa]; const cfg = STATUS[order.situa];
const nome = order.razaoCliente ?? order.nomeCliente;
return ( return (
<Card <Card
@@ -402,18 +435,22 @@ function MobileOrderCard({
marginBottom: 10, marginBottom: 10,
border: `1px solid ${cfg?.rowBg ?? '#EBF0F5'}`, border: `1px solid ${cfg?.rowBg ?? '#EBF0F5'}`,
background: cfg?.rowBg ?? '#fff', background: cfg?.rowBg ?? '#fff',
boxShadow: '0 1px 4px rgba(0,0,0,0.06)', boxShadow: '0 1px 4px rgba(0,0,0,0.05)',
}} }}
styles={{ body: { padding: '14px 16px' } }} styles={{ body: { padding: '14px 16px' } }}
> >
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}> <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
<Text strong style={{ fontSize: 15 }}> <Text strong style={{ fontSize: 15, color: '#003B8E' }}>
{order.numPedSar} {order.numPedSar}
</Text> </Text>
<OrderStatusBadge situa={order.situa} descr={order.statusDescr} /> <OrderStatusBadge situa={order.situa} descr={order.statusDescr} />
</div> </div>
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}> {nome && (
{order.razaoCliente ?? order.nomeCliente ?? `Cód. ${order.idCliente}`} ·{' '} <Text style={{ fontSize: 13, fontWeight: 500, display: 'block', marginBottom: 2 }}>
{nome}
</Text>
)}
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 6 }}>
{fmtDate(order.dtPedido)} {fmtDate(order.dtPedido)}
</Text> </Text>
<Text strong style={{ fontSize: 16, color: '#003B8E' }}> <Text strong style={{ fontSize: 16, color: '#003B8E' }}>
@@ -425,12 +462,14 @@ function MobileOrderCard({
icon={<EyeOutlined />} icon={<EyeOutlined />}
disabled={order.fonte === 'erp'} disabled={order.fonte === 'erp'}
onClick={() => onView(order.id)} onClick={() => onView(order.id)}
style={{ borderRadius: 6 }}
> >
Ver Ver
</Button> </Button>
<Button <Button
size="small" size="small"
icon={<CopyOutlined />} icon={<CopyOutlined />}
style={{ borderRadius: 6 }}
onClick={() => onClick={() =>
void navigate({ to: '/pedidos/novo', search: { clientId: String(order.idCliente) } }) void navigate({ to: '/pedidos/novo', search: { clientId: String(order.idCliente) } })
} }
@@ -442,38 +481,18 @@ function MobileOrderCard({
); );
} }
// ─── EmptyState ───────────────────────────────────────────────────────────────
function EmptyOrders({ onNew }: { onNew: () => void }) {
return (
<Empty
image={<ShoppingCartOutlined style={{ fontSize: 56, color: '#D9E2EC' }} />}
imageStyle={{ height: 64 }}
description={
<Space direction="vertical" size={4}>
<Text strong style={{ fontSize: 15 }}>
Nenhum pedido encontrado
</Text>
<Text type="secondary">Tente alterar os filtros ou crie um novo pedido.</Text>
</Space>
}
style={{ padding: '48px 0' }}
>
<Button type="primary" icon={<PlusOutlined />} onClick={onNew}>
Novo Pedido
</Button>
</Empty>
);
}
// ─── OrdersPage ─────────────────────────────────────────────────────────────── // ─── OrdersPage ───────────────────────────────────────────────────────────────
export function OrdersPage() { export function OrdersPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const screens = useBreakpoint(); const screens = useBreakpoint();
const isMobile = !screens.md; const isMobile = !screens.md;
const { message: msg } = App.useApp();
const stats = useOrderStats();
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [query, setQuery] = useState('');
const [situaFilter, setSituaFilter] = useState<number | undefined>(); const [situaFilter, setSituaFilter] = useState<number | undefined>();
const [period, setPeriod] = useState(''); const [period, setPeriod] = useState('');
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
@@ -482,8 +501,8 @@ export function OrdersPage() {
const { from, to } = period ? periodRange(period) : {}; const { from, to } = period ? periodRange(period) : {};
const { data, isLoading } = useOrderList({ const { data, isLoading, isFetching } = useOrderList({
numPedSar: search || undefined, numPedSar: query || undefined,
situa: situaFilter, situa: situaFilter,
from, from,
to, to,
@@ -494,28 +513,39 @@ export function OrdersPage() {
const rows = data?.data ?? []; const rows = data?.data ?? [];
const total = data?.total ?? 0; const total = data?.total ?? 0;
const hasFilters = !!query || !!situaFilter || !!period;
function commitSearch() {
setQuery(search.trim());
setPage(1);
}
function clearFilters() { function clearFilters() {
setSearch(''); setSearch('');
setQuery('');
setSituaFilter(undefined); setSituaFilter(undefined);
setPeriod(''); setPeriod('');
setPage(1); setPage(1);
} }
// ── Tabela desktop ───────────────────────────────────────────────────────── // Valor total da página atual (sem query separada)
const valorPagina = useMemo(() => rows.reduce((a, o) => a + Number(o.total), 0), [rows]);
// ── Colunas desktop ─────────────────────────────────────────────────────────
const columns: TableColumnsType<PedidoSummary> = [ const columns: TableColumnsType<PedidoSummary> = [
{ {
title: 'Pedido', title: 'Pedido',
dataIndex: 'numPedSar', key: 'pedido',
width: 140, width: 140,
render: (_: string, row: PedidoSummary) => { render: (_: unknown, row: PedidoSummary) => {
const label = row.numero ? String(row.numero) : row.numPedSar; const label = row.numero ? String(row.numero) : row.numPedSar;
return row.fonte === 'erp' ? ( return row.fonte === 'erp' ? (
<Text strong className="tabular-nums"> <Text strong className="tabular-nums" style={{ color: '#1F2937' }}>
{label} {label}
</Text> </Text>
) : ( ) : (
<Link to="/pedidos/$id" params={{ id: row.id }}> <Link to="/pedidos/$id" params={{ id: row.id }}>
<Text strong className="tabular-nums" style={{ color: '#0057D9' }}> <Text strong className="tabular-nums" style={{ color: '#003B8E' }}>
{label} {label}
</Text> </Text>
</Link> </Link>
@@ -525,28 +555,37 @@ export function OrdersPage() {
{ {
title: 'Cliente', title: 'Cliente',
key: 'cliente', key: 'cliente',
ellipsis: true, minWidth: 240,
render: (_: unknown, row: PedidoSummary) => { render: (_: unknown, row: PedidoSummary) => {
const nome = row.razaoCliente ?? row.nomeCliente; const nome = row.razaoCliente ?? row.nomeCliente;
const subtit = row.nomeCliente && row.razaoCliente ? row.nomeCliente : null;
return ( return (
<Space direction="vertical" size={0}> <div>
{nome ? ( {nome ? (
<Text style={{ fontWeight: 500 }}>{nome}</Text> <Text
strong
style={{ fontSize: 14, color: '#1F2937', display: 'block', lineHeight: 1.3 }}
>
{nome}
</Text>
) : ( ) : (
<Text type="secondary">Cód. {row.idCliente}</Text> <Text type="secondary" style={{ fontSize: 13 }}>
)} Cód. {row.idCliente}
{row.nomeCliente && row.razaoCliente && (
<Text type="secondary" style={{ fontSize: 11 }}>
{row.nomeCliente}
</Text> </Text>
)} )}
</Space> {subtit && (
<Text style={{ fontSize: 12, color: '#64748B', display: 'block', lineHeight: 1.3 }}>
{subtit}
</Text>
)}
</div>
); );
}, },
}, },
{ {
title: 'Status', title: 'Status',
dataIndex: 'situa', dataIndex: 'situa',
key: 'status',
width: 140, width: 140,
render: (s: number, row: PedidoSummary) => ( render: (s: number, row: PedidoSummary) => (
<OrderStatusBadge situa={s} descr={row.statusDescr} /> <OrderStatusBadge situa={s} descr={row.statusDescr} />
@@ -555,10 +594,11 @@ export function OrdersPage() {
{ {
title: 'Total', title: 'Total',
dataIndex: 'total', dataIndex: 'total',
key: 'total',
width: 130, width: 130,
align: 'right', align: 'right' as const,
render: (v: string) => ( render: (v: string) => (
<Text strong className="tabular-nums"> <Text strong className="tabular-nums" style={{ color: '#003B8E', fontSize: 14 }}>
{fmt(v)} {fmt(v)}
</Text> </Text>
), ),
@@ -566,22 +606,34 @@ export function OrdersPage() {
{ {
title: 'Data', title: 'Data',
dataIndex: 'dtPedido', dataIndex: 'dtPedido',
key: 'dtPedido',
width: 110, width: 110,
render: (v: string) => <Text type="secondary">{fmtDate(v)}</Text>, render: (v: string) => <Text style={{ fontSize: 13, color: '#475569' }}>{fmtDate(v)}</Text>,
}, },
{ {
title: '', title: '',
key: 'actions', key: 'actions',
width: 48, width: 100,
render: (_: unknown, row: PedidoSummary) => ( render: (_: unknown, row: PedidoSummary) => (
<Space size={4}>
<Button
size="small"
icon={<EyeOutlined />}
type="primary"
style={{ borderRadius: 6 }}
title="Ver detalhes"
disabled={row.fonte === 'erp'}
onClick={() => setDrawerOrderId(row.id)}
/>
<OrderActionsMenu order={row} onView={(id) => setDrawerOrderId(id)} /> <OrderActionsMenu order={row} onView={(id) => setDrawerOrderId(id)} />
</Space>
), ),
}, },
]; ];
return ( return (
<div style={{ maxWidth: 1200, margin: '0 auto' }}> <div style={{ maxWidth: 1200, margin: '0 auto' }}>
{/* ── Cabeçalho ───────────────────────────────────────────────── */} {/* ── Cabeçalho ─────────────────────────────────────────────────── */}
<div <div
style={{ style={{
display: 'flex', display: 'flex',
@@ -595,7 +647,7 @@ export function OrdersPage() {
Pedidos Pedidos
</Title> </Title>
<p style={{ margin: '4px 0 0', color: '#64748B', fontSize: 14 }}> <p style={{ margin: '4px 0 0', color: '#64748B', fontSize: 14 }}>
Acompanhe seus pedidos, status de envio e histórico comercial. Acompanhe seus pedidos, status de aprovação e histórico comercial.
</p> </p>
</div> </div>
{!isMobile && ( {!isMobile && (
@@ -603,49 +655,70 @@ export function OrdersPage() {
type="primary" type="primary"
icon={<PlusOutlined />} icon={<PlusOutlined />}
size="large" size="large"
style={{
borderRadius: 8,
fontWeight: 600,
backgroundColor: '#389e0d',
borderColor: '#389e0d',
}}
onClick={() => void navigate({ to: '/pedidos/novo' })} onClick={() => void navigate({ to: '/pedidos/novo' })}
style={{ borderRadius: 8, fontWeight: 600 }}
> >
Novo Pedido Novo Pedido
</Button> </Button>
)} )}
</div> </div>
{/* ── Métricas ────────────────────────────────────────────────── */} {/* ── Métricas ──────────────────────────────────────────────────── */}
<OrdersMetrics data={rows} /> <OrdersMetrics stats={stats} />
{/* ── Filtros ─────────────────────────────────────────────────── */} {/* ── Filtros ───────────────────────────────────────────────────── */}
<Card <Card
style={{ style={{
borderRadius: 10, borderRadius: 10,
border: '1px solid #EBF0F5', border: '1px solid #EBF0F5',
boxShadow: '0 1px 4px rgba(0,0,0,0.06)', boxShadow: '0 1px 4px rgba(0,0,0,0.05)',
marginBottom: 16, marginBottom: 20,
}} }}
styles={{ body: { padding: '14px 20px' } }} styles={{ body: { padding: '14px 20px' } }}
> >
<Row gutter={[12, 12]} align="middle"> <Row gutter={[12, 12]} align="middle">
<Col xs={24} sm={24} md={8}> {/* Busca */}
<Col xs={24} md={8}>
<div style={{ position: 'relative' }}>
<SearchOutlined
style={{
position: 'absolute',
left: 10,
top: '50%',
transform: 'translateY(-50%)',
color: '#94A3B8',
zIndex: 1,
}}
/>
<input <input
value={search} value={search}
onChange={(e) => { onChange={(e) => setSearch(e.target.value)}
setSearch(e.target.value); onKeyDown={(e) => {
setPage(1); if (e.key === 'Enter') commitSearch();
}} }}
onBlur={commitSearch}
placeholder="Buscar por nº do pedido..." placeholder="Buscar por nº do pedido..."
style={{ style={{
width: '100%', width: '100%',
height: 32, height: 32,
padding: '0 11px', padding: '0 11px 0 32px',
border: '1px solid #d9d9d9', border: '1px solid #d9d9d9',
borderRadius: 6, borderRadius: 6,
fontSize: 14, fontSize: 13,
outline: 'none', outline: 'none',
color: '#1F2937', color: '#1F2937',
boxSizing: 'border-box', boxSizing: 'border-box',
}} }}
/> />
</div>
</Col> </Col>
{/* Status */}
<Col xs={12} sm={8} md={5}> <Col xs={12} sm={8} md={5}>
<Select <Select
style={{ width: '100%' }} style={{ width: '100%' }}
@@ -664,6 +737,8 @@ export function OrdersPage() {
]} ]}
/> />
</Col> </Col>
{/* Período */}
<Col xs={12} sm={8} md={5}> <Col xs={12} sm={8} md={5}>
<Select <Select
style={{ width: '100%' }} style={{ width: '100%' }}
@@ -681,19 +756,29 @@ export function OrdersPage() {
]} ]}
/> />
</Col> </Col>
<Col xs={24} sm={8} md={4}>
{/* Limpar */}
<Col xs={12} sm={8} md={3}>
<Button <Button
style={{ width: '100%', borderRadius: 6 }} style={{ width: '100%', borderRadius: 6 }}
icon={<ClearOutlined />}
disabled={!hasFilters}
onClick={clearFilters} onClick={clearFilters}
disabled={!search && !situaFilter && !period}
> >
Limpar filtros Limpar
</Button> </Button>
</Col> </Col>
{/* Contador */}
<Col>
<Text style={{ fontSize: 12, color: '#94A3B8' }}>
{data?.total !== undefined ? `${total.toLocaleString('pt-BR')} pedidos` : '…'}
</Text>
</Col>
</Row> </Row>
</Card> </Card>
{/* ── Conteúdo principal ──────────────────────────────────────── */} {/* ── Lista / tabela ────────────────────────────────────────────── */}
{isLoading ? ( {isLoading ? (
<div style={{ textAlign: 'center', padding: 64 }}> <div style={{ textAlign: 'center', padding: 64 }}>
<Spin size="large" /> <Spin size="large" />
@@ -703,27 +788,51 @@ export function OrdersPage() {
style={{ borderRadius: 10, border: '1px solid #EBF0F5' }} style={{ borderRadius: 10, border: '1px solid #EBF0F5' }}
styles={{ body: { padding: 0 } }} styles={{ body: { padding: 0 } }}
> >
<EmptyOrders onNew={() => void navigate({ to: '/pedidos/novo' })} /> <div style={{ padding: '48px 0', textAlign: 'center' }}>
<ShoppingCartOutlined
style={{ fontSize: 56, color: '#D9E2EC', display: 'block', marginBottom: 16 }}
/>
<Text strong style={{ fontSize: 15, display: 'block' }}>
Nenhum pedido encontrado
</Text>
<Text type="secondary">Tente alterar os filtros ou crie um novo pedido.</Text>
<div style={{ marginTop: 20 }}>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => void navigate({ to: '/pedidos/novo' })}
style={{ borderRadius: 8 }}
>
Novo Pedido
</Button>
</div>
</div>
</Card> </Card>
) : isMobile ? ( ) : isMobile ? (
/* ── Mobile: cards ─────────────────────────────────────────── */ /* ── Mobile ────────────────────────────────────────────────────── */
<div> <div>
{rows.map((o) => ( {rows.map((o) => (
<MobileOrderCard key={o.id} order={o} onView={(id) => setDrawerOrderId(id)} /> <MobileOrderCard key={o.id} order={o} onView={(id) => setDrawerOrderId(id)} />
))} ))}
<div <Text
style={{ textAlign: 'center', padding: '8px 0 16px', color: '#64748B', fontSize: 13 }} style={{
fontSize: 12,
color: '#94A3B8',
display: 'block',
textAlign: 'center',
padding: '8px 0 16px',
}}
> >
Mostrando {rows.length} de {total} pedidos Mostrando {rows.length} de {total} pedidos
</div> </Text>
</div> </div>
) : ( ) : (
/* ── Desktop: tabela ────────────────────────────────────────── */ /* ── Desktop ────────────────────────────────────────────────────── */
<Card <Card
style={{ style={{
borderRadius: 10, borderRadius: 10,
border: '1px solid #EBF0F5', border: '1px solid #EBF0F5',
boxShadow: '0 1px 4px rgba(0,0,0,0.06)', boxShadow: '0 1px 6px rgba(0,0,0,0.06)',
}} }}
styles={{ body: { padding: 0 } }} styles={{ body: { padding: 0 } }}
> >
@@ -732,6 +841,8 @@ export function OrdersPage() {
columns={columns} columns={columns}
dataSource={rows} dataSource={rows}
size="middle" size="middle"
loading={isFetching}
scroll={{ x: 900 }}
onRow={(row) => ({ onRow={(row) => ({
onClick: () => { onClick: () => {
if (row.fonte !== 'erp') setDrawerOrderId(row.id); if (row.fonte !== 'erp') setDrawerOrderId(row.id);
@@ -739,6 +850,7 @@ export function OrdersPage() {
style: { style: {
background: STATUS[row.situa]?.rowBg ?? '#fff', background: STATUS[row.situa]?.rowBg ?? '#fff',
cursor: row.fonte !== 'erp' ? 'pointer' : 'default', cursor: row.fonte !== 'erp' ? 'pointer' : 'default',
verticalAlign: 'top',
}, },
})} })}
pagination={{ pagination={{
@@ -751,11 +863,21 @@ export function OrdersPage() {
style: { padding: '12px 24px' }, style: { padding: '12px 24px' },
}} }}
style={{ borderRadius: 10, overflow: 'hidden' }} style={{ borderRadius: 10, overflow: 'hidden' }}
footer={() => (
<div style={{ textAlign: 'right', padding: '4px 8px' }}>
<Text style={{ fontSize: 12, color: '#64748B' }}>
Valor nesta página:{' '}
<Text strong style={{ color: '#003B8E' }}>
{fmt(valorPagina)}
</Text>
</Text>
</div>
)}
/> />
</Card> </Card>
)} )}
{/* ── Drawer de detalhe ───────────────────────────────────────── */} {/* ── Drawer de detalhe ─────────────────────────────────────────── */}
<OrderDetailDrawer id={drawerOrderId} onClose={() => setDrawerOrderId(null)} /> <OrderDetailDrawer id={drawerOrderId} onClose={() => setDrawerOrderId(null)} />
{/* FAB mobile */} {/* FAB mobile */}
@@ -784,6 +906,16 @@ export function OrdersPage() {
/> />
)} )}
{/* Aviso de novo pedido via mensagem se tentativa em mobile */}
{isMobile && (
<Button
style={{ display: 'none' }}
onClick={() => void msg.info('Use o botão + para criar um novo pedido.')}
/>
)}
<Divider style={{ display: 'none' }} />
<style>{` <style>{`
.ant-table-row:hover td { background: inherit !important; filter: brightness(0.97); } .ant-table-row:hover td { background: inherit !important; filter: brightness(0.97); }
`}</style> `}</style>

View File

@@ -1,156 +1,39 @@
-- ============================================================================= -- =============================================================================
-- sarweb_views.sql -- sar_views.sql
-- Views PostgreSQL para o projeto SARWeb -- Views PostgreSQL do projeto SAR — schema `sar` no banco ERP (libreplast)
-- Gerado a partir da análise do SAR Android (sincronização ERP <-> App)
-- =============================================================================
-- IMPORTANTE: Este arquivo cobre os dois schemas operacionais (gerente e sig).
-- Se o seu banco usa apenas UM dos schemas, remova o bloco UNION ALL
-- correspondente ao schema ausente em cada view, ou a criação falhará.
-- --
-- Para saber qual schema o seu banco usa, execute: -- Executar como: psql -U postgres -d libreplast -f sar_views.sql
-- SELECT schemaname FROM pg_tables WHERE tablename='pedidos' AND schemaname NOT IN ('sarpalm');
-- --
-- STATUS — mapeamento normalizado por sistema: -- DECISÃO DE ARQUITETURA: todas as views residem no schema `sar` (mesmo schema
-- GERENTE: situa 1=Pendente | 2=Liberado | 3=Faturado | 4=Cancelado -- das tabelas Prisma). O search_path da conexão runtime é `sar`, portanto as
-- SIG: situa 1=Pendente | 2=Liberado | 4=Faturado | 5=Cancelado -- queries do backend usam os nomes curtos (vw_clientes, vw_pedidos_erp).
--
-- ERP base: SIG (schema sig.*)
-- Empresa gerencial: id_empresa = 1 (gestao.empresa)
-- Empresa fiscal: id_empresa = 9001 (sig.corrent / sig.pedidos)
-- Cancelados SIG: situa = 5 (≠ SAR que usa situa = 3)
-- Faturados SIG: situa = 4 (coincide com SAR)
-- ============================================================================= -- =============================================================================
CREATE SCHEMA IF NOT EXISTS sarweb; -- ---------------------------------------------------------------------------
-- 1. CLIENTES
-- ============================================================================= -- Fonte: sig.corrent
-- 1. EMPRESAS -- Obs: COALESCE(id_empresa, 1) cobre registros antigos sem id_empresa.
-- ============================================================================= -- cod_vendedor determina a carteira do representante.
CREATE OR REPLACE VIEW sarweb.vw_empresas AS -- ---------------------------------------------------------------------------
CREATE OR REPLACE VIEW sar.vw_clientes AS
SELECT SELECT
e.id_empresa, COALESCE(c.id_empresa, 1) AS id_empresa,
e.nome,
e.razao_social,
e.cnpj,
e.estado AS uf,
e.id_matriz,
e.id_portador_padrao,
s.origem_descmax,
s.tp_estoque,
s.bloq_preco_pedido,
s.ativar_prod_pauta,
s.preco_padrao,
s.preco_com_ipi,
(
SELECT COALESCE(schemaname, 'gerente')
FROM pg_tables
WHERE tablename = 'pedidos'
AND schemaname NOT IN ('sarpalm')
LIMIT 1
) AS sistema,
(
SELECT COALESCE(b1.id_empresa_tabcomp, e.id_empresa)
FROM gestao.empresa a1
LEFT JOIN gestao.tabcomp b1
ON b1.id_empresa = a1.id_empresa AND b1.nome_tabcomp = 'produtos'
WHERE a1.id_empresa = e.id_empresa
) AS id_empresa_prod,
(
SELECT COALESCE(b1.id_empresa_tabcomp, e.id_empresa)
FROM gestao.empresa a1
LEFT JOIN gestao.tabcomp b1
ON b1.id_empresa = a1.id_empresa AND b1.nome_tabcomp = 'grupos'
WHERE a1.id_empresa = e.id_empresa
) AS id_empresa_grup
FROM gestao.empresa e
LEFT JOIN gestao.sarcfg s ON s.id_empresa = e.id_empresa;
-- =============================================================================
-- 2. REPRESENTANTES / VENDEDORES
-- =============================================================================
CREATE OR REPLACE VIEW sarweb.vw_representantes AS
SELECT
v.id_vendedor,
v.id_empresa AS id_empresa_matriz,
v.codigo,
v.nome,
v.exp_sar AS habilitado_sar,
v.taxa_com,
v.forma_pag AS forma_pag_comissao,
v.cod_supervisor,
v.taxa_com_super,
v.forma_pag_super,
v.desconto_max,
v.permitir_flex,
COALESCE(f.saldo_flex, 0) AS saldo_flex,
v.vl_ped_minimo,
v.desc_rateio_com,
v.origem_com,
v.cod_pauta1,
v.cod_pauta2,
v.cod_pauta3,
v.cod_pauta4,
v.cod_pauta5,
v.cod_pauta6
FROM gestao.vendedor v
LEFT JOIN gestao.flex f
ON f.id_vendedor = v.id_vendedor;
-- =============================================================================
-- 3. CLIENTES (gerente.clientes UNION sig.corrent)
-- =============================================================================
CREATE OR REPLACE VIEW sarweb.vw_clientes AS
-- --- GERENTE ---
SELECT
'gerente' AS sistema,
c.id_empresa,
c.id_cliente AS id_cliente,
c.ativo,
c.nome,
c.razao,
c.pesso AS pessoa, -- 0=PJ 1=PF
c.consfinal,
c.cgcpf,
c.suf_cgcpf,
c.inscr AS inscricao_estadual,
c.ender AS endereco,
COALESCE(c.num_endereco, '') AS num_endereco,
c.bairr AS bairro,
c.id_municipio,
c.cep,
c.ddd,
c.telef AS telefone,
c.e_mail AS email,
c.data AS dt_cadastro,
c.obs,
c.cod_formapag,
(
SELECT fp.id_formapag
FROM gestao.formapag fp
WHERE fp.id_empresa = c.id_empresa
AND fp.codigo = c.cod_formapag
LIMIT 1
) AS id_formapag,
c.indicador_ie,
c.cod_pauta,
c.st_especifica,
COALESCE(c.limcred, 0) AS limite_credito,
c.cod_vendedor,
COALESCE(c.desc_cliente_rede, 0) AS desc_cliente_rede,
c.dt_atual
FROM gerente.clientes c
UNION ALL
-- --- SIG ---
SELECT
'sig' AS sistema,
c.id_empresa,
c.id_corrent AS id_cliente, c.id_corrent AS id_cliente,
c.ativo, c.ativo,
c.nome, COALESCE(NULLIF(TRIM(c.nome), ''), TRIM(c.razao)) AS nome,
c.razao, TRIM(c.razao) AS razao,
c.pesso AS pessoa, c.pesso AS pessoa,
c.consfinal, c.consfinal,
c.cgcpf, c.cgcpf,
c.suf_cgcpf, c.suf_cgcpf,
c.inscr AS inscricao_estadual, c.inscr AS inscricao_estadual,
c.endereco AS endereco, c.endereco,
COALESCE(c.num_endereco, '') AS num_endereco, COALESCE(c.num_endereco, '') AS num_endereco,
c.bairr AS bairro, c.bairr AS bairro,
c.id_municipio, c.id_municipio,
@@ -164,8 +47,8 @@ SELECT
( (
SELECT fp.id_formapag SELECT fp.id_formapag
FROM gestao.formapag fp FROM gestao.formapag fp
LEFT JOIN gestao.empresa e ON e.id_empresa = c.id_empresa LEFT JOIN gestao.empresa e ON e.id_empresa = COALESCE(c.id_empresa, 1)
WHERE fp.id_empresa = COALESCE(e.id_matriz, c.id_empresa) WHERE fp.id_empresa = COALESCE(e.id_matriz, COALESCE(c.id_empresa, 1))
AND fp.codigo = c.cod_formapag AND fp.codigo = c.cod_formapag
LIMIT 1 LIMIT 1
) AS id_formapag, ) AS id_formapag,
@@ -174,234 +57,17 @@ SELECT
c.st_especifica, c.st_especifica,
COALESCE(c.limcred, 0) AS limite_credito, COALESCE(c.limcred, 0) AS limite_credito,
c.cod_vendedor, c.cod_vendedor,
0 AS desc_cliente_rede,
c.dt_atual c.dt_atual
FROM sig.corrent c; FROM sig.corrent c;
-- ============================================================================= -- ---------------------------------------------------------------------------
-- 4. MUNICÍPIOS -- 2. PEDIDOS ERP
-- ============================================================================= -- Fonte: sig.pedidos
CREATE OR REPLACE VIEW sarweb.vw_municipios AS -- Situa SIG → SAR: 5=Cancelado(SIG) vs 3=Cancelado(SAR). O backend
-- normaliza em runtime (sigToSar / sarToSig em orders.service.ts).
-- ---------------------------------------------------------------------------
CREATE OR REPLACE VIEW sar.vw_pedidos_erp AS
SELECT SELECT
id_municipio,
nome,
estado AS uf,
codigo_ibge
FROM gestao.municipio;
-- =============================================================================
-- 5. FORMAS DE PAGAMENTO
-- =============================================================================
CREATE OR REPLACE VIEW sarweb.vw_formas_pagamento AS
SELECT
id_formapag,
id_empresa,
codigo,
descr AS descricao,
ativa,
numparc AS num_parcelas,
desco AS desconto_perc,
COALESCE(vl_ped_minimo, 0) AS vl_ped_minimo,
COALESCE(libera_credito, 0) AS libera_credito,
COALESCE(acresc, 0) AS tx_acrescimo,
integrar_sar,
dt_atual
FROM gestao.formapag;
-- =============================================================================
-- 6. PRODUTOS
-- =============================================================================
CREATE OR REPLACE VIEW sarweb.vw_produtos AS
SELECT
p.id_empresa,
p.id_erp,
p.codigo,
p.referencia,
p.descricao,
p.descr_det,
p.ativo,
p.cod_barra,
p.unidade,
p.tipo,
p.vl_preco1,
COALESCE(p.vl_preco2, 0) AS vl_preco2,
COALESCE(p.vl_preco3, 0) AS vl_preco3,
p.cod_grupo,
grp.descricao AS grupo,
p.cod_subgrupo,
sub.descricao AS subgrupo,
sub.desc_max,
COALESCE(p.grupo_st, '') AS grupo_st,
p.cod_marca,
COALESCE(mrc.nome, 'Sem Marca') AS marca,
p.classe_abc,
p.taxa_comissao,
p.cod_st,
st.aliq_ipi,
COALESCE(st.desc_ipi_bc, 0) AS desc_ipi_bc,
p.peso_liquido,
p.qtd_volume,
COALESCE(p.lote_mul_venda, 1) AS lote_mul_venda,
COALESCE(p.permitir_dif_lote, 0) AS permitir_dif_lote,
COALESCE(p.id_prodvinc, 0) AS id_prodvinc,
COALESCE(p.preco_promocional, 0) AS preco_promocional,
COALESCE(p.tx_desc_lote, 0) AS tx_desc_lote,
p.lista_pauta,
CASE WHEN p.dt_atual > sub.da THEN p.dt_atual ELSE sub.da END AS dt_atual
FROM gestao.produto p
LEFT JOIN gestao.grupo grp ON grp.codigo = p.cod_grupo AND grp.id_empresa = p.id_empresa
LEFT JOIN gestao.grupo sub ON sub.codigo = p.cod_subgrupo AND sub.id_empresa = p.id_empresa
LEFT JOIN gestao.marca mrc ON mrc.codigo = p.cod_marca AND mrc.id_empresa = p.id_empresa
LEFT JOIN gestao.st st ON st.codigo = p.cod_st
WHERE p.id_erp IS NOT NULL;
-- =============================================================================
-- 7. ESTOQUE
-- =============================================================================
CREATE OR REPLACE VIEW sarweb.vw_estoque AS
SELECT
p.id_empresa,
p.id_erp,
cfg.tp_estoque,
CASE cfg.tp_estoque
WHEN 'E' THEN COALESCE(e.qtdade, 0) - COALESCE(e.qtd_empenhada, 0)
WHEN 'P' THEN COALESCE(e.qtdade, 0) - COALESCE(e.qtd_empenhada, 0) - COALESCE(e.qtd_pedidos, 0)
WHEN 'Z' THEN 0
ELSE COALESCE(e.qtdade, 0)
END AS qtd_estoque,
COALESCE(e.qtdade, 0) AS qtd_fisico,
COALESCE(e.qtd_empenhada, 0) AS qtd_empenhada,
COALESCE(e.qtd_pedidos, 0) AS qtd_pedidos
FROM gestao.produto p
LEFT JOIN gestao.grupo grp ON grp.codigo = p.cod_grupo AND grp.id_empresa = p.id_empresa
LEFT JOIN gestao.grupo sub ON sub.codigo = p.cod_subgrupo AND sub.id_empresa = p.id_empresa
LEFT JOIN gestao.sarcfg cfg ON cfg.id_empresa = p.id_empresa
LEFT JOIN gestao.estsaldo e ON e.id_produto = p.id_erp
AND e.id_empresa = p.id_empresa
AND e.id_estlocal = p.cod_estlocal
WHERE p.id_erp IS NOT NULL
AND p.ativo = 1
AND p.lista_pauta = 1
AND grp.int_sar = 1
AND sub.int_sar = 1
AND (sub.produto_variacao = 0 OR p.id_prodvinc > 0);
-- =============================================================================
-- 8. SITUAÇÃO TRIBUTÁRIA (ST ICMS)
-- =============================================================================
CREATE OR REPLACE VIEW sarweb.vw_sticms AS
SELECT
st.id_empresa AS id_empresa_matriz,
si.id_sticms,
st.codigo AS cod_st,
si.uf,
si.st_especifica,
si.perc_bc_icms,
si.aliq_icms,
si.modal_bc_icmsst,
si.aliq_icmsst,
si.somar_icmsst_nf,
si.perc_marg_vl_icmsst,
si.contribuinte_icms
FROM gestao.sticms si
JOIN gestao.st st ON st.id_st = si.id_st;
-- =============================================================================
-- 9. PAUTAS DE PREÇO
-- =============================================================================
CREATE OR REPLACE VIEW sarweb.vw_pautas AS
SELECT
p.id_pauta,
p.id_empresa,
p.codigo,
p.ativo,
p.num_pauta,
COALESCE(p.dt_cadast, '1900-01-01'::date) AS dt_cadastro,
p.descricao,
p.obs,
COALESCE(p.dt_ini, '1900-01-01'::date) AS dt_inicio,
COALESCE(p.dt_fim, '2100-01-01'::date) AS dt_fim,
p.pauta_exclusiva_cliente,
COALESCE(p.vl_pedido1, 0) AS vl_pedido1,
COALESCE(p.vl_pedido2, 0) AS vl_pedido2,
COALESCE(p.vl_pedido3, 0) AS vl_pedido3,
COALESCE(p.vl_pedido4, 0) AS vl_pedido4,
COALESCE(p.vl_pedido5, 0) AS vl_pedido5,
COALESCE(p.tx_desconto1, 0) AS tx_desconto1,
COALESCE(p.tx_desconto2, 0) AS tx_desconto2,
COALESCE(p.tx_desconto3, 0) AS tx_desconto3,
COALESCE(p.tx_desconto4, 0) AS tx_desconto4,
COALESCE(p.tx_desconto5, 0) AS tx_desconto5,
COALESCE(p.tp_desconto, 0) AS tp_desconto
FROM gestao.pauta p;
-- =============================================================================
-- 10. PRODUTOS POR PAUTA
-- =============================================================================
CREATE OR REPLACE VIEW sarweb.vw_pauta_produtos AS
SELECT
pp.id_pauta,
COALESCE(pp.id_varprod, 0) AS id_varprod,
pp.id_prod AS id_produto,
pp.preco1,
COALESCE(pp.preco2, 0) AS preco2,
COALESCE(pp.preco3, 0) AS preco3,
COALESCE(pp.valor_pauta_icms_st, 0) AS valor_pauta_icms_st,
pp.tp_pauta
FROM gestao.pauxpro pp;
-- =============================================================================
-- 11. PEDIDOS (gerente.pedidos UNION sig.pedidos)
-- =============================================================================
CREATE OR REPLACE VIEW sarweb.vw_pedidos AS
-- --- GERENTE ---
SELECT
'gerente' AS sistema,
p.id_empresa,
p.id_pedido,
p.num_ped_sar,
p.numero,
p.tipo,
p.situa,
CASE p.situa
WHEN 1 THEN 'Pendente'
WHEN 2 THEN 'Liberado'
WHEN 3 THEN 'Faturado'
WHEN 4 THEN 'Cancelado'
ELSE 'Enviado'
END AS status_descr,
p.data AS dt_pedido,
p.dtemi AS dt_emissao,
p.clien AS id_cliente,
p.cod_vendedor,
p.cod_formapag,
fp.id_formapag,
fp.descr AS forma_pagamento,
p.num_pauta,
pau.id_pauta,
COALESCE(p.obs, '') AS obs,
p.totpr AS total_produtos,
COALESCE(p.ipi, 0) AS total_ipi,
COALESCE(p.vl_icmsst, 0) AS total_icmsst,
COALESCE(p.total, 0) AS total,
COALESCE(p.descp, 0) AS desconto_perc,
COALESCE(p.descv, 0) AS desconto_valor,
COALESCE(p.acrev, 0) AS acrescimo,
COALESCE(p.comis, 0) AS comissao,
COALESCE(p.ped_flex, 0) AS ped_flex,
p.cod_supervisor,
p.taxa_com_super
FROM gerente.pedidos p
LEFT JOIN gestao.formapag fp ON fp.codigo = p.cod_formapag
AND fp.id_empresa = p.id_empresa
LEFT JOIN gestao.pauta pau ON pau.num_pauta = p.num_pauta
UNION ALL
-- --- SIG ---
SELECT
'sig' AS sistema,
p.id_empresa, p.id_empresa,
p.id_pedido, p.id_pedido,
p.num_ped_sar, p.num_ped_sar,
@@ -422,7 +88,6 @@ SELECT
p.cod_formapag, p.cod_formapag,
fp.id_formapag, fp.id_formapag,
fp.descr AS forma_pagamento, fp.descr AS forma_pagamento,
NULL::integer AS num_pauta,
pau.id_pauta, pau.id_pauta,
COALESCE(p.obs, '') AS obs, COALESCE(p.obs, '') AS obs,
p.totpr AS total_produtos, p.totpr AS total_produtos,
@@ -433,147 +98,17 @@ SELECT
COALESCE(p.descv, 0) AS desconto_valor, COALESCE(p.descv, 0) AS desconto_valor,
COALESCE(p.tx_acrescimo, 0) AS acrescimo, COALESCE(p.tx_acrescimo, 0) AS acrescimo,
COALESCE(p.com_fat, 0) AS comissao, COALESCE(p.com_fat, 0) AS comissao,
COALESCE(p.ped_flex, 0) AS ped_flex, COALESCE(p.ped_flex::integer, 0) AS ped_flex,
p.cod_vend2 AS cod_supervisor, p.cod_vend2 AS cod_supervisor,
p.tx_com_vend2 AS taxa_com_super p.tx_com_vend2 AS taxa_com_super
FROM sig.pedidos p FROM sig.pedidos p
LEFT JOIN gestao.formapag fp ON fp.codigo = p.cod_formapag LEFT JOIN gestao.formapag fp
AND fp.id_empresa = CASE WHEN p.id_empresa > 9000 THEN p.id_empresa - 9000 ELSE p.id_empresa END ON fp.codigo = p.cod_formapag
LEFT JOIN gestao.pauta pau ON pau.codigo = p.cod_pauta AND fp.id_empresa = CASE WHEN p.id_empresa > 9000
AND pau.id_empresa = CASE WHEN p.id_empresa > 9000 THEN p.id_empresa - 9000 ELSE p.id_empresa END; THEN p.id_empresa - 9000
ELSE p.id_empresa END
-- ============================================================================= LEFT JOIN gestao.pauta pau
-- 12. ITENS DE PEDIDO (gerente.peditens UNION sig.peditens) ON pau.codigo = p.cod_pauta
-- ============================================================================= AND pau.id_empresa = CASE WHEN p.id_empresa > 9000
CREATE OR REPLACE VIEW sarweb.vw_peditens AS THEN p.id_empresa - 9000
ELSE p.id_empresa END;
-- --- GERENTE ---
SELECT
'gerente' AS sistema,
i.id_pedido,
i.ordem,
i.produ AS id_produto,
i.qtd,
i.pruni AS preco_unitario,
COALESCE(i.descp, 0) AS desconto_perc,
COALESCE(i.descv, 0) AS desconto_valor,
COALESCE(i.obs, '') AS obs,
COALESCE(i.preco_pauta, 0) AS preco_pauta,
COALESCE(i.vl_flex, 0) AS vl_flex,
COALESCE(i.comis, 0) AS comissao,
COALESCE(i.preco_com_ipi, 0) AS preco_com_ipi,
COALESCE(i.bc_ipi, 0) AS bc_ipi,
COALESCE(i.bc_ipi * i.aliq_ipi / 100.0, 0) AS vl_ipi,
COALESCE(i.bc_icmsst, 0) AS bc_icmsst,
COALESCE(i.vl_icmsst, 0) AS vl_icmsst,
COALESCE(i.vl_totliq, 0) AS vl_total_liquido,
COALESCE(i.total, 0) AS total,
COALESCE(i.num_oc, '') AS num_oc,
COALESCE(i.item_oc, '') AS item_oc,
i.id_tes
FROM gerente.peditens i
UNION ALL
-- --- SIG ---
SELECT
'sig' AS sistema,
i.id_pedido,
i.ordem,
i.produ AS id_produto,
i.qtd,
i.pruni AS preco_unitario,
COALESCE(i.descp, 0) AS desconto_perc,
COALESCE(i.descv, 0) AS desconto_valor,
COALESCE(i.obs, '') AS obs,
COALESCE(i.preco_pauta, 0) AS preco_pauta,
COALESCE(i.vl_flex, 0) AS vl_flex,
COALESCE(i.comis, 0) AS comissao,
COALESCE(i.preco_ipi, 0) AS preco_com_ipi,
0 AS bc_ipi,
COALESCE(i.ipi, 0) AS vl_ipi,
0 AS bc_icmsst,
0 AS vl_icmsst,
0 AS vl_total_liquido,
COALESCE(i.total, 0) AS total,
'' AS num_oc,
'' AS item_oc,
i.id_tes
FROM sig.peditens i;
-- =============================================================================
-- 13. CONTAS A RECEBER (gerente.ctr UNION sig.ctr)
-- =============================================================================
CREATE OR REPLACE VIEW sarweb.vw_ctr AS
-- --- GERENTE ---
SELECT
'gerente' AS sistema,
c.id_empresa,
c.id_ctr,
c.prefixo,
c.numero,
c.docto AS documento,
c.deved AS id_cliente,
c.id_pedido,
c.emiss AS dt_emissao,
c.vecto AS dt_vencimento,
c.valor,
COALESCE(c.despcart, 0) AS despesa_cartorio,
c.saldo,
c.situacao,
c.dt_baixa,
c.cod_vendedor
FROM gerente.ctr c
UNION ALL
-- --- SIG ---
SELECT
'sig' AS sistema,
c.id_empresa,
c.id_ctr,
c.prefixo,
c.numero,
c.docto AS documento,
c.deved AS id_cliente,
c.id_entrega AS id_pedido, -- sig usa id_entrega
c.emiss AS dt_emissao,
c.vecto AS dt_vencimento,
c.valor,
0 AS despesa_cartorio,
c.saldo,
c.situacao,
c.data_baixa AS dt_baixa,
c.cod_vendedor
FROM sig.ctr c;
-- =============================================================================
-- 14. GRUPOS DE PRODUTOS
-- =============================================================================
CREATE OR REPLACE VIEW sarweb.vw_grupos AS
SELECT
id_empresa,
codigo,
descricao,
int_sar,
produto_variacao,
desc_max,
da AS dt_atualizacao
FROM gestao.grupo;
-- =============================================================================
-- 15. MARCAS
-- =============================================================================
CREATE OR REPLACE VIEW sarweb.vw_marcas AS
SELECT
id_empresa,
codigo,
nome
FROM gestao.marca;
-- =============================================================================
-- GRANTS (ajuste o role conforme seu ambiente)
-- =============================================================================
-- GRANT USAGE ON SCHEMA sarweb TO sarweb_app;
-- GRANT SELECT ON ALL TABLES IN SCHEMA sarweb TO sarweb_app;