import { Injectable } from '@nestjs/common'; import { OnEvent } from '@nestjs/event-emitter'; import { addMilliseconds } from 'date-fns'; import { PubSub } from 'graphql-subscriptions'; import { generateIntervalForTimeRange } from 'src/utils'; import { CalculationService } from '../feat-calculation/calculation.service'; import { ETLEventTypes } from '../feat-etl/feat-importers/events/event-types.enum'; import { LoadingDoneEvent } from '../feat-etl/feat-importers/events/loading-done.event'; import { CarEntityService } from '../feat-business-objects/car-entity/car-entity.service'; @Injectable() export class TimePerTourKpiService { public readonly pubSub = new PubSub(); @OnEvent(ETLEventTypes.LoadingDone, { async: true }) notifyOnPlannedTimeUpdate(event: LoadingDoneEvent): void { this.pubSub.publish('timePerTourUpdated', { timePerTourUpdated: { job: event.jobId }, }); } constructor( private readonly calculationService: CalculationService, private readonly carEntityService: CarEntityService, ) {} async toursPerTimeKPI(ordinanceType: string, date = new Date()) { const fullRangeInterval = generateIntervalForTimeRange( '05:00-01:00', 'Europe/Berlin', date, ); const subIntervals = [ generateIntervalForTimeRange('05:00-12:00', 'Europe/Berlin'), generateIntervalForTimeRange('12:00-18:30', 'Europe/Berlin'), generateIntervalForTimeRange('18:30-01:00', 'Europe/Berlin'), ]; const carEntities = await this.carEntityService.getCarsInInterval( fullRangeInterval, ordinanceType, ); return Promise.all( subIntervals .map(({ start, end }, index, array) => ({ start, end: index + 1 !== array.length ? addMilliseconds(end, -1) : end, })) .map(async (interval) => { const cars = await Promise.all( carEntities.map(async (carEntity) => { const tourAmount = carEntity.tourAmountInInterval(interval); const availableTime = carEntity.availableTimeInInterval(interval); const { personnelCosts, consumptionCost, toursRevenue } = await carEntity.breakEvenInInterval( interval, this.calculationService, ); const baseValue = this.calculateKPIBaseValue( availableTime, tourAmount, ); return { carName: carEntity.carName ?? 'Touren ohne Zuweisung', kpi: { value: !carEntity.carName ? tourAmount : baseValue, meta: { tourAmount: Math.round(tourAmount * 100) / 100, availableTime: Math.round(availableTime * 100) / 100, baseValue, breakEven: Math.round( (toursRevenue - personnelCosts - consumptionCost) * 100, ) / 100, }, }, }; }), ); const tourAmount = carEntities.reduce( (sum, carEntity) => (sum += carEntity.tourAmountInInterval(interval)), 0, ); const availableTime = carEntities.reduce( (sum, carEntity) => (sum += carEntity.availableTimeInInterval(interval)), 0, ); let revenue = 0; let personnelCost = 0; let consumptionCost = 0; for (const carEntity of carEntities) { const { personnelCosts, consumptionCost: cc, toursRevenue, } = await carEntity.breakEvenInInterval( interval, this.calculationService, ); revenue += toursRevenue; personnelCost += personnelCosts; consumptionCost += cc; } const baseValue = this.calculateKPIBaseValue( availableTime, tourAmount, ); return { interval, kpi: { value: baseValue, meta: { tourAmount: Math.round(tourAmount * 100) / 100, availableTime: Math.round(availableTime * 100) / 100, baseValue, breakEven: Math.round( (revenue - personnelCost - consumptionCost) * 100, ) / 100, }, }, cars, }; }), ); } calculateKPIBaseValue(availableTime: number, tourAmount: number): number { if (tourAmount === 0 && availableTime > 0) { return Math.round((availableTime / 0.01) * 1000) / 1000; } else if (tourAmount === 0) { return -1; } return Math.round((availableTime / tourAmount) * 1000) / 1000; } }