init data-connector

This commit is contained in:
Marcel Arndt
2025-04-10 17:32:06 +02:00
parent 52c1a14609
commit 46631db5b2
105 changed files with 8081 additions and 338 deletions
@@ -15,25 +15,28 @@ import { AnomalyTypes } from './anomaly-detectors/anomaly-types.enum';
import { DriverCertificateDetector } from './anomaly-detectors/driver-certificate.detector';
import { DriverCodriverSameDetector } from './anomaly-detectors/driver-codriver-same.detector';
import { EmployeesLoggedInDetector } from './anomaly-detectors/employees-logged-in.detector';
import { OneTourAtATimeDetector } from './anomaly-detectors/one-tour-at-a-time.detector';
// import { OneTourAtATimeDetector } from './anomaly-detectors/one-tour-at-a-time.detector';
import { WorkingEmployeeNotInDistributionSystemDetector } from './anomaly-detectors/working-employee-not-in-distribution-system.detector';
import { AnomalyDetector } from './anomaly-detectors/anomaly-detector.class';
@Injectable()
export class AnomaliesService {
private detectors = [
new EmployeesLoggedInDetector(this.anomalyDetectorService),
new DriverCertificateDetector(this.anomalyDetectorService),
new DriverCodriverSameDetector(this.anomalyDetectorService),
new OneTourAtATimeDetector(this.anomalyDetectorService),
new WorkingEmployeeNotInDistributionSystemDetector(
this.anomalyDetectorService,
),
];
private detectors: AnomalyDetector[];
constructor(
private readonly prisma: PrismaService,
private readonly anomalyDetectorService: AnomalyDetectorService,
) {}
) {
this.detectors = [
new EmployeesLoggedInDetector(this.anomalyDetectorService),
new DriverCertificateDetector(this.anomalyDetectorService),
new DriverCodriverSameDetector(this.anomalyDetectorService),
// new OneTourAtATimeDetector(this.anomalyDetectorService),
new WorkingEmployeeNotInDistributionSystemDetector(
this.anomalyDetectorService,
),
];
}
detectAnomalies(interval: Interval): Promise<Anomaly[]> {
return Promise.all(
@@ -105,6 +105,9 @@ export class AnomalyDetectorService {
},
],
},
orderBy: {
startDate: 'asc',
},
});
}
@@ -267,19 +267,6 @@ export class EmployeesLoggedInDetector extends AnomalyDetector {
const datetimeToCheckWithBuffer = addMinutes(datetimeToCheck, 5);
if (personnelNumber === '23') {
console.log(
datetimeToCheck,
{
start: new Date(workTime.startDate),
end: new Date(workTime.endDate),
},
{
start: new Date(firstClockedIn.dateTime),
end: new Date(lastClockedOut.dateTime),
},
);
}
try {
if (workTime) {
const interval = {
@@ -1,5 +1,5 @@
import { Logger } from '@nestjs/common';
import { Anomaly, Prisma } from '@prisma/client';
import { Anomaly, Prisma, Tour } from '@prisma/client';
import {
addDays,
areIntervalsOverlapping,
@@ -7,11 +7,11 @@ import {
format,
Interval,
isBefore,
isWithinInterval,
isSameDay,
set,
startOfDay,
} from 'date-fns';
import { toZonedTime, fromZonedTime } from 'date-fns-tz';
import { fromZonedTime, toZonedTime } from 'date-fns-tz';
import { AnomalyDetector } from './anomaly-detector.class';
import { AnomalyTypes } from './anomaly-types.enum';
@@ -20,177 +20,209 @@ export class OneTourAtATimeDetector extends AnomalyDetector {
private readonly logger = new Logger(OneTourAtATimeDetector.name);
readonly type = AnomalyTypes.OneTourAtATime;
private groupToursByDay(tours: Tour[]): Record<string, Tour[]> {
return tours.reduce(
(acc, cur) => {
const dayString = new Date(cur.startDate).toDateString();
const group = acc[dayString] ?? [];
group.push(cur);
return { ...acc, [dayString]: group };
},
{} as Record<string, Tour[]>,
);
}
async detect(inInterval: Interval): Promise<Prisma.AnomalyCreateInput[]> {
const toursInRange = await this.dataService.findToursInInterval(inInterval);
return toursInRange
.map(
(
{
operationId,
startDate,
startBegin,
startEnd,
targetBegin,
target,
empty,
carName,
driverId,
driverName,
codriverId,
codriverName,
},
_,
tours,
) => {
// somehow startBegin can be after empty, so we use startBegin and startEnd and take the lower value and hope for the best
const [start] = [startBegin, startEnd].sort();
// somehow empty can be before everything else, so we use targetBegin, target and empty and take the higher value ...
const [end] = [targetBegin, target, empty]
.filter((val) => !!val)
.sort()
.reverse();
const toursMap = this.groupToursByDay(toursInRange);
const result = [];
for (const [, tours] of Object.entries(toursMap)) {
const intermediateStep = tours
.map(
(
{
operationId,
startDate,
startBegin,
startEnd,
targetBegin,
target,
empty,
carName,
driverId,
driverName,
codriverId,
codriverName,
},
_,
tours,
) => {
// somehow startBegin can be after empty, so we use startBegin and startEnd and take the lower value and hope for the best
const [start] = [startBegin, startEnd].sort();
// somehow empty can be before everything else, so we use targetBegin, target and empty and take the higher value ...
const [end] = [targetBegin, target, empty]
.filter((val) => !!val)
.sort()
.reverse();
if (!start || !end) {
if (isBefore(new Date(), startDate)) {
this.logger.warn(
`Tour ${operationId} started in the past, but is missing start or end time`,
);
}
return [];
}
const tourInterval = this.getTourInterval(start, end, startDate);
const toursInInterval = tours
.filter((tour) =>
isWithinInterval(tour.startDate, {
start: startOfDay(startDate),
end: endOfDay(startDate),
}),
)
.filter(
({
operationId: tourListOperationId,
carName: tourListCarName,
}) =>
tourListOperationId !== operationId &&
carName !== tourListCarName,
)
.filter(
({
startDate: tourListStartDate,
startBegin: tourListStartBegin,
startEnd: tourListStartEnd,
targetBegin: tourListTargetBegin,
target: tourListTarget,
empty: tourListEmpty,
}) => {
// somehow startBegin can be after empty, so we use startBegin and startEnd and take the lower value and hope for the best
const [tourListStart] = [
tourListStartBegin,
tourListStartEnd,
].sort();
// somehow empty can be before everything else, so we use targetBegin and empty and take the higher value ...
const [tourListEnd] = [
tourListTargetBegin,
tourListTarget,
tourListEmpty,
]
.filter((val) => !!val)
.sort()
.reverse();
if (!tourListStart || !tourListEnd) {
return false;
}
const tourListInterval = this.getTourInterval(
tourListStart,
tourListEnd,
tourListStartDate,
if (!start || !end) {
if (isBefore(new Date(), startDate)) {
this.logger.warn(
`Tour ${operationId} started in the past, but is missing start or end time`,
);
try {
return areIntervalsOverlapping(
tourInterval,
tourListInterval,
}
return [];
}
const tourInterval = this.getTourInterval(start, end, startDate);
const toursInInterval = tours
.filter(
(tour) => isSameDay(tour.startDate, startDate),
// isWithinInterval(tour.startDate, {
// start: startOfDay(startDate),
// end: endOfDay(startDate),
// }),
)
.filter(
({
operationId: tourListOperationId,
carName: tourListCarName,
}) =>
tourListOperationId !== operationId &&
carName !== tourListCarName,
)
.filter(
({
startDate: tourListStartDate,
startBegin: tourListStartBegin,
startEnd: tourListStartEnd,
targetBegin: tourListTargetBegin,
target: tourListTarget,
empty: tourListEmpty,
}) => {
// somehow startBegin can be after empty, so we use startBegin and startEnd and take the lower value and hope for the best
const [tourListStart] = [
tourListStartBegin,
tourListStartEnd,
].sort();
// somehow empty can be before everything else, so we use targetBegin and empty and take the higher value ...
const [tourListEnd] = [
tourListTargetBegin,
tourListTarget,
tourListEmpty,
]
.filter((val) => !!val)
.sort()
.reverse();
if (!tourListStart || !tourListEnd) {
return false;
}
const tourListInterval = this.getTourInterval(
tourListStart,
tourListEnd,
tourListStartDate,
);
} catch (error) {
this.logger.error(
'tourListInterval',
error,
tourListInterval,
tourInterval,
);
return false;
}
},
try {
return areIntervalsOverlapping(
tourInterval,
tourListInterval,
);
} catch (error) {
this.logger.error(
'tourListInterval',
error,
tourListInterval,
tourInterval,
);
return false;
}
},
);
const toursInIntervalWithDriver = toursInInterval.filter(
({
driverId: tourListDriverId,
codriverId: tourListCodriverId,
}) =>
driverId === tourListDriverId ||
driverId === tourListCodriverId,
);
const toursInIntervalWithCodriver = toursInInterval.filter(
({
driverId: tourListDriverId,
codriverId: tourListCodriverId,
}) =>
codriverId === tourListDriverId ||
codriverId === tourListCodriverId,
);
const toursInIntervalWithDriver = toursInInterval.filter(
({ driverId: tourListDriverId, codriverId: tourListCodriverId }) =>
driverId === tourListDriverId || driverId === tourListCodriverId,
);
const toursInIntervalWithCodriver = toursInInterval.filter(
({ driverId: tourListDriverId, codriverId: tourListCodriverId }) =>
codriverId === tourListDriverId ||
codriverId === tourListCodriverId,
);
const overlappingCarsWithDriver = toursInIntervalWithDriver.map(
({ carName }) => carName,
);
const overlappingTourInfoWithDriver = toursInIntervalWithDriver.map(
({ operationId, startDate }) => ({
startDate: format(new Date(startDate), 'HH:mm dd.MM.yy'),
operationId,
}),
);
const overlappingCarsWithCodriver = toursInIntervalWithCodriver.map(
({ carName }) => carName,
);
const overlappingTourInfoWithCodriver =
toursInIntervalWithCodriver.map(({ operationId, startDate }) => ({
startDate: format(new Date(startDate), 'HH:mm dd.MM.yy'),
operationId,
}));
return [
toursInIntervalWithDriver.length
? this.createAnomaly(
`${operationId}-${driverId}-${this.type}`,
operationId,
driverName,
startDate,
[carName, ...overlappingCarsWithDriver],
[
{
operationId,
startDate: format(new Date(startDate), 'HH:mm dd.MM.yy'),
},
...overlappingTourInfoWithDriver,
],
)
: undefined,
toursInIntervalWithCodriver.length
? this.createAnomaly(
`${operationId}-${codriverId}-${this.type}`,
operationId,
codriverName,
startDate,
[carName, ...overlappingCarsWithCodriver],
[
{
operationId,
startDate: format(new Date(startDate), 'HH:mm dd.MM.yy'),
},
...overlappingTourInfoWithCodriver,
],
)
: undefined,
];
},
)
.flat()
.filter((anomaly) => !!anomaly);
const overlappingCarsWithDriver = toursInIntervalWithDriver.map(
({ carName }) => carName,
);
const overlappingTourInfoWithDriver = toursInIntervalWithDriver.map(
({ operationId, startDate }) => ({
startDate: format(new Date(startDate), 'HH:mm dd.MM.yy'),
operationId,
}),
);
const overlappingCarsWithCodriver = toursInIntervalWithCodriver.map(
({ carName }) => carName,
);
const overlappingTourInfoWithCodriver =
toursInIntervalWithCodriver.map(({ operationId, startDate }) => ({
startDate: format(new Date(startDate), 'HH:mm dd.MM.yy'),
operationId,
}));
return [
toursInIntervalWithDriver.length
? this.createAnomaly(
`${operationId}-${driverId}-${this.type}`,
operationId,
driverName,
startDate,
[carName, ...overlappingCarsWithDriver],
[
{
operationId,
startDate: format(
new Date(startDate),
'HH:mm dd.MM.yy',
),
},
...overlappingTourInfoWithDriver,
],
)
: undefined,
toursInIntervalWithCodriver.length
? this.createAnomaly(
`${operationId}-${codriverId}-${this.type}`,
operationId,
codriverName,
startDate,
[carName, ...overlappingCarsWithCodriver],
[
{
operationId,
startDate: format(
new Date(startDate),
'HH:mm dd.MM.yy',
),
},
...overlappingTourInfoWithCodriver,
],
)
: undefined,
];
},
)
.flat()
.filter((anomaly) => !!anomaly);
result.push(intermediateStep);
}
return result.flat();
}
async validate(anomaly: Anomaly): Promise<Anomaly> {