mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
398 lines
12 KiB
TypeScript
398 lines
12 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
|
|
import {
|
|
BOOKING_LIST_TABS,
|
|
mapStatusCountsToTabs,
|
|
} from '../bookings/booking-list-tabs.config';
|
|
import type { OverviewRangeQuery } from './dto/overview-query.dto';
|
|
import type { OverviewResponseDto } from './dto/overview-response.dto';
|
|
import type {
|
|
OverviewBillingTabDto,
|
|
OverviewBookingsTabDto,
|
|
OverviewClearanceTabDto,
|
|
OverviewContractsTabDto,
|
|
OverviewCustomersTabDto,
|
|
OverviewFleetTabDto,
|
|
OverviewOperationsTabDto,
|
|
OverviewStaffTabDto,
|
|
} from './dto/overview-tab-response.dto';
|
|
import {
|
|
OVERVIEW_RANGE_DAYS,
|
|
mapContractStatusCountsToPipeline,
|
|
} from './overview.constants';
|
|
import { OverviewRepository } from './overview.repository';
|
|
|
|
@Injectable()
|
|
export class OverviewService {
|
|
constructor(private readonly overviewRepository: OverviewRepository) {}
|
|
|
|
private mapStatusCounts(statusCounts: Record<string, number>) {
|
|
const pipelineTabs = mapStatusCountsToTabs(statusCounts);
|
|
const bookingsByPipeline = BOOKING_LIST_TABS.filter(
|
|
(tab) => tab.key !== 'all',
|
|
).map((tab) => ({
|
|
stage: tab.key,
|
|
count: pipelineTabs[tab.key],
|
|
}));
|
|
|
|
const bookingsByStatus = Object.entries(statusCounts)
|
|
.map(([status, count]) => ({ status, count }))
|
|
.sort((a, b) => b.count - a.count);
|
|
|
|
return { bookingsByPipeline, bookingsByStatus };
|
|
}
|
|
|
|
async getDashboard(
|
|
range: OverviewRangeQuery = '30d',
|
|
dirs?: string[],
|
|
): Promise<OverviewResponseDto> {
|
|
const days = OVERVIEW_RANGE_DAYS[range];
|
|
|
|
const [
|
|
bookingKpis,
|
|
contractKpis,
|
|
operationsKpis,
|
|
customerKpis,
|
|
billingKpis,
|
|
staffKpis,
|
|
bookingTrend,
|
|
statusCounts,
|
|
paymentTrend,
|
|
current,
|
|
previous,
|
|
revenueByDirection,
|
|
revenueByFreightType,
|
|
previousPaymentTrend,
|
|
tonsTrend,
|
|
revenueFlows,
|
|
bookingHeatmap,
|
|
] = await Promise.all([
|
|
this.overviewRepository.getBookingKpis(dirs),
|
|
this.overviewRepository.getContractKpis(dirs),
|
|
this.overviewRepository.getOperationsKpis(),
|
|
this.overviewRepository.getCustomerKpis(),
|
|
this.overviewRepository.getBillingKpis(dirs),
|
|
this.overviewRepository.getStaffKpis(),
|
|
this.overviewRepository.getBookingTrend(days, dirs),
|
|
this.overviewRepository.getStatusCounts(dirs),
|
|
this.overviewRepository.getPaymentTrend(days, dirs),
|
|
this.overviewRepository.getPeriodTotals(days, 0, dirs),
|
|
this.overviewRepository.getPeriodTotals(days, days, dirs),
|
|
this.overviewRepository.getRevenueByDirection(days, dirs),
|
|
this.overviewRepository.getRevenueByFreightType(days, dirs),
|
|
this.overviewRepository.getPaymentTrend(days, dirs, days),
|
|
this.overviewRepository.getTonsTrend(days, dirs),
|
|
this.overviewRepository.getRevenueFlows(days, dirs),
|
|
this.overviewRepository.getBookingHeatmap(days, dirs),
|
|
]);
|
|
|
|
const { bookingsByPipeline, bookingsByStatus } =
|
|
this.mapStatusCounts(statusCounts);
|
|
|
|
return {
|
|
kpis: {
|
|
bookings: bookingKpis,
|
|
contracts: contractKpis,
|
|
operations: operationsKpis,
|
|
customers: customerKpis,
|
|
billing: billingKpis,
|
|
staff: staffKpis,
|
|
},
|
|
bookingTrend,
|
|
bookingsByStatus,
|
|
bookingsByPipeline,
|
|
paymentTrend,
|
|
current,
|
|
previous,
|
|
revenueByDirection,
|
|
revenueByFreightType,
|
|
previousPaymentTrend,
|
|
tonsTrend,
|
|
revenueFlows,
|
|
bookingHeatmap,
|
|
generatedAt: new Date().toISOString(),
|
|
};
|
|
}
|
|
|
|
async getBookingsTab(
|
|
range: OverviewRangeQuery = '30d',
|
|
dirs?: string[],
|
|
): Promise<OverviewBookingsTabDto> {
|
|
const days = OVERVIEW_RANGE_DAYS[range];
|
|
|
|
const [
|
|
kpis,
|
|
bookingTrend,
|
|
statusCounts,
|
|
bookingsByFreightType,
|
|
bookingsByCurrency,
|
|
recentBookings,
|
|
] = await Promise.all([
|
|
this.overviewRepository.getBookingKpis(dirs),
|
|
this.overviewRepository.getBookingTrend(days, dirs),
|
|
this.overviewRepository.getStatusCounts(dirs),
|
|
this.overviewRepository.getBookingsByFreightType(dirs),
|
|
this.overviewRepository.getBookingsByCurrency(dirs),
|
|
this.overviewRepository.getRecentBookings(8, dirs),
|
|
]);
|
|
|
|
const { bookingsByPipeline, bookingsByStatus } =
|
|
this.mapStatusCounts(statusCounts);
|
|
|
|
return {
|
|
kpis,
|
|
bookingTrend,
|
|
bookingsByStatus,
|
|
bookingsByPipeline,
|
|
bookingsByFreightType,
|
|
bookingsByCurrency,
|
|
recentBookings: recentBookings.map((row) => ({
|
|
...row,
|
|
createdAt: row.createdAt.toISOString(),
|
|
})),
|
|
generatedAt: new Date().toISOString(),
|
|
};
|
|
}
|
|
|
|
async getContractsTab(
|
|
range: OverviewRangeQuery = '30d',
|
|
dirs?: string[],
|
|
): Promise<OverviewContractsTabDto> {
|
|
const days = OVERVIEW_RANGE_DAYS[range];
|
|
|
|
const [
|
|
kpis,
|
|
contractTrend,
|
|
statusCounts,
|
|
contractsByKind,
|
|
contractsByFreightType,
|
|
recentContracts,
|
|
] = await Promise.all([
|
|
this.overviewRepository.getContractKpis(dirs),
|
|
this.overviewRepository.getContractTrend(days, dirs),
|
|
this.overviewRepository.getContractStatusCounts(dirs),
|
|
this.overviewRepository.getContractsByKind(dirs),
|
|
this.overviewRepository.getContractsByFreightType(dirs),
|
|
this.overviewRepository.getRecentContracts(8, dirs),
|
|
]);
|
|
|
|
const contractsByStatus = Object.entries(statusCounts)
|
|
.map(([status, count]) => ({ status, count }))
|
|
.sort((a, b) => b.count - a.count);
|
|
const contractsByPipeline = mapContractStatusCountsToPipeline(statusCounts);
|
|
|
|
return {
|
|
kpis,
|
|
contractTrend,
|
|
contractsByStatus,
|
|
contractsByPipeline,
|
|
contractsByKind,
|
|
contractsByFreightType,
|
|
recentContracts: recentContracts.map((row) => ({
|
|
...row,
|
|
validUntil: row.validUntil ? row.validUntil.toISOString() : null,
|
|
createdAt: row.createdAt.toISOString(),
|
|
})),
|
|
generatedAt: new Date().toISOString(),
|
|
};
|
|
}
|
|
|
|
async getBillingTab(
|
|
range: OverviewRangeQuery = '30d',
|
|
dirs?: string[],
|
|
): Promise<OverviewBillingTabDto> {
|
|
const days = OVERVIEW_RANGE_DAYS[range];
|
|
|
|
const [kpis, paymentTrend, paymentsByStatus, paymentsByMethod, revenueByCurrency] =
|
|
await Promise.all([
|
|
this.overviewRepository.getBillingKpis(dirs),
|
|
this.overviewRepository.getPaymentTrend(days, dirs),
|
|
this.overviewRepository.getPaymentsByStatus(dirs),
|
|
this.overviewRepository.getPaymentsByMethod(dirs),
|
|
this.overviewRepository.getRevenueByCurrency(dirs),
|
|
]);
|
|
|
|
return {
|
|
kpis,
|
|
paymentTrend,
|
|
paymentsByStatus,
|
|
paymentsByMethod,
|
|
revenueByCurrency,
|
|
generatedAt: new Date().toISOString(),
|
|
};
|
|
}
|
|
|
|
async getOperationsTab(
|
|
range: OverviewRangeQuery = '30d',
|
|
): Promise<OverviewOperationsTabDto> {
|
|
const days = OVERVIEW_RANGE_DAYS[range];
|
|
|
|
const [
|
|
kpis,
|
|
departureTrend,
|
|
scheduleStatusBreakdown,
|
|
wagonsByType,
|
|
wagonsByYard,
|
|
containersBySize,
|
|
cargoTonnageByType,
|
|
trainStatusBreakdown,
|
|
wagonStatusBreakdown,
|
|
containerStatusBreakdown,
|
|
cargoStatusBreakdown,
|
|
bookingsByPort,
|
|
bookingStatusByPort,
|
|
trainLoads,
|
|
] = await Promise.all([
|
|
this.overviewRepository.getOperationsKpis(),
|
|
this.overviewRepository.getDepartureTrend(days),
|
|
this.overviewRepository.getScheduleStatusBreakdown(),
|
|
this.overviewRepository.getWagonsByType(),
|
|
this.overviewRepository.getWagonsByYard(8),
|
|
this.overviewRepository.getContainersBySize(),
|
|
this.overviewRepository.getCargoTonnageByType(8),
|
|
this.overviewRepository.getTrainStatusBreakdown(),
|
|
this.overviewRepository.getWagonStatusBreakdown(),
|
|
this.overviewRepository.getContainerStatusBreakdown(),
|
|
this.overviewRepository.getCargoStatusBreakdown(),
|
|
this.overviewRepository.getBookingsByPort(days),
|
|
this.overviewRepository.getBookingStatusByPort(days),
|
|
this.overviewRepository.getTrainLoads(8),
|
|
]);
|
|
|
|
return {
|
|
kpis,
|
|
departureTrend,
|
|
scheduleStatusBreakdown,
|
|
wagonsByType,
|
|
wagonsByYard,
|
|
containersBySize,
|
|
cargoTonnageByType,
|
|
trainStatusBreakdown,
|
|
wagonStatusBreakdown,
|
|
containerStatusBreakdown,
|
|
cargoStatusBreakdown,
|
|
bookingsByPort,
|
|
bookingStatusByPort,
|
|
trainLoads,
|
|
generatedAt: new Date().toISOString(),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Rolling stock in depth: wagons and locomotives crossed with type and yard,
|
|
* plus how fast each train set turns around. Feeds the operations and control
|
|
* centre dashboards.
|
|
*/
|
|
async getFleetTab(
|
|
range: OverviewRangeQuery = '30d',
|
|
): Promise<OverviewFleetTabDto> {
|
|
const days = OVERVIEW_RANGE_DAYS[range];
|
|
|
|
const [
|
|
wagonStatusBreakdown,
|
|
wagonStatusByType,
|
|
wagonStatusByYard,
|
|
locomotiveStatusBreakdown,
|
|
locomotivesByYard,
|
|
locomotivesByType,
|
|
turnaround,
|
|
] = await Promise.all([
|
|
this.overviewRepository.getWagonStatusBreakdown(),
|
|
this.overviewRepository.getWagonStatusByType(),
|
|
this.overviewRepository.getWagonStatusByYard(),
|
|
this.overviewRepository.getLocomotiveStatusBreakdown(),
|
|
this.overviewRepository.getLocomotivesByYard(),
|
|
this.overviewRepository.getLocomotivesByType(),
|
|
this.overviewRepository.getTrainTurnaround(days, 8),
|
|
]);
|
|
|
|
return {
|
|
wagonStatusBreakdown,
|
|
wagonStatusByType,
|
|
wagonStatusByYard,
|
|
locomotiveStatusBreakdown,
|
|
locomotivesByYard,
|
|
locomotivesByType,
|
|
avgTurnaroundHours: turnaround.avgHours,
|
|
turnaroundByTrain: turnaround.rows,
|
|
generatedAt: new Date().toISOString(),
|
|
};
|
|
}
|
|
|
|
/** Document and invoice queues — the GL desks' and marketing's work in progress. */
|
|
async getClearanceTab(): Promise<OverviewClearanceTabDto> {
|
|
const [
|
|
bookingDocumentsByStatus,
|
|
contractDocumentsByStatus,
|
|
invoicesByStatus,
|
|
invoicesByType,
|
|
handoversByMile,
|
|
] = await Promise.all([
|
|
this.overviewRepository.getBookingDocumentsByStatus(),
|
|
this.overviewRepository.getContractDocumentsByStatus(),
|
|
this.overviewRepository.getInvoicesByStatus(),
|
|
this.overviewRepository.getInvoicesByType(),
|
|
this.overviewRepository.getHandoversByMile(),
|
|
]);
|
|
|
|
return {
|
|
bookingDocumentsByStatus,
|
|
contractDocumentsByStatus,
|
|
invoicesByStatus,
|
|
invoicesByType,
|
|
handoversByMile,
|
|
generatedAt: new Date().toISOString(),
|
|
};
|
|
}
|
|
|
|
async getCustomersTab(
|
|
range: OverviewRangeQuery = '30d',
|
|
dirs?: string[],
|
|
): Promise<OverviewCustomersTabDto> {
|
|
const days = OVERVIEW_RANGE_DAYS[range];
|
|
|
|
const [
|
|
kpis,
|
|
customerGrowthTrend,
|
|
customersByType,
|
|
topCustomersByBookings,
|
|
profilesByTypeStatus,
|
|
] = await Promise.all([
|
|
this.overviewRepository.getCustomerKpis(),
|
|
this.overviewRepository.getCustomerGrowthTrend(days),
|
|
this.overviewRepository.getCustomersByType(),
|
|
this.overviewRepository.getTopCustomersByBookings(8, dirs),
|
|
this.overviewRepository.getProfilesByTypeStatus(),
|
|
]);
|
|
|
|
return {
|
|
kpis,
|
|
customerGrowthTrend,
|
|
customersByType,
|
|
topCustomersByBookings,
|
|
profilesByTypeStatus,
|
|
generatedAt: new Date().toISOString(),
|
|
};
|
|
}
|
|
|
|
async getStaffTab(range: OverviewRangeQuery = '30d'): Promise<OverviewStaffTabDto> {
|
|
const days = OVERVIEW_RANGE_DAYS[range];
|
|
|
|
const [kpis, usersByStatus, employeeGrowthTrend, activeUsersBreakdown] =
|
|
await Promise.all([
|
|
this.overviewRepository.getStaffKpis(),
|
|
this.overviewRepository.getUsersByStatus(),
|
|
this.overviewRepository.getEmployeeGrowthTrend(days),
|
|
this.overviewRepository.getActiveUsersBreakdown(),
|
|
]);
|
|
|
|
return {
|
|
kpis,
|
|
usersByStatus,
|
|
employeeGrowthTrend,
|
|
activeUsersBreakdown,
|
|
generatedAt: new Date().toISOString(),
|
|
};
|
|
}
|
|
}
|