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:
@@ -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);
|
||||||
|
|||||||
@@ -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: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
Reference in New Issue
Block a user