feat(mvp-rep): formas de pagamento do ERP + suporte offline completo
Formas de pagamento: - Endpoint GET /catalog/payment-methods lendo vw_formas_pagamento filtrado por ativa=1 e integrar_sar=1 - FormaPagamento schema/type no shared api-interface - Hook useFormasPagamento (staleTime 1h) substituindo lista hardcoded Offline (FR-4.2 / NFR-2.1–2.4): - IndexedDB queue: lib/offline/idb.ts + order-queue.ts sem deps externos - NewOrderPage detecta !navigator.onLine → enqueueOrder() → toast + reset - useOfflineSync: auto-sync ao reconectar (POST orders + PATCH transmit) - usePendingOrders: fila reativa via CustomEvents - AppShell: banner offline + useOfflineSync() global - OrdersPage: seção de pedidos pendentes com retry/descartar - sw.js: network-first para API GETs cacheáveis + stale-while-revalidate para assets + app shell navigate fallback Docs: - architecture.md: documento de decisões de arquitetura do SAR MVP Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import type {
|
||||
EmpresaInfo,
|
||||
FormaPagamento,
|
||||
Pauta,
|
||||
ProdutoDetail,
|
||||
ProdutoListQuery,
|
||||
@@ -16,6 +18,14 @@ function escSql(s: string): string {
|
||||
return s.replace(/'/g, "''");
|
||||
}
|
||||
|
||||
// Produtos, pautas e estoque são por-empresa e vivem na MATRIZ. O ERP usa códigos
|
||||
// de empresa > 9000 para origem de pedido (ex.: 9001), mas o cadastro fica na
|
||||
// matriz correspondente (9001 → 1), espelhando o CASE de vw_pedidos_erp.
|
||||
// Pedidos continuam usando idEmpresa cru; só o catálogo normaliza.
|
||||
function matrizEmpresa(idEmpresa: number): number {
|
||||
return idEmpresa > 9000 ? idEmpresa - 9000 : idEmpresa;
|
||||
}
|
||||
|
||||
interface ProdutoRow {
|
||||
id_erp: number;
|
||||
codigo: string;
|
||||
@@ -45,10 +55,77 @@ interface ProdutoRow {
|
||||
export class CatalogService {
|
||||
constructor(private readonly cls: ClsService<WorkspaceClsStore>) {}
|
||||
|
||||
// Dados legais da empresa matriz que fatura o pedido (cabeçalho do PDF).
|
||||
async company(): Promise<EmpresaInfo> {
|
||||
const prisma = this.cls.get('prisma');
|
||||
if (!prisma) throw new Error('prisma não disponível no CLS');
|
||||
const idEmpresa = matrizEmpresa(this.cls.get('idEmpresa'));
|
||||
|
||||
interface Row {
|
||||
id_empresa: number;
|
||||
razao_social: string | null;
|
||||
nome_fantasia: string | null;
|
||||
cnpj: string | null;
|
||||
inscr_estadual: string | null;
|
||||
endereco: string | null;
|
||||
numero: string | null;
|
||||
complemento: string | null;
|
||||
bairro: string | null;
|
||||
cidade: string | null;
|
||||
uf: string | null;
|
||||
cep: string | null;
|
||||
telefone: string | null;
|
||||
email: string | null;
|
||||
}
|
||||
const rows = await prisma.$queryRawUnsafe<Row[]>(`
|
||||
SELECT e.id_empresa,
|
||||
TRIM(e.razao_social) AS razao_social,
|
||||
TRIM(e.nome) AS nome_fantasia,
|
||||
TRIM(e.cnpj) AS cnpj,
|
||||
TRIM(e.inscr_estadual) AS inscr_estadual,
|
||||
TRIM(e.endereco) AS endereco,
|
||||
NULLIF(e.numero, 0)::text AS numero,
|
||||
NULLIF(TRIM(e.complemento), '.') AS complemento,
|
||||
TRIM(e.bairro) AS bairro,
|
||||
TRIM(m.nome) AS cidade,
|
||||
TRIM(e.estado::text) AS uf,
|
||||
TRIM(e.cep::text) AS cep,
|
||||
TRIM(e.telefone::text) AS telefone,
|
||||
TRIM(e.email) AS email
|
||||
FROM gestao.empresa e
|
||||
LEFT JOIN sar.vw_municipios m ON m.id_municipio = e.id_municipio
|
||||
WHERE e.id_empresa = ${idEmpresa}
|
||||
LIMIT 1
|
||||
`);
|
||||
const r = rows[0];
|
||||
if (!r) throw new Error(`Empresa matriz ${idEmpresa} não encontrada`);
|
||||
|
||||
const clean = (v: string | null) => {
|
||||
const t = (v ?? '').trim();
|
||||
return t === '' ? null : t;
|
||||
};
|
||||
return {
|
||||
idEmpresa: Number(r.id_empresa),
|
||||
razaoSocial: clean(r.razao_social) ?? clean(r.nome_fantasia) ?? `Empresa ${r.id_empresa}`,
|
||||
nomeFantasia: clean(r.nome_fantasia),
|
||||
cnpj: clean(r.cnpj),
|
||||
inscricaoEstadual: clean(r.inscr_estadual),
|
||||
endereco: clean(r.endereco),
|
||||
numero: clean(r.numero),
|
||||
complemento: clean(r.complemento),
|
||||
bairro: clean(r.bairro),
|
||||
cidade: clean(r.cidade),
|
||||
uf: clean(r.uf),
|
||||
cep: clean(r.cep),
|
||||
telefone: clean(r.telefone),
|
||||
email: clean(r.email),
|
||||
};
|
||||
}
|
||||
|
||||
async pautas(): Promise<Pauta[]> {
|
||||
const prisma = this.cls.get('prisma');
|
||||
if (!prisma) throw new Error('prisma não disponível no CLS');
|
||||
const idEmpresa = this.cls.get('idEmpresa');
|
||||
const idEmpresa = matrizEmpresa(this.cls.get('idEmpresa'));
|
||||
const userId = this.cls.get('userId');
|
||||
const codVendedor = userId ? parseInt(userId, 10) : 0;
|
||||
|
||||
@@ -60,8 +137,7 @@ export class CatalogService {
|
||||
const rows = await prisma.$queryRawUnsafe<PautaRow[]>(`
|
||||
SELECT DISTINCT pa.id_pauta, pa.codigo, TRIM(pa.descricao) AS descricao
|
||||
FROM vw_pautas pa
|
||||
JOIN vw_representantes r ON r.id_empresa = pa.id_empresa
|
||||
AND pa.codigo IN (
|
||||
JOIN vw_representantes r ON pa.codigo IN (
|
||||
r.cod_pauta1, r.cod_pauta2, r.cod_pauta3,
|
||||
r.cod_pauta4, r.cod_pauta5, r.cod_pauta6
|
||||
)
|
||||
@@ -81,7 +157,7 @@ export class CatalogService {
|
||||
async list(query: ProdutoListQuery): Promise<ProdutoListResponse> {
|
||||
const prisma = this.cls.get('prisma');
|
||||
if (!prisma) throw new Error('prisma não disponível no CLS');
|
||||
const idEmpresa = this.cls.get('idEmpresa');
|
||||
const idEmpresa = matrizEmpresa(this.cls.get('idEmpresa'));
|
||||
|
||||
const { q, codGrupo, idPauta, page, limit } = query;
|
||||
const offset = (page - 1) * limit;
|
||||
@@ -183,7 +259,7 @@ export class CatalogService {
|
||||
async findOne(idErp: number): Promise<ProdutoDetail | null> {
|
||||
const prisma = this.cls.get('prisma');
|
||||
if (!prisma) throw new Error('prisma não disponível no CLS');
|
||||
const idEmpresa = this.cls.get('idEmpresa');
|
||||
const idEmpresa = matrizEmpresa(this.cls.get('idEmpresa'));
|
||||
|
||||
const rows = await prisma.$queryRawUnsafe<ProdutoRow[]>(`
|
||||
SELECT
|
||||
@@ -232,4 +308,33 @@ export class CatalogService {
|
||||
precoPromocional: p.preco_promocional,
|
||||
};
|
||||
}
|
||||
|
||||
async formasPagamento(): Promise<FormaPagamento[]> {
|
||||
const prisma = this.cls.get('prisma');
|
||||
if (!prisma) throw new Error('prisma não disponível no CLS');
|
||||
const idEmpresa = matrizEmpresa(this.cls.get('idEmpresa'));
|
||||
|
||||
interface Row {
|
||||
codigo: number;
|
||||
descricao: string;
|
||||
num_parcelas: number | null;
|
||||
tx_acrescimo: string;
|
||||
}
|
||||
|
||||
const rows = await prisma.$queryRawUnsafe<Row[]>(`
|
||||
SELECT codigo, TRIM(descricao) AS descricao, num_parcelas, tx_acrescimo::text
|
||||
FROM sar.vw_formas_pagamento
|
||||
WHERE id_empresa = ${idEmpresa}
|
||||
AND ativa = 1
|
||||
AND integrar_sar = 1
|
||||
ORDER BY codigo
|
||||
`);
|
||||
|
||||
return rows.map((r) => ({
|
||||
codigo: Number(r.codigo),
|
||||
descricao: r.descricao,
|
||||
numParcelas: r.num_parcelas !== null ? Number(r.num_parcelas) : null,
|
||||
txAcrescimo: r.tx_acrescimo ?? '0',
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user