feat(orders): detalhe completo de pedidos ERP com produtos e pagamento

- Endpoint GET /orders/erp/:idPedido para pedidos do histórico ERP
  (endpoint estático antes de /:id com ParseUUIDPipe, sem conflito)
- JOIN vw_peditens_erp + vw_produtos: itens com codigo + descricao do produto
- forma_pagamento direto da vw_pedidos_erp (ex: "28/35/42 DIAS")
- Retorna PedidoDetail completo: totais, ipi, icmsst, comissao, obs
- Frontend: useOrderDetail detecta 'erp-*' → chama /orders/erp/{id}
- OrderDetailPage: Cond. Pagamento nas Descriptions; oculta botões
  Transmitir/Aprovar/Recusar para pedidos ERP (read-only)
- PedidoItemSchema.id relaxado de uuid() para string() (ERP usa '{id}-{ordem}')
- PedidoDetailSchema: campo formaPagamento opcional adicionado

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 21:49:53 +00:00
parent a3c68f9f05
commit 6fbf8bfb8e
5 changed files with 148 additions and 4 deletions

View File

@@ -5,6 +5,7 @@ import {
Get, Get,
HttpCode, HttpCode,
Param, Param,
ParseIntPipe,
ParseUUIDPipe, ParseUUIDPipe,
Patch, Patch,
Post, Post,
@@ -79,6 +80,11 @@ export class OrdersController {
return this.orders.reject(id, parsed); return this.orders.reject(id, parsed);
} }
@Get('erp/:idPedido')
findOneErp(@Param('idPedido', ParseIntPipe) idPedido: number): Promise<PedidoDetail> {
return this.orders.findOneErp(idPedido);
}
@Get(':id') @Get(':id')
findOne(@Param('id', ParseUUIDPipe) id: string): Promise<PedidoDetail> { findOne(@Param('id', ParseUUIDPipe) id: string): Promise<PedidoDetail> {
return this.orders.findOne(id); return this.orders.findOne(id);

View File

@@ -595,4 +595,131 @@ export class OrdersService {
})), })),
}; };
} }
async findOneErp(idPedido: number): 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 role = this.cls.get('role');
const userId = this.cls.get('userId') ?? '0';
const codVendedor = parseInt(userId, 10);
interface ErpHeader {
id_pedido: number;
num_ped_sar: string;
numero: number;
id_cliente: number;
cod_vendedor: number;
situa: number;
status_descr: string;
dt_pedido: Date;
total_produtos: string;
total_ipi: string;
total_icmsst: string;
total: string;
desconto_perc: string;
desconto_valor: string;
acrescimo: string;
comissao: string;
ped_flex: string;
obs: string | null;
forma_pagamento: string | null;
nome_cliente: string | null;
razao_cliente: string | null;
nome_vendedor: string | null;
}
interface ErpItem {
ordem: number;
id_produto: number;
codigo: string | null;
descricao: string | null;
qtd: string;
preco_unitario: string;
desconto_perc: string;
total: string;
}
const vendedorFilter = role === 'rep' ? `AND e.cod_vendedor = ${codVendedor}` : '';
const idMatriz = idEmpresa > 9000 ? idEmpresa - 9000 : idEmpresa;
const [headerRows, itemRows] = await Promise.all([
prisma.$queryRawUnsafe<ErpHeader[]>(`
SELECT e.id_pedido, e.num_ped_sar, e.numero, e.id_cliente, e.cod_vendedor,
e.situa, e.status_descr, e.dt_pedido,
e.total_produtos::text, e.total_ipi::text, e.total_icmsst::text,
e.total::text, e.desconto_perc::text, e.desconto_valor::text,
e.acrescimo::text, e.comissao::text, e.ped_flex::text, e.obs,
TRIM(e.forma_pagamento) AS forma_pagamento,
c.nome AS nome_cliente, c.razao AS razao_cliente,
(SELECT r.nome FROM vw_representantes r
WHERE r.codigo = e.cod_vendedor LIMIT 1) AS nome_vendedor
FROM vw_pedidos_erp e
LEFT JOIN vw_clientes c ON c.id_cliente = e.id_cliente
WHERE e.id_empresa = ${idEmpresa}
AND e.id_pedido = ${idPedido}
${vendedorFilter}
LIMIT 1
`),
prisma.$queryRawUnsafe<ErpItem[]>(`
SELECT ei.ordem, ei.id_produto,
TRIM(p.codigo) AS codigo, TRIM(p.descricao) AS descricao,
ei.qtd::text, ei.preco_unitario::text,
ei.desconto_perc::text, ei.total::text
FROM vw_peditens_erp ei
LEFT JOIN vw_produtos p
ON p.id_erp = ei.id_produto
AND p.id_empresa = ${idMatriz}
WHERE ei.id_pedido = ${idPedido}
ORDER BY ei.ordem
`),
]);
if (!headerRows[0]) throw new NotFoundException(`Pedido ERP ${idPedido} não encontrado`);
const h = headerRows[0];
return {
id: `erp-${h.id_pedido}`,
numPedSar: (h.num_ped_sar ?? '').trim(),
numero: Number(h.numero),
idCliente: Number(h.id_cliente),
nomeCliente: h.nome_cliente ?? null,
razaoCliente: h.razao_cliente ?? null,
codVendedor: Number(h.cod_vendedor),
nomeVendedor: h.nome_vendedor ?? null,
situa: sigToSar(Number(h.situa)),
statusDescr: h.status_descr,
dtPedido: new Date(h.dt_pedido).toISOString(),
total: h.total ?? '0',
descontoPerc: h.desconto_perc ?? '0',
obs: h.obs?.trim() || null,
createdAt: new Date(h.dt_pedido).toISOString(),
updatedAt: new Date(h.dt_pedido).toISOString(),
fonte: 'erp' as const,
formaPagamento: h.forma_pagamento || null,
totalProdutos: h.total_produtos ?? '0',
totalIpi: h.total_ipi ?? '0',
totalIcmsst: h.total_icmsst ?? '0',
descontoValor: h.desconto_valor ?? '0',
acrescimo: h.acrescimo ?? '0',
comissao: h.comissao ?? '0',
pedFlex: h.ped_flex ?? '0',
aprovadoPor: null,
aprovadoEm: null,
motivoRecusa: null,
idempotencyKey: null,
itens: itemRows.map((it) => ({
id: `${idPedido}-${it.ordem}`,
idProduto: Number(it.id_produto),
codProduto: it.codigo ?? null,
descProduto: it.descricao ?? null,
ordem: Number(it.ordem),
qtd: it.qtd,
precoUnitario: it.preco_unitario,
descontoPerc: it.desconto_perc,
total: it.total,
})),
historico: [],
};
}
} }

View File

@@ -252,8 +252,9 @@ export function OrderDetailPage() {
const { data: clientOrders } = useClientOrders(order?.idCliente); const { data: clientOrders } = useClientOrders(order?.idCliente);
const role = getRoleFromToken(); const role = getRoleFromToken();
const canAct = role !== 'rep' && order?.situa === 1; const isErp = order?.fonte === 'erp';
const canTransmit = role === 'rep' && order?.situa === 0; const canAct = !isErp && role !== 'rep' && order?.situa === 1;
const canTransmit = !isErp && role === 'rep' && order?.situa === 0;
const canShare = const canShare =
role === 'rep' && role === 'rep' &&
(order?.situa === 2 || order?.situa === 4) && (order?.situa === 2 || order?.situa === 4) &&
@@ -417,6 +418,11 @@ export function OrderDetailPage() {
{new Date(order.aprovadoEm).toLocaleString('pt-BR')} cód. {order.aprovadoPor} {new Date(order.aprovadoEm).toLocaleString('pt-BR')} cód. {order.aprovadoPor}
</Descriptions.Item> </Descriptions.Item>
)} )}
{order.formaPagamento && (
<Descriptions.Item label="Cond. Pagamento" span={2}>
{order.formaPagamento}
</Descriptions.Item>
)}
<Descriptions.Item label="Total produtos">{fmt(order.totalProdutos)}</Descriptions.Item> <Descriptions.Item label="Total produtos">{fmt(order.totalProdutos)}</Descriptions.Item>
<Descriptions.Item label="Desc. Global">{order.descontoPerc}%</Descriptions.Item> <Descriptions.Item label="Desc. Global">{order.descontoPerc}%</Descriptions.Item>
<Descriptions.Item label="Total"> <Descriptions.Item label="Total">

View File

@@ -34,7 +34,11 @@ export function useOrderDetail(id: string | undefined) {
queryKey: ['orders', id], queryKey: ['orders', id],
enabled: !!id, enabled: !!id,
queryFn: async () => { queryFn: async () => {
const res = await apiFetch(`/orders/${id}`); // Pedidos ERP têm id 'erp-{idPedido}' — endpoint separado sem ParseUUIDPipe
const path = id?.startsWith('erp-')
? `/orders/erp/${id.replace('erp-', '')}`
: `/orders/${id}`;
const res = await apiFetch(path);
return PedidoDetailSchema.parse(res); return PedidoDetailSchema.parse(res);
}, },
}); });

View File

@@ -31,7 +31,7 @@ export const SITUA_LABEL: Record<number, string> = {
// ─── PedidoItem ─────────────────────────────────────────────────────────────── // ─── PedidoItem ───────────────────────────────────────────────────────────────
export const PedidoItemSchema = z.object({ export const PedidoItemSchema = z.object({
id: z.string().uuid(), id: z.string(), // UUID para pedidos SAR; '{idPedido}-{ordem}' para ERP
idProduto: z.number().int(), idProduto: z.number().int(),
codProduto: z.string().nullable(), codProduto: z.string().nullable(),
descProduto: z.string().nullable(), descProduto: z.string().nullable(),
@@ -87,6 +87,7 @@ export const PedidoDetailSchema = PedidoSummarySchema.extend({
acrescimo: z.string(), acrescimo: z.string(),
comissao: z.string(), comissao: z.string(),
pedFlex: z.string(), pedFlex: z.string(),
formaPagamento: z.string().nullable().optional(),
aprovadoPor: z.number().int().nullable(), aprovadoPor: z.number().int().nullable(),
aprovadoEm: z.iso.datetime().nullable(), aprovadoEm: z.iso.datetime().nullable(),
motivoRecusa: z.string().nullable(), motivoRecusa: z.string().nullable(),