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 @@
@for (step of target.steps; track step) {
-
- {{ step.description }}
+
}
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,