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) { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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(), }; } }