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 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
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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: 'Nº 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>
|
||||||
|
|||||||
551
sarweb_views.sql
551
sarweb_views.sql
@@ -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;
|
|
||||||
|
|||||||
Reference in New Issue
Block a user