diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..47c8cbd --- /dev/null +++ b/TODO.md @@ -0,0 +1,2 @@ +- [ ] Anomalie "Anwesenheit vor Arbeitszeit" abschliessen +- [ ] Add Dyflexis User Access Endpoints to ETL \ No newline at end of file diff --git a/service/app-hub/dashboard/src/app/pages/control-center/anomaly-list/anomaly-solution-dialog/anomaly-solution-dialog.component.html b/service/app-hub/dashboard/src/app/pages/control-center/anomaly-list/anomaly-solution-dialog/anomaly-solution-dialog.component.html index f94a3e6..f7245b1 100644 --- a/service/app-hub/dashboard/src/app/pages/control-center/anomaly-list/anomaly-solution-dialog/anomaly-solution-dialog.component.html +++ b/service/app-hub/dashboard/src/app/pages/control-center/anomaly-list/anomaly-solution-dialog/anomaly-solution-dialog.component.html @@ -10,7 +10,7 @@ diff --git a/service/app-hub/dashboard/src/app/pages/control-center/overview-dashboard/overview-dashboard.component.ts b/service/app-hub/dashboard/src/app/pages/control-center/overview-dashboard/overview-dashboard.component.ts index 693a6a7..341422a 100644 --- a/service/app-hub/dashboard/src/app/pages/control-center/overview-dashboard/overview-dashboard.component.ts +++ b/service/app-hub/dashboard/src/app/pages/control-center/overview-dashboard/overview-dashboard.component.ts @@ -25,7 +25,7 @@ import { Component } from '@angular/core'; class="w-full md:flex-grow lg:w-2/5 m-2 !flex flex-col" > - Anomalien + Anomalien in der Dokumentation @@ -36,7 +36,7 @@ import { Component } from '@angular/core'; class="w-full md:flex-grow lg:w-2/5 m-2 !flex flex-col" > - Mitarbeiter ohne Tour + Anomalien in der Zeiterfassung diff --git a/service/app-hub/dashboard/src/styles.scss b/service/app-hub/dashboard/src/styles.scss index 50046d0..37540b6 100644 --- a/service/app-hub/dashboard/src/styles.scss +++ b/service/app-hub/dashboard/src/styles.scss @@ -12,6 +12,16 @@ body { color: var(--mat-sys-on-surface); } +.dispolive-btn, .dyflexis-btn { + a[target="_blank"]::after { + content: " \2924"; /* Unicode-Pfeil */ + font-size: 0.8em; + margin-left: 0.2em; + vertical-align: super; + opacity: 0.8; + } +} + .pulse { animation: pulse 1.5s cubic-bezier(0.4, 0, 0.2, 1) infinite; animation-delay: 0.5s; diff --git a/service/data-hub/business-engine/.gitignore b/service/data-hub/business-engine/.gitignore new file mode 100644 index 0000000..5a9f2b6 --- /dev/null +++ b/service/data-hub/business-engine/.gitignore @@ -0,0 +1,31 @@ +tmp/ + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Code coverage profiles and other test artifacts +*.out +coverage.* +*.coverprofile +profile.cov + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# env file +.env + +# Editor/IDE +# .idea/ +# .vscode/ \ No newline at end of file diff --git a/service/data-hub/data-connector/.gitignore b/service/data-hub/data-connector/.gitignore index 07f9376..51289f3 100644 --- a/service/data-hub/data-connector/.gitignore +++ b/service/data-hub/data-connector/.gitignore @@ -1,4 +1,5 @@ .local/ +daten/ # compiled output /dist diff --git a/service/data-hub/data-connector/prisma/migrations/20250901133537_add_pause_and_duration/migration.sql b/service/data-hub/data-connector/prisma/migrations/20250901133537_add_pause_and_duration/migration.sql new file mode 100644 index 0000000..1948a69 --- /dev/null +++ b/service/data-hub/data-connector/prisma/migrations/20250901133537_add_pause_and_duration/migration.sql @@ -0,0 +1,16 @@ +/* + Warnings: + + - Added the required column `duration` to the `PlannedTimeStaging` table without a default value. This is not possible if the table is not empty. + - Added the required column `pause` to the `PlannedTimeStaging` table without a default value. This is not possible if the table is not empty. + - Added the required column `duration` to the `WorkTimeStaging` table without a default value. This is not possible if the table is not empty. + - Added the required column `pause` to the `WorkTimeStaging` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "PlannedTimeStaging" ADD COLUMN "duration" INTEGER NOT NULL, +ADD COLUMN "pause" INTEGER NOT NULL; + +-- AlterTable +ALTER TABLE "WorkTimeStaging" ADD COLUMN "duration" INTEGER NOT NULL, +ADD COLUMN "pause" INTEGER NOT NULL; diff --git a/service/data-hub/data-connector/prisma/migrations/20250918113015_add_contract_type_name/migration.sql b/service/data-hub/data-connector/prisma/migrations/20250918113015_add_contract_type_name/migration.sql new file mode 100644 index 0000000..8a80723 --- /dev/null +++ b/service/data-hub/data-connector/prisma/migrations/20250918113015_add_contract_type_name/migration.sql @@ -0,0 +1,9 @@ +-- AlterTable +ALTER TABLE "DyflexisEmployee" ADD COLUMN "contractTypeName" TEXT, +ALTER COLUMN "contractSalaryHour" SET DATA TYPE DOUBLE PRECISION; + +-- AlterTable +ALTER TABLE "EmployeeDyflexisSource" ADD COLUMN "contract_type_name" TEXT; + +-- AlterTable +ALTER TABLE "EmployeeDyflexisStaging" ADD COLUMN "contractTypeName" TEXT; diff --git a/service/data-hub/data-connector/prisma/migrations/20250918131046_dyflexis_employee_add_primary_key/migration.sql b/service/data-hub/data-connector/prisma/migrations/20250918131046_dyflexis_employee_add_primary_key/migration.sql new file mode 100644 index 0000000..25c5d0c --- /dev/null +++ b/service/data-hub/data-connector/prisma/migrations/20250918131046_dyflexis_employee_add_primary_key/migration.sql @@ -0,0 +1,10 @@ +/* + Warnings: + + - The primary key for the `DyflexisEmployee` table will be changed. If it partially fails, the table could be left without primary key constraint. + +*/ +-- AlterTable +ALTER TABLE "DyflexisEmployee" DROP CONSTRAINT "DyflexisEmployee_pkey", +ADD COLUMN "internalId" UUID NOT NULL DEFAULT gen_random_uuid(), +ADD CONSTRAINT "DyflexisEmployee_pkey" PRIMARY KEY ("internalId"); diff --git a/service/data-hub/data-connector/prisma/migrations/20250919092253_add_personnel_number/migration.sql b/service/data-hub/data-connector/prisma/migrations/20250919092253_add_personnel_number/migration.sql new file mode 100644 index 0000000..dc35730 --- /dev/null +++ b/service/data-hub/data-connector/prisma/migrations/20250919092253_add_personnel_number/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "AttendanceRegistration" ALTER COLUMN "personnelNumber" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "AttendanceRegistrationStaging" ADD COLUMN "personnelNumber" TEXT DEFAULT ''; diff --git a/service/data-hub/data-connector/prisma/migrations/20251023125926_versandliste_leitstellendaten/migration.sql b/service/data-hub/data-connector/prisma/migrations/20251023125926_versandliste_leitstellendaten/migration.sql new file mode 100644 index 0000000..0dae9fd --- /dev/null +++ b/service/data-hub/data-connector/prisma/migrations/20251023125926_versandliste_leitstellendaten/migration.sql @@ -0,0 +1,14 @@ +-- AlterTable +ALTER TABLE "Tour" ADD COLUMN "companyName" TEXT, +ADD COLUMN "overRTW" BOOLEAN NOT NULL DEFAULT false, +ADD COLUMN "schwerlast" BOOLEAN NOT NULL DEFAULT false; + +-- AlterTable +ALTER TABLE "TourSource" ADD COLUMN "companyName" TEXT, +ADD COLUMN "overRTW" TEXT, +ADD COLUMN "schwerlast" TEXT; + +-- AlterTable +ALTER TABLE "TourStaging" ADD COLUMN "companyName" TEXT, +ADD COLUMN "overRTW" BOOLEAN NOT NULL DEFAULT false, +ADD COLUMN "schwerlast" BOOLEAN NOT NULL DEFAULT false; diff --git a/service/data-hub/data-connector/prisma/migrations/20251029154643_employee_leave_tables/migration.sql b/service/data-hub/data-connector/prisma/migrations/20251029154643_employee_leave_tables/migration.sql new file mode 100644 index 0000000..8880574 --- /dev/null +++ b/service/data-hub/data-connector/prisma/migrations/20251029154643_employee_leave_tables/migration.sql @@ -0,0 +1,24 @@ +-- CreateTable +CREATE TABLE "Sickdays" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "start" TIMESTAMP(3) NOT NULL, + "end" TIMESTAMP(3) NOT NULL, + "hours" DOUBLE PRECISION NOT NULL, + "days" INTEGER NOT NULL, + "note" TEXT, + "locked" BOOLEAN NOT NULL, + "employeeId" INTEGER NOT NULL, + + CONSTRAINT "Sickdays_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Holiday" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "date" TIMESTAMP(3) NOT NULL, + "locked" BOOLEAN NOT NULL, + "excludedFromPayroll" BOOLEAN NOT NULL, + "employeeId" INTEGER NOT NULL, + + CONSTRAINT "Holiday_pkey" PRIMARY KEY ("id") +); diff --git a/service/data-hub/data-connector/prisma/schema.prisma b/service/data-hub/data-connector/prisma/schema.prisma index a84f49f..fa89946 100644 --- a/service/data-hub/data-connector/prisma/schema.prisma +++ b/service/data-hub/data-connector/prisma/schema.prisma @@ -97,6 +97,10 @@ model TourSource { fahrer String? // ID color String? // Hex rgb + + overRTW String? + companyName String? + schwerlast String? } model EmployeeDispoLiveSource { @@ -174,6 +178,7 @@ model EmployeeDyflexisSource { contract_end String? // yyyy-MM-dd contract_hours_week String? // Float string contract_salary_hour String? // Float string + contract_type_name String? } model PatientSource { @@ -256,6 +261,10 @@ model TourStaging { revenueDeviation Float @default(0) consumptionCosts Float @default(0) + overRTW Boolean @default(false) + companyName String? + schwerlast Boolean @default(false) + createdAt DateTime? updatedAt DateTime? } @@ -285,6 +294,7 @@ model AttendanceRegistrationStaging { employeeId Int dateTime DateTime event String + personnelNumber String? @default("") } model PlannedTimeStaging { @@ -299,6 +309,8 @@ model PlannedTimeStaging { endDate DateTime deleted Boolean mark String? + pause Int + duration Int } model WorkTimeStaging { @@ -311,6 +323,8 @@ model WorkTimeStaging { departmentName String? startDate DateTime endDate DateTime + pause Int + duration Int } model EmployeeDyflexisStaging { @@ -323,6 +337,7 @@ model EmployeeDyflexisStaging { contractEnd DateTime contractHoursWeek Float contractSalaryHour Float + contractTypeName String? } model EmployeeDispoLiveStaging { @@ -395,6 +410,10 @@ model Tour { consumptionCosts Float @default(0) revenueDeviation Float @default(0) + overRTW Boolean @default(false) + companyName String? + schwerlast Boolean @default(false) + createdAt DateTime? updatedAt DateTime? deletedAt DateTime? @@ -469,7 +488,7 @@ model TicketStateHistory { model AttendanceRegistration { attendanceRegistrationId Int @id employeeId Int - personnelNumber String + personnelNumber String? dateTime DateTime event String } @@ -511,13 +530,36 @@ model Employee { } model DyflexisEmployee { - id Int @id + internalId String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + id Int firstname String surname String contractStart DateTime contractEnd DateTime? contractHoursWeek Float - contractSalaryHour Int + contractSalaryHour Float + contractTypeName String? +} + +model Sickdays { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + start DateTime + end DateTime + hours Float + days Int + note String? + locked Boolean + + employeeId Int +} + +model Holiday { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + date DateTime + locked Boolean + excludedFromPayroll Boolean + + employeeId Int } enum CostTypeVariant { diff --git a/service/data-hub/data-connector/prisma/seed/tariffs/ktw-aok.ts b/service/data-hub/data-connector/prisma/seed/tariffs/ktw-aok.ts index 1f4af63..402ac92 100644 --- a/service/data-hub/data-connector/prisma/seed/tariffs/ktw-aok.ts +++ b/service/data-hub/data-connector/prisma/seed/tariffs/ktw-aok.ts @@ -179,7 +179,7 @@ export const KTW_AOK_TARIFFS = [ { description: 'AOK/BKK/IKK/Knappschaft KTW', validFrom: startOfDay(new Date('2024-01-01')), - validTo: endOfDay(new Date('2024-12-31')), + validTo: endOfDay(new Date('2024-10-31')), costTypes: [ { description: 'Grundpauschale je Fahrt', @@ -200,7 +200,7 @@ export const KTW_AOK_TARIFFS = [ '1/KTW-02', '1/KTW-03', '1/KTW-04', - '1/KTW-05-E', + '1/KTW-05', '1/KTW-06', ], }, @@ -262,6 +262,84 @@ export const KTW_AOK_TARIFFS = [ }, ], }, + { + description: 'AOK/BKK/IKK/Knappschaft KTW', + validFrom: startOfDay(new Date('2024-11-01')), + validTo: endOfDay(new Date('2024-12-31')), + 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 + }, + { + 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 18:30-06:30', + 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', + }, + ], + }, { description: 'AOK/BKK/IKK/Knappschaft KTW', validFrom: startOfDay(new Date('2025-01-01')), diff --git a/service/data-hub/data-connector/src/app.controller.spec.ts b/service/data-hub/data-connector/src/app.controller.spec.ts index d22f389..a22ca32 100644 --- a/service/data-hub/data-connector/src/app.controller.spec.ts +++ b/service/data-hub/data-connector/src/app.controller.spec.ts @@ -13,10 +13,4 @@ describe('AppController', () => { appController = app.get(AppController); }); - - describe('root', () => { - it('should return "Hello World!"', () => { - expect(appController.getHello()).toBe('Hello World!'); - }); - }); }); diff --git a/service/data-hub/data-connector/src/app.controller.ts b/service/data-hub/data-connector/src/app.controller.ts index 34430ed..dfda93c 100644 --- a/service/data-hub/data-connector/src/app.controller.ts +++ b/service/data-hub/data-connector/src/app.controller.ts @@ -65,6 +65,7 @@ export class AppController { @Get('updated-tours') async updatedTours(@Query('from') from: string, @Query('to') to: string) { + this.logger.log(`Fetch Tour updates in interval from=${from} to=${to}`); const interval = { start: new Date(from), end: new Date(to), @@ -165,4 +166,103 @@ export class AppController { } return toursWithRevenue; } + + @Get('leitstellendaten') + async versandlisteLeitstellendaten( + @Query('from') from: string, + @Query('to') to: string, + ) { + const interval = { + start: new Date(from), + end: new Date(to), + }; + const tours = await this.prisma.tour.findMany({ + where: { + AND: [ + { + startDate: { + gte: new Date(interval.start), + }, + }, + { + startDate: { + lt: new Date(interval.end), + }, + }, + { + healthInsurance: { + not: 'KOSTENTRÄGER PATIENT', + }, + }, + { + ordinanceType: { + contains: 'KTW', + }, + }, + { + carName: { + contains: 'KTW', + }, + }, + { + check: { + lt: 3, + }, + }, + { + deletedAt: null, + }, + { + OR: [ + { + patientId: null, + }, + { + patientId: { + // Pause + not: 'b7424477-a4b2-42f7-b7e7-a4bef24f6979', + }, + }, + { + patientId: { + // Feierabend + not: '3554e1a9-9c07-45e5-b53c-692a77e4fa9a', + }, + }, + ], + }, + ], + }, + orderBy: { + startDate: 'desc', + }, + select: { + companyName: true, + createdAt: true, + startDate: true, + startBegin: true, + startEnd: true, + empty: true, + startZip: true, + targetZip: true, + category: true, + rangeEndDate: true, + hasInfection: true, + schwerlast: true, + overRTW: true, + carName: true, + operationId: true, + ordinanceType: true, + patientName: true, + patientSurname: true, + infectionName: true, + }, + }); + return tours; + } + + @Get('dyflexis-user-access') + async loadDyflexisUserData() { + return this.importer.runDyflexisUserAccessTest(); + } } diff --git a/service/data-hub/data-connector/src/core/config/config.service.ts b/service/data-hub/data-connector/src/core/config/config.service.ts index ca5df0c..0342fa3 100644 --- a/service/data-hub/data-connector/src/core/config/config.service.ts +++ b/service/data-hub/data-connector/src/core/config/config.service.ts @@ -30,6 +30,15 @@ export class ConfigService extends NestConfigService { dyflexisV2ApiKey(): string { return this.get('DYFLEXIS_V2_API_KEY'); } + dyflexisAppBaseUrl(): string { + return this.get('DYFLEXIS_APP_BASE_URL'); + } + dyflexisAppUsername(): string { + return this.get('DYFLEXIS_APP_USERNAME'); + } + dyflexisAppPassword(): string { + return this.get('DYFLEXIS_APP_PASSWORD'); + } databasePort(): string { return this.get('DATABASE_PORT'); } diff --git a/service/data-hub/data-connector/src/core/database/types.ts b/service/data-hub/data-connector/src/core/database/types.ts index a9c2d93..4a60a31 100644 --- a/service/data-hub/data-connector/src/core/database/types.ts +++ b/service/data-hub/data-connector/src/core/database/types.ts @@ -60,7 +60,7 @@ export type Anomaly = { export type AttendanceRegistration = { attendanceRegistrationId: number; employeeId: number; - personnelNumber: string; + personnelNumber: string | null; dateTime: Timestamp; event: string; }; @@ -80,6 +80,7 @@ export type AttendanceRegistrationStaging = { employeeId: number; dateTime: Timestamp; event: string; + personnelNumber: Generated; }; export type AvicennaTenant = { id: Generated; @@ -102,6 +103,7 @@ export type CostType = { tariffId: string | null; }; export type DyflexisEmployee = { + internalId: Generated; id: number; firstname: string; surname: string; @@ -109,6 +111,7 @@ export type DyflexisEmployee = { contractEnd: Timestamp | null; contractHoursWeek: number; contractSalaryHour: number; + contractTypeName: string | null; }; export type Employee = { id: string; @@ -153,6 +156,7 @@ export type EmployeeDyflexisSource = { contract_end: string | null; contract_hours_week: string | null; contract_salary_hour: string | null; + contract_type_name: string | null; }; export type EmployeeDyflexisStaging = { internalId: Generated; @@ -163,6 +167,14 @@ export type EmployeeDyflexisStaging = { contractEnd: Timestamp; contractHoursWeek: number; contractSalaryHour: number; + contractTypeName: string | null; +}; +export type Holiday = { + id: Generated; + date: Timestamp; + locked: boolean; + excludedFromPayroll: boolean; + employeeId: number; }; export type Patient = { id: string; @@ -250,6 +262,18 @@ export type PlannedTimeStaging = { endDate: Timestamp; deleted: boolean; mark: string | null; + pause: number; + duration: number; +}; +export type Sickdays = { + id: Generated; + start: Timestamp; + end: Timestamp; + hours: number; + days: number; + note: string | null; + locked: boolean; + employeeId: number; }; export type SolutionTarget = { id: Generated; @@ -337,11 +361,25 @@ export type Tour = { revenue: Generated; consumptionCosts: Generated; revenueDeviation: Generated; + overRTW: Generated; + companyName: string | null; + schwerlast: Generated; createdAt: Timestamp | null; updatedAt: Timestamp | null; deletedAt: Timestamp | null; ticketId: string | null; }; +export type TourFile = { + id: Generated; + tourId: string; + ticketId: string; + storagePath: string; + originalFilename: string; + mimeType: string; + fileSizeBytes: number; + dlUploadStatus: Generated; + createdAt: Generated; +}; export type TourSource = { internalId: Generated; insertedAt: Generated; @@ -411,6 +449,9 @@ export type TourSource = { fahrerName: string | null; fahrer: string | null; color: string | null; + overRTW: string | null; + companyName: string | null; + schwerlast: string | null; }; export type TourStaging = { internalId: Generated; @@ -461,6 +502,9 @@ export type TourStaging = { revenue: Generated; revenueDeviation: Generated; consumptionCosts: Generated; + overRTW: Generated; + companyName: string | null; + schwerlast: Generated; createdAt: Timestamp | null; updatedAt: Timestamp | null; }; @@ -505,6 +549,8 @@ export type WorkTimeStaging = { departmentName: string | null; startDate: Timestamp; endDate: Timestamp; + pause: number; + duration: number; }; export type DB = { _AvicennaTenantToTariffGroup: AvicennaTenantToTariffGroup; @@ -520,12 +566,14 @@ export type DB = { EmployeeDispoLiveStaging: EmployeeDispoLiveStaging; EmployeeDyflexisSource: EmployeeDyflexisSource; EmployeeDyflexisStaging: EmployeeDyflexisStaging; + Holiday: Holiday; Patient: Patient; PatientSource: PatientSource; PatientStaging: PatientStaging; PlannedTime: PlannedTime; PlannedTimeSource: PlannedTimeSource; PlannedTimeStaging: PlannedTimeStaging; + Sickdays: Sickdays; SolutionTarget: SolutionTarget; SolutionTargetStep: SolutionTargetStep; Tariff: Tariff; @@ -533,6 +581,7 @@ export type DB = { Ticket: Ticket; TicketStateHistory: TicketStateHistory; Tour: Tour; + TourFile: TourFile; TourSource: TourSource; TourStaging: TourStaging; Trace: Trace; diff --git a/service/data-hub/data-connector/src/modules/app-accounting/tour.object-type.ts b/service/data-hub/data-connector/src/modules/app-accounting/tour.object-type.ts index 4945fa8..c580449 100644 --- a/service/data-hub/data-connector/src/modules/app-accounting/tour.object-type.ts +++ b/service/data-hub/data-connector/src/modules/app-accounting/tour.object-type.ts @@ -3,6 +3,9 @@ import { Tour } from '@prisma/client'; @ObjectType('Tour') export class TourObjectType implements Tour { + overRTW: boolean; + companyName: string; + schwerlast: boolean; @Field() id: string; @Field() diff --git a/service/data-hub/data-connector/src/modules/app-control-center/control-center.resolver.ts b/service/data-hub/data-connector/src/modules/app-control-center/control-center.resolver.ts index 927619a..bbbb79a 100644 --- a/service/data-hub/data-connector/src/modules/app-control-center/control-center.resolver.ts +++ b/service/data-hub/data-connector/src/modules/app-control-center/control-center.resolver.ts @@ -52,7 +52,11 @@ export class ControlCenterResolver { const activeAnomalies = await this.anomalyService.findAllVisible( skip, take, - [AnomalyTypes.WorkingEmployeeNotInDistributionSystem], + [ + AnomalyTypes.WorkingEmployeeNotInDistributionSystem, + AnomalyTypes.EmployeeStillWorking, + AnomalyTypes.IsAttendanceBeforeWorktime, + ], ); const totalCount = await this.anomalyService.countVisibleAnomalies([ @@ -134,6 +138,7 @@ export class ControlCenterResolver { AnomalyTypes.DriverCertificate, AnomalyTypes.EmployeesLoggedIn, AnomalyTypes.OneTourAtATime, + AnomalyTypes.IsGermanZip, ], ); diff --git a/service/data-hub/data-connector/src/modules/feat-anomalies/anomalies.service.ts b/service/data-hub/data-connector/src/modules/feat-anomalies/anomalies.service.ts index 1598c83..cb7c4d4 100644 --- a/service/data-hub/data-connector/src/modules/feat-anomalies/anomalies.service.ts +++ b/service/data-hub/data-connector/src/modules/feat-anomalies/anomalies.service.ts @@ -10,6 +10,10 @@ import { AnomalyCreateDto } from '../feat-business-objects/entities/anomaly/anom import { AnomalyService } from '../feat-business-objects/entities/anomaly/anomaly.service'; import { AnomalyDetector } from './anomaly-detectors/anomaly-detector.class'; import { WorkingEmployeeNotInDistributionSystemDetector } from './anomaly-detectors/working-employee-not-in-distribution-system.detector'; +import { IsGermanZipDetector } from './anomaly-detectors/is-german-zip.detector'; +import { EmployeeStillWorkingDetector } from './anomaly-detectors/employee-still-working.detector'; +import { IsAttendanceBeforeWorktimeDetector } from './anomaly-detectors/is-attendance-before-worktime.detector'; +import { IsWorkTimePlannedDetector } from './anomaly-detectors/is-work-time-planned.detector'; @Injectable() export class AnomaliesService { @@ -27,6 +31,10 @@ export class AnomaliesService { new WorkingEmployeeNotInDistributionSystemDetector( this.anomalyDetectorService, ), + new IsGermanZipDetector(this.anomalyDetectorService), + new EmployeeStillWorkingDetector(this.anomalyDetectorService), + new IsAttendanceBeforeWorktimeDetector(this.anomalyDetectorService), + // new IsWorkTimePlannedDetector(this.anomalyDetectorService) ]; } @@ -53,11 +61,13 @@ export class AnomaliesService { const anomalies = await this.anomalyService.findAllUnsolved(); return Promise.all( - anomalies.map((anomaly) => - this.detectors - .find((detector) => detector.type === anomaly.type) - .validate(anomaly), - ), + anomalies + .map((anomaly) => + this.detectors + .find((detector) => detector.type === anomaly.type) + ?.validate(anomaly), + ) + .filter(Boolean), ).then(async (anomalies) => { const validatedAnomalies = anomalies.flat(); const updatedAnomalies: Anomaly[] = []; diff --git a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detection.module.ts b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detection.module.ts index b992813..1015a2b 100644 --- a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detection.module.ts +++ b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detection.module.ts @@ -3,10 +3,11 @@ import { PrismaModule } from 'src/core/database/prisma.module'; import { AnomaliesService } from './anomalies.service'; import { AnomalyDetectorService } from './anomaly-detectors/anomaly-detector.service'; import { FeatBusinessObjectsModule } from '../feat-business-objects/feat-business-objects.module'; +import { AnomalyDetectorRepository } from './anomaly-detectors/anomaly-detector.repository'; @Module({ imports: [PrismaModule, FeatBusinessObjectsModule], - providers: [AnomaliesService, AnomalyDetectorService], + providers: [AnomaliesService, AnomalyDetectorService, AnomalyDetectorRepository], exports: [AnomaliesService], }) export class AnomalyDetectionModule {} diff --git a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/anomaly-detector.repository.ts b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/anomaly-detector.repository.ts new file mode 100644 index 0000000..a761b33 --- /dev/null +++ b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/anomaly-detector.repository.ts @@ -0,0 +1,255 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { AttendanceRegistration, Tour } from '@prisma/client'; +import { addDays, endOfDay, Interval, startOfDay, subDays } from 'date-fns'; +import { ExpressionBuilder, Kysely, sql } from 'kysely'; +import { KYSELY_CLIENT_PROVIDER_TOKEN } from 'src/core/database/database.module'; +import { DB } from 'src/core/database/types'; + +type TourEb = ExpressionBuilder; + +const PAUSE_PATIENT_ID = 'b7424477-a4b2-42f7-b7e7-a4bef24f6979'; +const FEIERABEND_PATIENT_ID = '3554e1a9-9c07-45e5-b53c-692a77e4fa9a'; + +function onlyRealTours(eb: TourEb) { + return eb.and([ + eb.or([ + eb('carName', 'is', null), + eb('carName', 'not in', ['Storno', 'Löschen', 'Abgabe']), + ]), + + eb('check', '!=', 3), + + eb('deletedAt', 'is', null), + + eb.or([ + eb('patientId', 'is', null), + eb('patientId', 'not in', [PAUSE_PATIENT_ID, FEIERABEND_PATIENT_ID]), + ]), + ]); +} + +@Injectable() +export class AnomalyDetectorRepository { + constructor( + @Inject(KYSELY_CLIENT_PROVIDER_TOKEN) private readonly db: Kysely, + ) {} + + async toursWithoutGermanZipInInterval(interval: Interval): Promise { + return await this.db + .selectFrom('Tour') + .selectAll() + .where((eb) => + eb.and([ + eb.or([ + eb(sql`LENGTH("startZip")`, '<', 5), + eb(sql`LENGTH("targetZip")`, '<', 5), + ]), + eb('Tour.startDate', '>=', new Date(interval.start)), + eb('Tour.startDate', '<=', new Date(interval.end)), + onlyRealTours(eb), + ]), + ) + .execute(); + } + + async employeeStillWorking( + date: Date, + ): Promise<{ employeeId: number; checkIn: Date; checkOut: Date }[]> { + const eventWithNextCte = this.db.with('EventWithNext', (db) => + db + .selectFrom('AttendanceRegistration') + .select([ + 'employeeId', + 'dateTime', + 'event', + sql` + LEAD("event") OVER ( + PARTITION BY "employeeId" + ORDER BY "dateTime" ASC + ) + `.as('nextEvent'), + ]) + .where('dateTime', '>=', subDays(date, 1)) + .where('dateTime', '<=', addDays(date, 2)), + ); + + const filteredEventsCte = eventWithNextCte.with('FilteredEvents', (db) => + db + .selectFrom('EventWithNext') + .select(['employeeId', 'dateTime', 'event']) + .where((eb) => + eb.or([ + eb('event', '=', 'clocked out'), + eb.and([ + eb('event', '=', 'clocked in'), + eb.or([ + eb('nextEvent', 'is', null), + eb('nextEvent', '!=', 'clocked in'), + ]), + ]), + ]), + ), + ); + + const pairedEventsCte = filteredEventsCte.with('PairedEvents', (db) => + db + .selectFrom('FilteredEvents') + .select([ + 'employeeId', + 'dateTime', + 'event', + sql`LEAD("dateTime") OVER (PARTITION BY "employeeId" ORDER BY "dateTime" ASC)`.as( + 'nextDateTime', + ), + sql`LEAD("event") OVER (PARTITION BY "employeeId" ORDER BY "dateTime" ASC)`.as( + 'nextEvent', + ), + ]), + ); + + return await pairedEventsCte + .selectFrom('PairedEvents') + .select(['employeeId', 'dateTime as checkIn', 'nextDateTime as checkOut']) + .where('event', '=', 'clocked in') + .where('dateTime', '>=', startOfDay(date)) + .where('dateTime', '<=', endOfDay(date)) + .where((eb) => + eb.or([ + eb.and([ + eb('nextEvent', '=', 'clocked out'), + eb(sql`"nextDateTime" - "dateTime"`, '>', sql`interval '23 hours'`), + ]), + eb.and([ + eb('nextEvent', 'is', null), + eb( + sql`${new Date()}::timestamp - "dateTime"`, + '>', + sql`interval '23 hours'`, + ), + ]), + ]), + ) + .execute(); + } + + async attendanceBeforeWorkTime(day: Date): Promise< + { + userId: string; + checkInTime: Date; + startWorkTime: Date; + name: string; + }[] + > { + const dailyWorkTimeCte = this.db.with('DailyWorkTime', (db) => + db + .selectFrom('WorkTime') + .select([ + 'WorkTime.userId', + 'WorkTime.startDate as startWorkTime', + 'WorkTime.firstname', + 'WorkTime.surname', + ]) + .where('WorkTime.startDate', '>=', startOfDay(day)) + .where('WorkTime.startDate', '<', endOfDay(day)) + .where('WorkTime.startDate', 'is not', null), + ); + + const firstCheckInCte = dailyWorkTimeCte.with('FirstCheckIn', (db) => + db + .selectFrom('AttendanceRegistration') + .select([ + 'AttendanceRegistration.employeeId', + (eb) => + eb.fn.min('AttendanceRegistration.dateTime').as('checkInTime'), + ]) + .where('AttendanceRegistration.event', '=', 'clocked in') + .where('AttendanceRegistration.dateTime', '>=', startOfDay(day)) + .where('AttendanceRegistration.dateTime', '<', endOfDay(day)) + .where('AttendanceRegistration.dateTime', 'is not', null) + .groupBy('AttendanceRegistration.employeeId'), + ); + + return await firstCheckInCte + .selectFrom('DailyWorkTime') + .innerJoin('FirstCheckIn', (eb) => + eb.onRef( + sql`"DailyWorkTime"."userId"::integer`, + '=', + 'FirstCheckIn.employeeId', + ), + ) + .select([ + 'DailyWorkTime.userId', + 'FirstCheckIn.checkInTime', + 'DailyWorkTime.startWorkTime', + sql`"DailyWorkTime"."firstname" || ' ' || "DailyWorkTime"."surname"`.as( + 'name', + ), + ]) + .whereRef( + 'FirstCheckIn.checkInTime', + '>', + sql`"DailyWorkTime"."startWorkTime" + interval '60 seconds'`, + ) + .execute(); + } + + async findWorkForPlannedTime(day: Date) { + const aggregatedPlannedTimeCte = this.db.with( + 'AggregatedPlannedTime', + (db) => + db + .selectFrom('PlannedTime') + .select([ + 'PlannedTime.userId', + sql`MIN("firstname")`.as('firstname'), + sql`MIN("surname")`.as('surname'), + (eb) => eb.fn.min('PlannedTime.startDate').as('plannedStart'), + (eb) => eb.fn.max('PlannedTime.endDate').as('plannedEnd'), + ]) + .where('PlannedTime.startDate', '>=', startOfDay(day)) + .where('PlannedTime.startDate', '<', endOfDay(day)) + .groupBy('PlannedTime.userId'), + ); + + const aggregatedWorkTimeCte = aggregatedPlannedTimeCte.with( + 'AggregatedWorkTime', + (db) => + db + .selectFrom('WorkTime') + .select([ + 'WorkTime.userId', + (eb) => eb.fn.min('WorkTime.startDate').as('actualStart'), + (eb) => eb.fn.max('WorkTime.endDate').as('actualEnd'), + sql`SUM("pause")`.as('actualPause'), + sql`SUM("duration")`.as('actualDuration'), + ]) + .where('WorkTime.startDate', '>=', startOfDay(day)) + .where('WorkTime.startDate', '<', endOfDay(day)) + .groupBy('WorkTime.userId'), + ); + + return await aggregatedWorkTimeCte + .selectFrom('AggregatedPlannedTime as pt') + .leftJoin('AggregatedWorkTime as wt', 'wt.userId', 'pt.userId') + .select([ + 'pt.userId', + 'pt.firstname', + 'pt.surname', + 'pt.plannedStart', + 'pt.plannedEnd', + 'wt.actualStart', + 'wt.actualEnd', + sql`("wt"."actualEnd" - "wt"."actualStart") - ("pt"."plannedEnd" - "pt"."plannedStart")`.as( + 'diff', + ), + ]) + .where((eb) => + eb.or([ + // eb('wt.userId', 'is', null), + sql`("wt"."actualEnd" - "wt"."actualStart") - ("pt"."plannedEnd" - "pt"."plannedStart") > interval '2 hours'`, + ]), + ) + .execute(); + } +} diff --git a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/anomaly-detector.service.ts b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/anomaly-detector.service.ts index 065c4ef..1e950ae 100644 --- a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/anomaly-detector.service.ts +++ b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/anomaly-detector.service.ts @@ -14,6 +14,7 @@ import { EmployeeService } from 'src/modules/feat-business-objects/entities/empl import { PlannedTimeService } from 'src/modules/feat-business-objects/entities/planned-time/planned-time.service'; import { TourService } from 'src/modules/feat-business-objects/entities/tour/tour.service'; import { WorkTimeService } from 'src/modules/feat-business-objects/entities/work-time/work-time.service'; +import { AnomalyDetectorRepository } from './anomaly-detector.repository'; export type AnomalySkeleton = Pick< Prisma.AnomalyCreateInput, @@ -33,8 +34,36 @@ export class AnomalyDetectorService { private readonly plannedTimeService: PlannedTimeService, private readonly tourService: TourService, private readonly workTimeService: WorkTimeService, + private readonly anomalyRepository: AnomalyDetectorRepository, ) {} + public findToursWithoutGermanZipInInterval( + interval: Interval, + ): Promise { + return this.anomalyRepository.toursWithoutGermanZipInInterval(interval); + } + + public findEmployeeStillWorking( + date: Date, + ): Promise<{ employeeId: number; checkIn: Date; checkOut: Date }[]> { + return this.anomalyRepository.employeeStillWorking(date); + } + + public findAttendanceBeforeWorkTime(day: Date): Promise< + { + userId: string; + checkInTime: Date; + startWorkTime: Date; + name: string; + }[] + > { + return this.anomalyRepository.attendanceBeforeWorkTime(day); + } + + public findWorkForPlannedTime(day: Date) { + return this.anomalyRepository.findWorkForPlannedTime(day); + } + /** * Find a list of tours in a given time interval * @param interval An interval defining the time range of tours diff --git a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/driver-certificate.detector.ts b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/driver-certificate.detector.ts index bb9ccd1..fe9986d 100644 --- a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/driver-certificate.detector.ts +++ b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/driver-certificate.detector.ts @@ -11,6 +11,9 @@ export class DriverCertificateDetector extends AnomalyDetector { readonly type = AnomalyTypes.DriverCertificate; async detect(inInterval: Interval): Promise { + this.logger.log( + `start '${DriverCertificateDetector.name}' with interval ${format(inInterval.start, 'yyyy-MM-dd HH:mm')} - ${format(inInterval.end, 'yyyy-MM-dd HH:mm')}`, + ); const toursInRange = await this.dataService.findToursInInterval(inInterval); const anomalies = await Promise.all( diff --git a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/driver-codriver-same.detector.ts b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/driver-codriver-same.detector.ts index 6650855..a722931 100644 --- a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/driver-codriver-same.detector.ts +++ b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/driver-codriver-same.detector.ts @@ -10,6 +10,9 @@ export class DriverCodriverSameDetector extends AnomalyDetector { readonly type = AnomalyTypes.DriverCodriverSame; async detect(inInterval: Interval): Promise { + this.logger.log( + `start '${DriverCodriverSameDetector.name}' with interval ${format(inInterval.start, 'yyyy-MM-dd HH:mm')} - ${format(inInterval.end, 'yyyy-MM-dd HH:mm')}`, + ); const toursInRange = await this.dataService.findToursInInterval( inInterval, ['KTW'], diff --git a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/employee-still-working.detector.ts b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/employee-still-working.detector.ts new file mode 100644 index 0000000..185f733 --- /dev/null +++ b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/employee-still-working.detector.ts @@ -0,0 +1,83 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { AnomalyDetector } from './anomaly-detector.class'; +import { Prisma, Anomaly } from '@prisma/client'; +import { eachDayOfInterval, format, Interval, parse } from 'date-fns'; +import { AnomalyTypes } from 'src/modules/feat-business-objects/entities/anomaly/anomaly-types.enum'; +import { UNGROUPED_ANOMALIES } from 'src/modules/feat-business-objects/entities/anomaly/anomalies.token'; + +@Injectable() +export class EmployeeStillWorkingDetector extends AnomalyDetector { + private readonly logger = new Logger(EmployeeStillWorkingDetector.name); + readonly type = AnomalyTypes.EmployeeStillWorking; + + async detect(inInterval: Interval): Promise { + this.logger.log( + `start '${EmployeeStillWorkingDetector.name}' with interval ${format(inInterval.start, 'yyyy-MM-dd HH:mm')} - ${format(inInterval.end, 'yyyy-MM-dd HH:mm')}`, + ); + const anomalies = []; + for (const day of eachDayOfInterval(inInterval)) { + const longWorkingEmployees = + await this.dataService.findEmployeeStillWorking(day); + + console.log(longWorkingEmployees) + if (longWorkingEmployees.length > 0) { + anomalies.push( + this.createAnomaly( + `${format(longWorkingEmployees.at(0).checkIn, 'yyyy/MM/dd')}-${this.type}`, + longWorkingEmployees.at(0).checkIn, + ), + ); + } + } + return anomalies; + } + async validate(anomaly: Anomaly): Promise { + const day = parse(anomaly.id.split('-').at(0), 'yyyy/MM/dd', new Date()); + const longWorkingEmployees = + await this.dataService.findEmployeeStillWorking(day); + + if (longWorkingEmployees.length == 0) { + return { + ...anomaly, + resolvedAt: new Date(), + }; + } + return { + ...anomaly, + }; + } + createDescription({ day }: { day: Date }): string { + return `${format(day, 'dd.MM.yyyy')}: Mitarbeiter arbeiten länger als 24 Stunden`; + } + createSolution({ day }: { day: Date }): Prisma.SolutionTargetCreateInput[] { + return [ + { + name: 'Dyflexis', + steps: { + create: [ + { + description: `Öffnen Sie die Stundenregistrierung für den ${format(day, 'dd.MM.yyyy')}.`, + }, + ], + }, + }, + ]; + } + + private createAnomaly( + anomalyId: string, + day: Date, + ): Prisma.AnomalyCreateInput { + return { + ...this.getAnomalySkeleton(), + id: anomalyId, + type: this.type, + groupKey: UNGROUPED_ANOMALIES, + groupDescription: '', + solution: { + create: this.createSolution({ day }), + }, + description: this.createDescription({ day }), + }; + } +} diff --git a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/employees-logged-in.detector.ts b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/employees-logged-in.detector.ts index 6fa6140..46512fc 100644 --- a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/employees-logged-in.detector.ts +++ b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/employees-logged-in.detector.ts @@ -22,6 +22,9 @@ export class EmployeesLoggedInDetector extends AnomalyDetector { readonly type = AnomalyTypes.EmployeesLoggedIn; async detect(inInterval: Interval): Promise { + this.logger.log( + `start '${EmployeesLoggedInDetector.name}' with interval ${format(inInterval.start, 'yyyy-MM-dd HH:mm')} - ${format(inInterval.end, 'yyyy-MM-dd HH:mm')}`, + ); const toursInRange = await this.dataService.findToursInInterval(inInterval); const anomaliesByTour = await Promise.all( @@ -41,14 +44,17 @@ export class EmployeesLoggedInDetector extends AnomalyDetector { const employeeId = remainingIdParts.slice(0, -1).join('-'); const tour = await this.dataService.findTourByOperationId(operationId); const employee = await this.dataService.findEmployeeById(employeeId); + if(!tour?.startDate) { + this.logger.error(`tour ${tour?.operationId} has no startDate`) + } const startOfTour = this.getStartOfTour( - new Date(tour.startDate), + new Date(tour?.startDate), [ - tour.startBegin, - tour.startEnd, - tour.target, - tour.targetBegin, - tour.empty, + tour?.startBegin, + tour?.startEnd, + tour?.target, + tour?.targetBegin, + tour?.empty, ].filter((val) => !!val)[0], ); diff --git a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/is-attendance-before-worktime.detector.ts b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/is-attendance-before-worktime.detector.ts new file mode 100644 index 0000000..27ff0c7 --- /dev/null +++ b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/is-attendance-before-worktime.detector.ts @@ -0,0 +1,99 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { AnomalyDetector } from './anomaly-detector.class'; +import { Prisma, Anomaly } from '@prisma/client'; +import { eachDayOfInterval, format, Interval } from 'date-fns'; +import { AnomalyTypes } from 'src/modules/feat-business-objects/entities/anomaly/anomaly-types.enum'; +import { UNGROUPED_ANOMALIES } from 'src/modules/feat-business-objects/entities/anomaly/anomalies.token'; +import { toZonedTime } from 'date-fns-tz'; + +@Injectable() +export class IsAttendanceBeforeWorktimeDetector extends AnomalyDetector { + private logger = new Logger(IsAttendanceBeforeWorktimeDetector.name); + readonly type = AnomalyTypes.IsAttendanceBeforeWorktime; + + async detect(inInterval: Interval): Promise { + this.logger.log( + `start '${IsAttendanceBeforeWorktimeDetector.name}' with interval ${format(inInterval.start, 'yyyy-MM-dd HH:mm')} - ${format(inInterval.end, 'yyyy-MM-dd HH:mm')}`, + ); + const anomalies = []; + + for (const day of eachDayOfInterval(inInterval)) { + const attendancesAfterWorktime = + await this.dataService.findAttendanceBeforeWorkTime(day); + + for (const attendanceAfterWorkTime of attendancesAfterWorktime) { + const { userId, checkInTime, startWorkTime, name } = + attendanceAfterWorkTime; + anomalies.push( + this.createAnomaly( + `${format(checkInTime, 'yyyy/MM/dd_HH:mm:ss')}-${userId}-${this.type}`, + day, + name, + checkInTime, + startWorkTime, + ), + ); + } + } + + return anomalies; + } + validate(anomaly: Anomaly): Promise { + return + } + createDescription({ day, name }: { day: Date; name: string }): string { + return `${format(day, 'dd.MM.yyyy')}: ${name}'s Stempelzeit liegt in/nach der Arbeitszeit`; + } + createSolution({ + day, + name, + checkIn, + workTime, + }: { + day: Date; + name: string; + checkIn: Date; + workTime: Date; + }): Prisma.SolutionTargetCreateInput[] { + return [ + { + name: 'Dyflexis', + steps: { + create: [ + { + description: `${name} hat sich am ${format(day, 'dd.MM.yy')}, um ${format(toZonedTime(checkIn, 'Europe/Berlin'), 'HH:mm')} Uhr eingestempelt, doch bereits um ${format(toZonedTime(workTime, 'Europe/Berlin'), 'HH:mm')} Uhr mit der Arbeit begonnen.`, + }, + { + description: `Öffnen Sie die Stundenregistrierung für den ${format(day, 'dd.MM.yyyy')}.`, + }, + ], + }, + }, + ]; + } + + private createAnomaly( + anomalyId: string, + day: Date, + employeeName: string, + checkInTime: Date, + workTime: Date, + ): Prisma.AnomalyCreateInput { + return { + ...this.getAnomalySkeleton(), + id: anomalyId, + type: this.type, + groupKey: UNGROUPED_ANOMALIES, + groupDescription: '', + solution: { + create: this.createSolution({ + day, + name: employeeName, + checkIn: checkInTime, + workTime, + }), + }, + description: this.createDescription({ day, name: employeeName }), + }; + } +} diff --git a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/is-german-zip.detector.ts b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/is-german-zip.detector.ts new file mode 100644 index 0000000..3fe3d61 --- /dev/null +++ b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/is-german-zip.detector.ts @@ -0,0 +1,83 @@ +import { Prisma, Anomaly } from '@prisma/client'; +import { format, Interval } from 'date-fns'; +import { AnomalyDetector } from './anomaly-detector.class'; +import { Logger } from '@nestjs/common'; +import { AnomalyTypes } from 'src/modules/feat-business-objects/entities/anomaly/anomaly-types.enum'; +import { toZonedTime } from 'date-fns-tz'; + +export class IsGermanZipDetector extends AnomalyDetector { + private readonly logger = new Logger(IsGermanZipDetector.name); + readonly type = AnomalyTypes.IsGermanZip; + + async detect(interval: Interval): Promise { + this.logger.log( + `start '${IsGermanZipDetector.name}' with interval ${format(interval.start, 'yyyy-MM-dd HH:mm')} - ${format(interval.end, 'yyyy-MM-dd HH:mm')}`, + ); + const tours = await this.dataService.findToursWithoutGermanZipInInterval(interval) + + return await Promise.all( + tours.map(tour => this.createAnomaly(`${tour.operationId}-${this.type}`, tour.id, tour.operationId, tour.startDate)) + ) + } + async validate(anomaly: Anomaly): Promise { + const [operationId] = anomaly.id.split('-') + try { + const tour = await this.dataService.findTourByOperationId(operationId) + + if(tour?.startZip.length == 5 && tour?.targetZip.length == 5) { + return { + ...anomaly, + resolvedAt: new Date() + } + } else { + return { + ...anomaly + } + } + } catch (error) { + this.logger.error(error, {anomalyId: anomaly.id}) + } + } + createDescription({operationId}: {operationId: string}): string { + return `${operationId}: PLZ ungültig` + } + createSolution({tourId}: {tourId: string}): Prisma.SolutionTargetCreateInput[] { + return [ + { + name: 'DispoLive', + steps: { + create: [ + { + description: `Öffnen Sie den Fahrbericht der Tour und ändern Sie die fehlerhafte Postleitzahl.`, + }, + ], + }, + }, + ]; + } + + private createAnomaly( + anomalyId: string, + tourId: string, + operationId: 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({tourId}) + }, + description: this.createDescription({operationId}) + } + } +} diff --git a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/is-work-time-planned.detector.ts b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/is-work-time-planned.detector.ts new file mode 100644 index 0000000..3a28403 --- /dev/null +++ b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/is-work-time-planned.detector.ts @@ -0,0 +1,34 @@ +import { Injectable, Logger } from "@nestjs/common"; +import { AnomalyDetector } from "./anomaly-detector.class"; +import { Prisma, Anomaly } from "@prisma/client"; +import { eachDayOfInterval, format, Interval } from "date-fns"; +import { AnomalyTypes } from "src/modules/feat-business-objects/entities/anomaly/anomaly-types.enum"; + +@Injectable() +export class IsWorkTimePlannedDetector extends AnomalyDetector { + private logger = new Logger(IsWorkTimePlannedDetector.name) + readonly type = AnomalyTypes.IsWorktimePlanned + + async detect(inInterval: Interval): Promise { + this.logger.log( + `start '${IsWorkTimePlannedDetector.name}' with interval ${format(inInterval.start, 'yyyy-MM-dd HH:mm')} - ${format(inInterval.end, 'yyyy-MM-dd HH:mm')}`, + ); + const anomalies = [] + + for (const day of eachDayOfInterval(inInterval)){ + const workTimeWithoutPlan = await this.dataService.findWorkForPlannedTime(day) + console.log(day, workTimeWithoutPlan) + } + + return + } + validate(anomaly: Anomaly): Promise { + throw new Error("Method not implemented."); + } + createDescription(data: unknown): string { + throw new Error("Method not implemented."); + } + createSolution(data: unknown): Prisma.SolutionTargetCreateInput[] { + throw new Error("Method not implemented."); + } +} \ No newline at end of file diff --git a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/one-tour-at-a-time.detector.ts b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/one-tour-at-a-time.detector.ts index 4c6bd98..32946a6 100644 --- a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/one-tour-at-a-time.detector.ts +++ b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/one-tour-at-a-time.detector.ts @@ -33,6 +33,9 @@ export class OneTourAtATimeDetector extends AnomalyDetector { } async detect(inInterval: Interval): Promise { + this.logger.log( + `start '${OneTourAtATimeDetector.name}' with interval ${format(inInterval.start, 'yyyy-MM-dd HH:mm')} - ${format(inInterval.end, 'yyyy-MM-dd HH:mm')}`, + ); const toursInRange = await this.dataService.findToursInInterval(inInterval); const toursMap = this.groupToursByDay(toursInRange); diff --git a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/working-employee-not-in-distribution-system.detector.ts b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/working-employee-not-in-distribution-system.detector.ts index 7fbe7e1..ad45dd2 100644 --- a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/working-employee-not-in-distribution-system.detector.ts +++ b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/working-employee-not-in-distribution-system.detector.ts @@ -14,6 +14,9 @@ export class WorkingEmployeeNotInDistributionSystemDetector extends AnomalyDetec readonly type = AnomalyTypes.WorkingEmployeeNotInDistributionSystem; async detect(inInterval: Interval): Promise { + this.logger.log( + `start '${WorkingEmployeeNotInDistributionSystemDetector.name}' with interval ${format(inInterval.start, 'yyyy-MM-dd HH:mm')} - ${format(inInterval.end, 'yyyy-MM-dd HH:mm')}`, + ); const attendanceRegistrationInInterval = await this.dataService.findAttendanceRegistrationInInterval(inInterval, [ AttendanceRegistrationEvents.ClockedIn, diff --git a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/anomaly/anomalies.token.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/anomaly/anomalies.token.ts index 638aa37..7f5582e 100644 --- a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/anomaly/anomalies.token.ts +++ b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/anomaly/anomalies.token.ts @@ -1 +1,2 @@ -export const ANOMALY_SLEEP_TIME_MS = 60 * 60 * 1000; \ No newline at end of file +export const ANOMALY_SLEEP_TIME_MS = 60 * 60 * 1000; +export const UNGROUPED_ANOMALIES = 'ungrouped'; \ No newline at end of file diff --git a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/anomaly/anomaly-types.enum.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/anomaly/anomaly-types.enum.ts index 5f9a6d7..1a4f04b 100644 --- a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/anomaly/anomaly-types.enum.ts +++ b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/anomaly/anomaly-types.enum.ts @@ -2,6 +2,10 @@ export enum AnomalyTypes { DriverCodriverSame = 'DriverCodriverSame', DriverCertificate = 'DriverCertificate', EmployeesLoggedIn = 'EmployeesLoggedIn', + EmployeeStillWorking = 'EmployeeStillWorking', + IsAttendanceBeforeWorktime = 'IsAttendanceBeforeWorktime', + IsGermanZip = 'IsGermanZip', + IsWorktimePlanned = 'IsWorktimePlanned', OneTourAtATime = 'OneTourAtATime', WorkingEmployeeNotInDistributionSystem = 'WorkingEmployeeNotInDistributionSystem', } diff --git a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/attendance-registration/attendance-registration.repository.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/attendance-registration/attendance-registration.repository.ts index 79f0d6f..2aec899 100644 --- a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/attendance-registration/attendance-registration.repository.ts +++ b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/attendance-registration/attendance-registration.repository.ts @@ -1,21 +1,29 @@ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { AttendanceRegistration } from '@prisma/client'; import { Interval } from 'date-fns'; -import { PrismaService } from 'src/core/database/prisma.service'; +import { Kysely } from 'kysely'; +import { KYSELY_CLIENT_PROVIDER_TOKEN } from 'src/core/database/database.module'; +import { DB } from 'src/core/database/types'; import { AttendanceRegistrationEvents } from './attendance-registration-events.enum'; @Injectable() export class AttendanceRegistrationRepository { - constructor(private readonly prisma: PrismaService) {} + constructor( + @Inject(KYSELY_CLIENT_PROVIDER_TOKEN) private readonly db: Kysely, + ) {} async findById( attendanceRegistrationId: number, ): Promise { - return await this.prisma.attendanceRegistration.findUnique({ - where: { + return await this.db + .selectFrom('AttendanceRegistration') + .selectAll() + .where( + 'AttendanceRegistration.attendanceRegistrationId', + '=', attendanceRegistrationId, - }, - }); + ) + .executeTakeFirst(); } async findInInterval( @@ -25,28 +33,14 @@ export class AttendanceRegistrationRepository { AttendanceRegistrationEvents.ClockedOut, ], ): Promise { - return await this.prisma.attendanceRegistration.findMany({ - where: { - event: { - in: eventTypes, - }, - AND: [ - { - dateTime: { - gte: new Date(interval.start), - }, - }, - { - dateTime: { - lt: new Date(interval.end), - }, - }, - ], - }, - orderBy: { - dateTime: 'asc', - }, - }); + return await this.db + .selectFrom('AttendanceRegistration') + .selectAll() + .where('AttendanceRegistration.event', 'in', eventTypes) + .where('AttendanceRegistration.dateTime', '>=', new Date(interval.start)) + .where('AttendanceRegistration.dateTime', '<', new Date(interval.end)) + .orderBy('AttendanceRegistration.dateTime asc') + .execute(); } async findByEmployeeInInterval( @@ -57,28 +51,14 @@ export class AttendanceRegistrationRepository { AttendanceRegistrationEvents.ClockedOut, ], ): Promise { - return await this.prisma.attendanceRegistration.findMany({ - where: { - employeeId, - event: { - in: eventTypes, - }, - AND: [ - { - dateTime: { - gte: new Date(timeRange.start), - }, - }, - { - dateTime: { - lt: new Date(timeRange.end), - }, - }, - ], - }, - orderBy: { - dateTime: 'asc', - }, - }); + return await this.db + .selectFrom('AttendanceRegistration') + .selectAll() + .where('AttendanceRegistration.employeeId', '=', employeeId) + .where('AttendanceRegistration.event', 'in', eventTypes) + .where('AttendanceRegistration.dateTime', '>=', new Date(timeRange.start)) + .where('AttendanceRegistration.dateTime', '<', new Date(timeRange.end)) + .orderBy('AttendanceRegistration.dateTime asc') + .execute(); } } diff --git a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/employee/employee.repository.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/employee/employee.repository.ts index 633fe93..013d8bb 100644 --- a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/employee/employee.repository.ts +++ b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/employee/employee.repository.ts @@ -1,16 +1,28 @@ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { Employee } from '@prisma/client'; -import { PrismaService } from 'src/core/database/prisma.service'; +import { Kysely } from 'kysely'; +import { KYSELY_CLIENT_PROVIDER_TOKEN } from 'src/core/database/database.module'; +import { DB } from 'src/core/database/types'; @Injectable() export class EmployeeRepository { - constructor(private readonly prisma: PrismaService) {} + constructor( + @Inject(KYSELY_CLIENT_PROVIDER_TOKEN) private readonly db: Kysely, + ) {} async findById(employeeId: string): Promise { - return await this.prisma.employee.findUnique({ where: { id: employeeId } }); + return await this.db + .selectFrom('Employee') + .selectAll() + .where('Employee.id', '=', employeeId) + .executeTakeFirst(); } async findByPersonnelNumber(personnelNumber: string): Promise { - return await this.prisma.employee.findFirst({ where: { personnelNumber } }); + return await this.db + .selectFrom('Employee') + .selectAll() + .where('Employee.personnelNumber', '=', personnelNumber) + .executeTakeFirst(); } } diff --git a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/planned-time/planned-time.repository.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/planned-time/planned-time.repository.ts index 5379b8f..0a78519 100644 --- a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/planned-time/planned-time.repository.ts +++ b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/planned-time/planned-time.repository.ts @@ -1,11 +1,17 @@ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { PlannedTime } from '@prisma/client'; -import { Interval } from 'date-fns'; +import { Interval, intervalToDuration } from 'date-fns'; +import { Kysely } from 'kysely'; +import { KYSELY_CLIENT_PROVIDER_TOKEN } from 'src/core/database/database.module'; import { PrismaService } from 'src/core/database/prisma.service'; +import { DB } from 'src/core/database/types'; @Injectable() export class PlannedTimeRepository { - constructor(private readonly prisma: PrismaService) {} + constructor( + private readonly prisma: PrismaService, + @Inject(KYSELY_CLIENT_PROVIDER_TOKEN) private readonly db: Kysely, + ) {} async findById(id: string): Promise { return await this.prisma.plannedTime.findUnique({ @@ -17,23 +23,13 @@ export class PlannedTimeRepository { personnelNumber: string, interval: Interval, ): Promise { - return await this.prisma.plannedTime.findMany({ - where: { - userId: personnelNumber, - AND: [ - { - startDate: { - gte: new Date(interval.start), - }, - }, - { - startDate: { - lt: new Date(interval.end), - }, - }, - ], - }, - }); + return await this.db + .selectFrom('PlannedTime') + .selectAll() + .where('PlannedTime.userId', '=', personnelNumber) + .where('PlannedTime.startDate', '>=', new Date(interval.start)) + .where('PlannedTime.startDate', '<', new Date(interval.end)) + .execute(); } async findByOrdinanceTypesInInterval( diff --git a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/ticket/ticket.repository.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/ticket/ticket.repository.ts index 332e15a..0897bba 100644 --- a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/ticket/ticket.repository.ts +++ b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/ticket/ticket.repository.ts @@ -133,18 +133,13 @@ export class TicketRepository { } async findIdByTourId(tourId: string): Promise<{ id: string }> { - return await this.prisma.ticket.findFirst({ - select: { - id: true, - }, - where: { - tours: { - some: { - id: tourId, - }, - }, - }, - }); + return await this.db + .selectFrom('Ticket') + .select(['Ticket.id']) + .innerJoin('Tour', 'Tour.ticketId', 'Ticket.id') + .where('Tour.id', '=', tourId) + .limit(1) + .executeTakeFirst(); } async search(query: string): Promise<(Ticket & { tours: Tour[] })[]> { diff --git a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/tour/tour.repository.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/tour/tour.repository.ts index ba815a8..df869c7 100644 --- a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/tour/tour.repository.ts +++ b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/tour/tour.repository.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Prisma, Tour } from '@prisma/client'; import { endOfDay, Interval, startOfDay } from 'date-fns'; -import { Kysely } from 'kysely'; +import { ExpressionBuilder, Kysely } from 'kysely'; import { KYSELY_CLIENT_PROVIDER_TOKEN } from 'src/core/database/database.module'; import { PrismaService } from 'src/core/database/prisma.service'; import { DB } from 'src/core/database/types'; @@ -12,6 +12,11 @@ import { TourAccountingFilterDto, } from './tours.filter'; +type TourEb = ExpressionBuilder; + +const PAUSE_PATIENT_ID = 'b7424477-a4b2-42f7-b7e7-a4bef24f6979'; +const FEIERABEND_PATIENT_ID = '3554e1a9-9c07-45e5-b53c-692a77e4fa9a'; + function onlyRealTours(): Prisma.TourWhereInput[] { return [ { @@ -58,13 +63,13 @@ function onlyRealTours(): Prisma.TourWhereInput[] { { patientId: { // Pause - not: 'b7424477-a4b2-42f7-b7e7-a4bef24f6979', + not: PAUSE_PATIENT_ID, }, }, { patientId: { // Feierabend - not: '3554e1a9-9c07-45e5-b53c-692a77e4fa9a', + not: FEIERABEND_PATIENT_ID, }, }, ], @@ -74,6 +79,24 @@ function onlyRealTours(): Prisma.TourWhereInput[] { ]; } +function onlyRealToursKysely(eb: TourEb) { + return eb.and([ + eb.or([ + eb('carName', 'is', null), + eb('carName', 'not in', ['Storno', 'Löschen', 'Abgabe']), + ]), + + eb('check', '!=', 3), + + eb('deletedAt', 'is', null), + + eb.or([ + eb('patientId', 'is', null), + eb('patientId', 'not in', [PAUSE_PATIENT_ID, FEIERABEND_PATIENT_ID]), + ]), + ]); +} + @Injectable() export class TourRepository { constructor( @@ -164,42 +187,24 @@ export class TourRepository { employeeId: string, ordinanceTypes: string[] = [], ): Promise { - return this.prisma.tour.findMany({ - where: { - AND: [ - { - OR: [ - { - driverId: employeeId, - }, - { - codriverId: employeeId, - }, - ], - }, - { - startDate: { - gte: new Date(start), - }, - }, - { - startDate: { - lt: new Date(end), - }, - }, - { - OR: [ - ...(ordinanceTypes.length - ? ordinanceTypes.map((ot) => ({ - ordinanceType: { contains: ot }, - })) - : []), - ], - }, - ...onlyRealTours(), - ], - }, - }); + return this.db + .selectFrom('Tour') + .selectAll() + .where((eb) => + eb.and([ + onlyRealToursKysely(eb), + eb.or([ + eb('Tour.driverId', '=', employeeId), + eb('Tour.codriverId', '=', employeeId), + ]), + eb('Tour.startDate', '>=', new Date(start)), + eb('Tour.startDate', '<', new Date(end)), + ...(ordinanceTypes.length > 0 + ? [eb('Tour.ordinanceType', 'in', ordinanceTypes)] + : []), + ]), + ) + .execute(); } async findByTicketId(ticketId: string): Promise { diff --git a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/work-time/work-time.repository.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/work-time/work-time.repository.ts index feeb4be..ff64f4b 100644 --- a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/work-time/work-time.repository.ts +++ b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/work-time/work-time.repository.ts @@ -1,38 +1,34 @@ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { WorkTime } from '@prisma/client'; import { Interval } from 'date-fns'; -import { PrismaService } from 'src/core/database/prisma.service'; +import { Kysely } from 'kysely'; +import { KYSELY_CLIENT_PROVIDER_TOKEN } from 'src/core/database/database.module'; +import { DB } from 'src/core/database/types'; @Injectable() export class WorkTimeRepository { - constructor(private readonly prisma: PrismaService) {} + constructor( + @Inject(KYSELY_CLIENT_PROVIDER_TOKEN) private readonly db: Kysely, + ) {} async findById(id: string): Promise { - return await this.prisma.workTime.findUnique({ - where: { id }, - }); + return await this.db + .selectFrom('WorkTime') + .selectAll() + .where('WorkTime.id', '=', id) + .executeTakeFirst(); } async findByPersonnelNumberInInterval( personnelNumber: string, interval: Interval, ): Promise { - return await this.prisma.workTime.findFirst({ - where: { - userId: personnelNumber, - AND: [ - { - startDate: { - gte: new Date(interval.start), - }, - }, - { - startDate: { - lte: new Date(interval.end), - }, - }, - ], - }, - }); + return await this.db + .selectFrom('WorkTime') + .selectAll() + .where('WorkTime.userId', '=', personnelNumber) + .where('WorkTime.startDate', '>=', new Date(interval.start)) + .where('WorkTime.startDate', '<=', new Date(interval.end)) + .executeTakeFirst(); } } diff --git a/service/data-hub/data-connector/src/modules/feat-calculation/feat-calculation.module.ts b/service/data-hub/data-connector/src/modules/feat-calculation/feat-calculation.module.ts index e3b7a7f..b020343 100644 --- a/service/data-hub/data-connector/src/modules/feat-calculation/feat-calculation.module.ts +++ b/service/data-hub/data-connector/src/modules/feat-calculation/feat-calculation.module.ts @@ -1,9 +1,9 @@ import { Module } from '@nestjs/common'; import { PrismaModule } from 'src/core/database/prisma.module'; +import { FeatBusinessObjectsModule } from '../feat-business-objects/feat-business-objects.module'; import { CalculationService } from './calculation.service'; import { PersonnelCostCalculationService } from './personnel-cost-calculation.service'; -import { FeatBusinessObjectsModule } from '../feat-business-objects/feat-business-objects.module'; @Module({ imports: [PrismaModule, FeatBusinessObjectsModule], diff --git a/service/data-hub/data-connector/src/modules/feat-etl/feat-extractors/data-access-extractors.module.ts b/service/data-hub/data-connector/src/modules/feat-etl/feat-extractors/data-access-extractors.module.ts index 7548d1d..90e4fa0 100644 --- a/service/data-hub/data-connector/src/modules/feat-etl/feat-extractors/data-access-extractors.module.ts +++ b/service/data-hub/data-connector/src/modules/feat-etl/feat-extractors/data-access-extractors.module.ts @@ -6,6 +6,7 @@ import { DyflexisExtractorsService } from './dyflexis-extractors.service'; import { ExtractorsService } from './extractors.service'; import { SourceRepository } from './source.repository'; import { DispoLiveApiExtractorsService } from './dispo-live-api-extractors.service'; +import { DyflexisUserAccessExtractorsService } from './dyflexis-user-access-extractors.service'; @Module({ imports: [PrismaModule, HttpModule], @@ -13,6 +14,7 @@ import { DispoLiveApiExtractorsService } from './dispo-live-api-extractors.servi ExtractorsService, DispoLiveExtractorsService, DispoLiveApiExtractorsService, + DyflexisUserAccessExtractorsService, DyflexisExtractorsService, SourceRepository, ], @@ -20,6 +22,7 @@ import { DispoLiveApiExtractorsService } from './dispo-live-api-extractors.servi ExtractorsService, DispoLiveApiExtractorsService, DispoLiveExtractorsService, + DyflexisUserAccessExtractorsService, DyflexisExtractorsService ], }) diff --git a/service/data-hub/data-connector/src/modules/feat-etl/feat-extractors/dyflexis-extractors.service.ts b/service/data-hub/data-connector/src/modules/feat-etl/feat-extractors/dyflexis-extractors.service.ts index 278e817..7f3174f 100644 --- a/service/data-hub/data-connector/src/modules/feat-etl/feat-extractors/dyflexis-extractors.service.ts +++ b/service/data-hub/data-connector/src/modules/feat-etl/feat-extractors/dyflexis-extractors.service.ts @@ -72,6 +72,7 @@ export class DyflexisExtractorsService { contract_end, contract_hours_week, contract_salary_hour, + contract_type_name, } = employee; return { id, @@ -82,6 +83,7 @@ export class DyflexisExtractorsService { contract_end, contract_hours_week, contract_salary_hour, + contract_type_name, }; }), ), diff --git a/service/data-hub/data-connector/src/modules/feat-etl/feat-extractors/dyflexis-user-access-extractors.service.ts b/service/data-hub/data-connector/src/modules/feat-etl/feat-extractors/dyflexis-user-access-extractors.service.ts new file mode 100644 index 0000000..e6c06fc --- /dev/null +++ b/service/data-hub/data-connector/src/modules/feat-etl/feat-extractors/dyflexis-user-access-extractors.service.ts @@ -0,0 +1,274 @@ +import { HttpService } from '@nestjs/axios'; +import { Injectable } from '@nestjs/common'; +import { SourceRepository } from './source.repository'; +import { ConfigService } from 'src/core/config/config.service'; +import { + catchError, + EMPTY, + expand, + forkJoin, + from, + map, + Observable, + of, + reduce, + switchMap, + tap, +} from 'rxjs'; +import { format, Interval } from 'date-fns'; +import { HolidayRaw, SickDaysRaw } from './source.interface'; + +interface DyflexisAuthHeader { + Cookie: string; + 'X-Authentication-Csrf-Token': string; +} + +interface DyflexisAppUser { + id: number; + publicId: string; + name: string; + initials: string; + email: string; +} + +@Injectable() +export class DyflexisUserAccessExtractorsService { + constructor( + private readonly http: HttpService, + private readonly repository: SourceRepository, + private readonly config: ConfigService, + ) {} + + loadData(): Observable { + // return this.extractSicknessDays({ + // start: new Date('2025-01-01T00:00:00.000Z'), + // end: new Date('2025-12-31T23:59:59.999Z'), + // }); + return this.extractHolidays(new Date('2025-01-01T00:00:00.000Z')); + } + + extractHolidays(year: Date): Observable { + return this.dyflexisLogin( + this.config.dyflexisAppUsername(), + this.config.dyflexisAppPassword(), + ).pipe( + switchMap((authHeader) => + forkJoin({ + authHeader: of(authHeader), + employees: this.fetchEmployees(authHeader), + }), + ), + switchMap(({ authHeader, employees }) => + forkJoin( + employees.map((employee) => + this.fetchHolidaysForEmployee(authHeader, employee.id, year), + ), + ), + ), + map((holidaysPerEmployee) => holidaysPerEmployee.flat(1)), + switchMap((holidays) => from(this.repository.createHolidays(holidays))), + map((res) => { + return { count: Number(res.at(0).numInsertedOrUpdatedRows) }; + }), + ); + } + + private fetchHolidaysForEmployee( + authHeader: DyflexisAuthHeader, + employeeId: number, + year: Date, + ): Observable<(HolidayRaw & { employeeId: number })[]> { + const endpoint = (employeeId: number, year: string) => + `hamburg/balance/holiday-balance-in-days/employee-balance-transaction/list/${employeeId}/${year}?sortBy=bookDate&descending=true&page=1&rowsPerPage=50`; + + return this.http + .get<{ + employeeBalanceTransactions: (HolidayRaw & { amountOfDays: number })[]; + }>( + `${this.config.dyflexisAppBaseUrl()}${endpoint(employeeId, year.getFullYear().toString())}`, + { + headers: { + ...authHeader, + }, + }, + ) + .pipe( + map((response) => + response.data.employeeBalanceTransactions + .filter(({ amountOfDays }) => amountOfDays < 0) + .map((holiday) => { + const { bookDate, isExcludedFromPayroll, isLocked } = holiday; + return { bookDate, isLocked, isExcludedFromPayroll, employeeId }; + }), + ), + ); + } + + extractSicknessDays(interval: Interval): Observable { + return this.dyflexisLogin( + this.config.dyflexisAppUsername(), + this.config.dyflexisAppPassword(), + ).pipe( + switchMap((authHeader) => + forkJoin({ + authHeader: of(authHeader), + employees: this.fetchEmployees(authHeader), + }), + ), + tap(({ employees }) => console.log(employees.length)), + switchMap(({ authHeader, employees }) => + forkJoin( + employees.map((employee) => + this.fetchSicknessDaysForEmployee( + authHeader, + employee.id, + interval, + ), + ), + ), + ), + map((sicknessDaysPerEmployee) => sicknessDaysPerEmployee.flat(1)), + switchMap((sicknessDays) => + from(this.repository.createSickdays(sicknessDays)), + ), + map((res) => { + return { count: Number(res.at(0).numInsertedOrUpdatedRows) }; + }), + ); + } + + private fetchSicknessDaysForEmployee( + authHeader: DyflexisAuthHeader, + employeeId: number, + interval: Interval, + ): Observable<(SickDaysRaw & { employeeId: number })[]> { + const endpoint = 'employee-management/sick-leave-registration-list/'; + + return this.http + .get<{ + sickLeaveRegistrations: SickDaysRaw[]; + }>( + `${this.config.dyflexisAppBaseUrl()}${endpoint}${employeeId}?startDate=${format(interval.start, 'yyyy-MM-dd')}&endDate=${format(interval.end, 'yyyy-MM-dd')}`, + { + headers: { + ...authHeader, + }, + }, + ) + .pipe( + map((response) => + response.data.sickLeaveRegistrations.map((sickDay) => { + const { startDate, endDate, days, hours, note, locked } = sickDay; + + return { + startDate, + endDate, + days, + hours, + note, + locked, + employeeId, + }; + }), + ), + ); + } + + private fetchEmployees( + authHeader: DyflexisAuthHeader, + ): Observable { + const employeeListEndpoint = 'employee-management/employee-list'; + + const getEmployeesPage = (pageNumber: number) => + this.http + .get<{ employees: DyflexisAppUser[] }>( + `${this.config.dyflexisAppBaseUrl()}${employeeListEndpoint}?page=${pageNumber}`, + { + headers: { + ...authHeader, + 'Content-Type': 'application/json', + }, + }, + ) + .pipe( + map((response) => + response.data.employees + .filter(({ name }) => name.indexOf('Shahrasebi') == -1) + .map((employee) => { + const { id, name, email } = employee; + return { id, name, email }; + }), + ), + ); + + return new Observable((observer) => + getEmployeesPage(1) + .pipe( + expand((data, i) => (data.length ? getEmployeesPage(i + 2) : EMPTY)), + reduce((acc, data) => [...acc, ...data], []), + catchError((error) => { + observer.error(error); + return of(error); + }), + ) + .subscribe((employees) => { + observer.next(employees); + observer.complete(); + }), + ); + } + + private dyflexisLogin( + username: string, + password: string, + ): Observable { + return this.http + .get('https://app.planning.nu/avicennaambulance/login') + .pipe( + map((response) => { + const cookie = response.headers['set-cookie']?.at(0).split(';').at(0); + + const html = response.data; + const nameAttr = 'authentication-csrf-token'; + const regex = new RegExp( + `]*name="${nameAttr}"[^>]*content="([^"]*)"[^>]*>`, + 'i', + ); + + const match = html.match(regex); + if (match && match[1]) { + return { + Cookie: cookie, + 'X-Authentication-Csrf-Token': match[1], + }; + } + return null; + }), + switchMap(({ Cookie, 'X-Authentication-Csrf-Token': csrfToken }) => + this.http + .post( + 'https://app.planning.nu/avicennaambulance/login/authenticate', + { + username, + password, + authenticatorCode: '', + rememberDevice: false, + }, + { + headers: { + 'Content-Type': 'application/json', + Cookie: Cookie, + 'X-Authentication-Csrf-Token': csrfToken, + }, + }, + ) + .pipe( + map((res) => ({ + Cookie: res.headers['set-cookie']?.at(0).split(';').at(0), + 'X-Authentication-Csrf-Token': csrfToken, + })), + ), + ), + ); + } +} diff --git a/service/data-hub/data-connector/src/modules/feat-etl/feat-extractors/extractors/tour.extractor.ts b/service/data-hub/data-connector/src/modules/feat-etl/feat-extractors/extractors/tour.extractor.ts index 1508455..6d7e38b 100644 --- a/service/data-hub/data-connector/src/modules/feat-etl/feat-extractors/extractors/tour.extractor.ts +++ b/service/data-hub/data-connector/src/modules/feat-etl/feat-extractors/extractors/tour.extractor.ts @@ -68,6 +68,9 @@ export class TourExtractor { beifahrerName, fahrer, fahrerName, + overRTW, + firmName, + schwerlast, color, } = tour; return { @@ -135,6 +138,9 @@ export class TourExtractor { beifahrerName: beifahrerName ? `${beifahrerName}` : undefined, fahrer: fahrer ? `${fahrer}` : undefined, fahrerName: fahrerName ? `${fahrerName}` : undefined, + overRTW: overRTW ? `${overRTW}` : undefined, + companyName: firmName ? `${firmName}` : undefined, + schwerlast: schwerlast ? `${schwerlast}` : undefined, color: color ? `${color}` : undefined, }; } diff --git a/service/data-hub/data-connector/src/modules/feat-etl/feat-extractors/source.interface.ts b/service/data-hub/data-connector/src/modules/feat-etl/feat-extractors/source.interface.ts index 90d492a..fee2946 100644 --- a/service/data-hub/data-connector/src/modules/feat-etl/feat-extractors/source.interface.ts +++ b/service/data-hub/data-connector/src/modules/feat-etl/feat-extractors/source.interface.ts @@ -67,6 +67,10 @@ export interface TourRaw { beifahrer?: string; // ID fahrerName?: string; fahrer?: string; // ID + + overRTW?: string; + firmName?: string; + schwerlast?: string; color?: string; // Hex rgb } @@ -141,6 +145,7 @@ export interface EmployeeDyflexisRaw { contract_end?: string; // yyyy-MM-dd contract_hours_week?: string; // Float string contract_salary_hour?: string; // Float string + contract_type_name?: string; } export interface TourSummaryDto { @@ -153,28 +158,44 @@ export interface TourSummaryDto { export interface FahrzeugplanungItemDto { _id?: string; - className: string; // 'greenItem' OR 'blocked' - group: string; // Fahrzeug ID - mitarbeiterId: string; // Mitarbeiter ID - start: string; // ISO 8601 TS (UTC) - end: string; // ISO 8601 TS (UTC) + className: string; // 'greenItem' OR 'blocked' + group: string; // Fahrzeug ID + mitarbeiterId: string; // Mitarbeiter ID + start: string; // ISO 8601 TS (UTC) + end: string; // ISO 8601 TS (UTC) } export interface FahrzeugItemDto { - _id: string - bezeichnung: string - marke: string - tuev: string - conn: string - carStatus: string - kaufDatum: string - erstzulassung: string - status: string - firmaId: string - kennzeichen: string - kaufPreis: string - kaufKm: string - fahrzeugIdentNr: string - kfzNr: string - verordnungsart: string + _id: string; + bezeichnung: string; + marke: string; + tuev: string; + conn: string; + carStatus: string; + kaufDatum: string; + erstzulassung: string; + status: string; + firmaId: string; + kennzeichen: string; + kaufPreis: string; + kaufKm: string; + fahrzeugIdentNr: string; + kfzNr: string; + verordnungsart: string; +} + + +export interface SickDaysRaw { + startDate: string; + endDate: string; + hours: number; + days: number; + note?: string; + locked: boolean; +} + +export interface HolidayRaw { + bookDate: string; + isLocked: boolean; + isExcludedFromPayroll: boolean; } \ No newline at end of file diff --git a/service/data-hub/data-connector/src/modules/feat-etl/feat-extractors/source.repository.ts b/service/data-hub/data-connector/src/modules/feat-etl/feat-extractors/source.repository.ts index 5f95997..edecad1 100644 --- a/service/data-hub/data-connector/src/modules/feat-etl/feat-extractors/source.repository.ts +++ b/service/data-hub/data-connector/src/modules/feat-etl/feat-extractors/source.repository.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { AttendanceRegistrationSource, EmployeeDispoLiveSource, @@ -6,6 +6,7 @@ import { PatientSource, PlannedTimeSource, Prisma, + Sickdays, TourSource, WorkTimeSource, } from '@prisma/client'; @@ -14,15 +15,23 @@ import { AttendanceRegistrationRaw, EmployeeDispoLiveRaw, EmployeeDyflexisRaw, + HolidayRaw, PatientRaw, PlannedTimeRaw, + SickDaysRaw, TourRaw, WorkTimeRaw, } from './source.interface'; +import { Kysely } from 'kysely'; +import { KYSELY_CLIENT_PROVIDER_TOKEN } from 'src/core/database/database.module'; +import { DB } from 'src/core/database/types'; @Injectable() export class SourceRepository { - constructor(private readonly prisma: PrismaService) {} + constructor( + private readonly prisma: PrismaService, + @Inject(KYSELY_CLIENT_PROVIDER_TOKEN) private readonly db: Kysely, + ) {} clearSource(): Promise { return Promise.all([ @@ -54,6 +63,55 @@ export class SourceRepository { }); } + createSickdays(sickdays: (SickDaysRaw & { employeeId: number })[]) { + return this.db.transaction().execute(async (trx) => { + await trx.deleteFrom('Sickdays').execute(); + return await this.db + .insertInto('Sickdays') + .values( + sickdays.map( + ({ + startDate, + endDate, + hours, + days, + note, + locked, + employeeId, + }): Partial => ({ + start: new Date(startDate), + end: new Date(endDate), + hours, + days, + note, + locked, + employeeId, + }), + ), + ) + .execute(); + }); + } + + createHolidays(holidays: (HolidayRaw & { employeeId: number })[]) { + return this.db.transaction().execute(async (trx) => { + await trx.deleteFrom('Holiday').execute(); + return await this.db + .insertInto('Holiday') + .values( + holidays.map( + ({ bookDate, isExcludedFromPayroll, isLocked, employeeId }) => ({ + date: new Date(bookDate), + locked: isLocked, + excludedFromPayroll: isExcludedFromPayroll, + employeeId, + }), + ), + ) + .execute(); + }); + } + createPatients( patients: PatientRaw[], jobId: string, diff --git a/service/data-hub/data-connector/src/modules/feat-etl/feat-importers/importer.service.ts b/service/data-hub/data-connector/src/modules/feat-etl/feat-importers/importer.service.ts index d3bf88b..2b33ac0 100644 --- a/service/data-hub/data-connector/src/modules/feat-etl/feat-importers/importer.service.ts +++ b/service/data-hub/data-connector/src/modules/feat-etl/feat-importers/importer.service.ts @@ -13,6 +13,7 @@ import { LoadingDoneEvent } from './events/loading-done.event'; import { TransformationDoneEvent } from './events/transformation-done.event'; import { TourUpdateChecker } from './producer/tour-update'; import { SchichtplanungSynchronizer } from './producer/schichtplanung'; +import { DyflexisUserAccessExtractorsService } from '../feat-extractors/dyflexis-user-access-extractors.service'; @Injectable() export class ImporterService { @@ -24,8 +25,13 @@ export class ImporterService { private readonly eventEmitter: EventEmitter2, private readonly tourUpdateChecker: TourUpdateChecker, private readonly schichtplanungSynchronizer: SchichtplanungSynchronizer, + private readonly dyflexisUserAccessService: DyflexisUserAccessExtractorsService ) {} + runDyflexisUserAccessTest() { + return this.dyflexisUserAccessService.loadData() + } + runTourUpdateCheck(interval: Interval) { return this.tourUpdateChecker.fetchTourUpdates(interval); } diff --git a/service/data-hub/data-connector/src/modules/feat-etl/feat-importers/processors/tours.consumer.ts b/service/data-hub/data-connector/src/modules/feat-etl/feat-importers/processors/tours.consumer.ts index 7e625a2..b7e256e 100644 --- a/service/data-hub/data-connector/src/modules/feat-etl/feat-importers/processors/tours.consumer.ts +++ b/service/data-hub/data-connector/src/modules/feat-etl/feat-importers/processors/tours.consumer.ts @@ -1,13 +1,13 @@ -import { InjectQueue, Processor, WorkerHost } from '@nestjs/bullmq'; -import { Job, Queue, Worker } from 'bullmq'; +import { Processor, WorkerHost } from '@nestjs/bullmq'; +import { Logger } from '@nestjs/common'; +import { TourSource } from '@prisma/client'; +import { Job } from 'bullmq'; +import { from, lastValueFrom, map, switchMap } from 'rxjs'; import { TourService } from 'src/modules/feat-business-objects/entities/tour/tour.service'; import { TOURS_QUEUE } from 'src/modules/feat-business-objects/entities/tour/tour.tokens'; import { DispoLiveExtractorsService } from '../../feat-extractors/dispo-live-extractors.service'; -import { firstValueFrom, from, lastValueFrom, map, switchMap, tap } from 'rxjs'; -import { TourTransformer } from '../../feat-transformers/transformers/tour.transformer'; -import { TourSource } from '@prisma/client'; import { ToursCalculationAdapater } from '../../feat-transformers/adapter/tours-calculation.adapter'; -import { Logger } from '@nestjs/common'; +import { TourTransformer } from '../../feat-transformers/transformers/tour.transformer'; @Processor(TOURS_QUEUE, { limiter: { @@ -53,9 +53,15 @@ export class ToursConsumer extends WorkerHost { ), ); if (isCreate || job.name === 'Create') { - return await this.service.createTour(tour).then(() => this.logger.log(`Created tour ${tour.id} successfully`)); - } - return await this.service.updateTour(tour.id, tour).then(() => this.logger.log(`Updated tour ${tour.id} successfully`)); + return await this.service + .createTour(tour) + .then(() => + this.logger.log(`Created tour ${tour.id} successfully`), + ); + } + return await this.service + .updateTour(tour.id, tour) + .then(() => this.logger.log(`Updated tour ${tour.id} successfully`)); default: console.log(`No case matched`, job); } diff --git a/service/data-hub/data-connector/src/modules/feat-etl/feat-importers/producer/schichtplanung.ts b/service/data-hub/data-connector/src/modules/feat-etl/feat-importers/producer/schichtplanung.ts index 3754b28..626d74d 100644 --- a/service/data-hub/data-connector/src/modules/feat-etl/feat-importers/producer/schichtplanung.ts +++ b/service/data-hub/data-connector/src/modules/feat-etl/feat-importers/producer/schichtplanung.ts @@ -1,5 +1,5 @@ import { InjectQueue } from '@nestjs/bullmq'; -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { Queue } from 'bullmq'; import { addMinutes, endOfDay, getMonth, getYear, isSameDay, startOfDay } from 'date-fns'; import { @@ -17,6 +17,8 @@ import { SCHICHTPLANUNG_QUEUE } from '../tokens'; @Injectable() export class SchichtplanungSynchronizer { + private logger = new Logger(SchichtplanungSynchronizer.name) + constructor( private readonly dlApiService: DispoLiveApiExtractorsService, private readonly dyflexisService: DyflexisExtractorsService, @@ -61,7 +63,7 @@ export class SchichtplanungSynchronizer { plannedTime.user_id, ); if (!employee) { - console.error(`couldn\'t find employee with id ${plannedTime.user_id}`); + this.logger.error(`couldn\'t find employee with id ${plannedTime.user_id}`); continue; } diff --git a/service/data-hub/data-connector/src/modules/feat-etl/feat-loaders/loaders/attendance-registration.loader.ts b/service/data-hub/data-connector/src/modules/feat-etl/feat-loaders/loaders/attendance-registration.loader.ts index 7777e6c..3ff26d9 100644 --- a/service/data-hub/data-connector/src/modules/feat-etl/feat-loaders/loaders/attendance-registration.loader.ts +++ b/service/data-hub/data-connector/src/modules/feat-etl/feat-loaders/loaders/attendance-registration.loader.ts @@ -68,7 +68,7 @@ export class AttendanceRegistrationLoader { private static attendanceRegistrationStagingToFinal( attendanceregistration: AttendanceRegistrationStaging, ): Prisma.AttendanceRegistrationCreateInput { - const { attendanceRegistrationId, employeeId, dateTime, event } = + const { attendanceRegistrationId, employeeId, dateTime, event, personnelNumber } = attendanceregistration; return { @@ -76,7 +76,7 @@ export class AttendanceRegistrationLoader { employeeId, dateTime, event, - personnelNumber: '', + personnelNumber, }; } } diff --git a/service/data-hub/data-connector/src/modules/feat-etl/feat-loaders/loaders/employee-dyflexis.ts b/service/data-hub/data-connector/src/modules/feat-etl/feat-loaders/loaders/employee-dyflexis.ts index 9c2c861..010731b 100644 --- a/service/data-hub/data-connector/src/modules/feat-etl/feat-loaders/loaders/employee-dyflexis.ts +++ b/service/data-hub/data-connector/src/modules/feat-etl/feat-loaders/loaders/employee-dyflexis.ts @@ -56,6 +56,7 @@ export class EmployeeDyflexisLoader { contractEnd, contractHoursWeek, contractSalaryHour, + contractTypeName } = employee; return { @@ -66,6 +67,7 @@ export class EmployeeDyflexisLoader { contractEnd, contractHoursWeek, contractSalaryHour, + contractTypeName }; } } diff --git a/service/data-hub/data-connector/src/modules/feat-etl/feat-loaders/loaders/planned-time.loader.ts b/service/data-hub/data-connector/src/modules/feat-etl/feat-loaders/loaders/planned-time.loader.ts index 3207b37..03cb14c 100644 --- a/service/data-hub/data-connector/src/modules/feat-etl/feat-loaders/loaders/planned-time.loader.ts +++ b/service/data-hub/data-connector/src/modules/feat-etl/feat-loaders/loaders/planned-time.loader.ts @@ -56,6 +56,8 @@ export class PlannedTimeLoader { startDate, endDate, mark, + pause, + duration, } = plannedtime; return { @@ -63,11 +65,11 @@ export class PlannedTimeLoader { surname, startDate, endDate, + pause, + duration, id: `${id}`, userId: `${employeeId}`, department: departmentName, - pause: 0, - duration: 0, note: mark, }; } diff --git a/service/data-hub/data-connector/src/modules/feat-etl/feat-loaders/loaders/tour.loader.ts b/service/data-hub/data-connector/src/modules/feat-etl/feat-loaders/loaders/tour.loader.ts index d1d71a4..ac6ad87 100644 --- a/service/data-hub/data-connector/src/modules/feat-etl/feat-loaders/loaders/tour.loader.ts +++ b/service/data-hub/data-connector/src/modules/feat-etl/feat-loaders/loaders/tour.loader.ts @@ -91,10 +91,13 @@ export class TourLoader { revenue, revenueDeviation, consumptionCosts, + overRTW, + companyName, + schwerlast, billDate, billNumber, createdAt, - updatedAt + updatedAt, } = tour; return { @@ -143,6 +146,9 @@ export class TourLoader { revenue, revenueDeviation, consumptionCosts, + overRTW, + companyName, + schwerlast, billDate, billNumber, createdAt, diff --git a/service/data-hub/data-connector/src/modules/feat-etl/feat-loaders/loaders/work-time.loader.ts b/service/data-hub/data-connector/src/modules/feat-etl/feat-loaders/loaders/work-time.loader.ts index 4453396..509eb80 100644 --- a/service/data-hub/data-connector/src/modules/feat-etl/feat-loaders/loaders/work-time.loader.ts +++ b/service/data-hub/data-connector/src/modules/feat-etl/feat-loaders/loaders/work-time.loader.ts @@ -53,6 +53,8 @@ export class WorkTimeLoader { departmentName, startDate, endDate, + pause, + duration } = worktime; return { @@ -60,11 +62,11 @@ export class WorkTimeLoader { surname, startDate, endDate, + pause, + duration, id: `${id}`, userId: `${employeeId}`, department: departmentName, - pause: 0, - duration: 0, }; } } diff --git a/service/data-hub/data-connector/src/modules/feat-etl/feat-transformers/staging.interface.ts b/service/data-hub/data-connector/src/modules/feat-etl/feat-transformers/staging.interface.ts index f56ba65..364ec04 100644 --- a/service/data-hub/data-connector/src/modules/feat-etl/feat-transformers/staging.interface.ts +++ b/service/data-hub/data-connector/src/modules/feat-etl/feat-transformers/staging.interface.ts @@ -56,6 +56,10 @@ export interface TourPreStaging { revenueDeviation?: number; consumptionCosts?: number; + overRTW?: boolean; + companyName?: string; + schwerlast?: boolean; + createdAt?: Date; updatedAt?: Date; } @@ -91,6 +95,8 @@ export interface PlannedTimePreStaging { departmentName: string; startDate: Date; endDate: Date; + pause: number; + duration: number; deleted: boolean; mark?: string; } @@ -103,6 +109,8 @@ export interface WorkTimePreStaging { departmentName: string; startDate: Date; endDate: Date; + pause: number; + duration: number; } export interface EmployeeDyflexisPreStaging { diff --git a/service/data-hub/data-connector/src/modules/feat-etl/feat-transformers/transformers/attendance-registration.transformer.ts b/service/data-hub/data-connector/src/modules/feat-etl/feat-transformers/transformers/attendance-registration.transformer.ts index 7646044..3767b47 100644 --- a/service/data-hub/data-connector/src/modules/feat-etl/feat-transformers/transformers/attendance-registration.transformer.ts +++ b/service/data-hub/data-connector/src/modules/feat-etl/feat-transformers/transformers/attendance-registration.transformer.ts @@ -23,7 +23,7 @@ export class AttendanceRegistrationTransformer { ), }; - const { attendanceRegistrationId, employeeId, dateTime, event } = + const { attendanceRegistrationId, employeeId, dateTime, event, personnelNumber } = attendanceRegistrationParsed; return { @@ -31,6 +31,7 @@ export class AttendanceRegistrationTransformer { employeeId, dateTime, event, + personnelNumber, } as unknown as AttendanceRegistrationPreStaging; } } diff --git a/service/data-hub/data-connector/src/modules/feat-etl/feat-transformers/transformers/employee-dyflexis.transformer.ts b/service/data-hub/data-connector/src/modules/feat-etl/feat-transformers/transformers/employee-dyflexis.transformer.ts index cdf5bfa..8430e7d 100644 --- a/service/data-hub/data-connector/src/modules/feat-etl/feat-transformers/transformers/employee-dyflexis.transformer.ts +++ b/service/data-hub/data-connector/src/modules/feat-etl/feat-transformers/transformers/employee-dyflexis.transformer.ts @@ -36,6 +36,7 @@ export class DyflexisEmployeeTransformer { contract_end, contract_hours_week, contract_salary_hour, + contract_type_name, } = dyflexisEmployeeParsed; return { @@ -46,6 +47,7 @@ export class DyflexisEmployeeTransformer { contractEnd: contract_end, contractHoursWeek: contract_hours_week, contractSalaryHour: contract_salary_hour, + contractTypeName: contract_type_name, } as unknown as EmployeeDyflexisPreStaging; } } diff --git a/service/data-hub/data-connector/src/modules/feat-etl/feat-transformers/transformers/planned-time.transformer.ts b/service/data-hub/data-connector/src/modules/feat-etl/feat-transformers/transformers/planned-time.transformer.ts index ee25a62..1ba6e1e 100644 --- a/service/data-hub/data-connector/src/modules/feat-etl/feat-transformers/transformers/planned-time.transformer.ts +++ b/service/data-hub/data-connector/src/modules/feat-etl/feat-transformers/transformers/planned-time.transformer.ts @@ -18,7 +18,7 @@ export class PlannedTimeTransformer { // firstname: faker.person.firstName(), // surname: faker.person.lastName(), ...transformProperties(parseInteger)( - pick(['id', 'user_id'])(plannedTimeSource), + pick(['id', 'user_id', 'pauze', 'duration'])(plannedTimeSource), ), ...transformProperties(parseCETDateTime('yyyy-MM-dd HH:mm:ss'))( pick(['start_date', 'end_date'])(plannedTimeSource), @@ -36,6 +36,8 @@ export class PlannedTimeTransformer { department_name, start_date, end_date, + pauze, + duration, deleted, mark, } = plannedTimeParsed; @@ -44,6 +46,8 @@ export class PlannedTimeTransformer { id, firstname, surname, + pause: pauze, + duration, deleted, mark, employeeId: user_id, diff --git a/service/data-hub/data-connector/src/modules/feat-etl/feat-transformers/transformers/tour.transformer.ts b/service/data-hub/data-connector/src/modules/feat-etl/feat-transformers/transformers/tour.transformer.ts index ae26d55..2ecf243 100644 --- a/service/data-hub/data-connector/src/modules/feat-etl/feat-transformers/transformers/tour.transformer.ts +++ b/service/data-hub/data-connector/src/modules/feat-etl/feat-transformers/transformers/tour.transformer.ts @@ -63,7 +63,7 @@ export class TourTransformer { ) : undefined), ...transformProperties(parseBoolean)( - pick(['infektion', 'gefahren'])(tourSource), + pick(['infektion', 'gefahren', 'overRTW', 'schwerlast'])(tourSource), ), }; @@ -111,6 +111,9 @@ export class TourTransformer { gesamtPreis, abrDate, abrNum, + overRTW, + companyName, + schwerlast, createTime, lastUpdate, } = tourParsed; @@ -160,6 +163,9 @@ export class TourTransformer { revenueDispoLive: gesamtPreis, billDate: abrDate, billNumber: abrNum, + overRTW, + companyName, + schwerlast, createdAt: createTime, updatedAt: lastUpdate, revenue: 0, diff --git a/service/data-hub/data-connector/src/modules/feat-etl/feat-transformers/transformers/work-time.transformer.ts b/service/data-hub/data-connector/src/modules/feat-etl/feat-transformers/transformers/work-time.transformer.ts index 1ae4761..66b2504 100644 --- a/service/data-hub/data-connector/src/modules/feat-etl/feat-transformers/transformers/work-time.transformer.ts +++ b/service/data-hub/data-connector/src/modules/feat-etl/feat-transformers/transformers/work-time.transformer.ts @@ -15,7 +15,7 @@ export class WorkTimeTransformer { // firstname: faker.person.firstName(), // surname: faker.person.lastName(), ...transformProperties(parseInteger)( - pick(['id', 'user_id'])(workTimeSource), + pick(['id', 'user_id', 'pauze', 'duration'])(workTimeSource), ), ...transformProperties(parseCETDateTime('yyyy-MM-dd HH:mm:ss'))( pick(['start_date', 'end_date'])(workTimeSource), @@ -30,12 +30,16 @@ export class WorkTimeTransformer { department_name, start_date, end_date, + pauze, + duration, } = workTimeParsed; return { id, firstname, surname, + duration, + pause: pauze, employeeId: user_id, departmentName: department_name, startDate: start_date, diff --git a/service/data-hub/data-connector/src/modules/feat-tickets/ticket-lifecycle.service.ts b/service/data-hub/data-connector/src/modules/feat-tickets/ticket-lifecycle.service.ts index 8879f13..21b618d 100644 --- a/service/data-hub/data-connector/src/modules/feat-tickets/ticket-lifecycle.service.ts +++ b/service/data-hub/data-connector/src/modules/feat-tickets/ticket-lifecycle.service.ts @@ -143,7 +143,8 @@ export class TicketLifecycleService { ({ ordinanceType }) => ordinanceType.indexOf('BTW') > -1 || ordinanceType.indexOf('TSW') > -1, - ) + ) && + ticket.currentState != TicketValidationState.TD_UNCERTAIN ) { await this.ticketService.updateTicket(ticket.id, { errors,