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">
|
<ul class="ml-4">
|
||||||
@for (step of target.steps; track step) {
|
@for (step of target.steps; track step) {
|
||||||
<li class="list-disc">
|
<li class="list-disc">
|
||||||
{{ step.description }}
|
<div [innerHTML]="step.description"></div>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
</ul>
|
</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"
|
class="w-full md:flex-grow lg:w-2/5 m-2 !flex flex-col"
|
||||||
>
|
>
|
||||||
<mat-card-header>
|
<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-header>
|
||||||
<mat-card-content class="flex-grow">
|
<mat-card-content class="flex-grow">
|
||||||
<dks-anomaly-tree></dks-anomaly-tree>
|
<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"
|
class="w-full md:flex-grow lg:w-2/5 m-2 !flex flex-col"
|
||||||
>
|
>
|
||||||
<mat-card-header>
|
<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-header>
|
||||||
<mat-card-content class="flex-grow">
|
<mat-card-content class="flex-grow">
|
||||||
<dks-anomaly-list></dks-anomaly-list>
|
<dks-anomaly-list></dks-anomaly-list>
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,16 @@ body {
|
||||||
color: var(--mat-sys-on-surface);
|
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 {
|
.pulse {
|
||||||
animation: pulse 1.5s cubic-bezier(0.4, 0, 0.2, 1) infinite;
|
animation: pulse 1.5s cubic-bezier(0.4, 0, 0.2, 1) infinite;
|
||||||
animation-delay: 0.5s;
|
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/
|
.local/
|
||||||
|
daten/
|
||||||
|
|
||||||
# compiled output
|
# compiled output
|
||||||
/dist
|
/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
|
fahrer String? // ID
|
||||||
|
|
||||||
color String? // Hex rgb
|
color String? // Hex rgb
|
||||||
|
|
||||||
|
overRTW String?
|
||||||
|
companyName String?
|
||||||
|
schwerlast String?
|
||||||
}
|
}
|
||||||
|
|
||||||
model EmployeeDispoLiveSource {
|
model EmployeeDispoLiveSource {
|
||||||
|
|
@ -174,6 +178,7 @@ model EmployeeDyflexisSource {
|
||||||
contract_end String? // yyyy-MM-dd
|
contract_end String? // yyyy-MM-dd
|
||||||
contract_hours_week String? // Float string
|
contract_hours_week String? // Float string
|
||||||
contract_salary_hour String? // Float string
|
contract_salary_hour String? // Float string
|
||||||
|
contract_type_name String?
|
||||||
}
|
}
|
||||||
|
|
||||||
model PatientSource {
|
model PatientSource {
|
||||||
|
|
@ -256,6 +261,10 @@ model TourStaging {
|
||||||
revenueDeviation Float @default(0)
|
revenueDeviation Float @default(0)
|
||||||
consumptionCosts Float @default(0)
|
consumptionCosts Float @default(0)
|
||||||
|
|
||||||
|
overRTW Boolean @default(false)
|
||||||
|
companyName String?
|
||||||
|
schwerlast Boolean @default(false)
|
||||||
|
|
||||||
createdAt DateTime?
|
createdAt DateTime?
|
||||||
updatedAt DateTime?
|
updatedAt DateTime?
|
||||||
}
|
}
|
||||||
|
|
@ -285,6 +294,7 @@ model AttendanceRegistrationStaging {
|
||||||
employeeId Int
|
employeeId Int
|
||||||
dateTime DateTime
|
dateTime DateTime
|
||||||
event String
|
event String
|
||||||
|
personnelNumber String? @default("")
|
||||||
}
|
}
|
||||||
|
|
||||||
model PlannedTimeStaging {
|
model PlannedTimeStaging {
|
||||||
|
|
@ -299,6 +309,8 @@ model PlannedTimeStaging {
|
||||||
endDate DateTime
|
endDate DateTime
|
||||||
deleted Boolean
|
deleted Boolean
|
||||||
mark String?
|
mark String?
|
||||||
|
pause Int
|
||||||
|
duration Int
|
||||||
}
|
}
|
||||||
|
|
||||||
model WorkTimeStaging {
|
model WorkTimeStaging {
|
||||||
|
|
@ -311,6 +323,8 @@ model WorkTimeStaging {
|
||||||
departmentName String?
|
departmentName String?
|
||||||
startDate DateTime
|
startDate DateTime
|
||||||
endDate DateTime
|
endDate DateTime
|
||||||
|
pause Int
|
||||||
|
duration Int
|
||||||
}
|
}
|
||||||
|
|
||||||
model EmployeeDyflexisStaging {
|
model EmployeeDyflexisStaging {
|
||||||
|
|
@ -323,6 +337,7 @@ model EmployeeDyflexisStaging {
|
||||||
contractEnd DateTime
|
contractEnd DateTime
|
||||||
contractHoursWeek Float
|
contractHoursWeek Float
|
||||||
contractSalaryHour Float
|
contractSalaryHour Float
|
||||||
|
contractTypeName String?
|
||||||
}
|
}
|
||||||
|
|
||||||
model EmployeeDispoLiveStaging {
|
model EmployeeDispoLiveStaging {
|
||||||
|
|
@ -395,6 +410,10 @@ model Tour {
|
||||||
consumptionCosts Float @default(0)
|
consumptionCosts Float @default(0)
|
||||||
revenueDeviation Float @default(0)
|
revenueDeviation Float @default(0)
|
||||||
|
|
||||||
|
overRTW Boolean @default(false)
|
||||||
|
companyName String?
|
||||||
|
schwerlast Boolean @default(false)
|
||||||
|
|
||||||
createdAt DateTime?
|
createdAt DateTime?
|
||||||
updatedAt DateTime?
|
updatedAt DateTime?
|
||||||
deletedAt DateTime?
|
deletedAt DateTime?
|
||||||
|
|
@ -469,7 +488,7 @@ model TicketStateHistory {
|
||||||
model AttendanceRegistration {
|
model AttendanceRegistration {
|
||||||
attendanceRegistrationId Int @id
|
attendanceRegistrationId Int @id
|
||||||
employeeId Int
|
employeeId Int
|
||||||
personnelNumber String
|
personnelNumber String?
|
||||||
dateTime DateTime
|
dateTime DateTime
|
||||||
event String
|
event String
|
||||||
}
|
}
|
||||||
|
|
@ -511,13 +530,36 @@ model Employee {
|
||||||
}
|
}
|
||||||
|
|
||||||
model DyflexisEmployee {
|
model DyflexisEmployee {
|
||||||
id Int @id
|
internalId String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||||
|
id Int
|
||||||
firstname String
|
firstname String
|
||||||
surname String
|
surname String
|
||||||
contractStart DateTime
|
contractStart DateTime
|
||||||
contractEnd DateTime?
|
contractEnd DateTime?
|
||||||
contractHoursWeek Float
|
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 {
|
enum CostTypeVariant {
|
||||||
|
|
|
||||||
|
|
@ -179,7 +179,7 @@ export const KTW_AOK_TARIFFS = [
|
||||||
{
|
{
|
||||||
description: 'AOK/BKK/IKK/Knappschaft KTW',
|
description: 'AOK/BKK/IKK/Knappschaft KTW',
|
||||||
validFrom: startOfDay(new Date('2024-01-01')),
|
validFrom: startOfDay(new Date('2024-01-01')),
|
||||||
validTo: endOfDay(new Date('2024-12-31')),
|
validTo: endOfDay(new Date('2024-10-31')),
|
||||||
costTypes: [
|
costTypes: [
|
||||||
{
|
{
|
||||||
description: 'Grundpauschale je Fahrt',
|
description: 'Grundpauschale je Fahrt',
|
||||||
|
|
@ -200,7 +200,7 @@ export const KTW_AOK_TARIFFS = [
|
||||||
'1/KTW-02',
|
'1/KTW-02',
|
||||||
'1/KTW-03',
|
'1/KTW-03',
|
||||||
'1/KTW-04',
|
'1/KTW-04',
|
||||||
'1/KTW-05-E',
|
'1/KTW-05',
|
||||||
'1/KTW-06',
|
'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',
|
description: 'AOK/BKK/IKK/Knappschaft KTW',
|
||||||
validFrom: startOfDay(new Date('2025-01-01')),
|
validFrom: startOfDay(new Date('2025-01-01')),
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,4 @@ describe('AppController', () => {
|
||||||
|
|
||||||
appController = app.get<AppController>(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')
|
@Get('updated-tours')
|
||||||
async updatedTours(@Query('from') from: string, @Query('to') to: string) {
|
async updatedTours(@Query('from') from: string, @Query('to') to: string) {
|
||||||
|
this.logger.log(`Fetch Tour updates in interval from=${from} to=${to}`);
|
||||||
const interval = {
|
const interval = {
|
||||||
start: new Date(from),
|
start: new Date(from),
|
||||||
end: new Date(to),
|
end: new Date(to),
|
||||||
|
|
@ -165,4 +166,103 @@ export class AppController {
|
||||||
}
|
}
|
||||||
return toursWithRevenue;
|
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 {
|
dyflexisV2ApiKey(): string {
|
||||||
return this.get('DYFLEXIS_V2_API_KEY');
|
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 {
|
databasePort(): string {
|
||||||
return this.get('DATABASE_PORT');
|
return this.get('DATABASE_PORT');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ export type Anomaly = {
|
||||||
export type AttendanceRegistration = {
|
export type AttendanceRegistration = {
|
||||||
attendanceRegistrationId: number;
|
attendanceRegistrationId: number;
|
||||||
employeeId: number;
|
employeeId: number;
|
||||||
personnelNumber: string;
|
personnelNumber: string | null;
|
||||||
dateTime: Timestamp;
|
dateTime: Timestamp;
|
||||||
event: string;
|
event: string;
|
||||||
};
|
};
|
||||||
|
|
@ -80,6 +80,7 @@ export type AttendanceRegistrationStaging = {
|
||||||
employeeId: number;
|
employeeId: number;
|
||||||
dateTime: Timestamp;
|
dateTime: Timestamp;
|
||||||
event: string;
|
event: string;
|
||||||
|
personnelNumber: Generated<string | null>;
|
||||||
};
|
};
|
||||||
export type AvicennaTenant = {
|
export type AvicennaTenant = {
|
||||||
id: Generated<string>;
|
id: Generated<string>;
|
||||||
|
|
@ -102,6 +103,7 @@ export type CostType = {
|
||||||
tariffId: string | null;
|
tariffId: string | null;
|
||||||
};
|
};
|
||||||
export type DyflexisEmployee = {
|
export type DyflexisEmployee = {
|
||||||
|
internalId: Generated<string>;
|
||||||
id: number;
|
id: number;
|
||||||
firstname: string;
|
firstname: string;
|
||||||
surname: string;
|
surname: string;
|
||||||
|
|
@ -109,6 +111,7 @@ export type DyflexisEmployee = {
|
||||||
contractEnd: Timestamp | null;
|
contractEnd: Timestamp | null;
|
||||||
contractHoursWeek: number;
|
contractHoursWeek: number;
|
||||||
contractSalaryHour: number;
|
contractSalaryHour: number;
|
||||||
|
contractTypeName: string | null;
|
||||||
};
|
};
|
||||||
export type Employee = {
|
export type Employee = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -153,6 +156,7 @@ export type EmployeeDyflexisSource = {
|
||||||
contract_end: string | null;
|
contract_end: string | null;
|
||||||
contract_hours_week: string | null;
|
contract_hours_week: string | null;
|
||||||
contract_salary_hour: string | null;
|
contract_salary_hour: string | null;
|
||||||
|
contract_type_name: string | null;
|
||||||
};
|
};
|
||||||
export type EmployeeDyflexisStaging = {
|
export type EmployeeDyflexisStaging = {
|
||||||
internalId: Generated<string>;
|
internalId: Generated<string>;
|
||||||
|
|
@ -163,6 +167,14 @@ export type EmployeeDyflexisStaging = {
|
||||||
contractEnd: Timestamp;
|
contractEnd: Timestamp;
|
||||||
contractHoursWeek: number;
|
contractHoursWeek: number;
|
||||||
contractSalaryHour: number;
|
contractSalaryHour: number;
|
||||||
|
contractTypeName: string | null;
|
||||||
|
};
|
||||||
|
export type Holiday = {
|
||||||
|
id: Generated<string>;
|
||||||
|
date: Timestamp;
|
||||||
|
locked: boolean;
|
||||||
|
excludedFromPayroll: boolean;
|
||||||
|
employeeId: number;
|
||||||
};
|
};
|
||||||
export type Patient = {
|
export type Patient = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -250,6 +262,18 @@ export type PlannedTimeStaging = {
|
||||||
endDate: Timestamp;
|
endDate: Timestamp;
|
||||||
deleted: boolean;
|
deleted: boolean;
|
||||||
mark: string | null;
|
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 = {
|
export type SolutionTarget = {
|
||||||
id: Generated<string>;
|
id: Generated<string>;
|
||||||
|
|
@ -337,11 +361,25 @@ export type Tour = {
|
||||||
revenue: Generated<number>;
|
revenue: Generated<number>;
|
||||||
consumptionCosts: Generated<number>;
|
consumptionCosts: Generated<number>;
|
||||||
revenueDeviation: Generated<number>;
|
revenueDeviation: Generated<number>;
|
||||||
|
overRTW: Generated<boolean>;
|
||||||
|
companyName: string | null;
|
||||||
|
schwerlast: Generated<boolean>;
|
||||||
createdAt: Timestamp | null;
|
createdAt: Timestamp | null;
|
||||||
updatedAt: Timestamp | null;
|
updatedAt: Timestamp | null;
|
||||||
deletedAt: Timestamp | null;
|
deletedAt: Timestamp | null;
|
||||||
ticketId: string | 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 = {
|
export type TourSource = {
|
||||||
internalId: Generated<string>;
|
internalId: Generated<string>;
|
||||||
insertedAt: Generated<Timestamp>;
|
insertedAt: Generated<Timestamp>;
|
||||||
|
|
@ -411,6 +449,9 @@ export type TourSource = {
|
||||||
fahrerName: string | null;
|
fahrerName: string | null;
|
||||||
fahrer: string | null;
|
fahrer: string | null;
|
||||||
color: string | null;
|
color: string | null;
|
||||||
|
overRTW: string | null;
|
||||||
|
companyName: string | null;
|
||||||
|
schwerlast: string | null;
|
||||||
};
|
};
|
||||||
export type TourStaging = {
|
export type TourStaging = {
|
||||||
internalId: Generated<string>;
|
internalId: Generated<string>;
|
||||||
|
|
@ -461,6 +502,9 @@ export type TourStaging = {
|
||||||
revenue: Generated<number>;
|
revenue: Generated<number>;
|
||||||
revenueDeviation: Generated<number>;
|
revenueDeviation: Generated<number>;
|
||||||
consumptionCosts: Generated<number>;
|
consumptionCosts: Generated<number>;
|
||||||
|
overRTW: Generated<boolean>;
|
||||||
|
companyName: string | null;
|
||||||
|
schwerlast: Generated<boolean>;
|
||||||
createdAt: Timestamp | null;
|
createdAt: Timestamp | null;
|
||||||
updatedAt: Timestamp | null;
|
updatedAt: Timestamp | null;
|
||||||
};
|
};
|
||||||
|
|
@ -505,6 +549,8 @@ export type WorkTimeStaging = {
|
||||||
departmentName: string | null;
|
departmentName: string | null;
|
||||||
startDate: Timestamp;
|
startDate: Timestamp;
|
||||||
endDate: Timestamp;
|
endDate: Timestamp;
|
||||||
|
pause: number;
|
||||||
|
duration: number;
|
||||||
};
|
};
|
||||||
export type DB = {
|
export type DB = {
|
||||||
_AvicennaTenantToTariffGroup: AvicennaTenantToTariffGroup;
|
_AvicennaTenantToTariffGroup: AvicennaTenantToTariffGroup;
|
||||||
|
|
@ -520,12 +566,14 @@ export type DB = {
|
||||||
EmployeeDispoLiveStaging: EmployeeDispoLiveStaging;
|
EmployeeDispoLiveStaging: EmployeeDispoLiveStaging;
|
||||||
EmployeeDyflexisSource: EmployeeDyflexisSource;
|
EmployeeDyflexisSource: EmployeeDyflexisSource;
|
||||||
EmployeeDyflexisStaging: EmployeeDyflexisStaging;
|
EmployeeDyflexisStaging: EmployeeDyflexisStaging;
|
||||||
|
Holiday: Holiday;
|
||||||
Patient: Patient;
|
Patient: Patient;
|
||||||
PatientSource: PatientSource;
|
PatientSource: PatientSource;
|
||||||
PatientStaging: PatientStaging;
|
PatientStaging: PatientStaging;
|
||||||
PlannedTime: PlannedTime;
|
PlannedTime: PlannedTime;
|
||||||
PlannedTimeSource: PlannedTimeSource;
|
PlannedTimeSource: PlannedTimeSource;
|
||||||
PlannedTimeStaging: PlannedTimeStaging;
|
PlannedTimeStaging: PlannedTimeStaging;
|
||||||
|
Sickdays: Sickdays;
|
||||||
SolutionTarget: SolutionTarget;
|
SolutionTarget: SolutionTarget;
|
||||||
SolutionTargetStep: SolutionTargetStep;
|
SolutionTargetStep: SolutionTargetStep;
|
||||||
Tariff: Tariff;
|
Tariff: Tariff;
|
||||||
|
|
@ -533,6 +581,7 @@ export type DB = {
|
||||||
Ticket: Ticket;
|
Ticket: Ticket;
|
||||||
TicketStateHistory: TicketStateHistory;
|
TicketStateHistory: TicketStateHistory;
|
||||||
Tour: Tour;
|
Tour: Tour;
|
||||||
|
TourFile: TourFile;
|
||||||
TourSource: TourSource;
|
TourSource: TourSource;
|
||||||
TourStaging: TourStaging;
|
TourStaging: TourStaging;
|
||||||
Trace: Trace;
|
Trace: Trace;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@ import { Tour } from '@prisma/client';
|
||||||
|
|
||||||
@ObjectType('Tour')
|
@ObjectType('Tour')
|
||||||
export class TourObjectType implements Tour {
|
export class TourObjectType implements Tour {
|
||||||
|
overRTW: boolean;
|
||||||
|
companyName: string;
|
||||||
|
schwerlast: boolean;
|
||||||
@Field()
|
@Field()
|
||||||
id: string;
|
id: string;
|
||||||
@Field()
|
@Field()
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,11 @@ export class ControlCenterResolver {
|
||||||
const activeAnomalies = await this.anomalyService.findAllVisible(
|
const activeAnomalies = await this.anomalyService.findAllVisible(
|
||||||
skip,
|
skip,
|
||||||
take,
|
take,
|
||||||
[AnomalyTypes.WorkingEmployeeNotInDistributionSystem],
|
[
|
||||||
|
AnomalyTypes.WorkingEmployeeNotInDistributionSystem,
|
||||||
|
AnomalyTypes.EmployeeStillWorking,
|
||||||
|
AnomalyTypes.IsAttendanceBeforeWorktime,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const totalCount = await this.anomalyService.countVisibleAnomalies([
|
const totalCount = await this.anomalyService.countVisibleAnomalies([
|
||||||
|
|
@ -134,6 +138,7 @@ export class ControlCenterResolver {
|
||||||
AnomalyTypes.DriverCertificate,
|
AnomalyTypes.DriverCertificate,
|
||||||
AnomalyTypes.EmployeesLoggedIn,
|
AnomalyTypes.EmployeesLoggedIn,
|
||||||
AnomalyTypes.OneTourAtATime,
|
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 { AnomalyService } from '../feat-business-objects/entities/anomaly/anomaly.service';
|
||||||
import { AnomalyDetector } from './anomaly-detectors/anomaly-detector.class';
|
import { AnomalyDetector } from './anomaly-detectors/anomaly-detector.class';
|
||||||
import { WorkingEmployeeNotInDistributionSystemDetector } from './anomaly-detectors/working-employee-not-in-distribution-system.detector';
|
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()
|
@Injectable()
|
||||||
export class AnomaliesService {
|
export class AnomaliesService {
|
||||||
|
|
@ -27,6 +31,10 @@ export class AnomaliesService {
|
||||||
new WorkingEmployeeNotInDistributionSystemDetector(
|
new WorkingEmployeeNotInDistributionSystemDetector(
|
||||||
this.anomalyDetectorService,
|
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();
|
const anomalies = await this.anomalyService.findAllUnsolved();
|
||||||
|
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
anomalies.map((anomaly) =>
|
anomalies
|
||||||
this.detectors
|
.map((anomaly) =>
|
||||||
.find((detector) => detector.type === anomaly.type)
|
this.detectors
|
||||||
.validate(anomaly),
|
.find((detector) => detector.type === anomaly.type)
|
||||||
),
|
?.validate(anomaly),
|
||||||
|
)
|
||||||
|
.filter(Boolean),
|
||||||
).then(async (anomalies) => {
|
).then(async (anomalies) => {
|
||||||
const validatedAnomalies = anomalies.flat();
|
const validatedAnomalies = anomalies.flat();
|
||||||
const updatedAnomalies: Anomaly[] = [];
|
const updatedAnomalies: Anomaly[] = [];
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,11 @@ import { PrismaModule } from 'src/core/database/prisma.module';
|
||||||
import { AnomaliesService } from './anomalies.service';
|
import { AnomaliesService } from './anomalies.service';
|
||||||
import { AnomalyDetectorService } from './anomaly-detectors/anomaly-detector.service';
|
import { AnomalyDetectorService } from './anomaly-detectors/anomaly-detector.service';
|
||||||
import { FeatBusinessObjectsModule } from '../feat-business-objects/feat-business-objects.module';
|
import { FeatBusinessObjectsModule } from '../feat-business-objects/feat-business-objects.module';
|
||||||
|
import { AnomalyDetectorRepository } from './anomaly-detectors/anomaly-detector.repository';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule, FeatBusinessObjectsModule],
|
imports: [PrismaModule, FeatBusinessObjectsModule],
|
||||||
providers: [AnomaliesService, AnomalyDetectorService],
|
providers: [AnomaliesService, AnomalyDetectorService, AnomalyDetectorRepository],
|
||||||
exports: [AnomaliesService],
|
exports: [AnomaliesService],
|
||||||
})
|
})
|
||||||
export class AnomalyDetectionModule {}
|
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 { 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 { 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 { WorkTimeService } from 'src/modules/feat-business-objects/entities/work-time/work-time.service';
|
||||||
|
import { AnomalyDetectorRepository } from './anomaly-detector.repository';
|
||||||
|
|
||||||
export type AnomalySkeleton = Pick<
|
export type AnomalySkeleton = Pick<
|
||||||
Prisma.AnomalyCreateInput,
|
Prisma.AnomalyCreateInput,
|
||||||
|
|
@ -33,8 +34,36 @@ export class AnomalyDetectorService {
|
||||||
private readonly plannedTimeService: PlannedTimeService,
|
private readonly plannedTimeService: PlannedTimeService,
|
||||||
private readonly tourService: TourService,
|
private readonly tourService: TourService,
|
||||||
private readonly workTimeService: WorkTimeService,
|
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
|
* Find a list of tours in a given time interval
|
||||||
* @param interval An interval defining the time range of tours
|
* @param interval An interval defining the time range of tours
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,9 @@ export class DriverCertificateDetector extends AnomalyDetector {
|
||||||
readonly type = AnomalyTypes.DriverCertificate;
|
readonly type = AnomalyTypes.DriverCertificate;
|
||||||
|
|
||||||
async detect(inInterval: Interval): Promise<Prisma.AnomalyCreateInput[]> {
|
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 toursInRange = await this.dataService.findToursInInterval(inInterval);
|
||||||
|
|
||||||
const anomalies = await Promise.all(
|
const anomalies = await Promise.all(
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,9 @@ export class DriverCodriverSameDetector extends AnomalyDetector {
|
||||||
readonly type = AnomalyTypes.DriverCodriverSame;
|
readonly type = AnomalyTypes.DriverCodriverSame;
|
||||||
|
|
||||||
async detect(inInterval: Interval): Promise<Prisma.AnomalyCreateInput[]> {
|
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(
|
const toursInRange = await this.dataService.findToursInInterval(
|
||||||
inInterval,
|
inInterval,
|
||||||
['KTW'],
|
['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;
|
readonly type = AnomalyTypes.EmployeesLoggedIn;
|
||||||
|
|
||||||
async detect(inInterval: Interval): Promise<Prisma.AnomalyCreateInput[]> {
|
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 toursInRange = await this.dataService.findToursInInterval(inInterval);
|
||||||
|
|
||||||
const anomaliesByTour = await Promise.all(
|
const anomaliesByTour = await Promise.all(
|
||||||
|
|
@ -41,14 +44,17 @@ export class EmployeesLoggedInDetector extends AnomalyDetector {
|
||||||
const employeeId = remainingIdParts.slice(0, -1).join('-');
|
const employeeId = remainingIdParts.slice(0, -1).join('-');
|
||||||
const tour = await this.dataService.findTourByOperationId(operationId);
|
const tour = await this.dataService.findTourByOperationId(operationId);
|
||||||
const employee = await this.dataService.findEmployeeById(employeeId);
|
const employee = await this.dataService.findEmployeeById(employeeId);
|
||||||
|
if(!tour?.startDate) {
|
||||||
|
this.logger.error(`tour ${tour?.operationId} has no startDate`)
|
||||||
|
}
|
||||||
const startOfTour = this.getStartOfTour(
|
const startOfTour = this.getStartOfTour(
|
||||||
new Date(tour.startDate),
|
new Date(tour?.startDate),
|
||||||
[
|
[
|
||||||
tour.startBegin,
|
tour?.startBegin,
|
||||||
tour.startEnd,
|
tour?.startEnd,
|
||||||
tour.target,
|
tour?.target,
|
||||||
tour.targetBegin,
|
tour?.targetBegin,
|
||||||
tour.empty,
|
tour?.empty,
|
||||||
].filter((val) => !!val)[0],
|
].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[]> {
|
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 toursInRange = await this.dataService.findToursInInterval(inInterval);
|
||||||
|
|
||||||
const toursMap = this.groupToursByDay(toursInRange);
|
const toursMap = this.groupToursByDay(toursInRange);
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,9 @@ export class WorkingEmployeeNotInDistributionSystemDetector extends AnomalyDetec
|
||||||
readonly type = AnomalyTypes.WorkingEmployeeNotInDistributionSystem;
|
readonly type = AnomalyTypes.WorkingEmployeeNotInDistributionSystem;
|
||||||
|
|
||||||
async detect(inInterval: Interval): Promise<Prisma.AnomalyCreateInput[]> {
|
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 =
|
const attendanceRegistrationInInterval =
|
||||||
await this.dataService.findAttendanceRegistrationInInterval(inInterval, [
|
await this.dataService.findAttendanceRegistrationInInterval(inInterval, [
|
||||||
AttendanceRegistrationEvents.ClockedIn,
|
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',
|
DriverCodriverSame = 'DriverCodriverSame',
|
||||||
DriverCertificate = 'DriverCertificate',
|
DriverCertificate = 'DriverCertificate',
|
||||||
EmployeesLoggedIn = 'EmployeesLoggedIn',
|
EmployeesLoggedIn = 'EmployeesLoggedIn',
|
||||||
|
EmployeeStillWorking = 'EmployeeStillWorking',
|
||||||
|
IsAttendanceBeforeWorktime = 'IsAttendanceBeforeWorktime',
|
||||||
|
IsGermanZip = 'IsGermanZip',
|
||||||
|
IsWorktimePlanned = 'IsWorktimePlanned',
|
||||||
OneTourAtATime = 'OneTourAtATime',
|
OneTourAtATime = 'OneTourAtATime',
|
||||||
WorkingEmployeeNotInDistributionSystem = 'WorkingEmployeeNotInDistributionSystem',
|
WorkingEmployeeNotInDistributionSystem = 'WorkingEmployeeNotInDistributionSystem',
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,29 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { AttendanceRegistration } from '@prisma/client';
|
import { AttendanceRegistration } from '@prisma/client';
|
||||||
import { Interval } from 'date-fns';
|
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';
|
import { AttendanceRegistrationEvents } from './attendance-registration-events.enum';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AttendanceRegistrationRepository {
|
export class AttendanceRegistrationRepository {
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
constructor(
|
||||||
|
@Inject(KYSELY_CLIENT_PROVIDER_TOKEN) private readonly db: Kysely<DB>,
|
||||||
|
) {}
|
||||||
|
|
||||||
async findById(
|
async findById(
|
||||||
attendanceRegistrationId: number,
|
attendanceRegistrationId: number,
|
||||||
): Promise<AttendanceRegistration> {
|
): Promise<AttendanceRegistration> {
|
||||||
return await this.prisma.attendanceRegistration.findUnique({
|
return await this.db
|
||||||
where: {
|
.selectFrom('AttendanceRegistration')
|
||||||
|
.selectAll()
|
||||||
|
.where(
|
||||||
|
'AttendanceRegistration.attendanceRegistrationId',
|
||||||
|
'=',
|
||||||
attendanceRegistrationId,
|
attendanceRegistrationId,
|
||||||
},
|
)
|
||||||
});
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
async findInInterval(
|
async findInInterval(
|
||||||
|
|
@ -25,28 +33,14 @@ export class AttendanceRegistrationRepository {
|
||||||
AttendanceRegistrationEvents.ClockedOut,
|
AttendanceRegistrationEvents.ClockedOut,
|
||||||
],
|
],
|
||||||
): Promise<AttendanceRegistration[]> {
|
): Promise<AttendanceRegistration[]> {
|
||||||
return await this.prisma.attendanceRegistration.findMany({
|
return await this.db
|
||||||
where: {
|
.selectFrom('AttendanceRegistration')
|
||||||
event: {
|
.selectAll()
|
||||||
in: eventTypes,
|
.where('AttendanceRegistration.event', 'in', eventTypes)
|
||||||
},
|
.where('AttendanceRegistration.dateTime', '>=', new Date(interval.start))
|
||||||
AND: [
|
.where('AttendanceRegistration.dateTime', '<', new Date(interval.end))
|
||||||
{
|
.orderBy('AttendanceRegistration.dateTime asc')
|
||||||
dateTime: {
|
.execute();
|
||||||
gte: new Date(interval.start),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
dateTime: {
|
|
||||||
lt: new Date(interval.end),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
orderBy: {
|
|
||||||
dateTime: 'asc',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async findByEmployeeInInterval(
|
async findByEmployeeInInterval(
|
||||||
|
|
@ -57,28 +51,14 @@ export class AttendanceRegistrationRepository {
|
||||||
AttendanceRegistrationEvents.ClockedOut,
|
AttendanceRegistrationEvents.ClockedOut,
|
||||||
],
|
],
|
||||||
): Promise<AttendanceRegistration[]> {
|
): Promise<AttendanceRegistration[]> {
|
||||||
return await this.prisma.attendanceRegistration.findMany({
|
return await this.db
|
||||||
where: {
|
.selectFrom('AttendanceRegistration')
|
||||||
employeeId,
|
.selectAll()
|
||||||
event: {
|
.where('AttendanceRegistration.employeeId', '=', employeeId)
|
||||||
in: eventTypes,
|
.where('AttendanceRegistration.event', 'in', eventTypes)
|
||||||
},
|
.where('AttendanceRegistration.dateTime', '>=', new Date(timeRange.start))
|
||||||
AND: [
|
.where('AttendanceRegistration.dateTime', '<', new Date(timeRange.end))
|
||||||
{
|
.orderBy('AttendanceRegistration.dateTime asc')
|
||||||
dateTime: {
|
.execute();
|
||||||
gte: new Date(timeRange.start),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
dateTime: {
|
|
||||||
lt: new Date(timeRange.end),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
orderBy: {
|
|
||||||
dateTime: 'asc',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,28 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Employee } from '@prisma/client';
|
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()
|
@Injectable()
|
||||||
export class EmployeeRepository {
|
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> {
|
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> {
|
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 { 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 { PrismaService } from 'src/core/database/prisma.service';
|
||||||
|
import { DB } from 'src/core/database/types';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PlannedTimeRepository {
|
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> {
|
async findById(id: string): Promise<PlannedTime> {
|
||||||
return await this.prisma.plannedTime.findUnique({
|
return await this.prisma.plannedTime.findUnique({
|
||||||
|
|
@ -17,23 +23,13 @@ export class PlannedTimeRepository {
|
||||||
personnelNumber: string,
|
personnelNumber: string,
|
||||||
interval: Interval,
|
interval: Interval,
|
||||||
): Promise<PlannedTime[]> {
|
): Promise<PlannedTime[]> {
|
||||||
return await this.prisma.plannedTime.findMany({
|
return await this.db
|
||||||
where: {
|
.selectFrom('PlannedTime')
|
||||||
userId: personnelNumber,
|
.selectAll()
|
||||||
AND: [
|
.where('PlannedTime.userId', '=', personnelNumber)
|
||||||
{
|
.where('PlannedTime.startDate', '>=', new Date(interval.start))
|
||||||
startDate: {
|
.where('PlannedTime.startDate', '<', new Date(interval.end))
|
||||||
gte: new Date(interval.start),
|
.execute();
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
startDate: {
|
|
||||||
lt: new Date(interval.end),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async findByOrdinanceTypesInInterval(
|
async findByOrdinanceTypesInInterval(
|
||||||
|
|
|
||||||
|
|
@ -133,18 +133,13 @@ export class TicketRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
async findIdByTourId(tourId: string): Promise<{ id: string }> {
|
async findIdByTourId(tourId: string): Promise<{ id: string }> {
|
||||||
return await this.prisma.ticket.findFirst({
|
return await this.db
|
||||||
select: {
|
.selectFrom('Ticket')
|
||||||
id: true,
|
.select(['Ticket.id'])
|
||||||
},
|
.innerJoin('Tour', 'Tour.ticketId', 'Ticket.id')
|
||||||
where: {
|
.where('Tour.id', '=', tourId)
|
||||||
tours: {
|
.limit(1)
|
||||||
some: {
|
.executeTakeFirst();
|
||||||
id: tourId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(query: string): Promise<(Ticket & { tours: Tour[] })[]> {
|
async search(query: string): Promise<(Ticket & { tours: Tour[] })[]> {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Prisma, Tour } from '@prisma/client';
|
import { Prisma, Tour } from '@prisma/client';
|
||||||
import { endOfDay, Interval, startOfDay } from 'date-fns';
|
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 { KYSELY_CLIENT_PROVIDER_TOKEN } from 'src/core/database/database.module';
|
||||||
import { PrismaService } from 'src/core/database/prisma.service';
|
import { PrismaService } from 'src/core/database/prisma.service';
|
||||||
import { DB } from 'src/core/database/types';
|
import { DB } from 'src/core/database/types';
|
||||||
|
|
@ -12,6 +12,11 @@ import {
|
||||||
TourAccountingFilterDto,
|
TourAccountingFilterDto,
|
||||||
} from './tours.filter';
|
} 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[] {
|
function onlyRealTours(): Prisma.TourWhereInput[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|
@ -58,13 +63,13 @@ function onlyRealTours(): Prisma.TourWhereInput[] {
|
||||||
{
|
{
|
||||||
patientId: {
|
patientId: {
|
||||||
// Pause
|
// Pause
|
||||||
not: 'b7424477-a4b2-42f7-b7e7-a4bef24f6979',
|
not: PAUSE_PATIENT_ID,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
patientId: {
|
patientId: {
|
||||||
// Feierabend
|
// 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()
|
@Injectable()
|
||||||
export class TourRepository {
|
export class TourRepository {
|
||||||
constructor(
|
constructor(
|
||||||
|
|
@ -164,42 +187,24 @@ export class TourRepository {
|
||||||
employeeId: string,
|
employeeId: string,
|
||||||
ordinanceTypes: string[] = [],
|
ordinanceTypes: string[] = [],
|
||||||
): Promise<Tour[]> {
|
): Promise<Tour[]> {
|
||||||
return this.prisma.tour.findMany({
|
return this.db
|
||||||
where: {
|
.selectFrom('Tour')
|
||||||
AND: [
|
.selectAll()
|
||||||
{
|
.where((eb) =>
|
||||||
OR: [
|
eb.and([
|
||||||
{
|
onlyRealToursKysely(eb),
|
||||||
driverId: employeeId,
|
eb.or([
|
||||||
},
|
eb('Tour.driverId', '=', employeeId),
|
||||||
{
|
eb('Tour.codriverId', '=', employeeId),
|
||||||
codriverId: employeeId,
|
]),
|
||||||
},
|
eb('Tour.startDate', '>=', new Date(start)),
|
||||||
],
|
eb('Tour.startDate', '<', new Date(end)),
|
||||||
},
|
...(ordinanceTypes.length > 0
|
||||||
{
|
? [eb('Tour.ordinanceType', 'in', ordinanceTypes)]
|
||||||
startDate: {
|
: []),
|
||||||
gte: new Date(start),
|
]),
|
||||||
},
|
)
|
||||||
},
|
.execute();
|
||||||
{
|
|
||||||
startDate: {
|
|
||||||
lt: new Date(end),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
OR: [
|
|
||||||
...(ordinanceTypes.length
|
|
||||||
? ordinanceTypes.map((ot) => ({
|
|
||||||
ordinanceType: { contains: ot },
|
|
||||||
}))
|
|
||||||
: []),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
...onlyRealTours(),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async findByTicketId(ticketId: string): Promise<Tour[]> {
|
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 { WorkTime } from '@prisma/client';
|
||||||
import { Interval } from 'date-fns';
|
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()
|
@Injectable()
|
||||||
export class WorkTimeRepository {
|
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> {
|
async findById(id: string): Promise<WorkTime> {
|
||||||
return await this.prisma.workTime.findUnique({
|
return await this.db
|
||||||
where: { id },
|
.selectFrom('WorkTime')
|
||||||
});
|
.selectAll()
|
||||||
|
.where('WorkTime.id', '=', id)
|
||||||
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
async findByPersonnelNumberInInterval(
|
async findByPersonnelNumberInInterval(
|
||||||
personnelNumber: string,
|
personnelNumber: string,
|
||||||
interval: Interval,
|
interval: Interval,
|
||||||
): Promise<WorkTime> {
|
): Promise<WorkTime> {
|
||||||
return await this.prisma.workTime.findFirst({
|
return await this.db
|
||||||
where: {
|
.selectFrom('WorkTime')
|
||||||
userId: personnelNumber,
|
.selectAll()
|
||||||
AND: [
|
.where('WorkTime.userId', '=', personnelNumber)
|
||||||
{
|
.where('WorkTime.startDate', '>=', new Date(interval.start))
|
||||||
startDate: {
|
.where('WorkTime.startDate', '<=', new Date(interval.end))
|
||||||
gte: new Date(interval.start),
|
.executeTakeFirst();
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
startDate: {
|
|
||||||
lte: new Date(interval.end),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { PrismaModule } from 'src/core/database/prisma.module';
|
import { PrismaModule } from 'src/core/database/prisma.module';
|
||||||
|
import { FeatBusinessObjectsModule } from '../feat-business-objects/feat-business-objects.module';
|
||||||
import { CalculationService } from './calculation.service';
|
import { CalculationService } from './calculation.service';
|
||||||
import { PersonnelCostCalculationService } from './personnel-cost-calculation.service';
|
import { PersonnelCostCalculationService } from './personnel-cost-calculation.service';
|
||||||
import { FeatBusinessObjectsModule } from '../feat-business-objects/feat-business-objects.module';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule, FeatBusinessObjectsModule],
|
imports: [PrismaModule, FeatBusinessObjectsModule],
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { DyflexisExtractorsService } from './dyflexis-extractors.service';
|
||||||
import { ExtractorsService } from './extractors.service';
|
import { ExtractorsService } from './extractors.service';
|
||||||
import { SourceRepository } from './source.repository';
|
import { SourceRepository } from './source.repository';
|
||||||
import { DispoLiveApiExtractorsService } from './dispo-live-api-extractors.service';
|
import { DispoLiveApiExtractorsService } from './dispo-live-api-extractors.service';
|
||||||
|
import { DyflexisUserAccessExtractorsService } from './dyflexis-user-access-extractors.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule, HttpModule],
|
imports: [PrismaModule, HttpModule],
|
||||||
|
|
@ -13,6 +14,7 @@ import { DispoLiveApiExtractorsService } from './dispo-live-api-extractors.servi
|
||||||
ExtractorsService,
|
ExtractorsService,
|
||||||
DispoLiveExtractorsService,
|
DispoLiveExtractorsService,
|
||||||
DispoLiveApiExtractorsService,
|
DispoLiveApiExtractorsService,
|
||||||
|
DyflexisUserAccessExtractorsService,
|
||||||
DyflexisExtractorsService,
|
DyflexisExtractorsService,
|
||||||
SourceRepository,
|
SourceRepository,
|
||||||
],
|
],
|
||||||
|
|
@ -20,6 +22,7 @@ import { DispoLiveApiExtractorsService } from './dispo-live-api-extractors.servi
|
||||||
ExtractorsService,
|
ExtractorsService,
|
||||||
DispoLiveApiExtractorsService,
|
DispoLiveApiExtractorsService,
|
||||||
DispoLiveExtractorsService,
|
DispoLiveExtractorsService,
|
||||||
|
DyflexisUserAccessExtractorsService,
|
||||||
DyflexisExtractorsService
|
DyflexisExtractorsService
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ export class DyflexisExtractorsService {
|
||||||
contract_end,
|
contract_end,
|
||||||
contract_hours_week,
|
contract_hours_week,
|
||||||
contract_salary_hour,
|
contract_salary_hour,
|
||||||
|
contract_type_name,
|
||||||
} = employee;
|
} = employee;
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
|
|
@ -82,6 +83,7 @@ export class DyflexisExtractorsService {
|
||||||
contract_end,
|
contract_end,
|
||||||
contract_hours_week,
|
contract_hours_week,
|
||||||
contract_salary_hour,
|
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,
|
beifahrerName,
|
||||||
fahrer,
|
fahrer,
|
||||||
fahrerName,
|
fahrerName,
|
||||||
|
overRTW,
|
||||||
|
firmName,
|
||||||
|
schwerlast,
|
||||||
color,
|
color,
|
||||||
} = tour;
|
} = tour;
|
||||||
return {
|
return {
|
||||||
|
|
@ -135,6 +138,9 @@ export class TourExtractor {
|
||||||
beifahrerName: beifahrerName ? `${beifahrerName}` : undefined,
|
beifahrerName: beifahrerName ? `${beifahrerName}` : undefined,
|
||||||
fahrer: fahrer ? `${fahrer}` : undefined,
|
fahrer: fahrer ? `${fahrer}` : undefined,
|
||||||
fahrerName: fahrerName ? `${fahrerName}` : undefined,
|
fahrerName: fahrerName ? `${fahrerName}` : undefined,
|
||||||
|
overRTW: overRTW ? `${overRTW}` : undefined,
|
||||||
|
companyName: firmName ? `${firmName}` : undefined,
|
||||||
|
schwerlast: schwerlast ? `${schwerlast}` : undefined,
|
||||||
color: color ? `${color}` : undefined,
|
color: color ? `${color}` : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,10 @@ export interface TourRaw {
|
||||||
beifahrer?: string; // ID
|
beifahrer?: string; // ID
|
||||||
fahrerName?: string;
|
fahrerName?: string;
|
||||||
fahrer?: string; // ID
|
fahrer?: string; // ID
|
||||||
|
|
||||||
|
overRTW?: string;
|
||||||
|
firmName?: string;
|
||||||
|
schwerlast?: string;
|
||||||
|
|
||||||
color?: string; // Hex rgb
|
color?: string; // Hex rgb
|
||||||
}
|
}
|
||||||
|
|
@ -141,6 +145,7 @@ export interface EmployeeDyflexisRaw {
|
||||||
contract_end?: string; // yyyy-MM-dd
|
contract_end?: string; // yyyy-MM-dd
|
||||||
contract_hours_week?: string; // Float string
|
contract_hours_week?: string; // Float string
|
||||||
contract_salary_hour?: string; // Float string
|
contract_salary_hour?: string; // Float string
|
||||||
|
contract_type_name?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TourSummaryDto {
|
export interface TourSummaryDto {
|
||||||
|
|
@ -153,28 +158,44 @@ export interface TourSummaryDto {
|
||||||
|
|
||||||
export interface FahrzeugplanungItemDto {
|
export interface FahrzeugplanungItemDto {
|
||||||
_id?: string;
|
_id?: string;
|
||||||
className: string; // 'greenItem' OR 'blocked'
|
className: string; // 'greenItem' OR 'blocked'
|
||||||
group: string; // Fahrzeug ID
|
group: string; // Fahrzeug ID
|
||||||
mitarbeiterId: string; // Mitarbeiter ID
|
mitarbeiterId: string; // Mitarbeiter ID
|
||||||
start: string; // ISO 8601 TS (UTC)
|
start: string; // ISO 8601 TS (UTC)
|
||||||
end: string; // ISO 8601 TS (UTC)
|
end: string; // ISO 8601 TS (UTC)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FahrzeugItemDto {
|
export interface FahrzeugItemDto {
|
||||||
_id: string
|
_id: string;
|
||||||
bezeichnung: string
|
bezeichnung: string;
|
||||||
marke: string
|
marke: string;
|
||||||
tuev: string
|
tuev: string;
|
||||||
conn: string
|
conn: string;
|
||||||
carStatus: string
|
carStatus: string;
|
||||||
kaufDatum: string
|
kaufDatum: string;
|
||||||
erstzulassung: string
|
erstzulassung: string;
|
||||||
status: string
|
status: string;
|
||||||
firmaId: string
|
firmaId: string;
|
||||||
kennzeichen: string
|
kennzeichen: string;
|
||||||
kaufPreis: string
|
kaufPreis: string;
|
||||||
kaufKm: string
|
kaufKm: string;
|
||||||
fahrzeugIdentNr: string
|
fahrzeugIdentNr: string;
|
||||||
kfzNr: string
|
kfzNr: string;
|
||||||
verordnungsart: 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 {
|
import {
|
||||||
AttendanceRegistrationSource,
|
AttendanceRegistrationSource,
|
||||||
EmployeeDispoLiveSource,
|
EmployeeDispoLiveSource,
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
PatientSource,
|
PatientSource,
|
||||||
PlannedTimeSource,
|
PlannedTimeSource,
|
||||||
Prisma,
|
Prisma,
|
||||||
|
Sickdays,
|
||||||
TourSource,
|
TourSource,
|
||||||
WorkTimeSource,
|
WorkTimeSource,
|
||||||
} from '@prisma/client';
|
} from '@prisma/client';
|
||||||
|
|
@ -14,15 +15,23 @@ import {
|
||||||
AttendanceRegistrationRaw,
|
AttendanceRegistrationRaw,
|
||||||
EmployeeDispoLiveRaw,
|
EmployeeDispoLiveRaw,
|
||||||
EmployeeDyflexisRaw,
|
EmployeeDyflexisRaw,
|
||||||
|
HolidayRaw,
|
||||||
PatientRaw,
|
PatientRaw,
|
||||||
PlannedTimeRaw,
|
PlannedTimeRaw,
|
||||||
|
SickDaysRaw,
|
||||||
TourRaw,
|
TourRaw,
|
||||||
WorkTimeRaw,
|
WorkTimeRaw,
|
||||||
} from './source.interface';
|
} 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()
|
@Injectable()
|
||||||
export class SourceRepository {
|
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[]> {
|
clearSource(): Promise<Prisma.BatchPayload[]> {
|
||||||
return Promise.all([
|
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(
|
createPatients(
|
||||||
patients: PatientRaw[],
|
patients: PatientRaw[],
|
||||||
jobId: string,
|
jobId: string,
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import { LoadingDoneEvent } from './events/loading-done.event';
|
||||||
import { TransformationDoneEvent } from './events/transformation-done.event';
|
import { TransformationDoneEvent } from './events/transformation-done.event';
|
||||||
import { TourUpdateChecker } from './producer/tour-update';
|
import { TourUpdateChecker } from './producer/tour-update';
|
||||||
import { SchichtplanungSynchronizer } from './producer/schichtplanung';
|
import { SchichtplanungSynchronizer } from './producer/schichtplanung';
|
||||||
|
import { DyflexisUserAccessExtractorsService } from '../feat-extractors/dyflexis-user-access-extractors.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ImporterService {
|
export class ImporterService {
|
||||||
|
|
@ -24,8 +25,13 @@ export class ImporterService {
|
||||||
private readonly eventEmitter: EventEmitter2,
|
private readonly eventEmitter: EventEmitter2,
|
||||||
private readonly tourUpdateChecker: TourUpdateChecker,
|
private readonly tourUpdateChecker: TourUpdateChecker,
|
||||||
private readonly schichtplanungSynchronizer: SchichtplanungSynchronizer,
|
private readonly schichtplanungSynchronizer: SchichtplanungSynchronizer,
|
||||||
|
private readonly dyflexisUserAccessService: DyflexisUserAccessExtractorsService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
runDyflexisUserAccessTest() {
|
||||||
|
return this.dyflexisUserAccessService.loadData()
|
||||||
|
}
|
||||||
|
|
||||||
runTourUpdateCheck(interval: Interval) {
|
runTourUpdateCheck(interval: Interval) {
|
||||||
return this.tourUpdateChecker.fetchTourUpdates(interval);
|
return this.tourUpdateChecker.fetchTourUpdates(interval);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import { InjectQueue, Processor, WorkerHost } from '@nestjs/bullmq';
|
import { Processor, WorkerHost } from '@nestjs/bullmq';
|
||||||
import { Job, Queue, Worker } from '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 { 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 { TOURS_QUEUE } from 'src/modules/feat-business-objects/entities/tour/tour.tokens';
|
||||||
import { DispoLiveExtractorsService } from '../../feat-extractors/dispo-live-extractors.service';
|
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 { ToursCalculationAdapater } from '../../feat-transformers/adapter/tours-calculation.adapter';
|
||||||
import { Logger } from '@nestjs/common';
|
import { TourTransformer } from '../../feat-transformers/transformers/tour.transformer';
|
||||||
|
|
||||||
@Processor(TOURS_QUEUE, {
|
@Processor(TOURS_QUEUE, {
|
||||||
limiter: {
|
limiter: {
|
||||||
|
|
@ -53,9 +53,15 @@ export class ToursConsumer extends WorkerHost {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (isCreate || job.name === 'Create') {
|
if (isCreate || job.name === 'Create') {
|
||||||
return await this.service.createTour(tour).then(() => this.logger.log(`Created tour ${tour.id} successfully`));
|
return await this.service
|
||||||
}
|
.createTour(tour)
|
||||||
return await this.service.updateTour(tour.id, tour).then(() => this.logger.log(`Updated tour ${tour.id} successfully`));
|
.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:
|
default:
|
||||||
console.log(`No case matched`, job);
|
console.log(`No case matched`, job);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { InjectQueue } from '@nestjs/bullmq';
|
import { InjectQueue } from '@nestjs/bullmq';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { Queue } from 'bullmq';
|
import { Queue } from 'bullmq';
|
||||||
import { addMinutes, endOfDay, getMonth, getYear, isSameDay, startOfDay } from 'date-fns';
|
import { addMinutes, endOfDay, getMonth, getYear, isSameDay, startOfDay } from 'date-fns';
|
||||||
import {
|
import {
|
||||||
|
|
@ -17,6 +17,8 @@ import { SCHICHTPLANUNG_QUEUE } from '../tokens';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SchichtplanungSynchronizer {
|
export class SchichtplanungSynchronizer {
|
||||||
|
private logger = new Logger(SchichtplanungSynchronizer.name)
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly dlApiService: DispoLiveApiExtractorsService,
|
private readonly dlApiService: DispoLiveApiExtractorsService,
|
||||||
private readonly dyflexisService: DyflexisExtractorsService,
|
private readonly dyflexisService: DyflexisExtractorsService,
|
||||||
|
|
@ -61,7 +63,7 @@ export class SchichtplanungSynchronizer {
|
||||||
plannedTime.user_id,
|
plannedTime.user_id,
|
||||||
);
|
);
|
||||||
if (!employee) {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ export class AttendanceRegistrationLoader {
|
||||||
private static attendanceRegistrationStagingToFinal(
|
private static attendanceRegistrationStagingToFinal(
|
||||||
attendanceregistration: AttendanceRegistrationStaging,
|
attendanceregistration: AttendanceRegistrationStaging,
|
||||||
): Prisma.AttendanceRegistrationCreateInput {
|
): Prisma.AttendanceRegistrationCreateInput {
|
||||||
const { attendanceRegistrationId, employeeId, dateTime, event } =
|
const { attendanceRegistrationId, employeeId, dateTime, event, personnelNumber } =
|
||||||
attendanceregistration;
|
attendanceregistration;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -76,7 +76,7 @@ export class AttendanceRegistrationLoader {
|
||||||
employeeId,
|
employeeId,
|
||||||
dateTime,
|
dateTime,
|
||||||
event,
|
event,
|
||||||
personnelNumber: '',
|
personnelNumber,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ export class EmployeeDyflexisLoader {
|
||||||
contractEnd,
|
contractEnd,
|
||||||
contractHoursWeek,
|
contractHoursWeek,
|
||||||
contractSalaryHour,
|
contractSalaryHour,
|
||||||
|
contractTypeName
|
||||||
} = employee;
|
} = employee;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -66,6 +67,7 @@ export class EmployeeDyflexisLoader {
|
||||||
contractEnd,
|
contractEnd,
|
||||||
contractHoursWeek,
|
contractHoursWeek,
|
||||||
contractSalaryHour,
|
contractSalaryHour,
|
||||||
|
contractTypeName
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,8 @@ export class PlannedTimeLoader {
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
mark,
|
mark,
|
||||||
|
pause,
|
||||||
|
duration,
|
||||||
} = plannedtime;
|
} = plannedtime;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -63,11 +65,11 @@ export class PlannedTimeLoader {
|
||||||
surname,
|
surname,
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
|
pause,
|
||||||
|
duration,
|
||||||
id: `${id}`,
|
id: `${id}`,
|
||||||
userId: `${employeeId}`,
|
userId: `${employeeId}`,
|
||||||
department: departmentName,
|
department: departmentName,
|
||||||
pause: 0,
|
|
||||||
duration: 0,
|
|
||||||
note: mark,
|
note: mark,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -91,10 +91,13 @@ export class TourLoader {
|
||||||
revenue,
|
revenue,
|
||||||
revenueDeviation,
|
revenueDeviation,
|
||||||
consumptionCosts,
|
consumptionCosts,
|
||||||
|
overRTW,
|
||||||
|
companyName,
|
||||||
|
schwerlast,
|
||||||
billDate,
|
billDate,
|
||||||
billNumber,
|
billNumber,
|
||||||
createdAt,
|
createdAt,
|
||||||
updatedAt
|
updatedAt,
|
||||||
} = tour;
|
} = tour;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -143,6 +146,9 @@ export class TourLoader {
|
||||||
revenue,
|
revenue,
|
||||||
revenueDeviation,
|
revenueDeviation,
|
||||||
consumptionCosts,
|
consumptionCosts,
|
||||||
|
overRTW,
|
||||||
|
companyName,
|
||||||
|
schwerlast,
|
||||||
billDate,
|
billDate,
|
||||||
billNumber,
|
billNumber,
|
||||||
createdAt,
|
createdAt,
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,8 @@ export class WorkTimeLoader {
|
||||||
departmentName,
|
departmentName,
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
|
pause,
|
||||||
|
duration
|
||||||
} = worktime;
|
} = worktime;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -60,11 +62,11 @@ export class WorkTimeLoader {
|
||||||
surname,
|
surname,
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
|
pause,
|
||||||
|
duration,
|
||||||
id: `${id}`,
|
id: `${id}`,
|
||||||
userId: `${employeeId}`,
|
userId: `${employeeId}`,
|
||||||
department: departmentName,
|
department: departmentName,
|
||||||
pause: 0,
|
|
||||||
duration: 0,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,10 @@ export interface TourPreStaging {
|
||||||
revenueDeviation?: number;
|
revenueDeviation?: number;
|
||||||
consumptionCosts?: number;
|
consumptionCosts?: number;
|
||||||
|
|
||||||
|
overRTW?: boolean;
|
||||||
|
companyName?: string;
|
||||||
|
schwerlast?: boolean;
|
||||||
|
|
||||||
createdAt?: Date;
|
createdAt?: Date;
|
||||||
updatedAt?: Date;
|
updatedAt?: Date;
|
||||||
}
|
}
|
||||||
|
|
@ -91,6 +95,8 @@ export interface PlannedTimePreStaging {
|
||||||
departmentName: string;
|
departmentName: string;
|
||||||
startDate: Date;
|
startDate: Date;
|
||||||
endDate: Date;
|
endDate: Date;
|
||||||
|
pause: number;
|
||||||
|
duration: number;
|
||||||
deleted: boolean;
|
deleted: boolean;
|
||||||
mark?: string;
|
mark?: string;
|
||||||
}
|
}
|
||||||
|
|
@ -103,6 +109,8 @@ export interface WorkTimePreStaging {
|
||||||
departmentName: string;
|
departmentName: string;
|
||||||
startDate: Date;
|
startDate: Date;
|
||||||
endDate: Date;
|
endDate: Date;
|
||||||
|
pause: number;
|
||||||
|
duration: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EmployeeDyflexisPreStaging {
|
export interface EmployeeDyflexisPreStaging {
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ export class AttendanceRegistrationTransformer {
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
const { attendanceRegistrationId, employeeId, dateTime, event } =
|
const { attendanceRegistrationId, employeeId, dateTime, event, personnelNumber } =
|
||||||
attendanceRegistrationParsed;
|
attendanceRegistrationParsed;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -31,6 +31,7 @@ export class AttendanceRegistrationTransformer {
|
||||||
employeeId,
|
employeeId,
|
||||||
dateTime,
|
dateTime,
|
||||||
event,
|
event,
|
||||||
|
personnelNumber,
|
||||||
} as unknown as AttendanceRegistrationPreStaging;
|
} as unknown as AttendanceRegistrationPreStaging;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ export class DyflexisEmployeeTransformer {
|
||||||
contract_end,
|
contract_end,
|
||||||
contract_hours_week,
|
contract_hours_week,
|
||||||
contract_salary_hour,
|
contract_salary_hour,
|
||||||
|
contract_type_name,
|
||||||
} = dyflexisEmployeeParsed;
|
} = dyflexisEmployeeParsed;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -46,6 +47,7 @@ export class DyflexisEmployeeTransformer {
|
||||||
contractEnd: contract_end,
|
contractEnd: contract_end,
|
||||||
contractHoursWeek: contract_hours_week,
|
contractHoursWeek: contract_hours_week,
|
||||||
contractSalaryHour: contract_salary_hour,
|
contractSalaryHour: contract_salary_hour,
|
||||||
|
contractTypeName: contract_type_name,
|
||||||
} as unknown as EmployeeDyflexisPreStaging;
|
} as unknown as EmployeeDyflexisPreStaging;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ export class PlannedTimeTransformer {
|
||||||
// firstname: faker.person.firstName(),
|
// firstname: faker.person.firstName(),
|
||||||
// surname: faker.person.lastName(),
|
// surname: faker.person.lastName(),
|
||||||
...transformProperties(parseInteger)(
|
...transformProperties(parseInteger)(
|
||||||
pick(['id', 'user_id'])(plannedTimeSource),
|
pick(['id', 'user_id', 'pauze', 'duration'])(plannedTimeSource),
|
||||||
),
|
),
|
||||||
...transformProperties(parseCETDateTime('yyyy-MM-dd HH:mm:ss'))(
|
...transformProperties(parseCETDateTime('yyyy-MM-dd HH:mm:ss'))(
|
||||||
pick(['start_date', 'end_date'])(plannedTimeSource),
|
pick(['start_date', 'end_date'])(plannedTimeSource),
|
||||||
|
|
@ -36,6 +36,8 @@ export class PlannedTimeTransformer {
|
||||||
department_name,
|
department_name,
|
||||||
start_date,
|
start_date,
|
||||||
end_date,
|
end_date,
|
||||||
|
pauze,
|
||||||
|
duration,
|
||||||
deleted,
|
deleted,
|
||||||
mark,
|
mark,
|
||||||
} = plannedTimeParsed;
|
} = plannedTimeParsed;
|
||||||
|
|
@ -44,6 +46,8 @@ export class PlannedTimeTransformer {
|
||||||
id,
|
id,
|
||||||
firstname,
|
firstname,
|
||||||
surname,
|
surname,
|
||||||
|
pause: pauze,
|
||||||
|
duration,
|
||||||
deleted,
|
deleted,
|
||||||
mark,
|
mark,
|
||||||
employeeId: user_id,
|
employeeId: user_id,
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ export class TourTransformer {
|
||||||
)
|
)
|
||||||
: undefined),
|
: undefined),
|
||||||
...transformProperties(parseBoolean)(
|
...transformProperties(parseBoolean)(
|
||||||
pick(['infektion', 'gefahren'])(tourSource),
|
pick(['infektion', 'gefahren', 'overRTW', 'schwerlast'])(tourSource),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -111,6 +111,9 @@ export class TourTransformer {
|
||||||
gesamtPreis,
|
gesamtPreis,
|
||||||
abrDate,
|
abrDate,
|
||||||
abrNum,
|
abrNum,
|
||||||
|
overRTW,
|
||||||
|
companyName,
|
||||||
|
schwerlast,
|
||||||
createTime,
|
createTime,
|
||||||
lastUpdate,
|
lastUpdate,
|
||||||
} = tourParsed;
|
} = tourParsed;
|
||||||
|
|
@ -160,6 +163,9 @@ export class TourTransformer {
|
||||||
revenueDispoLive: gesamtPreis,
|
revenueDispoLive: gesamtPreis,
|
||||||
billDate: abrDate,
|
billDate: abrDate,
|
||||||
billNumber: abrNum,
|
billNumber: abrNum,
|
||||||
|
overRTW,
|
||||||
|
companyName,
|
||||||
|
schwerlast,
|
||||||
createdAt: createTime,
|
createdAt: createTime,
|
||||||
updatedAt: lastUpdate,
|
updatedAt: lastUpdate,
|
||||||
revenue: 0,
|
revenue: 0,
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ export class WorkTimeTransformer {
|
||||||
// firstname: faker.person.firstName(),
|
// firstname: faker.person.firstName(),
|
||||||
// surname: faker.person.lastName(),
|
// surname: faker.person.lastName(),
|
||||||
...transformProperties(parseInteger)(
|
...transformProperties(parseInteger)(
|
||||||
pick(['id', 'user_id'])(workTimeSource),
|
pick(['id', 'user_id', 'pauze', 'duration'])(workTimeSource),
|
||||||
),
|
),
|
||||||
...transformProperties(parseCETDateTime('yyyy-MM-dd HH:mm:ss'))(
|
...transformProperties(parseCETDateTime('yyyy-MM-dd HH:mm:ss'))(
|
||||||
pick(['start_date', 'end_date'])(workTimeSource),
|
pick(['start_date', 'end_date'])(workTimeSource),
|
||||||
|
|
@ -30,12 +30,16 @@ export class WorkTimeTransformer {
|
||||||
department_name,
|
department_name,
|
||||||
start_date,
|
start_date,
|
||||||
end_date,
|
end_date,
|
||||||
|
pauze,
|
||||||
|
duration,
|
||||||
} = workTimeParsed;
|
} = workTimeParsed;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
firstname,
|
firstname,
|
||||||
surname,
|
surname,
|
||||||
|
duration,
|
||||||
|
pause: pauze,
|
||||||
employeeId: user_id,
|
employeeId: user_id,
|
||||||
departmentName: department_name,
|
departmentName: department_name,
|
||||||
startDate: start_date,
|
startDate: start_date,
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,8 @@ export class TicketLifecycleService {
|
||||||
({ ordinanceType }) =>
|
({ ordinanceType }) =>
|
||||||
ordinanceType.indexOf('BTW') > -1 ||
|
ordinanceType.indexOf('BTW') > -1 ||
|
||||||
ordinanceType.indexOf('TSW') > -1,
|
ordinanceType.indexOf('TSW') > -1,
|
||||||
)
|
) &&
|
||||||
|
ticket.currentState != TicketValidationState.TD_UNCERTAIN
|
||||||
) {
|
) {
|
||||||
await this.ticketService.updateTicket(ticket.id, {
|
await this.ticketService.updateTicket(ticket.id, {
|
||||||
errors,
|
errors,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue