feat(dashboard): painel Sandra — aprovações, pedidos do dia, inativos por rep (C8)
GET /dashboard/supervisor com fila de aprovações, KPIs do dia vs semana anterior e top 3 reps com mais clientes inativos. SandraPainel com polling 30s. Rota / role-aware: rep → RafaelPainel, supervisor/manager → SandraPainel. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import type { RepDashboard } from '@sar/api-interface';
|
||||
import type { RepDashboard, SupervisorDashboard } from '@sar/api-interface';
|
||||
import type { WorkspaceClsStore } from '../workspace/workspace.types';
|
||||
import { DashboardService } from './dashboard.service';
|
||||
|
||||
@@ -15,4 +15,9 @@ export class DashboardController {
|
||||
repDashboard(): Promise<RepDashboard> {
|
||||
return this.dashboard.repDashboard(this.cls.get('userId') ?? '');
|
||||
}
|
||||
|
||||
@Get('supervisor')
|
||||
supervisorDashboard(): Promise<SupervisorDashboard> {
|
||||
return this.dashboard.supervisorDashboard();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import { OrderStatus } from '@prisma/client';
|
||||
import type { RepDashboard } from '@sar/api-interface';
|
||||
import type { RepDashboard, SupervisorDashboard } from '@sar/api-interface';
|
||||
import type { WorkspaceClsStore } from '../workspace/workspace.types';
|
||||
|
||||
@Injectable()
|
||||
@@ -106,4 +106,78 @@ export class DashboardService {
|
||||
syncedAt: now.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
async supervisorDashboard(): Promise<SupervisorDashboard> {
|
||||
const prisma = this.cls.get('prisma');
|
||||
const now = new Date();
|
||||
|
||||
// Fila de aprovações — mais antigos primeiro
|
||||
const approvalQueue = await prisma.order.findMany({
|
||||
where: { deletedAt: null, status: OrderStatus.pending_approval },
|
||||
include: { client: { select: { name: true } } },
|
||||
orderBy: { issuedAt: 'asc' },
|
||||
take: 50,
|
||||
});
|
||||
|
||||
// Pedidos do dia (hoje, meia-noite até agora)
|
||||
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const todayOrders = await prisma.order.findMany({
|
||||
where: {
|
||||
deletedAt: null,
|
||||
status: { not: OrderStatus.cancelled },
|
||||
issuedAt: { gte: todayStart },
|
||||
},
|
||||
});
|
||||
|
||||
// Mesmo dia da semana passada (comparativo)
|
||||
const lastWeekStart = new Date(todayStart.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||
const lastWeekEnd = new Date(lastWeekStart.getTime() + 24 * 60 * 60 * 1000 - 1);
|
||||
const lastWeekOrders = await prisma.order.findMany({
|
||||
where: {
|
||||
deletedAt: null,
|
||||
status: { not: OrderStatus.cancelled },
|
||||
issuedAt: { gte: lastWeekStart, lte: lastWeekEnd },
|
||||
},
|
||||
});
|
||||
|
||||
// Inativos por rep — top 3 reps com mais clientes inativos (>30 dias)
|
||||
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||||
const inativosPorRep = await prisma.client.groupBy({
|
||||
by: ['repId'],
|
||||
where: {
|
||||
deletedAt: null,
|
||||
OR: [{ lastOrderAt: null }, { lastOrderAt: { lt: thirtyDaysAgo } }],
|
||||
},
|
||||
_count: { id: true },
|
||||
orderBy: { _count: { id: 'desc' } },
|
||||
take: 3,
|
||||
});
|
||||
|
||||
const mapOrder = (o: (typeof approvalQueue)[number]) => ({
|
||||
id: o.id,
|
||||
number: o.number,
|
||||
clientId: o.clientId,
|
||||
clientName: o.client.name,
|
||||
repId: o.repId,
|
||||
status: o.status,
|
||||
total: String(o.total),
|
||||
discountPct: String(o.discountPct),
|
||||
issuedAt: o.issuedAt.toISOString(),
|
||||
});
|
||||
|
||||
return {
|
||||
approvalQueue: approvalQueue.map(mapOrder),
|
||||
pedidosDia: {
|
||||
count: todayOrders.length,
|
||||
total: todayOrders.reduce((s, o) => s + Number(o.total), 0),
|
||||
countSemanaAnterior: lastWeekOrders.length,
|
||||
totalSemanaAnterior: lastWeekOrders.reduce((s, o) => s + Number(o.total), 0),
|
||||
},
|
||||
inativosPorRep: inativosPorRep.map((r) => ({
|
||||
repId: r.repId,
|
||||
inativosCount: r._count.id,
|
||||
})),
|
||||
syncedAt: now.toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user