avicenna/service/data-hub/data-connector/src/modules/app-manager/time-per-tour-kpi.service.ts

146 lines
4.8 KiB
TypeScript

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;
}
}