feat(erp): pedidos e dashboard leem histórico do ERP (vw_pedidos_erp)
- OrdersService.list: substitui sar.pedidos por vw_pedidos_erp — 44k pedidos
históricos do rep 29 visíveis; sar.pedidos continua sendo a tabela de escrita
para novos pedidos SAR que serão integrados ao ERP
- DashboardService: atingido/pedidosMes/recentes/inativos todos via vw_pedidos_erp;
supervisor usa vw_pedidos_erp para pedidosDia
- PedidoSummarySchema: id relaxado de uuid() para string(); adiciona numero,
statusDescr e fonte ('sar'|'erp')
- orders.ts: corrige bug — apiFetch retorna JSON diretamente, não Response;
remove res.ok/res.json() incorretos
- OrdersPage: coluna Nº mostra numero do ERP; statusDescr no badge
- DevLogin: atualiza para PAVEI COMERCIO cod 29
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,10 +3,8 @@ import { ClsService } from 'nestjs-cls';
|
|||||||
import type { RepDashboard, SupervisorDashboard } from '@sar/api-interface';
|
import type { RepDashboard, SupervisorDashboard } from '@sar/api-interface';
|
||||||
import type { WorkspaceClsStore } from '../workspace/workspace.types';
|
import type { WorkspaceClsStore } from '../workspace/workspace.types';
|
||||||
|
|
||||||
// Situa SAR: 1=Pendente, 2=Aprovado, 3=Cancelado, 4=Faturado
|
// Situa ERP: 2=Liberado, 4=Faturado, 5=Cancelado
|
||||||
const SITUA_APROVADO = 2;
|
// Situa SAR (pedidos novos): 1=Pendente, 2=Aprovado, 3=Cancelado, 4=Faturado
|
||||||
const SITUA_FATURADO = 4;
|
|
||||||
const SITUA_CANCELADO = 3;
|
|
||||||
const SITUA_PENDENTE = 1;
|
const SITUA_PENDENTE = 1;
|
||||||
|
|
||||||
// tipo='G' em gestao.metavenda = meta geral de valor do mês
|
// tipo='G' em gestao.metavenda = meta geral de valor do mês
|
||||||
@@ -81,17 +79,64 @@ export class DashboardService {
|
|||||||
});
|
});
|
||||||
const flexRate = flexOverride ? Number(flexOverride.taxaFlex) : 1;
|
const flexRate = flexOverride ? Number(flexOverride.taxaFlex) : 1;
|
||||||
|
|
||||||
// 4. Pedidos aprovados/faturados do mês (base de cálculo)
|
// 4. Atingido do mês — pedidos liberados/faturados no ERP (situa 2=Liberado, 4=Faturado)
|
||||||
const approvedThisMonth = await prisma.pedido.findMany({
|
const monthStartStr = monthStart.toISOString().slice(0, 10);
|
||||||
where: {
|
const monthEndStr = monthEnd.toISOString().slice(0, 10);
|
||||||
codVendedor,
|
|
||||||
idEmpresa,
|
|
||||||
situa: { in: [SITUA_APROVADO, SITUA_FATURADO] },
|
|
||||||
dtPedido: { gte: monthStart, lte: monthEnd },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const atingido = approvedThisMonth.reduce((s, o) => s + Number(o.total), 0);
|
interface TotalRow {
|
||||||
|
total: string;
|
||||||
|
}
|
||||||
|
interface CountRow {
|
||||||
|
count: string;
|
||||||
|
}
|
||||||
|
interface RecentRow {
|
||||||
|
id_pedido: number;
|
||||||
|
num_ped_sar: string;
|
||||||
|
numero: number;
|
||||||
|
id_cliente: number;
|
||||||
|
cod_vendedor: number;
|
||||||
|
situa: number;
|
||||||
|
status_descr: string;
|
||||||
|
dt_pedido: Date;
|
||||||
|
total: string;
|
||||||
|
desconto_perc: string;
|
||||||
|
obs: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [atingidoRows, pedidosMesRows, recentRows] = await Promise.all([
|
||||||
|
prisma.$queryRawUnsafe<TotalRow[]>(`
|
||||||
|
SELECT COALESCE(SUM(total), 0)::text AS total
|
||||||
|
FROM vw_pedidos_erp
|
||||||
|
WHERE id_empresa = ${idEmpresa}
|
||||||
|
AND cod_vendedor = ${codVendedor}
|
||||||
|
AND situa IN (2, 4)
|
||||||
|
AND dt_pedido >= '${monthStartStr}'
|
||||||
|
AND dt_pedido <= '${monthEndStr}'
|
||||||
|
`),
|
||||||
|
prisma.$queryRawUnsafe<CountRow[]>(`
|
||||||
|
SELECT COUNT(*)::text AS count
|
||||||
|
FROM vw_pedidos_erp
|
||||||
|
WHERE id_empresa = ${idEmpresa}
|
||||||
|
AND cod_vendedor = ${codVendedor}
|
||||||
|
AND situa != 5
|
||||||
|
AND dt_pedido >= '${monthStartStr}'
|
||||||
|
AND dt_pedido <= '${monthEndStr}'
|
||||||
|
`),
|
||||||
|
prisma.$queryRawUnsafe<RecentRow[]>(`
|
||||||
|
SELECT id_pedido, num_ped_sar, numero, id_cliente, cod_vendedor,
|
||||||
|
situa, status_descr, dt_pedido, total::text, desconto_perc::text, obs
|
||||||
|
FROM vw_pedidos_erp
|
||||||
|
WHERE id_empresa = ${idEmpresa}
|
||||||
|
AND cod_vendedor = ${codVendedor}
|
||||||
|
AND situa != 5
|
||||||
|
AND dt_pedido >= '${new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10)}'
|
||||||
|
ORDER BY dt_pedido DESC
|
||||||
|
LIMIT 10
|
||||||
|
`),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const atingido = Number(atingidoRows[0]?.total ?? 0);
|
||||||
|
const pedidosMes = Number(pedidosMesRows[0]?.count ?? 0);
|
||||||
const pct = targetAmount > 0 ? Math.round((atingido / targetAmount) * 100) : 0;
|
const pct = targetAmount > 0 ? Math.round((atingido / targetAmount) * 100) : 0;
|
||||||
const falta = Math.max(0, targetAmount - atingido);
|
const falta = Math.max(0, targetAmount - atingido);
|
||||||
|
|
||||||
@@ -101,30 +146,7 @@ export class DashboardService {
|
|||||||
? Math.round(atingido * flexRate) / 100
|
? Math.round(atingido * flexRate) / 100
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
// 5. Contagem de pedidos do mês (exceto cancelado)
|
// 7. Clientes inativos — sem pedido no ERP há >30 dias
|
||||||
const pedidosMes = await prisma.pedido.count({
|
|
||||||
where: {
|
|
||||||
codVendedor,
|
|
||||||
idEmpresa,
|
|
||||||
situa: { not: SITUA_CANCELADO },
|
|
||||||
dtPedido: { gte: monthStart, lte: monthEnd },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 6. Pedidos recentes — últimos 7 dias
|
|
||||||
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
||||||
const recentOrders = await prisma.pedido.findMany({
|
|
||||||
where: {
|
|
||||||
codVendedor,
|
|
||||||
idEmpresa,
|
|
||||||
situa: { not: SITUA_CANCELADO },
|
|
||||||
dtPedido: { gte: sevenDaysAgo },
|
|
||||||
},
|
|
||||||
orderBy: { dtPedido: 'desc' },
|
|
||||||
take: 10,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 7. Clientes inativos — sem pedido há >30 dias (vw_clientes + sar.pedidos)
|
|
||||||
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||||||
const inactiveClients = await prisma.$queryRawUnsafe<InativoRow[]>(`
|
const inactiveClients = await prisma.$queryRawUnsafe<InativoRow[]>(`
|
||||||
SELECT
|
SELECT
|
||||||
@@ -133,16 +155,16 @@ export class DashboardService {
|
|||||||
MAX(p.dt_pedido) AS dt_ultima_compra,
|
MAX(p.dt_pedido) AS dt_ultima_compra,
|
||||||
MAX(p.total)::text AS ultima_compra_valor
|
MAX(p.total)::text AS ultima_compra_valor
|
||||||
FROM vw_clientes c
|
FROM vw_clientes c
|
||||||
LEFT JOIN pedidos p
|
LEFT JOIN vw_pedidos_erp p
|
||||||
ON p.id_cliente = c.id_cliente
|
ON p.id_cliente = c.id_cliente
|
||||||
AND p.id_empresa = c.id_empresa
|
AND p.id_empresa = c.id_empresa
|
||||||
AND p.situa != ${SITUA_CANCELADO}
|
AND p.situa != 5
|
||||||
WHERE c.id_empresa = ${idEmpresa}
|
WHERE c.id_empresa = ${idEmpresa}
|
||||||
AND c.cod_vendedor = ${codVendedor}
|
AND c.cod_vendedor = ${codVendedor}
|
||||||
AND c.ativo = 1
|
AND c.ativo = 1
|
||||||
GROUP BY c.id_cliente, c.nome
|
GROUP BY c.id_cliente, c.nome
|
||||||
HAVING MAX(p.dt_pedido) IS NULL
|
HAVING MAX(p.dt_pedido) IS NULL
|
||||||
OR MAX(p.dt_pedido) < '${thirtyDaysAgo.toISOString()}'
|
OR MAX(p.dt_pedido) < '${thirtyDaysAgo.toISOString().slice(0, 10)}'
|
||||||
ORDER BY dt_ultima_compra ASC NULLS FIRST
|
ORDER BY dt_ultima_compra ASC NULLS FIRST
|
||||||
LIMIT 10
|
LIMIT 10
|
||||||
`);
|
`);
|
||||||
@@ -151,17 +173,20 @@ export class DashboardService {
|
|||||||
meta: { atingido, total: targetAmount, pct, falta },
|
meta: { atingido, total: targetAmount, pct, falta },
|
||||||
comissao: { fixa, flex, total: fixa + flex },
|
comissao: { fixa, flex, total: fixa + flex },
|
||||||
pedidosMes,
|
pedidosMes,
|
||||||
pedidosRecentes: recentOrders.map((o) => ({
|
pedidosRecentes: recentRows.map((o) => ({
|
||||||
id: o.id,
|
id: `erp-${o.id_pedido}`,
|
||||||
numPedSar: o.numPedSar,
|
numPedSar: (o.num_ped_sar ?? '').trim(),
|
||||||
idCliente: o.idCliente,
|
numero: Number(o.numero),
|
||||||
codVendedor: o.codVendedor,
|
idCliente: Number(o.id_cliente),
|
||||||
situa: o.situa,
|
codVendedor: Number(o.cod_vendedor),
|
||||||
dtPedido: o.dtPedido.toISOString(),
|
situa: Number(o.situa),
|
||||||
total: String(o.total),
|
statusDescr: o.status_descr,
|
||||||
descontoPerc: String(o.descontoPerc),
|
dtPedido: new Date(o.dt_pedido).toISOString(),
|
||||||
obs: o.obs,
|
total: o.total ?? '0',
|
||||||
createdAt: o.createdAt.toISOString(),
|
descontoPerc: o.desconto_perc ?? '0',
|
||||||
|
obs: o.obs ?? null,
|
||||||
|
createdAt: new Date(o.dt_pedido).toISOString(),
|
||||||
|
fonte: 'erp' as const,
|
||||||
})),
|
})),
|
||||||
clientesInativos: inactiveClients.map((c) => ({
|
clientesInativos: inactiveClients.map((c) => ({
|
||||||
idCliente: Number(c.id_cliente),
|
idCliente: Number(c.id_cliente),
|
||||||
@@ -181,51 +206,57 @@ export class DashboardService {
|
|||||||
const idEmpresa = this.cls.get('idEmpresa');
|
const idEmpresa = this.cls.get('idEmpresa');
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
// Fila de aprovações — mais antigos primeiro
|
// Fila de aprovações — pedidos SAR pendentes (novos, ainda não integrados ao ERP)
|
||||||
const approvalQueue = await prisma.pedido.findMany({
|
const approvalQueue = await prisma.pedido.findMany({
|
||||||
where: { idEmpresa, situa: SITUA_PENDENTE },
|
where: { idEmpresa, situa: SITUA_PENDENTE },
|
||||||
orderBy: { dtPedido: 'asc' },
|
orderBy: { dtPedido: 'asc' },
|
||||||
take: 50,
|
take: 50,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Pedidos do dia
|
// Pedidos do dia — lê do ERP (situa != 5=Cancelado)
|
||||||
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||||
const todayOrders = await prisma.pedido.findMany({
|
const todayStr = todayStart.toISOString().slice(0, 10);
|
||||||
where: {
|
const lastWeekStr = new Date(todayStart.getTime() - 7 * 24 * 60 * 60 * 1000)
|
||||||
idEmpresa,
|
.toISOString()
|
||||||
situa: { not: SITUA_CANCELADO },
|
.slice(0, 10);
|
||||||
dtPedido: { gte: todayStart },
|
const lastWeekEndStr = new Date(todayStart.getTime() - 6 * 24 * 60 * 60 * 1000)
|
||||||
},
|
.toISOString()
|
||||||
});
|
.slice(0, 10);
|
||||||
|
|
||||||
// Mesmo dia da semana passada (comparativo)
|
interface DayRow {
|
||||||
const lastWeekStart = new Date(todayStart.getTime() - 7 * 24 * 60 * 60 * 1000);
|
count: string;
|
||||||
const lastWeekEnd = new Date(lastWeekStart.getTime() + 24 * 60 * 60 * 1000 - 1);
|
total: string;
|
||||||
const lastWeekOrders = await prisma.pedido.findMany({
|
}
|
||||||
where: {
|
const [todayRows, lastWeekRows] = await Promise.all([
|
||||||
idEmpresa,
|
prisma.$queryRawUnsafe<DayRow[]>(`
|
||||||
situa: { not: SITUA_CANCELADO },
|
SELECT COUNT(*)::text AS count, COALESCE(SUM(total),0)::text AS total
|
||||||
dtPedido: { gte: lastWeekStart, lte: lastWeekEnd },
|
FROM vw_pedidos_erp
|
||||||
},
|
WHERE id_empresa = ${idEmpresa} AND situa != 5 AND dt_pedido >= '${todayStr}'
|
||||||
});
|
`),
|
||||||
|
prisma.$queryRawUnsafe<DayRow[]>(`
|
||||||
|
SELECT COUNT(*)::text AS count, COALESCE(SUM(total),0)::text AS total
|
||||||
|
FROM vw_pedidos_erp
|
||||||
|
WHERE id_empresa = ${idEmpresa} AND situa != 5
|
||||||
|
AND dt_pedido >= '${lastWeekStr}' AND dt_pedido < '${lastWeekEndStr}'
|
||||||
|
`),
|
||||||
|
]);
|
||||||
|
|
||||||
// Top 3 reps com mais clientes inativos (>30 dias sem compra)
|
// Top 3 reps com mais clientes inativos (>30 dias sem compra no ERP)
|
||||||
// Subconsulta agrupa por cliente, outer agrupa por rep
|
|
||||||
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||||||
const inativosPorRep = await prisma.$queryRawUnsafe<InativosPorRepRow[]>(`
|
const inativosPorRep = await prisma.$queryRawUnsafe<InativosPorRepRow[]>(`
|
||||||
SELECT cod_vendedor, COUNT(*)::text AS inativos_count
|
SELECT cod_vendedor, COUNT(*)::text AS inativos_count
|
||||||
FROM (
|
FROM (
|
||||||
SELECT c.id_cliente, c.cod_vendedor
|
SELECT c.id_cliente, c.cod_vendedor
|
||||||
FROM vw_clientes c
|
FROM vw_clientes c
|
||||||
LEFT JOIN pedidos p
|
LEFT JOIN vw_pedidos_erp p
|
||||||
ON p.id_cliente = c.id_cliente
|
ON p.id_cliente = c.id_cliente
|
||||||
AND p.id_empresa = c.id_empresa
|
AND p.id_empresa = c.id_empresa
|
||||||
AND p.situa != ${SITUA_CANCELADO}
|
AND p.situa != 5
|
||||||
WHERE c.id_empresa = ${idEmpresa}
|
WHERE c.id_empresa = ${idEmpresa}
|
||||||
AND c.ativo = 1
|
AND c.ativo = 1
|
||||||
GROUP BY c.id_cliente, c.cod_vendedor
|
GROUP BY c.id_cliente, c.cod_vendedor
|
||||||
HAVING MAX(p.dt_pedido) IS NULL
|
HAVING MAX(p.dt_pedido) IS NULL
|
||||||
OR MAX(p.dt_pedido) < '${thirtyDaysAgo.toISOString()}'
|
OR MAX(p.dt_pedido) < '${thirtyDaysAgo.toISOString().slice(0, 10)}'
|
||||||
) inativos
|
) inativos
|
||||||
GROUP BY cod_vendedor
|
GROUP BY cod_vendedor
|
||||||
ORDER BY COUNT(*) DESC
|
ORDER BY COUNT(*) DESC
|
||||||
@@ -243,15 +274,16 @@ export class DashboardService {
|
|||||||
descontoPerc: String(o.descontoPerc),
|
descontoPerc: String(o.descontoPerc),
|
||||||
obs: o.obs,
|
obs: o.obs,
|
||||||
createdAt: o.createdAt.toISOString(),
|
createdAt: o.createdAt.toISOString(),
|
||||||
|
fonte: 'sar' as const,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
approvalQueue: approvalQueue.map(mapPedido),
|
approvalQueue: approvalQueue.map(mapPedido),
|
||||||
pedidosDia: {
|
pedidosDia: {
|
||||||
count: todayOrders.length,
|
count: Number(todayRows[0]?.count ?? 0),
|
||||||
total: todayOrders.reduce((s, o) => s + Number(o.total), 0),
|
total: Number(todayRows[0]?.total ?? 0),
|
||||||
countSemanaAnterior: lastWeekOrders.length,
|
countSemanaAnterior: Number(lastWeekRows[0]?.count ?? 0),
|
||||||
totalSemanaAnterior: lastWeekOrders.reduce((s, o) => s + Number(o.total), 0),
|
totalSemanaAnterior: Number(lastWeekRows[0]?.total ?? 0),
|
||||||
},
|
},
|
||||||
inativosPorRep: inativosPorRep.map((r) => ({
|
inativosPorRep: inativosPorRep.map((r) => ({
|
||||||
codVendedor: Number(r.cod_vendedor),
|
codVendedor: Number(r.cod_vendedor),
|
||||||
|
|||||||
@@ -38,48 +38,65 @@ export class OrdersService {
|
|||||||
const codVendedor = userId ? parseInt(userId, 10) : 0;
|
const codVendedor = userId ? parseInt(userId, 10) : 0;
|
||||||
|
|
||||||
const { idCliente, situa, numPedSar, from, to, page, limit } = query;
|
const { idCliente, situa, numPedSar, from, to, page, limit } = query;
|
||||||
const skip = (page - 1) * limit;
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
const repFilter = role === 'rep' ? { codVendedor } : {};
|
const vendedorFilter = role === 'rep' ? `AND e.cod_vendedor = ${codVendedor}` : '';
|
||||||
|
const clienteFilter = idCliente != null ? `AND e.id_cliente = ${idCliente}` : '';
|
||||||
|
const situaFilter = situa != null ? `AND e.situa = ${situa}` : '';
|
||||||
|
const pedSarFilter = numPedSar ? `AND TRIM(e.num_ped_sar) ILIKE '%${numPedSar}%'` : '';
|
||||||
|
const fromFilter = from ? `AND e.dt_pedido >= '${from}'` : '';
|
||||||
|
const toFilter = to ? `AND e.dt_pedido <= '${to}'` : '';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
const filters = `
|
||||||
const where: any = {
|
WHERE e.id_empresa = ${idEmpresa}
|
||||||
idEmpresa,
|
${vendedorFilter} ${clienteFilter} ${situaFilter}
|
||||||
...repFilter,
|
${pedSarFilter} ${fromFilter} ${toFilter}
|
||||||
...(idCliente != null ? { idCliente } : {}),
|
`;
|
||||||
...(situa != null ? { situa } : {}),
|
|
||||||
...(numPedSar ? { numPedSar: { contains: numPedSar } } : {}),
|
|
||||||
...(from || to
|
|
||||||
? {
|
|
||||||
dtPedido: {
|
|
||||||
...(from ? { gte: new Date(from) } : {}),
|
|
||||||
...(to ? { lte: new Date(to) } : {}),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const [rows, total] = await Promise.all([
|
interface ErpRow {
|
||||||
prisma.pedido.findMany({
|
id_pedido: number;
|
||||||
where,
|
num_ped_sar: string;
|
||||||
skip,
|
numero: number;
|
||||||
take: limit,
|
id_cliente: number;
|
||||||
orderBy: { dtPedido: 'desc' },
|
cod_vendedor: number;
|
||||||
}),
|
situa: number;
|
||||||
prisma.pedido.count({ where }),
|
status_descr: string;
|
||||||
|
dt_pedido: Date;
|
||||||
|
total: string;
|
||||||
|
desconto_perc: string;
|
||||||
|
obs: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [rows, countRows] = await Promise.all([
|
||||||
|
prisma.$queryRawUnsafe<ErpRow[]>(`
|
||||||
|
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::text, e.desconto_perc::text, e.obs
|
||||||
|
FROM vw_pedidos_erp e
|
||||||
|
${filters}
|
||||||
|
ORDER BY e.dt_pedido DESC
|
||||||
|
LIMIT ${limit} OFFSET ${offset}
|
||||||
|
`),
|
||||||
|
prisma.$queryRawUnsafe<[{ count: string }]>(`
|
||||||
|
SELECT COUNT(*)::text AS count FROM vw_pedidos_erp e ${filters}
|
||||||
|
`),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const total = Number(countRows[0]?.count ?? 0);
|
||||||
|
|
||||||
const data: PedidoSummary[] = rows.map((o) => ({
|
const data: PedidoSummary[] = rows.map((o) => ({
|
||||||
id: o.id,
|
id: `erp-${o.id_pedido}`,
|
||||||
numPedSar: o.numPedSar,
|
numPedSar: (o.num_ped_sar ?? '').trim(),
|
||||||
idCliente: o.idCliente,
|
numero: Number(o.numero),
|
||||||
codVendedor: o.codVendedor,
|
idCliente: Number(o.id_cliente),
|
||||||
situa: o.situa,
|
codVendedor: Number(o.cod_vendedor),
|
||||||
dtPedido: o.dtPedido.toISOString(),
|
situa: Number(o.situa),
|
||||||
total: decimalToString(o.total),
|
statusDescr: o.status_descr,
|
||||||
descontoPerc: decimalToString(o.descontoPerc),
|
dtPedido: new Date(o.dt_pedido).toISOString(),
|
||||||
obs: o.obs,
|
total: o.total ?? '0',
|
||||||
createdAt: o.createdAt.toISOString(),
|
descontoPerc: o.desconto_perc ?? '0',
|
||||||
|
obs: o.obs ?? null,
|
||||||
|
createdAt: new Date(o.dt_pedido).toISOString(),
|
||||||
|
fonte: 'erp',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return { data, total, page, limit };
|
return { data, total, page, limit };
|
||||||
@@ -388,6 +405,7 @@ export class OrdersService {
|
|||||||
acrescimo: decimalToString(o.acrescimo),
|
acrescimo: decimalToString(o.acrescimo),
|
||||||
comissao: decimalToString(o.comissao),
|
comissao: decimalToString(o.comissao),
|
||||||
pedFlex: decimalToString(o.pedFlex),
|
pedFlex: decimalToString(o.pedFlex),
|
||||||
|
fonte: 'sar' as const,
|
||||||
aprovadoPor: o.aprovadoPor,
|
aprovadoPor: o.aprovadoPor,
|
||||||
aprovadoEm: o.aprovadoEm?.toISOString() ?? null,
|
aprovadoEm: o.aprovadoEm?.toISOString() ?? null,
|
||||||
motivoRecusa: o.motivoRecusa,
|
motivoRecusa: o.motivoRecusa,
|
||||||
|
|||||||
@@ -19,31 +19,39 @@ const SITUA_COLOR: Record<number, string> = {
|
|||||||
const columns: TableColumnsType<PedidoSummary> = [
|
const columns: TableColumnsType<PedidoSummary> = [
|
||||||
{
|
{
|
||||||
title: 'Nº',
|
title: 'Nº',
|
||||||
dataIndex: 'numPedSar',
|
dataIndex: 'numero',
|
||||||
width: 120,
|
width: 120,
|
||||||
render: (num: string, row: PedidoSummary) => (
|
render: (_: number, row: PedidoSummary) => {
|
||||||
<Link to="/pedidos/$id" params={{ id: row.id }}>
|
const label = row.numero ? String(row.numero) : row.numPedSar || row.id;
|
||||||
{num}
|
return row.fonte === 'erp' ? (
|
||||||
</Link>
|
<span style={{ fontVariantNumeric: 'tabular-nums' }}>{label}</span>
|
||||||
),
|
) : (
|
||||||
|
<Link to="/pedidos/$id" params={{ id: row.id }}>
|
||||||
|
{label}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Status',
|
title: 'Status',
|
||||||
dataIndex: 'situa',
|
dataIndex: 'situa',
|
||||||
width: 150,
|
width: 150,
|
||||||
render: (s: number) => (
|
render: (s: number, row: PedidoSummary) => {
|
||||||
<Badge
|
const label = row.statusDescr ?? SITUA_LABEL[s] ?? String(s);
|
||||||
status={
|
return (
|
||||||
(SITUA_COLOR[s] ?? 'default') as
|
<Badge
|
||||||
| 'default'
|
status={
|
||||||
| 'warning'
|
(SITUA_COLOR[s] ?? 'default') as
|
||||||
| 'processing'
|
| 'default'
|
||||||
| 'success'
|
| 'warning'
|
||||||
| 'error'
|
| 'processing'
|
||||||
}
|
| 'success'
|
||||||
text={<Tag color={SITUA_COLOR[s] ?? 'default'}>{SITUA_LABEL[s] ?? String(s)}</Tag>}
|
| 'error'
|
||||||
/>
|
}
|
||||||
),
|
text={<Tag color={SITUA_COLOR[s] ?? 'default'}>{label}</Tag>}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Total',
|
title: 'Total',
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ import { AuthTokenResponseSchema } from '@sar/api-interface';
|
|||||||
type DevUser = { userId: string; role: string; label: string };
|
type DevUser = { userId: string; role: string; label: string };
|
||||||
|
|
||||||
// userId = cod_vendedor como string; idEmpresa = empresa no ERP (dev default = 1)
|
// userId = cod_vendedor como string; idEmpresa = empresa no ERP (dev default = 1)
|
||||||
|
// Em dev, o backend força DEV_REP_CODE=29 independente do userId enviado.
|
||||||
const DEV_USERS: DevUser[] = [
|
const DEV_USERS: DevUser[] = [
|
||||||
{ userId: '101', role: 'rep', label: 'Rafael — Rep (cod 101)' },
|
{ userId: '29', role: 'rep', label: 'PAVEI COMERCIO (cod 29)' },
|
||||||
{ userId: '102', role: 'rep', label: 'Rep 2 (cod 102)' },
|
{ userId: '29', role: 'supervisor', label: 'PAVEI — Supervisor (cod 29)' },
|
||||||
{ userId: '201', role: 'supervisor', label: 'Sandra — Supervisora (cod 201)' },
|
{ userId: '29', role: 'manager', label: 'PAVEI — Gerente (cod 29)' },
|
||||||
{ userId: '301', role: 'manager', label: 'Gerente (cod 301)' },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export function DevLogin({ onLogin }: { onLogin: () => void }) {
|
export function DevLogin({ onLogin }: { onLogin: () => void }) {
|
||||||
|
|||||||
@@ -24,8 +24,7 @@ export function useOrderList(params: Partial<PedidoListQuery> = {}) {
|
|||||||
queryKey: ['orders', params],
|
queryKey: ['orders', params],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await apiFetch(`/orders${qs ? `?${qs}` : ''}`);
|
const res = await apiFetch(`/orders${qs ? `?${qs}` : ''}`);
|
||||||
if (!res.ok) throw new Error(`orders list error ${res.status}`);
|
return PedidoListResponseSchema.parse(res);
|
||||||
return PedidoListResponseSchema.parse(await res.json());
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -36,8 +35,7 @@ export function useOrderDetail(id: string | undefined) {
|
|||||||
enabled: !!id,
|
enabled: !!id,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await apiFetch(`/orders/${id}`);
|
const res = await apiFetch(`/orders/${id}`);
|
||||||
if (!res.ok) throw new Error(`order detail error ${res.status}`);
|
return PedidoDetailSchema.parse(res);
|
||||||
return PedidoDetailSchema.parse(await res.json());
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -48,8 +46,7 @@ export function useClientOrders(idCliente: number | undefined) {
|
|||||||
enabled: idCliente != null,
|
enabled: idCliente != null,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await apiFetch(`/orders?idCliente=${idCliente}&limit=10`);
|
const res = await apiFetch(`/orders?idCliente=${idCliente}&limit=10`);
|
||||||
if (!res.ok) throw new Error(`client orders error ${res.status}`);
|
const data = PedidoListResponseSchema.parse(res);
|
||||||
const data = PedidoListResponseSchema.parse(await res.json());
|
|
||||||
return data.data;
|
return data.data;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -47,16 +47,19 @@ export type HistoricoPedido = z.infer<typeof HistoricoPedidoSchema>;
|
|||||||
// ─── Pedido Summary (lista) ───────────────────────────────────────────────────
|
// ─── Pedido Summary (lista) ───────────────────────────────────────────────────
|
||||||
|
|
||||||
export const PedidoSummarySchema = z.object({
|
export const PedidoSummarySchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string(), // UUID para pedidos SAR, 'erp-{id}' para histórico ERP
|
||||||
numPedSar: z.string(),
|
numPedSar: z.string(),
|
||||||
|
numero: z.number().int().optional(), // número do pedido no ERP
|
||||||
idCliente: z.number().int(),
|
idCliente: z.number().int(),
|
||||||
codVendedor: z.number().int(),
|
codVendedor: z.number().int(),
|
||||||
situa: z.number().int(),
|
situa: z.number().int(),
|
||||||
|
statusDescr: z.string().optional(), // descrição legível do status
|
||||||
dtPedido: z.string(),
|
dtPedido: z.string(),
|
||||||
total: z.string(),
|
total: z.string(),
|
||||||
descontoPerc: z.string(),
|
descontoPerc: z.string(),
|
||||||
obs: z.string().nullable(),
|
obs: z.string().nullable(),
|
||||||
createdAt: z.iso.datetime(),
|
createdAt: z.iso.datetime(),
|
||||||
|
fonte: z.enum(['sar', 'erp']).default('sar'),
|
||||||
});
|
});
|
||||||
export type PedidoSummary = z.infer<typeof PedidoSummarySchema>;
|
export type PedidoSummary = z.infer<typeof PedidoSummarySchema>;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user