feat(web+api): cockpit rep — novo pedido v2, endereço de entrega e cancelamento de orçamento
- NewOrderPage: etapas 1-5 — merge de cards, busca de produto melhorada (autocomplete com estoque + modal catálogo), responsividade mobile (tabela → cards, footer compacto), histórico de compras do cliente e campo de endereço de entrega (cadastrado vs livre) - endEntrega: campo TEXT adicionado em sar.pedidos; contrato CreatePedidoSchema e PedidoDetailSchema atualizados; aparece no detalhe (OrderDetailPage, OrderDetailModal) e no PDF (OrderPrintPage) - Cancelamento de orçamento: PATCH /orders/:id/cancel — rep cancela seu próprio orçamento (situa 0); histórico registrado; UI com Modal.confirm substituindo alert() em OrderActionsMenu - Fix: useClientDetail chamado para cliente selecionado manualmente, garantindo que clientEndStr seja preenchido mesmo sem clientIdParam na URL Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -439,6 +439,7 @@ export class ClientsService {
|
||||
|
||||
interface Row {
|
||||
id_produto: number;
|
||||
codigo: string;
|
||||
descricao: string;
|
||||
unidade: string | null;
|
||||
qtd_total: string;
|
||||
@@ -449,37 +450,39 @@ export class ClientsService {
|
||||
|
||||
const rows = await prisma.$queryRawUnsafe<Row[]>(`
|
||||
SELECT
|
||||
i.produ AS id_produto,
|
||||
i.id_produto,
|
||||
TRIM(p.codigo) AS codigo,
|
||||
TRIM(p.descricao) AS descricao,
|
||||
TRIM(p.unidade) AS unidade,
|
||||
SUM(i.qtd)::numeric(15,3)::text AS qtd_total,
|
||||
COUNT(DISTINCT ped.id_pedido)::text AS num_pedidos,
|
||||
MAX(ped.data) AS ultima_compra,
|
||||
MAX(ped.dt_pedido) AS ultima_compra,
|
||||
(
|
||||
SELECT i2.pruni::numeric(15,2)::text
|
||||
FROM sig.peditens i2
|
||||
JOIN sig.pedidos p2 ON p2.id_pedido = i2.id_pedido
|
||||
WHERE i2.produ = i.produ
|
||||
AND p2.clien = ped.clien
|
||||
AND p2.id_empresa = ${idEmpresa}
|
||||
AND p2.situa NOT IN (5)
|
||||
ORDER BY p2.data DESC, p2.id_pedido DESC
|
||||
SELECT i2.preco_unitario::numeric(15,2)::text
|
||||
FROM sar.vw_peditens_erp i2
|
||||
JOIN sar.vw_pedidos_erp p2 ON p2.id_pedido = i2.id_pedido
|
||||
WHERE i2.id_produto = i.id_produto
|
||||
AND p2.id_cliente = ped.id_cliente
|
||||
AND p2.id_empresa = ${idEmpresa}
|
||||
AND p2.situa NOT IN (5)
|
||||
ORDER BY p2.dt_pedido DESC, p2.id_pedido DESC
|
||||
LIMIT 1
|
||||
) AS ultimo_preco
|
||||
FROM sig.pedidos ped
|
||||
JOIN sig.peditens i ON i.id_pedido = ped.id_pedido
|
||||
JOIN gestao.produto p ON p.id_erp = i.produ
|
||||
AND p.id_empresa = ${idEmpresaMatriz}
|
||||
WHERE ped.clien = ${idCliente}
|
||||
AND ped.id_empresa = ${idEmpresa}
|
||||
AND ped.situa NOT IN (5)
|
||||
GROUP BY i.produ, p.descricao, p.unidade, ped.clien
|
||||
FROM sar.vw_pedidos_erp ped
|
||||
JOIN sar.vw_peditens_erp i ON i.id_pedido = ped.id_pedido
|
||||
JOIN sar.vw_produtos p ON p.id_erp = i.id_produto
|
||||
AND p.id_empresa = ${idEmpresaMatriz}
|
||||
WHERE ped.id_cliente = ${idCliente}
|
||||
AND ped.id_empresa = ${idEmpresa}
|
||||
AND ped.situa NOT IN (5)
|
||||
GROUP BY i.id_produto, p.descricao, p.unidade, ped.id_cliente
|
||||
ORDER BY SUM(i.qtd) DESC
|
||||
LIMIT ${limit}
|
||||
`);
|
||||
|
||||
return rows.map((r) => ({
|
||||
idProduto: Number(r.id_produto),
|
||||
codProduto: r.codigo ?? '',
|
||||
descricao: r.descricao,
|
||||
unidade: r.unidade,
|
||||
qtdTotal: parseFloat(r.qtd_total ?? '0'),
|
||||
@@ -646,12 +649,12 @@ export class ClientsService {
|
||||
nf.dt_emissao,
|
||||
nf.vl_nf::text AS vl_nf,
|
||||
TRIM(nf.chave_acesso_nfe) AS chave_acesso_nfe
|
||||
FROM gestao.nf nf
|
||||
JOIN sig.pedidos p
|
||||
FROM sar.vw_nf nf
|
||||
JOIN sar.vw_pedidos_erp p
|
||||
ON p.numero = nf.num_entrega
|
||||
AND p.tipo = 'E'
|
||||
AND p.id_empresa IN (1, 9001)
|
||||
WHERE p.clien = $1
|
||||
WHERE p.id_cliente = $1
|
||||
AND nf.id_empresa = 1
|
||||
AND nf.status = 'E'
|
||||
AND TRIM(COALESCE(nf.chave_acesso_nfe, '')) != ''
|
||||
|
||||
@@ -84,6 +84,11 @@ export class OrdersController {
|
||||
return this.orders.reject(id, parsed);
|
||||
}
|
||||
|
||||
@Patch(':id/cancel')
|
||||
cancel(@Param('id', ParseUUIDPipe) id: string): Promise<PedidoDetail> {
|
||||
return this.orders.cancel(id);
|
||||
}
|
||||
|
||||
@Get('erp/:idPedido')
|
||||
findOneErp(@Param('idPedido', ParseIntPipe) idPedido: number): Promise<PedidoDetail> {
|
||||
return this.orders.findOneErp(idPedido);
|
||||
|
||||
@@ -128,10 +128,10 @@ export class OrdersService {
|
||||
const dataHistoricoStr = dataHistorico.toISOString().split('T')[0];
|
||||
|
||||
const vendedorFilter = role === 'rep' ? `AND a.cod_vendedor = ${codVendedor}` : '';
|
||||
const fromFilterE = from ? `AND COALESCE(a.data, b.data) >= '${from}'` : '';
|
||||
const toFilterE = to ? `AND COALESCE(a.data, b.data) <= '${to}'` : '';
|
||||
const fromFilterP = from ? `AND a.data >= '${from}'` : '';
|
||||
const toFilterP = to ? `AND a.data <= '${to}'` : '';
|
||||
const fromFilterE = from ? `AND COALESCE(a.dt_pedido, b.dt_pedido) >= '${from}'` : '';
|
||||
const toFilterE = to ? `AND COALESCE(a.dt_pedido, b.dt_pedido) <= '${to}'` : '';
|
||||
const fromFilterP = from ? `AND a.dt_pedido >= '${from}'` : '';
|
||||
const toFilterP = to ? `AND a.dt_pedido <= '${to}'` : '';
|
||||
const safe = (s: string) => s.replace(/'/g, "''");
|
||||
const searchFilter = search
|
||||
? `AND (r.numero_pedido::text ILIKE '%${safe(search)}%' OR LOWER(cl.nome) LIKE LOWER('%${safe(search)}%') OR LOWER(cl.razao) LIKE LOWER('%${safe(search)}%'))`
|
||||
@@ -164,14 +164,14 @@ export class OrdersService {
|
||||
WITH all_rows AS (
|
||||
SELECT
|
||||
b.numero AS numero_pedido,
|
||||
COALESCE(b.data, a.data) AS data,
|
||||
a.dtprevent AS dt_prevent,
|
||||
a.data_emissao,
|
||||
COALESCE(b.dt_pedido, a.dt_pedido) AS data,
|
||||
a.dt_prevent,
|
||||
a.dt_emissao AS data_emissao,
|
||||
a.situa,
|
||||
NULL::text AS status_descr,
|
||||
a.clien::integer AS id_cliente,
|
||||
a.id_cliente,
|
||||
a.cod_vendedor,
|
||||
TRIM(c.descr) AS forma_pagamento,
|
||||
a.forma_pagamento,
|
||||
a.total::text,
|
||||
a.obs,
|
||||
'E'::varchar(1) AS tipo,
|
||||
@@ -181,36 +181,33 @@ export class OrdersService {
|
||||
d.chave_acesso_nfe,
|
||||
b.situa AS sit_ped,
|
||||
a.id_pedido
|
||||
FROM sig.pedidos a
|
||||
LEFT JOIN sig.pedidos b
|
||||
FROM sar.vw_pedidos_erp a
|
||||
LEFT JOIN sar.vw_pedidos_erp b
|
||||
ON a.numer_ped_vinc = b.numero
|
||||
AND a.id_empresa = b.id_empresa
|
||||
AND b.tipo = 'P'
|
||||
LEFT JOIN gestao.formapag c
|
||||
ON c.id_empresa = ${idMatriz}
|
||||
AND a.cod_formapag = c.codigo
|
||||
LEFT JOIN gestao.nf d
|
||||
LEFT JOIN sar.vw_nf d
|
||||
ON a.numero = d.num_entrega
|
||||
AND d.id_empresa = ${idMatriz}
|
||||
WHERE a.id_empresa = ${idEmpresa}
|
||||
AND a.tipo = 'E'
|
||||
${vendedorFilter}
|
||||
AND b.id_pedido IS NOT NULL
|
||||
AND COALESCE(a.data, b.data) >= '${dataHistoricoStr}'
|
||||
AND COALESCE(a.dt_pedido, b.dt_pedido) >= '${dataHistoricoStr}'
|
||||
${fromFilterE} ${toFilterE}
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
a.numero AS numero_pedido,
|
||||
a.data,
|
||||
a.dtprevent AS dt_prevent,
|
||||
a.data_emissao,
|
||||
a.dt_pedido AS data,
|
||||
a.dt_prevent,
|
||||
a.dt_emissao AS data_emissao,
|
||||
a.situa,
|
||||
c.descricao AS status_descr,
|
||||
a.clien::integer AS id_cliente,
|
||||
a.id_cliente,
|
||||
a.cod_vendedor,
|
||||
TRIM(b.descr) AS forma_pagamento,
|
||||
a.forma_pagamento,
|
||||
a.total::text,
|
||||
a.obs,
|
||||
'P'::varchar(1) AS tipo,
|
||||
@@ -220,10 +217,7 @@ export class OrdersService {
|
||||
NULL::varchar AS chave_acesso_nfe,
|
||||
NULL::integer AS sit_ped,
|
||||
a.id_pedido
|
||||
FROM sig.pedidos a
|
||||
LEFT JOIN gestao.formapag b
|
||||
ON b.id_empresa = 1
|
||||
AND a.cod_formapag = b.codigo
|
||||
FROM sar.vw_pedidos_erp a
|
||||
LEFT JOIN vw_sitpedido c
|
||||
ON c.id_sitpedido = a.situa
|
||||
WHERE a.id_empresa = ${idEmpresa}
|
||||
@@ -369,6 +363,7 @@ export class OrdersService {
|
||||
descontoPerc: dto.descontoPerc,
|
||||
descontoValor: descontoValorGlobal,
|
||||
obs: dto.obs ?? null,
|
||||
endEntrega: dto.endEntrega ?? null,
|
||||
idempotencyKey: dto.idempotencyKey ?? null,
|
||||
itens: { create: itemsData },
|
||||
historico: {
|
||||
@@ -552,6 +547,44 @@ export class OrdersService {
|
||||
return this.mapDetail(final);
|
||||
}
|
||||
|
||||
async cancel(id: string): Promise<PedidoDetail> {
|
||||
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 userId = this.cls.get('userId') ?? '0';
|
||||
const codVendedor = parseInt(userId, 10);
|
||||
|
||||
// Rep só pode cancelar seu próprio orçamento (situa 0).
|
||||
const pedido = await prisma.pedido.findFirst({ where: { id, idEmpresa, codVendedor } });
|
||||
if (!pedido) throw new NotFoundException(`Pedido ${id} não encontrado`);
|
||||
if (pedido.situa !== SITUA_ORCAMENTO)
|
||||
throw new BadRequestException('Apenas orçamentos podem ser cancelados pelo representante');
|
||||
|
||||
const now = new Date();
|
||||
|
||||
await prisma.pedido.update({ where: { id }, data: { situa: SITUA_CANCELADO } });
|
||||
await prisma.historicoPedido.create({
|
||||
data: {
|
||||
idPedido: id,
|
||||
situaAnterior: SITUA_ORCAMENTO,
|
||||
situaNova: SITUA_CANCELADO,
|
||||
changedBy: codVendedor,
|
||||
changedAt: now,
|
||||
nota: 'Cancelado pelo representante',
|
||||
},
|
||||
});
|
||||
|
||||
const final = await prisma.pedido.findUniqueOrThrow({
|
||||
where: { id },
|
||||
include: {
|
||||
itens: { orderBy: { ordem: 'asc' } },
|
||||
historico: { orderBy: { changedAt: 'asc' } },
|
||||
},
|
||||
});
|
||||
|
||||
return this.mapDetail(final);
|
||||
}
|
||||
|
||||
// Resolve nome do cliente (nome + razão) e nome do representante a partir dos
|
||||
// códigos, lendo das views do ERP. Usado no detalhe de pedidos SAR-nativos.
|
||||
private async lookupNames(
|
||||
@@ -602,6 +635,7 @@ export class OrdersService {
|
||||
aprovadoEm: Date | null;
|
||||
motivoRecusa: string | null;
|
||||
obs: string | null;
|
||||
endEntrega: string | null;
|
||||
idempotencyKey: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
@@ -639,6 +673,7 @@ export class OrdersService {
|
||||
total: decimalToString(o.total),
|
||||
descontoPerc: decimalToString(o.descontoPerc),
|
||||
obs: o.obs,
|
||||
endEntrega: o.endEntrega,
|
||||
createdAt: o.createdAt.toISOString(),
|
||||
totalProdutos: decimalToString(o.totalProdutos),
|
||||
totalIpi: decimalToString(o.totalIpi),
|
||||
|
||||
Reference in New Issue
Block a user