feat(web+api): catálogo com detalhe do produto, preços por pauta e consulta de pedidos ERP

- Catálogo: modal de detalhe centralizado (1100px) com identificação, estoque,
  preços base e lista de preços por pauta do representante (DISTINCT para evitar duplicatas)
- Contrato: loteMulVenda promovido para ProdutoSummarySchema; PautaPrecoSchema adicionado
- Pedido novo: valida lote múltiplo de venda — qtd inicial = lote, step = lote,
  InputNumber em erro quando inválido, alerta e bloqueio do submit
- Consulta ERP: tela OrdersErpPage com listagem de pedidos do ERP (P/E), contrato
  PedidoErpConsulta, endpoint no orders.controller, rota e sidebar

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-24 20:27:19 +00:00
parent 0858e1e941
commit a2bab75bad
15 changed files with 1804 additions and 492 deletions

View File

@@ -283,6 +283,7 @@ export class CatalogService {
ativo: Number(p.ativo),
qtdEstoque: p.qtd_estoque,
listaParauta: p.lista_pauta !== null ? Number(p.lista_pauta) : null,
loteMulVenda: p.lote_mul_venda !== null ? Number(p.lote_mul_venda) : null,
};
}
@@ -290,36 +291,62 @@ export class CatalogService {
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'));
const userId = this.cls.get('userId');
const codVendedor = userId ? parseInt(userId, 10) : 0;
const rows = await prisma.$queryRawUnsafe<ProdutoRow[]>(`
SELECT
p.id_erp,
p.codigo,
p.descricao,
p.unidade,
p.vl_preco1::text,
p.cod_grupo,
p.grupo,
p.cod_subgrupo,
p.subgrupo,
p.marca,
p.ativo,
e.qtd_estoque::text,
p.lista_pauta,
p.referencia,
p.descr_det,
p.vl_preco2::text,
p.vl_preco3::text,
p.aliq_ipi::text,
p.peso_liquido::text,
p.qtd_volume::text,
p.lote_mul_venda,
p.preco_promocional::text
FROM vw_produtos p
LEFT JOIN vw_estoque e ON e.id_erp = p.id_erp AND e.id_empresa = ${idEmpresa}
WHERE p.id_erp = ${idErp} AND p.ativo = 1
LIMIT 1
`);
interface PautaPrecoRow {
id_pauta: number;
codigo: number;
descricao: string;
preco: string;
}
const [rows, pautaRows] = await Promise.all([
prisma.$queryRawUnsafe<ProdutoRow[]>(`
SELECT
p.id_erp,
p.codigo,
p.descricao,
p.unidade,
p.vl_preco1::text,
p.cod_grupo,
p.grupo,
p.cod_subgrupo,
p.subgrupo,
p.marca,
p.ativo,
e.qtd_estoque::text,
p.lista_pauta,
p.referencia,
p.descr_det,
p.vl_preco2::text,
p.vl_preco3::text,
p.aliq_ipi::text,
p.peso_liquido::text,
p.qtd_volume::text,
p.lote_mul_venda,
p.preco_promocional::text
FROM vw_produtos p
LEFT JOIN vw_estoque e ON e.id_erp = p.id_erp AND e.id_empresa = ${idEmpresa}
WHERE p.id_erp = ${idErp} AND p.ativo = 1
LIMIT 1
`),
prisma.$queryRawUnsafe<PautaPrecoRow[]>(`
SELECT DISTINCT pa.id_pauta, pa.codigo, TRIM(pa.descricao) AS descricao, pp.preco1::text AS preco
FROM vw_pauta_produtos pp
JOIN vw_pautas pa ON pa.id_pauta = pp.id_pauta
AND pa.id_empresa = ${idEmpresa}
AND pa.ativo = 1
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
)
WHERE pp.id_produto = ${idErp}
AND r.codigo = ${codVendedor}
AND pp.preco1 > 0
ORDER BY pa.codigo
`),
]);
const p = rows[0];
if (!p) return null;
@@ -336,6 +363,12 @@ export class CatalogService {
loteMulVenda: p.lote_mul_venda !== null ? Number(p.lote_mul_venda) : null,
precoComIpi: null,
precoPromocional: p.preco_promocional,
pautaPrecos: pautaRows.map((r) => ({
idPauta: Number(r.id_pauta),
codigo: Number(r.codigo),
descricao: r.descricao,
preco: r.preco,
})),
};
}