(data-hub): add etl feature

This commit is contained in:
Marcel Arndt
2024-07-09 17:23:08 +02:00
parent 092efb1f53
commit 8be07e1f9b
57 changed files with 5211 additions and 25 deletions
@@ -0,0 +1,19 @@
import { HttpModule } from '@nestjs/axios';
import { Module } from '@nestjs/common';
import { PrismaModule } from 'src/core/database/prisma.module';
import { DispoLiveExtractorsService } from './dispo-live-extractors.service';
import { DyflexisExtractorsService } from './dyflexis-extractors.service';
import { ExtractorsService } from './extractors.service';
import { SourceRepository } from './source.repository';
@Module({
imports: [PrismaModule, HttpModule],
providers: [
ExtractorsService,
DispoLiveExtractorsService,
DyflexisExtractorsService,
SourceRepository,
],
exports: [ExtractorsService],
})
export class DataAccessExtractorsModule {}
@@ -0,0 +1,481 @@
import { HttpService } from '@nestjs/axios';
import { Injectable, Logger } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import {
Interval,
eachDayOfInterval,
endOfDay,
format,
startOfDay,
} from 'date-fns';
import {
Observable,
combineLatest,
concatMap,
delay,
forkJoin,
from,
map,
of,
reduce,
switchMap,
} from 'rxjs';
import { EmployeeDispoLiveRaw, PatientRaw, TourRaw } from './source.interface';
import { SourceRepository } from './source.repository';
import { chunk } from 'src/utils';
import { ConfigService } from 'src/core/config/config.service';
@Injectable()
export class DispoLiveExtractorsService {
private readonly logger = new Logger(DispoLiveExtractorsService.name);
constructor(
private readonly http: HttpService,
private readonly repository: SourceRepository,
private readonly config: ConfigService,
) {}
extractTours(
interval: Interval,
jobId: string,
): Observable<Prisma.BatchPayload> {
const intervals = chunk(eachDayOfInterval(interval), 30).map((dates) => ({
start: startOfDay(dates.at(0)),
end: endOfDay(dates.at(-1)),
}));
return this.fetchTours(intervals).pipe(
map((rawTours) => {
return rawTours;
}),
switchMap((rawTours) =>
from(this.repository.createTours(rawTours, jobId)),
),
);
}
private fetchTours(intervals: Interval[]): Observable<TourRaw[]> {
const archiveEndpoint = 'custom/getArchive/getArchive';
const carsEndpoint = 'data/stammdatenFahrzeuge';
const fetchCars = (
authCookies: string,
): Observable<{ id: string; name: string }[]> =>
this.http
.post<{ data: { _id: string; name: string }[] }>(
`${this.config.dispoliveBaseUrl()}/${carsEndpoint}`,
{},
{
headers: {
'Content-Type': 'application/json',
Cookie: authCookies,
},
},
)
.pipe(
map((response) =>
response.data.data.map(({ _id, name }) => ({ id: _id, name })),
),
);
const fetchToursInInterval = (interval: Interval, authCookies: string) =>
this.http
.post<TourRaw[]>(
`${this.config.dispoliveBaseUrl()}/${archiveEndpoint}`,
{
dateFrom: format(new Date(interval.start), 'yyyy-MM-dd'),
dateTo: format(new Date(interval.end), 'yyyy-MM-dd'),
},
{
headers: {
'Content-Type': 'application/json',
Cookie: authCookies,
},
},
)
.pipe(
map((response) =>
response.data.map((tour) => {
const {
_id,
check,
createTime,
empfangenVonId,
endgen,
serienId,
serienKategorName,
serienKategorie,
zadStatus,
abrDate,
abrNum,
abrTime,
ausrueckZeit,
startTime,
startDate,
date,
date_iso,
dauergenehmigung,
direction,
distanceValue,
besetztKm,
gefahren,
gesamtPreis,
infektion,
itDesc,
itKrz,
itName,
kkId,
krankenkasse,
patId,
patName,
patSurname,
patStreet,
patZip,
patCity,
startInstitution,
startStreet,
startZip,
startCity,
targetInstitution,
targetStreet,
targetCity,
targetZip,
transportKrz,
transportName,
transportart,
verordnungsId,
verordnungsName,
type,
startBegin,
startEnd,
frei,
ziel,
zielBegin,
endTime,
endDate,
einsatzNummer,
carName,
carNo,
beifahrer,
beifahrerName,
fahrer,
fahrerName,
color,
} = tour;
return {
id: _id ? `${_id}` : undefined,
check: check ? `${check}` : undefined,
createTime: createTime ? `${createTime}` : undefined,
empfangenVonId: empfangenVonId
? `${empfangenVonId}`
: undefined,
endgen: endgen ? `${endgen}` : undefined,
serienId: serienId ? `${serienId}` : undefined,
serienKategorName: serienKategorName
? `${serienKategorName}`
: undefined,
serienKategorie: serienKategorie
? `${serienKategorie}`
: undefined,
zadStatus: zadStatus ? `${zadStatus}` : undefined,
abrDate: abrDate ? `${abrDate}` : undefined,
abrNum: abrNum ? `${abrNum}` : undefined,
abrTime: abrTime ? `${abrTime}` : undefined,
ausrueckZeit: ausrueckZeit ? `${ausrueckZeit}` : undefined,
startTime: startTime ? `${startTime}` : undefined,
startDate: startDate ? `${startDate}` : undefined,
date: date ? `${date}` : undefined,
date_iso: date_iso ? `${date_iso}` : undefined,
dauergenehmigung: dauergenehmigung
? `${dauergenehmigung}`
: undefined,
direction: direction ? `${direction}` : undefined,
distanceValue: distanceValue ? `${distanceValue}` : undefined,
besetztKm: besetztKm ? `${besetztKm}` : undefined,
gefahren: gefahren ? `${gefahren}` : undefined,
gesamtPreis: gesamtPreis ? `${gesamtPreis}` : undefined,
infektion: infektion ? `${infektion}` : undefined,
itDesc: itDesc ? `${itDesc}` : undefined,
itKrz: itKrz ? `${itKrz}` : undefined,
itName: itName ? `${itName}` : undefined,
kkId: kkId ? `${kkId}` : undefined,
krankenkasse: krankenkasse ? `${krankenkasse}` : undefined,
patId: patId ? `${patId}` : undefined,
patName: patName ? `${patName}` : undefined,
patSurname: patSurname ? `${patSurname}` : undefined,
patStreet: patStreet ? `${patStreet}` : undefined,
patZip: patZip ? `${patZip}` : undefined,
patCity: patCity ? `${patCity}` : undefined,
startInstitution: startInstitution
? `${startInstitution}`
: undefined,
startStreet: startStreet ? `${startStreet}` : undefined,
startZip: startZip ? `${startZip}` : undefined,
startCity: startCity ? `${startCity}` : undefined,
targetInstitution: targetInstitution
? `${targetInstitution}`
: undefined,
targetStreet: targetStreet ? `${targetStreet}` : undefined,
targetCity: targetCity ? `${targetCity}` : undefined,
targetZip: targetZip ? `${targetZip}` : undefined,
transportKrz: transportKrz ? `${transportKrz}` : undefined,
transportName: transportName ? `${transportName}` : undefined,
transportart: transportart ? `${transportart}` : undefined,
verordnungsId: verordnungsId ? `${verordnungsId}` : undefined,
verordnungsName: verordnungsName
? `${verordnungsName}`
: undefined,
type: type ? `${type}` : undefined,
startBegin: startBegin ? `${startBegin}` : undefined,
startEnd: startEnd ? `${startEnd}` : undefined,
frei: frei ? `${frei}` : undefined,
ziel: ziel ? `${ziel}` : undefined,
zielBegin: zielBegin ? `${zielBegin}` : undefined,
endTime: endTime ? `${endTime}` : undefined,
endDate: endDate ? `${endDate}` : undefined,
einsatzNummer: einsatzNummer ? `${einsatzNummer}` : undefined,
carName: carName ? `${carName}` : undefined,
carNo: carNo ? `${carNo}` : undefined,
beifahrer: beifahrer ? `${beifahrer}` : undefined,
beifahrerName: beifahrerName ? `${beifahrerName}` : undefined,
fahrer: fahrer ? `${fahrer}` : undefined,
fahrerName: fahrerName ? `${fahrerName}` : undefined,
color: color ? `${color}` : undefined,
};
}),
),
);
return this.dispoliveLogin(
this.config.dispoliveUsername(),
this.config.dispolivePassword(),
).pipe(
switchMap((authCookies) =>
forkJoin([of(authCookies), fetchCars(authCookies)]),
),
switchMap(([authCookies, cars]) =>
from(intervals).pipe(
delay(300),
concatMap((interval) => fetchToursInInterval(interval, authCookies)),
reduce((full, tours) => [...full, ...tours], []),
map((tours) =>
tours.map(({ carNo, ...tour }) => {
return {
...tour,
carNo,
carName:
cars.find(({ id }) => id === carNo)?.name ?? tour.carName,
};
}),
),
),
),
);
}
extractEmployees(jobId: string): Observable<Prisma.BatchPayload> {
return this.dispoliveLogin(
this.config.dispoliveUsername(),
this.config.dispolivePassword(),
).pipe(
switchMap((authCookies) => this.fetchEmployees(authCookies)),
switchMap((rawEmployees) =>
from(this.repository.createEmployeesDispoLive(rawEmployees, jobId)),
),
);
}
private fetchEmployees(
authCookies: string,
): Observable<EmployeeDispoLiveRaw[]> {
const employeeEndpoint = 'data/mitarbeiter';
return this.http
.post<{ data: EmployeeDispoLiveRaw[] }>(
`${this.config.dispoliveBaseUrl()}/${employeeEndpoint}`,
{ sort: { surname: 1, status_name: -1 } },
{
headers: {
'Content-Type': 'application/json',
Cookie: authCookies,
},
},
)
.pipe(
map((response) =>
response.data.data.map((employee) => {
const {
_id,
pScheinAblaufDatum,
name,
surname,
persId,
importPersId,
qualifikation,
} = employee;
return {
id: _id,
pScheinAblaufDatum,
name,
surname,
persId,
importPersId,
qualifikation,
};
}),
),
);
}
extractPatients(jobId: string): Observable<Prisma.BatchPayload> {
return this.dispoliveLogin(
this.config.dispoliveUsername(),
this.config.dispolivePassword(),
).pipe(
switchMap((authCookies) => this.fetchPatients(authCookies)),
switchMap((rawPatients) =>
from(this.repository.createPatients(rawPatients, jobId)),
),
);
}
private fetchPatients(authCookies: string): Observable<PatientRaw[]> {
const patientEndpoint = 'data/patientenStammdaten';
const careDegreeEndpoint = 'data/pflegestufe';
const disabilityMarkEndpoint = 'data/schwerbehindertenausweis';
const fetchCareDegrees = (
authCookies: string,
): Observable<{ id: string; careDegree: string }[]> =>
this.http
.post<{ data: { _id: string; pflegestufe: string }[] }>(
`${this.config.dispoliveBaseUrl()}/${careDegreeEndpoint}`,
{},
{
headers: {
'Content-Type': 'application/json',
Cookie: authCookies,
},
},
)
.pipe(
map((response) =>
response.data.data.map(({ _id, pflegestufe }) => ({
id: _id,
careDegree: pflegestufe,
})),
),
);
const fetchDisabilityMarks = (
authCookies: string,
): Observable<{ id: string; disabilityMark: string }[]> =>
this.http
.post<{ data: { _id: string; behinderungShortcut: string }[] }>(
`${this.config.dispoliveBaseUrl()}/${disabilityMarkEndpoint}`,
{},
{
headers: {
'Content-Type': 'application/json',
Cookie: authCookies,
},
},
)
.pipe(
map((response) =>
response.data.data.map(({ _id, behinderungShortcut }) => ({
id: _id,
disabilityMark: behinderungShortcut,
})),
),
);
return combineLatest([
fetchCareDegrees(authCookies),
fetchDisabilityMarks(authCookies),
]).pipe(
switchMap(([careDegrees, disabilityMarks]) =>
this.http
.post<{ data: PatientRaw[] }>(
`${this.config.dispoliveBaseUrl()}/${patientEndpoint}`,
{ sort: { surname: 1, status_name: -1 } },
{
headers: {
'Content-Type': 'application/json',
Cookie: authCookies,
},
},
)
.pipe(
map((response) =>
response.data.data.map((patient) => {
const {
_id,
name,
surname,
street,
zip,
city,
birthday,
kkId,
krankenkasse,
pflegestufe,
behinderung1,
behinderung2,
} = patient;
return {
id: _id,
name,
surname,
street,
zip,
city,
birthday,
kkId,
krankenkasse,
pflegestufe: careDegrees.find(({ id }) => id === pflegestufe)
?.careDegree,
behinderung1: disabilityMarks.find(
({ id }) => id === behinderung1,
)?.disabilityMark,
behinderung2: disabilityMarks.find(
({ id }) => id === behinderung2,
)?.disabilityMark,
};
}),
),
),
),
);
}
private dispoliveLogin(
username: string,
password: string,
): Observable<string> {
const endpoint = 'login';
return this.http
.post(
`${this.config.dispoliveBaseUrl()}/${endpoint}`,
{
username,
password,
},
{
headers: {
'Content-Type': 'application/json',
},
},
)
.pipe(
map((response) =>
response.headers['set-cookie']
// To retrieve only the necessary information of the 'set-cookie' header, we combine them with the same separator, which is used inside one 'set-cookie' request (e.g "key=value; path=/; httponly")
.join(';')
.split(';')
.filter((cookie) => cookie.indexOf('koa.sid') > -1)
.join('; '),
),
);
}
}
@@ -0,0 +1,343 @@
import { HttpService } from '@nestjs/axios';
import { Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import {
Interval,
eachDayOfInterval,
eachMonthOfInterval,
format,
getMonth,
getYear,
} from 'date-fns';
import {
EMPTY,
Observable,
catchError,
concatMap,
expand,
from,
map,
of,
reduce,
switchMap,
} from 'rxjs';
import {
AttendanceRegistrationRaw,
EmployeeDyflexisRaw,
PlannedTimeRaw,
WorkTimeRaw,
} from './source.interface';
import { SourceRepository } from './source.repository';
import { ConfigService } from 'src/core/config/config.service';
@Injectable()
export class DyflexisExtractorsService {
constructor(
private readonly http: HttpService,
private readonly repository: SourceRepository,
private readonly config: ConfigService,
) {}
extractEmployees(jobId: string): Observable<Prisma.BatchPayload> {
return this.fetchEmployees().pipe(
switchMap((rawEmployees) =>
from(this.repository.createEmployeesDyflexis(rawEmployees, jobId)),
),
);
}
fetchEmployees(): Observable<EmployeeDyflexisRaw[]> {
const endpoint = '/employee';
const getEmployeesPage = (pageNumber: number) =>
this.http
.get<{ response: { data: EmployeeDyflexisRaw[] } }>(
`${this.config.dyflexisV0BaseUrl()}${endpoint}?page=${pageNumber}`,
{
headers: {
'Content-Type': 'application/json',
'X-Dyflexis-AuthToken': `${this.config.dyflexisV0ApiKey()}`,
},
},
)
.pipe(
map((response) =>
response.data.response.data.map((employee) => {
const {
id,
firstname,
surname,
contract_id,
contract_start,
contract_end,
contract_hours_week,
contract_salary_hour,
} = employee;
return {
id,
firstname,
surname,
contract_id,
contract_start,
contract_end,
contract_hours_week,
contract_salary_hour,
};
}),
),
);
return new Observable<EmployeeDyflexisRaw[]>((observer) =>
getEmployeesPage(0)
.pipe(
expand((data, i) => (data.length ? getEmployeesPage(i + 1) : EMPTY)),
reduce((acc, data) => [...acc, ...data], []),
catchError((error) => {
observer.error(error);
return of(error);
}),
)
.subscribe((employees) => {
observer.next(employees);
observer.complete();
}),
);
}
extractAttendanceRegistrations(
interval: Interval,
jobId: string,
): Observable<Prisma.BatchPayload> {
const daysInInterval = eachDayOfInterval(interval);
return from(daysInInterval).pipe(
concatMap((day) => this.fetchAttendanceRegistrationsForDay(day)),
reduce(
(full, attendanceRegistrations) => [
...full,
...attendanceRegistrations,
],
[],
),
switchMap((rawAttendanceRegistrations) =>
from(
this.repository.createAttendanceRegistrations(
rawAttendanceRegistrations,
jobId,
),
),
),
);
}
fetchAttendanceRegistrationsForDay(
date: Date | string,
): Observable<AttendanceRegistrationRaw[]> {
const endpoint = '/attendance-registration';
return this.http
.get<{ attendanceRegistration: AttendanceRegistrationRaw[] }>(
`${this.config.dyflexisV2BaseUrl()}${endpoint}/${format(
new Date(date),
'yyyy-MM-dd',
)}`,
{
headers: {
'Content-Type': 'application/json',
Authorization: `Token ${this.config.dyflexisV2ApiKey()}`,
},
},
)
.pipe(
map((response) =>
response.data.attendanceRegistration.map((attendanceRegistration) => {
const {
attendanceRegistrationId,
employeeId,
personnelNumber,
dateTime,
event,
} = attendanceRegistration;
return {
attendanceRegistrationId: attendanceRegistrationId
? `${attendanceRegistrationId}`
: undefined,
employeeId: employeeId ? `${employeeId}` : undefined,
personnelNumber: personnelNumber
? `${personnelNumber}`
: undefined,
dateTime: dateTime ? `${dateTime}` : undefined,
event: event ? `${event}` : undefined,
};
}),
),
);
}
extractWorkTimes(
interval: Interval,
jobId: string,
): Observable<Prisma.BatchPayload> {
const monthsInInterval = eachMonthOfInterval(interval);
return from(monthsInInterval).pipe(
concatMap((month) =>
this.fetchWorkTimeForMonth(getMonth(month) + 1, getYear(month)),
),
reduce((full, workTimes) => [...full, ...workTimes], []),
switchMap((rawWorkTimes) =>
from(this.repository.createWorkTimes(rawWorkTimes, jobId)),
),
);
}
private fetchWorkTimeForMonth(
month: number,
year: number,
): Observable<WorkTimeRaw[]> {
const endpoint = '/worked';
const getWorkTimePage = (pageNumber: number) =>
this.http
.get<{ response: { data: WorkTimeRaw[] } }>(
`${this.config.dyflexisV0BaseUrl()}${endpoint}?month=${month}&year=${year}&page=${pageNumber}`,
{
headers: {
'Content-Type': 'application/json',
'X-Dyflexis-AuthToken': `${this.config.dyflexisV0ApiKey()}`,
},
},
)
.pipe(
map((response) =>
response.data.response.data.map((workTime) => {
const {
id,
user_id,
firstname,
surname,
department_id,
department_name,
start_date,
end_date,
pauze,
duration,
} = workTime;
return {
id,
user_id,
firstname,
surname,
department_id,
department_name,
start_date,
end_date,
pauze,
duration,
};
}),
),
);
return new Observable<WorkTimeRaw[]>((observer) =>
getWorkTimePage(0)
.pipe(
expand((data, i) => (data.length ? getWorkTimePage(i + 1) : EMPTY)),
reduce((acc, data) => [...acc, ...data], []),
catchError((error) => {
observer.error(error);
return of(error);
}),
)
.subscribe((workTimes) => {
observer.next(workTimes);
observer.complete();
}),
);
}
extractPlannedTimes(
interval: Interval,
jobId: string,
): Observable<Prisma.BatchPayload> {
const monthsInInterval = eachMonthOfInterval(interval);
return from(monthsInInterval).pipe(
concatMap((month) =>
this.fetchPlannedTimeForMonth(getMonth(month) + 1, getYear(month)),
),
reduce((full, plannedTimes) => [...full, ...plannedTimes], []),
switchMap((rawPlannedTimes) =>
from(this.repository.createPlannedTimes(rawPlannedTimes, jobId)),
),
);
}
private fetchPlannedTimeForMonth(
month: number,
year: number,
): Observable<PlannedTimeRaw[]> {
const endpoint = '/planned';
const getPlannedTimePage = (pageNumber: number) =>
this.http
.get<{ response: { data: PlannedTimeRaw[] } }>(
`${this.config.dyflexisV0BaseUrl()}${endpoint}?month=${month}&year=${year}&page=${pageNumber}`,
{
headers: {
'Content-Type': 'application/json',
'X-Dyflexis-AuthToken': `${this.config.dyflexisV0ApiKey()}`,
},
},
)
.pipe(
map((response) =>
response.data.response.data.map((plannedTime) => {
const {
id,
user_id,
firstname,
surname,
department_id,
department_name,
start_date,
end_date,
pauze,
duration,
deleted,
mark,
} = plannedTime;
return {
id,
user_id,
firstname,
surname,
department_id,
department_name,
start_date,
end_date,
pauze,
duration,
deleted,
mark,
};
}),
),
);
return new Observable<PlannedTimeRaw[]>((observer) =>
getPlannedTimePage(0)
.pipe(
expand((data, i) =>
data.length ? getPlannedTimePage(i + 1) : EMPTY,
),
reduce((acc, data) => [...acc, ...data], []),
catchError((error) => {
observer.error(error);
return of(error);
}),
)
.subscribe((plannedTimes) => {
observer.next(plannedTimes);
observer.complete();
}),
);
}
}
@@ -0,0 +1,141 @@
import { Injectable, Logger } from '@nestjs/common';
import {
AttendanceRegistrationSource,
EmployeeDispoLiveSource,
EmployeeDyflexisSource,
PatientSource,
PlannedTimeSource,
Prisma,
TourSource,
WorkTimeSource,
} from '@prisma/client';
import { Interval } from 'date-fns';
import { Observable, catchError, combineLatest, map } from 'rxjs';
import { DispoLiveExtractorsService } from './dispo-live-extractors.service';
import { DyflexisExtractorsService } from './dyflexis-extractors.service';
import { SourceRepository } from './source.repository';
@Injectable()
export class ExtractorsService {
private logger = new Logger(ExtractorsService.name);
constructor(
private readonly dispoLiveExtractors: DispoLiveExtractorsService,
private readonly dyflexisExtractors: DyflexisExtractorsService,
private readonly repository: SourceRepository,
) {}
clearSource(): Promise<Prisma.BatchPayload[]> {
return this.repository.clearSource();
}
extractTours(
interval: Interval,
jobId: string,
): Observable<Prisma.BatchPayload> {
return this.dispoLiveExtractors.extractTours(interval, jobId).pipe(
catchError((error) => {
this.logger.error(error);
throw error;
}),
);
}
extractEmployees(jobId: string): Observable<Prisma.BatchPayload> {
return combineLatest([
this.dispoLiveExtractors.extractEmployees(jobId),
this.dyflexisExtractors.extractEmployees(jobId),
]).pipe(
map(([{ count: countDispoLive }, { count: countDyflexis }]) => {
return {
count: countDispoLive + countDyflexis,
};
}),
catchError((error) => {
this.logger.error(error);
throw error;
}),
);
}
extractPatients(jobId: string): Observable<Prisma.BatchPayload> {
return this.dispoLiveExtractors.extractPatients(jobId).pipe(
catchError((error) => {
this.logger.error(error);
throw error;
}),
);
}
extractPlannedTimes(
interval: Interval,
jobId: string,
): Observable<Prisma.BatchPayload> {
return this.dyflexisExtractors.extractPlannedTimes(interval, jobId).pipe(
catchError((error) => {
this.logger.error(error);
throw error;
}),
);
}
extractWorkTimes(
interval: Interval,
jobId: string,
): Observable<Prisma.BatchPayload> {
return this.dyflexisExtractors.extractWorkTimes(interval, jobId).pipe(
catchError((error) => {
this.logger.error(error);
throw error;
}),
);
}
extractAttendanceRegistrations(
interval: Interval,
jobId: string,
): Observable<Prisma.BatchPayload> {
return this.dyflexisExtractors
.extractAttendanceRegistrations(interval, jobId)
.pipe(
catchError((error) => {
this.logger.error(error);
throw error;
}),
);
}
findToursByJobId(jobId: string): Promise<TourSource[]> {
return this.repository.findToursByJobId(jobId);
}
findPatientsByJobId(jobId: string): Promise<PatientSource[]> {
return this.repository.findPatientsByJobId(jobId);
}
findPlannedTimesByJobId(jobId: string): Promise<PlannedTimeSource[]> {
return this.repository.findPlannedTimesByJobId(jobId);
}
findWorkTimesByJobId(jobId: string): Promise<WorkTimeSource[]> {
return this.repository.findWorkTimesByJobId(jobId);
}
findAttendanceRegistrationsByJobId(
jobId: string,
): Promise<AttendanceRegistrationSource[]> {
return this.repository.findAttendanceRegistrationsByJobId(jobId);
}
findEmployeesDispoLiveByJobId(
jobId: string,
): Promise<EmployeeDispoLiveSource[]> {
return this.repository.findEmployeesDispoLiveByJobId(jobId);
}
findEmployeesDyflexisByJobId(
jobId: string,
): Promise<EmployeeDyflexisSource[]> {
return this.repository.findEmployeesDyflexisByJobId(jobId);
}
}
@@ -0,0 +1,142 @@
export interface TourRaw {
_id?: string;
check?: string;
createTime?: string; // yyyy-MM-dd HH:mm:ss 'Europe/Berlin' OR yyyy-MM-dd 'Europe/Berlin'
empfangenVonId?: string;
endgen?: string; // yyyy-MM-dd
serienId?: string;
serienKategorName?: string;
serienKategorie?: string;
zadStatus?: string;
abrDate?: string; // yyyy-MM-dd
abrNum?: string;
abrTime?: string; // HH:mm
ausrueckZeit?: string; // HH:mm 'Europe/Berlin'
startTime?: string; // HH:mm 'Europe/Berlin' (is after ausrueckZeit)
startDate?: string; // yyyy-MM-dd HH:mm:ss 'Europe/Berlin'
date?: string; // yyyy-MM-dd
date_iso?: string; // ISO 8601 String
dauergenehmigung?: string;
direction?: string;
distanceValue?: string; // meters (probably int)
besetztKm?: string; // kilometers (float)
gefahren?: string; // Boolean as number
gesamtPreis?: string; // float
infektion?: string; // Boolean as number
itDesc?: string;
itKrz?: string;
itName?: string;
kkId?: string;
krankenkasse?: string;
patId?: string;
patName?: string;
patSurname?: string;
patStreet?: string;
patZip?: string;
patCity?: string;
startInstitution?: string;
startStreet?: string;
startZip?: string;
startCity?: string;
targetInstitution?: string;
targetStreet?: string;
targetZip?: string;
targetCity?: string;
transportKrz?: string;
transportName?: string;
transportart?: string; // ID
verordnungsId?: string;
verordnungsName?: string;
type?: string;
startBegin?: string; // HH:mm 'Europe/Berlin'
startEnd?: string; // HH:mm 'Europe/Berlin'
frei?: string; // HH:mm 'Europe/Berlin'
ziel?: string; // HH:mm 'Europe/Berlin'
zielBegin?: string; // HH:mm 'Europe/Berlin'
endTime?: string; // HH:mm 'Europe/Berlin'
endDate?: string; // yyyy-MM-dd HH:mm:ss 'Europe/Berlin'
einsatzNummer?: string;
carName?: string;
carNo?: string; // ID
beifahrerName?: string;
beifahrer?: string; // ID
fahrerName?: string;
fahrer?: string; // ID
color?: string; // Hex rgb
}
export interface EmployeeDispoLiveRaw {
_id?: string;
pScheinAblaufDatum?: string; // yyyy-MM-dd
name?: string;
surname?: string;
persId?: string;
importPersId?: string; // Personal ID aus Dyflexis
qualifikation?: string; // ID
}
export interface PatientRaw {
_id?: string;
birthday?: string; // yyyy-MM-dd
name?: string;
surname?: string;
street?: string;
zip?: string;
city?: string;
kkId?: string;
krankenkasse?: string;
pflegestufe?: string;
behinderung1?: string;
behinderung2?: string;
}
export interface AttendanceRegistrationRaw {
attendanceRegistrationId?: string;
employeeId?: string;
personnelNumber?: string;
dateTime?: string; // ISO 8601 String with timezone
event?: string;
}
export interface PlannedTimeRaw {
id?: string;
user_id?: string;
firstname?: string;
surname?: string;
department_id?: string;
department_name?: string;
start_date?: string; // yyyy-MM-dd HH:mm:ss 'Europe/Berlin'
end_date?: string; // yyyy-MM-dd HH:mm:ss 'Europe/Berlin'
pauze?: string;
duration?: string;
deleted?: string;
mark?: string;
}
export interface WorkTimeRaw {
id?: string;
user_id?: string;
firstname?: string;
surname?: string;
department_id?: string;
department_name?: string;
start_date?: string; // yyyy-MM-dd HH:mm:ss 'Europe/Berlin'
end_date?: string; // yyyy-MM-dd HH:mm:ss 'Europe/Berlin'
pauze?: string;
duration?: string;
}
export interface EmployeeDyflexisRaw {
id?: string;
firstname?: string;
surname?: string;
contract_id?: string;
contract_start?: string; // yyyy-MM-dd
contract_end?: string; // yyyy-MM-dd
contract_hours_week?: string; // Float string
contract_salary_hour?: string; // Float string
}
@@ -0,0 +1,163 @@
import { Injectable } from '@nestjs/common';
import {
AttendanceRegistrationSource,
EmployeeDispoLiveSource,
EmployeeDyflexisSource,
PatientSource,
PlannedTimeSource,
Prisma,
TourSource,
WorkTimeSource,
} from '@prisma/client';
import { PrismaService } from 'src/core/database/prisma.service';
import {
AttendanceRegistrationRaw,
EmployeeDispoLiveRaw,
EmployeeDyflexisRaw,
PatientRaw,
PlannedTimeRaw,
TourRaw,
WorkTimeRaw,
} from './source.interface';
@Injectable()
export class SourceRepository {
constructor(private readonly prisma: PrismaService) {}
clearSource(): Promise<Prisma.BatchPayload[]> {
return Promise.all([
this.prisma.tourSource.deleteMany(),
this.prisma.employeeDispoLiveSource.deleteMany(),
this.prisma.employeeDyflexisSource.deleteMany(),
this.prisma.workTimeSource.deleteMany(),
this.prisma.plannedTimeSource.deleteMany(),
this.prisma.attendanceRegistrationSource.deleteMany(),
this.prisma.patientSource.deleteMany(),
]);
}
createTours(
tours: TourRaw[],
jobId: string,
): Prisma.PrismaPromise<Prisma.BatchPayload> {
return this.prisma.tourSource.createMany({
data: tours.map((data) => ({ jobId, ...data })),
});
}
createEmployeesDispoLive(
employees: EmployeeDispoLiveRaw[],
jobId: string,
): Prisma.PrismaPromise<Prisma.BatchPayload> {
return this.prisma.employeeDispoLiveSource.createMany({
data: employees.map((data) => ({ jobId, ...data })),
});
}
createPatients(
patients: PatientRaw[],
jobId: string,
): Promise<Prisma.BatchPayload> {
return this.prisma.patientSource.createMany({
data: patients.map((data) => ({ jobId, ...data })),
});
}
createEmployeesDyflexis(
employees: EmployeeDyflexisRaw[],
jobId: string,
): Prisma.PrismaPromise<Prisma.BatchPayload> {
return this.prisma.employeeDyflexisSource.createMany({
data: employees.map((data) => ({ jobId, ...data })),
});
}
createAttendanceRegistrations(
attendanceRegistrations: AttendanceRegistrationRaw[],
jobId: string,
): Prisma.PrismaPromise<Prisma.BatchPayload> {
return this.prisma.attendanceRegistrationSource.createMany({
data: attendanceRegistrations.map((data) => ({ jobId, ...data })),
});
}
createWorkTimes(
workTimes: WorkTimeRaw[],
jobId: string,
): Prisma.PrismaPromise<Prisma.BatchPayload> {
return this.prisma.workTimeSource.createMany({
data: workTimes.map((data) => ({ jobId, ...data })),
});
}
createPlannedTimes(
plannedTimes: PlannedTimeRaw[],
jobId: string,
): Prisma.PrismaPromise<Prisma.BatchPayload> {
return this.prisma.plannedTimeSource.createMany({
data: plannedTimes.map((data) => ({ jobId, ...data })),
});
}
findToursByJobId(jobId: string): Promise<TourSource[]> {
return this.prisma.tourSource.findMany({
where: {
jobId,
},
});
}
findPatientsByJobId(jobId: string): Promise<PatientSource[]> {
return this.prisma.patientSource.findMany({
where: {
jobId,
},
});
}
findPlannedTimesByJobId(jobId: string): Promise<PlannedTimeSource[]> {
return this.prisma.plannedTimeSource.findMany({
where: {
jobId,
},
});
}
findWorkTimesByJobId(jobId: string): Promise<WorkTimeSource[]> {
return this.prisma.workTimeSource.findMany({
where: {
jobId,
},
});
}
findAttendanceRegistrationsByJobId(
jobId: string,
): Promise<AttendanceRegistrationSource[]> {
return this.prisma.attendanceRegistrationSource.findMany({
where: {
jobId,
},
});
}
findEmployeesDyflexisByJobId(
jobId: string,
): Promise<EmployeeDyflexisSource[]> {
return this.prisma.employeeDyflexisSource.findMany({
where: {
jobId,
},
});
}
findEmployeesDispoLiveByJobId(
jobId: string,
): Promise<EmployeeDispoLiveSource[]> {
return this.prisma.employeeDispoLiveSource.findMany({
where: {
jobId,
},
});
}
}
@@ -0,0 +1,5 @@
export enum EventTypes {
ExtractionDone = 'etl.extraction',
TransformationDone = 'etl.transformation',
LoadingDone = 'etl.loading',
}
@@ -0,0 +1,3 @@
export class ExtractionDoneEvent {
constructor(public readonly jobId: string) {}
}
@@ -0,0 +1,3 @@
export class LoadingDoneEvent {
constructor(public readonly jobId: string) {}
}
@@ -0,0 +1,3 @@
export class TransformationDoneEvent {
constructor(public readonly jobId: string) {}
}
@@ -0,0 +1,17 @@
import { Module } from '@nestjs/common';
import { DataAccessExtractorsModule } from '../feat-extractors/data-access-extractors.module';
import { DataAccessLoadersModule } from '../feat-loaders/data-access-loaders.module';
import { DataAccessTransformersModule } from '../feat-transformers/data-access-transformers.module';
import { ImporterService } from './importer.service';
@Module({
imports: [
DataAccessExtractorsModule,
DataAccessTransformersModule,
DataAccessLoadersModule,
],
providers: [ImporterService],
exports: [ImporterService],
})
export class FeatureImporterModule {}
@@ -0,0 +1,113 @@
import { Injectable, Logger } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { randomUUID } from 'crypto';
import { Interval } from 'date-fns';
import { Observable, combineLatest, from, map, switchMap, tap } from 'rxjs';
import { ExtractorsService } from '../feat-extractors/extractors.service';
import { LoadersService } from '../feat-loaders/loaders.service';
import { TransformersService } from '../feat-transformers/transformers.service';
import { EventTypes } from './events/event-types.enum';
import { ExtractionDoneEvent } from './events/extraction-done.event';
import { LoadingDoneEvent } from './events/loading-done.event';
import { TransformationDoneEvent } from './events/transformation-done.event';
@Injectable()
export class ImporterService {
private logger = new Logger(ImporterService.name);
constructor(
private readonly extractorsService: ExtractorsService,
private readonly transformersService: TransformersService,
private readonly loadersService: LoadersService,
private readonly eventEmitter: EventEmitter2,
) {}
runImport(interval: Interval) {
const startTS = new Date().getTime();
const jobId = randomUUID();
return from(this.extractorsService.clearSource()).pipe(
switchMap(() => this.runExtractors(interval, jobId)),
tap(() =>
this.logger.log(
`Extraction of ${jobId} took ${new Date().getTime() - startTS}ms`,
),
),
switchMap(() =>
from(this.transformersService.clearStaging()).pipe(
switchMap(() => this.runTransformers(jobId)),
),
),
tap(() =>
this.logger.log(
`Transformation of ${jobId} took ${new Date().getTime() - startTS}ms`,
),
),
switchMap(() => this.runLoaders(jobId)),
tap(() =>
this.logger.log(
`Loading of ${jobId} took ${new Date().getTime() - startTS}ms`,
),
),
);
}
private runExtractors(interval: Interval, jobId: string): Observable<void> {
return combineLatest([
this.extractorsService.extractAttendanceRegistrations(interval, jobId),
this.extractorsService.extractEmployees(jobId),
this.extractorsService.extractPatients(jobId),
this.extractorsService.extractPlannedTimes(interval, jobId),
this.extractorsService.extractTours(interval, jobId),
this.extractorsService.extractWorkTimes(interval, jobId),
]).pipe(
tap(() =>
this.eventEmitter.emit(
EventTypes.ExtractionDone,
new ExtractionDoneEvent(jobId),
),
),
map(() => {
return;
}),
);
}
private runTransformers(jobId: string): Observable<void> {
return combineLatest([
this.transformersService.transformAttendanceRegistrations(jobId),
this.transformersService.transformEmployees(jobId),
this.transformersService.transformPatients(jobId),
this.transformersService.transformPlannedTimes(jobId),
this.transformersService.transformWorkTimes(jobId),
this.transformersService.transformTours(jobId),
]).pipe(
tap(() =>
this.eventEmitter.emit(
EventTypes.TransformationDone,
new TransformationDoneEvent(jobId),
),
),
map(() => {
return;
}),
);
}
private runLoaders(jobId: string): Observable<void> {
return this.loadersService.loadTours().pipe(
switchMap(() => this.loadersService.loadPatients()),
switchMap(() => this.loadersService.loadAttendanceRegistrations()),
switchMap(() => this.loadersService.loadPlannedTimes()),
switchMap(() => this.loadersService.loadWorkTimes()),
tap(() =>
this.eventEmitter.emit(
EventTypes.LoadingDone,
new LoadingDoneEvent(jobId),
),
),
map(() => {
return;
}),
);
}
}
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { PrismaModule } from 'src/core/database/prisma.module';
import { DataAccessTransformersModule } from '../feat-transformers/data-access-transformers.module';
import { FinalRepository } from './final.repository';
import { LoadersService } from './loaders.service';
@Module({
imports: [PrismaModule, DataAccessTransformersModule],
providers: [LoadersService, FinalRepository],
exports: [LoadersService],
})
export class DataAccessLoadersModule {}
@@ -0,0 +1,278 @@
import { Injectable } from '@nestjs/common';
import {
AttendanceRegistration,
Patient,
PlannedTime,
Prisma,
Tour,
WorkTime,
} from '@prisma/client';
import { Interval } from 'date-fns';
import { PrismaService } from 'src/core/database/prisma.service';
@Injectable()
export class FinalRepository {
constructor(private readonly prisma: PrismaService) {}
findToursInInterval(
interval: Interval,
operationIds: string[],
): Promise<Tour[]> {
return this.prisma.tour.findMany({
where: {
OR: [
{
AND: [
{
startDate: {
gte: new Date(interval.start),
},
},
{
startDate: {
lte: new Date(interval.end),
},
},
],
},
{
operationId: {
in: operationIds,
},
},
],
},
});
}
createTours(
tourCreateInput: Prisma.TourCreateInput[],
): Promise<Prisma.BatchPayload> {
return this.prisma.tour.createMany({
data: tourCreateInput,
skipDuplicates: true,
});
}
async updateTours(
tourCreateInput: Prisma.TourCreateInput[],
): Promise<Prisma.BatchPayload> {
await this.prisma.tour.deleteMany({
where: {
operationId: {
in: tourCreateInput.map(({ operationId }) => operationId),
},
},
});
return this.createTours(tourCreateInput);
}
deleteTours(operationIds: string[]): Promise<Prisma.BatchPayload> {
return this.prisma.tour.updateMany({
where: {
operationId: {
in: operationIds,
},
},
data: {
deletedAt: new Date(),
},
});
}
findPatients(): Promise<Patient[]> {
return this.prisma.patient.findMany({
orderBy: {
surname: 'asc',
},
});
}
createPatients(
patientCreateInput: Prisma.PatientCreateInput[],
): Promise<Prisma.BatchPayload> {
return this.prisma.patient.createMany({
data: patientCreateInput,
skipDuplicates: true,
});
}
async updatePatients(
patientCreateInput: Prisma.PatientCreateInput[],
): Promise<Prisma.BatchPayload> {
await this.deletePatients(patientCreateInput.map(({ id }) => id));
return this.createPatients(patientCreateInput);
}
deletePatients(ids: string[]): Promise<Prisma.BatchPayload> {
return this.prisma.patient.deleteMany({
where: {
id: {
in: ids,
},
},
});
}
findAttendanceRegistrationsInInterval(
interval: Interval,
): Promise<AttendanceRegistration[]> {
return this.prisma.attendanceRegistration.findMany({
where: {
AND: [
{
dateTime: {
gte: new Date(interval.start),
},
},
{
dateTime: {
lte: new Date(interval.end),
},
},
],
},
});
}
createAttendanceRegistrations(
attendanceRegistrationCreateInput: Prisma.AttendanceRegistrationCreateInput[],
): Promise<Prisma.BatchPayload> {
return this.prisma.attendanceRegistration.createMany({
data: attendanceRegistrationCreateInput,
skipDuplicates: true,
});
}
async updateAttendanceRegistrations(
attendanceRegistrationCreateInput: Prisma.AttendanceRegistrationCreateInput[],
): Promise<Prisma.BatchPayload> {
await this.prisma.attendanceRegistration.deleteMany({
where: {
attendanceRegistrationId: {
in: attendanceRegistrationCreateInput.map(
({ attendanceRegistrationId }) => attendanceRegistrationId,
),
},
},
});
return this.createAttendanceRegistrations(
attendanceRegistrationCreateInput,
);
}
deleteAttendanceRegistrations(
attendanceRegistrationIds: number[],
): Promise<Prisma.BatchPayload> {
return this.prisma.attendanceRegistration.deleteMany({
where: {
attendanceRegistrationId: {
in: attendanceRegistrationIds,
},
},
});
}
findPlannedTimesInInterval(interval: Interval): Promise<PlannedTime[]> {
return this.prisma.plannedTime.findMany({
where: {
AND: [
{
startDate: {
gte: new Date(interval.start),
},
},
{
startDate: {
lte: new Date(interval.end),
},
},
],
},
});
}
createPlannedTimes(
plannedTimeCreateInput: Prisma.PlannedTimeCreateInput[],
): Promise<Prisma.BatchPayload> {
return this.prisma.plannedTime.createMany({
data: plannedTimeCreateInput,
skipDuplicates: true,
});
}
async updatePlannedTimes(
plannedTimeCreateInput: Prisma.PlannedTimeCreateInput[],
): Promise<Prisma.BatchPayload> {
await this.prisma.plannedTime.deleteMany({
where: {
id: {
in: plannedTimeCreateInput.map(({ id }) => id),
},
},
});
return this.createPlannedTimes(plannedTimeCreateInput);
}
deletePlannedTimes(plannedTimeIds: string[]): Promise<Prisma.BatchPayload> {
return this.prisma.plannedTime.deleteMany({
where: {
id: {
in: plannedTimeIds,
},
},
});
}
findWorkTimesInInterval(interval: Interval): Promise<WorkTime[]> {
return this.prisma.workTime.findMany({
where: {
AND: [
{
startDate: {
gte: new Date(interval.start),
},
},
{
startDate: {
lte: new Date(interval.end),
},
},
],
},
});
}
createWorkTimes(
workTimeCreateInput: Prisma.WorkTimeCreateInput[],
): Promise<Prisma.BatchPayload> {
return this.prisma.workTime.createMany({
data: workTimeCreateInput,
skipDuplicates: true,
});
}
async updateWorkTimes(
workTimeCreateInput: Prisma.WorkTimeCreateInput[],
): Promise<Prisma.BatchPayload> {
await this.prisma.workTime.deleteMany({
where: {
id: {
in: workTimeCreateInput.map(({ id }) => id),
},
},
});
return this.createWorkTimes(workTimeCreateInput);
}
deleteWorkTimes(workTimeIds: string[]): Promise<Prisma.BatchPayload> {
return this.prisma.workTime.deleteMany({
where: {
id: {
in: workTimeIds,
},
},
});
}
}
@@ -0,0 +1,204 @@
import { Injectable } from '@nestjs/common';
import { from, map, of, switchMap } from 'rxjs';
import { TransformersService } from '../feat-transformers/transformers.service';
import { FinalRepository } from './final.repository';
import { AttendanceRegistrationLoader } from './loaders/attendance-registration.loader';
import { PatientLoader } from './loaders/patient.loader';
import { PlannedTimeLoader } from './loaders/planned-time.loader';
import { TourLoader } from './loaders/tour.loader';
import { WorkTimeLoader } from './loaders/work-time.loader';
@Injectable()
export class LoadersService {
constructor(
private readonly repository: FinalRepository,
private readonly transformersService: TransformersService,
) {}
loadTours() {
return from(this.transformersService.findTours()).pipe(
switchMap((stagingTours) => {
if (stagingTours.length === 0) {
return of({
staging: [],
final: [],
});
}
const { startDate: firstTourDate } = stagingTours.at(0);
const { startDate: lastTourDate } = stagingTours.at(-1);
return from(
this.repository.findToursInInterval(
{
start: firstTourDate,
end: lastTourDate,
},
stagingTours.map(({ operationId }) => operationId),
),
).pipe(
map((finalTours) => ({
final: finalTours,
staging: stagingTours,
})),
);
}),
map(({ staging, final }) =>
TourLoader.determineLoadOperations(staging, final),
),
switchMap(({ deleted, updated, created }) => {
return from(
Promise.all([
this.repository.deleteTours(deleted),
this.repository.updateTours(updated),
this.repository.createTours(created),
]),
);
}),
);
}
loadPatients() {
return from(this.transformersService.findPatients()).pipe(
switchMap((stagingPatients) =>
from(this.repository.findPatients()).pipe(
map((finalPatients) => ({
final: finalPatients,
staging: stagingPatients,
})),
),
),
map(({ staging, final }) =>
PatientLoader.determineLoadOperations(staging, final),
),
switchMap(({ deleted, updated, created }) =>
from(
Promise.all([
this.repository.deletePatients(deleted),
this.repository.updatePatients(updated),
this.repository.createPatients(created),
]),
),
),
);
}
loadAttendanceRegistrations() {
return from(this.transformersService.findAttendanceRegistrations()).pipe(
switchMap((stagingAttendanceRegistrations) => {
if (stagingAttendanceRegistrations.length === 0) {
return of({
staging: [],
final: [],
});
}
const { dateTime: firstAttendanceRegistration } = [
...stagingAttendanceRegistrations,
].shift();
const { dateTime: lastAttendanceRegistration } = [
...stagingAttendanceRegistrations,
].pop();
return from(
this.repository.findAttendanceRegistrationsInInterval({
start: firstAttendanceRegistration,
end: lastAttendanceRegistration,
}),
).pipe(
map((finalAttendanceRegistrations) => ({
final: finalAttendanceRegistrations,
staging: stagingAttendanceRegistrations,
})),
);
}),
map(({ staging, final }) =>
AttendanceRegistrationLoader.determineLoadOperations(staging, final),
),
switchMap(({ deleted, updated, created }) => {
return from(
Promise.all([
this.repository.deleteAttendanceRegistrations(deleted),
this.repository.updateAttendanceRegistrations(updated),
this.repository.createAttendanceRegistrations(created),
]),
);
}),
);
}
loadPlannedTimes() {
return from(this.transformersService.findPlannedTimes()).pipe(
switchMap((stagingPlannedTimes) => {
if (stagingPlannedTimes.length === 0) {
return of({
staging: [],
final: [],
});
}
const { startDate: firstPlannedTime } = [
...stagingPlannedTimes,
].shift();
const { startDate: lastPlannedTime } = [...stagingPlannedTimes].pop();
return from(
this.repository.findPlannedTimesInInterval({
start: firstPlannedTime,
end: lastPlannedTime,
}),
).pipe(
map((finalPlannedTimes) => ({
final: finalPlannedTimes,
staging: stagingPlannedTimes,
})),
);
}),
map(({ staging, final }) =>
PlannedTimeLoader.determineLoadOperations(staging, final),
),
switchMap(({ deleted, updated, created }) => {
return from(
Promise.all([
this.repository.deletePlannedTimes(deleted),
this.repository.updatePlannedTimes(updated),
this.repository.createPlannedTimes(created),
]),
);
}),
);
}
loadWorkTimes() {
return from(this.transformersService.findWorkTimes()).pipe(
switchMap((stagingWorkTimes) => {
if (stagingWorkTimes.length === 0) {
return of({
staging: [],
final: [],
});
}
const { startDate: firstWorkTime } = [...stagingWorkTimes].shift();
const { startDate: lastWorkTime } = [...stagingWorkTimes].pop();
return from(
this.repository.findWorkTimesInInterval({
start: firstWorkTime,
end: lastWorkTime,
}),
).pipe(
map((finalTours) => ({
final: finalTours,
staging: stagingWorkTimes,
})),
);
}),
map(({ staging, final }) =>
WorkTimeLoader.determineLoadOperations(staging, final),
),
switchMap(({ deleted, updated, created }) => {
return from(
Promise.all([
this.repository.deleteWorkTimes(deleted),
this.repository.updateWorkTimes(updated),
this.repository.createWorkTimes(created),
]),
);
}),
);
}
}
@@ -0,0 +1,82 @@
import {
Prisma,
AttendanceRegistration,
AttendanceRegistrationStaging,
} from '@prisma/client';
export class AttendanceRegistrationLoader {
public static determineLoadOperations(
stagingAttendanceRegistrations: AttendanceRegistrationStaging[],
finalAttendanceRegistrations: AttendanceRegistration[],
): {
deleted: number[];
updated: Prisma.AttendanceRegistrationCreateInput[];
created: Prisma.AttendanceRegistrationCreateInput[];
} {
const deletedAttendanceRegistrations = finalAttendanceRegistrations.filter(
(finalAttendanceRegistration) =>
!stagingAttendanceRegistrations.find(
(stagingAttendanceRegistration) =>
stagingAttendanceRegistration.attendanceRegistrationId ===
finalAttendanceRegistration.attendanceRegistrationId,
),
);
const attendanceRegistrationsToInsert =
stagingAttendanceRegistrations.filter(
({ attendanceRegistrationId }) =>
!!attendanceRegistrationId &&
!deletedAttendanceRegistrations.find(
(deletedAttendanceRegistration) =>
deletedAttendanceRegistration.attendanceRegistrationId ===
attendanceRegistrationId,
),
);
const newAttendanceRegistrations: Prisma.AttendanceRegistrationCreateInput[] =
[];
const updatedAttendanceRegistrations: Prisma.AttendanceRegistrationCreateInput[] =
[];
for (const stagingAttendanceRegistration of attendanceRegistrationsToInsert) {
const finalAttendanceRegistration = finalAttendanceRegistrations.find(
(finalAttendanceRegistration) =>
finalAttendanceRegistration.attendanceRegistrationId ===
stagingAttendanceRegistration.attendanceRegistrationId,
);
if (finalAttendanceRegistration) {
updatedAttendanceRegistrations.push(
this.attendanceRegistrationStagingToFinal(
stagingAttendanceRegistration,
),
);
} else {
newAttendanceRegistrations.push(
this.attendanceRegistrationStagingToFinal(
stagingAttendanceRegistration,
),
);
}
}
return {
deleted: deletedAttendanceRegistrations.map(
({ attendanceRegistrationId }) => attendanceRegistrationId,
),
updated: updatedAttendanceRegistrations,
created: newAttendanceRegistrations,
};
}
private static attendanceRegistrationStagingToFinal(
attendanceregistration: AttendanceRegistrationStaging,
): Prisma.AttendanceRegistrationCreateInput {
const { attendanceRegistrationId, employeeId, dateTime, event } =
attendanceregistration;
return {
attendanceRegistrationId,
employeeId,
dateTime,
event,
personnelNumber: '',
};
}
}
@@ -0,0 +1,77 @@
import { Prisma, Patient, PatientStaging } from '@prisma/client';
export class PatientLoader {
public static determineLoadOperations(
stagingPatients: PatientStaging[],
finalPatients: Patient[],
): {
deleted: string[];
updated: Prisma.PatientCreateInput[];
created: Prisma.PatientCreateInput[];
} {
const deletedPatients = finalPatients.filter(
(finalPatient) =>
!stagingPatients.find(
(stagingPatient) => stagingPatient.id === finalPatient.id,
),
);
const patientsToInsert = stagingPatients.filter(
({ id }) =>
!!id &&
!deletedPatients.find((deletedPatient) => deletedPatient.id === id),
);
const newPatients: Prisma.PatientCreateInput[] = [];
const updatedPatients: Prisma.PatientCreateInput[] = [];
for (const stagingPatient of patientsToInsert) {
const finalPatient = finalPatients.find(
(finalPatient) => finalPatient.id === stagingPatient.id,
);
if (finalPatient) {
updatedPatients.push(this.patientStagingToFinal(stagingPatient));
} else {
newPatients.push(this.patientStagingToFinal(stagingPatient));
}
}
return {
deleted: deletedPatients.map(({ id }) => id),
updated: updatedPatients,
created: newPatients,
};
}
private static patientStagingToFinal(
patient: PatientStaging,
): Prisma.PatientCreateInput {
const {
id,
surname,
name,
street,
zip,
city,
birthday,
healthinsurance,
healthinsuranceId,
careDegree,
disabilityMark1,
disabilityMark2,
} = patient;
return {
id,
surname,
name,
street,
zip,
city,
birthday,
healthinsurance,
healthinsuranceId,
careDegree,
disabilityMark1,
disabilityMark2,
};
}
}
@@ -0,0 +1,75 @@
import { Prisma, PlannedTime, PlannedTimeStaging } from '@prisma/client';
export class PlannedTimeLoader {
public static determineLoadOperations(
stagingPlannedTimes: PlannedTimeStaging[],
finalPlannedTimes: PlannedTime[],
): {
deleted: string[];
updated: Prisma.PlannedTimeCreateInput[];
created: Prisma.PlannedTimeCreateInput[];
} {
const deletedPlannedTimes = stagingPlannedTimes.filter(
({ deleted }) => deleted,
);
console.log(deletedPlannedTimes.length);
const plannedTimesToInsert = stagingPlannedTimes.filter(
({ id }) =>
!!id &&
!deletedPlannedTimes.find(
(deletedPlannedTime) => deletedPlannedTime.id === id,
),
);
const newPlannedTimes: Prisma.PlannedTimeCreateInput[] = [];
const updatedPlannedTimes: Prisma.PlannedTimeCreateInput[] = [];
for (const stagingPlannedTime of plannedTimesToInsert) {
const finalPlannedTime = finalPlannedTimes.find(
(finalPlannedTime) =>
finalPlannedTime.id === `${stagingPlannedTime.id}`,
);
if (finalPlannedTime) {
updatedPlannedTimes.push(
this.plannedTimeStagingToFinal(stagingPlannedTime),
);
} else {
newPlannedTimes.push(
this.plannedTimeStagingToFinal(stagingPlannedTime),
);
}
}
return {
deleted: deletedPlannedTimes.map(({ id }) => `${id}`),
updated: updatedPlannedTimes,
created: newPlannedTimes,
};
}
private static plannedTimeStagingToFinal(
plannedtime: PlannedTimeStaging,
): Prisma.PlannedTimeCreateInput {
const {
id,
employeeId,
firstname,
surname,
departmentName,
startDate,
endDate,
mark,
} = plannedtime;
return {
firstname,
surname,
startDate,
endDate,
id: `${id}`,
userId: `${employeeId}`,
department: departmentName,
pause: 0,
duration: 0,
note: mark,
};
}
}
@@ -0,0 +1,149 @@
import { Prisma, Tour, TourStaging } from '@prisma/client';
export class TourLoader {
public static determineLoadOperations(
stagingTours: TourStaging[],
finalTours: Tour[],
): {
deleted: string[];
updated: Prisma.TourCreateInput[];
created: Prisma.TourCreateInput[];
} {
const deletedTours = finalTours.filter(
(finalTour) =>
!stagingTours.find(
(stagingTour) => stagingTour.operationId === finalTour.operationId,
),
);
const toursToInsert = stagingTours.filter(
({ id, operationId }) =>
!!id &&
!deletedTours.find(
(deletedTour) => deletedTour.operationId === operationId,
),
);
const newTours: Prisma.TourCreateInput[] = [];
const updatedTours: Prisma.TourCreateInput[] = [];
for (const stagingTour of toursToInsert) {
const finalTour = finalTours.find(
(finalTour) => finalTour.operationId === stagingTour.operationId,
);
if (finalTour) {
updatedTours.push(
this.tourStagingToFinal(stagingTour, finalTour.ticketId),
);
} else {
newTours.push(this.tourStagingToFinal(stagingTour));
}
}
return {
deleted: deletedTours.map(({ operationId }) => operationId),
updated: updatedTours,
created: newTours,
};
}
private static tourStagingToFinal(tour: TourStaging, ticketId?: string) {
const {
done,
direction,
carName,
operationId,
driverId,
driverName,
codriverId,
codriverName,
patientId,
patientName,
patientSurname,
patientStreet,
patientZip,
patientCity,
healthInsurance,
healthInsuranceNumber,
type,
category,
transportType,
ordinanceType,
rangeEndDate,
startInstitution,
startStreet,
startZip,
startCity,
targetInstitution,
targetStreet,
targetZip,
targetCity,
startDate,
check,
occupiedKm,
totalKm,
startBegin,
startEnd,
target,
targetBegin,
empty,
hasInfection,
infectionName,
revenueDispoLive,
revenue,
revenueDeviation,
consumptionCosts,
billDate,
billNumber,
createdAt,
} = tour;
return {
done,
direction,
carName,
operationId,
driverId,
driverName,
codriverId,
codriverName,
patientId,
patientName,
patientSurname,
patientStreet,
patientZip,
patientCity,
healthInsurance,
healthInsuranceNumber,
type,
category,
transportType,
ordinanceType,
rangeEndDate,
startInstitution,
startStreet,
startZip,
startCity,
targetInstitution,
targetStreet,
targetZip,
targetCity,
startDate,
check,
occupiedKm,
totalKm,
startBegin,
startEnd,
target,
targetBegin,
empty,
hasInfection,
infectionName,
revenueDispoLive,
revenue,
revenueDeviation,
consumptionCosts,
billDate,
billNumber,
createdAt,
...(ticketId ? { ticketId } : {}),
};
}
}
@@ -0,0 +1,70 @@
import { Prisma, WorkTime, WorkTimeStaging } from '@prisma/client';
export class WorkTimeLoader {
public static determineLoadOperations(
stagingWorkTimes: WorkTimeStaging[],
finalWorkTimes: WorkTime[],
): {
deleted: string[];
updated: Prisma.WorkTimeCreateInput[];
created: Prisma.WorkTimeCreateInput[];
} {
const deletedWorkTimes = finalWorkTimes.filter(
(finalWorkTime) =>
!stagingWorkTimes.find(
(stagingWorkTime) => `${stagingWorkTime.id}` === finalWorkTime.id,
),
);
const workTimesToInsert = stagingWorkTimes.filter(
({ id }) =>
!!id &&
!deletedWorkTimes.find(
(deletedWorkTime) => deletedWorkTime.id === `${id}`,
),
);
const newWorkTimes: Prisma.WorkTimeCreateInput[] = [];
const updatedWorkTimes: Prisma.WorkTimeCreateInput[] = [];
for (const stagingWorkTime of workTimesToInsert) {
const finalWorkTime = finalWorkTimes.find(
(finalWorkTime) => finalWorkTime.id === `${stagingWorkTime.id}`,
);
if (finalWorkTime) {
updatedWorkTimes.push(this.workTimeStagingToFinal(stagingWorkTime));
} else {
newWorkTimes.push(this.workTimeStagingToFinal(stagingWorkTime));
}
}
return {
deleted: deletedWorkTimes.map(({ id }) => id),
updated: updatedWorkTimes,
created: newWorkTimes,
};
}
private static workTimeStagingToFinal(
worktime: WorkTimeStaging,
): Prisma.WorkTimeCreateInput {
const {
id,
employeeId,
firstname,
surname,
departmentName,
startDate,
endDate,
} = worktime;
return {
firstname,
surname,
startDate,
endDate,
id: `${id}`,
userId: `${employeeId}`,
department: departmentName,
pause: 0,
duration: 0,
};
}
}
@@ -0,0 +1,54 @@
// import { CalculationService } from '@dks/avicenna/api-util-calculation';
// import { Injectable } from '@nestjs/common';
// import { Tour } from '@prisma/client';
// import { chunk } from 'src/utils';
// import { TourPreStaging } from '../staging.interface';
// @Injectable()
// export class ToursCalculationAdapater {
// constructor(private readonly calculationService: CalculationService) {}
// public async addRevenueAndConsumptionCostsToTours(
// tours: TourPreStaging[],
// ): Promise<TourPreStaging[]> {
// const resultTours = [];
// for (const tourChunk of chunk(tours, 5)) {
// resultTours.push(
// await Promise.all(
// tourChunk.map((tour) =>
// this.addRevenueAndConsumptionCostsToTour(tour),
// ),
// ),
// );
// }
// return resultTours.flat();
// }
// public async addRevenueAndConsumptionCostsToTour(
// tour: TourPreStaging,
// ): Promise<TourPreStaging> {
// if (!tour?.ordinanceType) {
// return tour;
// }
// const revenue = await this.calculationService.calculateTourRevenue(
// tour as unknown as Tour,
// );
// const revenueDeviation =
// tour.revenueDispoLive > 0
// ? Math.floor((revenue - tour.revenueDispoLive) * 100) / 100
// : 0;
// const { consumptionCostsPerTour } =
// this.calculationService.getBillingInformationByOrdinanceType(
// tour.ordinanceType.match(/([A-Z])\w+/).at(0),
// tour.startDate,
// );
// return {
// ...tour,
// revenue,
// revenueDeviation,
// consumptionCosts: consumptionCostsPerTour,
// };
// }
// }
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
// import { ToursCalculationAdapater } from './adapter/tours-calculation.adapter';
import { StagingRepository } from './staging.repository';
import { TransformersService } from './transformers.service';
import { PrismaModule } from 'src/core/database/prisma.module';
import { DataAccessExtractorsModule } from '../feat-extractors/data-access-extractors.module';
@Module({
imports: [PrismaModule, DataAccessExtractorsModule],
providers: [TransformersService, StagingRepository],
exports: [TransformersService],
})
export class DataAccessTransformersModule {}
@@ -0,0 +1,117 @@
export interface TourPreStaging {
id?: string;
done: boolean;
direction: string;
carName?: string;
operationId: string;
driverId?: string;
driverName?: string;
codriverId?: string;
codriverName?: string;
patientId?: string;
patientName?: string;
patientSurname?: string;
patientStreet?: string;
patientZip?: string;
patientCity?: string;
healthInsurance?: string;
healthInsuranceNumber?: string;
type?: string;
category?: string;
transportType?: string;
ordinanceType?: string;
rangeEndDate?: string;
startInstitution?: string;
startStreet: string;
startZip: string;
startCity: string;
targetInstitution?: string;
targetStreet: string;
targetZip: string;
targetCity: string;
startDate: Date;
check: number;
occupiedKm?: number;
totalKm?: number;
startBegin?: string;
startEnd?: string;
target?: string;
targetBegin?: string;
empty?: string;
hasInfection: boolean;
infectionName?: string;
revenueDispoLive?: number;
billDate?: Date;
billNumber?: string;
revenue?: number;
revenueDeviation?: number;
consumptionCosts?: number;
createdAt?: Date;
}
export interface PatientPreStaging {
id?: string;
surname?: string;
name?: string;
street?: string;
zip?: string;
city?: string;
birthday?: Date;
healthinsuranceId?: string;
healthinsurance?: string;
careDegree?: string;
disabilityMark1?: string;
disabilityMark2?: string;
}
export interface AttendanceRegistrationPreStaging {
attendanceRegistrationId: number;
employeeId: number;
dateTime: Date;
event: string;
}
export interface PlannedTimePreStaging {
id: number;
employeeId: number;
firstname?: string;
surname?: string;
departmentName: string;
startDate: Date;
endDate: Date;
deleted: boolean;
mark?: string;
}
export interface WorkTimePreStaging {
id: number;
employeeId: number;
firstname: string;
surname: string;
departmentName: string;
startDate: Date;
endDate: Date;
}
export interface EmployeePreStaging {
id: number;
firstname: string;
surname: string;
contractStart: Date;
contractEnd: Date;
contractHoursWeek: number;
contractSalaryHour: number;
personTransportCertificate?: Date;
qualification?: string;
}
@@ -0,0 +1,118 @@
import { Injectable } from '@nestjs/common';
import {
AttendanceRegistrationStaging,
PatientStaging,
PlannedTimeStaging,
Prisma,
TourStaging,
WorkTimeStaging,
} from '@prisma/client';
import { PrismaService } from 'src/core/database/prisma.service';
import {
AttendanceRegistrationPreStaging,
EmployeePreStaging,
PatientPreStaging,
PlannedTimePreStaging,
TourPreStaging,
WorkTimePreStaging,
} from './staging.interface';
@Injectable()
export class StagingRepository {
constructor(private readonly prisma: PrismaService) {}
clearStaging(): Promise<Prisma.BatchPayload[]> {
return Promise.all([
this.prisma.tourStaging.deleteMany(),
this.prisma.employeeStaging.deleteMany(),
this.prisma.workTimeStaging.deleteMany(),
this.prisma.plannedTimeStaging.deleteMany(),
this.prisma.attendanceRegistrationStaging.deleteMany(),
this.prisma.patientStaging.deleteMany(),
]);
}
createPatients(patients: PatientPreStaging[]): Promise<Prisma.BatchPayload> {
return this.prisma.patientStaging.createMany({
data: patients,
});
}
createPlannedTimes(
plannedTimes: PlannedTimePreStaging[],
): Promise<Prisma.BatchPayload> {
return this.prisma.plannedTimeStaging.createMany({
data: plannedTimes,
});
}
createWorkTimes(
workTimes: WorkTimePreStaging[],
): Promise<Prisma.BatchPayload> {
return this.prisma.workTimeStaging.createMany({
data: workTimes,
});
}
createAttendanceRegistrations(
attendanceRegistrations: AttendanceRegistrationPreStaging[],
): Promise<Prisma.BatchPayload> {
return this.prisma.attendanceRegistrationStaging.createMany({
data: attendanceRegistrations,
});
}
createTours(tours: TourPreStaging[]): Promise<Prisma.BatchPayload> {
return this.prisma.tourStaging.createMany({
data: tours,
});
}
createEmployees(
employees: EmployeePreStaging[],
): Promise<Prisma.BatchPayload> {
return this.prisma.employeeStaging.createMany({
data: employees,
});
}
findPatients(): Promise<PatientStaging[]> {
return this.prisma.patientStaging.findMany({
orderBy: {
surname: 'asc',
},
});
}
findTours(): Promise<TourStaging[]> {
return this.prisma.tourStaging.findMany({
orderBy: {
startDate: 'asc',
},
});
}
findAttendanceRegistrations(): Promise<AttendanceRegistrationStaging[]> {
return this.prisma.attendanceRegistrationStaging.findMany({
orderBy: {
dateTime: 'asc',
},
});
}
findPlannedTimes(): Promise<PlannedTimeStaging[]> {
return this.prisma.plannedTimeStaging.findMany({
orderBy: {
startDate: 'asc',
},
});
}
findWorkTimes(): Promise<WorkTimeStaging[]> {
return this.prisma.workTimeStaging.findMany({
orderBy: {
startDate: 'asc',
},
});
}
}
@@ -0,0 +1,54 @@
import { parse, parseISO } from 'date-fns';
import { fromZonedTime } from 'date-fns-tz';
export const parseDateTime = (format: string) => (value: string) =>
parse(value, format, new Date());
export const parseIsoDateTime = (value: string) => parseISO(value);
export const parseZonedDateTime =
(tz: string) => (format: string) => (value: string) =>
fromZonedTime(parse(value, format, new Date()), tz);
export const parseCETDateTime = parseZonedDateTime('Europe/Berlin');
export const parseInteger = (value = '') => parseInt(value);
export const parseFloat = (value = '') =>
Number((value ?? '').replace(',', '.'));
export const parseBoolean = (value = '') => value === '1' || value === 'true';
export const pick =
<T extends Record<string, T[keyof T]>>(keys: (keyof T)[]) =>
(value: Record<keyof T, T[keyof T]>) =>
keys.reduce(
(obj, key) => {
if (value && Object.prototype.hasOwnProperty.call(value, key)) {
obj[key] = value[key];
}
return obj;
},
{} as Record<keyof T, T[keyof T]>,
);
export const pick2 = <T extends Record<string, unknown>>(
keys: (keyof T)[],
value: T,
) =>
keys.reduce((obj, key) => {
if (value && Object.prototype.hasOwnProperty.call(value, key)) {
obj[key] = value[key];
}
return obj;
}, {} as T);
export const transformProperties =
<T extends string, U>(transformer: (value: T) => U) =>
(value: Record<string, T>) =>
Object.entries(value).reduce(
(obj, [key, value]) => ({
...obj,
[key]: transformer(value),
}),
{} as Record<string, U>,
);
@@ -0,0 +1,149 @@
import { Injectable } from '@nestjs/common';
import {
AttendanceRegistrationStaging,
EmployeeDispoLiveSource,
EmployeeDyflexisSource,
PatientStaging,
PlannedTimeStaging,
Prisma,
TourStaging,
WorkTimeStaging,
} from '@prisma/client';
import { combineLatest, from, map, switchMap } from 'rxjs';
import { ExtractorsService } from '../feat-extractors/extractors.service';
// import { ToursCalculationAdapater } from './adapter/tours-calculation.adapter';
import { StagingRepository } from './staging.repository';
import { AttendanceRegistrationTransformer } from './transformers/attendance-registration.transformer';
import { EmployeeTransformer } from './transformers/employee.transformer';
import { PatientTransformer } from './transformers/patient.transformer';
import { PlannedTimeTransformer } from './transformers/planned-time.transformer';
import { TourTransformer } from './transformers/tour.transformer';
import { WorkTimeTransformer } from './transformers/work-time.transformer';
@Injectable()
export class TransformersService {
constructor(
private readonly extractorsService: ExtractorsService,
private readonly repository: StagingRepository,
// private readonly tourCalculationAdapter: ToursCalculationAdapater,
) {}
clearStaging(): Promise<Prisma.BatchPayload[]> {
return this.repository.clearStaging();
}
transformPlannedTimes(jobId: string) {
return from(this.extractorsService.findPlannedTimesByJobId(jobId)).pipe(
map((plannedTimes) =>
plannedTimes.map((plannedTime) =>
PlannedTimeTransformer.transform(plannedTime),
),
),
switchMap((plannedTimes) =>
this.repository.createPlannedTimes(plannedTimes),
),
);
}
transformWorkTimes(jobId: string) {
return from(this.extractorsService.findWorkTimesByJobId(jobId)).pipe(
map((workTimes) =>
workTimes.map((workTime) => WorkTimeTransformer.transform(workTime)),
),
switchMap((workTimes) => this.repository.createWorkTimes(workTimes)),
);
}
transformAttendanceRegistrations(jobId: string) {
return from(
this.extractorsService.findAttendanceRegistrationsByJobId(jobId),
).pipe(
map((attendanceRegistrations) =>
attendanceRegistrations.map((attendanceRegistration) =>
AttendanceRegistrationTransformer.transform(attendanceRegistration),
),
),
switchMap((attendanceRegistrations) =>
this.repository.createAttendanceRegistrations(attendanceRegistrations),
),
);
}
transformTours(jobId: string) {
return from(this.extractorsService.findToursByJobId(jobId)).pipe(
// TODO: Add validation filter
map((tours) =>
tours
.filter(({ einsatzNummer }) => !!einsatzNummer)
.map((tour) => TourTransformer.transform(tour)),
),
// switchMap((tours) =>
// from(
// this.tourCalculationAdapter.addRevenueAndConsumptionCostsToTours(
// tours,
// ),
// ),
// ),
switchMap((tours) => this.repository.createTours(tours)),
);
}
transformEmployees(jobId: string) {
return combineLatest([
from(this.extractorsService.findEmployeesDispoLiveByJobId(jobId)),
from(this.extractorsService.findEmployeesDyflexisByJobId(jobId)),
]).pipe(
map(([dispoLiveEmployees, dyflexisEmployees]) =>
dyflexisEmployees
.map((dyflexisEmployee) => {
const dispoLiveEmployee = dispoLiveEmployees.find(
(e) => e.importPersId === dyflexisEmployee.id,
);
if (!dispoLiveEmployee) return undefined;
return [dyflexisEmployee, dispoLiveEmployee];
})
.filter(Boolean),
),
map((employees) =>
employees.map(([dyflexisEmployee, dispoLiveEmployee]) =>
EmployeeTransformer.transform(
dyflexisEmployee as EmployeeDyflexisSource,
dispoLiveEmployee as EmployeeDispoLiveSource,
),
),
),
switchMap((employees) => this.repository.createEmployees(employees)),
);
}
transformPatients(jobId: string) {
return from(this.extractorsService.findPatientsByJobId(jobId)).pipe(
map((patients) =>
patients.map((patient) => PatientTransformer.transform(patient)),
),
switchMap((patients) => this.repository.createPatients(patients)),
);
}
findPatients(): Promise<PatientStaging[]> {
return this.repository.findPatients();
}
findTours(): Promise<TourStaging[]> {
return this.repository.findTours();
}
findAttendanceRegistrations(): Promise<AttendanceRegistrationStaging[]> {
return this.repository.findAttendanceRegistrations();
}
findPlannedTimes(): Promise<PlannedTimeStaging[]> {
return this.repository.findPlannedTimes();
}
findWorkTimes(): Promise<WorkTimeStaging[]> {
return this.repository.findWorkTimes();
}
}
@@ -0,0 +1,36 @@
import { AttendanceRegistrationSource } from '@prisma/client';
import { AttendanceRegistrationPreStaging } from '../staging.interface';
import {
parseInteger,
parseIsoDateTime,
pick,
transformProperties,
} from '../transformer.functions';
export class AttendanceRegistrationTransformer {
public static transform(
attendanceRegistrationSource: AttendanceRegistrationSource,
): AttendanceRegistrationPreStaging {
const attendanceRegistrationParsed = {
...attendanceRegistrationSource,
...transformProperties(parseInteger)(
pick(['attendanceRegistrationId', 'employeeId'])(
attendanceRegistrationSource,
),
),
...transformProperties(parseIsoDateTime)(
pick(['dateTime'])(attendanceRegistrationSource),
),
};
const { attendanceRegistrationId, employeeId, dateTime, event } =
attendanceRegistrationParsed;
return {
attendanceRegistrationId,
employeeId,
dateTime,
event,
} as unknown as AttendanceRegistrationPreStaging;
}
}
@@ -0,0 +1,67 @@
import {
EmployeeDispoLiveSource,
EmployeeDyflexisSource,
} from '@prisma/client';
import { isValidDate } from 'src/utils';
import { EmployeePreStaging } from '../staging.interface';
import {
parseDateTime,
parseFloat,
parseInteger,
pick,
transformProperties,
} from '../transformer.functions';
export class EmployeeTransformer {
public static transform(
dyflexisEmployeeSource: EmployeeDyflexisSource,
dispoLiveEmployeeSource: EmployeeDispoLiveSource,
): EmployeePreStaging {
const dyflexisEmployeeParsed = {
...dyflexisEmployeeSource,
...transformProperties(parseFloat)(
pick(['contract_hours_week', 'contract_salary_hour'])(
dyflexisEmployeeSource,
),
),
...transformProperties(parseInteger)(
pick(['id'])(dyflexisEmployeeSource),
),
...transformProperties(parseDateTime('yyyy-MM-dd'))(
pick(['contract_start', 'contract_end'])(dyflexisEmployeeSource),
),
};
const dispoLiveEmployeeParsed = {
...dispoLiveEmployeeSource,
...transformProperties(parseDateTime('yyyy-MM-dd'))(
pick(['pScheinAblaufDatum'])(dispoLiveEmployeeSource),
),
};
const {
id,
firstname,
surname,
contract_start,
contract_end,
contract_hours_week,
contract_salary_hour,
} = dyflexisEmployeeParsed;
const { pScheinAblaufDatum, qualifikation } = dispoLiveEmployeeParsed;
return {
id,
firstname,
surname,
contractStart: contract_start,
contractEnd: contract_end,
contractHoursWeek: contract_hours_week,
contractSalaryHour: contract_salary_hour,
personTransportCertificate: isValidDate(new Date(pScheinAblaufDatum))
? pScheinAblaufDatum
: undefined,
qualification: qualifikation,
} as unknown as EmployeePreStaging;
}
}
@@ -0,0 +1,50 @@
import { PatientSource } from '@prisma/client';
import { isValidDate } from 'src/utils';
import { PatientPreStaging } from '../staging.interface';
import {
parseDateTime,
pick,
transformProperties,
} from '../transformer.functions';
export class PatientTransformer {
public static transform(patientSource: PatientSource): PatientPreStaging {
const patientParsed = {
...patientSource,
...transformProperties(parseDateTime('yyyy-MM-dd'))(
pick(['birthday'])(patientSource),
),
};
const {
id,
surname,
name,
street,
zip,
city,
birthday,
kkId,
krankenkasse,
pflegestufe,
behinderung1,
behinderung2,
} = patientParsed;
return {
id,
surname,
name,
street,
zip,
city,
birthday: isValidDate(new Date(birthday)) ? birthday : undefined,
healthinsurance: krankenkasse,
healthinsuranceId: kkId,
careDegree: pflegestufe,
disabilityMark1: behinderung1,
disabilityMark2: behinderung2,
} as unknown as PatientPreStaging;
}
}
@@ -0,0 +1,52 @@
import { PlannedTimeSource } from '@prisma/client';
import { PlannedTimePreStaging } from '../staging.interface';
import {
parseBoolean,
parseCETDateTime,
parseInteger,
pick,
transformProperties,
} from '../transformer.functions';
export class PlannedTimeTransformer {
public static transform(
plannedTimeSource: PlannedTimeSource,
): PlannedTimePreStaging {
const plannedTimeParsed = {
...plannedTimeSource,
...transformProperties(parseInteger)(
pick(['id', 'user_id'])(plannedTimeSource),
),
...transformProperties(parseCETDateTime('yyyy-MM-dd HH:mm:ss'))(
pick(['start_date', 'end_date'])(plannedTimeSource),
),
...transformProperties(parseBoolean)(
pick(['deleted'])(plannedTimeSource),
),
};
const {
id,
user_id,
firstname,
surname,
department_name,
start_date,
end_date,
deleted,
mark,
} = plannedTimeParsed;
return {
id,
firstname,
surname,
deleted,
mark,
employeeId: user_id,
departmentName: department_name,
startDate: start_date,
endDate: end_date,
} as unknown as PlannedTimePreStaging;
}
}
@@ -0,0 +1,142 @@
import { TourSource } from '@prisma/client';
import { TourPreStaging } from '../staging.interface';
import {
parseBoolean,
parseCETDateTime,
parseDateTime,
parseFloat,
parseInteger,
parseIsoDateTime,
pick,
transformProperties,
} from '../transformer.functions';
export class TourTransformer {
public static transform(tourSource: TourSource): TourPreStaging {
const tourParsed = {
...tourSource,
...transformProperties(parseInteger)(pick(['check'])(tourSource)),
...transformProperties(parseFloat)(
pick(['besetztKm', 'gesamtPreis'])(tourSource),
),
...transformProperties(parseIsoDateTime)(pick(['date_iso'])(tourSource)),
...(tourSource.abrDate
? transformProperties(parseDateTime('yyyy-MM-dd'))(
pick(['abrDate'])(tourSource),
)
: undefined),
...(tourSource.endgen
? transformProperties(parseDateTime('yyyy-MM-dd'))(
pick(['endgen'])(tourSource),
)
: undefined),
...(tourSource.createTime.length > 10
? transformProperties(parseCETDateTime('yyyy-MM-dd HH:mm:ss'))(
pick(['createTime'])(tourSource),
)
: transformProperties(parseCETDateTime('yyyy-MM-dd'))(
pick(['createTime'])(tourSource),
)),
...transformProperties(parseBoolean)(
pick(['infektion', 'gefahren'])(tourSource),
),
};
const {
id,
gefahren,
direction,
carName,
einsatzNummer,
fahrer,
fahrerName,
beifahrer,
beifahrerName,
patId,
patName,
patSurname,
patStreet,
patZip,
patCity,
krankenkasse,
kkId,
type,
serienKategorName,
transportKrz,
verordnungsName,
endgen,
startInstitution,
startStreet,
startZip,
startCity,
targetInstitution,
targetStreet,
targetZip,
targetCity,
date_iso,
check,
besetztKm,
startBegin,
startEnd,
ziel,
zielBegin,
frei,
infektion,
itKrz,
gesamtPreis,
abrDate,
abrNum,
createTime,
} = tourParsed;
return {
id,
direction,
carName,
startInstitution,
startStreet,
startZip,
startCity,
targetInstitution,
targetStreet,
targetZip,
targetCity,
check,
type,
done: gefahren,
operationId: einsatzNummer,
driverId: fahrer,
driverName: fahrerName,
codriverId: beifahrer,
codriverName: beifahrerName,
patientId: patId,
patientName: patName,
patientSurname: patSurname,
patientStreet: patStreet,
patientZip: patZip,
patientCity: patCity,
healthInsurance: krankenkasse,
healthInsuranceNumber: kkId,
category: serienKategorName,
transportType: transportKrz,
ordinanceType: verordnungsName,
rangeEndDate: endgen,
startDate: date_iso,
occupiedKm: besetztKm,
totalKm: besetztKm,
startBegin,
startEnd,
target: ziel,
targetBegin: zielBegin,
empty: frei,
hasInfection: infektion,
infectionName: itKrz,
revenueDispoLive: gesamtPreis,
billDate: abrDate,
billNumber: abrNum,
createdAt: createTime,
revenue: 0,
consumptionCosts: 0,
} as unknown as TourPreStaging;
}
}
@@ -0,0 +1,42 @@
import { WorkTimeSource } from '@prisma/client';
import { WorkTimePreStaging } from '../staging.interface';
import {
parseCETDateTime,
parseInteger,
pick,
transformProperties,
} from '../transformer.functions';
export class WorkTimeTransformer {
public static transform(workTimeSource: WorkTimeSource): WorkTimePreStaging {
const workTimeParsed = {
...workTimeSource,
...transformProperties(parseInteger)(
pick(['id', 'user_id'])(workTimeSource),
),
...transformProperties(parseCETDateTime('yyyy-MM-dd HH:mm:ss'))(
pick(['start_date', 'end_date'])(workTimeSource),
),
};
const {
id,
user_id,
firstname,
surname,
department_name,
start_date,
end_date,
} = workTimeParsed;
return {
id,
firstname,
surname,
employeeId: user_id,
departmentName: department_name,
startDate: start_date,
endDate: end_date,
} as unknown as WorkTimePreStaging;
}
}