add business model processing
This commit is contained in:
parent
8be07e1f9b
commit
53e945253c
|
|
@ -0,0 +1,21 @@
|
|||
FROM node:20.15.1-alpine as builder
|
||||
|
||||
WORKDIR /app/builder
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN npm install \
|
||||
&& npm run build
|
||||
|
||||
FROM node:20.15.1-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/builder/node_modules ./node_modules
|
||||
COPY --from=builder /app/builder/prisma ./prisma
|
||||
COPY --from=builder /app/builder/dist .
|
||||
COPY --from=builder /app/builder/docker-entrypoint.sh .
|
||||
|
||||
ENTRYPOINT [ "./docker-entrypoint.sh" ]
|
||||
|
||||
CMD [ "node", "./src/main.js" ]
|
||||
|
|
@ -1,6 +1,29 @@
|
|||
version: '3'
|
||||
|
||||
services:
|
||||
|
||||
avicenna-data:
|
||||
image: thedevilisdero/avic-data-connector
|
||||
ports:
|
||||
- 3000:3000
|
||||
env_file: .env
|
||||
environment:
|
||||
DATABASE_PORT: ${DATABASE_PORT}
|
||||
DATABASE_HOST: ${DATABASE_HOST}
|
||||
DATABASE_USERNAME: ${DATABASE_USERNAME}
|
||||
DATABASE_PASSWORD: ${DATABASE_PASSWORD}
|
||||
DATABASE_DATABASE: ${DATABASE_DATABASE}
|
||||
DATABASE_URL: ${DATABASE_URL}
|
||||
DISPO_LIVE_BASE_URL: ${DISPO_LIVE_BASE_URL}
|
||||
DISPO_LIVE_USERNAME: ${DISPO_LIVE_USERNAME}
|
||||
DISPO_LIVE_PASSWORD: ${DISPO_LIVE_PASSWORD}
|
||||
DYFLEXIS_V0_BASE_URL: ${DYFLEXIS_V0_BASE_URL}
|
||||
DYFLEXIS_V2_BASE_URL: ${DYFLEXIS_V2_BASE_URL}
|
||||
DYFLEXIS_V0_API_KEY: ${DYFLEXIS_V0_API_KEY}
|
||||
DYFLEXIS_V2_API_KEY: ${DYFLEXIS_V2_API_KEY}
|
||||
networks:
|
||||
- avic
|
||||
|
||||
avicenna-db:
|
||||
image: postgres:16.3
|
||||
ports:
|
||||
|
|
@ -14,6 +37,11 @@ services:
|
|||
PG_DATA: /var/lib/postgresql/data
|
||||
volumes:
|
||||
- ./.local/pgdata:/var/lib/postgresql/data
|
||||
networks:
|
||||
- avic
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
|
||||
networks:
|
||||
avic:
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
#!/bin/sh
|
||||
npx prisma generate
|
||||
|
||||
exec "$@"
|
||||
|
|
@ -0,0 +1,535 @@
|
|||
import type { ColumnType } from "kysely";
|
||||
export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>
|
||||
? ColumnType<S, I | undefined, U>
|
||||
: ColumnType<T, T | undefined, T>;
|
||||
export type Timestamp = ColumnType<Date, Date | string, Date | string>;
|
||||
|
||||
export const TicketState = {
|
||||
CREATED: "CREATED",
|
||||
APPROVAL_REQUIRED: "APPROVAL_REQUIRED",
|
||||
READY: "READY",
|
||||
DONE: "DONE",
|
||||
BILLABLE: "BILLABLE",
|
||||
ARCHIVED: "ARCHIVED"
|
||||
} as const;
|
||||
export type TicketState = (typeof TicketState)[keyof typeof TicketState];
|
||||
export const TicketValidationState = {
|
||||
CREATED: "CREATED",
|
||||
USER_INTERACTION_REQUIRED: "USER_INTERACTION_REQUIRED",
|
||||
TD_UNCERTAIN: "TD_UNCERTAIN",
|
||||
DOCUMENTS_MISSING: "DOCUMENTS_MISSING",
|
||||
ARCHIVED: "ARCHIVED"
|
||||
} as const;
|
||||
export type TicketValidationState = (typeof TicketValidationState)[keyof typeof TicketValidationState];
|
||||
export const ApprovalState = {
|
||||
UNKNOWN: "UNKNOWN",
|
||||
FREE: "FREE",
|
||||
REQUIRED: "REQUIRED",
|
||||
REQUESTED: "REQUESTED",
|
||||
APPROVED: "APPROVED"
|
||||
} as const;
|
||||
export type ApprovalState = (typeof ApprovalState)[keyof typeof ApprovalState];
|
||||
export const CostTypeVariant = {
|
||||
NACHTZUSCHLAG: "NACHTZUSCHLAG",
|
||||
WOCHENENDZUSCHLAG: "WOCHENENDZUSCHLAG",
|
||||
FEIERTAGSZUSCHLAG: "FEIERTAGSZUSCHLAG",
|
||||
DESINFEKTIONSZUSCHLAG: "DESINFEKTIONSZUSCHLAG",
|
||||
KILOMETERZUSCHLAG: "KILOMETERZUSCHLAG",
|
||||
FAHRZEUGZUSCHLAG: "FAHRZEUGZUSCHLAG",
|
||||
TSWDIFFERENZIERUNG: "TSWDIFFERENZIERUNG",
|
||||
GRUNDPREIS: "GRUNDPREIS"
|
||||
} as const;
|
||||
export type CostTypeVariant = (typeof CostTypeVariant)[keyof typeof CostTypeVariant];
|
||||
export type Anomaly = {
|
||||
id: string;
|
||||
type: string;
|
||||
description: string;
|
||||
groupKey: string;
|
||||
groupDescription: string;
|
||||
workedOnBy: string;
|
||||
sleepTimer: number;
|
||||
sleepSince: Timestamp | null;
|
||||
resolvedAt: Timestamp | null;
|
||||
manuallyResolved: Generated<boolean>;
|
||||
createdAt: Generated<Timestamp>;
|
||||
};
|
||||
export type AttendanceRegistration = {
|
||||
attendanceRegistrationId: number;
|
||||
employeeId: number;
|
||||
personnelNumber: string;
|
||||
dateTime: Timestamp;
|
||||
event: string;
|
||||
};
|
||||
export type AttendanceRegistrationSource = {
|
||||
internalId: Generated<string>;
|
||||
insertedAt: Generated<Timestamp>;
|
||||
jobId: string;
|
||||
attendanceRegistrationId: string | null;
|
||||
employeeId: string | null;
|
||||
personnelNumber: string | null;
|
||||
dateTime: string | null;
|
||||
event: string | null;
|
||||
};
|
||||
export type AttendanceRegistrationStaging = {
|
||||
internalId: Generated<string>;
|
||||
attendanceRegistrationId: number;
|
||||
employeeId: number;
|
||||
dateTime: Timestamp;
|
||||
event: string;
|
||||
};
|
||||
export type AvicennaTenant = {
|
||||
id: Generated<string>;
|
||||
healthInsuranceNumber: string;
|
||||
name: string;
|
||||
};
|
||||
export type AvicennaTenantToTariffGroup = {
|
||||
A: string;
|
||||
B: string;
|
||||
};
|
||||
export type CostType = {
|
||||
id: Generated<string>;
|
||||
description: string;
|
||||
variant: CostTypeVariant;
|
||||
costAmount: number;
|
||||
timeRange: string | null;
|
||||
kmInclusive: number | null;
|
||||
excludeCarNames: string[];
|
||||
infectionType: string | null;
|
||||
tariffId: string | null;
|
||||
};
|
||||
export type DyflexisEmployee = {
|
||||
id: number;
|
||||
firstname: string;
|
||||
surname: string;
|
||||
contractStart: Timestamp;
|
||||
contractEnd: Timestamp | null;
|
||||
contractHoursWeek: number;
|
||||
contractSalaryHour: number;
|
||||
};
|
||||
export type Employee = {
|
||||
id: string;
|
||||
name: string;
|
||||
surname: string;
|
||||
personnelNumber: string | null;
|
||||
mobile: string | null;
|
||||
qualification: string | null;
|
||||
occuptationalHealthExamination: Timestamp | null;
|
||||
personTransportCertificate: Timestamp | null;
|
||||
};
|
||||
export type EmployeeDispoLiveSource = {
|
||||
internalId: Generated<string>;
|
||||
insertedAt: Generated<Timestamp>;
|
||||
jobId: string;
|
||||
id: string | null;
|
||||
pScheinAblaufDatum: string | null;
|
||||
name: string | null;
|
||||
surname: string | null;
|
||||
persId: string | null;
|
||||
importPersId: string | null;
|
||||
qualifikation: string | null;
|
||||
};
|
||||
export type EmployeeDispoLiveStaging = {
|
||||
internalId: Generated<string>;
|
||||
id: string;
|
||||
name: string | null;
|
||||
surname: string | null;
|
||||
personnelNumber: string | null;
|
||||
qualification: string | null;
|
||||
personTransportCertificate: Timestamp | null;
|
||||
};
|
||||
export type EmployeeDyflexisSource = {
|
||||
internalId: Generated<string>;
|
||||
insertedAt: Generated<Timestamp>;
|
||||
jobId: string;
|
||||
id: string | null;
|
||||
firstname: string | null;
|
||||
surname: string | null;
|
||||
contract_id: string | null;
|
||||
contract_start: string | null;
|
||||
contract_end: string | null;
|
||||
contract_hours_week: string | null;
|
||||
contract_salary_hour: string | null;
|
||||
};
|
||||
export type EmployeeDyflexisStaging = {
|
||||
internalId: Generated<string>;
|
||||
id: number;
|
||||
firstname: string;
|
||||
surname: string;
|
||||
contractStart: Timestamp;
|
||||
contractEnd: Timestamp;
|
||||
contractHoursWeek: number;
|
||||
contractSalaryHour: number;
|
||||
};
|
||||
export type Patient = {
|
||||
id: string;
|
||||
surname: string | null;
|
||||
name: string | null;
|
||||
street: string | null;
|
||||
zip: string | null;
|
||||
city: string | null;
|
||||
birthday: Timestamp | null;
|
||||
healthinsuranceId: string | null;
|
||||
healthinsurance: string | null;
|
||||
careDegree: string | null;
|
||||
disabilityMark1: string | null;
|
||||
disabilityMark2: string | null;
|
||||
};
|
||||
export type PatientSource = {
|
||||
internalId: Generated<string>;
|
||||
insertedAt: Generated<Timestamp>;
|
||||
jobId: string;
|
||||
id: string | null;
|
||||
surname: string | null;
|
||||
name: string | null;
|
||||
street: string | null;
|
||||
zip: string | null;
|
||||
city: string | null;
|
||||
birthday: string | null;
|
||||
kkId: string | null;
|
||||
krankenkasse: string | null;
|
||||
pflegestufe: string | null;
|
||||
behinderung1: string | null;
|
||||
behinderung2: string | null;
|
||||
};
|
||||
export type PatientStaging = {
|
||||
internalId: Generated<string>;
|
||||
id: string | null;
|
||||
surname: string | null;
|
||||
name: string | null;
|
||||
street: string | null;
|
||||
zip: string | null;
|
||||
city: string | null;
|
||||
birthday: Timestamp | null;
|
||||
healthinsuranceId: string | null;
|
||||
healthinsurance: string | null;
|
||||
careDegree: string | null;
|
||||
disabilityMark1: string | null;
|
||||
disabilityMark2: string | null;
|
||||
};
|
||||
export type PlannedTime = {
|
||||
id: string;
|
||||
userId: string;
|
||||
firstname: string | null;
|
||||
surname: string | null;
|
||||
startDate: Timestamp | null;
|
||||
endDate: Timestamp | null;
|
||||
department: string | null;
|
||||
pause: number;
|
||||
duration: number;
|
||||
note: string | null;
|
||||
};
|
||||
export type PlannedTimeSource = {
|
||||
internalId: Generated<string>;
|
||||
insertedAt: Generated<Timestamp>;
|
||||
jobId: string;
|
||||
id: string | null;
|
||||
user_id: string | null;
|
||||
firstname: string | null;
|
||||
surname: string | null;
|
||||
department_id: string | null;
|
||||
department_name: string | null;
|
||||
start_date: string | null;
|
||||
end_date: string | null;
|
||||
pauze: string | null;
|
||||
duration: string | null;
|
||||
deleted: string | null;
|
||||
mark: string | null;
|
||||
};
|
||||
export type PlannedTimeStaging = {
|
||||
internalId: Generated<string>;
|
||||
id: number;
|
||||
employeeId: number;
|
||||
firstname: string | null;
|
||||
surname: string | null;
|
||||
departmentName: string | null;
|
||||
startDate: Timestamp;
|
||||
endDate: Timestamp;
|
||||
deleted: boolean;
|
||||
mark: string | null;
|
||||
};
|
||||
export type SolutionTarget = {
|
||||
id: Generated<string>;
|
||||
name: string;
|
||||
anomalyId: string | null;
|
||||
};
|
||||
export type SolutionTargetStep = {
|
||||
id: Generated<string>;
|
||||
description: string;
|
||||
solutionTargetId: string | null;
|
||||
};
|
||||
export type Tariff = {
|
||||
id: Generated<string>;
|
||||
validFrom: Timestamp | null;
|
||||
validTo: Timestamp | null;
|
||||
tariffGroupId: string | null;
|
||||
};
|
||||
export type TariffGroup = {
|
||||
id: Generated<string>;
|
||||
description: string;
|
||||
ordinanceType: string;
|
||||
};
|
||||
export type Ticket = {
|
||||
id: Generated<string>;
|
||||
currentState: TicketValidationState;
|
||||
notes: string[];
|
||||
errors: string[];
|
||||
isUrgent: Generated<boolean>;
|
||||
urgency: Generated<Timestamp>;
|
||||
approvalState: ApprovalState | null;
|
||||
documentInfo: Generated<unknown | null>;
|
||||
createdAt: Generated<Timestamp>;
|
||||
};
|
||||
export type TicketStateHistory = {
|
||||
id: Generated<string>;
|
||||
state: TicketValidationState;
|
||||
createdAt: Generated<Timestamp>;
|
||||
ticketId: string;
|
||||
};
|
||||
export type Tour = {
|
||||
id: Generated<string>;
|
||||
done: boolean;
|
||||
direction: string;
|
||||
carName: string | null;
|
||||
operationId: string;
|
||||
driverId: string | null;
|
||||
driverName: string | null;
|
||||
codriverId: string | null;
|
||||
codriverName: string | null;
|
||||
patientId: string | null;
|
||||
patientName: string | null;
|
||||
patientSurname: string | null;
|
||||
patientStreet: string | null;
|
||||
patientZip: string | null;
|
||||
patientCity: string | null;
|
||||
healthInsurance: string | null;
|
||||
healthInsuranceNumber: string | null;
|
||||
type: string | null;
|
||||
category: string | null;
|
||||
transportType: string | null;
|
||||
ordinanceType: string | null;
|
||||
rangeEndDate: Timestamp | null;
|
||||
startInstitution: string | null;
|
||||
startStreet: string | null;
|
||||
startZip: string | null;
|
||||
startCity: string | null;
|
||||
targetInstitution: string | null;
|
||||
targetStreet: string | null;
|
||||
targetZip: string | null;
|
||||
targetCity: string | null;
|
||||
startDate: Timestamp;
|
||||
check: Generated<number>;
|
||||
occupiedKm: number | null;
|
||||
totalKm: number | null;
|
||||
startBegin: string | null;
|
||||
startEnd: string | null;
|
||||
target: string | null;
|
||||
targetBegin: string | null;
|
||||
empty: string | null;
|
||||
hasInfection: Generated<boolean>;
|
||||
infectionName: string | null;
|
||||
revenueDispoLive: number | null;
|
||||
billDate: Timestamp | null;
|
||||
billNumber: string | null;
|
||||
revenue: Generated<number>;
|
||||
consumptionCosts: Generated<number>;
|
||||
revenueDeviation: Generated<number>;
|
||||
createdAt: Timestamp | null;
|
||||
deletedAt: Timestamp | null;
|
||||
ticketId: string | null;
|
||||
};
|
||||
export type TourSource = {
|
||||
internalId: Generated<string>;
|
||||
insertedAt: Generated<Timestamp>;
|
||||
jobId: string;
|
||||
id: string | null;
|
||||
check: string | null;
|
||||
createTime: string | null;
|
||||
empfangenVonId: string | null;
|
||||
endgen: string | null;
|
||||
serienId: string | null;
|
||||
serienKategorName: string | null;
|
||||
serienKategorie: string | null;
|
||||
zadStatus: string | null;
|
||||
abrDate: string | null;
|
||||
abrNum: string | null;
|
||||
abrTime: string | null;
|
||||
ausrueckZeit: string | null;
|
||||
startTime: string | null;
|
||||
startDate: string | null;
|
||||
date: string | null;
|
||||
date_iso: string | null;
|
||||
dauergenehmigung: string | null;
|
||||
direction: string | null;
|
||||
distanceValue: string | null;
|
||||
besetztKm: string | null;
|
||||
gefahren: string | null;
|
||||
gesamtPreis: string | null;
|
||||
infektion: string | null;
|
||||
itDesc: string | null;
|
||||
itKrz: string | null;
|
||||
itName: string | null;
|
||||
kkId: string | null;
|
||||
krankenkasse: string | null;
|
||||
patId: string | null;
|
||||
patName: string | null;
|
||||
patSurname: string | null;
|
||||
patStreet: string | null;
|
||||
patZip: string | null;
|
||||
patCity: string | null;
|
||||
startInstitution: string | null;
|
||||
startStreet: string | null;
|
||||
startZip: string | null;
|
||||
startCity: string | null;
|
||||
targetInstitution: string | null;
|
||||
targetStreet: string | null;
|
||||
targetZip: string | null;
|
||||
targetCity: string | null;
|
||||
transportKrz: string | null;
|
||||
transportName: string | null;
|
||||
transportart: string | null;
|
||||
verordnungsId: string | null;
|
||||
verordnungsName: string | null;
|
||||
type: string | null;
|
||||
startBegin: string | null;
|
||||
startEnd: string | null;
|
||||
frei: string | null;
|
||||
ziel: string | null;
|
||||
zielBegin: string | null;
|
||||
endTime: string | null;
|
||||
endDate: string | null;
|
||||
einsatzNummer: string | null;
|
||||
carName: string | null;
|
||||
carNo: string | null;
|
||||
beifahrerName: string | null;
|
||||
beifahrer: string | null;
|
||||
fahrerName: string | null;
|
||||
fahrer: string | null;
|
||||
color: string | null;
|
||||
};
|
||||
export type TourStaging = {
|
||||
internalId: Generated<string>;
|
||||
id: string | null;
|
||||
done: boolean | null;
|
||||
direction: string | null;
|
||||
carName: string | null;
|
||||
operationId: string | null;
|
||||
driverId: string | null;
|
||||
driverName: string | null;
|
||||
codriverId: string | null;
|
||||
codriverName: string | null;
|
||||
patientId: string | null;
|
||||
patientName: string | null;
|
||||
patientSurname: string | null;
|
||||
patientStreet: string | null;
|
||||
patientZip: string | null;
|
||||
patientCity: string | null;
|
||||
healthInsurance: string | null;
|
||||
healthInsuranceNumber: string | null;
|
||||
type: string | null;
|
||||
category: string | null;
|
||||
transportType: string | null;
|
||||
ordinanceType: string | null;
|
||||
rangeEndDate: Timestamp | null;
|
||||
startInstitution: string | null;
|
||||
startStreet: string | null;
|
||||
startZip: string | null;
|
||||
startCity: string | null;
|
||||
targetInstitution: string | null;
|
||||
targetStreet: string | null;
|
||||
targetZip: string | null;
|
||||
targetCity: string | null;
|
||||
startDate: Timestamp | null;
|
||||
check: Generated<number>;
|
||||
occupiedKm: number | null;
|
||||
totalKm: number | null;
|
||||
startBegin: string | null;
|
||||
startEnd: string | null;
|
||||
target: string | null;
|
||||
targetBegin: string | null;
|
||||
empty: string | null;
|
||||
hasInfection: Generated<boolean>;
|
||||
infectionName: string | null;
|
||||
revenueDispoLive: number | null;
|
||||
billDate: Timestamp | null;
|
||||
billNumber: string | null;
|
||||
revenue: Generated<number>;
|
||||
revenueDeviation: Generated<number>;
|
||||
consumptionCosts: Generated<number>;
|
||||
createdAt: Timestamp | null;
|
||||
};
|
||||
export type Trace = {
|
||||
id: Generated<string>;
|
||||
type: string;
|
||||
entityKey: string;
|
||||
createdAt: Generated<Timestamp>;
|
||||
};
|
||||
export type WorkTime = {
|
||||
id: string;
|
||||
userId: string;
|
||||
firstname: string;
|
||||
surname: string;
|
||||
startDate: Timestamp | null;
|
||||
endDate: Timestamp | null;
|
||||
department: string | null;
|
||||
pause: number;
|
||||
duration: number;
|
||||
};
|
||||
export type WorkTimeSource = {
|
||||
internalId: Generated<string>;
|
||||
insertedAt: Generated<Timestamp>;
|
||||
jobId: string;
|
||||
id: string | null;
|
||||
user_id: string | null;
|
||||
firstname: string | null;
|
||||
surname: string | null;
|
||||
department_id: string | null;
|
||||
department_name: string | null;
|
||||
start_date: string | null;
|
||||
end_date: string | null;
|
||||
pauze: string | null;
|
||||
duration: string | null;
|
||||
};
|
||||
export type WorkTimeStaging = {
|
||||
internalId: Generated<string>;
|
||||
id: number;
|
||||
employeeId: number;
|
||||
firstname: string;
|
||||
surname: string;
|
||||
departmentName: string | null;
|
||||
startDate: Timestamp;
|
||||
endDate: Timestamp;
|
||||
};
|
||||
export type DB = {
|
||||
_AvicennaTenantToTariffGroup: AvicennaTenantToTariffGroup;
|
||||
Anomaly: Anomaly;
|
||||
AttendanceRegistration: AttendanceRegistration;
|
||||
AttendanceRegistrationSource: AttendanceRegistrationSource;
|
||||
AttendanceRegistrationStaging: AttendanceRegistrationStaging;
|
||||
AvicennaTenant: AvicennaTenant;
|
||||
CostType: CostType;
|
||||
DyflexisEmployee: DyflexisEmployee;
|
||||
Employee: Employee;
|
||||
EmployeeDispoLiveSource: EmployeeDispoLiveSource;
|
||||
EmployeeDispoLiveStaging: EmployeeDispoLiveStaging;
|
||||
EmployeeDyflexisSource: EmployeeDyflexisSource;
|
||||
EmployeeDyflexisStaging: EmployeeDyflexisStaging;
|
||||
Patient: Patient;
|
||||
PatientSource: PatientSource;
|
||||
PatientStaging: PatientStaging;
|
||||
PlannedTime: PlannedTime;
|
||||
PlannedTimeSource: PlannedTimeSource;
|
||||
PlannedTimeStaging: PlannedTimeStaging;
|
||||
SolutionTarget: SolutionTarget;
|
||||
SolutionTargetStep: SolutionTargetStep;
|
||||
Tariff: Tariff;
|
||||
TariffGroup: TariffGroup;
|
||||
Ticket: Ticket;
|
||||
TicketStateHistory: TicketStateHistory;
|
||||
Tour: Tour;
|
||||
TourSource: TourSource;
|
||||
TourStaging: TourStaging;
|
||||
Trace: Trace;
|
||||
WorkTime: WorkTime;
|
||||
WorkTimeSource: WorkTimeSource;
|
||||
WorkTimeStaging: WorkTimeStaging;
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -19,6 +19,9 @@
|
|||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
},
|
||||
"prisma": {
|
||||
"seed": "ts-node prisma/seed/seed.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/axios": "^3.0.2",
|
||||
"@nestjs/common": "^10.0.0",
|
||||
|
|
@ -30,6 +33,9 @@
|
|||
"@prisma/client": "^5.16.2",
|
||||
"date-fns": "^3.6.0",
|
||||
"date-fns-tz": "^3.1.3",
|
||||
"kysely": "^0.27.4",
|
||||
"pg": "^8.12.0",
|
||||
"prisma-kysely": "^1.8.0",
|
||||
"reflect-metadata": "^0.2.0",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,184 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `EmployeeStaging` table. If the table is not empty, all the data it contains will be lost.
|
||||
|
||||
*/
|
||||
-- CreateEnum
|
||||
CREATE TYPE "CostTypeVariant" AS ENUM ('NACHTZUSCHLAG', 'WOCHENENDZUSCHLAG', 'FEIERTAGSZUSCHLAG', 'DESINFEKTIONSZUSCHLAG', 'KILOMETERZUSCHLAG', 'FAHRZEUGZUSCHLAG', 'TSWDIFFERENZIERUNG', 'GRUNDPREIS');
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "EmployeeStaging";
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "EmployeeDyflexisStaging" (
|
||||
"internalId" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"id" INTEGER NOT NULL,
|
||||
"firstname" TEXT NOT NULL,
|
||||
"surname" TEXT NOT NULL,
|
||||
"contractStart" TIMESTAMP(3) NOT NULL,
|
||||
"contractEnd" TIMESTAMP(3) NOT NULL,
|
||||
"contractHoursWeek" DOUBLE PRECISION NOT NULL,
|
||||
"contractSalaryHour" DOUBLE PRECISION NOT NULL,
|
||||
|
||||
CONSTRAINT "EmployeeDyflexisStaging_pkey" PRIMARY KEY ("internalId")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "EmployeeDispoLiveStaging" (
|
||||
"internalId" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT,
|
||||
"surname" TEXT,
|
||||
"personnelNumber" TEXT,
|
||||
"qualification" TEXT,
|
||||
"personTransportCertificate" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "EmployeeDispoLiveStaging_pkey" PRIMARY KEY ("internalId")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Employee" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"surname" TEXT NOT NULL,
|
||||
"personnelNumber" TEXT,
|
||||
"mobile" TEXT,
|
||||
"qualification" TEXT,
|
||||
"occuptationalHealthExamination" TIMESTAMP(3),
|
||||
"personTransportCertificate" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "Employee_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "DyflexisEmployee" (
|
||||
"id" INTEGER NOT NULL,
|
||||
"firstname" TEXT NOT NULL,
|
||||
"surname" TEXT NOT NULL,
|
||||
"contractStart" TIMESTAMP(3) NOT NULL,
|
||||
"contractEnd" TIMESTAMP(3),
|
||||
"contractHoursWeek" DOUBLE PRECISION NOT NULL,
|
||||
"contractSalaryHour" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "DyflexisEmployee_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AvicennaTenant" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"healthInsuranceNumber" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "AvicennaTenant_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "TariffGroup" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"description" TEXT NOT NULL,
|
||||
"ordinanceType" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "TariffGroup_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Tariff" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"validFrom" TIMESTAMP(3),
|
||||
"validTo" TIMESTAMP(3),
|
||||
"tariffGroupId" UUID,
|
||||
|
||||
CONSTRAINT "Tariff_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "CostType" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"description" TEXT NOT NULL,
|
||||
"variant" "CostTypeVariant" NOT NULL,
|
||||
"costAmount" INTEGER NOT NULL,
|
||||
"timeRange" TEXT,
|
||||
"kmInclusive" INTEGER,
|
||||
"excludeCarNames" TEXT[],
|
||||
"infectionType" TEXT,
|
||||
"tariffId" UUID
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Trace" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"type" TEXT NOT NULL,
|
||||
"entityKey" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Trace_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Anomaly" (
|
||||
"id" TEXT NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"description" TEXT NOT NULL,
|
||||
"groupKey" TEXT NOT NULL,
|
||||
"groupDescription" TEXT NOT NULL,
|
||||
"workedOnBy" TEXT NOT NULL,
|
||||
"sleepTimer" INTEGER NOT NULL,
|
||||
"sleepSince" TIMESTAMP(3),
|
||||
"resolvedAt" TIMESTAMP(3),
|
||||
"manuallyResolved" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Anomaly_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "SolutionTarget" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"name" TEXT NOT NULL,
|
||||
"anomalyId" TEXT,
|
||||
|
||||
CONSTRAINT "SolutionTarget_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "SolutionTargetStep" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"description" TEXT NOT NULL,
|
||||
"solutionTargetId" UUID,
|
||||
|
||||
CONSTRAINT "SolutionTargetStep_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_AvicennaTenantToTariffGroup" (
|
||||
"A" UUID NOT NULL,
|
||||
"B" UUID NOT NULL
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "CostType_id_key" ON "CostType"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "_AvicennaTenantToTariffGroup_AB_unique" ON "_AvicennaTenantToTariffGroup"("A", "B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_AvicennaTenantToTariffGroup_B_index" ON "_AvicennaTenantToTariffGroup"("B");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Tariff" ADD CONSTRAINT "Tariff_tariffGroupId_fkey" FOREIGN KEY ("tariffGroupId") REFERENCES "TariffGroup"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "CostType" ADD CONSTRAINT "CostType_tariffId_fkey" FOREIGN KEY ("tariffId") REFERENCES "Tariff"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "SolutionTarget" ADD CONSTRAINT "SolutionTarget_anomalyId_fkey" FOREIGN KEY ("anomalyId") REFERENCES "Anomaly"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "SolutionTargetStep" ADD CONSTRAINT "SolutionTargetStep_solutionTargetId_fkey" FOREIGN KEY ("solutionTargetId") REFERENCES "SolutionTarget"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_AvicennaTenantToTariffGroup" ADD CONSTRAINT "_AvicennaTenantToTariffGroup_A_fkey" FOREIGN KEY ("A") REFERENCES "AvicennaTenant"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_AvicennaTenantToTariffGroup" ADD CONSTRAINT "_AvicennaTenantToTariffGroup_B_fkey" FOREIGN KEY ("B") REFERENCES "TariffGroup"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
|
@ -6,7 +6,15 @@
|
|||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
previewFeatures = ["postgresqlExtensions", "fieldReference"]
|
||||
previewFeatures = ["postgresqlExtensions"]
|
||||
}
|
||||
|
||||
generator kysely {
|
||||
provider = "prisma-kysely"
|
||||
previewFeatures = ["postgresqlExtensions"]
|
||||
|
||||
output = "../libs/api/data-access-database/src/lib"
|
||||
fileName = "types.ts"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
|
|
@ -303,18 +311,27 @@ model WorkTimeStaging {
|
|||
endDate DateTime
|
||||
}
|
||||
|
||||
model EmployeeStaging {
|
||||
model EmployeeDyflexisStaging {
|
||||
internalId String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
|
||||
id Int
|
||||
firstname String
|
||||
surname String
|
||||
contractStart DateTime
|
||||
contractEnd DateTime
|
||||
contractHoursWeek Float
|
||||
contractSalaryHour Float
|
||||
personTransportCertificate DateTime?
|
||||
qualification String?
|
||||
id Int
|
||||
firstname String
|
||||
surname String
|
||||
contractStart DateTime
|
||||
contractEnd DateTime
|
||||
contractHoursWeek Float
|
||||
contractSalaryHour Float
|
||||
}
|
||||
|
||||
model EmployeeDispoLiveStaging {
|
||||
internalId String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
|
||||
id String
|
||||
name String?
|
||||
surname String?
|
||||
personnelNumber String? // Personal ID aus Dyflexis
|
||||
qualification String? // ID
|
||||
personTransportCertificate DateTime? // yyyy-MM-dd
|
||||
}
|
||||
|
||||
model Tour {
|
||||
|
|
@ -478,3 +495,114 @@ model PlannedTime {
|
|||
duration Int
|
||||
note String?
|
||||
}
|
||||
|
||||
model Employee {
|
||||
id String @id
|
||||
name String
|
||||
surname String
|
||||
personnelNumber String?
|
||||
mobile String?
|
||||
qualification String?
|
||||
occuptationalHealthExamination DateTime?
|
||||
personTransportCertificate DateTime?
|
||||
}
|
||||
|
||||
model DyflexisEmployee {
|
||||
id Int @id
|
||||
firstname String
|
||||
surname String
|
||||
contractStart DateTime
|
||||
contractEnd DateTime?
|
||||
contractHoursWeek Float
|
||||
contractSalaryHour Int
|
||||
}
|
||||
|
||||
enum CostTypeVariant {
|
||||
NACHTZUSCHLAG
|
||||
WOCHENENDZUSCHLAG
|
||||
FEIERTAGSZUSCHLAG
|
||||
DESINFEKTIONSZUSCHLAG
|
||||
KILOMETERZUSCHLAG
|
||||
FAHRZEUGZUSCHLAG
|
||||
TSWDIFFERENZIERUNG
|
||||
GRUNDPREIS
|
||||
}
|
||||
|
||||
model AvicennaTenant {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
healthInsuranceNumber String
|
||||
name String
|
||||
|
||||
tariffGroups TariffGroup[]
|
||||
}
|
||||
|
||||
model TariffGroup {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
description String
|
||||
ordinanceType String
|
||||
|
||||
tenants AvicennaTenant[]
|
||||
tariffs Tariff[]
|
||||
}
|
||||
|
||||
model Tariff {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
validFrom DateTime?
|
||||
validTo DateTime?
|
||||
|
||||
costTypes CostType[]
|
||||
tariffGroup TariffGroup? @relation(fields: [tariffGroupId], references: [id])
|
||||
tariffGroupId String? @db.Uuid
|
||||
}
|
||||
|
||||
model CostType {
|
||||
id String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
description String
|
||||
variant CostTypeVariant
|
||||
costAmount Int
|
||||
timeRange String?
|
||||
kmInclusive Int?
|
||||
excludeCarNames String[]
|
||||
infectionType String?
|
||||
|
||||
tariff Tariff? @relation(fields: [tariffId], references: [id])
|
||||
tariffId String? @db.Uuid
|
||||
}
|
||||
|
||||
model Trace {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
type String
|
||||
entityKey String
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model Anomaly {
|
||||
id String @id
|
||||
type String
|
||||
description String
|
||||
groupKey String
|
||||
groupDescription String
|
||||
workedOnBy String
|
||||
sleepTimer Int
|
||||
sleepSince DateTime?
|
||||
solution SolutionTarget[] @relation()
|
||||
resolvedAt DateTime?
|
||||
manuallyResolved Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model SolutionTarget {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
name String
|
||||
steps SolutionTargetStep[] @relation()
|
||||
anomaly Anomaly? @relation(fields: [anomalyId], references: [id])
|
||||
anomalyId String?
|
||||
}
|
||||
|
||||
model SolutionTargetStep {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
description String
|
||||
solutionTarget SolutionTarget? @relation(fields: [solutionTargetId], references: [id])
|
||||
solutionTargetId String? @db.Uuid
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,141 @@
|
|||
import { CostTypeVariant } from '@prisma/client';
|
||||
import { startOfDay, endOfDay } from 'date-fns';
|
||||
|
||||
export const BTW_AOK_TARIFFS = [
|
||||
{
|
||||
description: 'AOK/BKK/IKK/Knappschaft BTW',
|
||||
validFrom: startOfDay(new Date('2021-01-01')),
|
||||
validTo: endOfDay(new Date('2021-06-30')),
|
||||
costTypes: [
|
||||
{
|
||||
description: 'Grundpauschale je Fahrt',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 3400,
|
||||
},
|
||||
{
|
||||
description: 'ab dem 21. km',
|
||||
variant: CostTypeVariant.KILOMETERZUSCHLAG,
|
||||
costAmount: 135,
|
||||
kmInclusive: 20,
|
||||
},
|
||||
{
|
||||
description: 'Nachtzuschlag',
|
||||
variant: CostTypeVariant.NACHTZUSCHLAG,
|
||||
costAmount: 1665,
|
||||
timeRange: '19:00-07:00',
|
||||
},
|
||||
{
|
||||
description: 'Wochenendzuschlag',
|
||||
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
|
||||
costAmount: 1665,
|
||||
},
|
||||
{
|
||||
description: 'Feiertagszuschlag',
|
||||
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
|
||||
costAmount: 1665,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'AOK/BKK/IKK/Knappschaft BTW',
|
||||
validFrom: startOfDay(new Date('2021-07-01')),
|
||||
validTo: endOfDay(new Date('2022-08-31')),
|
||||
costTypes: [
|
||||
{
|
||||
description: 'Grundpauschale je Fahrt',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 3465,
|
||||
},
|
||||
{
|
||||
description: 'ab dem 21. km',
|
||||
variant: CostTypeVariant.KILOMETERZUSCHLAG,
|
||||
costAmount: 135,
|
||||
kmInclusive: 20,
|
||||
},
|
||||
{
|
||||
description: 'Nachtzuschlag',
|
||||
variant: CostTypeVariant.NACHTZUSCHLAG,
|
||||
costAmount: 1665,
|
||||
timeRange: '19:00-07:00',
|
||||
},
|
||||
{
|
||||
description: 'Wochenendzuschlag',
|
||||
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
|
||||
costAmount: 1665,
|
||||
},
|
||||
{
|
||||
description: 'Feiertagszuschlag',
|
||||
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
|
||||
costAmount: 1665,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'AOK/BKK/IKK/Knappschaft BTW',
|
||||
validFrom: startOfDay(new Date('2022-09-01')),
|
||||
validTo: endOfDay(new Date('2023-04-30')),
|
||||
costTypes: [
|
||||
{
|
||||
description: 'Grundpauschale je Fahrt',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 3700,
|
||||
},
|
||||
{
|
||||
description: 'ab dem 21. km',
|
||||
variant: CostTypeVariant.KILOMETERZUSCHLAG,
|
||||
costAmount: 160,
|
||||
kmInclusive: 20,
|
||||
},
|
||||
{
|
||||
description: 'Nachtzuschlag',
|
||||
variant: CostTypeVariant.NACHTZUSCHLAG,
|
||||
costAmount: 1765,
|
||||
timeRange: '18:00-07:30',
|
||||
},
|
||||
{
|
||||
description: 'Wochenendzuschlag',
|
||||
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
|
||||
costAmount: 1765,
|
||||
},
|
||||
{
|
||||
description: 'Feiertagszuschlag',
|
||||
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
|
||||
costAmount: 1765,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'AOK/BKK/IKK/Knappschaft BTW',
|
||||
validFrom: startOfDay(new Date('2023-05-01')),
|
||||
validTo: null,
|
||||
costTypes: [
|
||||
{
|
||||
description: 'Grundpauschale je Fahrt',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 3885,
|
||||
},
|
||||
{
|
||||
description: 'ab dem 21. km',
|
||||
variant: CostTypeVariant.KILOMETERZUSCHLAG,
|
||||
costAmount: 180,
|
||||
kmInclusive: 20,
|
||||
},
|
||||
{
|
||||
description: 'Nachtzuschlag',
|
||||
variant: CostTypeVariant.NACHTZUSCHLAG,
|
||||
costAmount: 1865,
|
||||
timeRange: '18:00-07:30',
|
||||
},
|
||||
{
|
||||
description: 'Wochenendzuschlag',
|
||||
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
|
||||
costAmount: 1865,
|
||||
},
|
||||
{
|
||||
description: 'Feiertagszuschlag',
|
||||
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
|
||||
costAmount: 1865,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
import { CostTypeVariant } from '@prisma/client';
|
||||
import { endOfDay, startOfDay } from 'date-fns';
|
||||
|
||||
export const BTW_DAK_VDEK_TARIFFS = [
|
||||
{
|
||||
description: 'DAK + VDEK BTW',
|
||||
validFrom: startOfDay(new Date('2021-01-01')),
|
||||
validTo: endOfDay(new Date('2023-05-31')),
|
||||
costTypes: [
|
||||
{
|
||||
description: 'Grundpauschale je Fahrt',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 3700,
|
||||
},
|
||||
{
|
||||
description: 'ab dem 21. km',
|
||||
variant: CostTypeVariant.KILOMETERZUSCHLAG,
|
||||
costAmount: 159,
|
||||
kmInclusive: 20,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'DAK + VDEK BTW',
|
||||
validFrom: startOfDay(new Date('2023-06-01')),
|
||||
validTo: null,
|
||||
costTypes: [
|
||||
{
|
||||
description: 'Grundpauschale je Fahrt',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 4171,
|
||||
},
|
||||
{
|
||||
description: 'ab dem 21. km',
|
||||
variant: CostTypeVariant.KILOMETERZUSCHLAG,
|
||||
costAmount: 166,
|
||||
kmInclusive: 21,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import { CostTypeVariant } from '@prisma/client';
|
||||
import { startOfDay } from 'date-fns';
|
||||
|
||||
export const BTW_PRIVAT_TARIFFS = [
|
||||
{
|
||||
description: 'Privat BTW',
|
||||
validFrom: startOfDay(new Date('2020-01-01')),
|
||||
validTo: null,
|
||||
costTypes: [
|
||||
{
|
||||
description: 'Grundpauschale je Fahrt',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 6500,
|
||||
},
|
||||
{
|
||||
description: 'QMS Zuschlag',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 2000,
|
||||
},
|
||||
{
|
||||
description: 'ab dem 16. km',
|
||||
variant: CostTypeVariant.KILOMETERZUSCHLAG,
|
||||
costAmount: 250,
|
||||
kmInclusive: 15,
|
||||
},
|
||||
{
|
||||
description: 'Nachtzuschlag',
|
||||
variant: CostTypeVariant.NACHTZUSCHLAG,
|
||||
costAmount: 4000,
|
||||
timeRange: '19:00-07:00',
|
||||
},
|
||||
{
|
||||
description: 'Wochenendzuschlag',
|
||||
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
|
||||
costAmount: 4000,
|
||||
},
|
||||
{
|
||||
description: 'Feiertagszuschlag',
|
||||
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
|
||||
costAmount: 4000,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
@ -0,0 +1,304 @@
|
|||
import { CostTypeVariant } from '@prisma/client';
|
||||
import { endOfDay, startOfDay } from 'date-fns';
|
||||
|
||||
export const KTW_AOK_TARIFFS = [
|
||||
{
|
||||
description: 'AOK/BKK/IKK/Knappschaft KTW',
|
||||
validFrom: startOfDay(new Date('2019-04-01')),
|
||||
validTo: endOfDay(new Date('2021-04-22')),
|
||||
costTypes: [
|
||||
{
|
||||
description: 'Grundpauschale je Fahrt',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 8936,
|
||||
},
|
||||
{
|
||||
description: 'Desinfektionszuschlag',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 70,
|
||||
},
|
||||
{
|
||||
description: 'ab dem 18. km',
|
||||
variant: CostTypeVariant.KILOMETERZUSCHLAG,
|
||||
costAmount: 286,
|
||||
kmInclusive: 18,
|
||||
},
|
||||
{
|
||||
description: 'Nachtzuschlag',
|
||||
variant: CostTypeVariant.NACHTZUSCHLAG,
|
||||
costAmount: 4003,
|
||||
timeRange: '19:00-07:00',
|
||||
},
|
||||
{
|
||||
description: 'Wochenendzuschlag',
|
||||
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
|
||||
costAmount: 4003,
|
||||
},
|
||||
{
|
||||
description: 'Feiertagszuschlag',
|
||||
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
|
||||
costAmount: 4003,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'AOK/BKK/IKK/Knappschaft KTW',
|
||||
validFrom: startOfDay(new Date('2021-04-23')),
|
||||
validTo: endOfDay(new Date('2021-06-30')),
|
||||
costTypes: [
|
||||
{
|
||||
description: 'Grundpauschale je Fahrt',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 8936,
|
||||
},
|
||||
{
|
||||
description: 'QMS Zuschlag',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 177,
|
||||
},
|
||||
{
|
||||
description: 'Desinfektionszuschlag',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 70,
|
||||
},
|
||||
{
|
||||
description: 'Nachtzuschlag',
|
||||
variant: CostTypeVariant.NACHTZUSCHLAG,
|
||||
costAmount: 4003,
|
||||
timeRange: '19:00-07:00',
|
||||
},
|
||||
{
|
||||
description: 'Wochenendzuschlag',
|
||||
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
|
||||
costAmount: 4003,
|
||||
},
|
||||
{
|
||||
description: 'Feiertagszuschlag',
|
||||
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
|
||||
costAmount: 4003,
|
||||
},
|
||||
{
|
||||
description: 'ab dem 18. km',
|
||||
variant: CostTypeVariant.KILOMETERZUSCHLAG,
|
||||
costAmount: 286,
|
||||
kmInclusive: 18,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'AOK/BKK/IKK/Knappschaft KTW',
|
||||
validFrom: startOfDay(new Date('2021-07-01')),
|
||||
validTo: endOfDay(new Date('2023-04-30')),
|
||||
costTypes: [
|
||||
{
|
||||
description: 'Grundpauschale je Fahrt',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 9162,
|
||||
},
|
||||
{
|
||||
description: 'Desinfektionszuschlag',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 72,
|
||||
},
|
||||
{
|
||||
description: 'QMS Zuschlag',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 181,
|
||||
},
|
||||
{
|
||||
description: 'Nachtzuschlag',
|
||||
variant: CostTypeVariant.NACHTZUSCHLAG,
|
||||
costAmount: 4104,
|
||||
timeRange: '19:00-07:00',
|
||||
},
|
||||
{
|
||||
description: 'Wochenendzuschlag',
|
||||
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
|
||||
costAmount: 4104,
|
||||
},
|
||||
{
|
||||
description: 'Feiertagszuschlag',
|
||||
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
|
||||
costAmount: 4104,
|
||||
},
|
||||
{
|
||||
description: 'ab dem 18. km',
|
||||
variant: CostTypeVariant.KILOMETERZUSCHLAG,
|
||||
costAmount: 293,
|
||||
kmInclusive: 18,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'AOK/BKK/IKK/Knappschaft KTW',
|
||||
validFrom: startOfDay(new Date('2023-05-01')),
|
||||
validTo: endOfDay(new Date('2023-12-31')),
|
||||
costTypes: [
|
||||
{
|
||||
description: 'Grundpauschale je Fahrt',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 7839,
|
||||
},
|
||||
{
|
||||
description: 'QMS Zuschlag',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 2006,
|
||||
},
|
||||
{
|
||||
description: 'Modernes Einsatzfahrzeug',
|
||||
variant: CostTypeVariant.FAHRZEUGZUSCHLAG,
|
||||
costAmount: 1121,
|
||||
excludeCarNames: [
|
||||
'1/KTW-01',
|
||||
'1/KTW-02',
|
||||
'1/KTW-03',
|
||||
'1/KTW-04',
|
||||
'1/KTW-05',
|
||||
'1/KTW-06',
|
||||
]
|
||||
},
|
||||
{
|
||||
description: 'Leitstellenmanagement',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 485,
|
||||
},
|
||||
{
|
||||
description: 'Ausbildungsbetrieb Rettungssanitäter',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 298,
|
||||
},
|
||||
{
|
||||
description: 'ab dem 17. km',
|
||||
variant: CostTypeVariant.KILOMETERZUSCHLAG,
|
||||
costAmount: 320,
|
||||
kmInclusive: 17,
|
||||
},
|
||||
{
|
||||
description: 'Nachtzuschlag',
|
||||
variant: CostTypeVariant.NACHTZUSCHLAG,
|
||||
costAmount: 6500,
|
||||
timeRange: '18:30-06:30',
|
||||
},
|
||||
{
|
||||
description: 'Wochenendzuschlag',
|
||||
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
|
||||
costAmount: 6500,
|
||||
},
|
||||
{
|
||||
description: 'Feiertagszuschlag',
|
||||
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
|
||||
costAmount: 6500,
|
||||
},
|
||||
{
|
||||
description: 'Desinfektionszuschlag 2.A',
|
||||
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
|
||||
costAmount: 3839,
|
||||
infectionType: '2.A',
|
||||
},
|
||||
{
|
||||
description: 'Desinfektionszuschlag 2.B',
|
||||
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
|
||||
costAmount: 15732,
|
||||
infectionType: '2.B',
|
||||
},
|
||||
{
|
||||
description: 'Desinfektionszuschlag 3.1',
|
||||
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
|
||||
costAmount: 18191,
|
||||
infectionType: '3.1',
|
||||
},
|
||||
{
|
||||
description: 'Desinfektionszuschlag 3.2',
|
||||
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
|
||||
costAmount: 18191,
|
||||
infectionType: '3.2',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'AOK/BKK/IKK/Knappschaft KTW',
|
||||
validFrom: startOfDay(new Date('2024-01-01')),
|
||||
validTo: null,
|
||||
costTypes: [
|
||||
{
|
||||
description: 'Grundpauschale je Fahrt',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 6921,
|
||||
},
|
||||
{
|
||||
description: 'QMS Zuschlag',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 2829,
|
||||
},
|
||||
{
|
||||
description: 'Modernes Einsatzfahrzeug',
|
||||
variant: CostTypeVariant.FAHRZEUGZUSCHLAG,
|
||||
costAmount: 1428,
|
||||
excludeCarNames: [
|
||||
'1/KTW-01',
|
||||
'1/KTW-02',
|
||||
'1/KTW-03',
|
||||
'1/KTW-04',
|
||||
'1/KTW-05-E',
|
||||
'1/KTW-06',
|
||||
]
|
||||
},
|
||||
{
|
||||
description: 'Leitstellenmanagement',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 665,
|
||||
},
|
||||
{
|
||||
description: 'Ausbildungsbetrieb Rettungssanitäter',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 305,
|
||||
},
|
||||
{
|
||||
description: 'ab dem 17. km',
|
||||
variant: CostTypeVariant.KILOMETERZUSCHLAG,
|
||||
costAmount: 359,
|
||||
kmInclusive: 17,
|
||||
},
|
||||
{
|
||||
description: 'Nachtzuschlag',
|
||||
variant: CostTypeVariant.NACHTZUSCHLAG,
|
||||
costAmount: 6919,
|
||||
timeRange: '18:30-06:30',
|
||||
},
|
||||
{
|
||||
description: 'Wochenendzuschlag',
|
||||
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
|
||||
costAmount: 6919,
|
||||
},
|
||||
{
|
||||
description: 'Feiertagszuschlag',
|
||||
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
|
||||
costAmount: 6919,
|
||||
},
|
||||
{
|
||||
description: 'Desinfektionszuschlag 2.A',
|
||||
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
|
||||
costAmount: 3935,
|
||||
infectionType: '2.A',
|
||||
},
|
||||
{
|
||||
description: 'Desinfektionszuschlag 2.B',
|
||||
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
|
||||
costAmount: 16126,
|
||||
infectionType: '2.B',
|
||||
},
|
||||
{
|
||||
description: 'Desinfektionszuschlag 3.1',
|
||||
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
|
||||
costAmount: 18646,
|
||||
infectionType: '3.1',
|
||||
},
|
||||
{
|
||||
description: 'Desinfektionszuschlag 3.2',
|
||||
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
|
||||
costAmount: 18646,
|
||||
infectionType: '3.2',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
@ -0,0 +1,300 @@
|
|||
import { CostTypeVariant } from '@prisma/client';
|
||||
import { startOfDay, endOfDay } from 'date-fns';
|
||||
|
||||
export const KTW_DAK_TARIFFS = [
|
||||
{
|
||||
description: 'DAK KTW',
|
||||
validFrom: startOfDay(new Date('2018-02-01')),
|
||||
validTo: endOfDay(new Date('2021-04-22')),
|
||||
costTypes: [
|
||||
{
|
||||
description: 'Grundpauschale je Fahrt',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 8507,
|
||||
},
|
||||
{
|
||||
description: 'ab dem 18. km',
|
||||
variant: CostTypeVariant.KILOMETERZUSCHLAG,
|
||||
costAmount: 284,
|
||||
kmInclusive: 17,
|
||||
},
|
||||
{
|
||||
description: 'Nachtzuschlag',
|
||||
variant: CostTypeVariant.NACHTZUSCHLAG,
|
||||
costAmount: 4728,
|
||||
timeRange: '18:30-06:30',
|
||||
},
|
||||
{
|
||||
description: 'Wochenendzuschlag',
|
||||
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
|
||||
costAmount: 4728,
|
||||
},
|
||||
{
|
||||
description: 'Feiertagszuschlag',
|
||||
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
|
||||
costAmount: 4728,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'DAK KTW',
|
||||
validFrom: startOfDay(new Date('2021-04-23')),
|
||||
validTo: endOfDay(new Date('2021-09-30')),
|
||||
costTypes: [
|
||||
{
|
||||
description: 'Grundpauschale je Fahrt',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 8507,
|
||||
},
|
||||
{
|
||||
description: 'QMS Zuschlag inkl. Modernes Einsatzfahrzeug',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 790,
|
||||
},
|
||||
{
|
||||
description: 'ab dem 18. km',
|
||||
variant: CostTypeVariant.KILOMETERZUSCHLAG,
|
||||
costAmount: 284,
|
||||
kmInclusive: 17,
|
||||
},
|
||||
{
|
||||
description: 'Nachtzuschlag',
|
||||
variant: CostTypeVariant.NACHTZUSCHLAG,
|
||||
costAmount: 4728,
|
||||
timeRange: '18:30-06:30',
|
||||
},
|
||||
{
|
||||
description: 'Wochenendzuschlag',
|
||||
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
|
||||
costAmount: 4728,
|
||||
},
|
||||
{
|
||||
description: 'Feiertagszuschlag',
|
||||
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
|
||||
costAmount: 4728,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'DAK KTW',
|
||||
validFrom: startOfDay(new Date('2021-10-01')),
|
||||
validTo: endOfDay(new Date('2022-12-31')),
|
||||
costTypes: [
|
||||
{
|
||||
description: 'Grundpauschale je Fahrt',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 7650,
|
||||
},
|
||||
{
|
||||
description: 'QMS Zuschlag',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 1380,
|
||||
},
|
||||
{
|
||||
description: 'Modernes Einsatzfahrzeug',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 750,
|
||||
},
|
||||
{
|
||||
description: 'ab dem 17. km',
|
||||
variant: CostTypeVariant.KILOMETERZUSCHLAG,
|
||||
costAmount: 300,
|
||||
kmInclusive: 16,
|
||||
},
|
||||
{
|
||||
description: 'Nachtzuschlag',
|
||||
variant: CostTypeVariant.NACHTZUSCHLAG,
|
||||
costAmount: 5300,
|
||||
timeRange: '18:30-06:30',
|
||||
},
|
||||
{
|
||||
description: 'Wochenendzuschlag',
|
||||
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
|
||||
costAmount: 5230,
|
||||
},
|
||||
{
|
||||
description: 'Feiertagszuschlag',
|
||||
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
|
||||
costAmount: 5230,
|
||||
},
|
||||
{
|
||||
description: 'Desinfektionszuschlag 2.A',
|
||||
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
|
||||
costAmount: 3590,
|
||||
infectionType: '2.A',
|
||||
},
|
||||
{
|
||||
description: 'Desinfektionszuschlag 2.B',
|
||||
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
|
||||
costAmount: 14350,
|
||||
infectionType: '2.B',
|
||||
},
|
||||
{
|
||||
description: 'Desinfektionszuschlag 3.1',
|
||||
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
|
||||
costAmount: 16500,
|
||||
infectionType: '3.1',
|
||||
},
|
||||
{
|
||||
description: 'Desinfektionszuschlag 3.2',
|
||||
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
|
||||
costAmount: 16500,
|
||||
infectionType: '3.2',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'DAK KTW',
|
||||
validFrom: startOfDay(new Date('2023-01-01')),
|
||||
validTo: endOfDay(new Date('2023-12-31')),
|
||||
costTypes: [
|
||||
{
|
||||
description: 'Grundpauschale je Fahrt',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 7920,
|
||||
},
|
||||
{
|
||||
description: 'QMS Zuschlag',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 1430,
|
||||
},
|
||||
{
|
||||
description: 'Modernes Einsatzfahrzeug',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 780,
|
||||
},
|
||||
{
|
||||
description: 'ab dem 17. km',
|
||||
variant: CostTypeVariant.KILOMETERZUSCHLAG,
|
||||
costAmount: 315,
|
||||
kmInclusive: 16,
|
||||
},
|
||||
{
|
||||
description: 'Nachtzuschlag',
|
||||
variant: CostTypeVariant.NACHTZUSCHLAG,
|
||||
costAmount: 5485,
|
||||
timeRange: '18:30-06:30',
|
||||
},
|
||||
{
|
||||
description: 'Wochenendzuschlag',
|
||||
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
|
||||
costAmount: 5410,
|
||||
},
|
||||
{
|
||||
description: 'Feiertagszuschlag',
|
||||
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
|
||||
costAmount: 5410,
|
||||
},
|
||||
{
|
||||
description: 'Desinfektionszuschlag 2.A',
|
||||
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
|
||||
costAmount: 3714,
|
||||
infectionType: '2.A',
|
||||
},
|
||||
{
|
||||
description: 'Desinfektionszuschlag 2.B',
|
||||
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
|
||||
costAmount: 14845,
|
||||
infectionType: '2.B',
|
||||
},
|
||||
{
|
||||
description: 'Desinfektionszuschlag 3.1',
|
||||
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
|
||||
costAmount: 17069,
|
||||
infectionType: '3.1',
|
||||
},
|
||||
{
|
||||
description: 'Desinfektionszuschlag 3.2',
|
||||
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
|
||||
costAmount: 17069,
|
||||
infectionType: '3.2',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'DAK KTW',
|
||||
validFrom: startOfDay(new Date('2024-01-01')),
|
||||
validTo: null,
|
||||
costTypes: [
|
||||
{
|
||||
description: 'Grundpauschale je Fahrt',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 9000,
|
||||
},
|
||||
{
|
||||
description: 'QMS Zuschlag',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 2000,
|
||||
},
|
||||
{
|
||||
description: 'Modernes Einsatzfahrzeug',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 1000,
|
||||
},
|
||||
{
|
||||
description: 'ab dem 17. km',
|
||||
variant: CostTypeVariant.KILOMETERZUSCHLAG,
|
||||
costAmount: 330,
|
||||
kmInclusive: 16,
|
||||
},
|
||||
{
|
||||
description: 'Nachtzuschlag',
|
||||
variant: CostTypeVariant.NACHTZUSCHLAG,
|
||||
costAmount: 5728,
|
||||
timeRange: '19:00-19:59',
|
||||
},
|
||||
{
|
||||
description: 'Nachtzuschlag',
|
||||
variant: CostTypeVariant.NACHTZUSCHLAG,
|
||||
costAmount: 6644,
|
||||
timeRange: '20:00-21:59',
|
||||
},
|
||||
{
|
||||
description: 'Nachtzuschlag',
|
||||
variant: CostTypeVariant.NACHTZUSCHLAG,
|
||||
costAmount: 7198,
|
||||
timeRange: '22:00-04:59',
|
||||
},
|
||||
{
|
||||
description: 'Nachtzuschlag',
|
||||
variant: CostTypeVariant.NACHTZUSCHLAG,
|
||||
costAmount: 5728,
|
||||
timeRange: '05:00-06:00',
|
||||
},
|
||||
{
|
||||
description: 'Wochenendzuschlag',
|
||||
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
|
||||
costAmount: 5410,
|
||||
},
|
||||
{
|
||||
description: 'Feiertagszuschlag',
|
||||
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
|
||||
costAmount: 5410,
|
||||
},
|
||||
{
|
||||
description: 'Desinfektionszuschlag 2.A',
|
||||
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
|
||||
costAmount: 3714,
|
||||
infectionType: '2.A',
|
||||
},
|
||||
{
|
||||
description: 'Desinfektionszuschlag 2.B',
|
||||
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
|
||||
costAmount: 14845,
|
||||
infectionType: '2.B',
|
||||
},
|
||||
{
|
||||
description: 'Desinfektionszuschlag 3.1',
|
||||
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
|
||||
costAmount: 17069,
|
||||
infectionType: '3.1',
|
||||
},
|
||||
{
|
||||
description: 'Desinfektionszuschlag 3.2',
|
||||
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
|
||||
costAmount: 17069,
|
||||
infectionType: '3.2',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
import { CostTypeVariant } from '@prisma/client';
|
||||
import { startOfDay } from 'date-fns';
|
||||
|
||||
export const KTW_PRIVAT_TARIFFS = [
|
||||
{
|
||||
description: 'Privat KTW',
|
||||
validFrom: startOfDay(new Date('2020-01-01')),
|
||||
validTo: null,
|
||||
costTypes: [
|
||||
{
|
||||
description: 'Grundpauschale je Fahrt',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 19000,
|
||||
},
|
||||
{
|
||||
description: 'QMS Zuschlag',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 2500,
|
||||
},
|
||||
{
|
||||
description: 'ab dem 16. km',
|
||||
variant: CostTypeVariant.KILOMETERZUSCHLAG,
|
||||
costAmount: 390,
|
||||
kmInclusive: 15,
|
||||
},
|
||||
{
|
||||
description: 'Nachtzuschlag',
|
||||
variant: CostTypeVariant.NACHTZUSCHLAG,
|
||||
costAmount: 8800,
|
||||
timeRange: '19:00-07:00',
|
||||
},
|
||||
{
|
||||
description: 'Wochenendzuschlag',
|
||||
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
|
||||
costAmount: 8800,
|
||||
},
|
||||
{
|
||||
description: 'Feiertagszuschlag',
|
||||
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
|
||||
costAmount: 8800,
|
||||
},
|
||||
{
|
||||
description: 'Desinfektionszuschlag 2.A',
|
||||
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
|
||||
costAmount: 8800,
|
||||
infectionType: '2.A',
|
||||
},
|
||||
{
|
||||
description: 'Desinfektionszuschlag 2.B',
|
||||
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
|
||||
costAmount: 8800,
|
||||
infectionType: '2.B',
|
||||
},
|
||||
{
|
||||
description: 'Desinfektionszuschlag 3.1',
|
||||
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
|
||||
costAmount: 17500,
|
||||
infectionType: '3.1',
|
||||
},
|
||||
{
|
||||
description: 'Desinfektionszuschlag 3.2',
|
||||
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
|
||||
costAmount: 17500,
|
||||
infectionType: '3.2',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
@ -0,0 +1,281 @@
|
|||
import { CostTypeVariant } from '@prisma/client';
|
||||
import { endOfDay, startOfDay } from 'date-fns';
|
||||
|
||||
export const KTW_VDEK_TARIFFS = [
|
||||
{
|
||||
description: 'VDEK KTW',
|
||||
validFrom: startOfDay(new Date('2017-09-01')),
|
||||
validTo: endOfDay(new Date('2021-04-22')),
|
||||
costTypes: [
|
||||
{
|
||||
description: 'Grundpauschale je Fahrt',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 8150,
|
||||
},
|
||||
{
|
||||
description: 'ab dem 18. km',
|
||||
variant: CostTypeVariant.KILOMETERZUSCHLAG,
|
||||
costAmount: 273,
|
||||
kmInclusive: 17,
|
||||
},
|
||||
{
|
||||
description: 'Nachtzuschlag',
|
||||
variant: CostTypeVariant.NACHTZUSCHLAG,
|
||||
costAmount: 4550,
|
||||
timeRange: '18:30-06:30',
|
||||
},
|
||||
{
|
||||
description: 'Wochenendzuschlag',
|
||||
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
|
||||
costAmount: 4550,
|
||||
},
|
||||
{
|
||||
description: 'Feiertagszuschlag',
|
||||
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
|
||||
costAmount: 4550,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'VDEK KTW',
|
||||
validFrom: startOfDay(new Date('2021-04-23')),
|
||||
validTo: endOfDay(new Date('2021-06-30')),
|
||||
costTypes: [
|
||||
{
|
||||
description: 'Grundpauschale je Fahrt',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 8150,
|
||||
},
|
||||
{
|
||||
description: 'QMS Zuschlag',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 950,
|
||||
},
|
||||
{
|
||||
description: 'ab dem 18. km',
|
||||
variant: CostTypeVariant.KILOMETERZUSCHLAG,
|
||||
costAmount: 273,
|
||||
kmInclusive: 17,
|
||||
},
|
||||
{
|
||||
description: 'Nachtzuschlag',
|
||||
variant: CostTypeVariant.NACHTZUSCHLAG,
|
||||
costAmount: 4550,
|
||||
timeRange: '18:30-06:30',
|
||||
},
|
||||
{
|
||||
description: 'Wochenendzuschlag',
|
||||
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
|
||||
costAmount: 4550,
|
||||
},
|
||||
{
|
||||
description: 'Feiertagszuschlag',
|
||||
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
|
||||
costAmount: 4550,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'VDEK KTW',
|
||||
validFrom: startOfDay(new Date('2021-07-01')),
|
||||
validTo: endOfDay(new Date('2021-12-31')),
|
||||
costTypes: [
|
||||
{
|
||||
description: 'Grundpauschale je Fahrt',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 8977,
|
||||
},
|
||||
{
|
||||
description: 'QMS Zuschlag',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 1037,
|
||||
},
|
||||
{
|
||||
description: 'ab dem 18. km',
|
||||
variant: CostTypeVariant.KILOMETERZUSCHLAG,
|
||||
costAmount: 295,
|
||||
kmInclusive: 17,
|
||||
},
|
||||
{
|
||||
description: 'Nachtzuschlag',
|
||||
variant: CostTypeVariant.NACHTZUSCHLAG,
|
||||
costAmount: 5028,
|
||||
timeRange: '18:30-06:30',
|
||||
},
|
||||
{
|
||||
description: 'Wochenendzuschlag',
|
||||
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
|
||||
costAmount: 5028,
|
||||
},
|
||||
{
|
||||
description: 'Feiertagszuschlag',
|
||||
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
|
||||
costAmount: 5028,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'VDEK KTW',
|
||||
validFrom: startOfDay(new Date('2022-01-01')),
|
||||
validTo: endOfDay(new Date('2023-04-30')),
|
||||
costTypes: [
|
||||
{
|
||||
description: 'Grundpauschale je Fahrt',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 7043,
|
||||
},
|
||||
{
|
||||
description: 'QMS Zuschlag',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 2790,
|
||||
},
|
||||
{
|
||||
description: 'Modernes Einsatzfahrzeug',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 1393,
|
||||
},
|
||||
{
|
||||
description: 'Leitstellenmanagement',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 649,
|
||||
},
|
||||
{
|
||||
description: 'ab dem 17. km',
|
||||
variant: CostTypeVariant.KILOMETERZUSCHLAG,
|
||||
costAmount: 317,
|
||||
kmInclusive: 16,
|
||||
},
|
||||
{
|
||||
description: 'Nachtzuschlag',
|
||||
variant: CostTypeVariant.NACHTZUSCHLAG,
|
||||
costAmount: 5537,
|
||||
timeRange: '18:30-06:30',
|
||||
},
|
||||
{
|
||||
description: 'Wochenendzuschlag',
|
||||
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
|
||||
costAmount: 6212,
|
||||
},
|
||||
{
|
||||
description: 'Feiertagszuschlag',
|
||||
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
|
||||
costAmount: 6212,
|
||||
},
|
||||
{
|
||||
description: 'Desinfektionszuschlag 2.A',
|
||||
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
|
||||
costAmount: 4134,
|
||||
infectionType: '2.A',
|
||||
},
|
||||
{
|
||||
description: 'Desinfektionszuschlag 2.B',
|
||||
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
|
||||
costAmount: 16941,
|
||||
infectionType: '2.B',
|
||||
},
|
||||
{
|
||||
description: 'Desinfektionszuschlag 3.1',
|
||||
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
|
||||
costAmount: 19590,
|
||||
infectionType: '3.1',
|
||||
},
|
||||
{
|
||||
description: 'Desinfektionszuschlag 3.2',
|
||||
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
|
||||
costAmount: 19590,
|
||||
infectionType: '3.2',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'VDEK KTW',
|
||||
validFrom: startOfDay(new Date('2023-05-01')),
|
||||
validTo: null,
|
||||
costTypes: [
|
||||
{
|
||||
description: 'Grundpauschale je Fahrt',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 7325,
|
||||
},
|
||||
{
|
||||
description: 'QMS Zuschlag',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 2886,
|
||||
},
|
||||
{
|
||||
description: 'Modernes Einsatzfahrzeug',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 1450,
|
||||
},
|
||||
{
|
||||
description: 'Leitstellenmanagement',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 671,
|
||||
},
|
||||
{
|
||||
description: 'ab dem 17. km',
|
||||
variant: CostTypeVariant.KILOMETERZUSCHLAG,
|
||||
costAmount: 349,
|
||||
kmInclusive: 16,
|
||||
},
|
||||
{
|
||||
description: 'Nachtzuschlag',
|
||||
variant: CostTypeVariant.NACHTZUSCHLAG,
|
||||
costAmount: 5728,
|
||||
timeRange: '19:00-19:59',
|
||||
},
|
||||
{
|
||||
description: 'Nachtzuschlag',
|
||||
variant: CostTypeVariant.NACHTZUSCHLAG,
|
||||
costAmount: 6644,
|
||||
timeRange: '20:00-21:59',
|
||||
},
|
||||
{
|
||||
description: 'Nachtzuschlag',
|
||||
variant: CostTypeVariant.NACHTZUSCHLAG,
|
||||
costAmount: 7198,
|
||||
timeRange: '22:00-04:59',
|
||||
},
|
||||
{
|
||||
description: 'Nachtzuschlag',
|
||||
variant: CostTypeVariant.NACHTZUSCHLAG,
|
||||
costAmount: 5728,
|
||||
timeRange: '05:00-06:00',
|
||||
},
|
||||
{
|
||||
description: 'Wochenendzuschlag',
|
||||
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
|
||||
costAmount: 6426,
|
||||
},
|
||||
{
|
||||
description: 'Feiertagszuschlag',
|
||||
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
|
||||
costAmount: 6426,
|
||||
},
|
||||
{
|
||||
description: 'Desinfektionszuschlag 2.A',
|
||||
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
|
||||
costAmount: 4277,
|
||||
infectionType: '2.A',
|
||||
},
|
||||
{
|
||||
description: 'Desinfektionszuschlag 2.B',
|
||||
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
|
||||
costAmount: 17525,
|
||||
infectionType: '2.B',
|
||||
},
|
||||
{
|
||||
description: 'Desinfektionszuschlag 3.1',
|
||||
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
|
||||
costAmount: 20266,
|
||||
infectionType: '3.1',
|
||||
},
|
||||
{
|
||||
description: 'Desinfektionszuschlag 3.2',
|
||||
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
|
||||
costAmount: 20266,
|
||||
infectionType: '3.2',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
import { CostTypeVariant } from '@prisma/client';
|
||||
import { startOfDay, endOfDay } from 'date-fns';
|
||||
|
||||
export const TSW_AOK_TARIFFS = [
|
||||
{
|
||||
description: 'AOK/BKK/IKK/Knappschaft TSW',
|
||||
validFrom: startOfDay(new Date('2021-01-01')),
|
||||
validTo: endOfDay(new Date('2021-06-30')),
|
||||
costTypes: [
|
||||
{
|
||||
description: 'Grundpauschale je Fahrt',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 5000,
|
||||
},
|
||||
{
|
||||
description: 'ab dem 21. km',
|
||||
variant: CostTypeVariant.KILOMETERZUSCHLAG,
|
||||
costAmount: 170,
|
||||
kmInclusive: 20,
|
||||
},
|
||||
{
|
||||
description: 'Nachtzuschlag',
|
||||
variant: CostTypeVariant.NACHTZUSCHLAG,
|
||||
costAmount: 2445,
|
||||
timeRange: '19:00-07:00',
|
||||
},
|
||||
{
|
||||
description: 'Wochenendzuschlag',
|
||||
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
|
||||
costAmount: 2445,
|
||||
},
|
||||
{
|
||||
description: 'Feiertagszuschlag',
|
||||
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
|
||||
costAmount: 2445,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'AOK/BKK/IKK/Knappschaft TSW',
|
||||
validFrom: startOfDay(new Date('2021-07-01')),
|
||||
validTo: endOfDay(new Date('2022-08-31')),
|
||||
costTypes: [
|
||||
{
|
||||
description: 'Grundpauschale je Fahrt',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 5100,
|
||||
},
|
||||
{
|
||||
description: 'ab dem 21. km',
|
||||
variant: CostTypeVariant.KILOMETERZUSCHLAG,
|
||||
costAmount: 170,
|
||||
kmInclusive: 20,
|
||||
},
|
||||
{
|
||||
description: 'Nachtzuschlag',
|
||||
variant: CostTypeVariant.NACHTZUSCHLAG,
|
||||
costAmount: 2445,
|
||||
timeRange: '19:00-07:00',
|
||||
},
|
||||
{
|
||||
description: 'Wochenendzuschlag',
|
||||
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
|
||||
costAmount: 2445,
|
||||
},
|
||||
{
|
||||
description: 'Feiertagszuschlag',
|
||||
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
|
||||
costAmount: 2445,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'AOK/BKK/IKK/Knappschaft TSW',
|
||||
validFrom: startOfDay(new Date('2022-09-01')),
|
||||
validTo: endOfDay(new Date('2023-04-30')),
|
||||
costTypes: [
|
||||
{
|
||||
description: 'Grundpauschale je Fahrt',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 5450,
|
||||
},
|
||||
{
|
||||
description: 'ab dem 21. km',
|
||||
variant: CostTypeVariant.KILOMETERZUSCHLAG,
|
||||
costAmount: 180,
|
||||
kmInclusive: 20,
|
||||
},
|
||||
{
|
||||
description: 'Nachtzuschlag',
|
||||
variant: CostTypeVariant.NACHTZUSCHLAG,
|
||||
costAmount: 2600,
|
||||
timeRange: '18:00-07:30',
|
||||
},
|
||||
{
|
||||
description: 'Wochenendzuschlag',
|
||||
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
|
||||
costAmount: 2600,
|
||||
},
|
||||
{
|
||||
description: 'Feiertagszuschlag',
|
||||
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
|
||||
costAmount: 2600,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'AOK/BKK/IKK/Knappschaft TSW',
|
||||
validFrom: startOfDay(new Date('2023-05-01')),
|
||||
validTo: null,
|
||||
costTypes: [
|
||||
{
|
||||
description: 'Grundpauschale je Fahrt',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 5800,
|
||||
},
|
||||
{
|
||||
description: 'ab dem 21. km',
|
||||
variant: CostTypeVariant.KILOMETERZUSCHLAG,
|
||||
costAmount: 190,
|
||||
kmInclusive: 20,
|
||||
},
|
||||
{
|
||||
description: 'Nachtzuschlag',
|
||||
variant: CostTypeVariant.NACHTZUSCHLAG,
|
||||
costAmount: 2700,
|
||||
timeRange: '18:00-07:30',
|
||||
},
|
||||
{
|
||||
description: 'Wochenendzuschlag',
|
||||
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
|
||||
costAmount: 2700,
|
||||
},
|
||||
{
|
||||
description: 'Feiertagszuschlag',
|
||||
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
|
||||
costAmount: 2700,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
import { CostTypeVariant } from '@prisma/client';
|
||||
import { endOfDay, startOfDay } from 'date-fns';
|
||||
|
||||
export const TSW_DAK_VDEK_TARIFFS = [
|
||||
{
|
||||
description: 'DAK + VDEK TSW',
|
||||
validFrom: startOfDay(new Date('2021-01-01')),
|
||||
validTo: endOfDay(new Date('2023-05-31')),
|
||||
costTypes: [
|
||||
{
|
||||
description: 'Grundpauschale je Fahrt',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 4922,
|
||||
},
|
||||
{
|
||||
description: 'ab dem 22. km',
|
||||
variant: CostTypeVariant.KILOMETERZUSCHLAG,
|
||||
costAmount: 159,
|
||||
kmInclusive: 21,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'DAK + VDEK TSW',
|
||||
validFrom: startOfDay(new Date('2023-06-01')),
|
||||
validTo: null,
|
||||
costTypes: [
|
||||
{
|
||||
description: 'Grundpauschale je Fahrt (TSW 2)',
|
||||
variant: CostTypeVariant.TSWDIFFERENZIERUNG,
|
||||
costAmount: 5880,
|
||||
},
|
||||
{
|
||||
description: 'Grundpauschale je Fahrt (TSW 1)',
|
||||
variant: CostTypeVariant.TSWDIFFERENZIERUNG,
|
||||
costAmount: 4380,
|
||||
},
|
||||
{
|
||||
description: 'Nachtzuschlag',
|
||||
variant: CostTypeVariant.NACHTZUSCHLAG,
|
||||
costAmount: 407,
|
||||
timeRange: '18:30-20:00',
|
||||
},
|
||||
{
|
||||
description: 'Nachtzuschlag',
|
||||
variant: CostTypeVariant.NACHTZUSCHLAG,
|
||||
costAmount: 407,
|
||||
timeRange: '06:00-07:00',
|
||||
},
|
||||
{
|
||||
description: 'ab dem 22. km',
|
||||
variant: CostTypeVariant.KILOMETERZUSCHLAG,
|
||||
costAmount: 159,
|
||||
kmInclusive: 21,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import { CostTypeVariant } from '@prisma/client';
|
||||
import { startOfDay } from 'date-fns';
|
||||
|
||||
export const TSW_PRIVAT_TARIFFS = [
|
||||
{
|
||||
description: 'Privat TSW',
|
||||
validFrom: startOfDay(new Date('2020-01-01')),
|
||||
validTo: null,
|
||||
costTypes: [
|
||||
{
|
||||
description: 'Grundpauschale je Fahrt',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 11500,
|
||||
},
|
||||
{
|
||||
description: 'QMS Zuschlag',
|
||||
variant: CostTypeVariant.GRUNDPREIS,
|
||||
costAmount: 2000,
|
||||
},
|
||||
{
|
||||
description: 'ab dem 16. km',
|
||||
variant: CostTypeVariant.KILOMETERZUSCHLAG,
|
||||
costAmount: 250,
|
||||
kmInclusive: 15,
|
||||
},
|
||||
{
|
||||
description: 'Nachtzuschlag',
|
||||
variant: CostTypeVariant.NACHTZUSCHLAG,
|
||||
costAmount: 5500,
|
||||
timeRange: '19:00-07:00',
|
||||
},
|
||||
{
|
||||
description: 'Wochenendzuschlag',
|
||||
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
|
||||
costAmount: 5500,
|
||||
},
|
||||
{
|
||||
description: 'Feiertagszuschlag',
|
||||
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
|
||||
costAmount: 5500,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
@ -6,14 +6,30 @@ import { AppService } from './app.service';
|
|||
import { PrismaModule } from './core/database/prisma.module';
|
||||
import { FeatureImporterModule } from './modules/feat-importers/feature-importer.module';
|
||||
import { ConfigModule } from './core/config/config.module';
|
||||
import { TicketSystemModule } from './modules/feat-tickets/ticket-system.module';
|
||||
import { DatabaseModule } from './core/database/database.module';
|
||||
import { PostgresDialect } from 'kysely';
|
||||
import { Pool } from 'pg';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule,
|
||||
DatabaseModule.forRoot({
|
||||
dialect: new PostgresDialect({
|
||||
pool: new Pool({
|
||||
host: 'localhost',
|
||||
port: 5432,
|
||||
database: 'avicenna',
|
||||
user: 'johndoe',
|
||||
password: 'randompassword',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
PrismaModule.forRoot(),
|
||||
ScheduleModule.forRoot(),
|
||||
EventEmitterModule.forRoot(),
|
||||
FeatureImporterModule,
|
||||
TicketSystemModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
// import { TicketService } from '@dks/avicenna/api-feature-ticket-system';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Cron, SchedulerRegistry } from '@nestjs/schedule';
|
||||
import { SchedulerRegistry } from '@nestjs/schedule';
|
||||
import {
|
||||
Interval,
|
||||
Interval as TimeInterval,
|
||||
addDays,
|
||||
eachDayOfInterval,
|
||||
endOfDay,
|
||||
|
|
@ -10,15 +9,16 @@ import {
|
|||
set,
|
||||
startOfDay,
|
||||
} from 'date-fns';
|
||||
import { concatMap, from, lastValueFrom, take, tap } from 'rxjs';
|
||||
import { concatMap, from, lastValueFrom, switchMap, take, tap } from 'rxjs';
|
||||
import { ImporterService } from './modules/feat-importers/importer.service';
|
||||
import { TicketService } from './modules/feat-tickets/tickets.service';
|
||||
import { chunk } from './utils';
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
private readonly logger = new Logger(AppService.name);
|
||||
|
||||
@Cron('0 */5 2-20 * * *')
|
||||
// @Cron('0 */5 2-20 * * *')
|
||||
async fetchDataContinously() {
|
||||
const now = new Date();
|
||||
const fetchInterval = {
|
||||
|
|
@ -34,7 +34,7 @@ export class AppService {
|
|||
`import took ${(Date.now() - now.getTime()) / 1000}s`,
|
||||
),
|
||||
),
|
||||
// switchMap(() => from(this.ticketService.createTickets(fetchInterval))),
|
||||
switchMap(() => from(this.ticketService.createTickets(fetchInterval))),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
|
@ -46,7 +46,7 @@ export class AppService {
|
|||
if (timeouts.length) return;
|
||||
|
||||
const now = new Date();
|
||||
const fetchInterval: Interval = {
|
||||
const fetchInterval: TimeInterval = {
|
||||
start: set(new Date(), {
|
||||
year: now.getFullYear(),
|
||||
month: 0,
|
||||
|
|
@ -70,7 +70,7 @@ export class AppService {
|
|||
from([interval])
|
||||
.pipe(
|
||||
concatMap((interval) => this.importer.runImport(interval)),
|
||||
// switchMap(() => from(this.ticketService.createTickets(interval))),
|
||||
switchMap(() => from(this.ticketService.createTickets(interval))),
|
||||
)
|
||||
.subscribe();
|
||||
};
|
||||
|
|
@ -81,7 +81,7 @@ export class AppService {
|
|||
// @Cron('0 2,7,12,17,22,27,32,37,42,47,52,57 2-20 * * *')
|
||||
async fetchDataSixWeeks() {
|
||||
const now = new Date();
|
||||
const fetchInterval: Interval = {
|
||||
const fetchInterval: TimeInterval = {
|
||||
start: startOfDay(addDays(now, -90)),
|
||||
end: endOfDay(now),
|
||||
};
|
||||
|
|
@ -111,7 +111,7 @@ export class AppService {
|
|||
// @Cron('* * 0 * * *')
|
||||
async fetchDataYearly() {
|
||||
const now = new Date();
|
||||
const fetchInterval: Interval = {
|
||||
const fetchInterval: TimeInterval = {
|
||||
start: set(new Date(), {
|
||||
year: now.getFullYear(),
|
||||
month: 0,
|
||||
|
|
@ -137,21 +137,21 @@ export class AppService {
|
|||
),
|
||||
),
|
||||
),
|
||||
// switchMap(() => from(this.ticketService.createTickets(fetchInterval))),
|
||||
switchMap(() => from(this.ticketService.createTickets(fetchInterval))),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly importer: ImporterService,
|
||||
// private readonly ticketService: TicketService,
|
||||
private readonly ticketService: TicketService,
|
||||
private readonly schedulerRegistry: SchedulerRegistry,
|
||||
) {}
|
||||
|
||||
private splitIntervalIntoPeriods(
|
||||
interval: Interval,
|
||||
interval: TimeInterval,
|
||||
chunkSize = 30,
|
||||
): Interval[] {
|
||||
): TimeInterval[] {
|
||||
return chunk(eachDayOfInterval(interval), chunkSize).map((dates) => ({
|
||||
start: startOfDay(dates.at(0) as Date),
|
||||
end: endOfDay(dates.at(-1) as Date),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
export enum AttendanceRegistrationEvents {
|
||||
ClockedIn = 'clocked in',
|
||||
ClockedOut = 'clocked out',
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export enum TraceTypes {
|
||||
AnomalyAccepted = 'anomaly.accepted',
|
||||
AnomalyIgnored = 'anomaly.ignored',
|
||||
TourWarning = 'tour.warning',
|
||||
TourCritical = 'tour.critical',
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { DynamicModule, Global, Module, Provider } from '@nestjs/common';
|
||||
import { KyselyConfig } from 'kysely';
|
||||
import { createKyselyProvider } from './kysely-client.factory';
|
||||
|
||||
export const KYSELY_CLIENT_PROVIDER_TOKEN = 'KyselyClientProviderToken';
|
||||
|
||||
@Global()
|
||||
@Module({})
|
||||
export class DatabaseModule {
|
||||
public static forRoot(config: KyselyConfig): DynamicModule {
|
||||
const provider: Provider = createKyselyProvider(config);
|
||||
|
||||
return {
|
||||
module: DatabaseModule,
|
||||
exports: [provider],
|
||||
providers: [provider],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { Provider } from '@nestjs/common';
|
||||
import { Kysely, KyselyConfig } from 'kysely';
|
||||
import { KYSELY_CLIENT_PROVIDER_TOKEN } from './database.module';
|
||||
|
||||
export const createKyselyClient = <DB>(options: KyselyConfig): Kysely<DB> => {
|
||||
return new Kysely<DB>(options);
|
||||
};
|
||||
|
||||
export const createKyselyProvider = (options: KyselyConfig): Provider => ({
|
||||
provide: KYSELY_CLIENT_PROVIDER_TOKEN,
|
||||
useValue: createKyselyClient(options),
|
||||
});
|
||||
|
|
@ -0,0 +1,530 @@
|
|||
import type { ColumnType } from 'kysely';
|
||||
export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>
|
||||
? ColumnType<S, I | undefined, U>
|
||||
: ColumnType<T, T | undefined, T>;
|
||||
export type Timestamp = ColumnType<Date, Date | string, Date | string>;
|
||||
|
||||
export const CostTypeVariant = {
|
||||
NACHTZUSCHLAG: 'NACHTZUSCHLAG',
|
||||
WOCHENENDZUSCHLAG: 'WOCHENENDZUSCHLAG',
|
||||
FEIERTAGSZUSCHLAG: 'FEIERTAGSZUSCHLAG',
|
||||
DESINFEKTIONSZUSCHLAG: 'DESINFEKTIONSZUSCHLAG',
|
||||
KILOMETERZUSCHLAG: 'KILOMETERZUSCHLAG',
|
||||
FAHRZEUGZUSCHLAG: 'FAHRZEUGZUSCHLAG',
|
||||
TSWDIFFERENZIERUNG: 'TSWDIFFERENZIERUNG',
|
||||
GRUNDPREIS: 'GRUNDPREIS',
|
||||
} as const;
|
||||
export type CostTypeVariant =
|
||||
(typeof CostTypeVariant)[keyof typeof CostTypeVariant];
|
||||
export const TicketState = {
|
||||
CREATED: 'CREATED',
|
||||
APPROVAL_REQUIRED: 'APPROVAL_REQUIRED',
|
||||
READY: 'READY',
|
||||
DONE: 'DONE',
|
||||
BILLABLE: 'BILLABLE',
|
||||
ARCHIVED: 'ARCHIVED',
|
||||
} as const;
|
||||
export type TicketState = (typeof TicketState)[keyof typeof TicketState];
|
||||
export const TicketValidationState = {
|
||||
CREATED: 'CREATED',
|
||||
USER_INTERACTION_REQUIRED: 'USER_INTERACTION_REQUIRED',
|
||||
TD_UNCERTAIN: 'TD_UNCERTAIN',
|
||||
DOCUMENTS_MISSING: 'DOCUMENTS_MISSING',
|
||||
ARCHIVED: 'ARCHIVED',
|
||||
} as const;
|
||||
export type TicketValidationState =
|
||||
(typeof TicketValidationState)[keyof typeof TicketValidationState];
|
||||
export const ApprovalState = {
|
||||
UNKNOWN: 'UNKNOWN',
|
||||
FREE: 'FREE',
|
||||
REQUIRED: 'REQUIRED',
|
||||
REQUESTED: 'REQUESTED',
|
||||
APPROVED: 'APPROVED',
|
||||
} as const;
|
||||
export type ApprovalState = (typeof ApprovalState)[keyof typeof ApprovalState];
|
||||
export type Anomaly = {
|
||||
id: string;
|
||||
type: string;
|
||||
description: string;
|
||||
groupKey: string;
|
||||
groupDescription: string;
|
||||
workedOnBy: string;
|
||||
sleepTimer: number;
|
||||
sleepSince: Timestamp | null;
|
||||
resolvedAt: Timestamp | null;
|
||||
manuallyResolved: Generated<boolean>;
|
||||
createdAt: Generated<Timestamp>;
|
||||
};
|
||||
export type AttendanceRegistration = {
|
||||
attendanceRegistrationId: number;
|
||||
employeeId: number;
|
||||
personnelNumber: string;
|
||||
dateTime: Timestamp;
|
||||
event: string;
|
||||
};
|
||||
export type AttendanceRegistrationSource = {
|
||||
internalId: Generated<string>;
|
||||
insertedAt: Generated<Timestamp>;
|
||||
jobId: string;
|
||||
attendanceRegistrationId: string | null;
|
||||
employeeId: string | null;
|
||||
personnelNumber: string | null;
|
||||
dateTime: string | null;
|
||||
event: string | null;
|
||||
};
|
||||
export type AttendanceRegistrationStaging = {
|
||||
internalId: Generated<string>;
|
||||
attendanceRegistrationId: number;
|
||||
employeeId: number;
|
||||
dateTime: Timestamp;
|
||||
event: string;
|
||||
};
|
||||
export type AvicennaTenant = {
|
||||
id: Generated<string>;
|
||||
healthInsuranceNumber: string;
|
||||
name: string;
|
||||
};
|
||||
export type AvicennaTenantToTariffGroup = {
|
||||
A: string;
|
||||
B: string;
|
||||
};
|
||||
export type CostType = {
|
||||
id: Generated<string>;
|
||||
description: string;
|
||||
variant: CostTypeVariant;
|
||||
costAmount: number;
|
||||
timeRange: string | null;
|
||||
kmInclusive: number | null;
|
||||
excludeCarNames: string[];
|
||||
infectionType: string | null;
|
||||
tariffId: string | null;
|
||||
};
|
||||
export type DyflexisEmployee = {
|
||||
id: Generated<string>;
|
||||
userId: string;
|
||||
firstname: string;
|
||||
surname: string;
|
||||
contractStart: string;
|
||||
contractEnd: string | null;
|
||||
contractHoursWeek: number;
|
||||
contractSalaryHour: number;
|
||||
};
|
||||
export type Employee = {
|
||||
id: string;
|
||||
name: string;
|
||||
surname: string;
|
||||
personnelNumber: string | null;
|
||||
mobile: string | null;
|
||||
qualification: string | null;
|
||||
occuptationalHealthExamination: Timestamp | null;
|
||||
personTransportCertificate: Timestamp | null;
|
||||
};
|
||||
export type EmployeeDispoLiveSource = {
|
||||
internalId: Generated<string>;
|
||||
insertedAt: Generated<Timestamp>;
|
||||
jobId: string;
|
||||
id: string | null;
|
||||
pScheinAblaufDatum: string | null;
|
||||
name: string | null;
|
||||
surname: string | null;
|
||||
persId: string | null;
|
||||
importPersId: string | null;
|
||||
qualifikation: string | null;
|
||||
};
|
||||
export type EmployeeDyflexisSource = {
|
||||
internalId: Generated<string>;
|
||||
insertedAt: Generated<Timestamp>;
|
||||
jobId: string;
|
||||
id: string | null;
|
||||
firstname: string | null;
|
||||
surname: string | null;
|
||||
contract_id: string | null;
|
||||
contract_start: string | null;
|
||||
contract_end: string | null;
|
||||
contract_hours_week: string | null;
|
||||
contract_salary_hour: string | null;
|
||||
};
|
||||
export type EmployeeStaging = {
|
||||
internalId: Generated<string>;
|
||||
id: number;
|
||||
firstname: string;
|
||||
surname: string;
|
||||
contractStart: Timestamp;
|
||||
contractEnd: Timestamp;
|
||||
contractHoursWeek: number;
|
||||
contractSalaryHour: number;
|
||||
personTransportCertificate: Timestamp | null;
|
||||
qualification: string | null;
|
||||
};
|
||||
export type Patient = {
|
||||
id: string;
|
||||
surname: string | null;
|
||||
name: string | null;
|
||||
street: string | null;
|
||||
zip: string | null;
|
||||
city: string | null;
|
||||
birthday: Timestamp | null;
|
||||
healthinsuranceId: string | null;
|
||||
healthinsurance: string | null;
|
||||
careDegree: string | null;
|
||||
disabilityMark1: string | null;
|
||||
disabilityMark2: string | null;
|
||||
};
|
||||
export type PatientSource = {
|
||||
internalId: Generated<string>;
|
||||
insertedAt: Generated<Timestamp>;
|
||||
jobId: string;
|
||||
id: string | null;
|
||||
surname: string | null;
|
||||
name: string | null;
|
||||
street: string | null;
|
||||
zip: string | null;
|
||||
city: string | null;
|
||||
birthday: string | null;
|
||||
kkId: string | null;
|
||||
krankenkasse: string | null;
|
||||
pflegestufe: string | null;
|
||||
behinderung1: string | null;
|
||||
behinderung2: string | null;
|
||||
};
|
||||
export type PatientStaging = {
|
||||
internalId: Generated<string>;
|
||||
id: string | null;
|
||||
surname: string | null;
|
||||
name: string | null;
|
||||
street: string | null;
|
||||
zip: string | null;
|
||||
city: string | null;
|
||||
birthday: Timestamp | null;
|
||||
healthinsuranceId: string | null;
|
||||
healthinsurance: string | null;
|
||||
careDegree: string | null;
|
||||
disabilityMark1: string | null;
|
||||
disabilityMark2: string | null;
|
||||
};
|
||||
export type PlannedTime = {
|
||||
id: string;
|
||||
userId: string;
|
||||
firstname: string | null;
|
||||
surname: string | null;
|
||||
startDate: Timestamp | null;
|
||||
endDate: Timestamp | null;
|
||||
department: string | null;
|
||||
pause: number;
|
||||
duration: number;
|
||||
note: string | null;
|
||||
};
|
||||
export type PlannedTimeSource = {
|
||||
internalId: Generated<string>;
|
||||
insertedAt: Generated<Timestamp>;
|
||||
jobId: string;
|
||||
id: string | null;
|
||||
user_id: string | null;
|
||||
firstname: string | null;
|
||||
surname: string | null;
|
||||
department_id: string | null;
|
||||
department_name: string | null;
|
||||
start_date: string | null;
|
||||
end_date: string | null;
|
||||
pauze: string | null;
|
||||
duration: string | null;
|
||||
deleted: string | null;
|
||||
mark: string | null;
|
||||
};
|
||||
export type PlannedTimeStaging = {
|
||||
internalId: Generated<string>;
|
||||
id: number;
|
||||
employeeId: number;
|
||||
firstname: string | null;
|
||||
surname: string | null;
|
||||
departmentName: string | null;
|
||||
startDate: Timestamp;
|
||||
endDate: Timestamp;
|
||||
deleted: boolean;
|
||||
mark: string | null;
|
||||
};
|
||||
export type SolutionTarget = {
|
||||
id: Generated<string>;
|
||||
name: string;
|
||||
anomalyId: string | null;
|
||||
};
|
||||
export type SolutionTargetStep = {
|
||||
id: Generated<string>;
|
||||
description: string;
|
||||
solutionTargetId: string | null;
|
||||
};
|
||||
export type Tariff = {
|
||||
id: Generated<string>;
|
||||
validFrom: Timestamp | null;
|
||||
validTo: Timestamp | null;
|
||||
tariffGroupId: string | null;
|
||||
};
|
||||
export type TariffGroup = {
|
||||
id: Generated<string>;
|
||||
description: string;
|
||||
ordinanceType: string;
|
||||
};
|
||||
export type Ticket = {
|
||||
id: Generated<string>;
|
||||
currentState: TicketValidationState;
|
||||
notes: string[];
|
||||
errors: string[];
|
||||
isUrgent: Generated<boolean>;
|
||||
urgency: Generated<Timestamp>;
|
||||
approvalState: ApprovalState | null;
|
||||
documentInfo: Generated<unknown | null>;
|
||||
createdAt: Generated<Timestamp>;
|
||||
};
|
||||
export type TicketStateHistory = {
|
||||
id: Generated<string>;
|
||||
state: TicketValidationState;
|
||||
createdAt: Generated<Timestamp>;
|
||||
ticketId: string;
|
||||
};
|
||||
export type Tour = {
|
||||
id: Generated<string>;
|
||||
done: boolean;
|
||||
direction: string;
|
||||
carName: string | null;
|
||||
operationId: string;
|
||||
driverId: string | null;
|
||||
driverName: string | null;
|
||||
codriverId: string | null;
|
||||
codriverName: string | null;
|
||||
patientId: string | null;
|
||||
patientName: string | null;
|
||||
patientSurname: string | null;
|
||||
patientStreet: string | null;
|
||||
patientZip: string | null;
|
||||
patientCity: string | null;
|
||||
healthInsurance: string | null;
|
||||
healthInsuranceNumber: string | null;
|
||||
type: string | null;
|
||||
category: string | null;
|
||||
transportType: string | null;
|
||||
ordinanceType: string | null;
|
||||
rangeEndDate: Timestamp | null;
|
||||
startInstitution: string | null;
|
||||
startStreet: string | null;
|
||||
startZip: string | null;
|
||||
startCity: string | null;
|
||||
targetInstitution: string | null;
|
||||
targetStreet: string | null;
|
||||
targetZip: string | null;
|
||||
targetCity: string | null;
|
||||
startDate: Timestamp;
|
||||
check: Generated<number>;
|
||||
occupiedKm: number | null;
|
||||
totalKm: number | null;
|
||||
startBegin: string | null;
|
||||
startEnd: string | null;
|
||||
target: string | null;
|
||||
targetBegin: string | null;
|
||||
empty: string | null;
|
||||
hasInfection: Generated<boolean>;
|
||||
infectionName: string | null;
|
||||
revenueDispoLive: number | null;
|
||||
billDate: Timestamp | null;
|
||||
billNumber: string | null;
|
||||
revenue: Generated<number>;
|
||||
consumptionCosts: Generated<number>;
|
||||
revenueDeviation: Generated<number>;
|
||||
createdAt: Timestamp | null;
|
||||
deletedAt: Timestamp | null;
|
||||
ticketId: string | null;
|
||||
};
|
||||
export type TourSource = {
|
||||
internalId: Generated<string>;
|
||||
insertedAt: Generated<Timestamp>;
|
||||
jobId: string;
|
||||
id: string | null;
|
||||
check: string | null;
|
||||
createTime: string | null;
|
||||
empfangenVonId: string | null;
|
||||
endgen: string | null;
|
||||
serienId: string | null;
|
||||
serienKategorName: string | null;
|
||||
serienKategorie: string | null;
|
||||
zadStatus: string | null;
|
||||
abrDate: string | null;
|
||||
abrNum: string | null;
|
||||
abrTime: string | null;
|
||||
ausrueckZeit: string | null;
|
||||
startTime: string | null;
|
||||
startDate: string | null;
|
||||
date: string | null;
|
||||
date_iso: string | null;
|
||||
dauergenehmigung: string | null;
|
||||
direction: string | null;
|
||||
distanceValue: string | null;
|
||||
besetztKm: string | null;
|
||||
gefahren: string | null;
|
||||
gesamtPreis: string | null;
|
||||
infektion: string | null;
|
||||
itDesc: string | null;
|
||||
itKrz: string | null;
|
||||
itName: string | null;
|
||||
kkId: string | null;
|
||||
krankenkasse: string | null;
|
||||
patId: string | null;
|
||||
patName: string | null;
|
||||
patSurname: string | null;
|
||||
patStreet: string | null;
|
||||
patZip: string | null;
|
||||
patCity: string | null;
|
||||
startInstitution: string | null;
|
||||
startStreet: string | null;
|
||||
startZip: string | null;
|
||||
startCity: string | null;
|
||||
targetInstitution: string | null;
|
||||
targetStreet: string | null;
|
||||
targetZip: string | null;
|
||||
targetCity: string | null;
|
||||
transportKrz: string | null;
|
||||
transportName: string | null;
|
||||
transportart: string | null;
|
||||
verordnungsId: string | null;
|
||||
verordnungsName: string | null;
|
||||
type: string | null;
|
||||
startBegin: string | null;
|
||||
startEnd: string | null;
|
||||
frei: string | null;
|
||||
ziel: string | null;
|
||||
zielBegin: string | null;
|
||||
endTime: string | null;
|
||||
endDate: string | null;
|
||||
einsatzNummer: string | null;
|
||||
carName: string | null;
|
||||
carNo: string | null;
|
||||
beifahrerName: string | null;
|
||||
beifahrer: string | null;
|
||||
fahrerName: string | null;
|
||||
fahrer: string | null;
|
||||
color: string | null;
|
||||
};
|
||||
export type TourStaging = {
|
||||
internalId: Generated<string>;
|
||||
id: string | null;
|
||||
done: boolean | null;
|
||||
direction: string | null;
|
||||
carName: string | null;
|
||||
operationId: string | null;
|
||||
driverId: string | null;
|
||||
driverName: string | null;
|
||||
codriverId: string | null;
|
||||
codriverName: string | null;
|
||||
patientId: string | null;
|
||||
patientName: string | null;
|
||||
patientSurname: string | null;
|
||||
patientStreet: string | null;
|
||||
patientZip: string | null;
|
||||
patientCity: string | null;
|
||||
healthInsurance: string | null;
|
||||
healthInsuranceNumber: string | null;
|
||||
type: string | null;
|
||||
category: string | null;
|
||||
transportType: string | null;
|
||||
ordinanceType: string | null;
|
||||
rangeEndDate: Timestamp | null;
|
||||
startInstitution: string | null;
|
||||
startStreet: string | null;
|
||||
startZip: string | null;
|
||||
startCity: string | null;
|
||||
targetInstitution: string | null;
|
||||
targetStreet: string | null;
|
||||
targetZip: string | null;
|
||||
targetCity: string | null;
|
||||
startDate: Timestamp | null;
|
||||
check: Generated<number>;
|
||||
occupiedKm: number | null;
|
||||
totalKm: number | null;
|
||||
startBegin: string | null;
|
||||
startEnd: string | null;
|
||||
target: string | null;
|
||||
targetBegin: string | null;
|
||||
empty: string | null;
|
||||
hasInfection: Generated<boolean>;
|
||||
infectionName: string | null;
|
||||
revenueDispoLive: number | null;
|
||||
billDate: Timestamp | null;
|
||||
billNumber: string | null;
|
||||
revenue: Generated<number>;
|
||||
revenueDeviation: Generated<number>;
|
||||
consumptionCosts: Generated<number>;
|
||||
createdAt: Timestamp | null;
|
||||
};
|
||||
export type Trace = {
|
||||
id: Generated<string>;
|
||||
type: string;
|
||||
entityKey: string;
|
||||
createdAt: Generated<Timestamp>;
|
||||
};
|
||||
export type WorkTime = {
|
||||
id: string;
|
||||
userId: string;
|
||||
firstname: string;
|
||||
surname: string;
|
||||
startDate: Timestamp | null;
|
||||
endDate: Timestamp | null;
|
||||
department: string | null;
|
||||
pause: number;
|
||||
duration: number;
|
||||
};
|
||||
export type WorkTimeSource = {
|
||||
internalId: Generated<string>;
|
||||
insertedAt: Generated<Timestamp>;
|
||||
jobId: string;
|
||||
id: string | null;
|
||||
user_id: string | null;
|
||||
firstname: string | null;
|
||||
surname: string | null;
|
||||
department_id: string | null;
|
||||
department_name: string | null;
|
||||
start_date: string | null;
|
||||
end_date: string | null;
|
||||
pauze: string | null;
|
||||
duration: string | null;
|
||||
};
|
||||
export type WorkTimeStaging = {
|
||||
internalId: Generated<string>;
|
||||
id: number;
|
||||
employeeId: number;
|
||||
firstname: string;
|
||||
surname: string;
|
||||
departmentName: string | null;
|
||||
startDate: Timestamp;
|
||||
endDate: Timestamp;
|
||||
};
|
||||
export type DB = {
|
||||
_AvicennaTenantToTariffGroup: AvicennaTenantToTariffGroup;
|
||||
Anomaly: Anomaly;
|
||||
AttendanceRegistration: AttendanceRegistration;
|
||||
AttendanceRegistrationSource: AttendanceRegistrationSource;
|
||||
AttendanceRegistrationStaging: AttendanceRegistrationStaging;
|
||||
AvicennaTenant: AvicennaTenant;
|
||||
CostType: CostType;
|
||||
DyflexisEmployee: DyflexisEmployee;
|
||||
Employee: Employee;
|
||||
EmployeeDispoLiveSource: EmployeeDispoLiveSource;
|
||||
EmployeeDyflexisSource: EmployeeDyflexisSource;
|
||||
EmployeeStaging: EmployeeStaging;
|
||||
Patient: Patient;
|
||||
PatientSource: PatientSource;
|
||||
PatientStaging: PatientStaging;
|
||||
PlannedTime: PlannedTime;
|
||||
PlannedTimeSource: PlannedTimeSource;
|
||||
PlannedTimeStaging: PlannedTimeStaging;
|
||||
SolutionTarget: SolutionTarget;
|
||||
SolutionTargetStep: SolutionTargetStep;
|
||||
Tariff: Tariff;
|
||||
TariffGroup: TariffGroup;
|
||||
Ticket: Ticket;
|
||||
TicketStateHistory: TicketStateHistory;
|
||||
Tour: Tour;
|
||||
TourSource: TourSource;
|
||||
TourStaging: TourStaging;
|
||||
Trace: Trace;
|
||||
WorkTime: WorkTime;
|
||||
WorkTimeSource: WorkTimeSource;
|
||||
WorkTimeStaging: WorkTimeStaging;
|
||||
};
|
||||
|
|
@ -0,0 +1,248 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
Anomaly,
|
||||
Prisma,
|
||||
SolutionTarget,
|
||||
SolutionTargetStep,
|
||||
Tour,
|
||||
} from '@prisma/client';
|
||||
import { Interval } from 'date-fns';
|
||||
|
||||
import { TraceTypes } from 'src/core/base/trace.types';
|
||||
import { PrismaService } from 'src/core/database/prisma.service';
|
||||
import { AnomalyDetectorService } from './anomaly-detectors/anomaly-detector.service';
|
||||
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 { WorkingEmployeeNotInDistributionSystemDetector } from './anomaly-detectors/working-employee-not-in-distribution-system.detector';
|
||||
|
||||
@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,
|
||||
),
|
||||
];
|
||||
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly anomalyDetectorService: AnomalyDetectorService,
|
||||
) {}
|
||||
|
||||
detectAnomalies(interval: Interval): Promise<Anomaly[]> {
|
||||
return Promise.all(
|
||||
this.detectors.map((detector) => detector.detect(interval)),
|
||||
).then(async (anomaliesPerDetector) => {
|
||||
const anomalies = anomaliesPerDetector.flat();
|
||||
const anomalyEntities: Anomaly[] = [];
|
||||
for (const anomaly of anomalies) {
|
||||
if (anomaly) {
|
||||
anomalyEntities.push(
|
||||
await this.prisma.anomaly.upsert({
|
||||
where: {
|
||||
id: anomaly.id,
|
||||
},
|
||||
create: {
|
||||
...anomaly,
|
||||
},
|
||||
update: {},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
return anomalyEntities;
|
||||
});
|
||||
}
|
||||
|
||||
async validateAnomalies(): Promise<Anomaly[]> {
|
||||
const anomalies = await this.prisma.anomaly.findMany({
|
||||
where: {
|
||||
resolvedAt: null,
|
||||
},
|
||||
});
|
||||
|
||||
return Promise.all(
|
||||
anomalies.map((anomaly) =>
|
||||
this.detectors
|
||||
.find((detector) => detector.type === anomaly.type)
|
||||
.validate(anomaly),
|
||||
),
|
||||
).then(async (anomalies) => {
|
||||
const validatedAnomalies = anomalies.flat();
|
||||
const updatedAnomalies: Anomaly[] = [];
|
||||
for (const anomaly of validatedAnomalies) {
|
||||
updatedAnomalies.push(
|
||||
await this.prisma.anomaly.update({
|
||||
where: {
|
||||
id: anomaly.id,
|
||||
},
|
||||
data: {
|
||||
resolvedAt: anomaly.resolvedAt,
|
||||
manuallyResolved: false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
return updatedAnomalies;
|
||||
});
|
||||
}
|
||||
|
||||
countActiveAnomalies(types: AnomalyTypes[] = []): Promise<number> {
|
||||
return this.prisma.anomaly.count({
|
||||
where: this.getWhereActiveAnomalyQuery(types),
|
||||
});
|
||||
}
|
||||
|
||||
findActiveAnomalies(
|
||||
skip?: number,
|
||||
take?: number,
|
||||
types: AnomalyTypes[] = [],
|
||||
): Promise<Anomaly[]> {
|
||||
return this.prisma.anomaly.findMany({
|
||||
...{
|
||||
...(skip ? { skip, take: 10 } : {}),
|
||||
...(take ? { take } : {}),
|
||||
...(take && !skip ? { skip: 0 } : {}),
|
||||
},
|
||||
where: this.getWhereActiveAnomalyQuery(types),
|
||||
orderBy: {
|
||||
groupKey: 'asc',
|
||||
},
|
||||
include: {
|
||||
solution: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findActiveAnomaliesGroupedByGroupKey(
|
||||
skip = 0,
|
||||
take = 10,
|
||||
): Promise<{
|
||||
totalCount: number;
|
||||
anomalies: { key: string; anomalies: Anomaly[] }[];
|
||||
}> {
|
||||
const activeAnomalies = await this.findActiveAnomalies(
|
||||
undefined,
|
||||
undefined,
|
||||
[
|
||||
AnomalyTypes.DriverCertificate,
|
||||
AnomalyTypes.EmployeesLoggedIn,
|
||||
AnomalyTypes.OneTourAtATime,
|
||||
],
|
||||
);
|
||||
|
||||
const groupedAnomalies = this.groupAnomaliesByGroupKey(activeAnomalies);
|
||||
|
||||
return {
|
||||
totalCount: Object.keys(groupedAnomalies).length,
|
||||
anomalies: Object.entries(groupedAnomalies)
|
||||
.map(([key, anomalies]) => ({
|
||||
key,
|
||||
anomalies,
|
||||
}))
|
||||
.slice(skip, skip + take),
|
||||
};
|
||||
}
|
||||
|
||||
findById(id: string): Promise<Anomaly> {
|
||||
return this.prisma.anomaly.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
findSolutionTargetsByAnomalyId(anomalyId: string): Promise<SolutionTarget[]> {
|
||||
return this.prisma.solutionTarget.findMany({
|
||||
where: {
|
||||
anomalyId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
findSolutionTargetStepsBySolutionTargetId(
|
||||
solutionTargetId: string,
|
||||
): Promise<SolutionTargetStep[]> {
|
||||
return this.prisma.solutionTargetStep.findMany({
|
||||
where: {
|
||||
solutionTargetId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
findTourByOperationId(operationId: string): Promise<Tour> {
|
||||
return this.prisma.tour.findUnique({ where: { operationId } });
|
||||
}
|
||||
|
||||
async setAnomalyToSleep(anomalyId: string): Promise<Anomaly> {
|
||||
await this.prisma.trace.create({
|
||||
data: {
|
||||
entityKey: anomalyId,
|
||||
type: TraceTypes.AnomalyIgnored,
|
||||
},
|
||||
});
|
||||
return this.prisma.anomaly.update({
|
||||
where: {
|
||||
id: anomalyId,
|
||||
},
|
||||
data: {
|
||||
sleepSince: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async acceptAnomaly(anomalyId: string): Promise<Anomaly> {
|
||||
await this.prisma.trace.create({
|
||||
data: {
|
||||
entityKey: anomalyId,
|
||||
type: TraceTypes.AnomalyAccepted,
|
||||
},
|
||||
});
|
||||
return this.prisma.anomaly.update({
|
||||
where: {
|
||||
id: anomalyId,
|
||||
},
|
||||
data: {
|
||||
resolvedAt: new Date(),
|
||||
manuallyResolved: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private getWhereActiveAnomalyQuery(
|
||||
types: AnomalyTypes[] = [],
|
||||
): Prisma.AnomalyWhereInput {
|
||||
return {
|
||||
...(types.length ? { type: { in: types } } : {}),
|
||||
resolvedAt: null,
|
||||
OR: [
|
||||
{
|
||||
sleepSince: null,
|
||||
},
|
||||
{
|
||||
sleepSince: {
|
||||
lt: new Date(new Date().getTime() - 60 * 60 * 1000),
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private groupAnomaliesByGroupKey(
|
||||
anomalies: Anomaly[],
|
||||
): Record<string, Anomaly[]> {
|
||||
return anomalies.reduce(
|
||||
(grouped, anomaly) => ({
|
||||
...grouped,
|
||||
[anomaly.groupKey]: [...(grouped[anomaly.groupKey] ?? []), anomaly],
|
||||
}),
|
||||
{} as Record<string, Anomaly[]>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { PrismaModule } from 'src/core/database/prisma.module';
|
||||
import { AnomaliesService } from './anomalies.service';
|
||||
import { AnomalyDetectorService } from './anomaly-detectors/anomaly-detector.service';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
providers: [AnomaliesService, AnomalyDetectorService],
|
||||
exports: [AnomaliesService],
|
||||
})
|
||||
export class AnomalyDetectionModule {}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import { Anomaly, Prisma } from '@prisma/client';
|
||||
import { Interval } from 'date-fns';
|
||||
|
||||
import { AnomalyDetectorService } from './anomaly-detector.service';
|
||||
|
||||
export type AnomalySkeleton = Pick<
|
||||
Prisma.AnomalyCreateInput,
|
||||
'createdAt' | 'workedOnBy' | 'sleepTimer' | 'sleepSince'
|
||||
>;
|
||||
export abstract class AnomalyDetector {
|
||||
constructor(protected readonly dataService: AnomalyDetectorService) {}
|
||||
/**
|
||||
* Unique identifier of the Anomaly Type which is implemented by the class
|
||||
* TODO: Make the type typesafe
|
||||
*/
|
||||
abstract readonly type: string;
|
||||
/**
|
||||
* Generic method call to detect anomalies in the data
|
||||
*/
|
||||
abstract detect(inInterval: Interval): Promise<Prisma.AnomalyCreateInput[]>;
|
||||
/**
|
||||
* Generic method call to validate if an anomaly has been solved
|
||||
*/
|
||||
abstract validate(anomaly: Anomaly): Promise<Anomaly>;
|
||||
abstract createDescription(data: unknown): string;
|
||||
abstract createSolution(data: unknown): Prisma.SolutionTargetCreateInput[];
|
||||
|
||||
protected getAnomalySkeleton(): AnomalySkeleton {
|
||||
return {
|
||||
createdAt: new Date(),
|
||||
workedOnBy: '',
|
||||
sleepTimer: 60 * 60 * 1000,
|
||||
sleepSince: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,347 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
AttendanceRegistration,
|
||||
Employee,
|
||||
PlannedTime,
|
||||
Prisma,
|
||||
Tour,
|
||||
WorkTime,
|
||||
} from '@prisma/client';
|
||||
import { Interval, addDays, addHours, endOfDay, startOfDay } from 'date-fns';
|
||||
import { AttendanceRegistrationEvents } from 'src/core/base/attendance-registration-events.enum';
|
||||
import { PrismaService } from 'src/core/database/prisma.service';
|
||||
|
||||
export type AnomalySkeleton = Pick<
|
||||
Prisma.AnomalyCreateInput,
|
||||
| 'createdAt'
|
||||
| 'workedOnBy'
|
||||
| 'sleepTimer'
|
||||
| 'sleepSince'
|
||||
| 'resolvedAt'
|
||||
| 'manuallyResolved'
|
||||
>;
|
||||
|
||||
@Injectable()
|
||||
export class AnomalyDetectorService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
/**
|
||||
* Find a list of tours in a given time interval
|
||||
* @param interval An interval defining the time range of tours
|
||||
* @returns
|
||||
*/
|
||||
public findToursInInterval(
|
||||
{ start, end }: Interval,
|
||||
ordinanceTypes: string[] = [],
|
||||
): Promise<Tour[]> {
|
||||
return this.prisma.tour.findMany({
|
||||
where: {
|
||||
AND: [
|
||||
{
|
||||
startDate: {
|
||||
gte: new Date(start),
|
||||
},
|
||||
},
|
||||
{
|
||||
startDate: {
|
||||
lt: new Date(end),
|
||||
},
|
||||
},
|
||||
{
|
||||
check: {
|
||||
lte: 2,
|
||||
},
|
||||
},
|
||||
ordinanceTypes.length
|
||||
? { ordinanceType: { in: ordinanceTypes } }
|
||||
: {},
|
||||
{
|
||||
OR: [
|
||||
{
|
||||
carName: null,
|
||||
},
|
||||
{
|
||||
AND: [
|
||||
{
|
||||
carName: {
|
||||
not: 'Storno',
|
||||
},
|
||||
},
|
||||
{
|
||||
carName: {
|
||||
not: 'Löschen',
|
||||
},
|
||||
},
|
||||
{
|
||||
carName: {
|
||||
not: 'Abgabe',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
deletedAt: null,
|
||||
},
|
||||
{
|
||||
OR: [
|
||||
{
|
||||
patientId: null,
|
||||
},
|
||||
{
|
||||
patientId: {
|
||||
// Pause
|
||||
not: 'b7424477-a4b2-42f7-b7e7-a4bef24f6979',
|
||||
},
|
||||
},
|
||||
{
|
||||
patientId: {
|
||||
// Feierabend
|
||||
not: '3554e1a9-9c07-45e5-b53c-692a77e4fa9a',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public findToursInIntervalWithEmployee(
|
||||
{ start, end }: Interval,
|
||||
employeeId: string,
|
||||
ordinanceType?: string,
|
||||
): Promise<Tour[]> {
|
||||
return this.prisma.tour.findMany({
|
||||
where: {
|
||||
AND: [
|
||||
{
|
||||
OR: [
|
||||
{
|
||||
driverId: employeeId,
|
||||
},
|
||||
{
|
||||
codriverId: employeeId,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
startDate: {
|
||||
gte: new Date(start),
|
||||
},
|
||||
},
|
||||
{
|
||||
startDate: {
|
||||
lt: new Date(end),
|
||||
},
|
||||
},
|
||||
...(ordinanceType ? [{ ordinanceType }] : []),
|
||||
{
|
||||
OR: [
|
||||
{
|
||||
carName: null,
|
||||
},
|
||||
{
|
||||
AND: [
|
||||
{
|
||||
carName: {
|
||||
not: 'Storno',
|
||||
},
|
||||
},
|
||||
{
|
||||
carName: {
|
||||
not: 'Löschen',
|
||||
},
|
||||
},
|
||||
{
|
||||
carName: {
|
||||
not: 'Abgabe',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
deletedAt: null,
|
||||
},
|
||||
{
|
||||
OR: [
|
||||
{
|
||||
patientId: null,
|
||||
},
|
||||
{
|
||||
patientId: {
|
||||
// Pause
|
||||
not: 'b7424477-a4b2-42f7-b7e7-a4bef24f6979',
|
||||
},
|
||||
},
|
||||
{
|
||||
patientId: {
|
||||
// Feierabend
|
||||
not: '3554e1a9-9c07-45e5-b53c-692a77e4fa9a',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public findTourByOperationId(operationId): Promise<Tour> {
|
||||
return this.prisma.tour.findUnique({ where: { operationId } });
|
||||
}
|
||||
|
||||
public findWorkTimeByPersonnelNumberForDay(
|
||||
personnelNumber: string,
|
||||
day: Date,
|
||||
): Promise<WorkTime | undefined> {
|
||||
let startOfGivenDay: Date;
|
||||
if (day.getHours() >= 0 && day.getHours() < 3) {
|
||||
startOfGivenDay = addDays(day, -1);
|
||||
} else {
|
||||
startOfGivenDay = startOfDay(day);
|
||||
}
|
||||
const endOfGivenDay = endOfDay(startOfGivenDay);
|
||||
return this.prisma.workTime.findFirst({
|
||||
where: {
|
||||
userId: personnelNumber,
|
||||
AND: [
|
||||
{
|
||||
startDate: {
|
||||
gte: startOfGivenDay,
|
||||
},
|
||||
},
|
||||
{
|
||||
startDate: {
|
||||
lte: endOfGivenDay,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public findAttendanceRegistrationByPersonnelNumberForShift(
|
||||
personnelNumber: string,
|
||||
day: Date,
|
||||
eventTypes: AttendanceRegistrationEvents[] = [
|
||||
AttendanceRegistrationEvents.ClockedIn,
|
||||
AttendanceRegistrationEvents.ClockedOut,
|
||||
],
|
||||
): Promise<AttendanceRegistration[]> {
|
||||
let startOfGivenDay: Date;
|
||||
if (day.getHours() >= 0 && day.getHours() < 3) {
|
||||
startOfGivenDay = addDays(day, -1);
|
||||
} else {
|
||||
startOfGivenDay = startOfDay(day);
|
||||
}
|
||||
const endOfShift = addHours(day, 12);
|
||||
return this.prisma.attendanceRegistration.findMany({
|
||||
where: {
|
||||
employeeId: +personnelNumber,
|
||||
event: {
|
||||
in: eventTypes,
|
||||
},
|
||||
AND: [
|
||||
{
|
||||
dateTime: {
|
||||
gte: startOfGivenDay,
|
||||
},
|
||||
},
|
||||
{
|
||||
dateTime: {
|
||||
lt: endOfShift,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
orderBy: {
|
||||
dateTime: 'asc',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public findPlannedTimeByPersonnelNumberForDay(
|
||||
personnelNumber: string,
|
||||
day: Date,
|
||||
): Promise<PlannedTime[]> {
|
||||
let startOfGivenDay: Date;
|
||||
if (day.getHours() >= 0 && day.getHours() < 3) {
|
||||
startOfGivenDay = addDays(day, -1);
|
||||
} else {
|
||||
startOfGivenDay = startOfDay(day);
|
||||
}
|
||||
const endOfShift = addHours(day, 12);
|
||||
return this.prisma.plannedTime.findMany({
|
||||
where: {
|
||||
userId: personnelNumber,
|
||||
AND: [
|
||||
{
|
||||
startDate: {
|
||||
gte: startOfGivenDay,
|
||||
},
|
||||
},
|
||||
{
|
||||
startDate: {
|
||||
lt: endOfShift,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public findAttendanceRegistrationById(
|
||||
attendanceRegistrationId: number,
|
||||
): Promise<AttendanceRegistration> {
|
||||
return this.prisma.attendanceRegistration.findUnique({
|
||||
where: {
|
||||
attendanceRegistrationId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public findAttendanceRegistrationInInterval(
|
||||
interval: Interval,
|
||||
eventTypes: AttendanceRegistrationEvents[] = [
|
||||
AttendanceRegistrationEvents.ClockedIn,
|
||||
AttendanceRegistrationEvents.ClockedOut,
|
||||
],
|
||||
): Promise<AttendanceRegistration[]> {
|
||||
return this.prisma.attendanceRegistration.findMany({
|
||||
where: {
|
||||
event: {
|
||||
in: eventTypes,
|
||||
},
|
||||
AND: [
|
||||
{
|
||||
dateTime: {
|
||||
gte: startOfDay(interval.start),
|
||||
},
|
||||
},
|
||||
{
|
||||
dateTime: {
|
||||
lt: endOfDay(interval.end),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
orderBy: {
|
||||
dateTime: 'asc',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public findEmployeeById(employeeId: string): Promise<Employee> {
|
||||
return this.prisma.employee.findUnique({ where: { id: employeeId } });
|
||||
}
|
||||
|
||||
public findEmployeeByPersonnelNumber(
|
||||
personnelNumber: string,
|
||||
): Promise<Employee> {
|
||||
return this.prisma.employee.findFirst({ where: { personnelNumber } });
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
export enum AnomalyTypes {
|
||||
DriverCodriverSame = 'DriverCodriverSame',
|
||||
DriverCertificate = 'DriverCertificate',
|
||||
EmployeesLoggedIn = 'EmployeesLoggedIn',
|
||||
OneTourAtATime = 'OneTourAtATime',
|
||||
WorkingEmployeeNotInDistributionSystem = 'WorkingEmployeeNotInDistributionSystem',
|
||||
}
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
import { Logger } from '@nestjs/common';
|
||||
import { Anomaly, Prisma } from '@prisma/client';
|
||||
import { Interval, format } from 'date-fns';
|
||||
import { toZonedTime } from 'date-fns-tz';
|
||||
|
||||
import { AnomalyDetector } from './anomaly-detector.class';
|
||||
import { AnomalyTypes } from './anomaly-types.enum';
|
||||
|
||||
export class DriverCertificateDetector extends AnomalyDetector {
|
||||
private readonly logger = new Logger(DriverCertificateDetector.name);
|
||||
readonly type = AnomalyTypes.DriverCertificate;
|
||||
|
||||
async detect(inInterval: Interval): Promise<Prisma.AnomalyCreateInput[]> {
|
||||
const toursInRange = await this.dataService.findToursInInterval(inInterval);
|
||||
|
||||
const anomalies = await Promise.all(
|
||||
toursInRange
|
||||
.filter(({ driverId }) => !!driverId)
|
||||
.map(
|
||||
async ({ driverId, startDate, driverName, carName, operationId }) => {
|
||||
try {
|
||||
const driverEmployee =
|
||||
await this.dataService.findEmployeeById(driverId);
|
||||
if (!driverEmployee) {
|
||||
return;
|
||||
return this.createAnomaly(
|
||||
`${operationId}-${driverId}-${this.type}`,
|
||||
operationId,
|
||||
carName,
|
||||
driverName ?? '<Fahrer gelöscht>',
|
||||
new Date(startDate),
|
||||
);
|
||||
}
|
||||
if (
|
||||
!this.isPersonTransportCertificateValidAtStartOfTour(
|
||||
driverEmployee.personTransportCertificate,
|
||||
startDate,
|
||||
)
|
||||
) {
|
||||
return this.createAnomaly(
|
||||
`${operationId}-${driverId}-${this.type}`,
|
||||
operationId,
|
||||
carName,
|
||||
driverName,
|
||||
new Date(startDate),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
return;
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
return anomalies.filter((anomaly) => !!anomaly);
|
||||
}
|
||||
|
||||
async validate(anomaly: Anomaly): Promise<Anomaly> {
|
||||
const [operationId] = anomaly.id.split('-');
|
||||
try {
|
||||
const tour = await this.dataService.findTourByOperationId(operationId);
|
||||
if (!tour.driverId) {
|
||||
return { ...anomaly };
|
||||
}
|
||||
const employee = await this.dataService.findEmployeeById(tour.driverId);
|
||||
if (!employee) {
|
||||
return { ...anomaly };
|
||||
}
|
||||
if (
|
||||
this.isPersonTransportCertificateValidAtStartOfTour(
|
||||
employee.personTransportCertificate,
|
||||
tour.startDate,
|
||||
)
|
||||
) {
|
||||
return {
|
||||
...anomaly,
|
||||
resolvedAt: new Date(),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...anomaly,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(error, anomaly.id);
|
||||
return anomaly;
|
||||
}
|
||||
}
|
||||
|
||||
createDescription({
|
||||
name,
|
||||
carName,
|
||||
}: {
|
||||
name: string;
|
||||
carName: string;
|
||||
}): string {
|
||||
return `${carName}: ${name} hat keinen gültigen P-Schein`;
|
||||
}
|
||||
|
||||
createSolution({
|
||||
carName,
|
||||
operationId,
|
||||
startDate,
|
||||
}: {
|
||||
carName: string;
|
||||
operationId: string;
|
||||
startDate: Date;
|
||||
}): Prisma.SolutionTargetCreateInput[] {
|
||||
return [
|
||||
{
|
||||
name: `Fahrzeug ${carName}`,
|
||||
steps: {
|
||||
create: [
|
||||
{
|
||||
description: `Klären Sie ob im Fahrzeug ein Fahrer mit P-Schein sitzt`,
|
||||
},
|
||||
{
|
||||
description: `Informieren Sie das Team des Fahrzeugs, dass sie sich neu anmelden müssen`,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'DispoLive',
|
||||
steps: {
|
||||
create: [
|
||||
{
|
||||
description: `Passen Sie den Fahrer und Beifahrer der Tour ${operationId} an. Startzeit: ${format(
|
||||
startDate,
|
||||
'HH:mm dd.MM.yy',
|
||||
)}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private createAnomaly(
|
||||
anomalyId: string,
|
||||
operationId: string,
|
||||
carName: string,
|
||||
name: string,
|
||||
startDate: Date,
|
||||
): Prisma.AnomalyCreateInput {
|
||||
return {
|
||||
...this.getAnomalySkeleton(),
|
||||
id: anomalyId,
|
||||
type: this.type,
|
||||
groupKey: `${format(
|
||||
new Date(startDate),
|
||||
'yyyy-MM-dd-HH-mm',
|
||||
)}-${operationId}`,
|
||||
groupDescription: `${operationId} - ${format(
|
||||
toZonedTime(new Date(startDate), 'Europe/Berlin'),
|
||||
'HH:mm dd.MM.yy',
|
||||
)}`,
|
||||
solution: {
|
||||
create: this.createSolution({ carName, operationId, startDate }),
|
||||
},
|
||||
description: this.createDescription({ name, carName }),
|
||||
};
|
||||
}
|
||||
|
||||
private isPersonTransportCertificateValidAtStartOfTour(
|
||||
personTransportCertificateValidUntil: Date,
|
||||
startDateOfTour: Date,
|
||||
): boolean {
|
||||
return (
|
||||
new Date(personTransportCertificateValidUntil).getTime() >=
|
||||
new Date(startDateOfTour).getTime()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
import { Logger } from '@nestjs/common';
|
||||
import { Anomaly, Prisma } from '@prisma/client';
|
||||
import { Interval, format } from 'date-fns';
|
||||
import { toZonedTime } from 'date-fns-tz';
|
||||
import { AnomalyDetector } from './anomaly-detector.class';
|
||||
import { AnomalyTypes } from './anomaly-types.enum';
|
||||
|
||||
export class DriverCodriverSameDetector extends AnomalyDetector {
|
||||
private readonly logger = new Logger(DriverCodriverSameDetector.name);
|
||||
readonly type = AnomalyTypes.DriverCodriverSame;
|
||||
|
||||
async detect(inInterval: Interval): Promise<Prisma.AnomalyCreateInput[]> {
|
||||
const toursInRange = await this.dataService.findToursInInterval(
|
||||
inInterval,
|
||||
['KTW'],
|
||||
);
|
||||
|
||||
const anomalies = await Promise.all(
|
||||
toursInRange.map(
|
||||
({ operationId, carName, startDate, driverId, codriverId }) => {
|
||||
if (driverId === codriverId) {
|
||||
return this.createAnomaly(
|
||||
`${operationId}-${this.type}`,
|
||||
operationId,
|
||||
carName,
|
||||
startDate,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
return anomalies.filter((anomaly) => !!anomaly);
|
||||
}
|
||||
|
||||
async validate(anomaly: Anomaly): Promise<Anomaly> {
|
||||
const [operationId] = anomaly.id.split('-');
|
||||
const tour = await this.dataService.findTourByOperationId(operationId);
|
||||
if (tour?.driverId !== tour?.codriverId) {
|
||||
return {
|
||||
...anomaly,
|
||||
resolvedAt: new Date(),
|
||||
};
|
||||
}
|
||||
return { ...anomaly };
|
||||
}
|
||||
|
||||
createDescription({ carName }: { carName: string }): string {
|
||||
return `${carName}: Fahrer und Beifahrer identisch`;
|
||||
}
|
||||
|
||||
createSolution({
|
||||
carName,
|
||||
}: {
|
||||
carName: string;
|
||||
}): Prisma.SolutionTargetCreateInput[] {
|
||||
return [
|
||||
{
|
||||
name: `Fahrzeug ${carName}`,
|
||||
steps: {
|
||||
create: [
|
||||
{
|
||||
description: `Stellen Sie sicher, dass im Fahrzeug zwei Personen sitzen.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private createAnomaly(
|
||||
anomalyId: string,
|
||||
operationId: string,
|
||||
carName: string,
|
||||
startDate: Date,
|
||||
): Prisma.AnomalyCreateInput {
|
||||
return {
|
||||
...this.getAnomalySkeleton(),
|
||||
id: anomalyId,
|
||||
type: this.type,
|
||||
groupKey: `${format(
|
||||
new Date(startDate),
|
||||
'yyyy-MM-dd-HH-mm',
|
||||
)}-${operationId}`,
|
||||
groupDescription: `${operationId} - ${format(
|
||||
toZonedTime(new Date(startDate), 'Europe/Berlin'),
|
||||
'HH:mm dd.MM.yy',
|
||||
)}`,
|
||||
solution: { create: this.createSolution({ carName }) },
|
||||
description: this.createDescription({ carName }),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,346 @@
|
|||
import { Logger } from '@nestjs/common';
|
||||
import { Anomaly, Employee, Prisma, Tour } from '@prisma/client';
|
||||
import {
|
||||
Interval,
|
||||
addMinutes,
|
||||
format,
|
||||
isBefore,
|
||||
isWithinInterval,
|
||||
set,
|
||||
} from 'date-fns';
|
||||
import { toZonedTime, fromZonedTime } from 'date-fns-tz';
|
||||
|
||||
import { AnomalyDetector } from './anomaly-detector.class';
|
||||
import { AnomalyTypes } from './anomaly-types.enum';
|
||||
|
||||
export class EmployeesLoggedInDetector extends AnomalyDetector {
|
||||
private readonly logger = new Logger(EmployeesLoggedInDetector.name);
|
||||
readonly type = AnomalyTypes.EmployeesLoggedIn;
|
||||
|
||||
async detect(inInterval: Interval): Promise<Prisma.AnomalyCreateInput[]> {
|
||||
const toursInRange = await this.dataService.findToursInInterval(inInterval);
|
||||
|
||||
const anomaliesByTour = await Promise.all(
|
||||
toursInRange.map(async (tour) => {
|
||||
return Promise.all([
|
||||
this.detectAnomalyForEmployee(tour, 'driver'),
|
||||
this.detectAnomalyForEmployee(tour, 'codriver'),
|
||||
]);
|
||||
}),
|
||||
);
|
||||
return anomaliesByTour.filter((anomaly) => !!anomaly).flat();
|
||||
}
|
||||
|
||||
async validate(anomaly: Anomaly): Promise<Anomaly> {
|
||||
const [operationId, ...remainingIdParts] = anomaly.id.split('-');
|
||||
try {
|
||||
const employeeId = remainingIdParts.slice(0, -1).join('-');
|
||||
const tour = await this.dataService.findTourByOperationId(operationId);
|
||||
const employee = await this.dataService.findEmployeeById(employeeId);
|
||||
const startOfTour = this.getStartOfTour(
|
||||
new Date(tour.startDate),
|
||||
[
|
||||
tour.startBegin,
|
||||
tour.startEnd,
|
||||
tour.target,
|
||||
tour.targetBegin,
|
||||
tour.empty,
|
||||
].filter((val) => !!val)[0],
|
||||
);
|
||||
|
||||
if (!startOfTour) return anomaly;
|
||||
|
||||
if (this.validateEmployee(employee)) {
|
||||
const [isEmployeeLoggedIn] = await this.isEmployeeLoggedInAtDateTime(
|
||||
employee.personnelNumber,
|
||||
startOfTour,
|
||||
);
|
||||
|
||||
if (isEmployeeLoggedIn) {
|
||||
return {
|
||||
...anomaly,
|
||||
resolvedAt: new Date(),
|
||||
};
|
||||
} else {
|
||||
return { ...anomaly };
|
||||
}
|
||||
} else {
|
||||
return anomaly;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(error, anomaly.id);
|
||||
return anomaly;
|
||||
}
|
||||
}
|
||||
|
||||
createDescription(name: string): string {
|
||||
return `${name} ist nicht in der Zeiterfassung angemeldet`;
|
||||
}
|
||||
|
||||
createSolution({
|
||||
name,
|
||||
mobile,
|
||||
startOfTour,
|
||||
attendanceInterval,
|
||||
}: {
|
||||
name: string;
|
||||
mobile: string;
|
||||
startOfTour: Date;
|
||||
attendanceInterval: Interval;
|
||||
}): Prisma.SolutionTargetCreateInput[] {
|
||||
return [
|
||||
{
|
||||
name: 'Mitarbeiter',
|
||||
steps: {
|
||||
create: [
|
||||
{
|
||||
description: `Informieren Sie ${name}, dass er sich einstempeln muss. ${
|
||||
mobile ? `(${mobile})` : ''
|
||||
}`,
|
||||
},
|
||||
{
|
||||
description: `Zeitpunkt der Tour Anfahrt: ${format(
|
||||
toZonedTime(startOfTour, 'Europe/Berlin'),
|
||||
'HH:mm',
|
||||
)}`,
|
||||
},
|
||||
...(attendanceInterval
|
||||
? [
|
||||
{
|
||||
description: `Registrierte Anwesenheit: ${format(
|
||||
toZonedTime(attendanceInterval.start, 'Europe/Berlin'),
|
||||
'HH:mm',
|
||||
)}${
|
||||
attendanceInterval.end
|
||||
? ` - ${format(
|
||||
toZonedTime(
|
||||
attendanceInterval.end,
|
||||
'Europe/Berlin',
|
||||
),
|
||||
'HH:mm',
|
||||
)}`
|
||||
: ''
|
||||
}`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Dyflexis',
|
||||
steps: {
|
||||
create: [
|
||||
{
|
||||
description: `Korrigieren Sie den Zeitstempel auf den Arbeitsbeginn des Mitarbeiters`,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private async detectAnomalyForEmployee(
|
||||
{
|
||||
driverId,
|
||||
codriverId,
|
||||
startDate,
|
||||
startBegin,
|
||||
startEnd,
|
||||
target,
|
||||
targetBegin,
|
||||
empty,
|
||||
operationId,
|
||||
}: Tour,
|
||||
driverType: 'driver' | 'codriver',
|
||||
): Promise<Prisma.AnomalyCreateInput | undefined> {
|
||||
const employeeId = driverType === 'driver' ? driverId : codriverId;
|
||||
if (!employeeId) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
const employee = await this.dataService.findEmployeeById(employeeId);
|
||||
if (!employee) return;
|
||||
|
||||
const startOfTour = this.getStartOfTour(
|
||||
new Date(startDate),
|
||||
[startBegin, startEnd, target, targetBegin, empty].filter(
|
||||
(val) => !!val,
|
||||
)[0],
|
||||
);
|
||||
|
||||
if (!startOfTour) return Promise.resolve(undefined);
|
||||
|
||||
if (this.validateEmployee(employee)) {
|
||||
const [isEmployeeLoggedIn, checkedInterval] =
|
||||
await this.isEmployeeLoggedInAtDateTime(
|
||||
employee.personnelNumber,
|
||||
startOfTour,
|
||||
);
|
||||
|
||||
if (!isEmployeeLoggedIn) {
|
||||
const employeeName = `${employee.name} ${employee.surname}`;
|
||||
return this.createAnomaly(
|
||||
`${operationId}-${driverId}-${this.type}`,
|
||||
operationId,
|
||||
employeeName,
|
||||
employee.mobile,
|
||||
startDate,
|
||||
startOfTour,
|
||||
checkedInterval,
|
||||
);
|
||||
}
|
||||
}
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
private validateEmployee(employee: Employee | undefined) {
|
||||
if (!employee) {
|
||||
this.logger.warn(`Employee does not exist`);
|
||||
return false;
|
||||
}
|
||||
if (!employee.personnelNumber) {
|
||||
this.logger.warn(
|
||||
`Employee has no personnelNumber`,
|
||||
employee.id,
|
||||
employee.surname,
|
||||
employee.name,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private createAnomaly(
|
||||
anomalyId: string,
|
||||
operationId: string,
|
||||
name: string,
|
||||
mobile: string,
|
||||
startDate: Date,
|
||||
startOfTour: Date,
|
||||
attendanceInterval: Interval,
|
||||
): Prisma.AnomalyCreateInput {
|
||||
return {
|
||||
...this.getAnomalySkeleton(),
|
||||
id: anomalyId,
|
||||
type: this.type,
|
||||
groupKey: `${format(
|
||||
new Date(startDate),
|
||||
'yyyy-MM-dd-HH-mm',
|
||||
)}-${operationId}`,
|
||||
groupDescription: `${operationId} - ${format(
|
||||
toZonedTime(new Date(startDate), 'Europe/Berlin'),
|
||||
'HH:mm dd.MM.yy',
|
||||
)}`,
|
||||
solution: {
|
||||
create: this.createSolution({
|
||||
name,
|
||||
mobile,
|
||||
startOfTour,
|
||||
attendanceInterval,
|
||||
}),
|
||||
},
|
||||
description: this.createDescription(name),
|
||||
};
|
||||
}
|
||||
|
||||
private async isEmployeeLoggedInAtDateTime(
|
||||
personnelNumber: string,
|
||||
datetimeToCheck: Date,
|
||||
): Promise<[boolean, Interval]> {
|
||||
const workTime = await this.dataService.findWorkTimeByPersonnelNumberForDay(
|
||||
personnelNumber,
|
||||
datetimeToCheck,
|
||||
);
|
||||
const attendanceRegistrations =
|
||||
await this.dataService.findAttendanceRegistrationByPersonnelNumberForShift(
|
||||
personnelNumber,
|
||||
datetimeToCheck,
|
||||
);
|
||||
|
||||
const firstClockedIn = attendanceRegistrations.filter(
|
||||
({ event }) => event === 'clocked in',
|
||||
)[0];
|
||||
const lastClockedOut = attendanceRegistrations
|
||||
.filter(({ event }) => event === 'clocked out')
|
||||
.reverse()[0];
|
||||
|
||||
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 = {
|
||||
start: new Date(workTime.startDate),
|
||||
end: new Date(workTime.endDate),
|
||||
};
|
||||
return [
|
||||
isWithinInterval(datetimeToCheckWithBuffer, interval) ||
|
||||
isWithinInterval(datetimeToCheck, interval),
|
||||
interval,
|
||||
];
|
||||
} else if (
|
||||
firstClockedIn &&
|
||||
lastClockedOut &&
|
||||
isBefore(lastClockedOut.dateTime, firstClockedIn.dateTime)
|
||||
) {
|
||||
return [
|
||||
false,
|
||||
{
|
||||
start: firstClockedIn.dateTime,
|
||||
end: undefined,
|
||||
},
|
||||
];
|
||||
} else if (firstClockedIn && lastClockedOut) {
|
||||
const interval = {
|
||||
start: new Date(firstClockedIn.dateTime),
|
||||
end: new Date(lastClockedOut.dateTime),
|
||||
};
|
||||
return [
|
||||
isWithinInterval(datetimeToCheckWithBuffer, interval) ||
|
||||
isWithinInterval(datetimeToCheck, interval),
|
||||
interval,
|
||||
];
|
||||
} else if (firstClockedIn && !lastClockedOut) {
|
||||
return [
|
||||
isBefore(new Date(firstClockedIn.dateTime), datetimeToCheck) ||
|
||||
isBefore(
|
||||
new Date(firstClockedIn.dateTime),
|
||||
datetimeToCheckWithBuffer,
|
||||
),
|
||||
{ start: firstClockedIn.dateTime, end: undefined },
|
||||
];
|
||||
} else {
|
||||
return [false, undefined];
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
error,
|
||||
datetimeToCheck.toISOString(),
|
||||
workTime,
|
||||
firstClockedIn,
|
||||
lastClockedOut,
|
||||
);
|
||||
return [false, undefined];
|
||||
}
|
||||
}
|
||||
|
||||
private getStartOfTour(startDate: Date, startBegin: string) {
|
||||
if (!startBegin) return null;
|
||||
const zonedTime = toZonedTime(startDate, 'Europe/Berlin');
|
||||
const [hours, minutes] = startBegin.split(':').map((v) => +v);
|
||||
return fromZonedTime(set(zonedTime, { hours, minutes }), 'Europe/Berlin');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,371 @@
|
|||
import { Logger } from '@nestjs/common';
|
||||
import { Anomaly, Prisma } from '@prisma/client';
|
||||
import {
|
||||
addDays,
|
||||
areIntervalsOverlapping,
|
||||
endOfDay,
|
||||
format,
|
||||
Interval,
|
||||
isBefore,
|
||||
isWithinInterval,
|
||||
set,
|
||||
startOfDay,
|
||||
} from 'date-fns';
|
||||
import { toZonedTime, fromZonedTime } from 'date-fns-tz';
|
||||
|
||||
import { AnomalyDetector } from './anomaly-detector.class';
|
||||
import { AnomalyTypes } from './anomaly-types.enum';
|
||||
|
||||
export class OneTourAtATimeDetector extends AnomalyDetector {
|
||||
private readonly logger = new Logger(OneTourAtATimeDetector.name);
|
||||
readonly type = AnomalyTypes.OneTourAtATime;
|
||||
|
||||
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();
|
||||
|
||||
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,
|
||||
);
|
||||
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 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);
|
||||
}
|
||||
|
||||
async validate(anomaly: Anomaly): Promise<Anomaly> {
|
||||
const [operationId, ...remainingIdParts] = anomaly.id.split('-');
|
||||
const employeeId = remainingIdParts.slice(0, -1).join('-');
|
||||
|
||||
try {
|
||||
const tour = await this.dataService.findTourByOperationId(operationId);
|
||||
const { startBegin, startEnd, target, targetBegin, empty, startDate } =
|
||||
tour;
|
||||
|
||||
// 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 tourInterval: Interval = this.getTourInterval(
|
||||
start,
|
||||
end,
|
||||
startDate,
|
||||
);
|
||||
const startOfDayWithTour = startOfDay(startDate);
|
||||
const endOfDayWithTour = endOfDay(startDate);
|
||||
|
||||
const toursWithEmployee =
|
||||
await this.dataService.findToursInIntervalWithEmployee(
|
||||
{ start: startOfDayWithTour, end: endOfDayWithTour },
|
||||
employeeId,
|
||||
'KTW',
|
||||
);
|
||||
|
||||
if (
|
||||
!toursWithEmployee
|
||||
.filter(
|
||||
({ operationId: tourListOperationId }) =>
|
||||
operationId !== tourListOperationId,
|
||||
)
|
||||
.some(
|
||||
({
|
||||
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 [start] = [tourListStartBegin, tourListStartEnd].sort();
|
||||
// somehow empty can be before everything else, so we use targetBegin, target and empty and take the higher value ...
|
||||
const [end] = [tourListTargetBegin, tourListTarget, tourListEmpty]
|
||||
.filter((val) => !!val)
|
||||
.sort()
|
||||
.reverse();
|
||||
|
||||
const tourListTourInterval = this.getTourInterval(
|
||||
start,
|
||||
end,
|
||||
tourListStartDate,
|
||||
);
|
||||
return areIntervalsOverlapping(
|
||||
tourInterval,
|
||||
tourListTourInterval,
|
||||
);
|
||||
},
|
||||
)
|
||||
) {
|
||||
return {
|
||||
...anomaly,
|
||||
resolvedAt: new Date(),
|
||||
};
|
||||
}
|
||||
return { ...anomaly };
|
||||
} catch (error) {
|
||||
this.logger.error(error, anomaly.id);
|
||||
return anomaly;
|
||||
}
|
||||
}
|
||||
|
||||
createDescription({ name }: { name: string }): string {
|
||||
return `${name} ist auf mehr als einer Tour gleichzeitig`;
|
||||
}
|
||||
|
||||
createSolution({
|
||||
name,
|
||||
carNames,
|
||||
tourInfo,
|
||||
}: {
|
||||
name: string;
|
||||
carNames: string[];
|
||||
tourInfo: { operationId: string; startDate: string }[];
|
||||
}): Prisma.SolutionTargetCreateInput[] {
|
||||
return [
|
||||
{
|
||||
name: `Mitarbeiter ${name}`,
|
||||
steps: {
|
||||
create: [
|
||||
{
|
||||
description: `Sprechen Sie mit ${name} und klären Sie in welchem Fahrzeug er ist`,
|
||||
},
|
||||
{
|
||||
description: `Informieren Sie das Team des Fahrzeugs auf dem der Mitarbeiter nicht ist, dass sie sich neu anmelden müssen`,
|
||||
},
|
||||
{
|
||||
description: `Fahrzeuge mit diesem Mitarbeiter: ${carNames.join(
|
||||
', ',
|
||||
)}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'DispoLive',
|
||||
steps: {
|
||||
create: [
|
||||
{
|
||||
description: `Prüfen Sie ob bei den hier aufgelisteten Touren der Fahrer und Beifahrer richtig eingetragen sind`,
|
||||
},
|
||||
...tourInfo.map(({ operationId, startDate }) => ({
|
||||
description: `Einsatznummer: ${operationId} - ${startDate}`,
|
||||
})),
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private createAnomaly(
|
||||
anomalyId: string,
|
||||
operationId: string,
|
||||
name: string,
|
||||
startDate,
|
||||
carNames: string[],
|
||||
tourInfo: { operationId: string; startDate: string }[],
|
||||
): Prisma.AnomalyCreateInput {
|
||||
return {
|
||||
...this.getAnomalySkeleton(),
|
||||
id: anomalyId,
|
||||
type: this.type,
|
||||
groupKey: `${format(
|
||||
new Date(startDate),
|
||||
'yyyy-MM-dd-HH-mm',
|
||||
)}-${operationId}`,
|
||||
groupDescription: `${operationId} - ${format(
|
||||
toZonedTime(new Date(startDate), 'Europe/Berlin'),
|
||||
'HH:mm dd.MM.yy',
|
||||
)}`,
|
||||
solution: {
|
||||
create: this.createSolution({ name, carNames, tourInfo }),
|
||||
},
|
||||
description: this.createDescription({ name }),
|
||||
};
|
||||
}
|
||||
|
||||
private getTourInterval(
|
||||
startTime: string,
|
||||
endTime: string,
|
||||
startDate: Date | string,
|
||||
): Interval {
|
||||
return {
|
||||
start: this.getStartOfTour(new Date(startDate), startTime),
|
||||
end: this.getStartOfTour(new Date(startDate), endTime),
|
||||
};
|
||||
}
|
||||
|
||||
private getStartOfTour(startDate: Date, startBegin: string) {
|
||||
if (!startBegin) return null;
|
||||
const zonedTime = toZonedTime(startDate, 'Europe/Berlin');
|
||||
const [hours, minutes] = startBegin.split(':').map((v) => +v);
|
||||
let baseDate = new Date(zonedTime);
|
||||
if (hours >= 0 && hours < 3 && startDate.getHours() > 3) {
|
||||
baseDate = addDays(baseDate, 1);
|
||||
}
|
||||
return fromZonedTime(set(baseDate, { hours, minutes }), 'Europe/Berlin');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,171 @@
|
|||
import { Logger } from '@nestjs/common';
|
||||
import { Anomaly, Prisma } from '@prisma/client';
|
||||
import { Interval, endOfDay, format, startOfDay } from 'date-fns';
|
||||
import { toZonedTime } from 'date-fns-tz';
|
||||
|
||||
import { AttendanceRegistrationEvents } from 'src/core/base/attendance-registration-events.enum';
|
||||
import { AnomalyDetector } from './anomaly-detector.class';
|
||||
import { AnomalyTypes } from './anomaly-types.enum';
|
||||
|
||||
export class WorkingEmployeeNotInDistributionSystemDetector extends AnomalyDetector {
|
||||
private readonly logger = new Logger(
|
||||
WorkingEmployeeNotInDistributionSystemDetector.name,
|
||||
);
|
||||
readonly type = AnomalyTypes.WorkingEmployeeNotInDistributionSystem;
|
||||
|
||||
async detect(inInterval: Interval): Promise<Prisma.AnomalyCreateInput[]> {
|
||||
const attendanceRegistrationInInterval =
|
||||
await this.dataService.findAttendanceRegistrationInInterval(inInterval, [
|
||||
AttendanceRegistrationEvents.ClockedIn,
|
||||
]);
|
||||
|
||||
const anomalies = await Promise.all(
|
||||
attendanceRegistrationInInterval.map(
|
||||
async ({
|
||||
attendanceRegistrationId,
|
||||
// As worktime and plannedtime use the internal employeeId of dyflexis we use it as the personnelNumber entered in dispoLive
|
||||
employeeId: personnelNumber,
|
||||
dateTime,
|
||||
}) => {
|
||||
try {
|
||||
const employee =
|
||||
await this.dataService.findEmployeeByPersonnelNumber(
|
||||
`${personnelNumber}`,
|
||||
);
|
||||
if (!employee) return;
|
||||
|
||||
const plannedTime =
|
||||
await this.dataService.findPlannedTimeByPersonnelNumberForDay(
|
||||
`${personnelNumber}`,
|
||||
dateTime,
|
||||
);
|
||||
if (
|
||||
plannedTime.find(
|
||||
({ department }) =>
|
||||
department.toLowerCase() == 'Büro'.toLowerCase() ||
|
||||
department.toLowerCase() == 'Leitstelle'.toLowerCase(),
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const toursWithEmployee =
|
||||
await this.dataService.findToursInIntervalWithEmployee(
|
||||
{
|
||||
start: startOfDay(new Date(dateTime)),
|
||||
end: endOfDay(new Date(dateTime)),
|
||||
},
|
||||
employee.id,
|
||||
);
|
||||
if (toursWithEmployee.length === 0) {
|
||||
return this.createAnomaly(
|
||||
`${attendanceRegistrationId}-${employee.id}`,
|
||||
format(new Date(dateTime), 'yyyy-MM-dd'),
|
||||
`${employee.name} ${employee.surname}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`Employee does not exist! PersonnelNumber: ${personnelNumber}`,
|
||||
error,
|
||||
);
|
||||
return;
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
return anomalies.filter((anomaly) => !!anomaly);
|
||||
}
|
||||
|
||||
async validate(anomaly: Anomaly): Promise<Anomaly> {
|
||||
const [attendanceRegistrationId, ...employeeIdParts] =
|
||||
anomaly.id.split('-');
|
||||
const attendanceRegistration =
|
||||
await this.dataService.findAttendanceRegistrationById(
|
||||
+attendanceRegistrationId,
|
||||
);
|
||||
const employeeId = employeeIdParts.join('-');
|
||||
const employee = await this.dataService.findEmployeeById(employeeId);
|
||||
|
||||
const plannedTime =
|
||||
await this.dataService.findPlannedTimeByPersonnelNumberForDay(
|
||||
`${employee.personnelNumber}`,
|
||||
attendanceRegistration.dateTime,
|
||||
);
|
||||
if (
|
||||
plannedTime.find(
|
||||
({ department }) =>
|
||||
department.toLowerCase() == 'Büro'.toLowerCase() ||
|
||||
department.toLowerCase() == 'Leitstelle'.toLowerCase(),
|
||||
)
|
||||
) {
|
||||
return {
|
||||
...anomaly,
|
||||
resolvedAt: new Date(),
|
||||
};
|
||||
}
|
||||
const toursWithEmployee =
|
||||
await this.dataService.findToursInIntervalWithEmployee(
|
||||
{
|
||||
start: startOfDay(new Date(attendanceRegistration.dateTime)),
|
||||
end: endOfDay(new Date(attendanceRegistration.dateTime)),
|
||||
},
|
||||
employee.id,
|
||||
);
|
||||
if (toursWithEmployee.length > 0) {
|
||||
return {
|
||||
...anomaly,
|
||||
resolvedAt: new Date(),
|
||||
};
|
||||
}
|
||||
return {
|
||||
...anomaly,
|
||||
};
|
||||
}
|
||||
|
||||
createDescription(name: string): string {
|
||||
return `${name} ist keinem Fahrzeug zugewiesen`;
|
||||
}
|
||||
|
||||
createSolution(date: string): Prisma.SolutionTargetCreateInput[] {
|
||||
return [
|
||||
{
|
||||
name: `Mitarbeiter ${date}`,
|
||||
steps: {
|
||||
create: [
|
||||
{
|
||||
description:
|
||||
'Ist der Mitarbeiter teil des Fahrerteams, dann informieren Sie ihn, dass er sich am Fahrzeug anmelden muss',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Ist der Mitarbeiter nicht teil des Fahrerteams, akzeptieren Sie diese Anomalie',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private createAnomaly(
|
||||
anomalyId: string,
|
||||
dateString: string,
|
||||
name: string,
|
||||
): Prisma.AnomalyCreateInput {
|
||||
return {
|
||||
...this.getAnomalySkeleton(),
|
||||
id: anomalyId,
|
||||
type: this.type,
|
||||
groupKey: dateString,
|
||||
groupDescription: `${format(toZonedTime(new Date(dateString), 'Europe/Berlin'), 'dd.MM.yy')}`,
|
||||
solution: {
|
||||
create: this.createSolution(
|
||||
format(
|
||||
toZonedTime(new Date(dateString), 'Europe/Berlin'),
|
||||
'dd.MM.yy',
|
||||
),
|
||||
),
|
||||
},
|
||||
description: this.createDescription(name),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
export const BILLING_INFORMATION_BY_ORDINANCE_TYPE = {
|
||||
BTW: {
|
||||
'Mo-Fr': {
|
||||
'03:00-11:59': {
|
||||
personnelCostsPerHour: 20.23,
|
||||
consumptionCostsPerTour: 3.68,
|
||||
revenuePerTour: 41.22,
|
||||
},
|
||||
'12:00-18:29': {
|
||||
personnelCostsPerHour: 20.23,
|
||||
consumptionCostsPerTour: 3.68,
|
||||
revenuePerTour: 41.22,
|
||||
},
|
||||
'18:30-02:59': {
|
||||
personnelCostsPerHour: 24.59,
|
||||
consumptionCostsPerTour: 3.68,
|
||||
revenuePerTour: 41.22,
|
||||
},
|
||||
},
|
||||
Sa: {
|
||||
'03:00-11:59': {
|
||||
personnelCostsPerHour: 23.27,
|
||||
consumptionCostsPerTour: 3.68,
|
||||
revenuePerTour: 41.22,
|
||||
},
|
||||
'12:00-18:29': {
|
||||
personnelCostsPerHour: 23.27,
|
||||
consumptionCostsPerTour: 3.68,
|
||||
revenuePerTour: 41.22,
|
||||
},
|
||||
'18:30-02:59': {
|
||||
personnelCostsPerHour: 23.27,
|
||||
consumptionCostsPerTour: 3.68,
|
||||
revenuePerTour: 41.22,
|
||||
},
|
||||
},
|
||||
},
|
||||
TSW: {
|
||||
'Mo-Fr': {
|
||||
'03:00-11:59': {
|
||||
personnelCostsPerHour: 20.23,
|
||||
consumptionCostsPerTour: 3.68,
|
||||
revenuePerTour: 58.08,
|
||||
},
|
||||
'12:00-18:29': {
|
||||
personnelCostsPerHour: 20.23,
|
||||
consumptionCostsPerTour: 3.68,
|
||||
revenuePerTour: 58.08,
|
||||
},
|
||||
'18:30-02:59': {
|
||||
personnelCostsPerHour: 24.59,
|
||||
consumptionCostsPerTour: 3.68,
|
||||
revenuePerTour: 58.08,
|
||||
},
|
||||
},
|
||||
Sa: {
|
||||
'03:00-11:59': {
|
||||
personnelCostsPerHour: 23.27,
|
||||
consumptionCostsPerTour: 3.68,
|
||||
revenuePerTour: 58.08,
|
||||
},
|
||||
'12:00-18:29': {
|
||||
personnelCostsPerHour: 23.27,
|
||||
consumptionCostsPerTour: 3.68,
|
||||
revenuePerTour: 58.08,
|
||||
},
|
||||
'18:30-02:59': {
|
||||
personnelCostsPerHour: 23.27,
|
||||
consumptionCostsPerTour: 3.68,
|
||||
revenuePerTour: 58.08,
|
||||
},
|
||||
},
|
||||
},
|
||||
KTW: {
|
||||
'Mo-Fr': {
|
||||
'03:00-11:59': {
|
||||
personnelCostsPerHour: 56.36 / 2,
|
||||
consumptionCostsPerTour: 4.02,
|
||||
revenuePerTour: 128.08,
|
||||
},
|
||||
'12:00-18:29': {
|
||||
personnelCostsPerHour: 56.36 / 2,
|
||||
consumptionCostsPerTour: 4.02,
|
||||
revenuePerTour: 128.08,
|
||||
},
|
||||
'18:30-02:59': {
|
||||
personnelCostsPerHour: 68.5 / 2,
|
||||
consumptionCostsPerTour: 4.02,
|
||||
revenuePerTour: 128.08,
|
||||
},
|
||||
},
|
||||
Sa: {
|
||||
'03:00-11:59': {
|
||||
personnelCostsPerHour: 64.81 / 2,
|
||||
consumptionCostsPerTour: 4.02,
|
||||
revenuePerTour: 128.08,
|
||||
},
|
||||
'12:00-18:29': {
|
||||
personnelCostsPerHour: 64.81 / 2,
|
||||
consumptionCostsPerTour: 4.02,
|
||||
revenuePerTour: 128.08,
|
||||
},
|
||||
'18:30-02:59': {
|
||||
personnelCostsPerHour: 64.81 / 2,
|
||||
consumptionCostsPerTour: 4.02,
|
||||
revenuePerTour: 128.08,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,476 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import {
|
||||
CostType,
|
||||
CostTypeVariant,
|
||||
Tariff,
|
||||
TariffGroup,
|
||||
Tour,
|
||||
} from '@prisma/client';
|
||||
import {
|
||||
Interval,
|
||||
addDays,
|
||||
isSameDay,
|
||||
isSaturday,
|
||||
isWeekend,
|
||||
isWithinInterval,
|
||||
} from 'date-fns';
|
||||
import { toZonedTime } from 'date-fns-tz';
|
||||
|
||||
import { PrismaService } from 'src/core/database/prisma.service';
|
||||
import { generateIntervalForTimeRange } from 'src/utils';
|
||||
import { BILLING_INFORMATION_BY_ORDINANCE_TYPE } from './billing-information-by-ordinance-type';
|
||||
import { INFECTION_TYPES } from './infection-types';
|
||||
import { PersonnelCostCalculationService } from './personnel-cost-calculation.service';
|
||||
import { PUBLIC_HOLIDAYS_HH } from './public-holidays';
|
||||
|
||||
enum OrdinanceTypes {
|
||||
BTW = 'BTW',
|
||||
TSW = 'TSW',
|
||||
KTW = 'KTW',
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class CalculationService {
|
||||
private readonly logger = new Logger(CalculationService.name);
|
||||
|
||||
private healthInsurancesWithNoTenant = [];
|
||||
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly personnelCostCalculationService: PersonnelCostCalculationService,
|
||||
) {}
|
||||
|
||||
async calculateTourRevenue(tour: Tour): Promise<number> {
|
||||
const { healthInsurance, ordinanceType, startDate } = tour;
|
||||
if (!ordinanceType) {
|
||||
return 0;
|
||||
}
|
||||
const tariff = await this.findTariff(
|
||||
healthInsurance,
|
||||
ordinanceType.match(/([A-Z])\w+/).at(0),
|
||||
new Date(startDate),
|
||||
);
|
||||
if (!tariff) {
|
||||
this.logger.error(
|
||||
`No tariff found for tour ${tour.operationId} (${tour.healthInsurance})`,
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
const revenue = this.calculateRevenue(tour, tariff) / 100;
|
||||
return revenue;
|
||||
}
|
||||
|
||||
public calculatePersonnelCostInInterval(
|
||||
interval: Interval,
|
||||
employeeAmount: number,
|
||||
ordinanceType: string,
|
||||
): number {
|
||||
return this.personnelCostCalculationService.calculatePersonnelCostInInterval(
|
||||
interval,
|
||||
ordinanceType,
|
||||
employeeAmount,
|
||||
);
|
||||
}
|
||||
|
||||
public getBillingInformationByOrdinanceType(
|
||||
ordinanceType: string,
|
||||
date: Date,
|
||||
) {
|
||||
if (isSaturday(date)) {
|
||||
if (ordinanceType.indexOf('KTW') > -1) {
|
||||
return {
|
||||
personnelCostsPerHour: 64.81 / 2,
|
||||
consumptionCostsPerTour: 4.02,
|
||||
};
|
||||
}
|
||||
return {
|
||||
personnelCostsPerHour: 23.27,
|
||||
consumptionCostsPerTour: 3.68,
|
||||
};
|
||||
}
|
||||
if (ordinanceType.indexOf('KTW') > -1) {
|
||||
if (
|
||||
isWithinInterval(
|
||||
date,
|
||||
generateIntervalForTimeRange('18:30-03:00', 'Europe/Berlin', date),
|
||||
)
|
||||
) {
|
||||
return {
|
||||
personnelCostsPerHour: 68.5 / 2,
|
||||
consumptionCostsPerTour: 4.02,
|
||||
};
|
||||
}
|
||||
return {
|
||||
personnelCostsPerHour: 56.36 / 2,
|
||||
consumptionCostsPerTour: 4.02,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
isWithinInterval(
|
||||
date,
|
||||
generateIntervalForTimeRange('18:30-03:00', 'Europe/Berlin', date),
|
||||
)
|
||||
) {
|
||||
return {
|
||||
personnelCostsPerHour: 24.59,
|
||||
consumptionCostsPerTour: 3.68,
|
||||
};
|
||||
}
|
||||
return {
|
||||
personnelCostsPerHour: 20.23,
|
||||
consumptionCostsPerTour: 3.68,
|
||||
};
|
||||
}
|
||||
|
||||
public getModellBillingInformationByOrdinanceType(
|
||||
ordinanceType: string,
|
||||
date: Date,
|
||||
) {
|
||||
const daysKey = isSaturday(date) ? 'Sa' : 'Mo-Fr';
|
||||
|
||||
if (
|
||||
!Object.values(OrdinanceTypes).includes(ordinanceType as OrdinanceTypes)
|
||||
) {
|
||||
throw new Error(
|
||||
`OrdinanceType not supported. Supported types ${Object.values(
|
||||
OrdinanceTypes,
|
||||
).join(', ')}, got ${ordinanceType}`,
|
||||
);
|
||||
}
|
||||
|
||||
const billingInformationsByIntervals =
|
||||
BILLING_INFORMATION_BY_ORDINANCE_TYPE[ordinanceType][daysKey] as Record<
|
||||
string,
|
||||
{
|
||||
personnelCostsPerHour: number;
|
||||
consumptionCostsPerTour: number;
|
||||
revenuePerTour: number;
|
||||
}
|
||||
>;
|
||||
const billingInformation = Object.entries(
|
||||
billingInformationsByIntervals,
|
||||
).find(([timeRange]) =>
|
||||
isWithinInterval(
|
||||
date,
|
||||
generateIntervalForTimeRange(
|
||||
timeRange,
|
||||
'Europe/Berlin',
|
||||
date.getHours() >= 0 && date.getHours() <= 3
|
||||
? addDays(date, -1)
|
||||
: date,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (!billingInformation) {
|
||||
this.logger.error(
|
||||
"Couldn't find billing information",
|
||||
ordinanceType,
|
||||
date.toISOString(),
|
||||
);
|
||||
throw new Error("Couldn't find billing information");
|
||||
}
|
||||
return billingInformation[1];
|
||||
}
|
||||
|
||||
private calculateRevenue(
|
||||
tour: Tour,
|
||||
tariff: Tariff & { costTypes: CostType[]; tariffGroup: TariffGroup },
|
||||
): number {
|
||||
const highestZuschlag = this.getHighestZuschlag(tour, tariff);
|
||||
// if TariffGroup is VDEK KTW, sum up costTypes
|
||||
const isVdek =
|
||||
tariff.tariffGroup?.description?.indexOf('VDEK KTW') > -1 ?? false;
|
||||
|
||||
return tariff.costTypes.reduce((sum, costType) => {
|
||||
switch (costType.variant) {
|
||||
case CostTypeVariant.GRUNDPREIS:
|
||||
// TODO: decide if you need a CostTypeVariant for QMS Zuschlag and exclude the category there
|
||||
if (
|
||||
costType.description.indexOf('QMS') > -1 &&
|
||||
tour.category === 'Privatfahrt Rechnung'
|
||||
) {
|
||||
return sum;
|
||||
}
|
||||
return sum + costType.costAmount;
|
||||
case CostTypeVariant.TSWDIFFERENZIERUNG:
|
||||
return sum + this.calculateTSW(tour, costType);
|
||||
case CostTypeVariant.DESINFEKTIONSZUSCHLAG:
|
||||
return sum + this.calculateDesinfektionszuschlag(tour, costType);
|
||||
case CostTypeVariant.KILOMETERZUSCHLAG:
|
||||
return sum + this.calculateKilometerzuschlag(tour, costType);
|
||||
case CostTypeVariant.FAHRZEUGZUSCHLAG:
|
||||
return sum + this.calculateFahrzeugzuschlag(tour, costType);
|
||||
case CostTypeVariant.NACHTZUSCHLAG:
|
||||
if (highestZuschlag === CostTypeVariant.NACHTZUSCHLAG || isVdek) {
|
||||
return sum + this.calculateNachtzuschlag(tour, costType);
|
||||
}
|
||||
return sum;
|
||||
case CostTypeVariant.WOCHENENDZUSCHLAG:
|
||||
if (highestZuschlag === CostTypeVariant.WOCHENENDZUSCHLAG || isVdek) {
|
||||
return sum + this.calculateWochenendzuschlag(tour, costType);
|
||||
}
|
||||
return sum;
|
||||
case CostTypeVariant.FEIERTAGSZUSCHLAG:
|
||||
if (highestZuschlag === CostTypeVariant.FEIERTAGSZUSCHLAG || isVdek) {
|
||||
return sum + this.calculateFeiertagszuschlag(tour, costType);
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
private getHighestZuschlag(
|
||||
tour: Tour,
|
||||
tariff: Tariff & { costTypes: CostType[] },
|
||||
): CostTypeVariant {
|
||||
const zuschlaegeHighestToLowest = tariff.costTypes
|
||||
.filter((costType) =>
|
||||
[
|
||||
CostTypeVariant.NACHTZUSCHLAG,
|
||||
CostTypeVariant.FEIERTAGSZUSCHLAG,
|
||||
CostTypeVariant.WOCHENENDZUSCHLAG,
|
||||
].includes(costType.variant as any),
|
||||
)
|
||||
.map((costType): [CostTypeVariant, number] => {
|
||||
switch (costType.variant) {
|
||||
case CostTypeVariant.NACHTZUSCHLAG:
|
||||
return [
|
||||
CostTypeVariant.NACHTZUSCHLAG,
|
||||
this.calculateNachtzuschlag(tour, costType),
|
||||
];
|
||||
case CostTypeVariant.WOCHENENDZUSCHLAG:
|
||||
return [
|
||||
CostTypeVariant.WOCHENENDZUSCHLAG,
|
||||
this.calculateWochenendzuschlag(tour, costType),
|
||||
];
|
||||
case CostTypeVariant.FEIERTAGSZUSCHLAG:
|
||||
return [
|
||||
CostTypeVariant.FEIERTAGSZUSCHLAG,
|
||||
this.calculateFeiertagszuschlag(tour, costType),
|
||||
];
|
||||
}
|
||||
})
|
||||
.sort(
|
||||
([, costAmountA], [, costAmountB]) =>
|
||||
(costAmountA as number) - (costAmountB as number),
|
||||
)
|
||||
.reverse();
|
||||
if (zuschlaegeHighestToLowest.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return zuschlaegeHighestToLowest[0][0];
|
||||
}
|
||||
|
||||
private calculateDesinfektionszuschlag(
|
||||
tour: Tour,
|
||||
costType: CostType,
|
||||
): number {
|
||||
if (tour.hasInfection && !tour.infectionName) {
|
||||
return 0;
|
||||
}
|
||||
if (
|
||||
tour.hasInfection &&
|
||||
(tour.infectionName.indexOf(costType.infectionType) > -1 ||
|
||||
INFECTION_TYPES[tour.infectionName] === costType.infectionType)
|
||||
) {
|
||||
return costType.costAmount;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private calculateTSW(tour: Tour, costType: CostType): number {
|
||||
if (
|
||||
costType.description.includes('TSW 1') &&
|
||||
tour.driverId === tour.codriverId
|
||||
) {
|
||||
return costType.costAmount;
|
||||
} else if (
|
||||
costType.description.includes('TSW 2') &&
|
||||
tour.driverId !== tour.codriverId
|
||||
) {
|
||||
return costType.costAmount;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private calculateFeiertagszuschlag(tour: Tour, costType: CostType): number {
|
||||
if (
|
||||
PUBLIC_HOLIDAYS_HH.some((day) =>
|
||||
isSameDay(
|
||||
new Date(day),
|
||||
new Date(toZonedTime(tour.startDate, 'Europe/Berlin')),
|
||||
),
|
||||
)
|
||||
) {
|
||||
return costType.costAmount;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private calculateNachtzuschlag(tour: Tour, costType: CostType): number {
|
||||
const firstInterval = generateIntervalForTimeRange(
|
||||
costType.timeRange,
|
||||
'Europe/Berlin',
|
||||
addDays(tour.startDate, -1),
|
||||
);
|
||||
const secondInterval = generateIntervalForTimeRange(
|
||||
costType.timeRange,
|
||||
'Europe/Berlin',
|
||||
tour.startDate,
|
||||
);
|
||||
if (
|
||||
isWithinInterval(tour.startDate, firstInterval) ||
|
||||
isWithinInterval(tour.startDate, secondInterval)
|
||||
) {
|
||||
return costType.costAmount;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private calculateKilometerzuschlag(tour: Tour, costType: CostType): number {
|
||||
const kilometerOverLimit = Math.ceil(
|
||||
tour.occupiedKm - costType.kmInclusive,
|
||||
);
|
||||
if (kilometerOverLimit <= 0) return 0;
|
||||
return costType.costAmount * kilometerOverLimit;
|
||||
}
|
||||
|
||||
private calculateFahrzeugzuschlag(tour: Tour, costType: CostType): number {
|
||||
const isCarNameSupported = !costType.excludeCarNames.includes(tour.carName);
|
||||
if (!isCarNameSupported) return 0;
|
||||
return costType.costAmount;
|
||||
}
|
||||
|
||||
private calculateWochenendzuschlag(tour: Tour, costType: CostType): number {
|
||||
if (isWeekend(toZonedTime(tour.startDate, 'Europe/Berlin')))
|
||||
return costType.costAmount;
|
||||
return 0;
|
||||
}
|
||||
|
||||
private async findTariff(
|
||||
healthInsurance: string,
|
||||
ordinanceType: string,
|
||||
tourDate: Date,
|
||||
) {
|
||||
if (!healthInsurance || healthInsurance === '') {
|
||||
return this.prisma.tariff.findFirst({
|
||||
where: {
|
||||
tariffGroup: {
|
||||
description: {
|
||||
contains: 'AOK',
|
||||
},
|
||||
ordinanceType,
|
||||
},
|
||||
OR: [
|
||||
{
|
||||
AND: [
|
||||
{ validTo: null },
|
||||
{
|
||||
validFrom: {
|
||||
lte: tourDate,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
AND: [
|
||||
{ validTo: { gte: tourDate } },
|
||||
{ validFrom: { lte: tourDate } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
include: {
|
||||
tariffGroup: true,
|
||||
costTypes: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
const tenant = await this.prisma.avicennaTenant.findFirst({
|
||||
where: {
|
||||
name: {
|
||||
contains: healthInsurance,
|
||||
},
|
||||
tariffGroups: {
|
||||
some: {
|
||||
ordinanceType,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!tenant) {
|
||||
this.healthInsurancesWithNoTenant.push(healthInsurance);
|
||||
return this.prisma.tariff.findFirst({
|
||||
where: {
|
||||
tariffGroup: {
|
||||
description: {
|
||||
contains: 'AOK',
|
||||
},
|
||||
ordinanceType,
|
||||
},
|
||||
OR: [
|
||||
{
|
||||
AND: [
|
||||
{ validTo: null },
|
||||
{
|
||||
validFrom: {
|
||||
lte: tourDate,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
AND: [
|
||||
{ validTo: { gte: tourDate } },
|
||||
{ validFrom: { lte: tourDate } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
include: {
|
||||
tariffGroup: true,
|
||||
costTypes: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
return this.prisma.tariff.findFirst({
|
||||
where: {
|
||||
tariffGroup: {
|
||||
tenants: {
|
||||
some: {
|
||||
id: tenant.id,
|
||||
},
|
||||
},
|
||||
ordinanceType,
|
||||
},
|
||||
OR: [
|
||||
{
|
||||
AND: [
|
||||
{ validTo: null },
|
||||
{
|
||||
validFrom: {
|
||||
lte: tourDate,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
AND: [
|
||||
{ validTo: { gte: tourDate } },
|
||||
{ validFrom: { lte: tourDate } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
include: {
|
||||
tariffGroup: true,
|
||||
costTypes: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getHealthInsurancesWithNoTenant(): string[] {
|
||||
return [...new Set(this.healthInsurancesWithNoTenant)];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
export const INFECTION_TYPES = {
|
||||
'3 MRGN': '2.A',
|
||||
'4 MRGN': '2.A',
|
||||
Aids: '2.A',
|
||||
Clostridien: '2.B',
|
||||
'Clostridium diffcile': '2.B',
|
||||
'Covid-19': '3.1',
|
||||
Durchfall: '2.A',
|
||||
'Durchfallerkrankungen/Salmonellosen': '2.A',
|
||||
ESBL: '2.A',
|
||||
'Extended-spectrum Beta-Laktamase': '2.A',
|
||||
'Hepatitis B inkl. D': '2.A',
|
||||
'Hepatitis B inklusive D': '2.A',
|
||||
'Hepatitis C': '2.A',
|
||||
HIV: '2.A',
|
||||
Meningitis: '3.1',
|
||||
'Methicillin resistente Staphylococcus Aureus': '2.A',
|
||||
MRSA: '2.A',
|
||||
Norovirus: '2.B',
|
||||
Sonstiges: '2.A',
|
||||
VRE: '2.A',
|
||||
};
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { Interval, getISODay, isSameDay } from 'date-fns';
|
||||
import { toZonedTime } from 'date-fns-tz';
|
||||
|
||||
import { generateIntervalForTimeRange, getHoursInInterval } from 'src/utils';
|
||||
import { PUBLIC_HOLIDAYS_HH } from './public-holidays';
|
||||
|
||||
enum PersonnelCostCondition {
|
||||
NACHTZUSCHLAG = 'NACHTZUSCHLAG',
|
||||
SAMSTAGSZUSCHLAG = 'SAMSTAGSZUSCHLAG',
|
||||
SONNTAGSZUSCHLAG = 'SONNTAGSZUSCHLAG',
|
||||
FEIERTAGSZUSCHLAG = 'FEIERTAGSZUSCHLAG',
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class PersonnelCostCalculationService {
|
||||
private baseCost: Record<
|
||||
'KTW' | 'OTHER',
|
||||
{
|
||||
base: number;
|
||||
withAdditional: number;
|
||||
}
|
||||
> = {
|
||||
KTW: {
|
||||
base: 1733,
|
||||
withAdditional: 2306,
|
||||
},
|
||||
OTHER: {
|
||||
base: 1440,
|
||||
withAdditional: 1797,
|
||||
},
|
||||
};
|
||||
private costTypes = [
|
||||
{
|
||||
description: 'Nachtzuschlag von 20 Uhr bis 0 Uhr',
|
||||
condition: PersonnelCostCondition.NACHTZUSCHLAG,
|
||||
percent: 25,
|
||||
timeRange: '20:00-23:59',
|
||||
},
|
||||
{
|
||||
description: 'Nachtzuschlag von 0 Uhr bis 4 Uhr',
|
||||
condition: PersonnelCostCondition.NACHTZUSCHLAG,
|
||||
percent: 40,
|
||||
timeRange: '00:00-04:00',
|
||||
},
|
||||
{
|
||||
description: 'Samstagszuschlag',
|
||||
condition: PersonnelCostCondition.SAMSTAGSZUSCHLAG,
|
||||
percent: 15,
|
||||
},
|
||||
{
|
||||
description: 'Sonntagszuschlag',
|
||||
condition: PersonnelCostCondition.SONNTAGSZUSCHLAG,
|
||||
percent: 50,
|
||||
},
|
||||
{
|
||||
description: 'Feiertagszuschlag',
|
||||
condition: PersonnelCostCondition.FEIERTAGSZUSCHLAG,
|
||||
percent: 125,
|
||||
},
|
||||
];
|
||||
|
||||
calculatePersonnelCostInInterval(
|
||||
interval: Interval,
|
||||
ordinanceType: string,
|
||||
employeeAmount: number,
|
||||
): number {
|
||||
const { base, withAdditional } =
|
||||
this.getBaseCostForOrdinanceType(ordinanceType);
|
||||
const baseCostInInterval = getHoursInInterval(interval) * withAdditional;
|
||||
const additionalCosts = this.calculateAdditionalCosts(interval, base);
|
||||
return (baseCostInInterval + additionalCosts) * employeeAmount;
|
||||
}
|
||||
|
||||
calculateAdditionalCosts(interval: Interval, baseCost: number): number {
|
||||
return this.costTypes
|
||||
.map(({ condition, percent, timeRange }) => {
|
||||
switch (condition) {
|
||||
case PersonnelCostCondition.NACHTZUSCHLAG:
|
||||
return this.calculateNachtZuschlag(
|
||||
interval,
|
||||
baseCost,
|
||||
percent,
|
||||
timeRange,
|
||||
);
|
||||
case PersonnelCostCondition.SAMSTAGSZUSCHLAG:
|
||||
return this.calculateWeekdayZuschlag(
|
||||
interval,
|
||||
baseCost,
|
||||
percent,
|
||||
6,
|
||||
);
|
||||
case PersonnelCostCondition.SONNTAGSZUSCHLAG:
|
||||
return this.calculateWeekdayZuschlag(
|
||||
interval,
|
||||
baseCost,
|
||||
percent,
|
||||
7,
|
||||
);
|
||||
case PersonnelCostCondition.FEIERTAGSZUSCHLAG:
|
||||
return this.calculateFeiertagZuschlag(interval, baseCost, percent);
|
||||
}
|
||||
})
|
||||
.reduce((sum, cost) => (sum += cost), 0);
|
||||
}
|
||||
|
||||
private calculateNachtZuschlag(
|
||||
interval: Interval,
|
||||
baseCost: number,
|
||||
percent: number,
|
||||
timeRange: string,
|
||||
): number {
|
||||
const timeRangeIntervalForStart = generateIntervalForTimeRange(
|
||||
timeRange,
|
||||
'Europe/Berlin',
|
||||
new Date(interval.start),
|
||||
);
|
||||
const timeRangeIntervalForEnd = generateIntervalForTimeRange(
|
||||
timeRange,
|
||||
'Europe/Berlin',
|
||||
new Date(interval.end),
|
||||
);
|
||||
const costsForStart =
|
||||
(getHoursInInterval(interval, timeRangeIntervalForStart) *
|
||||
baseCost *
|
||||
percent) /
|
||||
100;
|
||||
const costsForEnd =
|
||||
(getHoursInInterval(interval, timeRangeIntervalForEnd) *
|
||||
baseCost *
|
||||
percent) /
|
||||
100;
|
||||
return costsForStart > costsForEnd ? costsForStart : costsForEnd;
|
||||
}
|
||||
|
||||
private calculateWeekdayZuschlag(
|
||||
interval: Interval,
|
||||
baseCost: number,
|
||||
percent: number,
|
||||
weekday: number,
|
||||
): number {
|
||||
if (getISODay(toZonedTime(interval.start, 'Europe/Berlin')) === weekday) {
|
||||
return (getHoursInInterval(interval) * baseCost * percent) / 100;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private calculateFeiertagZuschlag(
|
||||
interval: Interval,
|
||||
baseCost: number,
|
||||
percent: number,
|
||||
): number {
|
||||
if (
|
||||
PUBLIC_HOLIDAYS_HH.some((day) =>
|
||||
isSameDay(
|
||||
new Date(day),
|
||||
new Date(toZonedTime(interval.start, 'Europe/Berlin')),
|
||||
),
|
||||
)
|
||||
) {
|
||||
return (getHoursInInterval(interval) * baseCost * percent) / 100;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private getBaseCostForOrdinanceType(ordinanceType: string): {
|
||||
base: number;
|
||||
withAdditional: number;
|
||||
} {
|
||||
if (ordinanceType.indexOf('KTW') > -1) {
|
||||
return this.baseCost.KTW;
|
||||
}
|
||||
return this.baseCost.OTHER;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
export const PUBLIC_HOLIDAYS_HH = [
|
||||
'2020-01-01T00:00:00.000Z',
|
||||
'2020-04-10T00:00:00.000Z',
|
||||
'2020-04-13T00:00:00.000Z',
|
||||
'2020-05-01T00:00:00.000Z',
|
||||
'2020-05-21T00:00:00.000Z',
|
||||
'2020-06-01T00:00:00.000Z',
|
||||
'2020-10-03T00:00:00.000Z',
|
||||
'2020-10-31T00:00:00.000Z',
|
||||
'2020-12-25T00:00:00.000Z',
|
||||
'2020-12-26T00:00:00.000Z',
|
||||
'2021-01-01T00:00:00.000Z',
|
||||
'2021-04-02T00:00:00.000Z',
|
||||
'2021-04-05T00:00:00.000Z',
|
||||
'2021-05-01T00:00:00.000Z',
|
||||
'2021-05-13T00:00:00.000Z',
|
||||
'2021-05-24T00:00:00.000Z',
|
||||
'2021-10-03T00:00:00.000Z',
|
||||
'2021-10-31T00:00:00.000Z',
|
||||
'2021-12-25T00:00:00.000Z',
|
||||
'2021-12-26T00:00:00.000Z',
|
||||
'2022-01-01T00:00:00.000Z',
|
||||
'2022-04-15T00:00:00.000Z',
|
||||
'2022-04-18T00:00:00.000Z',
|
||||
'2022-05-01T00:00:00.000Z',
|
||||
'2022-06-06T00:00:00.000Z',
|
||||
'2022-05-26T00:00:00.000Z',
|
||||
'2022-10-03T00:00:00.000Z',
|
||||
'2022-10-31T00:00:00.000Z',
|
||||
'2022-12-25T00:00:00.000Z',
|
||||
'2022-12-26T00:00:00.000Z',
|
||||
'2023-01-01T00:00:00.000Z',
|
||||
'2023-04-07T00:00:00.000Z',
|
||||
'2023-04-10T00:00:00.000Z',
|
||||
'2023-05-01T00:00:00.000Z',
|
||||
'2023-05-18T00:00:00.000Z',
|
||||
'2023-05-29T00:00:00.000Z',
|
||||
'2023-10-03T00:00:00.000Z',
|
||||
'2023-10-31T00:00:00.000Z',
|
||||
'2023-12-25T00:00:00.000Z',
|
||||
'2023-12-26T00:00:00.000Z',
|
||||
'2024-01-01T00:00:00.000Z',
|
||||
'2024-03-29T00:00:00.000Z',
|
||||
'2024-04-01T00:00:00.000Z',
|
||||
'2024-05-01T00:00:00.000Z',
|
||||
'2024-05-09T00:00:00.000Z',
|
||||
'2024-05-20T00:00:00.000Z',
|
||||
'2024-10-03T00:00:00.000Z',
|
||||
'2024-10-31T00:00:00.000Z',
|
||||
'2024-12-25T00:00:00.000Z',
|
||||
'2024-12-26T00:00:00.000Z',
|
||||
'2025-01-01T00:00:00.000Z',
|
||||
'2025-04-18T00:00:00.000Z',
|
||||
'2025-04-21T00:00:00.000Z',
|
||||
'2025-05-01T00:00:00.000Z',
|
||||
'2025-05-29T00:00:00.000Z',
|
||||
'2025-06-09T00:00:00.000Z',
|
||||
'2025-10-03T00:00:00.000Z',
|
||||
'2025-10-31T00:00:00.000Z',
|
||||
'2025-12-25T00:00:00.000Z',
|
||||
'2025-12-26T00:00:00.000Z',
|
||||
'2026-01-01T00:00:00.000Z',
|
||||
'2026-04-03T00:00:00.000Z',
|
||||
'2026-04-06T00:00:00.000Z',
|
||||
'2026-05-01T00:00:00.000Z',
|
||||
'2026-05-14T00:00:00.000Z',
|
||||
'2026-05-25T00:00:00.000Z',
|
||||
'2026-10-03T00:00:00.000Z',
|
||||
'2026-10-31T00:00:00.000Z',
|
||||
'2026-12-25T00:00:00.000Z',
|
||||
'2026-12-26T00:00:00.000Z',
|
||||
'2027-01-01T00:00:00.000Z',
|
||||
'2027-03-26T00:00:00.000Z',
|
||||
'2027-03-29T00:00:00.000Z',
|
||||
'2027-05-01T00:00:00.000Z',
|
||||
'2027-05-06T00:00:00.000Z',
|
||||
'2027-05-17T00:00:00.000Z',
|
||||
'2027-10-03T00:00:00.000Z',
|
||||
'2027-10-31T00:00:00.000Z',
|
||||
'2027-12-25T00:00:00.000Z',
|
||||
'2027-12-26T00:00:00.000Z',
|
||||
'2028-01-01T00:00:00.000Z',
|
||||
'2028-04-14T00:00:00.000Z',
|
||||
'2028-04-17T00:00:00.000Z',
|
||||
'2028-05-01T00:00:00.000Z',
|
||||
'2028-05-25T00:00:00.000Z',
|
||||
'2028-06-05T00:00:00.000Z',
|
||||
'2028-10-03T00:00:00.000Z',
|
||||
'2028-10-31T00:00:00.000Z',
|
||||
'2028-12-25T00:00:00.000Z',
|
||||
'2028-12-26T00:00:00.000Z',
|
||||
'2029-01-01T00:00:00.000Z',
|
||||
'2029-03-30T00:00:00.000Z',
|
||||
'2029-04-02T00:00:00.000Z',
|
||||
'2029-05-01T00:00:00.000Z',
|
||||
'2029-05-10T00:00:00.000Z',
|
||||
'2029-05-21T00:00:00.000Z',
|
||||
'2029-10-03T00:00:00.000Z',
|
||||
'2029-10-31T00:00:00.000Z',
|
||||
'2029-12-25T00:00:00.000Z',
|
||||
'2029-12-26T00:00:00.000Z',
|
||||
'2030-01-01T00:00:00.000Z',
|
||||
'2030-04-19T00:00:00.000Z',
|
||||
'2030-04-22T00:00:00.000Z',
|
||||
'2030-05-01T00:00:00.000Z',
|
||||
'2030-05-30T00:00:00.000Z',
|
||||
'2030-06-10T00:00:00.000Z',
|
||||
'2030-10-03T00:00:00.000Z',
|
||||
'2030-10-31T00:00:00.000Z',
|
||||
'2030-12-25T00:00:00.000Z',
|
||||
'2030-12-26T00:00:00.000Z',
|
||||
];
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { PrismaModule } from 'src/core/database/prisma.module';
|
||||
import { CalculationService } from './calculation.service';
|
||||
import { PersonnelCostCalculationService } from './personnel-cost-calculation.service';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
providers: [CalculationService, PersonnelCostCalculationService],
|
||||
exports: [CalculationService],
|
||||
})
|
||||
export class UtilCalculationModule {}
|
||||
|
|
@ -75,7 +75,8 @@ export class ImporterService {
|
|||
private runTransformers(jobId: string): Observable<void> {
|
||||
return combineLatest([
|
||||
this.transformersService.transformAttendanceRegistrations(jobId),
|
||||
this.transformersService.transformEmployees(jobId),
|
||||
this.transformersService.transformDyflexisEmployees(jobId),
|
||||
this.transformersService.transformDispoliveEmployees(jobId),
|
||||
this.transformersService.transformPatients(jobId),
|
||||
this.transformersService.transformPlannedTimes(jobId),
|
||||
this.transformersService.transformWorkTimes(jobId),
|
||||
|
|
@ -96,6 +97,8 @@ export class ImporterService {
|
|||
private runLoaders(jobId: string): Observable<void> {
|
||||
return this.loadersService.loadTours().pipe(
|
||||
switchMap(() => this.loadersService.loadPatients()),
|
||||
switchMap(() => this.loadersService.loadDyflexisEmployees()),
|
||||
switchMap(() => this.loadersService.loadDispoliveEmployees()),
|
||||
switchMap(() => this.loadersService.loadAttendanceRegistrations()),
|
||||
switchMap(() => this.loadersService.loadPlannedTimes()),
|
||||
switchMap(() => this.loadersService.loadWorkTimes()),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
AttendanceRegistration,
|
||||
DyflexisEmployee,
|
||||
Employee,
|
||||
Patient,
|
||||
PlannedTime,
|
||||
Prisma,
|
||||
|
|
@ -115,6 +117,76 @@ export class FinalRepository {
|
|||
});
|
||||
}
|
||||
|
||||
findEmployeeDyflexis(): Promise<DyflexisEmployee[]> {
|
||||
return this.prisma.dyflexisEmployee.findMany({
|
||||
orderBy: {
|
||||
surname: 'asc',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
createEmployeeDyflexis(
|
||||
employeeDyflexisCreateInput: Prisma.DyflexisEmployeeCreateInput[],
|
||||
): Promise<Prisma.BatchPayload> {
|
||||
return this.prisma.dyflexisEmployee.createMany({
|
||||
data: employeeDyflexisCreateInput,
|
||||
skipDuplicates: true,
|
||||
});
|
||||
}
|
||||
|
||||
async updateEmployeeDyflexis(
|
||||
employeeDyflexisCreateInput: Prisma.DyflexisEmployeeCreateInput[],
|
||||
): Promise<Prisma.BatchPayload> {
|
||||
await this.deleteEmployeeDyflexis(
|
||||
employeeDyflexisCreateInput.map(({ id }) => id),
|
||||
);
|
||||
return this.createEmployeeDyflexis(employeeDyflexisCreateInput);
|
||||
}
|
||||
|
||||
deleteEmployeeDyflexis(ids: number[]): Promise<Prisma.BatchPayload> {
|
||||
return this.prisma.dyflexisEmployee.deleteMany({
|
||||
where: {
|
||||
id: {
|
||||
in: ids,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
findEmployeeDispolive(): Promise<Employee[]> {
|
||||
return this.prisma.employee.findMany({
|
||||
orderBy: {
|
||||
surname: 'asc',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
createEmployeeDispolive(
|
||||
employeeCreateInput: Prisma.EmployeeCreateInput[],
|
||||
): Promise<Prisma.BatchPayload> {
|
||||
return this.prisma.employee.createMany({
|
||||
data: employeeCreateInput,
|
||||
skipDuplicates: true,
|
||||
});
|
||||
}
|
||||
|
||||
async updateEmployeeDispolive(
|
||||
employeeCreateInput: Prisma.EmployeeCreateInput[],
|
||||
): Promise<Prisma.BatchPayload> {
|
||||
await this.deleteEmployeeDispolive(employeeCreateInput.map(({ id }) => id));
|
||||
return this.createEmployeeDispolive(employeeCreateInput);
|
||||
}
|
||||
|
||||
deleteEmployeeDispolive(ids: string[]): Promise<Prisma.BatchPayload> {
|
||||
return this.prisma.employee.deleteMany({
|
||||
where: {
|
||||
id: {
|
||||
in: ids,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
findAttendanceRegistrationsInInterval(
|
||||
interval: Interval,
|
||||
): Promise<AttendanceRegistration[]> {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import { from, map, of, switchMap } from 'rxjs';
|
|||
import { TransformersService } from '../feat-transformers/transformers.service';
|
||||
import { FinalRepository } from './final.repository';
|
||||
import { AttendanceRegistrationLoader } from './loaders/attendance-registration.loader';
|
||||
import { EmployeeDispoliveLoader } from './loaders/employee-dispolive';
|
||||
import { EmployeeDyflexisLoader } from './loaders/employee-dyflexis';
|
||||
import { PatientLoader } from './loaders/patient.loader';
|
||||
import { PlannedTimeLoader } from './loaders/planned-time.loader';
|
||||
import { TourLoader } from './loaders/tour.loader';
|
||||
|
|
@ -82,6 +84,56 @@ export class LoadersService {
|
|||
);
|
||||
}
|
||||
|
||||
loadDyflexisEmployees() {
|
||||
return from(this.transformersService.findDyflexisEmployees()).pipe(
|
||||
switchMap((stagingEmployees) =>
|
||||
from(this.repository.findEmployeeDyflexis()).pipe(
|
||||
map((finalEmployees) => ({
|
||||
final: finalEmployees,
|
||||
staging: stagingEmployees,
|
||||
})),
|
||||
),
|
||||
),
|
||||
map(({ staging, final }) =>
|
||||
EmployeeDyflexisLoader.determineLoadOperations(staging, final),
|
||||
),
|
||||
switchMap(({ deleted, updated, created }) =>
|
||||
from(
|
||||
Promise.all([
|
||||
this.repository.deleteEmployeeDyflexis(deleted),
|
||||
this.repository.updateEmployeeDyflexis(updated),
|
||||
this.repository.createEmployeeDyflexis(created),
|
||||
]),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
loadDispoliveEmployees() {
|
||||
return from(this.transformersService.findDispoliveEmployees()).pipe(
|
||||
switchMap((stagingEmployees) =>
|
||||
from(this.repository.findEmployeeDispolive()).pipe(
|
||||
map((finalEmployees) => ({
|
||||
final: finalEmployees,
|
||||
staging: stagingEmployees,
|
||||
})),
|
||||
),
|
||||
),
|
||||
map(({ staging, final }) =>
|
||||
EmployeeDispoliveLoader.determineLoadOperations(staging, final),
|
||||
),
|
||||
switchMap(({ deleted, updated, created }) =>
|
||||
from(
|
||||
Promise.all([
|
||||
this.repository.deleteEmployeeDispolive(deleted),
|
||||
this.repository.updateEmployeeDispolive(updated),
|
||||
this.repository.createEmployeeDispolive(created),
|
||||
]),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
loadAttendanceRegistrations() {
|
||||
return from(this.transformersService.findAttendanceRegistrations()).pipe(
|
||||
switchMap((stagingAttendanceRegistrations) => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
import { Employee, EmployeeDispoLiveStaging, Prisma } from '@prisma/client';
|
||||
|
||||
export class EmployeeDispoliveLoader {
|
||||
public static determineLoadOperations(
|
||||
stagingEmployees: EmployeeDispoLiveStaging[],
|
||||
finalEmployees: Employee[],
|
||||
): {
|
||||
deleted: string[];
|
||||
updated: Prisma.EmployeeCreateInput[];
|
||||
created: Prisma.EmployeeCreateInput[];
|
||||
} {
|
||||
const deletedEmployees = finalEmployees.filter(
|
||||
(finalEmployee) =>
|
||||
!stagingEmployees.find(
|
||||
(stagingEmployee) => stagingEmployee.id === finalEmployee.id,
|
||||
),
|
||||
);
|
||||
|
||||
const employeesToInsert = stagingEmployees.filter(
|
||||
({ id }) =>
|
||||
!!id &&
|
||||
!deletedEmployees.find((deletedEmployee) => deletedEmployee.id === id),
|
||||
);
|
||||
const newEmployees: Prisma.EmployeeCreateInput[] = [];
|
||||
const updatedEmployees: Prisma.EmployeeCreateInput[] = [];
|
||||
|
||||
for (const stagingEmployee of employeesToInsert) {
|
||||
const finalEmployee = finalEmployees.find(
|
||||
(finalEmployee) => finalEmployee.id === stagingEmployee.id,
|
||||
);
|
||||
if (finalEmployee) {
|
||||
updatedEmployees.push(this.employeeStagingToFinal(stagingEmployee));
|
||||
} else {
|
||||
newEmployees.push(this.employeeStagingToFinal(stagingEmployee));
|
||||
}
|
||||
}
|
||||
return {
|
||||
deleted: deletedEmployees.map(({ id }) => id),
|
||||
updated: updatedEmployees,
|
||||
created: newEmployees,
|
||||
};
|
||||
}
|
||||
|
||||
private static employeeStagingToFinal(
|
||||
employee: EmployeeDispoLiveStaging,
|
||||
): Prisma.EmployeeCreateInput {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
surname,
|
||||
personnelNumber,
|
||||
qualification,
|
||||
personTransportCertificate,
|
||||
} = employee;
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
surname,
|
||||
personnelNumber,
|
||||
qualification,
|
||||
personTransportCertificate,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import {
|
||||
DyflexisEmployee,
|
||||
EmployeeDyflexisStaging,
|
||||
Prisma,
|
||||
} from '@prisma/client';
|
||||
|
||||
export class EmployeeDyflexisLoader {
|
||||
public static determineLoadOperations(
|
||||
stagingEmployees: EmployeeDyflexisStaging[],
|
||||
finalEmployees: DyflexisEmployee[],
|
||||
): {
|
||||
deleted: number[];
|
||||
updated: Prisma.DyflexisEmployeeCreateInput[];
|
||||
created: Prisma.DyflexisEmployeeCreateInput[];
|
||||
} {
|
||||
const deletedEmployees = finalEmployees.filter(
|
||||
(finalEmployee) =>
|
||||
!stagingEmployees.find(
|
||||
(stagingEmployee) => stagingEmployee.id === finalEmployee.id,
|
||||
),
|
||||
);
|
||||
|
||||
const employeesToInsert = stagingEmployees.filter(
|
||||
({ id }) =>
|
||||
!!id &&
|
||||
!deletedEmployees.find((deletedEmployee) => deletedEmployee.id === id),
|
||||
);
|
||||
const newEmployees: Prisma.DyflexisEmployeeCreateInput[] = [];
|
||||
const updatedEmployees: Prisma.DyflexisEmployeeCreateInput[] = [];
|
||||
|
||||
for (const stagingEmployee of employeesToInsert) {
|
||||
const finalEmployee = finalEmployees.find(
|
||||
(finalEmployee) => finalEmployee.id === stagingEmployee.id,
|
||||
);
|
||||
if (finalEmployee) {
|
||||
updatedEmployees.push(this.employeeStagingToFinal(stagingEmployee));
|
||||
} else {
|
||||
newEmployees.push(this.employeeStagingToFinal(stagingEmployee));
|
||||
}
|
||||
}
|
||||
return {
|
||||
deleted: deletedEmployees.map(({ id }) => id),
|
||||
updated: updatedEmployees,
|
||||
created: newEmployees,
|
||||
};
|
||||
}
|
||||
|
||||
private static employeeStagingToFinal(
|
||||
employee: EmployeeDyflexisStaging,
|
||||
): Prisma.DyflexisEmployeeCreateInput {
|
||||
const {
|
||||
id,
|
||||
firstname,
|
||||
surname,
|
||||
contractStart,
|
||||
contractEnd,
|
||||
contractHoursWeek,
|
||||
contractSalaryHour,
|
||||
} = employee;
|
||||
|
||||
return {
|
||||
id,
|
||||
firstname,
|
||||
surname,
|
||||
contractStart,
|
||||
contractEnd,
|
||||
contractHoursWeek,
|
||||
contractSalaryHour,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import { ApprovalState } from '@prisma/client';
|
||||
import { TicketFilter } from './ticket-filter';
|
||||
|
||||
export class TicketFilterArgs implements TicketFilter {
|
||||
errors: string[];
|
||||
approvalStates: ApprovalState[];
|
||||
documentInfo: ('approval' | 'transport_document')[];
|
||||
}
|
||||
|
||||
export interface ApprovalInfoArgs {
|
||||
isNeeded?: boolean;
|
||||
whoRequested?: string;
|
||||
answeredFrom?: string;
|
||||
location?: string;
|
||||
}
|
||||
|
||||
export interface DocumentInfoArgs {
|
||||
approval: ApprovalInfoArgs;
|
||||
tdLocation?: string;
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import { ApprovalState, Prisma } from '@prisma/client';
|
||||
|
||||
export interface TicketFilter {
|
||||
errors?: string[];
|
||||
approvalStates?: ApprovalState[];
|
||||
documentInfo?: ('approval' | 'transport_document')[];
|
||||
}
|
||||
|
||||
export function createPrismaQueryFromTicketFilters(
|
||||
filter: TicketFilter,
|
||||
): Prisma.TicketWhereInput[] {
|
||||
const conditions: Prisma.TicketWhereInput[] = [];
|
||||
|
||||
Object.entries(filter ?? {}).forEach(([property, args]) => {
|
||||
switch (property) {
|
||||
case 'errors':
|
||||
if (args.length > 0)
|
||||
conditions.push({
|
||||
errors: {
|
||||
hasSome: [...args],
|
||||
},
|
||||
});
|
||||
return;
|
||||
case 'approvalStates':
|
||||
if (args.length > 0)
|
||||
conditions.push({
|
||||
approvalState: {
|
||||
in: [...args],
|
||||
},
|
||||
});
|
||||
return;
|
||||
case 'documentInfo':
|
||||
if (args.includes('approval')) {
|
||||
conditions.push({
|
||||
OR: [
|
||||
{
|
||||
documentInfo: {
|
||||
path: ['approval', 'location'],
|
||||
not: 'company',
|
||||
},
|
||||
},
|
||||
{
|
||||
documentInfo: {
|
||||
path: ['approval', 'location'],
|
||||
equals: Prisma.JsonNull,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (args.includes('td_company')) {
|
||||
conditions.push({
|
||||
documentInfo: {
|
||||
path: ['tdLocation'],
|
||||
equals: 'company',
|
||||
},
|
||||
});
|
||||
}
|
||||
if (args.includes('td_customer')) {
|
||||
conditions.push({
|
||||
documentInfo: {
|
||||
path: ['tdLocation'],
|
||||
equals: 'customer',
|
||||
},
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
});
|
||||
return conditions;
|
||||
}
|
||||
|
|
@ -0,0 +1,240 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import {
|
||||
ApprovalState,
|
||||
Ticket,
|
||||
TicketValidationState,
|
||||
Tour,
|
||||
} from '@prisma/client';
|
||||
import {
|
||||
ValidationResult,
|
||||
validatePatient,
|
||||
validatePatientDataOnTours,
|
||||
validateTour,
|
||||
} from './ticket-validation.functions';
|
||||
import { TicketRepository } from './ticket.repository';
|
||||
import { DocumentInfoArgs } from './ticket-arg.types';
|
||||
import { Interval } from 'date-fns';
|
||||
|
||||
@Injectable()
|
||||
export class TicketLifecycleService {
|
||||
private readonly logger = new Logger(TicketLifecycleService.name);
|
||||
|
||||
constructor(private readonly repository: TicketRepository) {}
|
||||
|
||||
async createTickets(interval: Interval): Promise<void> {
|
||||
const tours = await this.repository.findTours(interval);
|
||||
|
||||
for (const tour of tours) {
|
||||
if (tour.ticketId) {
|
||||
continue;
|
||||
}
|
||||
const ticketId = await this.repository.searchTicketIDForTour(tour);
|
||||
if (ticketId) {
|
||||
await this.repository.addToursToTicket(ticketId, [tour]);
|
||||
} else {
|
||||
await this.repository.createTicket([tour]);
|
||||
}
|
||||
}
|
||||
|
||||
await this.validateCreatedTickets();
|
||||
// await this.archiveClosedTickets();
|
||||
}
|
||||
|
||||
async validateDocumentInfoUpdate(
|
||||
ticketId: string,
|
||||
documentInfo: DocumentInfoArgs,
|
||||
) {
|
||||
const ticket = await this.repository.findById(ticketId);
|
||||
|
||||
switch (ticket.currentState) {
|
||||
case TicketValidationState.USER_INTERACTION_REQUIRED:
|
||||
const approvalStateUpdate = documentInfo.approval.isNeeded
|
||||
? ApprovalState.REQUESTED
|
||||
: ApprovalState.FREE;
|
||||
|
||||
let stateUpdate: TicketValidationState =
|
||||
TicketValidationState.USER_INTERACTION_REQUIRED;
|
||||
if (
|
||||
documentInfo.tdLocation === 'company' &&
|
||||
approvalStateUpdate === ApprovalState.FREE
|
||||
)
|
||||
stateUpdate = TicketValidationState.ARCHIVED;
|
||||
else if (documentInfo.tdLocation === 'uncertain')
|
||||
stateUpdate = TicketValidationState.TD_UNCERTAIN;
|
||||
else stateUpdate = TicketValidationState.DOCUMENTS_MISSING;
|
||||
|
||||
return this.repository.updateTicket(ticketId, {
|
||||
documentInfo,
|
||||
approvalState: approvalStateUpdate,
|
||||
state: stateUpdate,
|
||||
});
|
||||
|
||||
case TicketValidationState.TD_UNCERTAIN:
|
||||
return this.repository.updateTicket(ticketId, {
|
||||
documentInfo,
|
||||
state:
|
||||
ticket.approvalState === ApprovalState.FREE &&
|
||||
documentInfo.tdLocation === 'company'
|
||||
? TicketValidationState.ARCHIVED
|
||||
: TicketValidationState.DOCUMENTS_MISSING,
|
||||
});
|
||||
case TicketValidationState.DOCUMENTS_MISSING:
|
||||
return this.repository.updateTicket(ticketId, {
|
||||
documentInfo,
|
||||
...((ticket.approvalState === ApprovalState.FREE ||
|
||||
documentInfo.approval.location === 'company') &&
|
||||
documentInfo.tdLocation === 'company'
|
||||
? { state: TicketValidationState.ARCHIVED }
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async validateCreatedTickets(): Promise<void> {
|
||||
const tickets = await this.repository.findAllCreated();
|
||||
this.logger.debug(`Found ${tickets.length} with state CREATED`);
|
||||
|
||||
for (const ticket of tickets) {
|
||||
const {
|
||||
result: isValid,
|
||||
errors,
|
||||
notes,
|
||||
} = await this.validateCreatedTicket(ticket);
|
||||
|
||||
if (isValid) {
|
||||
const { result: approvalResult } = await this.isApprovalNeeded(ticket);
|
||||
if (approvalResult) {
|
||||
await this.repository.updateTicket(ticket.id, {
|
||||
errors,
|
||||
notes,
|
||||
state: TicketValidationState.USER_INTERACTION_REQUIRED,
|
||||
approvalState: ticket.tours.every(({ patientId }) => !patientId)
|
||||
? ApprovalState.UNKNOWN
|
||||
: ApprovalState.REQUIRED,
|
||||
});
|
||||
} else {
|
||||
await this.repository.updateTicket(ticket.id, {
|
||||
errors,
|
||||
notes,
|
||||
state: TicketValidationState.USER_INTERACTION_REQUIRED,
|
||||
approvalState: ApprovalState.FREE,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await this.repository.setActionablesOnTicket(ticket.id, errors, notes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async archiveClosedTickets() {
|
||||
const closedTickets = await this.repository.findAllClosed();
|
||||
for (const closedTicket of closedTickets) {
|
||||
await this.repository.setStateOfTicket(
|
||||
closedTicket.id,
|
||||
TicketValidationState.ARCHIVED,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async validateCreatedTicket(
|
||||
ticket: Ticket & { tours: Tour[] },
|
||||
): Promise<ValidationResult> {
|
||||
const patientIds = [
|
||||
...new Set(ticket.tours.map(({ patientId }) => patientId)),
|
||||
];
|
||||
if (patientIds.length > 1) {
|
||||
return {
|
||||
result: false,
|
||||
errors: ['Ticket: Die Touren umfassen mehr als einen Patienten'],
|
||||
notes: [],
|
||||
};
|
||||
}
|
||||
const [patientId] = patientIds;
|
||||
if (!patientId && ticket.tours.at(0)?.type !== 'Spontan') {
|
||||
return {
|
||||
result: false,
|
||||
errors: [],
|
||||
notes: ['Patient: Der Patient ist nicht in DispoLive hinterlegt'],
|
||||
};
|
||||
}
|
||||
|
||||
let patientResult: ValidationResult;
|
||||
if (!patientId && ticket.tours.at(0)?.type === 'Spontan') {
|
||||
patientResult = validatePatientDataOnTours(ticket.tours);
|
||||
} else {
|
||||
try {
|
||||
const patient = await this.repository.findPatientById(patientId);
|
||||
patientResult = validatePatient(patient);
|
||||
} catch {
|
||||
patientResult = validatePatientDataOnTours(ticket.tours);
|
||||
}
|
||||
}
|
||||
const tourResults = ticket.tours.map((tour) => validateTour(tour));
|
||||
|
||||
return [patientResult, ...tourResults].reduce(
|
||||
(result, results) => ({
|
||||
result: result.result && results.result,
|
||||
errors: [...result.errors, ...results.errors],
|
||||
notes: [...result.notes, ...results.notes],
|
||||
}),
|
||||
{ result: true, errors: [], notes: [] } as ValidationResult,
|
||||
);
|
||||
}
|
||||
|
||||
private async isApprovalNeeded(
|
||||
ticket: Ticket & { tours: Tour[] },
|
||||
): Promise<ValidationResult> {
|
||||
const approvalRequirements = await Promise.all(
|
||||
ticket.tours.map((tour) => this.checkTourApprovalRequirement(tour)),
|
||||
);
|
||||
const isApprovalRequired = approvalRequirements.reduce(
|
||||
(result, requirement) => result || requirement,
|
||||
false,
|
||||
);
|
||||
|
||||
return {
|
||||
result: isApprovalRequired,
|
||||
errors: [],
|
||||
notes:
|
||||
isApprovalRequired && ticket.tours.every(({ patientId }) => !patientId)
|
||||
? [
|
||||
'Patient nicht in Stammdaten: Prüfen Sie ob eine Genehmigung benötigt wird',
|
||||
]
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
private async checkTourApprovalRequirement(tour: Tour): Promise<boolean> {
|
||||
if (
|
||||
[
|
||||
'Einweisung',
|
||||
'Entlassung',
|
||||
'Entlassung Vorbestellung',
|
||||
'Konsil KH - Dia',
|
||||
'Privatfahrt Rechnung',
|
||||
'Tagesklinik',
|
||||
'Verlegung',
|
||||
].includes(tour.category) ||
|
||||
tour.healthInsurance === 'KOSTENTRÄGER PATIENT'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
tour.patientId &&
|
||||
['BTW', 'TSW'].includes(tour.ordinanceType) &&
|
||||
tour.category !== 'Dialyse'
|
||||
) {
|
||||
const patient = await this.repository.findPatientById(tour.patientId);
|
||||
if (
|
||||
['3', '4', '5'].includes(patient.careDegree) &&
|
||||
(['aG', 'Bi', 'H'].includes(patient.disabilityMark1) ||
|
||||
['aG', 'Bi', 'H'].includes(patient.disabilityMark2))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
export enum TicketSystemEventTypes {
|
||||
Validated = 'ticket-system.validated',
|
||||
}
|
||||
|
||||
export class TicketSystemValidatedEvent {
|
||||
constructor(public readonly count: number) {}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { PrismaModule } from 'src/core/database/prisma.module';
|
||||
import { TicketLifecycleService } from './ticket-lifecycle.service';
|
||||
import { TicketRepository } from './ticket.repository';
|
||||
import { TicketService } from './tickets.service';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
providers: [TicketRepository, TicketService, TicketLifecycleService],
|
||||
exports: [TicketService],
|
||||
})
|
||||
export class TicketSystemModule {}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
import { Patient, Tour } from '@prisma/client';
|
||||
|
||||
export interface ValidationResult {
|
||||
result: boolean;
|
||||
notes: string[];
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
export function validatePatient(patient: Patient): ValidationResult {
|
||||
const notes: string[] = [];
|
||||
const errors: string[] = [];
|
||||
let isValid = true;
|
||||
|
||||
if (!patient?.name) {
|
||||
isValid = false;
|
||||
errors.push('Patient: Vorname ist nicht gefüllt');
|
||||
}
|
||||
if (!patient?.surname) {
|
||||
isValid = false;
|
||||
errors.push('Patient: Nachname ist nicht gefüllt');
|
||||
}
|
||||
if (!patient?.birthday) {
|
||||
isValid = false;
|
||||
notes.push('Patient: Geburtstag ist nicht gefüllt');
|
||||
}
|
||||
if (!patient?.healthinsurance) {
|
||||
isValid = false;
|
||||
errors.push('Patient: Krankenkasse ist nicht gefüllt');
|
||||
}
|
||||
if (!patient?.careDegree) {
|
||||
notes.push('Patient: Pflegegrad ist nicht gefüllt');
|
||||
}
|
||||
if (!patient?.disabilityMark1) {
|
||||
notes.push('Patient: Merkzeichen 1 ist nicht gefüllt');
|
||||
}
|
||||
if (!patient?.disabilityMark2) {
|
||||
notes.push('Patient: Merkzeichen 2 ist nicht gefüllt');
|
||||
}
|
||||
|
||||
return {
|
||||
result: isValid,
|
||||
errors,
|
||||
notes,
|
||||
};
|
||||
}
|
||||
|
||||
export function validatePatientDataOnTours(tours: Tour[]): ValidationResult {
|
||||
const errors: string[] = [];
|
||||
let isValid = true;
|
||||
|
||||
for (const tour of tours) {
|
||||
if (!tour.patientName) {
|
||||
isValid = false;
|
||||
errors.push(
|
||||
`Tour ${tour.operationId}: Patient Vorname ist nicht gefüllt`,
|
||||
);
|
||||
}
|
||||
if (!tour.patientSurname) {
|
||||
isValid = false;
|
||||
errors.push(
|
||||
`Tour ${tour.operationId}: Patient Nachname ist nicht gefüllt`,
|
||||
);
|
||||
}
|
||||
if (!tour.healthInsurance) {
|
||||
isValid = false;
|
||||
errors.push(
|
||||
`Tour ${tour.operationId}: Patient Krankenversicherung ist nicht gefüllt`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
result: isValid,
|
||||
errors,
|
||||
notes: [],
|
||||
};
|
||||
}
|
||||
|
||||
export function validateTour(tour: Tour): ValidationResult {
|
||||
const errors: string[] = [];
|
||||
let isValid = true;
|
||||
|
||||
if (!tour.ordinanceType) {
|
||||
isValid = false;
|
||||
errors.push(`Tour ${tour.operationId}: Verordnungsart ist nicht gefüllt`);
|
||||
}
|
||||
if (!tour.transportType) {
|
||||
isValid = false;
|
||||
errors.push(`Tour ${tour.operationId}: Transportart ist nicht gefüllt`);
|
||||
}
|
||||
if (!tour.category) {
|
||||
isValid = false;
|
||||
errors.push(`Tour ${tour.operationId}: Kategorie ist nicht gefüllt`);
|
||||
}
|
||||
if (!tour.startDate) {
|
||||
isValid = false;
|
||||
errors.push(`Tour ${tour.operationId}: Startzeit ist nicht gefüllt`);
|
||||
}
|
||||
if (!tour.startStreet || !tour.startZip || !tour.startCity) {
|
||||
isValid = false;
|
||||
errors.push(`Tour ${tour.operationId}: Start Adresse ist nicht gefüllt`);
|
||||
}
|
||||
if (!tour.targetStreet || !tour.targetZip || !tour.targetCity) {
|
||||
isValid = false;
|
||||
errors.push(`Tour ${tour.operationId}: Ziel Adresse ist nicht gefüllt`);
|
||||
}
|
||||
if (tour.occupiedKm > 9990) {
|
||||
isValid = false;
|
||||
errors.push(`Tour ${tour.operationId}: Besetzte Kilometer sind zu hoch`);
|
||||
}
|
||||
|
||||
return {
|
||||
result: isValid,
|
||||
errors,
|
||||
notes: [],
|
||||
};
|
||||
}
|
||||
|
||||
export function validateTransport(tour: Tour): ValidationResult {
|
||||
const errors: string[] = [];
|
||||
let isValid = true;
|
||||
|
||||
if (!tour.carName) {
|
||||
isValid = false;
|
||||
errors.push(`Tour ${tour.operationId}: Fahrzeugname ist nicht angegeben`);
|
||||
}
|
||||
|
||||
if (!tour.driverName) {
|
||||
isValid = false;
|
||||
errors.push(`Tour ${tour.operationId}: Fahrer ist nicht angegeben`);
|
||||
}
|
||||
|
||||
if (!tour.codriverName) {
|
||||
isValid = false;
|
||||
errors.push(`Tour ${tour.operationId}: Beifahrer ist nicht angegeben`);
|
||||
}
|
||||
|
||||
if (!tour.startBegin) {
|
||||
isValid = false;
|
||||
errors.push(`Tour ${tour.operationId}: Anfahrt-Stempel fehlt`);
|
||||
}
|
||||
|
||||
if (!tour.empty) {
|
||||
isValid = false;
|
||||
errors.push(`Tour ${tour.operationId}: Frei-Stempel fehlt`);
|
||||
}
|
||||
|
||||
return {
|
||||
result: isValid,
|
||||
errors,
|
||||
notes: [],
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,499 @@
|
|||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { Patient, Prisma, TicketValidationState, Tour } from '@prisma/client';
|
||||
import { Interval, endOfDay, startOfDay } from 'date-fns';
|
||||
import { ExpressionBuilder, Kysely } from 'kysely';
|
||||
import { jsonArrayFrom } from 'kysely/helpers/postgres';
|
||||
|
||||
import { KYSELY_CLIENT_PROVIDER_TOKEN } from 'src/core/database/database.module';
|
||||
import { PrismaService } from 'src/core/database/prisma.service';
|
||||
import { ApprovalState, DB } from 'src/core/database/types';
|
||||
import { DocumentInfoArgs, TicketFilterArgs } from './ticket-arg.types';
|
||||
import { createPrismaQueryFromTicketFilters } from './ticket-filter';
|
||||
|
||||
function withTours(eb: ExpressionBuilder<DB, 'Ticket'>) {
|
||||
return jsonArrayFrom(
|
||||
eb
|
||||
.selectFrom('Tour')
|
||||
.selectAll('Tour')
|
||||
.whereRef('Tour.ticketId', '=', 'Ticket.id')
|
||||
.orderBy('Tour.operationId'),
|
||||
).as('tours');
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class TicketRepository {
|
||||
private logger = new Logger(TicketRepository.name);
|
||||
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
@Inject(KYSELY_CLIENT_PROVIDER_TOKEN) private readonly db: Kysely<DB>,
|
||||
) {}
|
||||
|
||||
async createTicket(tours: Tour[]) {
|
||||
const ticket = await this.db
|
||||
.insertInto('Ticket')
|
||||
.values({
|
||||
currentState: TicketValidationState.CREATED,
|
||||
})
|
||||
.returningAll()
|
||||
.executeTakeFirst();
|
||||
|
||||
await this.db
|
||||
.insertInto('TicketStateHistory')
|
||||
.values({
|
||||
state: TicketValidationState.CREATED,
|
||||
ticketId: ticket.id,
|
||||
})
|
||||
.executeTakeFirst();
|
||||
|
||||
await this.db
|
||||
.updateTable('Tour')
|
||||
.set({
|
||||
ticketId: ticket.id,
|
||||
})
|
||||
.where(
|
||||
'id',
|
||||
'in',
|
||||
tours.map(({ id }) => id),
|
||||
)
|
||||
.execute();
|
||||
|
||||
return ticket;
|
||||
}
|
||||
|
||||
async addToursToTicket(ticketId: string, tours: Tour[]) {
|
||||
await this.db
|
||||
.updateTable('Tour')
|
||||
.set({
|
||||
ticketId: ticketId,
|
||||
})
|
||||
.where(
|
||||
'id',
|
||||
'in',
|
||||
tours.map(({ id }) => id),
|
||||
)
|
||||
.execute();
|
||||
|
||||
return this.db
|
||||
.selectFrom('Ticket')
|
||||
.where('id', '=', ticketId)
|
||||
.selectAll()
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
setActionablesOnTicket(ticketId: string, errors: string[], notes: string[]) {
|
||||
return this.db
|
||||
.updateTable('Ticket')
|
||||
.set({ errors, notes })
|
||||
.where('id', '=', ticketId)
|
||||
.returningAll()
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async setStateOfTicket(ticketId: string, state: TicketValidationState) {
|
||||
await this.db
|
||||
.updateTable('Ticket')
|
||||
.set({
|
||||
currentState: state,
|
||||
})
|
||||
.where('Ticket.id', '=', ticketId)
|
||||
.execute();
|
||||
|
||||
return this.db
|
||||
.insertInto('TicketStateHistory')
|
||||
.values({ state, ticketId })
|
||||
.returningAll()
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async updateTicket(
|
||||
ticketId: string,
|
||||
data: {
|
||||
state?: TicketValidationState;
|
||||
errors?: string[];
|
||||
notes?: string[];
|
||||
isUrgent?: boolean;
|
||||
urgency?: Date;
|
||||
approvalState?: ApprovalState;
|
||||
documentInfo?: DocumentInfoArgs;
|
||||
approvalPath?: string;
|
||||
},
|
||||
) {
|
||||
const { state, ...ticketData } = data;
|
||||
|
||||
await this.db
|
||||
.updateTable('Ticket')
|
||||
.set({ ...ticketData })
|
||||
.where('id', '=', ticketId)
|
||||
.execute();
|
||||
|
||||
if (state) {
|
||||
await this.setStateOfTicket(ticketId, state);
|
||||
}
|
||||
|
||||
return this.db
|
||||
.selectFrom('Ticket')
|
||||
.where('id', '=', ticketId)
|
||||
.selectAll('Ticket')
|
||||
.select((eb) => withTours(eb))
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
findById(id: string) {
|
||||
return this.prisma.ticket.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
include: {
|
||||
tours: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
findAll() {
|
||||
return this.prisma.ticket.findMany({
|
||||
where: {
|
||||
state: {
|
||||
none: {
|
||||
state: TicketValidationState.ARCHIVED,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
tours: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
findAllCreated() {
|
||||
return this.prisma.ticket.findMany({
|
||||
where: {
|
||||
state: {
|
||||
none: {
|
||||
state: TicketValidationState.USER_INTERACTION_REQUIRED,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
tours: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
findAllClosed() {
|
||||
return this.prisma.ticket.findMany({
|
||||
where: {
|
||||
tours: {
|
||||
every: {
|
||||
check: {
|
||||
not: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
search(query: string) {
|
||||
return this.prisma.ticket.findMany({
|
||||
where: {
|
||||
state: {
|
||||
none: {
|
||||
state: TicketValidationState.ARCHIVED,
|
||||
},
|
||||
},
|
||||
tours: {
|
||||
some: {
|
||||
OR: [
|
||||
...query.split(' ').flatMap((word) => [
|
||||
{
|
||||
patientName: {
|
||||
contains: word,
|
||||
mode: Prisma.QueryMode.insensitive,
|
||||
},
|
||||
},
|
||||
{
|
||||
patientSurname: {
|
||||
contains: word,
|
||||
mode: Prisma.QueryMode.insensitive,
|
||||
},
|
||||
},
|
||||
{
|
||||
operationId: {
|
||||
contains: word,
|
||||
mode: Prisma.QueryMode.insensitive,
|
||||
},
|
||||
},
|
||||
]),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
take: 5,
|
||||
include: {
|
||||
tours: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findByState(
|
||||
state: TicketValidationState,
|
||||
filter: TicketFilterArgs,
|
||||
take?: number,
|
||||
skip?: number,
|
||||
) {
|
||||
const query: Prisma.TicketFindManyArgs = {
|
||||
...{
|
||||
...(skip ? { skip, take: 10 } : {}),
|
||||
...(take ? { take } : {}),
|
||||
...(take && !skip ? { skip: 0 } : {}),
|
||||
},
|
||||
where: {
|
||||
AND: createPrismaQueryFromTicketFilters(filter),
|
||||
currentState: state,
|
||||
tours: {
|
||||
some: {
|
||||
check: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
};
|
||||
|
||||
const [tickets, count] = await this.prisma.$transaction([
|
||||
this.prisma.ticket.findMany({ ...query }),
|
||||
this.prisma.ticket.count({ where: query.where }),
|
||||
]);
|
||||
return {
|
||||
tickets,
|
||||
total: count,
|
||||
};
|
||||
}
|
||||
|
||||
findPatientById(patientId: string): Promise<Patient> {
|
||||
return this.prisma.patient.findUnique({ where: { id: patientId } });
|
||||
}
|
||||
|
||||
findStatesByTicketId(
|
||||
ticketId: string,
|
||||
): Promise<{ state: TicketValidationState }[]> {
|
||||
return this.prisma.ticketStateHistory.findMany({
|
||||
where: {
|
||||
ticketId,
|
||||
},
|
||||
select: {
|
||||
state: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
findToursByTicketId(ticketId: string): Promise<Tour[]> {
|
||||
return this.prisma.tour.findMany({
|
||||
where: {
|
||||
ticketId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async searchTicketIDForTour(tour: Tour): Promise<string | undefined> {
|
||||
const tours = await Promise.all([
|
||||
this.findFirstRecurringTour(tour),
|
||||
this.findTourOnSameDayWithOppositeDirection(tour),
|
||||
]);
|
||||
const firstFoundTour = tours.filter((tour) => !!tour)[0];
|
||||
if (firstFoundTour) {
|
||||
const ticket = await this.prisma.ticket.findFirst({
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
where: {
|
||||
tours: {
|
||||
some: {
|
||||
id: firstFoundTour.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return ticket?.id;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
findFirstRecurringTour(tour: Tour): Promise<Tour | null> {
|
||||
const institutionStreet =
|
||||
tour.direction === 'Hinfahrt' ? tour.targetStreet : tour.startStreet;
|
||||
return this.prisma.tour.findFirst({
|
||||
where: {
|
||||
type: 'Serie',
|
||||
category: tour.category,
|
||||
ordinanceType: tour.ordinanceType,
|
||||
patientId: tour.patientId,
|
||||
patientName: tour.patientName,
|
||||
patientSurname: tour.patientSurname,
|
||||
patientStreet: tour.patientStreet,
|
||||
patientCity: tour.patientCity,
|
||||
patientZip: tour.patientZip,
|
||||
OR: [
|
||||
{ targetStreet: institutionStreet },
|
||||
{ startStreet: institutionStreet },
|
||||
],
|
||||
rangeEndDate: {
|
||||
gte: tour.startDate,
|
||||
},
|
||||
ticketId: {
|
||||
not: null,
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
startDate: 'desc',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
findTourOnSameDayWithOppositeDirection(tour: Tour): Promise<Tour | null> {
|
||||
return this.prisma.tour.findFirst({
|
||||
where: {
|
||||
AND: [
|
||||
{
|
||||
startDate: {
|
||||
gt: startOfDay(new Date(tour.startDate)),
|
||||
},
|
||||
},
|
||||
{
|
||||
startDate: {
|
||||
lt: endOfDay(new Date(tour.startDate)),
|
||||
},
|
||||
},
|
||||
{
|
||||
startStreet: tour.targetStreet,
|
||||
},
|
||||
{
|
||||
targetStreet: tour.startStreet,
|
||||
},
|
||||
{
|
||||
direction: tour.direction === 'Hinfahrt' ? 'Rückfahrt' : 'Hinfahrt',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
findToursWithTicket(interval: Interval): Promise<Tour[]> {
|
||||
return this.prisma.tour.findMany({
|
||||
where: {
|
||||
AND: [
|
||||
{
|
||||
ticketId: {
|
||||
not: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
startDate: {
|
||||
gte: new Date(interval.start),
|
||||
},
|
||||
},
|
||||
{
|
||||
startDate: {
|
||||
lt: new Date(interval.end),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
removeTourFromTicket(tourId: string): Promise<Tour> {
|
||||
return this.prisma.tour.update({
|
||||
where: {
|
||||
id: tourId,
|
||||
},
|
||||
data: {
|
||||
Ticket: {
|
||||
disconnect: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
findTours(interval: Interval): Promise<Tour[]> {
|
||||
return this.prisma.tour.findMany({
|
||||
where: {
|
||||
AND: [
|
||||
{
|
||||
startDate: {
|
||||
gte: new Date(interval.start),
|
||||
},
|
||||
},
|
||||
{
|
||||
startDate: {
|
||||
lt: new Date(interval.end),
|
||||
},
|
||||
},
|
||||
{
|
||||
OR: [
|
||||
{
|
||||
carName: null,
|
||||
},
|
||||
{
|
||||
AND: [
|
||||
{
|
||||
carName: {
|
||||
not: 'Storno',
|
||||
},
|
||||
},
|
||||
{
|
||||
carName: {
|
||||
not: 'Löschen',
|
||||
},
|
||||
},
|
||||
{
|
||||
carName: {
|
||||
not: 'Abgabe',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
check: {
|
||||
not: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
deletedAt: null,
|
||||
},
|
||||
{
|
||||
ticketId: null,
|
||||
},
|
||||
{
|
||||
AND: [
|
||||
{
|
||||
patientId: {
|
||||
// Pause
|
||||
not: 'b7424477-a4b2-42f7-b7e7-a4bef24f6979',
|
||||
},
|
||||
},
|
||||
{
|
||||
patientId: {
|
||||
// Feierabend
|
||||
not: '3554e1a9-9c07-45e5-b53c-692a77e4fa9a',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async deleteEmptyTickets(): Promise<Prisma.BatchPayload> {
|
||||
return this.prisma.ticket.deleteMany({
|
||||
where: {
|
||||
tours: {
|
||||
none: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { Ticket, TicketValidationState, Tour } from '@prisma/client';
|
||||
import { Interval } from 'date-fns';
|
||||
import { DocumentInfoArgs } from './ticket-arg.types';
|
||||
import { TicketLifecycleService } from './ticket-lifecycle.service';
|
||||
import {
|
||||
TicketSystemEventTypes,
|
||||
TicketSystemValidatedEvent,
|
||||
} from './ticket-system.events';
|
||||
import { TicketRepository } from './ticket.repository';
|
||||
|
||||
@Injectable()
|
||||
export class TicketService {
|
||||
constructor(
|
||||
private readonly repository: TicketRepository,
|
||||
private readonly lifecycle: TicketLifecycleService,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
) {}
|
||||
|
||||
async createTickets(interval: Interval): Promise<void> {
|
||||
await this.lifecycle.createTickets(interval);
|
||||
|
||||
this.eventEmitter.emit(
|
||||
TicketSystemEventTypes.Validated,
|
||||
new TicketSystemValidatedEvent(0),
|
||||
);
|
||||
}
|
||||
|
||||
async updateDocumentInfo(ticketId: string, documentInfo: DocumentInfoArgs) {
|
||||
return this.lifecycle.validateDocumentInfoUpdate(ticketId, documentInfo);
|
||||
}
|
||||
|
||||
findById(id: string): Promise<Ticket> {
|
||||
return this.repository.findById(id);
|
||||
}
|
||||
|
||||
findAll(): Promise<Ticket[]> {
|
||||
return this.repository.findAll() as Promise<Ticket[]>;
|
||||
}
|
||||
|
||||
searchTickets(query: string): Promise<Ticket[]> {
|
||||
return this.repository.search(query);
|
||||
}
|
||||
|
||||
findToursByTicketId(ticketId: string): Promise<Tour[]> {
|
||||
return this.repository.findToursByTicketId(ticketId);
|
||||
}
|
||||
|
||||
findStatesByTicketId(ticketId: string): Promise<TicketValidationState[]> {
|
||||
return this.repository
|
||||
.findStatesByTicketId(ticketId)
|
||||
.then((states) => states.map(({ state }) => state));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,54 +1,55 @@
|
|||
// import { CalculationService } from '@dks/avicenna/api-util-calculation';
|
||||
// import { Injectable } from '@nestjs/common';
|
||||
// import { Tour } from '@prisma/client';
|
||||
// import { chunk } from 'src/utils';
|
||||
// import { TourPreStaging } from '../staging.interface';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Tour } from '@prisma/client';
|
||||
|
||||
// @Injectable()
|
||||
// export class ToursCalculationAdapater {
|
||||
// constructor(private readonly calculationService: CalculationService) {}
|
||||
import { CalculationService } from 'src/modules/feat-calculation/calculation.service';
|
||||
import { chunk } from 'src/utils';
|
||||
import { TourPreStaging } from '../staging.interface';
|
||||
|
||||
// public async addRevenueAndConsumptionCostsToTours(
|
||||
// tours: TourPreStaging[],
|
||||
// ): Promise<TourPreStaging[]> {
|
||||
// const resultTours = [];
|
||||
// for (const tourChunk of chunk(tours, 5)) {
|
||||
// resultTours.push(
|
||||
// await Promise.all(
|
||||
// tourChunk.map((tour) =>
|
||||
// this.addRevenueAndConsumptionCostsToTour(tour),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// return resultTours.flat();
|
||||
// }
|
||||
@Injectable()
|
||||
export class ToursCalculationAdapater {
|
||||
constructor(private readonly calculationService: CalculationService) {}
|
||||
|
||||
// public async addRevenueAndConsumptionCostsToTour(
|
||||
// tour: TourPreStaging,
|
||||
// ): Promise<TourPreStaging> {
|
||||
// if (!tour?.ordinanceType) {
|
||||
// return tour;
|
||||
// }
|
||||
// const revenue = await this.calculationService.calculateTourRevenue(
|
||||
// tour as unknown as Tour,
|
||||
// );
|
||||
// const revenueDeviation =
|
||||
// tour.revenueDispoLive > 0
|
||||
// ? Math.floor((revenue - tour.revenueDispoLive) * 100) / 100
|
||||
// : 0;
|
||||
public async addRevenueAndConsumptionCostsToTours(
|
||||
tours: TourPreStaging[],
|
||||
): Promise<TourPreStaging[]> {
|
||||
const resultTours = [];
|
||||
for (const tourChunk of chunk(tours, 5)) {
|
||||
resultTours.push(
|
||||
await Promise.all(
|
||||
tourChunk.map((tour) =>
|
||||
this.addRevenueAndConsumptionCostsToTour(tour),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return resultTours.flat();
|
||||
}
|
||||
|
||||
// const { consumptionCostsPerTour } =
|
||||
// this.calculationService.getBillingInformationByOrdinanceType(
|
||||
// tour.ordinanceType.match(/([A-Z])\w+/).at(0),
|
||||
// tour.startDate,
|
||||
// );
|
||||
public async addRevenueAndConsumptionCostsToTour(
|
||||
tour: TourPreStaging,
|
||||
): Promise<TourPreStaging> {
|
||||
if (!tour?.ordinanceType) {
|
||||
return tour;
|
||||
}
|
||||
const revenue = await this.calculationService.calculateTourRevenue(
|
||||
tour as unknown as Tour,
|
||||
);
|
||||
const revenueDeviation =
|
||||
tour.revenueDispoLive > 0
|
||||
? Math.floor((revenue - tour.revenueDispoLive) * 100) / 100
|
||||
: 0;
|
||||
|
||||
// return {
|
||||
// ...tour,
|
||||
// revenue,
|
||||
// revenueDeviation,
|
||||
// consumptionCosts: consumptionCostsPerTour,
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
const { consumptionCostsPerTour } =
|
||||
this.calculationService.getBillingInformationByOrdinanceType(
|
||||
tour.ordinanceType.match(/([A-Z])\w+/).at(0),
|
||||
tour.startDate,
|
||||
);
|
||||
|
||||
return {
|
||||
...tour,
|
||||
revenue,
|
||||
revenueDeviation,
|
||||
consumptionCosts: consumptionCostsPerTour,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
// import { ToursCalculationAdapater } from './adapter/tours-calculation.adapter';
|
||||
import { PrismaModule } from 'src/core/database/prisma.module';
|
||||
import { UtilCalculationModule } from '../feat-calculation/util-calculation.module';
|
||||
import { DataAccessExtractorsModule } from '../feat-extractors/data-access-extractors.module';
|
||||
import { ToursCalculationAdapater } from './adapter/tours-calculation.adapter';
|
||||
import { StagingRepository } from './staging.repository';
|
||||
import { TransformersService } from './transformers.service';
|
||||
import { PrismaModule } from 'src/core/database/prisma.module';
|
||||
import { DataAccessExtractorsModule } from '../feat-extractors/data-access-extractors.module';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, DataAccessExtractorsModule],
|
||||
providers: [TransformersService, StagingRepository],
|
||||
imports: [PrismaModule, DataAccessExtractorsModule, UtilCalculationModule],
|
||||
providers: [TransformersService, StagingRepository, ToursCalculationAdapater],
|
||||
exports: [TransformersService],
|
||||
})
|
||||
export class DataAccessTransformersModule {}
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ export interface WorkTimePreStaging {
|
|||
endDate: Date;
|
||||
}
|
||||
|
||||
export interface EmployeePreStaging {
|
||||
export interface EmployeeDyflexisPreStaging {
|
||||
id: number;
|
||||
firstname: string;
|
||||
surname: string;
|
||||
|
|
@ -112,6 +112,13 @@ export interface EmployeePreStaging {
|
|||
contractEnd: Date;
|
||||
contractHoursWeek: number;
|
||||
contractSalaryHour: number;
|
||||
personTransportCertificate?: Date;
|
||||
qualification?: string;
|
||||
}
|
||||
|
||||
export interface EmployeeDispoLivePreStaging {
|
||||
id: string;
|
||||
name: string;
|
||||
surname: string;
|
||||
personnelNumber?: string;
|
||||
qualification?: string;
|
||||
personTransportCertificate?: Date;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
AttendanceRegistrationStaging,
|
||||
EmployeeDispoLiveStaging,
|
||||
EmployeeDyflexisStaging,
|
||||
PatientStaging,
|
||||
PlannedTimeStaging,
|
||||
Prisma,
|
||||
|
|
@ -10,7 +12,8 @@ import {
|
|||
import { PrismaService } from 'src/core/database/prisma.service';
|
||||
import {
|
||||
AttendanceRegistrationPreStaging,
|
||||
EmployeePreStaging,
|
||||
EmployeeDispoLivePreStaging,
|
||||
EmployeeDyflexisPreStaging,
|
||||
PatientPreStaging,
|
||||
PlannedTimePreStaging,
|
||||
TourPreStaging,
|
||||
|
|
@ -24,7 +27,8 @@ export class StagingRepository {
|
|||
clearStaging(): Promise<Prisma.BatchPayload[]> {
|
||||
return Promise.all([
|
||||
this.prisma.tourStaging.deleteMany(),
|
||||
this.prisma.employeeStaging.deleteMany(),
|
||||
this.prisma.employeeDispoLiveStaging.deleteMany(),
|
||||
this.prisma.employeeDyflexisStaging.deleteMany(),
|
||||
this.prisma.workTimeStaging.deleteMany(),
|
||||
this.prisma.plannedTimeStaging.deleteMany(),
|
||||
this.prisma.attendanceRegistrationStaging.deleteMany(),
|
||||
|
|
@ -68,14 +72,38 @@ export class StagingRepository {
|
|||
});
|
||||
}
|
||||
|
||||
createEmployees(
|
||||
employees: EmployeePreStaging[],
|
||||
createDyflexisEmployees(
|
||||
employees: EmployeeDyflexisPreStaging[],
|
||||
): Promise<Prisma.BatchPayload> {
|
||||
return this.prisma.employeeStaging.createMany({
|
||||
return this.prisma.employeeDyflexisStaging.createMany({
|
||||
data: employees,
|
||||
});
|
||||
}
|
||||
|
||||
createDispoliveEmployees(
|
||||
employees: EmployeeDispoLivePreStaging[],
|
||||
): Promise<Prisma.BatchPayload> {
|
||||
return this.prisma.employeeDispoLiveStaging.createMany({
|
||||
data: employees,
|
||||
});
|
||||
}
|
||||
|
||||
findDyflexisEmployees(): Promise<EmployeeDyflexisStaging[]> {
|
||||
return this.prisma.employeeDyflexisStaging.findMany({
|
||||
orderBy: {
|
||||
surname: 'asc',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
findDispoliveEmployees(): Promise<EmployeeDispoLiveStaging[]> {
|
||||
return this.prisma.employeeDispoLiveStaging.findMany({
|
||||
orderBy: {
|
||||
surname: 'asc',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
findPatients(): Promise<PatientStaging[]> {
|
||||
return this.prisma.patientStaging.findMany({
|
||||
orderBy: {
|
||||
|
|
|
|||
|
|
@ -2,20 +2,23 @@ import { Injectable } from '@nestjs/common';
|
|||
import {
|
||||
AttendanceRegistrationStaging,
|
||||
EmployeeDispoLiveSource,
|
||||
EmployeeDispoLiveStaging,
|
||||
EmployeeDyflexisSource,
|
||||
EmployeeDyflexisStaging,
|
||||
PatientStaging,
|
||||
PlannedTimeStaging,
|
||||
Prisma,
|
||||
TourStaging,
|
||||
WorkTimeStaging,
|
||||
} from '@prisma/client';
|
||||
import { combineLatest, from, map, switchMap } from 'rxjs';
|
||||
import { from, map, switchMap } from 'rxjs';
|
||||
|
||||
import { ExtractorsService } from '../feat-extractors/extractors.service';
|
||||
// import { ToursCalculationAdapater } from './adapter/tours-calculation.adapter';
|
||||
import { ToursCalculationAdapater } from './adapter/tours-calculation.adapter';
|
||||
import { StagingRepository } from './staging.repository';
|
||||
import { AttendanceRegistrationTransformer } from './transformers/attendance-registration.transformer';
|
||||
import { EmployeeTransformer } from './transformers/employee.transformer';
|
||||
import { DispoliveEmployeeTransformer } from './transformers/employee-dispolive.transformer';
|
||||
import { DyflexisEmployeeTransformer } from './transformers/employee-dyflexis.transformer';
|
||||
import { PatientTransformer } from './transformers/patient.transformer';
|
||||
import { PlannedTimeTransformer } from './transformers/planned-time.transformer';
|
||||
import { TourTransformer } from './transformers/tour.transformer';
|
||||
|
|
@ -26,7 +29,7 @@ export class TransformersService {
|
|||
constructor(
|
||||
private readonly extractorsService: ExtractorsService,
|
||||
private readonly repository: StagingRepository,
|
||||
// private readonly tourCalculationAdapter: ToursCalculationAdapater,
|
||||
private readonly tourCalculationAdapter: ToursCalculationAdapater,
|
||||
) {}
|
||||
|
||||
clearStaging(): Promise<Prisma.BatchPayload[]> {
|
||||
|
|
@ -78,43 +81,48 @@ export class TransformersService {
|
|||
.filter(({ einsatzNummer }) => !!einsatzNummer)
|
||||
.map((tour) => TourTransformer.transform(tour)),
|
||||
),
|
||||
// switchMap((tours) =>
|
||||
// from(
|
||||
// this.tourCalculationAdapter.addRevenueAndConsumptionCostsToTours(
|
||||
// tours,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
switchMap((tours) =>
|
||||
from(
|
||||
this.tourCalculationAdapter.addRevenueAndConsumptionCostsToTours(
|
||||
tours,
|
||||
),
|
||||
),
|
||||
),
|
||||
switchMap((tours) => this.repository.createTours(tours)),
|
||||
);
|
||||
}
|
||||
|
||||
transformEmployees(jobId: string) {
|
||||
return combineLatest([
|
||||
from(this.extractorsService.findEmployeesDispoLiveByJobId(jobId)),
|
||||
from(this.extractorsService.findEmployeesDyflexisByJobId(jobId)),
|
||||
]).pipe(
|
||||
map(([dispoLiveEmployees, dyflexisEmployees]) =>
|
||||
dyflexisEmployees
|
||||
.map((dyflexisEmployee) => {
|
||||
const dispoLiveEmployee = dispoLiveEmployees.find(
|
||||
(e) => e.importPersId === dyflexisEmployee.id,
|
||||
);
|
||||
if (!dispoLiveEmployee) return undefined;
|
||||
|
||||
return [dyflexisEmployee, dispoLiveEmployee];
|
||||
})
|
||||
.filter(Boolean),
|
||||
),
|
||||
transformDyflexisEmployees(jobId: string) {
|
||||
return from(
|
||||
this.extractorsService.findEmployeesDyflexisByJobId(jobId),
|
||||
).pipe(
|
||||
map((employees) =>
|
||||
employees.map(([dyflexisEmployee, dispoLiveEmployee]) =>
|
||||
EmployeeTransformer.transform(
|
||||
employees.map((dyflexisEmployee) =>
|
||||
DyflexisEmployeeTransformer.transform(
|
||||
dyflexisEmployee as EmployeeDyflexisSource,
|
||||
),
|
||||
),
|
||||
),
|
||||
switchMap((employees) =>
|
||||
this.repository.createDyflexisEmployees(employees),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
transformDispoliveEmployees(jobId: string) {
|
||||
return from(
|
||||
this.extractorsService.findEmployeesDispoLiveByJobId(jobId),
|
||||
).pipe(
|
||||
map((employees) =>
|
||||
employees.map((dispoLiveEmployee) =>
|
||||
DispoliveEmployeeTransformer.transform(
|
||||
dispoLiveEmployee as EmployeeDispoLiveSource,
|
||||
),
|
||||
),
|
||||
),
|
||||
switchMap((employees) => this.repository.createEmployees(employees)),
|
||||
switchMap((employees) =>
|
||||
this.repository.createDispoliveEmployees(employees),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -131,6 +139,14 @@ export class TransformersService {
|
|||
return this.repository.findPatients();
|
||||
}
|
||||
|
||||
findDyflexisEmployees(): Promise<EmployeeDyflexisStaging[]> {
|
||||
return this.repository.findDyflexisEmployees();
|
||||
}
|
||||
|
||||
findDispoliveEmployees(): Promise<EmployeeDispoLiveStaging[]> {
|
||||
return this.repository.findDispoliveEmployees();
|
||||
}
|
||||
|
||||
findTours(): Promise<TourStaging[]> {
|
||||
return this.repository.findTours();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
import { EmployeeDispoLiveSource } from '@prisma/client';
|
||||
|
||||
import { isValidDate } from 'src/utils';
|
||||
import { EmployeeDispoLivePreStaging } from '../staging.interface';
|
||||
import {
|
||||
parseDateTime,
|
||||
pick,
|
||||
transformProperties,
|
||||
} from '../transformer.functions';
|
||||
|
||||
export class DispoliveEmployeeTransformer {
|
||||
public static transform(
|
||||
dispoLiveEmployeeSource: EmployeeDispoLiveSource,
|
||||
): EmployeeDispoLivePreStaging {
|
||||
const dispoLiveEmployeeParsed = {
|
||||
...dispoLiveEmployeeSource,
|
||||
...transformProperties(parseDateTime('yyyy-MM-dd'))(
|
||||
pick(['pScheinAblaufDatum'])(dispoLiveEmployeeSource),
|
||||
),
|
||||
};
|
||||
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
surname,
|
||||
importPersId,
|
||||
pScheinAblaufDatum,
|
||||
qualifikation,
|
||||
} = dispoLiveEmployeeParsed;
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
surname,
|
||||
personnelNumber: importPersId,
|
||||
personTransportCertificate: isValidDate(new Date(pScheinAblaufDatum))
|
||||
? pScheinAblaufDatum
|
||||
: undefined,
|
||||
qualification: qualifikation,
|
||||
} as unknown as EmployeeDispoLivePreStaging;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,6 @@
|
|||
import {
|
||||
EmployeeDispoLiveSource,
|
||||
EmployeeDyflexisSource,
|
||||
} from '@prisma/client';
|
||||
import { EmployeeDyflexisSource } from '@prisma/client';
|
||||
|
||||
import { isValidDate } from 'src/utils';
|
||||
import { EmployeePreStaging } from '../staging.interface';
|
||||
import { EmployeeDyflexisPreStaging } from '../staging.interface';
|
||||
import {
|
||||
parseDateTime,
|
||||
parseFloat,
|
||||
|
|
@ -13,11 +9,10 @@ import {
|
|||
transformProperties,
|
||||
} from '../transformer.functions';
|
||||
|
||||
export class EmployeeTransformer {
|
||||
export class DyflexisEmployeeTransformer {
|
||||
public static transform(
|
||||
dyflexisEmployeeSource: EmployeeDyflexisSource,
|
||||
dispoLiveEmployeeSource: EmployeeDispoLiveSource,
|
||||
): EmployeePreStaging {
|
||||
): EmployeeDyflexisPreStaging {
|
||||
const dyflexisEmployeeParsed = {
|
||||
...dyflexisEmployeeSource,
|
||||
...transformProperties(parseFloat)(
|
||||
|
|
@ -32,12 +27,6 @@ export class EmployeeTransformer {
|
|||
pick(['contract_start', 'contract_end'])(dyflexisEmployeeSource),
|
||||
),
|
||||
};
|
||||
const dispoLiveEmployeeParsed = {
|
||||
...dispoLiveEmployeeSource,
|
||||
...transformProperties(parseDateTime('yyyy-MM-dd'))(
|
||||
pick(['pScheinAblaufDatum'])(dispoLiveEmployeeSource),
|
||||
),
|
||||
};
|
||||
|
||||
const {
|
||||
id,
|
||||
|
|
@ -48,7 +37,6 @@ export class EmployeeTransformer {
|
|||
contract_hours_week,
|
||||
contract_salary_hour,
|
||||
} = dyflexisEmployeeParsed;
|
||||
const { pScheinAblaufDatum, qualifikation } = dispoLiveEmployeeParsed;
|
||||
|
||||
return {
|
||||
id,
|
||||
|
|
@ -58,10 +46,6 @@ export class EmployeeTransformer {
|
|||
contractEnd: contract_end,
|
||||
contractHoursWeek: contract_hours_week,
|
||||
contractSalaryHour: contract_salary_hour,
|
||||
personTransportCertificate: isValidDate(new Date(pScheinAblaufDatum))
|
||||
? pScheinAblaufDatum
|
||||
: undefined,
|
||||
qualification: qualifikation,
|
||||
} as unknown as EmployeePreStaging;
|
||||
} as unknown as EmployeeDyflexisPreStaging;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
import {
|
||||
Interval,
|
||||
addDays,
|
||||
clamp,
|
||||
differenceInMilliseconds,
|
||||
set,
|
||||
} from 'date-fns';
|
||||
import { fromZonedTime } from 'date-fns-tz';
|
||||
|
||||
/**
|
||||
* Generates an interval for the given time range for the baseDate, today by default. If the second time is lower than the first, the interval spans over to the next day
|
||||
* @param timeRange A user readable time range formatted as "HH:mm-HH:mm"
|
||||
*/
|
||||
export function generateIntervalForTimeRange(
|
||||
timeRange: string,
|
||||
timeZone = 'Europe/London',
|
||||
baseDate = new Date(),
|
||||
): Interval {
|
||||
const [firstTime, secondTime] = timeRange.split('-');
|
||||
if (
|
||||
!firstTime ||
|
||||
!secondTime ||
|
||||
firstTime?.indexOf(':') === -1 ||
|
||||
secondTime?.indexOf(':') === -1
|
||||
) {
|
||||
throw new Error(
|
||||
`Invalid argument for 'timeRange', expected format 'HH:mm-HH:mm', got ${timeRange}`,
|
||||
);
|
||||
}
|
||||
const [startHours, startMinutes] = firstTime.split(':').map((val) => +val);
|
||||
const [endHours, endMinutes] = secondTime.split(':').map((val) => +val);
|
||||
const startDate = baseDate;
|
||||
let endDate = baseDate;
|
||||
if (endHours < startHours) {
|
||||
endDate = addDays(endDate, 1);
|
||||
}
|
||||
return {
|
||||
start: fromZonedTime(
|
||||
set(startDate, {
|
||||
hours: startHours,
|
||||
minutes: startMinutes,
|
||||
seconds: 0,
|
||||
milliseconds: 0,
|
||||
}),
|
||||
timeZone,
|
||||
),
|
||||
end: fromZonedTime(
|
||||
set(endDate, {
|
||||
hours: endHours,
|
||||
minutes: endMinutes,
|
||||
seconds: 0,
|
||||
milliseconds: 0,
|
||||
}),
|
||||
timeZone,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export function clampInterval(
|
||||
interval: Interval,
|
||||
intervalClamp: Interval,
|
||||
): Interval {
|
||||
return {
|
||||
start: clamp(interval.start, intervalClamp),
|
||||
end: clamp(interval.end, intervalClamp),
|
||||
};
|
||||
}
|
||||
|
||||
export function getHoursInInterval(
|
||||
interval: Interval,
|
||||
intervalClamp?: Interval,
|
||||
): number {
|
||||
const clampedInterval: Interval = clampInterval(
|
||||
interval,
|
||||
intervalClamp ?? interval,
|
||||
);
|
||||
|
||||
return (
|
||||
differenceInMilliseconds(clampedInterval.end, clampedInterval.start) /
|
||||
60 /
|
||||
60 /
|
||||
1000
|
||||
);
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
export * from './chunk';
|
||||
export * from './helper';
|
||||
export * from './is-valid-date';
|
||||
export * from './range';
|
||||
|
|
|
|||
Loading…
Reference in New Issue