src/analytics/analytics.service.ts
Methods |
|
constructor(prisma: PrismaService)
|
||||||
|
Defined in src/analytics/analytics.service.ts:5
|
||||||
|
Parameters :
|
| Async exportOrdersCsv |
exportOrdersCsv(from: Date, to: Date, filters: literal type, orgId: string)
|
|
Defined in src/analytics/analytics.service.ts:625
|
|
Returns :
Promise<string>
|
| Async generateReport | ||||||||||||||||||
generateReport(type: string, from: Date, to: Date, clientId?: string, orgId?: string)
|
||||||||||||||||||
|
Defined in src/analytics/analytics.service.ts:243
|
||||||||||||||||||
|
Parameters :
Returns :
unknown
|
| Async getDashboard | ||||||
getDashboard(organizationId: string)
|
||||||
|
Defined in src/analytics/analytics.service.ts:8
|
||||||
|
Parameters :
Returns :
unknown
|
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
@Injectable()
export class AnalyticsService {
constructor(private prisma: PrismaService) {}
async getDashboard(organizationId: string) {
const [
totalOrders,
ordersByStatus,
ordersBySource,
totalTrips,
tripsByStatus,
totalVehicles,
vehiclesByStatus,
totalDrivers,
totalClients,
recentOrders,
ordersByClient,
onTimeDelivery,
fleetUtilization,
] = await Promise.all([
// Total orders
this.prisma.order.count({ where: { organizationId } }),
// Orders by status
this.getOrdersByStatus(organizationId),
// Orders by source (MANUAL / UPLOAD / PORTAL_FORM / PORTAL_UPLOAD / API / EMAIL)
this.getOrdersBySource(organizationId),
// Total trips
this.prisma.trip.count({ where: { organizationId } }),
// Trips by status
this.getTripsByStatus(organizationId),
// Total vehicles
this.prisma.vehicle.count({ where: { organizationId } }),
// Vehicles by status
this.getVehiclesByStatus(organizationId),
// Total drivers
this.prisma.driver.count({ where: { organizationId, isActive: true } }),
// Total clients
this.prisma.client.count({ where: { organizationId, isActive: true } }),
// Recent orders (last 10)
this.prisma.order.findMany({
where: { organizationId },
orderBy: { createdAt: 'desc' },
take: 10,
include: {
client: { select: { id: true, name: true, code: true } },
},
}),
// Orders by client (top 10)
this.getOrdersByClient(organizationId),
// On-time delivery rate
this.getOnTimeDeliveryRate(organizationId),
// Fleet utilization
this.getFleetUtilization(organizationId),
]);
// Orders this week — group by day
const ordersThisWeek = await this.getOrdersThisWeek(organizationId);
return {
kpis: {
totalOrders,
totalTrips,
totalVehicles,
totalDrivers,
totalClients,
onTimeDeliveryRate: onTimeDelivery,
fleetUtilizationRate: fleetUtilization,
},
ordersByStatus,
ordersBySource,
tripsByStatus,
vehiclesByStatus,
ordersByClient,
recentOrders,
ordersThisWeek,
};
}
private async getOrdersBySource(organizationId: string) {
const results = await this.prisma.order.groupBy({
by: ['source'],
where: { organizationId },
_count: { source: true },
});
const map: Record<string, number> = {};
for (const r of results) {
map[r.source] = r._count.source;
}
return map;
}
private async getOrdersByStatus(organizationId: string) {
const results = await this.prisma.order.groupBy({
by: ['status'],
where: { organizationId },
_count: { status: true },
});
const statusMap: Record<string, number> = {};
for (const r of results) {
statusMap[r.status] = r._count.status;
}
return statusMap;
}
private async getTripsByStatus(organizationId: string) {
const results = await this.prisma.trip.groupBy({
by: ['status'],
where: { organizationId },
_count: { status: true },
});
const statusMap: Record<string, number> = {};
for (const r of results) {
statusMap[r.status] = r._count.status;
}
return statusMap;
}
private async getVehiclesByStatus(organizationId: string) {
const results = await this.prisma.vehicle.groupBy({
by: ['status'],
where: { organizationId },
_count: { status: true },
});
const statusMap: Record<string, number> = {};
for (const r of results) {
statusMap[r.status] = r._count.status;
}
return statusMap;
}
private async getOrdersByClient(organizationId: string) {
const results = await this.prisma.order.groupBy({
by: ['clientId'],
where: { organizationId },
_count: { clientId: true },
orderBy: { _count: { clientId: 'desc' } },
take: 10,
});
const clientIds = results.map((r) => r.clientId);
const clients = await this.prisma.client.findMany({
where: { id: { in: clientIds } },
select: { id: true, name: true, code: true },
});
const clientMap = new Map(clients.map((c: { id: string; name: string; code: string }) => [c.id, c]));
return results.map((r: { clientId: string; _count: { clientId: number } }) => ({
client: clientMap.get(r.clientId) || { id: r.clientId, name: 'Unknown', code: 'UNK' },
orderCount: r._count.clientId,
}));
}
private async getOnTimeDeliveryRate(organizationId: string): Promise<number> {
const delivered = await this.prisma.order.count({
where: { organizationId, status: 'DELIVERED' },
});
if (delivered === 0) return 0;
// Get orders where actual <= planned
const deliveredOrders = await this.prisma.order.findMany({
where: {
organizationId,
status: 'DELIVERED',
actualDelivery: { not: null },
deliveryDate: { not: null },
},
select: { actualDelivery: true, deliveryDate: true },
});
const onTimeCount = deliveredOrders.filter(
(o: { actualDelivery: Date | null; deliveryDate: Date | null }) =>
o.actualDelivery && o.deliveryDate && o.actualDelivery <= o.deliveryDate,
).length;
return delivered > 0
? Math.round((onTimeCount / delivered) * 10000) / 100
: 0;
}
private async getFleetUtilization(organizationId: string): Promise<number> {
const total = await this.prisma.vehicle.count({
where: { organizationId },
});
if (total === 0) return 0;
const active = await this.prisma.vehicle.count({
where: {
organizationId,
status: { in: ['ASSIGNED', 'IN_TRANSIT'] },
},
});
return Math.round((active / total) * 10000) / 100;
}
private async getOrdersThisWeek(organizationId: string) {
const now = new Date();
const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const result: { day: string; orders: number }[] = [];
for (let i = 6; i >= 0; i--) {
const date = new Date(now);
date.setDate(date.getDate() - i);
const startOfDay = new Date(date.getFullYear(), date.getMonth(), date.getDate());
const endOfDay = new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1);
const count = await this.prisma.order.count({
where: {
organizationId,
createdAt: { gte: startOfDay, lt: endOfDay },
},
});
result.push({ day: dayNames[date.getDay()], orders: count });
}
return result;
}
// ── Report Generation ──────────────────────────────────────
async generateReport(
type: string,
from: Date,
to: Date,
clientId?: string,
orgId?: string,
) {
const organizationId = orgId || '';
const dateFilter = { gte: from, lte: to };
switch (type) {
case 'daily':
case 'weekly':
case 'monthly':
return this.generateOperationsReport(organizationId, dateFilter, type, from, to);
case 'client':
return this.generateClientReport(organizationId, dateFilter, clientId, from, to);
case 'vehicle':
return this.generateVehicleReport(organizationId, dateFilter, from, to);
default:
return this.generateOperationsReport(organizationId, dateFilter, type, from, to);
}
}
private async generateOperationsReport(
organizationId: string,
dateFilter: { gte: Date; lte: Date },
periodType: string,
from: Date,
to: Date,
) {
const baseWhere = { organizationId, createdAt: dateFilter };
const [
totalOrders,
deliveredOrders,
cancelledOrders,
inTransitOrders,
pendingOrders,
allDeliveredOrders,
totalWeight,
topClients,
alertsSummary,
fleetTotal,
fleetActive,
allOrdersInPeriod,
] = await Promise.all([
this.prisma.order.count({ where: baseWhere }),
this.prisma.order.count({ where: { ...baseWhere, status: 'DELIVERED' } }),
this.prisma.order.count({ where: { ...baseWhere, status: 'CANCELLED' } }),
this.prisma.order.count({ where: { ...baseWhere, status: 'IN_TRANSIT' } }),
this.prisma.order.count({
where: { ...baseWhere, status: { in: ['DRAFT', 'PENDING_VALIDATION', 'VALIDATED'] } },
}),
this.prisma.order.findMany({
where: {
organizationId,
status: 'DELIVERED',
createdAt: dateFilter,
actualDelivery: { not: null },
deliveryDate: { not: null },
},
select: { actualDelivery: true, deliveryDate: true },
}),
this.prisma.order.aggregate({
where: baseWhere,
_sum: { weight: true },
}),
this.getTopClientsForPeriod(organizationId, dateFilter),
this.getAlertsSummary(organizationId, dateFilter),
this.prisma.vehicle.count({ where: { organizationId } }),
this.prisma.vehicle.count({
where: { organizationId, status: { in: ['ASSIGNED', 'IN_TRANSIT'] } },
}),
this.prisma.order.findMany({
where: baseWhere,
include: {
client: { select: { id: true, name: true, code: true } },
},
orderBy: { createdAt: 'desc' },
take: 200,
}),
]);
const onTimeCount = allDeliveredOrders.filter(
(o: { actualDelivery: Date | null; deliveryDate: Date | null }) =>
o.actualDelivery && o.deliveryDate && o.actualDelivery <= o.deliveryDate,
).length;
const onTimeRate =
allDeliveredOrders.length > 0
? Math.round((onTimeCount / allDeliveredOrders.length) * 10000) / 100
: 0;
const fleetUtilization =
fleetTotal > 0 ? Math.round((fleetActive / fleetTotal) * 10000) / 100 : 0;
const totalDistanceResult = await this.prisma.trip.aggregate({
where: { organizationId, createdAt: dateFilter },
_sum: { totalDistance: true },
});
return {
reportType: 'operations',
periodType,
from: from.toISOString(),
to: to.toISOString(),
generatedAt: new Date().toISOString(),
summary: {
totalOrders,
delivered: deliveredOrders,
cancelled: cancelledOrders,
inTransit: inTransitOrders,
pending: pendingOrders,
onTimeRate,
fleetUtilization,
totalWeight: totalWeight._sum.weight || 0,
totalDistance: totalDistanceResult._sum.totalDistance || 0,
},
topClients,
alerts: alertsSummary,
orders: allOrdersInPeriod.map((o: any) => ({
id: o.id,
orderNumber: o.orderNumber,
clientName: o.client?.name || 'Unknown',
clientCode: o.client?.code || 'UNK',
status: o.status,
priority: o.priority,
originCity: o.originCity,
destCity: o.destCity,
weight: o.weight,
pickupDate: o.pickupDate,
deliveryDate: o.deliveryDate,
actualDelivery: o.actualDelivery,
createdAt: o.createdAt,
})),
};
}
private async generateClientReport(
organizationId: string,
dateFilter: { gte: Date; lte: Date },
clientId: string | undefined,
from: Date,
to: Date,
) {
const baseWhere: any = { organizationId, createdAt: dateFilter };
if (clientId) baseWhere.clientId = clientId;
let clientInfo: { id: string; name: string; code: string; contactName: string | null; contactEmail: string | null } | null = null;
if (clientId) {
clientInfo = await this.prisma.client.findUnique({
where: { id: clientId },
select: { id: true, name: true, code: true, contactName: true, contactEmail: true },
});
}
const [totalOrders, deliveredOrders, cancelledOrders, exceptionOrders, allDelivered, orders] =
await Promise.all([
this.prisma.order.count({ where: baseWhere }),
this.prisma.order.count({ where: { ...baseWhere, status: 'DELIVERED' } }),
this.prisma.order.count({ where: { ...baseWhere, status: 'CANCELLED' } }),
this.prisma.order.count({ where: { ...baseWhere, status: 'EXCEPTION' } }),
this.prisma.order.findMany({
where: {
...baseWhere,
status: 'DELIVERED',
actualDelivery: { not: null },
deliveryDate: { not: null },
},
select: { actualDelivery: true, deliveryDate: true, pickupDate: true },
}),
this.prisma.order.findMany({
where: baseWhere,
include: {
client: { select: { id: true, name: true, code: true } },
},
orderBy: { createdAt: 'desc' },
take: 200,
}),
]);
const onTimeCount = allDelivered.filter(
(o: { actualDelivery: Date | null; deliveryDate: Date | null }) =>
o.actualDelivery && o.deliveryDate && o.actualDelivery <= o.deliveryDate,
).length;
const otifRate =
allDelivered.length > 0
? Math.round((onTimeCount / allDelivered.length) * 10000) / 100
: 0;
// Average delivery time in hours
const deliveryTimes = allDelivered
.filter((o: any) => o.actualDelivery && o.pickupDate)
.map((o: any) => {
const diff = new Date(o.actualDelivery).getTime() - new Date(o.pickupDate).getTime();
return diff / (1000 * 60 * 60);
});
const avgDeliveryTime =
deliveryTimes.length > 0
? Math.round((deliveryTimes.reduce((a: number, b: number) => a + b, 0) / deliveryTimes.length) * 10) / 10
: 0;
return {
reportType: 'client',
from: from.toISOString(),
to: to.toISOString(),
generatedAt: new Date().toISOString(),
client: clientInfo,
summary: {
totalOrders,
delivered: deliveredOrders,
cancelled: cancelledOrders,
exceptions: exceptionOrders,
otifRate,
avgDeliveryTimeHours: avgDeliveryTime,
},
orders: orders.map((o: any) => ({
id: o.id,
orderNumber: o.orderNumber,
clientName: o.client?.name || 'Unknown',
clientCode: o.client?.code || 'UNK',
status: o.status,
priority: o.priority,
originCity: o.originCity,
destCity: o.destCity,
weight: o.weight,
pickupDate: o.pickupDate,
deliveryDate: o.deliveryDate,
actualDelivery: o.actualDelivery,
createdAt: o.createdAt,
})),
};
}
private async generateVehicleReport(
organizationId: string,
dateFilter: { gte: Date; lte: Date },
from: Date,
to: Date,
) {
const vehicles = await this.prisma.vehicle.findMany({
where: { organizationId },
select: {
id: true,
unitNumber: true,
type: true,
status: true,
make: true,
model: true,
year: true,
licensePlate: true,
},
});
const vehicleIds = vehicles.map((v: { id: string }) => v.id);
const trips = await this.prisma.trip.findMany({
where: {
organizationId,
vehicleId: { in: vehicleIds },
createdAt: dateFilter,
},
select: {
vehicleId: true,
totalDistance: true,
totalDuration: true,
status: true,
},
});
const tripsByVehicle: Record<string, typeof trips> = {};
for (const trip of trips) {
if (trip.vehicleId) {
if (!tripsByVehicle[trip.vehicleId]) tripsByVehicle[trip.vehicleId] = [];
tripsByVehicle[trip.vehicleId].push(trip);
}
}
const vehicleStats = vehicles.map((v: any) => {
const vTrips = tripsByVehicle[v.id] || [];
const totalDistance = vTrips.reduce(
(sum: number, t: any) => sum + (t.totalDistance || 0),
0,
);
const totalDuration = vTrips.reduce(
(sum: number, t: any) => sum + (t.totalDuration || 0),
0,
);
const completedTrips = vTrips.filter((t: any) => t.status === 'COMPLETED').length;
return {
id: v.id,
unitNumber: v.unitNumber,
type: v.type,
status: v.status,
make: v.make,
model: v.model,
year: v.year,
licensePlate: v.licensePlate,
totalTrips: vTrips.length,
completedTrips,
totalDistance: Math.round(totalDistance * 10) / 10,
totalDurationMinutes: totalDuration,
};
});
const totalVehicles = vehicles.length;
const activeVehicles = vehicles.filter(
(v: any) => v.status === 'ASSIGNED' || v.status === 'IN_TRANSIT',
).length;
const maintenanceVehicles = vehicles.filter(
(v: any) => v.status === 'MAINTENANCE',
).length;
return {
reportType: 'vehicle',
from: from.toISOString(),
to: to.toISOString(),
generatedAt: new Date().toISOString(),
summary: {
totalVehicles,
activeVehicles,
maintenanceVehicles,
utilization:
totalVehicles > 0
? Math.round((activeVehicles / totalVehicles) * 10000) / 100
: 0,
},
vehicles: vehicleStats,
};
}
private async getTopClientsForPeriod(
organizationId: string,
dateFilter: { gte: Date; lte: Date },
) {
const results = await this.prisma.order.groupBy({
by: ['clientId'],
where: { organizationId, createdAt: dateFilter },
_count: { clientId: true },
orderBy: { _count: { clientId: 'desc' } },
take: 10,
});
const clientIds = results.map((r: { clientId: string }) => r.clientId);
const clients = await this.prisma.client.findMany({
where: { id: { in: clientIds } },
select: { id: true, name: true, code: true },
});
const clientMap = new Map(clients.map((c: { id: string; name: string; code: string }) => [c.id, c]));
return results.map((r: { clientId: string; _count: { clientId: number } }) => ({
client: clientMap.get(r.clientId) || { id: r.clientId, name: 'Unknown', code: 'UNK' },
orderCount: r._count.clientId,
}));
}
private async getAlertsSummary(
organizationId: string,
dateFilter: { gte: Date; lte: Date },
) {
const [critical, warning, info, resolved] = await Promise.all([
this.prisma.alert.count({
where: { organizationId, createdAt: dateFilter, severity: 'CRITICAL' },
}),
this.prisma.alert.count({
where: { organizationId, createdAt: dateFilter, severity: 'WARNING' },
}),
this.prisma.alert.count({
where: { organizationId, createdAt: dateFilter, severity: 'INFO' },
}),
this.prisma.alert.count({
where: { organizationId, createdAt: dateFilter, status: 'RESOLVED' },
}),
]);
return { critical, warning, info, resolved };
}
// ── CSV Export ─────────────────────────────────────────────
async exportOrdersCsv(
from: Date,
to: Date,
filters: { status?: string; clientId?: string },
orgId: string,
): Promise<string> {
const where: any = {
organizationId: orgId,
createdAt: { gte: from, lte: to },
};
if (filters.status) where.status = filters.status;
if (filters.clientId) where.clientId = filters.clientId;
const orders = await this.prisma.order.findMany({
where,
include: {
client: { select: { name: true } },
},
orderBy: { createdAt: 'desc' },
});
const headers = [
'Order Number',
'Client',
'Origin',
'Destination',
'Status',
'Priority',
'Weight',
'Pickup Date',
'Delivery Date',
'Created',
];
const escCsv = (val: string | null | undefined): string => {
if (val == null) return '';
const s = String(val);
if (s.includes(',') || s.includes('"') || s.includes('\n')) {
return `"${s.replace(/"/g, '""')}"`;
}
return s;
};
const rows = orders.map((o: any) => {
const origin = [o.originName, o.originCity, o.originState].filter(Boolean).join(', ');
const dest = [o.destName, o.destCity, o.destState].filter(Boolean).join(', ');
return [
escCsv(o.orderNumber),
escCsv(o.client?.name),
escCsv(origin),
escCsv(dest),
escCsv(o.status),
escCsv(o.priority),
escCsv(o.weight?.toString()),
escCsv(o.pickupDate ? new Date(o.pickupDate).toISOString().split('T')[0] : null),
escCsv(o.deliveryDate ? new Date(o.deliveryDate).toISOString().split('T')[0] : null),
escCsv(o.createdAt ? new Date(o.createdAt).toISOString().split('T')[0] : null),
].join(',');
});
return [headers.join(','), ...rows].join('\n');
}
}