commit open changes from previous iterations
This commit is contained in:
parent
4cf79f3094
commit
c33ab89354
|
|
@ -0,0 +1,2 @@
|
|||
- [ ] Anomalie "Anwesenheit vor Arbeitszeit" abschliessen
|
||||
- [ ] Add Dyflexis User Access Endpoints to ETL
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
<ul class="ml-4">
|
||||
@for (step of target.steps; track step) {
|
||||
<li class="list-disc">
|
||||
{{ step.description }}
|
||||
<div [innerHTML]="step.description"></div>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import { Component } from '@angular/core';
|
|||
class="w-full md:flex-grow lg:w-2/5 m-2 !flex flex-col"
|
||||
>
|
||||
<mat-card-header>
|
||||
<mat-card-title> Anomalien </mat-card-title>
|
||||
<mat-card-title> Anomalien in der Dokumentation</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content class="flex-grow">
|
||||
<dks-anomaly-tree></dks-anomaly-tree>
|
||||
|
|
@ -36,7 +36,7 @@ import { Component } from '@angular/core';
|
|||
class="w-full md:flex-grow lg:w-2/5 m-2 !flex flex-col"
|
||||
>
|
||||
<mat-card-header>
|
||||
<mat-card-title> Mitarbeiter ohne Tour </mat-card-title>
|
||||
<mat-card-title> Anomalien in der Zeiterfassung </mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content class="flex-grow">
|
||||
<dks-anomaly-list></dks-anomaly-list>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
.local/
|
||||
daten/
|
||||
|
||||
# compiled output
|
||||
/dist
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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");
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "AttendanceRegistration" ALTER COLUMN "personnelNumber" DROP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "AttendanceRegistrationStaging" ADD COLUMN "personnelNumber" TEXT DEFAULT '';
|
||||
|
|
@ -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;
|
||||
|
|
@ -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")
|
||||
);
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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')),
|
||||
|
|
|
|||
|
|
@ -13,10 +13,4 @@ describe('AppController', () => {
|
|||
|
||||
appController = app.get<AppController>(AppController);
|
||||
});
|
||||
|
||||
describe('root', () => {
|
||||
it('should return "Hello World!"', () => {
|
||||
expect(appController.getHello()).toBe('Hello World!');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string | null>;
|
||||
};
|
||||
export type AvicennaTenant = {
|
||||
id: Generated<string>;
|
||||
|
|
@ -102,6 +103,7 @@ export type CostType = {
|
|||
tariffId: string | null;
|
||||
};
|
||||
export type DyflexisEmployee = {
|
||||
internalId: Generated<string>;
|
||||
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<string>;
|
||||
|
|
@ -163,6 +167,14 @@ export type EmployeeDyflexisStaging = {
|
|||
contractEnd: Timestamp;
|
||||
contractHoursWeek: number;
|
||||
contractSalaryHour: number;
|
||||
contractTypeName: string | null;
|
||||
};
|
||||
export type Holiday = {
|
||||
id: Generated<string>;
|
||||
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<string>;
|
||||
start: Timestamp;
|
||||
end: Timestamp;
|
||||
hours: number;
|
||||
days: number;
|
||||
note: string | null;
|
||||
locked: boolean;
|
||||
employeeId: number;
|
||||
};
|
||||
export type SolutionTarget = {
|
||||
id: Generated<string>;
|
||||
|
|
@ -337,11 +361,25 @@ export type Tour = {
|
|||
revenue: Generated<number>;
|
||||
consumptionCosts: Generated<number>;
|
||||
revenueDeviation: Generated<number>;
|
||||
overRTW: Generated<boolean>;
|
||||
companyName: string | null;
|
||||
schwerlast: Generated<boolean>;
|
||||
createdAt: Timestamp | null;
|
||||
updatedAt: Timestamp | null;
|
||||
deletedAt: Timestamp | null;
|
||||
ticketId: string | null;
|
||||
};
|
||||
export type TourFile = {
|
||||
id: Generated<string>;
|
||||
tourId: string;
|
||||
ticketId: string;
|
||||
storagePath: string;
|
||||
originalFilename: string;
|
||||
mimeType: string;
|
||||
fileSizeBytes: number;
|
||||
dlUploadStatus: Generated<string>;
|
||||
createdAt: Generated<Timestamp | null>;
|
||||
};
|
||||
export type TourSource = {
|
||||
internalId: Generated<string>;
|
||||
insertedAt: Generated<Timestamp>;
|
||||
|
|
@ -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<string>;
|
||||
|
|
@ -461,6 +502,9 @@ export type TourStaging = {
|
|||
revenue: Generated<number>;
|
||||
revenueDeviation: Generated<number>;
|
||||
consumptionCosts: Generated<number>;
|
||||
overRTW: Generated<boolean>;
|
||||
companyName: string | null;
|
||||
schwerlast: Generated<boolean>;
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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[] = [];
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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<DB, 'Tour'>;
|
||||
|
||||
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<DB>,
|
||||
) {}
|
||||
|
||||
async toursWithoutGermanZipInInterval(interval: Interval): Promise<Tour[]> {
|
||||
return await this.db
|
||||
.selectFrom('Tour')
|
||||
.selectAll()
|
||||
.where((eb) =>
|
||||
eb.and([
|
||||
eb.or([
|
||||
eb(sql<number>`LENGTH("startZip")`, '<', 5),
|
||||
eb(sql<number>`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<string>`
|
||||
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<Date>`LEAD("dateTime") OVER (PARTITION BY "employeeId" ORDER BY "dateTime" ASC)`.as(
|
||||
'nextDateTime',
|
||||
),
|
||||
sql<string>`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<string>`"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<string>`MIN("firstname")`.as('firstname'),
|
||||
sql<string>`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<number>`SUM("pause")`.as('actualPause'),
|
||||
sql<number>`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<boolean>`("wt"."actualEnd" - "wt"."actualStart") - ("pt"."plannedEnd" - "pt"."plannedStart")`.as(
|
||||
'diff',
|
||||
),
|
||||
])
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
// eb('wt.userId', 'is', null),
|
||||
sql<boolean>`("wt"."actualEnd" - "wt"."actualStart") - ("pt"."plannedEnd" - "pt"."plannedStart") > interval '2 hours'`,
|
||||
]),
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Tour[]> {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ export class DriverCertificateDetector extends AnomalyDetector {
|
|||
readonly type = AnomalyTypes.DriverCertificate;
|
||||
|
||||
async detect(inInterval: Interval): Promise<Prisma.AnomalyCreateInput[]> {
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ export class DriverCodriverSameDetector extends AnomalyDetector {
|
|||
readonly type = AnomalyTypes.DriverCodriverSame;
|
||||
|
||||
async detect(inInterval: Interval): Promise<Prisma.AnomalyCreateInput[]> {
|
||||
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'],
|
||||
|
|
|
|||
|
|
@ -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<Prisma.AnomalyCreateInput[]> {
|
||||
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<Anomaly> {
|
||||
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 <a class="dyflexis-btn" href="https://app.planning.nu/avicennaambulance/hamburg/ur3/info_day?periode=${format(day, 'yyyy-MM-dd')}" target="_blank">${format(day, 'dd.MM.yyyy')}</a>.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
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 }),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,9 @@ export class EmployeesLoggedInDetector extends AnomalyDetector {
|
|||
readonly type = AnomalyTypes.EmployeesLoggedIn;
|
||||
|
||||
async detect(inInterval: Interval): Promise<Prisma.AnomalyCreateInput[]> {
|
||||
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],
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Prisma.AnomalyCreateInput[]> {
|
||||
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<Anomaly> {
|
||||
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 <a class="dyflexis-btn" href="https://app.planning.nu/avicennaambulance/hamburg/ur3/info_day?periode=${format(day, 'yyyy-MM-dd')}" target="_blank">${format(day, 'dd.MM.yyyy')}</a>.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
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 }),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Prisma.AnomalyCreateInput[]> {
|
||||
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<Anomaly> {
|
||||
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 <a class="dispolive-btn" href="https://avicenna.dispolive.de/fahrbericht/${tourId}" target="_blank">Fahrbericht</a> 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})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Prisma.AnomalyCreateInput[]> {
|
||||
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<Anomaly> {
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
|
@ -33,6 +33,9 @@ export class OneTourAtATimeDetector extends AnomalyDetector {
|
|||
}
|
||||
|
||||
async detect(inInterval: Interval): Promise<Prisma.AnomalyCreateInput[]> {
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -14,6 +14,9 @@ export class WorkingEmployeeNotInDistributionSystemDetector extends AnomalyDetec
|
|||
readonly type = AnomalyTypes.WorkingEmployeeNotInDistributionSystem;
|
||||
|
||||
async detect(inInterval: Interval): Promise<Prisma.AnomalyCreateInput[]> {
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
export const ANOMALY_SLEEP_TIME_MS = 60 * 60 * 1000;
|
||||
export const ANOMALY_SLEEP_TIME_MS = 60 * 60 * 1000;
|
||||
export const UNGROUPED_ANOMALIES = 'ungrouped';
|
||||
|
|
@ -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',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<DB>,
|
||||
) {}
|
||||
|
||||
async findById(
|
||||
attendanceRegistrationId: number,
|
||||
): Promise<AttendanceRegistration> {
|
||||
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<AttendanceRegistration[]> {
|
||||
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<AttendanceRegistration[]> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<DB>,
|
||||
) {}
|
||||
|
||||
async findById(employeeId: string): Promise<Employee> {
|
||||
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<Employee> {
|
||||
return await this.prisma.employee.findFirst({ where: { personnelNumber } });
|
||||
return await this.db
|
||||
.selectFrom('Employee')
|
||||
.selectAll()
|
||||
.where('Employee.personnelNumber', '=', personnelNumber)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<DB>,
|
||||
) {}
|
||||
|
||||
async findById(id: string): Promise<PlannedTime> {
|
||||
return await this.prisma.plannedTime.findUnique({
|
||||
|
|
@ -17,23 +23,13 @@ export class PlannedTimeRepository {
|
|||
personnelNumber: string,
|
||||
interval: Interval,
|
||||
): Promise<PlannedTime[]> {
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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[] })[]> {
|
||||
|
|
|
|||
|
|
@ -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<DB, 'Tour'>;
|
||||
|
||||
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<Tour[]> {
|
||||
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<Tour[]> {
|
||||
|
|
|
|||
|
|
@ -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<DB>,
|
||||
) {}
|
||||
|
||||
async findById(id: string): Promise<WorkTime> {
|
||||
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<WorkTime> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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<any> {
|
||||
// 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<any> {
|
||||
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<any> {
|
||||
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<DyflexisAppUser[]> {
|
||||
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<any>((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<DyflexisAuthHeader> {
|
||||
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(
|
||||
`<meta[^>]*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,
|
||||
})),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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<DB>,
|
||||
) {}
|
||||
|
||||
clearSource(): Promise<Prisma.BatchPayload[]> {
|
||||
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<Sickdays> => ({
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ export class EmployeeDyflexisLoader {
|
|||
contractEnd,
|
||||
contractHoursWeek,
|
||||
contractSalaryHour,
|
||||
contractTypeName
|
||||
} = employee;
|
||||
|
||||
return {
|
||||
|
|
@ -66,6 +67,7 @@ export class EmployeeDyflexisLoader {
|
|||
contractEnd,
|
||||
contractHoursWeek,
|
||||
contractSalaryHour,
|
||||
contractTypeName
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue