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:
@@ -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`.
|
||||
@@ -13,6 +13,7 @@ import type { WorkspaceClsStore } from '../workspace/workspace.types';
|
||||
const ALERT_DAYS = 30;
|
||||
const INACTIVE_DAYS = 60;
|
||||
|
||||
// Usado apenas por findOne (já tem dt_ultima_compra calculado pelo SQL)
|
||||
function activityStatus(dtUltimaCompra: Date | null): ActivityStatus {
|
||||
if (!dtUltimaCompra) return 'inactive';
|
||||
const days = Math.floor((Date.now() - dtUltimaCompra.getTime()) / 86_400_000);
|
||||
@@ -51,6 +52,34 @@ interface ClientRow {
|
||||
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()
|
||||
export class ClientsService {
|
||||
constructor(private readonly cls: ClsService<WorkspaceClsStore>) {}
|
||||
@@ -72,56 +101,59 @@ export class ClientsService {
|
||||
? `AND (c.nome ILIKE '%${escSql(q)}%' OR c.cgcpf LIKE '%${escSql(q)}%')`
|
||||
: '';
|
||||
|
||||
const rows = await prisma.$queryRawUnsafe<ClientRow[]>(`
|
||||
SELECT
|
||||
c.id_cliente,
|
||||
c.id_empresa,
|
||||
c.nome,
|
||||
c.razao,
|
||||
c.cgcpf,
|
||||
c.email,
|
||||
c.telefone,
|
||||
c.cod_vendedor,
|
||||
c.limite_credito::text,
|
||||
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::text,
|
||||
c.dt_atual::text,
|
||||
MAX(p.dt_pedido) AS dt_ultima_compra
|
||||
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
|
||||
WHERE c.id_empresa = ${idEmpresa}
|
||||
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
|
||||
LIMIT ${limit} OFFSET ${offset}
|
||||
`);
|
||||
// Filtro de status calculado em SQL — evita paginação quebrada do filtro pós-SQL
|
||||
const statusFilter = status ? `AND ${ACTIVITY_CASE()} = '${status}'` : '';
|
||||
|
||||
const totalRows = await prisma.$queryRawUnsafe<[{ count: string }]>(`
|
||||
SELECT COUNT(*)::text AS count
|
||||
FROM vw_clientes c
|
||||
const baseWhere = `
|
||||
WHERE c.id_empresa = ${idEmpresa}
|
||||
AND c.ativo = 1
|
||||
${vendedorFilter}
|
||||
${searchFilter}
|
||||
`);
|
||||
${statusFilter}
|
||||
`;
|
||||
|
||||
const [rows, totalRows] = await Promise.all([
|
||||
prisma.$queryRawUnsafe<ClientRow[]>(`
|
||||
SELECT
|
||||
c.id_cliente,
|
||||
c.id_empresa,
|
||||
c.nome,
|
||||
c.razao,
|
||||
c.cgcpf,
|
||||
c.email,
|
||||
c.telefone,
|
||||
c.cod_vendedor,
|
||||
c.limite_credito::text,
|
||||
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::text,
|
||||
c.dt_atual::text,
|
||||
GREATEST(erp_ped.dt_max, sar_ped.dt_max) AS dt_ultima_compra
|
||||
FROM vw_clientes c
|
||||
${PEDIDOS_JOINS}
|
||||
${baseWhere}
|
||||
ORDER BY c.nome
|
||||
LIMIT ${limit} OFFSET ${offset}
|
||||
`),
|
||||
prisma.$queryRawUnsafe<[{ count: string }]>(`
|
||||
SELECT COUNT(*)::text AS count
|
||||
FROM vw_clientes c
|
||||
${PEDIDOS_JOINS}
|
||||
${baseWhere}
|
||||
`),
|
||||
]);
|
||||
|
||||
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),
|
||||
idEmpresa: Number(r.id_empresa),
|
||||
nome: r.nome,
|
||||
@@ -135,8 +167,6 @@ export class ClientsService {
|
||||
dtUltimaCompra: r.dt_ultima_compra?.toISOString() ?? null,
|
||||
}));
|
||||
|
||||
if (status) mapped = mapped.filter((c) => c.activityStatus === status);
|
||||
|
||||
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.bairro, c.cep, c.ddd, c.obs, c.cod_pauta,
|
||||
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
|
||||
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}
|
||||
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
|
||||
`);
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,11 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useMemo } from 'react';
|
||||
import {
|
||||
App,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Drawer,
|
||||
Dropdown,
|
||||
Empty,
|
||||
Grid,
|
||||
Row,
|
||||
Select,
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
Tag,
|
||||
Timeline,
|
||||
Typography,
|
||||
Divider,
|
||||
} from 'antd';
|
||||
import type { TableColumnsType } from 'antd';
|
||||
import type { MenuProps } from 'antd';
|
||||
@@ -29,6 +30,8 @@ import {
|
||||
FilePdfOutlined,
|
||||
PlusOutlined,
|
||||
ShoppingCartOutlined,
|
||||
ClearOutlined,
|
||||
SearchOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Link, useNavigate } from '@tanstack/react-router';
|
||||
import type { PedidoSummary } from '@sar/api-interface';
|
||||
@@ -68,7 +71,7 @@ function periodRange(p: string): { from?: string; to?: string } {
|
||||
return {};
|
||||
}
|
||||
|
||||
// ─── Status ───────────────────────────────────────────────────────────────────
|
||||
// ─── Status Config ────────────────────────────────────────────────────────────
|
||||
|
||||
const STATUS: Record<number, { label: string; color: string; rowBg: string; tagColor: string }> = {
|
||||
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' },
|
||||
};
|
||||
|
||||
// ─── OrderStatusBadge ─────────────────────────────────────────────────────────
|
||||
// ─── useOrderStats ────────────────────────────────────────────────────────────
|
||||
|
||||
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>
|
||||
);
|
||||
function useOrderStats() {
|
||||
const all = useOrderList({ limit: 1 });
|
||||
const pendentes = useOrderList({ limit: 1, situa: 1 });
|
||||
const aprovados = useOrderList({ limit: 1, situa: 2 });
|
||||
const faturados = useOrderList({ limit: 1, situa: 4 });
|
||||
return {
|
||||
total: all.data?.total ?? 0,
|
||||
pendentes: pendentes.data?.total ?? 0,
|
||||
aprovados: aprovados.data?.total ?? 0,
|
||||
faturados: faturados.data?.total ?? 0,
|
||||
loaded: !!all.data,
|
||||
};
|
||||
}
|
||||
|
||||
type OrderStats = ReturnType<typeof useOrderStats>;
|
||||
|
||||
// ─── OrdersMetrics ────────────────────────────────────────────────────────────
|
||||
|
||||
function OrdersMetrics({ data }: { data: PedidoSummary[] }) {
|
||||
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;
|
||||
|
||||
function OrdersMetrics({ stats }: { stats: OrderStats }) {
|
||||
const metrics = [
|
||||
{
|
||||
label: 'Total de Pedidos',
|
||||
value: String(data.length),
|
||||
value: stats.total,
|
||||
icon: <ShoppingCartOutlined />,
|
||||
color: '#003B8E',
|
||||
},
|
||||
{ label: 'Total Vendido', value: fmt(total), icon: <DollarOutlined />, color: '#389e0d' },
|
||||
{
|
||||
label: 'Ag. Aprovação',
|
||||
value: String(pendentes),
|
||||
value: stats.pendentes,
|
||||
icon: <ClockCircleOutlined />,
|
||||
color: '#d46b08',
|
||||
},
|
||||
{
|
||||
label: 'Aprovados',
|
||||
value: String(aprovados),
|
||||
icon: <CheckCircleOutlined />,
|
||||
color: '#389e0d',
|
||||
},
|
||||
{ label: 'Ticket Médio', value: fmt(ticket), icon: <DollarOutlined />, color: '#1d39c4' },
|
||||
{ label: 'Aprovados', value: stats.aprovados, icon: <CheckCircleOutlined />, color: '#389e0d' },
|
||||
{ label: 'Faturados', value: stats.faturados, icon: <DollarOutlined />, color: '#1d39c4' },
|
||||
];
|
||||
|
||||
return (
|
||||
<Row gutter={[12, 12]} style={{ marginBottom: 16 }}>
|
||||
<Row gutter={[12, 12]} style={{ marginBottom: 20 }}>
|
||||
{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
|
||||
style={{
|
||||
borderRadius: 10,
|
||||
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' } }}
|
||||
>
|
||||
<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>
|
||||
<Text
|
||||
style={{
|
||||
@@ -150,8 +160,8 @@ function OrdersMetrics({ data }: { data: PedidoSummary[] }) {
|
||||
>
|
||||
{m.label}
|
||||
</Text>
|
||||
<Text strong style={{ fontSize: 18, color: '#1F2937', lineHeight: 1.2 }}>
|
||||
{m.value}
|
||||
<Text strong style={{ fontSize: 20, color: '#1F2937', lineHeight: 1.2 }}>
|
||||
{stats.loaded ? m.value.toLocaleString('pt-BR') : <Spin size="small" />}
|
||||
</Text>
|
||||
</div>
|
||||
</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 ─────────────────────────────────────────────────────────
|
||||
|
||||
function OrderActionsMenu({
|
||||
@@ -250,7 +275,7 @@ function OrderDetailDrawer({ id, onClose }: { id: string | null; onClose: () =>
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.08em',
|
||||
textTransform: 'uppercase',
|
||||
color: '#64748B',
|
||||
color: '#94A3B8',
|
||||
marginBottom: 2,
|
||||
display: 'block',
|
||||
};
|
||||
@@ -261,6 +286,7 @@ function OrderDetailDrawer({ id, onClose }: { id: string | null; onClose: () =>
|
||||
open={!!id}
|
||||
onClose={onClose}
|
||||
width={520}
|
||||
placement="right"
|
||||
styles={{ body: { padding: '16px 24px' } }}
|
||||
footer={
|
||||
<Space>
|
||||
@@ -296,13 +322,13 @@ function OrderDetailDrawer({ id, onClose }: { id: string | null; onClose: () =>
|
||||
<span style={label}>Data</span>
|
||||
<Text>{fmtDate(data.dtPedido)}</Text>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Col span={24}>
|
||||
<span style={label}>Cliente</span>
|
||||
<Text strong>
|
||||
<Text strong style={{ display: 'block' }}>
|
||||
{data.razaoCliente ?? data.nomeCliente ?? `Cód. ${data.idCliente}`}
|
||||
</Text>
|
||||
{data.nomeCliente && data.razaoCliente && (
|
||||
<Text type="secondary" style={{ fontSize: 12, display: 'block' }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{data.nomeCliente}
|
||||
</Text>
|
||||
)}
|
||||
@@ -313,6 +339,12 @@ function OrderDetailDrawer({ id, onClose }: { id: string | null; onClose: () =>
|
||||
{fmt(data.total)}
|
||||
</Text>
|
||||
</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 && (
|
||||
<Col span={24}>
|
||||
<span style={label}>Observações</span>
|
||||
@@ -394,6 +426,7 @@ function MobileOrderCard({
|
||||
}) {
|
||||
const navigate = useNavigate();
|
||||
const cfg = STATUS[order.situa];
|
||||
const nome = order.razaoCliente ?? order.nomeCliente;
|
||||
|
||||
return (
|
||||
<Card
|
||||
@@ -402,18 +435,22 @@ function MobileOrderCard({
|
||||
marginBottom: 10,
|
||||
border: `1px solid ${cfg?.rowBg ?? '#EBF0F5'}`,
|
||||
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' } }}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
|
||||
<Text strong style={{ fontSize: 15 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
|
||||
<Text strong style={{ fontSize: 15, color: '#003B8E' }}>
|
||||
{order.numPedSar}
|
||||
</Text>
|
||||
<OrderStatusBadge situa={order.situa} descr={order.statusDescr} />
|
||||
</div>
|
||||
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>
|
||||
{order.razaoCliente ?? order.nomeCliente ?? `Cód. ${order.idCliente}`} ·{' '}
|
||||
{nome && (
|
||||
<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)}
|
||||
</Text>
|
||||
<Text strong style={{ fontSize: 16, color: '#003B8E' }}>
|
||||
@@ -425,12 +462,14 @@ function MobileOrderCard({
|
||||
icon={<EyeOutlined />}
|
||||
disabled={order.fonte === 'erp'}
|
||||
onClick={() => onView(order.id)}
|
||||
style={{ borderRadius: 6 }}
|
||||
>
|
||||
Ver
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
style={{ borderRadius: 6 }}
|
||||
onClick={() =>
|
||||
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 ───────────────────────────────────────────────────────────────
|
||||
|
||||
export function OrdersPage() {
|
||||
const navigate = useNavigate();
|
||||
const screens = useBreakpoint();
|
||||
const isMobile = !screens.md;
|
||||
const { message: msg } = App.useApp();
|
||||
|
||||
const stats = useOrderStats();
|
||||
|
||||
const [search, setSearch] = useState('');
|
||||
const [query, setQuery] = useState('');
|
||||
const [situaFilter, setSituaFilter] = useState<number | undefined>();
|
||||
const [period, setPeriod] = useState('');
|
||||
const [page, setPage] = useState(1);
|
||||
@@ -482,8 +501,8 @@ export function OrdersPage() {
|
||||
|
||||
const { from, to } = period ? periodRange(period) : {};
|
||||
|
||||
const { data, isLoading } = useOrderList({
|
||||
numPedSar: search || undefined,
|
||||
const { data, isLoading, isFetching } = useOrderList({
|
||||
numPedSar: query || undefined,
|
||||
situa: situaFilter,
|
||||
from,
|
||||
to,
|
||||
@@ -494,28 +513,39 @@ export function OrdersPage() {
|
||||
const rows = data?.data ?? [];
|
||||
const total = data?.total ?? 0;
|
||||
|
||||
const hasFilters = !!query || !!situaFilter || !!period;
|
||||
|
||||
function commitSearch() {
|
||||
setQuery(search.trim());
|
||||
setPage(1);
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
setSearch('');
|
||||
setQuery('');
|
||||
setSituaFilter(undefined);
|
||||
setPeriod('');
|
||||
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> = [
|
||||
{
|
||||
title: 'Nº Pedido',
|
||||
dataIndex: 'numPedSar',
|
||||
title: 'Pedido',
|
||||
key: 'pedido',
|
||||
width: 140,
|
||||
render: (_: string, row: PedidoSummary) => {
|
||||
render: (_: unknown, row: PedidoSummary) => {
|
||||
const label = row.numero ? String(row.numero) : row.numPedSar;
|
||||
return row.fonte === 'erp' ? (
|
||||
<Text strong className="tabular-nums">
|
||||
<Text strong className="tabular-nums" style={{ color: '#1F2937' }}>
|
||||
{label}
|
||||
</Text>
|
||||
) : (
|
||||
<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}
|
||||
</Text>
|
||||
</Link>
|
||||
@@ -525,28 +555,37 @@ export function OrdersPage() {
|
||||
{
|
||||
title: 'Cliente',
|
||||
key: 'cliente',
|
||||
ellipsis: true,
|
||||
minWidth: 240,
|
||||
render: (_: unknown, row: PedidoSummary) => {
|
||||
const nome = row.razaoCliente ?? row.nomeCliente;
|
||||
const subtit = row.nomeCliente && row.razaoCliente ? row.nomeCliente : null;
|
||||
return (
|
||||
<Space direction="vertical" size={0}>
|
||||
<div>
|
||||
{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>
|
||||
)}
|
||||
{row.nomeCliente && row.razaoCliente && (
|
||||
<Text type="secondary" style={{ fontSize: 11 }}>
|
||||
{row.nomeCliente}
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>
|
||||
Cód. {row.idCliente}
|
||||
</Text>
|
||||
)}
|
||||
</Space>
|
||||
{subtit && (
|
||||
<Text style={{ fontSize: 12, color: '#64748B', display: 'block', lineHeight: 1.3 }}>
|
||||
{subtit}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
dataIndex: 'situa',
|
||||
key: 'status',
|
||||
width: 140,
|
||||
render: (s: number, row: PedidoSummary) => (
|
||||
<OrderStatusBadge situa={s} descr={row.statusDescr} />
|
||||
@@ -555,10 +594,11 @@ export function OrdersPage() {
|
||||
{
|
||||
title: 'Total',
|
||||
dataIndex: 'total',
|
||||
key: 'total',
|
||||
width: 130,
|
||||
align: 'right',
|
||||
align: 'right' as const,
|
||||
render: (v: string) => (
|
||||
<Text strong className="tabular-nums">
|
||||
<Text strong className="tabular-nums" style={{ color: '#003B8E', fontSize: 14 }}>
|
||||
{fmt(v)}
|
||||
</Text>
|
||||
),
|
||||
@@ -566,22 +606,34 @@ export function OrdersPage() {
|
||||
{
|
||||
title: 'Data',
|
||||
dataIndex: 'dtPedido',
|
||||
key: 'dtPedido',
|
||||
width: 110,
|
||||
render: (v: string) => <Text type="secondary">{fmtDate(v)}</Text>,
|
||||
render: (v: string) => <Text style={{ fontSize: 13, color: '#475569' }}>{fmtDate(v)}</Text>,
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
key: 'actions',
|
||||
width: 48,
|
||||
width: 100,
|
||||
render: (_: unknown, row: PedidoSummary) => (
|
||||
<OrderActionsMenu order={row} onView={(id) => setDrawerOrderId(id)} />
|
||||
<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)} />
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 1200, margin: '0 auto' }}>
|
||||
{/* ── Cabeçalho ───────────────────────────────────────────────── */}
|
||||
{/* ── Cabeçalho ─────────────────────────────────────────────────── */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
@@ -595,7 +647,7 @@ export function OrdersPage() {
|
||||
Pedidos
|
||||
</Title>
|
||||
<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>
|
||||
</div>
|
||||
{!isMobile && (
|
||||
@@ -603,49 +655,70 @@ export function OrdersPage() {
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
size="large"
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
fontWeight: 600,
|
||||
backgroundColor: '#389e0d',
|
||||
borderColor: '#389e0d',
|
||||
}}
|
||||
onClick={() => void navigate({ to: '/pedidos/novo' })}
|
||||
style={{ borderRadius: 8, fontWeight: 600 }}
|
||||
>
|
||||
Novo Pedido
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── Métricas ────────────────────────────────────────────────── */}
|
||||
<OrdersMetrics data={rows} />
|
||||
{/* ── Métricas ──────────────────────────────────────────────────── */}
|
||||
<OrdersMetrics stats={stats} />
|
||||
|
||||
{/* ── Filtros ─────────────────────────────────────────────────── */}
|
||||
{/* ── Filtros ───────────────────────────────────────────────────── */}
|
||||
<Card
|
||||
style={{
|
||||
borderRadius: 10,
|
||||
border: '1px solid #EBF0F5',
|
||||
boxShadow: '0 1px 4px rgba(0,0,0,0.06)',
|
||||
marginBottom: 16,
|
||||
boxShadow: '0 1px 4px rgba(0,0,0,0.05)',
|
||||
marginBottom: 20,
|
||||
}}
|
||||
styles={{ body: { padding: '14px 20px' } }}
|
||||
>
|
||||
<Row gutter={[12, 12]} align="middle">
|
||||
<Col xs={24} sm={24} md={8}>
|
||||
<input
|
||||
value={search}
|
||||
onChange={(e) => {
|
||||
setSearch(e.target.value);
|
||||
setPage(1);
|
||||
}}
|
||||
placeholder="Buscar por nº do pedido..."
|
||||
style={{
|
||||
width: '100%',
|
||||
height: 32,
|
||||
padding: '0 11px',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: 6,
|
||||
fontSize: 14,
|
||||
outline: 'none',
|
||||
color: '#1F2937',
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
/>
|
||||
{/* 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
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') commitSearch();
|
||||
}}
|
||||
onBlur={commitSearch}
|
||||
placeholder="Buscar por nº do pedido..."
|
||||
style={{
|
||||
width: '100%',
|
||||
height: 32,
|
||||
padding: '0 11px 0 32px',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: 6,
|
||||
fontSize: 13,
|
||||
outline: 'none',
|
||||
color: '#1F2937',
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
{/* Status */}
|
||||
<Col xs={12} sm={8} md={5}>
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
@@ -664,6 +737,8 @@ export function OrdersPage() {
|
||||
]}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
{/* Período */}
|
||||
<Col xs={12} sm={8} md={5}>
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
@@ -681,19 +756,29 @@ export function OrdersPage() {
|
||||
]}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={8} md={4}>
|
||||
|
||||
{/* Limpar */}
|
||||
<Col xs={12} sm={8} md={3}>
|
||||
<Button
|
||||
style={{ width: '100%', borderRadius: 6 }}
|
||||
icon={<ClearOutlined />}
|
||||
disabled={!hasFilters}
|
||||
onClick={clearFilters}
|
||||
disabled={!search && !situaFilter && !period}
|
||||
>
|
||||
Limpar filtros
|
||||
Limpar
|
||||
</Button>
|
||||
</Col>
|
||||
|
||||
{/* Contador */}
|
||||
<Col>
|
||||
<Text style={{ fontSize: 12, color: '#94A3B8' }}>
|
||||
{data?.total !== undefined ? `${total.toLocaleString('pt-BR')} pedidos` : '…'}
|
||||
</Text>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
{/* ── Conteúdo principal ──────────────────────────────────────── */}
|
||||
{/* ── Lista / tabela ────────────────────────────────────────────── */}
|
||||
{isLoading ? (
|
||||
<div style={{ textAlign: 'center', padding: 64 }}>
|
||||
<Spin size="large" />
|
||||
@@ -703,27 +788,51 @@ export function OrdersPage() {
|
||||
style={{ borderRadius: 10, border: '1px solid #EBF0F5' }}
|
||||
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>
|
||||
) : isMobile ? (
|
||||
/* ── Mobile: cards ─────────────────────────────────────────── */
|
||||
/* ── Mobile ────────────────────────────────────────────────────── */
|
||||
<div>
|
||||
{rows.map((o) => (
|
||||
<MobileOrderCard key={o.id} order={o} onView={(id) => setDrawerOrderId(id)} />
|
||||
))}
|
||||
<div
|
||||
style={{ textAlign: 'center', padding: '8px 0 16px', color: '#64748B', fontSize: 13 }}
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: '#94A3B8',
|
||||
display: 'block',
|
||||
textAlign: 'center',
|
||||
padding: '8px 0 16px',
|
||||
}}
|
||||
>
|
||||
Mostrando {rows.length} de {total} pedidos
|
||||
</div>
|
||||
</Text>
|
||||
</div>
|
||||
) : (
|
||||
/* ── Desktop: tabela ────────────────────────────────────────── */
|
||||
/* ── Desktop ────────────────────────────────────────────────────── */
|
||||
<Card
|
||||
style={{
|
||||
borderRadius: 10,
|
||||
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 } }}
|
||||
>
|
||||
@@ -732,6 +841,8 @@ export function OrdersPage() {
|
||||
columns={columns}
|
||||
dataSource={rows}
|
||||
size="middle"
|
||||
loading={isFetching}
|
||||
scroll={{ x: 900 }}
|
||||
onRow={(row) => ({
|
||||
onClick: () => {
|
||||
if (row.fonte !== 'erp') setDrawerOrderId(row.id);
|
||||
@@ -739,6 +850,7 @@ export function OrdersPage() {
|
||||
style: {
|
||||
background: STATUS[row.situa]?.rowBg ?? '#fff',
|
||||
cursor: row.fonte !== 'erp' ? 'pointer' : 'default',
|
||||
verticalAlign: 'top',
|
||||
},
|
||||
})}
|
||||
pagination={{
|
||||
@@ -751,11 +863,21 @@ export function OrdersPage() {
|
||||
style: { padding: '12px 24px' },
|
||||
}}
|
||||
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>
|
||||
)}
|
||||
|
||||
{/* ── Drawer de detalhe ───────────────────────────────────────── */}
|
||||
{/* ── Drawer de detalhe ─────────────────────────────────────────── */}
|
||||
<OrderDetailDrawer id={drawerOrderId} onClose={() => setDrawerOrderId(null)} />
|
||||
|
||||
{/* 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>{`
|
||||
.ant-table-row:hover td { background: inherit !important; filter: brightness(0.97); }
|
||||
`}</style>
|
||||
|
||||
605
sarweb_views.sql
605
sarweb_views.sql
@@ -1,407 +1,73 @@
|
||||
-- =============================================================================
|
||||
-- sarweb_views.sql
|
||||
-- Views PostgreSQL para o projeto SARWeb
|
||||
-- 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á.
|
||||
-- sar_views.sql
|
||||
-- Views PostgreSQL do projeto SAR — schema `sar` no banco ERP (libreplast)
|
||||
--
|
||||
-- Para saber qual schema o seu banco usa, execute:
|
||||
-- SELECT schemaname FROM pg_tables WHERE tablename='pedidos' AND schemaname NOT IN ('sarpalm');
|
||||
-- Executar como: psql -U postgres -d libreplast -f sar_views.sql
|
||||
--
|
||||
-- STATUS — mapeamento normalizado por sistema:
|
||||
-- GERENTE: situa 1=Pendente | 2=Liberado | 3=Faturado | 4=Cancelado
|
||||
-- SIG: situa 1=Pendente | 2=Liberado | 4=Faturado | 5=Cancelado
|
||||
-- DECISÃO DE ARQUITETURA: todas as views residem no schema `sar` (mesmo schema
|
||||
-- das tabelas Prisma). O search_path da conexão runtime é `sar`, portanto as
|
||||
-- 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. EMPRESAS
|
||||
-- =============================================================================
|
||||
CREATE OR REPLACE VIEW sarweb.vw_empresas AS
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 1. CLIENTES
|
||||
-- Fonte: sig.corrent
|
||||
-- Obs: COALESCE(id_empresa, 1) cobre registros antigos sem id_empresa.
|
||||
-- cod_vendedor determina a carteira do representante.
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE OR REPLACE VIEW sar.vw_clientes AS
|
||||
SELECT
|
||||
e.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,
|
||||
COALESCE(c.id_empresa, 1) AS id_empresa,
|
||||
c.id_corrent AS id_cliente,
|
||||
c.ativo,
|
||||
c.nome,
|
||||
c.razao,
|
||||
c.pesso AS pessoa, -- 0=PJ 1=PF
|
||||
COALESCE(NULLIF(TRIM(c.nome), ''), TRIM(c.razao)) AS nome,
|
||||
TRIM(c.razao) AS razao,
|
||||
c.pesso AS pessoa,
|
||||
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.inscr AS inscricao_estadual,
|
||||
c.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.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
|
||||
LEFT JOIN gestao.empresa e ON e.id_empresa = COALESCE(c.id_empresa, 1)
|
||||
WHERE fp.id_empresa = COALESCE(e.id_matriz, COALESCE(c.id_empresa, 1))
|
||||
AND fp.codigo = c.cod_formapag
|
||||
LIMIT 1
|
||||
) AS id_formapag,
|
||||
) AS id_formapag,
|
||||
c.indicador_ie,
|
||||
c.cod_pauta,
|
||||
c.st_especifica,
|
||||
COALESCE(c.limcred, 0) AS limite_credito,
|
||||
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.ativo,
|
||||
c.nome,
|
||||
c.razao,
|
||||
c.pesso AS pessoa,
|
||||
c.consfinal,
|
||||
c.cgcpf,
|
||||
c.suf_cgcpf,
|
||||
c.inscr AS inscricao_estadual,
|
||||
c.endereco 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
|
||||
LEFT JOIN gestao.empresa e ON e.id_empresa = c.id_empresa
|
||||
WHERE fp.id_empresa = COALESCE(e.id_matriz, 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,
|
||||
0 AS desc_cliente_rede,
|
||||
c.dt_atual
|
||||
FROM sig.corrent c;
|
||||
|
||||
-- =============================================================================
|
||||
-- 4. MUNICÍPIOS
|
||||
-- =============================================================================
|
||||
CREATE OR REPLACE VIEW sarweb.vw_municipios AS
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 2. PEDIDOS ERP
|
||||
-- Fonte: sig.pedidos
|
||||
-- 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
|
||||
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_pedido,
|
||||
p.num_ped_sar,
|
||||
@@ -414,166 +80,35 @@ SELECT
|
||||
WHEN 4 THEN 'Faturado'
|
||||
WHEN 5 THEN 'Cancelado'
|
||||
ELSE 'Enviado'
|
||||
END AS status_descr,
|
||||
p.data AS dt_pedido,
|
||||
p.data_emissao AS dt_emissao,
|
||||
p.clien AS id_cliente,
|
||||
END AS status_descr,
|
||||
p.data AS dt_pedido,
|
||||
p.data_emissao AS dt_emissao,
|
||||
p.clien AS id_cliente,
|
||||
p.cod_vendedor,
|
||||
p.cod_formapag,
|
||||
fp.id_formapag,
|
||||
fp.descr AS forma_pagamento,
|
||||
NULL::integer AS num_pauta,
|
||||
fp.descr AS forma_pagamento,
|
||||
pau.id_pauta,
|
||||
COALESCE(p.obs, '') AS obs,
|
||||
p.totpr AS total_produtos,
|
||||
COALESCE(p.ipi, 0) AS total_ipi,
|
||||
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.tx_acrescimo, 0) AS acrescimo,
|
||||
COALESCE(p.com_fat, 0) AS comissao,
|
||||
COALESCE(p.ped_flex, 0) AS ped_flex,
|
||||
p.cod_vend2 AS cod_supervisor,
|
||||
p.tx_com_vend2 AS taxa_com_super
|
||||
COALESCE(p.obs, '') AS obs,
|
||||
p.totpr AS total_produtos,
|
||||
COALESCE(p.ipi, 0) AS total_ipi,
|
||||
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.tx_acrescimo, 0) AS acrescimo,
|
||||
COALESCE(p.com_fat, 0) AS comissao,
|
||||
COALESCE(p.ped_flex::integer, 0) AS ped_flex,
|
||||
p.cod_vend2 AS cod_supervisor,
|
||||
p.tx_com_vend2 AS taxa_com_super
|
||||
FROM sig.pedidos p
|
||||
LEFT JOIN gestao.formapag fp ON fp.codigo = p.cod_formapag
|
||||
AND fp.id_empresa = CASE WHEN p.id_empresa > 9000 THEN p.id_empresa - 9000 ELSE p.id_empresa END
|
||||
LEFT JOIN gestao.pauta pau ON pau.codigo = p.cod_pauta
|
||||
AND pau.id_empresa = CASE WHEN p.id_empresa > 9000 THEN p.id_empresa - 9000 ELSE p.id_empresa END;
|
||||
|
||||
-- =============================================================================
|
||||
-- 12. ITENS DE PEDIDO (gerente.peditens UNION sig.peditens)
|
||||
-- =============================================================================
|
||||
CREATE OR REPLACE VIEW sarweb.vw_peditens AS
|
||||
|
||||
-- --- 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;
|
||||
LEFT JOIN gestao.formapag fp
|
||||
ON fp.codigo = p.cod_formapag
|
||||
AND fp.id_empresa = CASE WHEN p.id_empresa > 9000
|
||||
THEN p.id_empresa - 9000
|
||||
ELSE p.id_empresa END
|
||||
LEFT JOIN gestao.pauta pau
|
||||
ON pau.codigo = p.cod_pauta
|
||||
AND pau.id_empresa = CASE WHEN p.id_empresa > 9000
|
||||
THEN p.id_empresa - 9000
|
||||
ELSE p.id_empresa END;
|
||||
|
||||
Reference in New Issue
Block a user