modernize ticket and accounting

iteration 0
This commit is contained in:
Marcel Arndt 2025-11-24 21:31:43 +01:00
parent c33ab89354
commit b1a70673a4
97 changed files with 13619 additions and 2398 deletions

82
GEMINI.md Normal file
View File

@ -0,0 +1,82 @@
# Avicenna Project Documentation
This document provides a comprehensive overview of the Avicenna project, its components, and instructions for development and deployment.
## Project Overview
The Avicenna project is a multi-component system composed of a web application, a data connector, a data transformation service, and the necessary infrastructure to run them. The project is divided into the following main parts:
- **service**: Contains the core application services, including the frontend dashboard and the backend data connector.
- **plattform**: Includes the data transformation and integration services.
- **infrastructure**: Defines the infrastructure as code for deploying the project.
## Services
### App Hub Dashboard (Angular Frontend)
The App Hub Dashboard is the user-facing web application for the Avicenna project, developed with Angular.
- **Location**: `service/app-hub/dashboard`
- **Key Files**:
- `angular.json`: Angular project configuration.
- `package.json`: Project dependencies and scripts.
- `tailwind.config.js`: Tailwind CSS configuration.
#### Commands
- **Run development server**: `ng serve`
- **Build for production**: `ng build`
- **Run unit tests**: `ng test`
### Data Hub Connector (NestJS Backend)
The Data Hub Connector is a NestJS-based backend service responsible for managing data, with database interactions handled by Prisma.
- **Location**: `service/data-hub/data-connector`
- **Key Files**:
- `nest-cli.json`: NestJS project configuration.
- `package.json`: Project dependencies and scripts.
- `prisma/schema.prisma`: Database schema definition.
#### Commands
- **Install dependencies**: `npm install`
- **Run in development mode**: `npm run start:dev`
- **Run in production mode**: `npm run start:prod`
- **Run tests**: `npm run test`
### Synapse (Go Plattforms)
Synapse is a data transformation service written in Go, responsible for processing and integrating data from various sources.
- **Location**: `plattform/synapse`
- **Key Files**:
- `go.mod`: Go module definition.
- `cmd/synapse/main.go`: Main application entry point.
#### Commands
- **Run the application**: `go run ./cmd/synapse`
- **Build the application**: `go build -o synapse ./cmd/synapse`
## Infrastructure
The infrastructure for the Avicenna project is managed using Pulumi and Ansible, ensuring a reproducible and automated deployment process.
- **Location**: `infrastructure`
- **Key Components**:
- **Pulumi**: Used for provisioning cloud resources.
- **Ansible**: Used for configuring services like Traefik, Portainer, and Kestra.
## Workflows
The project uses Kestra for orchestrating complex workflows, such as ETL processes and ticket management.
- **Location**: `service/workflows`
- **Key Files**:
- `avic-etl.yml`: Defines the ETL workflow.
- `avic-tickets.yml`: Defines the ticket management workflow.
## Development
To work on the Avicenna project, follow the instructions in the respective service directories. Ensure you have the required runtimes and dependencies installed, such as Node.js, Go, and the Angular CLI.

View File

@ -1,4 +1,4 @@
FROM node:20.15.1-alpine AS builder FROM node:20.19.1-alpine AS builder
WORKDIR /app/builder WORKDIR /app/builder

View File

@ -0,0 +1,4 @@
{
"componentsPath": "src/app/core/base-components",
"importAlias": "@spartan-ng/helm"
}

View File

@ -1,5 +1,6 @@
#!/bin/bash #!/bin/bash
set -xe set -xe
: "${API_BASEURL?API_BASEURL is not provided}"
: "${GRAPHQL_ENDPOINT?GRAPHQL_ENDPOINT is not provided}" : "${GRAPHQL_ENDPOINT?GRAPHQL_ENDPOINT is not provided}"
: "${GRAPHQL_WS_ENDPOINT?GRAPHQL_WS_ENDPOINT is not provided}" : "${GRAPHQL_WS_ENDPOINT?GRAPHQL_WS_ENDPOINT is not provided}"
: "${MS_AUTHENTICATION_CLIENT_ID?MS_AUTHENTICATION_CLIENT_ID is not provided}" : "${MS_AUTHENTICATION_CLIENT_ID?MS_AUTHENTICATION_CLIENT_ID is not provided}"
@ -10,14 +11,14 @@ set -xe
: "${OTLP_SERVICE_NAME?OTLP_SERVICE_NAME is not provided}" : "${OTLP_SERVICE_NAME?OTLP_SERVICE_NAME is not provided}"
: "${COLLECTOR_SECRET?COLLECTOR_SECRET is not provided}" : "${COLLECTOR_SECRET?COLLECTOR_SECRET is not provided}"
env_vars=("GRAPHQL_ENDPOINT" "GRAPHQL_WS_ENDPOINT" "MS_AUTHENTICATION_CLIENT_ID" "MS_AUTHENTICATION_AUTHORITY" "MS_AUTHENTICATION_REDIRECT_URI" "MS_AUTHENTICATION_POST_LOGOUT_REDIRECT_URI" "OTLP_ENDPOINT" "OTLP_SERVICE_NAME" "COLLECTOR_SECRET") env_vars=("API_BASEURL" "GRAPHQL_ENDPOINT" "GRAPHQL_WS_ENDPOINT" "MS_AUTHENTICATION_CLIENT_ID" "MS_AUTHENTICATION_AUTHORITY" "MS_AUTHENTICATION_REDIRECT_URI" "MS_AUTHENTICATION_POST_LOGOUT_REDIRECT_URI" "OTLP_ENDPOINT" "OTLP_SERVICE_NAME" "COLLECTOR_SECRET")
ls /usr/share/nginx/html/ ls /usr/share/nginx/html/
for var in "${env_vars[@]}" for var in "${env_vars[@]}"
do do
echo "$var ${!var}" echo "$var ${!var}"
sed -i "s~{{$var}}~${!var}~gi" /usr/share/nginx/html/main*.js sed -i "s~{{$var}}~${!var}~gi" /usr/share/nginx/html/*.js
done done
exec "$@" exec "$@"

File diff suppressed because it is too large Load Diff

View File

@ -25,8 +25,8 @@
"@apollo/client": "^3.13.8", "@apollo/client": "^3.13.8",
"@azure/msal-angular": "^3.0.22", "@azure/msal-angular": "^3.0.22",
"@azure/msal-browser": "^3.19.1", "@azure/msal-browser": "^3.19.1",
"@grafana/faro-web-sdk": "^1.15.0", "@ng-icons/core": "^31.4.0",
"@grafana/faro-web-tracing": "^1.15.0", "@ng-icons/lucide": "^31.4.0",
"@ngneat/dialog": "^5.1.1", "@ngneat/dialog": "^5.1.1",
"@ngneat/overview": "^6.0.0", "@ngneat/overview": "^6.0.0",
"@ngxpert/hot-toast": "^3.0.0", "@ngxpert/hot-toast": "^3.0.0",
@ -41,12 +41,16 @@
"@opentelemetry/resources": "^1.30.1", "@opentelemetry/resources": "^1.30.1",
"@opentelemetry/sdk-trace-web": "^1.30.1", "@opentelemetry/sdk-trace-web": "^1.30.1",
"@opentelemetry/semantic-conventions": "^1.34.0", "@opentelemetry/semantic-conventions": "^1.34.0",
"@spartan-ng/brain": "^0.0.1-alpha.561",
"apollo-angular": "^10.0.3", "apollo-angular": "^10.0.3",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"echarts": "^5.5.1", "echarts": "^5.5.1",
"graphql": "^16.11.0", "graphql": "^16.11.0",
"graphql-ws": "^5.16.0", "graphql-ws": "^5.16.0",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
"tailwind-merge": "^3.3.1",
"tslib": "^2.3.0", "tslib": "^2.3.0",
"zone.js": "~0.15.0" "zone.js": "~0.15.0"
}, },
@ -60,6 +64,7 @@
"@graphql-codegen/typescript-apollo-angular": "^4.0.0", "@graphql-codegen/typescript-apollo-angular": "^4.0.0",
"@graphql-codegen/typescript-operations": "^4.6.0", "@graphql-codegen/typescript-operations": "^4.6.0",
"@material/material-color-utilities": "^0.3.0", "@material/material-color-utilities": "^0.3.0",
"@spartan-ng/cli": "^0.0.1-alpha.561",
"@tailwindcss/postcss": "^4.1.4", "@tailwindcss/postcss": "^4.1.4",
"@types/jasmine": "~5.1.0", "@types/jasmine": "~5.1.0",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
@ -71,6 +76,7 @@
"karma-jasmine-html-reporter": "~2.1.0", "karma-jasmine-html-reporter": "~2.1.0",
"postcss": "^8.4.39", "postcss": "^8.4.39",
"tailwindcss": "^4.1.4", "tailwindcss": "^4.1.4",
"tw-animate-css": "^1.4.0",
"typescript": "~5.5.2" "typescript": "~5.5.2"
} }
} }

View File

@ -17,10 +17,6 @@ import { graphqlProvider } from './graphql.provider';
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
providers: [ providers: [
// provideAppInitializer(() => {
// const initializerFn = faroInitializer();
// return initializerFn();
// }),
{ {
provide: ErrorHandler, provide: ErrorHandler,
useClass: GlobalErrorHandler, useClass: GlobalErrorHandler,

View File

@ -0,0 +1,5 @@
import { HlmInput } from './lib/hlm-input';
export * from './lib/hlm-input';
export const HlmInputImports = [HlmInput] as const;

View File

@ -0,0 +1,103 @@
import {
computed,
Directive,
type DoCheck,
effect,
forwardRef,
inject,
Injector,
input,
linkedSignal,
signal,
untracked,
} from '@angular/core';
import { FormGroupDirective, NgControl, NgForm } from '@angular/forms';
import { BrnFormFieldControl } from '@spartan-ng/brain/form-field';
import { ErrorStateMatcher, ErrorStateTracker } from '@spartan-ng/brain/forms';
import { hlm } from '../../../utils/src';
import { cva, type VariantProps } from 'class-variance-authority';
import type { ClassValue } from 'clsx';
export const inputVariants = cva(
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-[3px] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
{
variants: {
error: {
auto: '[&.ng-invalid.ng-touched]:border-destructive [&.ng-invalid.ng-touched]:ring-destructive/20 dark:[&.ng-invalid.ng-touched]:ring-destructive/40',
true: 'border-destructive focus-visible:border-destructive focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40',
},
},
defaultVariants: {
error: 'auto',
},
},
);
type InputVariants = VariantProps<typeof inputVariants>;
@Directive({
selector: '[hlmInput]',
providers: [
{
provide: BrnFormFieldControl,
useExisting: forwardRef(() => HlmInput),
},
],
host: {
'[class]': '_computedClass()',
},
})
export class HlmInput implements BrnFormFieldControl, DoCheck {
private readonly _injector = inject(Injector);
private readonly _additionalClasses = signal<ClassValue>('');
private readonly _errorStateTracker: ErrorStateTracker;
private readonly _defaultErrorStateMatcher = inject(ErrorStateMatcher);
private readonly _parentForm = inject(NgForm, { optional: true });
private readonly _parentFormGroup = inject(FormGroupDirective, { optional: true });
public readonly userClass = input<ClassValue>('', { alias: 'class' });
protected readonly _computedClass = computed(() =>
hlm(inputVariants({ error: this._state().error }), this.userClass(), this._additionalClasses()),
);
public readonly error = input<InputVariants['error']>('auto');
protected readonly _state = linkedSignal(() => ({ error: this.error() }));
public readonly ngControl: NgControl | null = this._injector.get(NgControl, null);
public readonly errorState = computed(() => this._errorStateTracker.errorState());
constructor() {
this._errorStateTracker = new ErrorStateTracker(
this._defaultErrorStateMatcher,
this.ngControl,
this._parentFormGroup,
this._parentForm,
);
effect(() => {
const error = this._errorStateTracker.errorState();
untracked(() => {
if (this.ngControl) {
const shouldShowError = error && this.ngControl.invalid && (this.ngControl.touched || this.ngControl.dirty);
this._errorStateTracker.errorState.set(shouldShowError ? true : false);
this.setError(shouldShowError ? true : 'auto');
}
});
});
}
ngDoCheck() {
this._errorStateTracker.updateErrorState();
}
setError(error: InputVariants['error']) {
this._state.set({ error });
}
setClass(classes: string): void {
this._additionalClasses.set(classes);
}
}

View File

@ -0,0 +1,5 @@
import { HlmLabel } from './lib/hlm-label';
export * from './lib/hlm-label';
export const HlmLabelImports = [HlmLabel] as const;

View File

@ -0,0 +1,27 @@
import { Directive, computed, input } from '@angular/core';
import { BrnLabel } from '@spartan-ng/brain/label';
import { hlm } from '@spartan-ng/helm/utils';
import type { ClassValue } from 'clsx';
@Directive({
selector: '[hlmLabel]',
hostDirectives: [
{
directive: BrnLabel,
inputs: ['id'],
},
],
host: {
'[class]': '_computedClass()',
},
})
export class HlmLabel {
public readonly userClass = input<ClassValue>('', { alias: 'class' });
protected readonly _computedClass = computed(() =>
hlm(
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50 peer-data-[disabled]:cursor-not-allowed peer-data-[disabled]:opacity-50 has-[[disabled]]:cursor-not-allowed has-[[disabled]]:opacity-50',
this.userClass(),
),
);
}

View File

@ -0,0 +1 @@
export * from './lib/hlm';

View File

@ -0,0 +1,6 @@
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function hlm(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View File

@ -0,0 +1,12 @@
<div class="flex w-full max-w-sm gap-3">
<input
hlmInput
type="file"
(change)="onFileSelected($event)"
#fileInput
id="fileInput"
/>
<button mat-button (click)="onUpload()" [disabled]="!selectedFile">
Hochladen
</button>
</div>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TransportDocumentUploadComponent } from './transport-document-upload.component';
describe('FileUploadComponent', () => {
let component: TransportDocumentUploadComponent;
let fixture: ComponentFixture<TransportDocumentUploadComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TransportDocumentUploadComponent]
})
.compileComponents();
fixture = TestBed.createComponent(TransportDocumentUploadComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,63 @@
import { HttpEventType } from '@angular/common/http';
import { Component, inject, input, output } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { HlmInputImports } from '@spartan-ng/helm/input';
import { HlmLabelImports } from '@spartan-ng/helm/label';
import { environment } from '../../../../environments/environment';
import { TransportDocumentUploadService } from './transport-document-upload.service';
import { HotToastService } from '@ngxpert/hot-toast';
@Component({
selector: 'app-transport-document-upload',
imports: [HlmInputImports, HlmLabelImports, MatButtonModule],
templateUrl: './transport-document-upload.component.html',
styleUrl: './transport-document-upload.component.scss',
providers: [TransportDocumentUploadService],
})
export class TransportDocumentUploadComponent {
toast = inject(HotToastService)
ticketId = input.required<string>();
uploaded = output<void>()
selectedFile: File | null = null;
uploadProgress = 0;
uploadStatus: string | null = null;
constructor(private uploadService: TransportDocumentUploadService) {}
onFileSelected(event: Event): void {
const input = event.target as HTMLInputElement;
if (input.files && input.files.length > 0) {
this.selectedFile = input.files[0];
this.uploadProgress = 0;
this.uploadStatus = null;
}
}
onUpload(): void {
if (!this.selectedFile) {
return;
}
const uploadUrl = `${
environment.api.baseUrl
}/tickets/${this.ticketId()}/files`;
this.uploadService.uploadFile(uploadUrl, this.selectedFile).subscribe({
next: (event) => {
if (event.type === HttpEventType.UploadProgress && event.total) {
this.uploadProgress = Math.round(100 * (event.loaded / event.total));
} else if (event.type === HttpEventType.Response) {
this.selectedFile = null;
this.toast.success("Transportschein erfolgreich hochgeladen")
this.uploaded.emit()
}
},
error: (err) => {
this.uploadProgress = 0;
this.toast.error("Fehler beim Upload, bitte versuchen Sie es erneut")
},
});
}
}

View File

@ -0,0 +1,27 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpRequest, HttpEvent } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable()
export class TransportDocumentUploadService {
constructor(private http: HttpClient) { }
/**
* Lädt eine Datei zu einem *vom Aufrufer* definierten Endpunkt hoch.
* @param url Der vollständige API-Endpunkt für den Upload
* @param file Die hochzuladende Datei
* @returns Ein Observable mit dem Upload-Fortschritt
*/
uploadFile(url: string, file: File): Observable<HttpEvent<any>> {
const formData: FormData = new FormData();
formData.append('file', file, file.name);
const req = new HttpRequest('POST', url, formData, {
reportProgress: true,
});
return this.http.request(req);
}
}

View File

@ -27,6 +27,7 @@ export type AccountingFilterArgs = {
export enum AccountingState { export enum AccountingState {
Billable = 'Billable', Billable = 'Billable',
Billed = 'Billed', Billed = 'Billed',
Pending = 'Pending',
PreparedForBilling = 'PreparedForBilling' PreparedForBilling = 'PreparedForBilling'
} }
@ -165,6 +166,7 @@ export type Metric = {
export type Mutation = { export type Mutation = {
__typename?: 'Mutation'; __typename?: 'Mutation';
acceptAnomaly: Anomaly; acceptAnomaly: Anomaly;
sendApprovalRequest: Scalars['Boolean']['output'];
setAnomalyToSleep: Anomaly; setAnomalyToSleep: Anomaly;
setDocumentInfo: Ticket; setDocumentInfo: Ticket;
}; };
@ -175,6 +177,12 @@ export type MutationAcceptAnomalyArgs = {
}; };
export type MutationSendApprovalRequestArgs = {
email: Scalars['String']['input'];
ticketId: Scalars['String']['input'];
};
export type MutationSetAnomalyToSleepArgs = { export type MutationSetAnomalyToSleepArgs = {
id: Scalars['String']['input']; id: Scalars['String']['input'];
}; };
@ -523,6 +531,7 @@ export type Tour = {
totalKm: Scalars['Float']['output']; totalKm: Scalars['Float']['output'];
transportType?: Maybe<Scalars['String']['output']>; transportType?: Maybe<Scalars['String']['output']>;
type: Scalars['String']['output']; type: Scalars['String']['output'];
updatedAt?: Maybe<Scalars['DateTime']['output']>;
}; };
export type TourCostBreakdown = { export type TourCostBreakdown = {
@ -701,6 +710,14 @@ export type SetDocumentInfoMutationVariables = Exact<{
export type SetDocumentInfoMutation = { __typename?: 'Mutation', setDocumentInfo: { __typename?: 'Ticket', id: string, currentState: TicketValidationState, approvalState?: ApprovalState | null, errors: Array<string>, notes: Array<string>, isUrgent: boolean, urgency: any, createdAt: any, documentInfo?: { __typename?: 'DocumentInfo', tdLocation?: string | null, approval: { __typename?: 'ApprovalInfo', isNeeded?: boolean | null, whoRequested?: string | null, answeredFrom?: string | null, location?: string | null } } | null, tours: Array<{ __typename?: 'Tour', id: string, done: boolean, direction: string, carName?: string | null, operationId: string, driverName?: string | null, codriverName?: string | null, patientName?: string | null, patientSurname?: string | null, patientStreet?: string | null, patientZip?: string | null, patientCity?: string | null, healthInsurance?: string | null, category?: string | null, transportType?: string | null, ordinanceType?: string | null, startStreet?: string | null, startZip?: string | null, startCity?: string | null, targetStreet?: string | null, targetZip?: string | null, targetCity?: string | null, startDate?: any | null, check: number, occupiedKm: number, totalKm: number, type: string, rangeEndDate?: any | null, startBegin?: string | null, startEnd?: string | null, target?: string | null, targetBegin?: string | null, empty?: string | null }> } }; export type SetDocumentInfoMutation = { __typename?: 'Mutation', setDocumentInfo: { __typename?: 'Ticket', id: string, currentState: TicketValidationState, approvalState?: ApprovalState | null, errors: Array<string>, notes: Array<string>, isUrgent: boolean, urgency: any, createdAt: any, documentInfo?: { __typename?: 'DocumentInfo', tdLocation?: string | null, approval: { __typename?: 'ApprovalInfo', isNeeded?: boolean | null, whoRequested?: string | null, answeredFrom?: string | null, location?: string | null } } | null, tours: Array<{ __typename?: 'Tour', id: string, done: boolean, direction: string, carName?: string | null, operationId: string, driverName?: string | null, codriverName?: string | null, patientName?: string | null, patientSurname?: string | null, patientStreet?: string | null, patientZip?: string | null, patientCity?: string | null, healthInsurance?: string | null, category?: string | null, transportType?: string | null, ordinanceType?: string | null, startStreet?: string | null, startZip?: string | null, startCity?: string | null, targetStreet?: string | null, targetZip?: string | null, targetCity?: string | null, startDate?: any | null, check: number, occupiedKm: number, totalKm: number, type: string, rangeEndDate?: any | null, startBegin?: string | null, startEnd?: string | null, target?: string | null, targetBegin?: string | null, empty?: string | null }> } };
export type SendApprovalRequestMutationVariables = Exact<{
email: Scalars['String']['input'];
ticketId: Scalars['String']['input'];
}>;
export type SendApprovalRequestMutation = { __typename?: 'Mutation', sendApprovalRequest: boolean };
export type GetUpcomingToursQueryVariables = Exact<{ [key: string]: never; }>; export type GetUpcomingToursQueryVariables = Exact<{ [key: string]: never; }>;
@ -1513,6 +1530,22 @@ export const SetDocumentInfoDocument = gql`
export class SetDocumentInfoGQL extends Apollo.Mutation<SetDocumentInfoMutation, SetDocumentInfoMutationVariables> { export class SetDocumentInfoGQL extends Apollo.Mutation<SetDocumentInfoMutation, SetDocumentInfoMutationVariables> {
override document = SetDocumentInfoDocument; override document = SetDocumentInfoDocument;
constructor(apollo: Apollo.Apollo) {
super(apollo);
}
}
export const SendApprovalRequestDocument = gql`
mutation SendApprovalRequest($email: String!, $ticketId: String!) {
sendApprovalRequest(email: $email, ticketId: $ticketId)
}
`;
@Injectable({
providedIn: 'root'
})
export class SendApprovalRequestGQL extends Apollo.Mutation<SendApprovalRequestMutation, SendApprovalRequestMutationVariables> {
override document = SendApprovalRequestDocument;
constructor(apollo: Apollo.Apollo) { constructor(apollo: Apollo.Apollo) {
super(apollo); super(apollo);
} }

View File

@ -217,3 +217,7 @@ mutation SetDocumentInfo(
} }
} }
} }
mutation SendApprovalRequest($email: String!, $ticketId: String!) {
sendApprovalRequest(email: $email, ticketId: $ticketId)
}

View File

@ -0,0 +1,79 @@
import { Injectable } from '@angular/core';
import { TimePerTourUpdatedGQL } from '../data-access/graphql/generated/generated';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { tap } from 'rxjs';
interface ActivityMap {
[entityId: string]: number;
}
@Injectable({ providedIn: 'root' })
export class ActivityService {
private readonly STORAGE_KEY = 'user_pending_activities';
private readonly MAX_AGE_MS = 6 * 60 * 1000;
constructor(backendUpdatedGql: TimePerTourUpdatedGQL) {
backendUpdatedGql
.subscribe()
.pipe(
takeUntilDestroyed(),
tap(() => this.handleEtlFinishedEvent())
)
.subscribe();
}
markAsDirty(entityId: string): void {
const activities = this.loadActivities();
activities[entityId] = Date.now();
this.saveActivities(activities)
console.log(activities)
}
isDirty(entityId: string): boolean {
const activities = this.loadActivities();
const timestamp = activities[entityId];
if (!timestamp) return false;
if (Date.now() - timestamp > this.MAX_AGE_MS) {
return false;
}
return true;
}
handleEtlFinishedEvent(): void {
const activities = this.loadActivities();
const now = Date.now();
const safeThreshold = 60 * 1000;
const filteredActivities: ActivityMap = {};
Object.keys(activities).forEach((id) => {
const timestamp = activities[id];
if (now - timestamp < safeThreshold) {
filteredActivities[id] = timestamp;
}
});
this.saveActivities(filteredActivities);
}
private loadActivities(): ActivityMap {
const raw = localStorage.getItem(this.STORAGE_KEY);
return raw ? JSON.parse(raw) : {};
}
private saveActivities(map: ActivityMap): void {
console.log(this.loadActivities())
const now = Date.now();
const cleanMap: ActivityMap = {};
Object.keys(map).forEach((id) => {
if (now - map[id] <= this.MAX_AGE_MS) {
cleanMap[id] = map[id];
}
});
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(cleanMap));
}
}

View File

@ -1,68 +0,0 @@
// https://github.com/grafana/faro-web-sdk/tree/main
// https://github.com/grafana/faro-web-sdk/blob/main/packages/web-tracing/src/faroMetaAttributesSpanProcessor.ts
import type { Context } from '@opentelemetry/api';
import type { ReadableSpan, Span, SpanProcessor } from '@opentelemetry/sdk-trace-web';
// False positive. Package can be resolved.
// eslint-disable-next-line import/no-unresolved
import { ATTR_SESSION_ID } from '@opentelemetry/semantic-conventions/incubating';
import type { Metas } from '@grafana/faro-web-sdk';
export class FaroMetaAttributesSpanProcessor implements SpanProcessor {
constructor(
private processor: SpanProcessor,
private metas: Metas
) {}
forceFlush(): Promise<void> {
return this.processor.forceFlush();
}
onStart(span: Span, parentContext: Context): void {
const session = this.metas.value.session;
if (session?.id) {
span.attributes[ATTR_SESSION_ID] = session.id;
/**
* @deprecated will be removed in the future and has been replaced by ATTR_SESSION_ID (session.id)
*/
span.attributes['session_id'] = session.id;
}
const user = this.metas.value.user ?? {};
if (user.email) {
span.attributes['user.email'] = user.email;
}
if (user.id) {
span.attributes['user.id'] = user.id;
}
if (user.username) {
span.attributes['user.name'] = user.username;
}
if (user.fullName) {
span.attributes['user.full_name'] = user.fullName;
}
if (user.roles) {
span.attributes['user.roles'] = user.roles.split(',').map((role) => role.trim());
}
if (user.hash) {
span.attributes['user.hash'] = user.hash;
}
this.processor.onStart(span, parentContext);
}
onEnd(span: ReadableSpan): void {
this.processor.onEnd(span);
}
shutdown(): Promise<void> {
return this.processor.shutdown();
}
}

View File

@ -1,50 +0,0 @@
// https://github.com/grafana/faro-web-sdk/tree/main
// https://github.com/grafana/faro-web-sdk/blob/main/packages/web-tracing/src/faroUserActionSpanProcessor.ts
import { type Context, SpanKind } from '@opentelemetry/api';
import type { ReadableSpan, Span, SpanProcessor } from '@opentelemetry/sdk-trace-web';
import { USER_ACTION_CANCEL, USER_ACTION_END, USER_ACTION_START } from '@grafana/faro-core';
import { apiMessageBus, type UserActionStartMessage } from '@grafana/faro-web-sdk';
export class FaroUserActionSpanProcessor implements SpanProcessor {
message: UserActionStartMessage | undefined;
constructor(private processor: SpanProcessor) {
apiMessageBus.subscribe((msg) => {
if (msg.type === USER_ACTION_START) {
this.message = msg;
return;
}
if ([USER_ACTION_END, USER_ACTION_CANCEL].includes(msg.type)) {
this.message = undefined;
}
});
}
forceFlush(): Promise<void> {
return this.processor.forceFlush();
}
onStart(span: Span, parentContext: Context): void {
if (span.kind === SpanKind.CLIENT) {
// If the span is created when the message object is available it is created before the user action timeout has been reached so it belongs to the user-action.
// In this case we can add the user action name and parentId to the span attributes.
// If the span is created after the user action timeout span, the message object will be undefined which means the action has been cancelled or is ended.
if (this.message) {
span.attributes['faro.action.user.name'] = this.message?.name;
span.attributes['faro.action.user.parentId'] = this.message?.parentId;
}
}
this.processor.onStart(span, parentContext);
}
onEnd(span: ReadableSpan): void {
this.processor.onEnd(span);
}
shutdown(): Promise<void> {
return this.processor.shutdown();
}
}

View File

@ -1,122 +0,0 @@
import {
ConsoleTransport,
FetchTransport,
getWebInstrumentations,
initializeFaro,
} from '@grafana/faro-web-sdk';
import {
FaroTraceExporter,
TracingInstrumentation,
} from '@grafana/faro-web-tracing';
import { context, trace } from '@opentelemetry/api';
import { getWebAutoInstrumentations } from '@opentelemetry/auto-instrumentations-web';
import { ZoneContextManager } from '@opentelemetry/context-zone';
import { W3CTraceContextPropagator } from '@opentelemetry/core';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import { Resource } from '@opentelemetry/resources';
import {
BatchSpanProcessor,
ConsoleSpanExporter,
SimpleSpanProcessor,
WebTracerProvider,
} from '@opentelemetry/sdk-trace-web';
import {
ATTR_SERVICE_NAME,
ATTR_SERVICE_VERSION,
} from '@opentelemetry/semantic-conventions';
import { environment } from '../../../environments/environment';
import { FaroMetaAttributesSpanProcessor } from './external/faro-meta-attributes-span-processor';
import { FaroUserActionSpanProcessor } from './external/faro-user-action-span-processor';
const VERSION = '1.0.0';
export function faroInitializer(): Function {
return async () => {
const faro = initializeFaro({
instrumentations: [
...getWebInstrumentations({
captureConsole: true,
captureConsoleDisabledLevels: [],
}),
new TracingInstrumentation(),
],
transports: [
new FetchTransport({
url: environment.telemetry.otlpEndpoint,
apiKey: environment.telemetry.collectorSecret,
}),
new ConsoleTransport(),
],
trackResources: true,
trackUserActionsPreview: true,
app: {
name: environment.telemetry.serviceName,
version: VERSION,
namespace: 'avicenna',
},
});
const resource = Resource.default().merge(
new Resource({
[ATTR_SERVICE_NAME]: environment.telemetry.serviceName,
[ATTR_SERVICE_VERSION]: VERSION,
})
);
const provider = new WebTracerProvider({
resource,
spanProcessors: [
new SimpleSpanProcessor(new ConsoleSpanExporter()),
new FaroUserActionSpanProcessor(
new FaroMetaAttributesSpanProcessor(
new BatchSpanProcessor(new FaroTraceExporter({ ...faro })),
{
add: () => {},
remove: () => {},
addListener: () => {},
removeListener: () => {},
value: {
session: {
id: 'session-id',
},
user: {
email: 'email',
id: 'id',
username: 'user-short-name',
fullName: 'user-full-name',
roles: 'admin, editor,viewer',
hash: 'hash',
},
},
}
)
),
],
});
provider.register({
propagator: new W3CTraceContextPropagator(),
contextManager: new ZoneContextManager(),
});
const ignoreUrls = [environment.telemetry.otlpEndpoint];
registerInstrumentations({
instrumentations: [
getWebAutoInstrumentations({
// not needed to add the following, but it better shows the intention
'@opentelemetry/instrumentation-document-load': {},
'@opentelemetry/instrumentation-user-interaction': {},
'@opentelemetry/instrumentation-fetch': {
ignoreUrls,
},
'@opentelemetry/instrumentation-xml-http-request': {
ignoreUrls,
},
}),
],
});
faro.api.initOTEL(trace, context);
};
}

View File

@ -17,7 +17,7 @@ import { DashboardAccountingService } from '../dashboard-accounting.service';
<div class="flex justify-around items-center"> <div class="flex justify-around items-center">
<div class="flex flex-col justify-center items-center"> <div class="flex flex-col justify-center items-center">
<div class="text-xl">{{ kpi()?.count }}</div> <div class="text-xl">{{ kpi()?.count }}</div>
<div class="text-sm">Tickets</div> <div class="text-sm">Touren</div>
</div> </div>
@if(kpi()?.revenueSum && accountingState() !== AccountingState.Billed) { @if(kpi()?.revenueSum && accountingState() !== AccountingState.Billed) {
<div class="flex flex-col justify-center items-center"> <div class="flex flex-col justify-center items-center">

View File

@ -22,34 +22,41 @@ import {
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'dks-accounting-item', selector: 'dks-accounting-item',
template: ` <div class="flex justify-between"> template: `
<div class="flex items-center"> <div class="flex justify-between">
<button <div class="flex items-center">
mat-icon-button <button
(click)="notifyCopy($event)" mat-icon-button
[cdkCopyToClipboard]="tour.operationId" (click)="notifyCopy($event)"
> [cdkCopyToClipboard]="tour.operationId"
<mat-icon class="text-sm">content_copy</mat-icon> >
</button> <mat-icon class="text-sm">content_copy</mat-icon>
</button>
<a
class="dispolive-btn"
target="_blank"
[href]="'https://avicenna.dispolive.de/fahrbericht/' + tour.id"
>
{{ tour.operationId }} {{ tour.operationId }}
</div> </a>
<span>{{ tour.ordinanceType }}</span>
</div> </div>
<div> <span>{{ tour.ordinanceType }}</span>
@if (accountingState !== AccountingState.Billed) { </div>
<span <div>
[title]="!tour.healthInsurance ? 'Keine Krankenkasse hinterlegt' : ''" @if (accountingState !== AccountingState.Billed) {
[ngClass]="{ 'bg-yellow-200': !tour.healthInsurance }" <span
> [title]="!tour.healthInsurance ? 'Keine Krankenkasse hinterlegt' : ''"
{{ tour.revenue | currency : 'EUR' : 'symbol' : '1.2-2' : 'de-DE' }} [ngClass]="{ 'bg-yellow-200': !tour.healthInsurance }"
</span> >
} @else { {{ tour.revenue | currency : 'EUR' : 'symbol' : '1.2-2' : 'de-DE' }}
{{ </span>
tour.revenueDeviation | currency : 'EUR' : 'symbol' : '1.2-2' : 'de-DE' } @else {
}} {{
} tour.revenueDeviation | currency : 'EUR' : 'symbol' : '1.2-2' : 'de-DE'
</div> }}
`, }
</div>
`,
host: { host: {
class: 'flex flex-col rounded border p-2 m-1 bg-white cursor-pointer', class: 'flex flex-col rounded border p-2 m-1 bg-white cursor-pointer',
}, },
@ -71,8 +78,8 @@ import {
MatBadgeModule, MatBadgeModule,
ClipboardModule, ClipboardModule,
NgClass, NgClass,
CurrencyPipe CurrencyPipe,
], ],
}) })
export class AccountingItemComponent implements AfterViewInit { export class AccountingItemComponent implements AfterViewInit {
toast = inject(HotToastService); toast = inject(HotToastService);

View File

@ -1,4 +1,3 @@
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
@ -11,30 +10,28 @@ import {
AccountingState, AccountingState,
} from '../../../core/data-access/graphql/generated/generated'; } from '../../../core/data-access/graphql/generated/generated';
import { AccountingLaneHeaderFilterComponent } from './accounting-lane-header-filter.component'; import { AccountingLaneHeaderFilterComponent } from './accounting-lane-header-filter.component';
import { RoleDirective } from 'src/app/core/components/ms-authentication/role.directive';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'dks-accounting-lane-header', selector: 'dks-accounting-lane-header',
template: `<div></div> template: `<div></div>
@switch (state) { @switch (state) { @case(AccountingState.Pending) {
@case (AccountingState.Billable) { <div>Offen</div>
} @case (AccountingState.Billable) {
<div>Abrechenbar</div> <div>Abrechenbar</div>
} } @case (AccountingState.PreparedForBilling) {
@case (AccountingState.PreparedForBilling) { <div>für Abrechnung vorbereitet</div>
} @case (AccountingState.Billed) {
<div>Abgerechnet (mit Abweichung)</div>
} }
<div> <div>
für Abrechnung vorbereitet <dks-accounting-lane-header-filter
</div> *dksRole="['admin']"
} [state]="state"
@case (AccountingState.Billed) { (filterChange)="filterChanged($event)"
<div> ></dks-accounting-lane-header-filter>
Abgerechnet (mit Abweichung) </div>`,
</div>
}
}
<dks-accounting-lane-header-filter
[state]="state"
(filterChange)="filterChanged($event)"
></dks-accounting-lane-header-filter>`,
host: { host: {
class: 'flex items-center justify-between p-2 bg-secondary-container', class: 'flex items-center justify-between p-2 bg-secondary-container',
}, },
@ -45,7 +42,7 @@ import { AccountingLaneHeaderFilterComponent } from './accounting-lane-header-fi
} }
`, `,
], ],
imports: [AccountingLaneHeaderFilterComponent], imports: [AccountingLaneHeaderFilterComponent, RoleDirective],
}) })
export class AccountingLaneHeaderComponent { export class AccountingLaneHeaderComponent {
@Input() state!: AccountingState; @Input() state!: AccountingState;

View File

@ -98,7 +98,6 @@ export class AccountingTourCostBreakdownComponent {
servicePositions: any[] | undefined, servicePositions: any[] | undefined,
idx: number idx: number
): number { ): number {
console.log(servicePositions, idx);
return (servicePositions ?? []) return (servicePositions ?? [])
.slice(0, idx + 1) .slice(0, idx + 1)
.reduce((sum, { value }) => (sum += value), 0); .reduce((sum, { value }) => (sum += value), 0);

View File

@ -10,6 +10,10 @@ import { AccountingLaneComponent } from './accounting-lane/accounting-lane.compo
<div class="col-span-1 md:col-span-3 flex justify-end items-center"> <div class="col-span-1 md:col-span-3 flex justify-end items-center">
<!-- Action Buttons --> <!-- Action Buttons -->
</div> </div>
<app-accounting-info-card
class="self-center"
[accountingState]="AccountingState.Pending"
></app-accounting-info-card>
<app-accounting-info-card <app-accounting-info-card
class="self-center" class="self-center"
[accountingState]="AccountingState.Billable" [accountingState]="AccountingState.Billable"
@ -18,20 +22,16 @@ import { AccountingLaneComponent } from './accounting-lane/accounting-lane.compo
class="self-center" class="self-center"
[accountingState]="AccountingState.PreparedForBilling" [accountingState]="AccountingState.PreparedForBilling"
></app-accounting-info-card> ></app-accounting-info-card>
<app-accounting-info-card
class="self-center"
[accountingState]="AccountingState.Billed"
></app-accounting-info-card>
<div> <div>
<dks-accounting-lane <dks-accounting-lane
[accountingState]="AccountingState.Billable" [accountingState]="AccountingState.Pending"
></dks-accounting-lane> ></dks-accounting-lane>
</div> </div>
<dks-accounting-lane <dks-accounting-lane
[accountingState]="AccountingState.PreparedForBilling" [accountingState]="AccountingState.Billable"
></dks-accounting-lane> ></dks-accounting-lane>
<dks-accounting-lane <dks-accounting-lane
[accountingState]="AccountingState.Billed" [accountingState]="AccountingState.PreparedForBilling"
></dks-accounting-lane> ></dks-accounting-lane>
</section>`, </section>`,
styles: [ styles: [

View File

@ -10,7 +10,6 @@ import {
PaginatedAnomaly, PaginatedAnomaly,
PaginatedAnomalyGroup, PaginatedAnomalyGroup,
} from '../../core/data-access/graphql/generated/generated'; } from '../../core/data-access/graphql/generated/generated';
import { faro, LogLevel } from '@grafana/faro-web-sdk';
@Injectable() @Injectable()
export class AnomaliesService { export class AnomaliesService {
@ -90,12 +89,6 @@ export class AnomaliesService {
'Leider ist etwas schief gelaufen, versuchen Sie es bitte nochmal' 'Leider ist etwas schief gelaufen, versuchen Sie es bitte nochmal'
); );
} else { } else {
faro.api.pushLog([`Sleep anomaly ${anomalyId}`], {
context: {
payload: anomalyId,
},
level: LogLevel.TRACE,
})
this.toast.success( this.toast.success(
'Die Anomalie wurde für eine Stunde stummgeschaltet!' 'Die Anomalie wurde für eine Stunde stummgeschaltet!'
); );
@ -119,12 +112,6 @@ export class AnomaliesService {
'Leider ist etwas schief gelaufen, versuchen Sie es bitte nochmal' 'Leider ist etwas schief gelaufen, versuchen Sie es bitte nochmal'
); );
} else { } else {
faro.api.pushLog([`Accept anomaly ${anomalyId}`], {
context: {
payload: anomalyId,
},
level: LogLevel.TRACE,
})
this.toast.success('Die Anomalie wurde akzeptiert!'); this.toast.success('Die Anomalie wurde akzeptiert!');
} }
}) })

View File

@ -76,6 +76,5 @@ export class AnomalyViewComponent {
} }
ngOnInit() { ngOnInit() {
console.log(this.anomalyId());
} }
} }

View File

@ -11,7 +11,6 @@ import {
import { ManagerInfoCardComponent } from '../manager/components/manager-info-card/manager-info-card.component'; import { ManagerInfoCardComponent } from '../manager/components/manager-info-card/manager-info-card.component';
import { TicketStateInfoCardComponent } from '../ticket-system/ticket-state-info-card/ticket-state-info-card.component'; import { TicketStateInfoCardComponent } from '../ticket-system/ticket-state-info-card/ticket-state-info-card.component';
import { LinkCardComponent } from './link-card/link-card.component'; import { LinkCardComponent } from './link-card/link-card.component';
import { SkeletonDirective } from '../../core/components/skeleton-rect/skeleton.directive';
@Component({ @Component({
selector: 'app-home', selector: 'app-home',
@ -22,7 +21,6 @@ import { SkeletonDirective } from '../../core/components/skeleton-rect/skeleton.
LinkCardComponent, LinkCardComponent,
MatDividerModule, MatDividerModule,
CurrencyPipe, CurrencyPipe,
SkeletonDirective,
], ],
template: ` template: `
<div class="flex flex-col m-0 md:m-4"> <div class="flex flex-col m-0 md:m-4">

View File

@ -28,7 +28,6 @@ export class TrendGraphComponent {
private chart?: ECharts; private chart?: ECharts;
public readonly options = computed<EChartsOption>(() => { public readonly options = computed<EChartsOption>(() => {
console.log(this.data());
return this.generateEchartsOptions(); return this.generateEchartsOptions();
}); });

View File

@ -7,11 +7,10 @@ import {
Metric, Metric,
} from '../../../../core/data-access/graphql/generated/generated'; } from '../../../../core/data-access/graphql/generated/generated';
import { ManagerDashboardService } from '../../manager-dashboard.service'; import { ManagerDashboardService } from '../../manager-dashboard.service';
import { SkeletonDirective } from '../../../../core/components/skeleton-rect/skeleton.directive';
@Component({ @Component({
selector: 'app-manager-info-card', selector: 'app-manager-info-card',
imports: [InfoCardModule, DecimalPipe, SkeletonDirective], imports: [InfoCardModule, DecimalPipe],
templateUrl: './manager-info-card.component.html', templateUrl: './manager-info-card.component.html',
styleUrl: './manager-info-card.component.scss', styleUrl: './manager-info-card.component.scss',
}) })

View File

@ -1,29 +1,25 @@
@if (ticket) { @if (ticket) {
<div class="flex justify-between"> <div class="flex justify-between">
<span class="text-base"> <span class="text-base">
{{ ticket.tours.at(0)?.ordinanceType }} {{ ticket.tours.at(0)?.ordinanceType }}
</span> </span>
<div> <div>
@if (ticket.errors.length) { @if (ticket.errors.length) {
<mat-icon <mat-icon
[matBadge]="ticket.errors.length" [matBadge]="ticket.errors.length"
matBadgeColor="warn" matBadgeColor="warn"
[title]="ticket.errors.length + ' Anmerkungen'" [title]="ticket.errors.length + ' Anmerkungen'"
aria-hidden="false" aria-hidden="false"
>
error
</mat-icon>
}
</div>
</div>
<span
>{{ ticket.tours.at(0)?.patientName }}
{{ ticket.tours.at(0)?.patientSurname }}</span
> >
<span error
>{{ getToursLength() }} Tour{{ </mat-icon>
getToursLength() > 1 ? 'en' : ''
}}</span
>
<span class="text-xs self-end">{{ getDistance(ticket.createdAt) }}</span>
} }
</div>
</div>
<span>
{{ ticket.tours.at(0)?.patientName }}
{{ ticket.tours.at(0)?.patientSurname }}
</span>
<span>{{ getToursLength() }} Tour{{ getToursLength() > 1 ? "en" : "" }}</span>
<span class="text-xs self-end">{{ getDistance(ticket.createdAt) }}</span>
}

View File

@ -1,8 +1,9 @@
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
HostBinding, HostBinding,
inject,
input,
Input, Input,
OnInit, OnInit,
} from '@angular/core'; } from '@angular/core';
@ -24,6 +25,10 @@ import CategoryColors from './category-colors';
host: { host: {
class: class:
'flex flex-col rounded-sm border-l-2 border-r-8 p-2 m-2 bg-white cursor-pointer', 'flex flex-col rounded-sm border-l-2 border-r-8 p-2 m-2 bg-white cursor-pointer',
'[class.bg-gray-100]': 'hasActivity',
'[class.bg-white]': '!hasActivity',
'[style.border-left-color]': 'leftBorderColor',
'[style.border-right-color]': 'rightBorderColor',
}, },
styles: [ styles: [
` `
@ -35,11 +40,10 @@ import CategoryColors from './category-colors';
imports: [MatIconModule, MatBadgeModule], imports: [MatIconModule, MatBadgeModule],
}) })
export class TicketItemComponent implements OnInit { export class TicketItemComponent implements OnInit {
hasActivity = input(false)
@Input() ticket!: Ticket & { tours: Tour[] }; @Input() ticket!: Ticket & { tours: Tour[] };
@HostBinding('style.border-left-color')
leftBorderColor = '#ffffff'; leftBorderColor = '#ffffff';
@HostBinding('style.border-right-color')
rightBorderColor = '#ffffff'; rightBorderColor = '#ffffff';
@HostBinding('style.background-color') @HostBinding('style.background-color')

View File

@ -1,4 +1,3 @@
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
@ -11,50 +10,42 @@ import {
TicketValidationState, TicketValidationState,
} from '../../../core/data-access/graphql/generated/generated'; } from '../../../core/data-access/graphql/generated/generated';
import { TicketLaneHeaderFilterComponent } from './ticket-lane-header-filter.component'; import { TicketLaneHeaderFilterComponent } from './ticket-lane-header-filter.component';
import { RoleDirective } from 'src/app/core/components/ms-authentication/role.directive';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'dks-ticket-lane-header', selector: 'dks-ticket-lane-header',
template: `<div></div> template: `<div></div>
@switch (state) { @switch (state) { @case (TicketValidationState.Created) {
@case (TicketValidationState.Created) { <div>Daten ergänzen</div>
<div>Touren angenommen</div> } @case (TicketValidationState.UserInteractionRequired) {
} <div>Genehmigung einleiten</div>
@case (TicketValidationState.UserInteractionRequired) { } @case (TicketValidationState.TdUncertain) {
<div> <div>Transportschein organisieren</div>
Genehmigung/Transportschein } @case (TicketValidationState.DocumentsMissing) {
</div> <div>Dokumente prüfen</div>
} } @case (TicketValidationState.Archived) {
@case (TicketValidationState.TdUncertain) {
<div>
Transportschein unklar
</div>
}
@case (TicketValidationState.DocumentsMissing) {
<div>
Dokumente fehlen
</div>
}
@case (TicketValidationState.Archived) {
<div>Archiv</div> <div>Archiv</div>
} } }
} <div>
<dks-ticket-lane-header-filter <dks-ticket-lane-header-filter
[state]="state" *dksRole="['admin']"
(filterChange)="filterChanged($event)" [state]="state"
></dks-ticket-lane-header-filter>`, (filterChange)="filterChanged($event)"
host: { ></dks-ticket-lane-header-filter>
class: 'flex items-center justify-between p-2', </div>`,
}, host: {
styles: [ class: 'flex items-center justify-between p-2',
` },
styles: [
`
:host { :host {
color: var(--mat-sys-on-secondary-container); color: var(--mat-sys-on-secondary-container);
background-color: var(--mat-sys-secondary-container); background-color: var(--mat-sys-secondary-container);
} }
`, `,
], ],
imports: [TicketLaneHeaderFilterComponent] imports: [TicketLaneHeaderFilterComponent, RoleDirective],
}) })
export class TicketLaneHeaderComponent { export class TicketLaneHeaderComponent {
@Input() state!: TicketValidationState; @Input() state!: TicketValidationState;

View File

@ -13,6 +13,7 @@
class="mat-elevation-z4 animate-fadeIn" class="mat-elevation-z4 animate-fadeIn"
(click)="openTicketView(ticket)" (click)="openTicketView(ticket)"
[ticket]="ticket" [ticket]="ticket"
[hasActivity]="activityService.isDirty(ticket.id)"
> >
</dks-ticket-item> </dks-ticket-item>
</cdk-virtual-scroll-viewport> </cdk-virtual-scroll-viewport>

View File

@ -5,6 +5,7 @@ import {
import { AsyncPipe } from '@angular/common'; import { AsyncPipe } from '@angular/common';
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef,
Component, Component,
Input, Input,
ViewChild, ViewChild,
@ -31,6 +32,7 @@ import { TicketItemComponent } from '../ticket-item/ticket-item.component';
import { TicketViewComponent } from '../ticket-view/ticket-view.component'; import { TicketViewComponent } from '../ticket-view/ticket-view.component';
import { TicketsService } from '../tickets.service'; import { TicketsService } from '../tickets.service';
import { TicketLaneHeaderComponent } from './ticket-lane-header.component'; import { TicketLaneHeaderComponent } from './ticket-lane-header.component';
import { ActivityService } from '../../../core/services/activity.service';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@ -58,6 +60,8 @@ import { TicketLaneHeaderComponent } from './ticket-lane-header.component';
export class TicketLaneComponent { export class TicketLaneComponent {
private dialog = inject(DialogService); private dialog = inject(DialogService);
private ticketsService = inject(TicketsService); private ticketsService = inject(TicketsService);
activityService = inject(ActivityService);
private cdr = inject(ChangeDetectorRef);
private ticketState$ = new BehaviorSubject<TicketValidationState | undefined>( private ticketState$ = new BehaviorSubject<TicketValidationState | undefined>(
undefined undefined
@ -129,10 +133,15 @@ export class TicketLaneComponent {
} }
openTicketView(ticket: Ticket & { tours: Tour[] }): void { openTicketView(ticket: Ticket & { tours: Tour[] }): void {
this.activityService.markAsDirty(ticket.id);
const dialogRef = this.dialog.open(TicketViewComponent, { const dialogRef = this.dialog.open(TicketViewComponent, {
data: ticket, data: ticket,
width: '40vw', width: '40vw',
minHeight: '400px', minHeight: '400px',
enableClose: {
backdrop: true,
escape: false,
},
}); });
dialogRef.afterClosed$ dialogRef.afterClosed$

View File

@ -1,7 +1,7 @@
<!-- <ng-container <!-- <ng-container
*skeleton="isLoading(); repeat: 5; className: 'mr-2'; height: '50px'" *skeleton="true; repeat: 1; className: 'mr-2'; height: '100px'"
></ng-container> --> ></ng-container> -->
@if (kpi()?.main?.label) { @if (kpi()?.main?.label != '') {
<app-info-card <app-info-card
[chartValue]="kpi()?.main?.value!" [chartValue]="kpi()?.main?.value!"
[chartLabel]="kpi()?.main?.label!" [chartLabel]="kpi()?.main?.label!"
@ -19,4 +19,15 @@
</app-info-card-footer> </app-info-card-footer>
} }
</app-info-card> </app-info-card>
} @else {
<app-info-card>
<div class="flex justify-around items-center">
@for(metric of metrics(); track metric.label) {
<div class="flex flex-col justify-center items-center">
<div class="text-xl">{{ metric.value }}</div>
<div class="text-sm">{{ metric.label }}</div>
</div>
}
</div>
</app-info-card>
} }

View File

@ -8,11 +8,10 @@ import {
TicketValidationState, TicketValidationState,
} from '../../../core/data-access/graphql/generated/generated'; } from '../../../core/data-access/graphql/generated/generated';
import { TicketsService } from '../tickets.service'; import { TicketsService } from '../tickets.service';
import { SkeletonDirective } from '../../../core/components/skeleton-rect/skeleton.directive';
@Component({ @Component({
selector: 'app-ticket-state-info-card', selector: 'app-ticket-state-info-card',
imports: [InfoCardModule, DecimalPipe, SkeletonDirective], imports: [InfoCardModule, DecimalPipe],
templateUrl: './ticket-state-info-card.component.html', templateUrl: './ticket-state-info-card.component.html',
styleUrl: './ticket-state-info-card.component.scss', styleUrl: './ticket-state-info-card.component.scss',
}) })

View File

@ -0,0 +1,133 @@
import {
ChangeDetectionStrategy,
Component,
computed,
inject,
input,
output,
signal,
} from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button';
import { MatOptionModule } from '@angular/material/core';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { TransportDocumentUploadComponent } from 'src/app/core/components/transport-document-upload/transport-document-upload.component';
import {
Ticket,
Tour,
} from 'src/app/core/data-access/graphql/generated/generated';
import { TicketsService } from '../tickets.service';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { tap } from 'rxjs';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'app-ticket-approval-request',
template: `<div class="flex items-center p-3">
<form #form="ngForm" class="flex flex-col w-full">
<div class="flex flex-col p-2">
<label>Transportschein hochladen</label>
<app-transport-document-upload
[ticketId]="ticket().id"
(uploaded)="activateRecipientInput()"
class="flex-1"
></app-transport-document-upload>
</div>
<mat-form-field class="p-2">
<mat-label>Empfänger Adresse der Krankenkasse</mat-label>
<input
type="email"
name="approval"
matInput
[readonly]="!allowRecipientInput()"
[ngModel]="address()"
(ngModelChange)="address.set($event)"
[matAutocomplete]="auto"
required
email
data-bwignore="true"
data-1p-ignore="true"
data-lpignore="true"
/>
<mat-autocomplete #auto="matAutocomplete">
@for (option of filteredAddresses(); track option) {
<mat-option [value]="option">{{ option }}</mat-option>
}
</mat-autocomplete>
</mat-form-field>
<button
mat-flat-button
color="primary"
[disabled]="form.invalid"
(click)="saveForm()"
>
Genehmigung anfragen
</button>
</form>
</div>`,
imports: [
MatButtonModule,
MatIconModule,
MatInputModule,
MatFormFieldModule,
MatOptionModule,
MatAutocompleteModule,
FormsModule,
TransportDocumentUploadComponent,
],
})
export class TicketApprovalRequestComponent {
ticketService = inject(TicketsService);
ticket = input.required<Ticket & { tours: Tour[] }>();
approvalRequested = output<void>()
allowRecipientInput = signal(false);
addresses = [
'ktw-genehmigung@nds.aok.de',
'service@nordost.aok.de',
'kontakt@nw.aok.de',
'aok@rh.aok.de',
'service@barmer.de',
'service@tk.de',
'service@dak.de',
'kontakt@hek.de',
'info@hkk.de',
'Service@ikk-classic.de',
'kundenservice@ikk-gesundplus.de',
'info@bkk-firmus.de',
'info@bkk-linde.de',
'service@bahn-bkk.de',
'zentrale@kbs.de',
'service@viactiv.de',
'mail@securvita-bkk.de',
'kundencenter@vividabkk.de',
'info@service.mobil-krankenkasse.de',
'info@novitas-bkk.de',
'service@kkh.de',
];
address = signal('');
filteredAddresses = computed(() =>
this.addresses.filter((address) =>
address.toLowerCase().includes(this.address())
)
);
saveForm() {
this.ticketService
.sendApprovalRequest(this.ticket().id, this.address())
.pipe(
tap(() => this.approvalRequested.emit())
)
.subscribe();
}
activateRecipientInput() {
this.allowRecipientInput.set(true);
}
}

View File

@ -1,5 +1,5 @@
import { NgClass } from '@angular/common'; import { NgClass } from '@angular/common';
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { ChangeDetectionStrategy, Component, input, Input } from '@angular/core';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatListModule } from '@angular/material/list'; import { MatListModule } from '@angular/material/list';
import { import {
@ -9,95 +9,69 @@ import {
} from '../../../core/data-access/graphql/generated/generated'; } from '../../../core/data-access/graphql/generated/generated';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'dks-ticket-overview', selector: 'dks-ticket-overview',
template: `<div class="grid grid-cols-4"> template: `<div class="grid grid-cols-4">
<mat-list class="col-span-2" role="list"> <mat-list class="col-span-2" role="list">
<div mat-subheader class="text-sm">Ticket Informationen</div> <div mat-subheader class="text-sm">Ticket Informationen</div>
<mat-list-item role="listitem"> <mat-list-item role="listitem">
Typ: Typ:
<span class="font-bold"> <span class="font-bold">
{{ ticket.tours[0].type }} {{ ticket().tours[0].type }}
</span> </span>
</mat-list-item> </mat-list-item>
<mat-list-item role="listitem"> <mat-list-item role="listitem">
Kategorie: Kategorie:
<span class="font-bold"> <span class="font-bold">
{{ ticket.tours[0].category }} {{ ticket().tours[0].category }}
</span> </span>
</mat-list-item> </mat-list-item>
</mat-list> </mat-list>
@if (ticket.approvalState) { @if (ticket().approvalState) {
<mat-list class="col-span-2" role="list"> <mat-list class="col-span-2" role="list">
<div mat-subheader class="text-sm">Gehnemigungsinformationen</div> <div mat-subheader class="text-sm">Gehnemigungsinformationen</div>
<mat-list-item role="listitem"> <mat-list-item role="listitem">
@switch (ticket.approvalState) { @switch (ticket().approvalState) { @case (ApprovalState.Free) {
@case (ApprovalState.Free) { <span>Genehmigungsfrei</span>
<span>Genehmigungsfrei</span> } @case (ApprovalState.Unknown) {
} <span title="Automatische Prüfung nicht möglich"
@case (ApprovalState.Unknown) { >Automatische Prüfung nicht möglich</span
<span >
title="Automatische Prüfung nicht möglich" } @case (ApprovalState.Required) {
>Automatische Prüfung nicht möglich</span <span>Genehmigung benötigt</span>
> } @case (ApprovalState.Requested) {
} <span>Genehmigung angefragt</span>
@case (ApprovalState.Required) { } @case (ApprovalState.Approved) {
<span <span>Genehmigung eingegangen</span>
>Genehmigung benötigt</span } }
> </mat-list-item>
} </mat-list>
@case (ApprovalState.Requested) { } @if (ticket().errors.length > 0 || ticket().notes.length > 0) {
<span <mat-list class="col-span-2 max-h-96 overflow-y-auto" role="list">
>Genehmigung angefragt</span <div mat-subheader class="text-sm">Ticket Anmerkungen</div>
> @for (error of ticket().errors; track error) {
} <mat-list-item lines="2" role="listitem">
@case (ApprovalState.Approved) { <mat-icon matListItemIcon class="!text-red-600">error</mat-icon>
<span <span matListItemTitle>{{ error.split(':').at(0) }}</span>
>Genehmigung eingegangen</span <span [title]="error.split(':').at(1)">{{
> error.split(':').at(1)
} }}</span>
} </mat-list-item>
</mat-list-item> }
</mat-list> <!-- @for (note of ticket.notes; track note) {
} <mat-list-item lines="2" role="listitem">
@if (ticket.errors.length > 0 || ticket.notes.length > 0) { <mat-icon matListItemIcon class="!text-yellow-400">warning</mat-icon>
<mat-list <span matListItemTitle>{{ note.split(':').at(0) }}</span>
class="col-span-2 max-h-96 overflow-y-auto" <span [title]="note.split(':').at(1)">{{ note.split(':').at(1) }}</span>
role="list" </mat-list-item>
> } -->
<div mat-subheader class="text-sm">Ticket Anmerkungen</div> </mat-list>
@for (error of ticket.errors; track error) { }
<mat-list-item </div>`,
lines="2" imports: [MatListModule, MatIconModule],
role="listitem"
>
<mat-icon matListItemIcon class="!text-red-600">error</mat-icon>
<span matListItemTitle>{{ error.split(':').at(0) }}</span>
<span [title]="error.split(':').at(1)">{{
error.split(':').at(1)
}}</span>
</mat-list-item>
}
@for (note of ticket.notes; track note) {
<mat-list-item
lines="2"
role="listitem"
>
<mat-icon matListItemIcon class="!text-yellow-400">warning</mat-icon>
<span matListItemTitle>{{ note.split(':').at(0) }}</span>
<span [title]="note.split(':').at(1)">{{ note.split(':').at(1) }}</span>
</mat-list-item>
}
</mat-list>
}
</div>`,
imports: [
MatListModule,
MatIconModule
]
}) })
export class TicketOverviewComponent { export class TicketOverviewComponent {
@Input() ticket!: Ticket & { tours: Tour[] }; ticket = input.required<Ticket & { tours: Tour[] }>()
ApprovalState = ApprovalState; ApprovalState = ApprovalState;
} }

View File

@ -4,7 +4,7 @@ import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
Input, Input,
OnInit OnInit,
} from '@angular/core'; } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox';
@ -14,11 +14,12 @@ import {
Ticket, Ticket,
Tour, Tour,
} from '../../../core/data-access/graphql/generated/generated'; } from '../../../core/data-access/graphql/generated/generated';
import { TransportDocumentUploadComponent } from '../../../core/components/transport-document-upload/transport-document-upload.component';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'dks-ticket-tours', selector: 'dks-ticket-tours',
template: ` <div class="max-h-96 overflow-y-auto"> template: ` <div class="max-h-96 overflow-y-auto">
<table mat-table [dataSource]="dataSource" class="w-full"> <table mat-table [dataSource]="dataSource" class="w-full">
<ng-container matColumnDef="select"> <ng-container matColumnDef="select">
<th mat-header-cell *matHeaderCellDef> <th mat-header-cell *matHeaderCellDef>
@ -41,7 +42,14 @@ import {
<ng-container matColumnDef="operationId"> <ng-container matColumnDef="operationId">
<th mat-header-cell *matHeaderCellDef>Einsatznummer</th> <th mat-header-cell *matHeaderCellDef>Einsatznummer</th>
<td mat-cell *matCellDef="let tour">{{ tour.operationId }}</td> <td mat-cell *matCellDef="let tour">
<a
class="dispolive-btn"
target="_blank"
[href]="'https://avicenna.dispolive.de/fahrbericht/' + tour.id"
>{{ tour.operationId }}</a
>
</td>
</ng-container> </ng-container>
<ng-container matColumnDef="startDate"> <ng-container matColumnDef="startDate">
<th mat-header-cell *matHeaderCellDef>Anfahrt</th> <th mat-header-cell *matHeaderCellDef>Anfahrt</th>
@ -78,13 +86,13 @@ import {
></tr> ></tr>
</table> </table>
</div>`, </div>`,
imports: [ imports: [
NgClass, NgClass,
DatePipe, DatePipe,
MatButtonModule, MatButtonModule,
MatCheckboxModule, MatCheckboxModule,
MatTableModule, MatTableModule,
] ],
}) })
export class TicketToursComponent implements OnInit { export class TicketToursComponent implements OnInit {
@Input() ticket!: Ticket & { tours: Tour[] }; @Input() ticket!: Ticket & { tours: Tour[] };
@ -107,7 +115,6 @@ export class TicketToursComponent implements OnInit {
); );
ngOnInit(): void { ngOnInit(): void {
console.log(this.ticket.tours)
this.dataSource = [...this.ticket.tours] this.dataSource = [...this.ticket.tours]
.filter(({ check }) => check === 0) .filter(({ check }) => check === 0)
.sort( .sort(

View File

@ -1,10 +1,7 @@
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef,
Component, Component,
computed, computed,
effect,
EventEmitter, EventEmitter,
inject, inject,
Input, Input,
@ -19,6 +16,7 @@ import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatRadioModule } from '@angular/material/radio'; import { MatRadioModule } from '@angular/material/radio';
import { AuthService } from '../../../core/components/ms-authentication/auth.service';
import { import {
ApprovalState, ApprovalState,
DocumentInfo, DocumentInfo,
@ -26,94 +24,91 @@ import {
TicketValidationState, TicketValidationState,
Tour, Tour,
} from '../../../core/data-access/graphql/generated/generated'; } from '../../../core/data-access/graphql/generated/generated';
import { AuthService } from '../../../core/components/ms-authentication/auth.service';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'dks-ticket-user-action-required', selector: 'dks-ticket-user-action-required',
template: `<div class="flex flex-col p-3"> template: `<div class="flex flex-col p-3">
<form #form="ngForm" class="flex flex-col w-full"> <form #form="ngForm" class="flex flex-col w-full">
<div class="flex justify-end pt-4"> <div class="flex justify-end pt-4">
<button mat-stroked-button class="mr-2" (click)="form.resetForm()"> <button mat-stroked-button class="mr-2" (click)="form.resetForm()">
Zurücksetzen Zurücksetzen
</button> </button>
<button <button
mat-flat-button mat-flat-button
color="primary" color="primary"
[disabled]="form.invalid" [disabled]="form.invalid"
(click)="saveForm()" (click)="saveForm()"
> >
Speichern Speichern
</button> </button>
</div> </div>
<div class="flex flex-col p-2"> <div class="flex flex-col p-2">
<label>Wird eine Genehmigung benötigt?</label> <label>Wird eine Genehmigung benötigt?</label>
<mat-radio-group <mat-radio-group
[(ngModel)]="formModel().approval.isNeeded" [(ngModel)]="formModel().approval.isNeeded"
name="approvalNeeded" name="approvalNeeded"
required required
> >
<mat-radio-button [value]="false"> Nein </mat-radio-button> <mat-radio-button [value]="false"> Nein </mat-radio-button>
<mat-radio-button [value]="true"> Ja </mat-radio-button> <mat-radio-button [value]="true"> Ja </mat-radio-button>
</mat-radio-group> </mat-radio-group>
</div> </div>
@if ( @if ( (ticket.approvalState === ApprovalState.Required &&
(ticket.approvalState === ApprovalState.Required && formModel().approval.isNeeded) || (ticket.approvalState ===
formModel().approval.isNeeded) || ApprovalState.Unknown && formModel().approval.isNeeded) ) {
(ticket.approvalState === ApprovalState.Unknown && <div class="flex flex-col p-2">
formModel().approval.isNeeded) <label>Wurde die Genehmigung bereits angefragt?</label>
) { <mat-radio-group
<div class="flex flex-col p-2"> [(ngModel)]="formModel().approval.whoRequested"
<label>Wer fragte die Genehmigung an?</label> name="who"
<mat-radio-group required
[(ngModel)]="formModel().approval.whoRequested" >
name="who" <mat-radio-button value="Avicenna"> Ja, durch Avicenna </mat-radio-button>
required <mat-radio-button value="Kunde"> Ja, durch den Kunden </mat-radio-button>
> <mat-radio-button value="no"> Nein, noch nicht </mat-radio-button>
<mat-radio-button value="Avicenna"> Avicenna </mat-radio-button> </mat-radio-group>
<mat-radio-button value="Kunde"> Kunde </mat-radio-button> </div>
</mat-radio-group> <mat-form-field class="p-2">
</div> <mat-label>Name des Sachbearbeiters?</mat-label>
<mat-form-field class="p-2"> <input
<mat-label>Name des Sachbearbeiters?</mat-label> type="text"
<input matInput
type="text" id="staff"
matInput [(ngModel)]="formModel().approval.answeredFrom"
id="staff" name="staff"
[(ngModel)]="formModel().approval.answeredFrom" required
name="staff" />
required </mat-form-field>
/> }
</mat-form-field>
}
<div class="flex flex-col p-2"> <div class="flex flex-col p-2">
<label>Wo befindet sich der Transportschein?</label> <label>Wo befindet sich der Transportschein im Original?</label>
<mat-radio-group <mat-radio-group
[(ngModel)]="formModel().tdLocation" [(ngModel)]="formModel().tdLocation"
name="tdLocation" name="tdLocation"
required required
> >
<mat-radio-button [value]="'company'"> Im Betrieb </mat-radio-button> <mat-radio-button [value]="'company'"> Im Betrieb </mat-radio-button>
<mat-radio-button [value]="'practice'"> <mat-radio-button [value]="'practice'">
In der Praxis In der Praxis
</mat-radio-button> </mat-radio-button>
<mat-radio-button [value]="'customer'"> <mat-radio-button [value]="'customer'">
Beim Kunden Beim Kunden
</mat-radio-button> </mat-radio-button>
<mat-radio-button [value]="'uncertain'"> Unklar </mat-radio-button> <mat-radio-button [value]="'uncertain'"> Unklar </mat-radio-button>
</mat-radio-group> </mat-radio-group>
</div> </div>
</form> </form>
</div>`, </div>`,
imports: [ imports: [
MatButtonModule, MatButtonModule,
MatIconModule, MatIconModule,
MatFormFieldModule, MatFormFieldModule,
MatRadioModule, MatRadioModule,
MatInputModule, MatInputModule,
FormsModule FormsModule,
], ],
}) })
export class TicketUserActionRequiredComponent implements OnInit { export class TicketUserActionRequiredComponent implements OnInit {
@Input() ticket!: Ticket & { tours: Tour[] }; @Input() ticket!: Ticket & { tours: Tour[] };

View File

@ -4,29 +4,35 @@
</h3> </h3>
<hr /> <hr />
<mat-tab-group> <mat-tab-group>
@if(ref.data.currentState !== TicketState.Created) { @if(!!ref.data.approvalState && [ApprovalState.Unknown,
ApprovalState.Required,
ApprovalState.Requested].includes(ref.data.approvalState)) {
<mat-tab label="Genehmigungsanfrage">
<app-ticket-approval-request
[ticket]="ref.data"
(approvalRequested)="ref.close()"
></app-ticket-approval-request>
</mat-tab>
} @if(ref.data.currentState !== TicketState.Created) {
<mat-tab label="Aktionen"> <mat-tab label="Aktionen">
<ng-container> <ng-container>
@switch (ref.data.currentState) { @switch (ref.data.currentState) { @case
@case (TicketState.UserInteractionRequired) { (TicketState.UserInteractionRequired) {
<dks-ticket-user-action-required <dks-ticket-user-action-required
[ticket]="ref.data" [ticket]="ref.data"
(saveFormData)="setDocumentInfo($event)" (saveFormData)="setDocumentInfo($event)"
></dks-ticket-user-action-required> ></dks-ticket-user-action-required>
} } @case (TicketState.TdUncertain) {
@case (TicketState.TdUncertain) { <dks-ticket-td-uncertain
<dks-ticket-td-uncertain [ticket]="ref.data"
[ticket]="ref.data" (saveFormData)="setDocumentInfo($event)"
(saveFormData)="setDocumentInfo($event)" ></dks-ticket-td-uncertain>
></dks-ticket-td-uncertain> } @case (TicketState.DocumentsMissing) {
} <dks-ticket-documents-missing
@case (TicketState.DocumentsMissing) { [ticket]="ref.data"
<dks-ticket-documents-missing (saveFormData)="setDocumentInfo($event)"
[ticket]="ref.data" ></dks-ticket-documents-missing>
(saveFormData)="setDocumentInfo($event)" } }
></dks-ticket-documents-missing>
}
}
</ng-container> </ng-container>
</mat-tab> </mat-tab>
} }

View File

@ -16,6 +16,7 @@ import { TicketOverviewComponent } from './ticket-overview.component';
import { TicketTdUncertainComponent } from './ticket-td-uncertain.component'; import { TicketTdUncertainComponent } from './ticket-td-uncertain.component';
import { TicketToursComponent } from './ticket-tours.component'; import { TicketToursComponent } from './ticket-tours.component';
import { TicketUserActionRequiredComponent } from './ticket-user-action-required.component'; import { TicketUserActionRequiredComponent } from './ticket-user-action-required.component';
import { TicketApprovalRequestComponent } from './ticket-approval-request.component';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@ -27,6 +28,7 @@ import { TicketUserActionRequiredComponent } from './ticket-user-action-required
TicketOverviewComponent, TicketOverviewComponent,
TicketToursComponent, TicketToursComponent,
TicketUserActionRequiredComponent, TicketUserActionRequiredComponent,
TicketApprovalRequestComponent,
TicketTdUncertainComponent, TicketTdUncertainComponent,
TicketDocumentsMissingComponent, TicketDocumentsMissingComponent,
], ],

View File

@ -21,6 +21,7 @@ import {
GetTicketStateMetaQuery, GetTicketStateMetaQuery,
SearchTicketsGQL, SearchTicketsGQL,
SearchTicketsQuery, SearchTicketsQuery,
SendApprovalRequestGQL,
SetDocumentInfoGQL, SetDocumentInfoGQL,
SetDocumentInfoMutationVariables, SetDocumentInfoMutationVariables,
TicketFilterArgs, TicketFilterArgs,
@ -53,11 +54,33 @@ export class TicketsService {
private readonly searchTicketsGql: SearchTicketsGQL, private readonly searchTicketsGql: SearchTicketsGQL,
private readonly setDocumentInfoGql: SetDocumentInfoGQL, private readonly setDocumentInfoGql: SetDocumentInfoGQL,
private readonly ticketStateKpiGql: TicketStateKpiGQL, private readonly ticketStateKpiGql: TicketStateKpiGQL,
private readonly sendApprovalRequestGql: SendApprovalRequestGQL,
private readonly toast: HotToastService private readonly toast: HotToastService
) { ) {
this.updateTickets$.next(); this.updateTickets$.next();
} }
sendApprovalRequest(ticketId: string, email: string) {
return this.sendApprovalRequestGql
.mutate({
email,
ticketId,
})
.pipe(
tap((mutationResult) => {
if (mutationResult.errors) {
this.toast.error(
'Leider ist etwas schief gelaufen, versuchen Sie es bitte nochmal'
);
} else {
this.toast.success(
'Die Genehmigungsanfrage wird versendet.'
);
}
})
);
}
fetchTicketsByState( fetchTicketsByState(
state: TicketValidationState, state: TicketValidationState,
cursor: string, cursor: string,

View File

@ -1,9 +1,19 @@
export const environment = { export const environment = {
production: false, production: false,
api: {
baseUrl: 'http://localhost:3000/'
},
graphQl: { graphQl: {
endpoint: 'http://localhost:3000/graphql', endpoint: 'http://localhost:3000/graphql',
ws: 'ws://localhost:3000/graphql', ws: 'ws://localhost:3000/graphql',
}, },
// msAuth: {
// clientId: 'd1f302fb-9060-4aaa-8730-5156fc31a585',
// authority:
// 'https://login.microsoftonline.com/0d2df07a-f64d-446c-8e4e-2807638a3141',
// redirectUri: 'https://dashboard.avicenna.hamburg/',
// postLogoutRedirectUri: 'http://dashboard.avicenna.hamburg/',
// },
msAuth: { msAuth: {
clientId: '9a4a1e7a-9544-439f-9cd3-3c20066a8a82', clientId: '9a4a1e7a-9544-439f-9cd3-3c20066a8a82',
authority: authority:

View File

@ -1,5 +1,8 @@
export const environment = { export const environment = {
production: true, production: true,
api: {
baseUrl: '{{API_BASEURL}}'
},
graphQl: { graphQl: {
endpoint: '{{GRAPHQL_ENDPOINT}}', endpoint: '{{GRAPHQL_ENDPOINT}}',
ws: '{{GRAPHQL_WS_ENDPOINT}}' ws: '{{GRAPHQL_WS_ENDPOINT}}'

View File

@ -1,5 +1,8 @@
export const environment = { export const environment = {
production: false, production: false,
api: {
baseUrl: ''
},
graphQl: { graphQl: {
endpoint: '', endpoint: '',
ws: '', ws: '',

View File

@ -1,5 +1,12 @@
/* You can add global styles to this file, and also import other style files */
@use "@angular/material" as mat; @use "@angular/material" as mat;
@layer theme, base, components, utilities;
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/preflight.css" layer(base);
@import "tailwindcss/utilities.css";
@import "@angular/cdk/overlay-prebuilt.css";
/* You can add global styles to this file, and also import other style files */
html, html,
body { body {
@ -12,7 +19,8 @@ body {
color: var(--mat-sys-on-surface); color: var(--mat-sys-on-surface);
} }
.dispolive-btn, .dyflexis-btn { .dispolive-btn,
.dyflexis-btn {
a[target="_blank"]::after { a[target="_blank"]::after {
content: " \2924"; /* Unicode-Pfeil */ content: " \2924"; /* Unicode-Pfeil */
font-size: 0.8em; font-size: 0.8em;
@ -38,3 +46,83 @@ body {
opacity: 1; opacity: 1;
} }
} }
@import "@spartan-ng/brain/hlm-tailwind-preset.css";
:root {
--dialog-backdrop-z-index: 999;
color-scheme: light;
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji",
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
:root.dark {
color-scheme: dark;
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.985 0 0);
--sidebar-primary-foreground: oklch(0.205 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

View File

@ -20,7 +20,19 @@
"ES2022", "ES2022",
"dom", "dom",
"esnext.asynciterable" "esnext.asynciterable"
] ],
"baseUrl": "./",
"paths": {
"@spartan-ng/helm/input": [
"src/app/core/base-components/input/src/index.ts"
],
"@spartan-ng/helm/utils": [
"src/app/core/base-components/utils/src/index.ts"
],
"@spartan-ng/helm/label": [
"./src/app/core/base-components/label/src/index.ts"
]
}
}, },
"angularCompilerOptions": { "angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false, "enableI18nLegacyMessageIdFormat": false,

View File

@ -0,0 +1,73 @@
,,,Vertrag,Vertrag,Vertrag,Vertrag,Vertrag,Vertrag,Vertrag,Vertrag,"Stunden innerhalb der Soll-Stunden","Stunden innerhalb der Soll-Stunden","Stunden innerhalb der Soll-Stunden","Stunden innerhalb der Soll-Stunden","Stunden innerhalb der Soll-Stunden","Stunden innerhalb der Soll-Stunden",Ist-/Soll-Stunden,Ist-/Soll-Stunden,Ist-/Soll-Stunden,Ist-/Soll-Stunden,Berechnung,Berechnung,Berechnung,Berechnung,Berechnung,Berechnung,Berechnung,Berechnung,Berechnung,Berechnung,Berechnung,Berechnung,Berechnung,Berechnung,Berechnung,Berechnung,Berechnung,Salden,Salden,Salden,Salden,Salden,Salden,Salden,Salden,Salden,Auszahlen,Auszahlen,Auszahlen,Auszahlen,Auszahlen,Auszahlen,Auszahlen,Auszahlen,Auszahlen,Auszahlen,Auszahlen
,,,,,,,,,,,"Gearbeitete Stunden","Gearbeitete Stunden","Urlaub nehmen","Urlaub nehmen",Krankheitsstunden,Krankheitsstunden,Gesamt,Gesamt,Vertrag,Vertrag,Plus/Min,Mehrstunden,Überstunden,"Zuschlag P-Schein","Krank ohne AU","Krank ohne AU","Nachtzuschlag 25 (25%)","Nachtzuschlag 25 (25%)","Nachtzuschlag 40 (40%)","Nachtzuschlag 40 (40%)","Sonntagszuschlag (50%)","Sonntagszuschlag (50%)","Feiertagszuschlag (125%)","Feiertagszuschlag (125%)","Verpfl.-pauschale (14%)","Samstagzuschlag (15%)","Samstagzuschlag (15%)",Zeitausgleich,Zeitausgleich,Zeitausgleich,Zeitausgleich,Zeitausgleich,Urlaub,Urlaub,Urlaub,Urlaub,Mehrstunden,"Überstunden auszahlen",Fahrtkosten,Verpflegungspauschale,"Zuschlag P-Schein",25%,40%,50%,125%,14%,15%
Persnr.,Nachname,Vorname,Art,P-Schein,"Recht auf Zuschläge?",Start,Tage,Stunden,Max,"Urlaubstage pro Jahr",Tage,Stunden,Tage,Stunden,Tage,Stunden,Tage,Stunden,Tage,Stunden,Stunden,Stunden,Stunden,anzahl,Tage,Stunden,Tage,Stunden,Tage,Stunden,Tage,Stunden,Tage,Stunden,Tage,Tage,Stunden,Start,+/-,Änderungen,Auszahlen,Jetzt,Start,Abbau,Korrektur,Jetzt,Stunden,Stunden,km,Tage,Anzahl,Stunden,Stunden,Stunden,Stunden,Stunden,Stunden
169,Safari,"Abdul Latif ",Fest,Ja,Ja,2025-09-01,5.00,40.00,0.00,20,20,180.4167,5,40,0,0,25,220.41666666667,21.667,173.333,47.08366666667,0.00033333332999064,47.08333333334,20,0,0,0,0,0,0,0,0,2,16.166666666667,16,2,17.35,0.334,47.08366666667,47.08366666667,47.08366666667,0.334,19,5,0,14,0.00033333332999064,47.08333333334,0.00,20,20,0,0,0,16.166666666667,18.9,17.35
93,"Moshtagh Askari",Adel,Fest,Ja,Ja,2025-01-01,5.00,40.00,0.00,20,25,218.4167,0,0,0,0,25,218.41666666667,21.667,173.333,45.08366666667,0.00033333332999064,45.08333333334,25,0,0,0,0,0,0,0,0,2,18.65,20,3,24.95,0.007,45.08366666667,45.08366666667,45.08366666667,0.0069999999999979,31,0,0,31,0.00033333332999064,45.08333333334,0.00,25,25,0,0,0,18.65,14.816666666667,24.95
18,Mihailescu,"Adrian Lucian",Fest,Ja,Ja,2025-09-01,5.00,40.00,0.00,20,21,177.5667,0,0,0,0,21,177.56666666667,21.667,173.333,4.23366666667,0.00033333332999064,4.23333333334,21,0,0,0,0,0,0,0,0,1,8.6333333333333,15,3,24.116666666667,0,4.23366666667,4.23366666667,4.23366666667,0,20,0,0,20,0.00033333332999064,4.23333333334,0.00,18,21,0,0,0,8.6333333333333,11.1,24.116666666667
,"Esmaeili Dashtbayaz",Amin,Fest,Nein,Ja,2025-04-01,5.00,40.00,0.00,,20,171.2667,0,0,0,0,20,171.26666666667,21.667,173.333,-2.06633333333,0,0,,0,0,0,0,0,0,0,0,0,0,18,2,16.083333333333,0,-2.06633333333,-2.06633333333,0,-2.06633333333,-3,0,15,12,0,0,0.00,20,0,0,0,0,0,11.183333333333,16.083333333333
,Ahmadpour,Amirhossein,Fest,Ja,Ja,2025-09-01,5.00,40.00,0.00,24,21,193.6333,2,16,0,0,23,209.63333333333,21.667,173.333,36.30033333333,0.00033333332999064,36.3,21,0,0,0,0,0,0,1,8.4666666666667,1,8.7833333333333,19,0,0,0,36.30033333333,36.30033333333,36.30033333333,0,-2,2,13,9,0.00033333332999064,36.3,0.00,21,21,0,0,8.4666666666667,8.7833333333333,24.383333333333,0
188,"Malani Karizi",Arasch,Fest,Nein,Ja,2025-09-01,5.00,40.00,0.00,20,24,211.9833,0,0,0,0,24,211.98333333333,21.667,173.333,38.65033333333,0.00033333332999064,38.65,,0,0,6,2.6,0,0,0,0,1,8.25,20,3,24.7,-147.647,38.65033333333,38.65033333333,0,-108.99666666667,5,0,0,5,0,0,0.00,24,0,2.6,0,0,8.25,16.433333333333,24.7
113,Ziaudin,Atiq,Fest,Ja,Ja,2025-02-24,5.00,40.00,0.00,20,23,194.3167,0,0,0,0,23,194.31666666667,21.667,173.333,20.98366666667,0.00033333332999064,20.98333333334,23,0,0,0,0,0,0,0,0,1,6.4333333333333,20,2,15.05,14.047,20.98366666667,20.98366666667,20.98366666667,14.047,9,0,0,9,0.00033333332999064,20.98333333334,0.00,21,23,0,0,0,6.4333333333333,12.833333333333,15.05
,Rahmati,Behnam,Fest,Ja,Ja,2025-04-01,5.00,40.00,0.00,,20,176.05,0,0,0,0,20,176.05,21.667,173.333,2.717,0.00033333332999064,2.71666666667,20,1,8.5,0,0,0,0,0,0,0,0,20,0,0,0,2.717,2.717,2.717,0,-2,0,15,13,0.00033333332999064,2.71666666667,0.00,20,20,0,0,0,0,16.05,0
,Altner,"Christian Van Manh",Fest,Ja,Ja,2025-10-01,5.00,40.00,0.00,20,22,184.0833,0,0,0,0,22,184.08333333333,21.667,173.333,10.75033333333,0.00033333332999064,10.75,22,0,0,1,0.2,0,0,0,0,1,7.9333333333333,20,1,8.0166666666667,0,10.75033333333,10.75033333333,10.75033333333,0,0,0,2,2,0.00033333332999064,10.75,0.00,21,22,0.2,0,0,7.9333333333333,7.9333333333333,8.0166666666667
,Aba,Cuan,Fest,Nein,Ja,2025-08-01,5.00,40.00,0.00,,8,50.25,6,39,0,0,14,89.25,21.667,173.333,-84.083,0,0,,0,0,0,0,0,0,0,0,0,0,0,0,0,-144.25,-84.083,-84.083,0,-228.333,0,6,10,4,0,0,0.00,,0,0,0,0,0,0,0
,Haghighivand,Danial,Fest,Nein,Ja,2025-04-01,5.00,40.00,0.00,,19,166.0333,2,16,0,0,21,182.03333333333,21.667,173.333,8.70033333333,0.00033333332999064,8.7,,0,0,0,0,0,0,0,0,0,0,19,0,0,0,8.70033333333,8.70033333333,8.70033333333,0,-10,2,15,3,0.00033333332999064,8.7,0.00,19,0,0,0,0,0,14.033333333333,0
,Mohammadi,Danial,Fest,Ja,Ja,2025-01-01,5.00,40.00,0.00,20,21,182.8667,0,0,0,0,21,182.86666666667,21.667,173.333,9.53366666667,0.00033333332999064,9.53333333334,21,0,0,0,0,0,0,0,0,1,8.3333333333333,18,2,16.616666666667,0,9.53366666667,9.53366666667,9.53366666667,0,7,0,0,7,0.00033333332999064,9.53333333334,0.00,20,21,0,0,0,8.3333333333333,13.916666666667,16.616666666667
,Shahrasebi,Danial,Fest,,,2021-05-01,0.00,0.00,0.00,,,,0,0,0,0,0,0,0,0,0,0,0,,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,0,5,0,0,,,0,0,0,0,0,0,0
,Emkani,Daniel,Fest,Nein,Ja,2025-07-15,3.00,25.00,0.00,,14,118.45,0,0,0,0,14,118.45,13,108.333,10.117,10.117,0,,0,0,0,0,0,0,0,0,1,8.6833333333333,0,2,13.3,0,10.117,10.117,10.117,0,0,0,7,7,10.117,0,0.00,12,0,0,0,0,8.6833333333333,0,13.3
,Lolies,Danny,Fest,Ja,Ja,2025-03-01,5.00,40.00,0.00,,19,158.9,0,0,1,8.5,20,167.4,21.667,173.333,-5.933,0,0,19,3,24,0,0,0,0,0,0,1,5.55,15,2,17.05,0,-5.933,-5.933,0,-5.933,10,0,0,10,0,0,0.00,17,19,0,0,0,5.55,11.05,17.05
,Hastedt,David,Fest,Nein,Ja,2025-01-27,4.00,32.00,0.00,20,14,112.3167,0,0,4,32,18,144.31666666667,17.333,138.667,5.64966666667,5.64966666667,0,,0,0,0,0,0,0,0,0,0,0,11,0,0,0,5.64966666667,5.64966666667,5.64966666667,0,2,0,0,2,5.64966666667,0,0.00,11,0,0,0,0,0,7.4166666666667,0
2,Shahrasebi,David,Fest,Ja,Ja,2022-10-01,5.00,40.00,0.00,20,,0,0,0,0,0,0,0,21.667,173.333,0,0,0,,1,8.5,0,0,0,0,0,0,0,0,0,0,0,151.34,0,0,0,151.34,2,0,0,2,0,0,,,0,0,0,0,0,0,0
,Golmohammadi,"Davood ",Fest,Ja,Ja,2025-08-01,5.00,40.00,0.00,20,20,182.2167,0,0,0,0,20,182.21666666667,21.667,173.333,8.88366666667,0.00033333332999064,8.88333333334,20,1,8,0,0,0,0,0,0,0,0,20,0,0,0,8.88366666667,8.88366666667,8.88366666667,0,-6,0,8,2,0.00033333332999064,8.88333333334,0.00,20,20,0,0,0,0,22.216666666667,0
202,Pouzesh,Esmail,Fest,Ja,Ja,2025-09-01,5.00,40.00,0.00,20,24,202.0833,0,0,0,0,24,202.08333333333,21.667,173.333,28.75033333333,0.00033333332999064,28.75,24,0,0,21,70.416666666667,9,20.6,1,9.5,4,17.816666666667,0,0,0,0,28.75033333333,28.75033333333,28.75033333333,0,12,0,0,12,0.00033333332999064,28.75,0.00,24,24,70.416666666667,20.6,9.5,17.816666666667,0,0
187,"Sajaei Khouzestani",Farid,Fest,Ja,Ja,2025-04-07,5.00,40.00,0.00,20,22,197.4333,0,0,0,0,22,197.43333333333,21.667,173.333,24.10033333333,0.00033333332999064,24.1,22,0,0,0,0,0,0,0,0,2,17.316666666667,18,2,16.366666666667,0,24.10033333333,24.10033333333,24.10033333333,0,19,0,0,19,0.00033333332999064,24.1,0.00,22,22,0,0,0,17.316666666667,19.75,16.366666666667
191,"Faridani Rad",Farzad,Fest,Ja,Ja,2025-02-01,5.00,40.00,0.00,20,21,174.0833,0,0,2,16,23,190.08333333333,21.667,173.333,16.75033333333,0.00033333332999064,16.75,21,0,0,14,44.5,2,5.6666666666667,0,0,3,16.15,5,0,0,0,16.75033333333,16.75033333333,16.75033333333,0,0,0,0,0,0.00033333332999064,16.75,0.00,21,21,44.5,5.6666666666667,0,16.15,1.7666666666667,0
,Movafagh,"Farzad ",Fest,Ja,Ja,2025-02-03,5.00,40.00,0.00,20,23,202.6167,0,0,1,8,24,210.61666666667,21.667,173.333,37.28366666667,0.00033333332999064,37.28333333334,23,0,0,0,0,0,0,0,0,0,0,20,3,24.55,0,37.28366666667,37.28366666667,37.28366666667,0,17,0,0,17,0.00033333332999064,37.28333333334,0.00,23,23,0,0,0,0,18.066666666667,24.55
89,Hashimazada,Firuz,Fest,Ja,Ja,2025-01-01,5.00,40.00,0.00,20,18,155.35,0,0,3,24,21,179.35,21.667,173.333,6.017,0.00033333332999064,6.01666666667,18,1,8,0,0,0,0,0,0,1,8.5333333333333,17,0,0,0,6.017,6.017,6.017,0,8,0,0,8,0.00033333332999064,6.01666666667,0.00,18,18,0,0,0,8.5333333333333,10.816666666667,0
,Saeedpour,Fouad,Fest,Ja,Ja,2025-09-01,5.00,40.00,0.00,,23,203.2833,0,0,0,0,23,203.28333333333,21.667,173.333,29.95033333333,0.00033333332999064,29.95,23,0,0,2,4.25,0,0,3,26.8,1,9,16,3,25.25,0,29.95033333333,29.95033333333,29.95033333333,0,26,0,0,26,0.00033333332999064,29.95,0.00,23,23,4.25,0,26.8,9,9.9833333333333,25.25
55,Khodayaran,Hossein,Fest,Ja,Ja,2025-01-01,5.00,40.00,0.00,20,13,105.3,10,80,0,0,23,185.3,21.667,173.333,11.967,0.00033333332999064,11.96666666667,13,0,0,11,31,2,5.6666666666667,0,0,2,8.1333333333333,0,0,0,0,11.967,11.967,11.967,0,16,10,0,6,0.00033333332999064,11.96666666667,0.00,12,13,31,5.6666666666667,0,8.1333333333333,0,0
206,Rasulkhani,Hossein,Fest,Ja,Ja,2025-03-06,5.00,40.00,0.00,20,23,194.8667,0,0,0,0,23,194.86666666667,21.667,173.333,21.53366666667,0.00033333332999064,21.53333333334,23,0,0,4,10.433333333333,0,0,1,9.25,1,9,13,4,32.366666666667,0,21.53366666667,21.53366666667,21.53366666667,0,29,0,0,29,0.00033333332999064,21.53333333334,0.00,23,23,10.433333333333,0,9.25,9,7.8166666666667,32.366666666667
,"Mohamed Salah","Hussin Noureldin",Fest,Ja,Ja,2025-06-06,5.00,40.00,0.00,20,22,186.5833,0,0,0,0,22,186.58333333333,21.667,173.333,13.25033333333,0.00033333332999064,13.25,22,0,0,0,0,0,0,0,0,1,8.5166666666667,18,3,24.55,0,13.25033333333,13.25033333333,13.25033333333,0,-11,0,11,0,0.00033333332999064,13.25,0.00,21,22,0,0,0,8.5166666666667,9.5166666666667,24.55
50,"Rohany Oskoee",Iman,Fest,Nein,Ja,2025-01-01,5.00,40.00,0.00,20,22,190.3833,0,0,0,0,22,190.38333333333,21.667,173.333,17.05033333333,0.00033333332999064,17.05,,0,0,0,0,0,0,0,0,1,8.7333333333333,20,1,8.0333333333333,0,17.05033333333,17.05033333333,17.05033333333,0,36,0,0,36,0.00033333332999064,17.05,0.00,22,0,0,0,0,8.7333333333333,13.616666666667,8.0333333333333
7,Salehi,Iman,Fest,Ja,Ja,2025-02-01,5.00,40.00,0.00,20,26,220.1,0,0,0,0,26,220.1,21.667,173.333,46.767,0.00033333332999064,46.76666666667,26,0,0,21,68.766666666667,7,13.483333333333,2,18.2,2,16.383333333333,0,1,8.0166666666667,-14.233,46.767,46.767,32.534,7.105427357601E-15,0,0,0,0,0.00033333332999064,32.53366666667,0.00,26,26,68.766666666667,13.483333333333,18.2,16.383333333333,0,8.0166666666667
,Krüger,Julian,Fest,Ja,Ja,2025-01-15,5.00,40.00,0.00,20,23,197.35,0,0,0,0,23,197.35,21.667,173.333,24.017,0.00033333332999064,24.01666666667,23,0,0,0,0,0,0,0,0,2,14.283333333333,0,2,15.116666666667,0,24.017,24.017,24.017,0,-30,0,38,8,0.00033333332999064,24.01666666667,0.00,19,23,0,0,0,14.283333333333,0,15.116666666667
,Hazrati,Kamyar,Fest,,Ja,2025-02-24,5.00,40.00,0.00,20,21,182.75,0,0,0,0,21,182.75,21.667,173.333,9.417,0.00033333332999064,9.41666666667,,0,0,0,0,0,0,0,0,0,0,19,2,16.616666666667,0,9.417,9.417,9.417,0,10,0,0,10,0.00033333332999064,9.41666666667,0.00,20,0,0,0,0,0,14.133333333333,16.616666666667
,Saei,Kasra,Fest,Nein,Ja,2025-08-01,5.00,40.00,0.00,,6,37.2667,10,80,0,0,16,117.26666666667,21.667,173.333,-56.06633333333,0,0,,0,0,0,0,0,0,0,0,0,0,0,0,0,-130.782,-56.06633333333,-56.06633333333,0,-186.84833333333,0,10,10,0,0,0,0.00,,0,0,0,0,0,0,0
182,Kalantari,Kaveh,Fest,Ja,Ja,2025-03-01,5.00,40.00,0.00,20,18,159.05,5,40,0,0,23,199.05,21.667,173.333,25.717,0.00033333332999064,25.71666666667,18,0,0,2,4.25,0,0,1,10.25,1,9.9,14,2,16.916666666667,0,25.717,25.717,25.717,0,22,5,0,17,0.00033333332999064,25.71666666667,0.00,18,18,4.25,0,10.25,9.9,5.7333333333333,16.916666666667
,Sahem,Khalid,Fest,Ja,Ja,2025-06-16,5.00,40.00,0.00,20,24,204.35,0,0,0,0,24,204.35,21.667,173.333,31.017,0.00033333332999064,31.01666666667,24,0,0,0,0,0,0,0,0,2,16.833333333333,19,3,26.033333333333,0,31.017,31.017,31.017,0,15,0,0,15,0.00033333332999064,31.01666666667,0.00,24,24,0,0,0,16.833333333333,9.4833333333333,26.033333333333
194,Saffarnajib,Majid,Fest,Ja,Ja,2025-04-01,3.00,21.00,0.00,16,16,133.3667,0,0,0,0,16,133.36666666667,13,91,42.36666666667,42.36666666667,0,16,0,0,0,0,0,0,0,0,0,0,16,0,0,0,42.36666666667,42.36666666667,42.36666666667,0,0,0,16,16,42.36666666667,0,0.00,16,16,0,0,0,0,5.3666666666667,0
177,Juyande,Masoud,Fest,Nein,Ja,2025-01-01,5.00,40.00,0.00,20,22,182.9,0,0,0,0,22,182.9,21.667,173.333,9.567,0.00033333332999064,9.56666666667,,0,0,21,68.1,10,23.783333333333,0,0,2,8.5166666666667,0,0,0,0.009,9.567,9.567,9.567,0.0090000000000003,-7,0,0,-7,0.00033333332999064,9.56666666667,0.00,22,0,68.1,23.783333333333,0,8.5166666666667,0,0
53,Eggerstedt,Matthias,Fest,Ja,Ja,2025-01-01,5.00,40.00,0.00,20,22,190.1333,0,0,0,0,22,190.13333333333,21.667,173.333,16.80033333333,0.00033333332999064,16.8,22,0,0,0,0,0,0,0,0,1,8.6333333333333,20,0,0,0,16.80033333333,16.80033333333,16.80033333333,0,8,0,2,10,0.00033333332999064,16.8,0.00,21,22,0,0,0,8.6333333333333,15.233333333333,0
,Arabnia,Mehdi,Fest,Ja,Ja,2025-05-05,5.00,40.00,0.00,,22,194.3833,0,0,0,0,22,194.38333333333,21.667,173.333,21.05033333333,0.00033333332999064,21.05,22,0,0,0,0,0,0,0,0,0,0,20,2,15.783333333333,0,21.05033333333,21.05033333333,21.05033333333,0,-5,0,12,7,0.00033333332999064,21.05,0.00,21,22,0,0,0,0,18.6,15.783333333333
,"Jalali Wagener",Mehdi,Fest,Ja,Ja,2025-02-01,5.00,40.00,0.00,,24,204.4833,0,0,0,0,24,204.48333333333,21.667,173.333,31.15033333333,0.00033333332999064,31.15,24,0,0,0,0,0,0,0,0,1,8.5333333333333,21,2,16.35,0,31.15033333333,31.15033333333,31.15033333333,0,-2,0,1,-1,0.00033333332999064,31.15,0.00,24,24,0,0,0,8.5333333333333,11.6,16.35
,Taghadosi,Mehdi,Fest,Ja,Ja,2025-08-08,5.00,40.00,0.00,,22,188.9333,0,0,0,0,22,188.93333333333,21.667,173.333,15.60033333333,0.00033333332999064,15.6,22,0,0,0,0,0,0,0,0,1,6.3,19,2,16.6,0,15.60033333333,15.60033333333,15.60033333333,0,10,0,0,10,0.00033333332999064,15.6,0.00,21,22,0,0,0,6.3,14.033333333333,16.6
,Mazidi,Mehrdad,Fest,Ja,Ja,2025-07-01,5.00,40.00,0.00,20,21,183.1667,0,0,0,0,21,183.16666666667,21.667,173.333,9.83366666667,0.00033333332999064,9.83333333334,21,0,0,0,0,0,0,0,0,1,8.1166666666667,18,2,17.033333333333,0,9.83366666667,9.83366666667,9.83366666667,0,0,0,10,10,0.00033333332999064,9.83333333334,0.00,21,21,0,0,0,8.1166666666667,14.016666666667,17.033333333333
165,Koell,"Melanie Nadine",Fest,Nein,Ja,2025-01-01,5.00,40.00,0.00,20,,0,0,0,4,32,4,32,21.667,173.333,-141.333,0,0,,0,0,0,0,0,0,0,0,0,0,0,0,0,-165.263,-141.333,-141.333,0,-306.596,12,0,0,12,0,0,,,0,0,0,0,0,0,0
119,Pourhossein,Milad,Fest,Ja,Ja,2025-09-01,5.00,40.00,0.00,20,15,125.6167,7,56,0,0,22,181.61666666667,21.667,173.333,8.28366666667,0.00033333332999064,8.28333333334,15,0,0,0,0,0,0,0,0,1,7.9333333333333,12,2,16.6,-57.848,8.28366666667,8.28366666667,0,-49.56433333333,-6,7,23,10,0,0,0.00,14,15,0,0,0,7.9333333333333,5.0833333333333,16.6
144,Maleki,Mohammad,Fest,Ja,Ja,2025-09-01,3.00,24.00,0.00,20,5,41.1167,0,0,0,0,5,41.116666666667,13,104,-62.883333333333,0,0,5,2,16,0,0,0,0,0,0,0,0,3,0,0,-250.962,-62.883333333333,-62.883333333333,0,-313.84533333333,-1,0,0,-1,0,0,0.00,3,5,0,0,0,0,1.4,0
217,"Rasul Khani",Mohammad,Fest,Ja,Ja,2025-03-03,5.00,40.00,0.00,20,24,209.4833,0,0,0,0,24,209.48333333333,21.667,173.333,36.15033333333,0.00033333332999064,36.15,24,0,0,1,2.25,0,0,2,17.45,1,8.25,20,1,8.0166666666667,0,36.15033333333,36.15033333333,36.15033333333,0,20,0,0,20,0.00033333332999064,36.15,0.00,24,24,2.25,0,17.45,8.25,13.516666666667,8.0166666666667
96,Sabahi,Mohammad,Fest,Ja,Ja,2025-01-01,5.00,40.00,0.00,20,18,152.5833,1,8,4,32,23,192.58333333333,21.667,173.333,19.25033333333,0.00033333332999064,19.25,18,0,0,0,0,0,0,0,0,2,16.583333333333,16,0,0,0,19.25033333333,19.25033333333,19.25033333333,0,10,1,0,9,0.00033333332999064,19.25,0.00,18,18,0,0,0,16.583333333333,8,0
78,Tajikzadeh,Mohammad,Fest,Ja,Ja,2025-01-01,5.00,40.00,0.00,20,18,152.3167,5,40,0,0,23,192.31666666667,21.667,173.333,18.98366666667,0.00033333332999064,18.98333333334,18,0,0,0,0,0,0,0,0,1,8.25,16,1,8.0833333333333,0,18.98366666667,18.98366666667,18.98366666667,0,7,5,0,2,0.00033333332999064,18.98333333334,0.00,18,18,0,0,0,8.25,7.9833333333333,8.0833333333333
,Atrafi,"Mohammad Nasir",Fest,Ja,Ja,2025-10-01,5.00,4.00,0.00,20,19,162.9667,0,0,0,0,19,162.96666666667,21.667,17.333,145.63366666667,145.63366666667,0,19,0,0,0,0,0,0,0,0,0,0,16,3,22.5,0,145.63366666667,145.63366666667,145.63366666667,0,0,0,5,5,145.63366666667,0,0.00,17,19,0,0,0,0,12.466666666667,22.5
,Amiri,"Mohammad Samir",Fest,Ja,Ja,2025-09-01,5.00,40.00,0.00,20,22,190.15,0,0,3,24,25,214.15,21.667,173.333,40.817,0.00033333332999064,40.81666666667,22,0,0,5,17.25,5,12.633333333333,1,10.166666666667,0,0,0,2,12.316666666667,0,40.817,40.817,40.817,0,11,0,0,11,0.00033333332999064,40.81666666667,0.00,20,22,17.25,12.633333333333,10.166666666667,0,0,12.316666666667
,"El Moustaiqid",Najlae,Fest,,Ja,2025-09-01,5.00,40.00,0.00,,20,151.7,1,8,0,0,21,159.7,21.667,173.333,-13.633,0,0,,0,0,0,0,0,0,0,0,0,0,0,0,0,-1140.63,-13.633,-13.633,0,-1154.263,-5,1,15,9,0,0,0.00,,0,0,0,0,0,0,0
,Jasarevic,Noel,Fest,Nein,Ja,2025-09-15,5.00,40.00,0.00,,14,116.6333,7,56,0,0,21,172.63333333333,21.667,173.333,-0.69966666667,0,0,,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.69966666667,-0.69966666667,0,-0.69966666667,0,7,6,-1,0,0,0.00,12,0,0,0,0,0,0,0
,"Sharh Ghaboos",Peyman,Fest,Ja,Ja,2025-03-01,5.00,40.00,0.00,,24,212.6333,0,0,0,0,24,212.63333333333,21.667,173.333,39.30033333333,0.00033333332999064,39.3,24,0,0,6,2.6,0,0,0,0,1,8.25,21,2,16.683333333333,0,39.30033333333,39.30033333333,39.30033333333,0,20,0,0,20,0.00033333332999064,39.3,0.00,24,24,2.6,0,0,8.25,17.1,16.683333333333
227,Feilbach,"Raoul Nikolai ",Fest,Nein,Ja,2025-09-01,5.00,40.00,0.00,20,24,215.8833,0,0,0,0,24,215.88333333333,21.667,173.333,42.55033333333,0.00033333332999064,42.55,,0,0,16,55.5,16,40.75,1,8.7,4,17.483333333333,0,0,0,0.007,42.55033333333,42.55033333333,42.55033333333,0.0069999999999979,-14,0,20,6,0.00033333332999064,42.55,0.00,24,0,55.5,40.75,8.7,17.483333333333,0,0
,Hatami,Reza,Fest,Ja,Ja,2025-09-15,5.00,40.00,0.00,20,20,173.9667,0,0,0,0,20,173.96666666667,21.667,173.333,0.63366666667,0.00033333332999064,0.63333333334,20,0,0,0,0,0,0,0,0,0,0,18,2,16.6,0,0.63366666667,0.63366666667,0.63366666666999,9.9920072216264E-15,0,0,6,6,0.00033333332999064,0.63333333334,0.00,20,20,0,0,0,0,13.366666666667,16.6
137,Pourhossein,Reza,Fest,Ja,Ja,2025-04-01,5.00,40.00,0.00,20,21,181.55,0,0,0,0,21,181.55,21.667,173.333,8.217,0.00033333332999064,8.21666666667,21,0,0,12,36.533333333333,7,15.016666666667,0,0,2,16.316666666667,7,1,8.0166666666667,0,8.217,8.217,8.217,0,29,0,0,29,0.00033333332999064,8.21666666667,0.00,20,21,36.533333333333,15.016666666667,0,16.316666666667,4.1666666666667,8.0166666666667
225,"Hosseini Jahed",Sajad,Fest,Ja,Ja,2025-03-03,5.00,40.00,0.00,20,21,193.3833,2,16,0,0,23,209.38333333333,21.667,173.333,36.05033333333,0.00033333332999064,36.05,21,0,0,0,0,0,0,1,8.4666666666667,1,8.7833333333333,18,0,0,0,36.05033333333,36.05033333333,36.05033333333,0,39,2,-17,20,0.00033333332999064,36.05,0.00,20,21,0,0,8.4666666666667,8.7833333333333,24.3,0
,Saki,Sara,Fest,Ja,Ja,2025-02-01,5.00,40.00,0.00,,14,117.6333,10,80,0,0,24,197.63333333333,21.667,173.333,24.30033333333,0.00033333332999064,24.3,14,0,0,11,35.05,0,0,1,8.7,2,16.383333333333,0,0,0,0,24.30033333333,24.30033333333,24.30033333333,0,23,10,-2,11,0.00033333332999064,24.3,0.00,14,14,35.05,0,8.7,16.383333333333,0,0
,"Perez Y Waldt",Sebastian,Fest,Nein,Ja,2024-09-23,5.00,40.00,0.00,,22,181.9,0,0,0,0,22,181.9,21.667,173.333,8.567,0.00033333332999064,8.56666666667,,0,0,4,14.1,0,0,0,0,1,8.0166666666667,11,3,24.666666666667,-756.446,8.567,8.567,0,-747.879,14,0,0,14,0,0,0.00,17,0,14.1,0,0,8.0166666666667,6.5166666666667,24.666666666667
38,Hosseini,"Seyed Hamid",Fest,Ja,Ja,2025-03-10,5.00,40.00,0.00,20,18,153.4167,0,0,5,40,23,193.41666666667,21.667,173.333,20.08366666667,0.00033333332999064,20.08333333334,18,0,0,0,0,0,0,0,0,2,16.5,15,1,8.0833333333333,0,20.08366666667,20.08366666667,20.08366666667,0,26,0,0,26,0.00033333332999064,20.08333333334,0.00,18,18,0,0,0,16.5,8.8333333333333,8.0833333333333
,Jabbari,Shahram,Fest,Nein,Ja,2025-04-01,5.00,40.00,0.00,,20,175.2167,0,0,0,0,20,175.21666666667,21.667,173.333,1.88366666667,0.00033333332999064,1.88333333334,,0,0,0,0,0,0,0,0,0,0,18,2,16.95,0,1.88366666667,1.88366666667,1.88366666667,0,-5,0,0,-5,0.00033333332999064,1.88333333334,0.00,20,0,0,0,0,0,14.266666666667,16.95
124,Imanian,Shahriyar,Fest,Ja,Ja,2025-03-10,5.00,40.00,0.00,20,12,104.2833,12,96,0,0,24,200.28333333333,21.667,173.333,26.95033333333,0.00033333332999064,26.95,12,0,0,1,0.083333333333333,0,0,1,8.35,0,0,10,1,8.1666666666667,0,26.95033333333,26.95033333333,26.95033333333,0,36,12,0,24,0.00033333332999064,26.95,0.00,12,12,0.083333333333333,0,8.35,0,7.6833333333333,8.1666666666667
164,Tokhi,Shokrollah,Fest,Ja,Ja,2025-03-04,5.00,40.00,0.00,20,18,152.6,5,40,0,0,23,192.6,21.667,173.333,19.267,0.00033333332999064,19.26666666667,18,0,0,14,47.75,9,21.483333333333,0,0,2,9.3,2,1,8.6333333333333,0,19.267,19.267,19.267,0,14,5,0,9,0.00033333332999064,19.26666666667,0.00,18,18,47.75,21.483333333333,0,9.3,0.93333333333333,8.6333333333333
210,Rahimzadeh,Siamak,Fest,Ja,Ja,2025-03-01,5.00,40.00,0.00,20,23,197.5,0,0,0,0,23,197.5,21.667,173.333,24.167,0.00033333332999064,24.16666666667,23,0,0,0,0,0,0,0,0,2,16.25,18,3,25.016666666667,0.027,24.167,24.167,24.167,0.027000000000001,39,0,0,39,0.00033333332999064,24.16666666667,0.00,23,23,0,0,0,16.25,12.233333333333,25.016666666667
,Florescu,Silviu-Cezar,Fest,Nein,Ja,2025-10-01,5.00,40.00,0.00,,20,173.5333,0,0,0,0,20,173.53333333333,21.667,173.333,0.20033333333001,0.00033333332999064,0.20000000000002,,0,0,2,0.28333333333333,0,0,0,0,0,0,19,1,8.35,0,0.20033333333001,0.20033333333001,0.20033333333001,0,9,0,0,9,0.00033333332999064,0.20000000000002,0.00,20,0,0.28333333333333,0,0,0,12.9,8.35
,Vafaei,Soheil,Fest,Ja,Ja,2025-04-01,5.00,40.00,0.00,,18,155.4833,3,24,1,8,22,187.48333333333,21.667,173.333,14.15033333333,0.00033333332999064,14.15,18,0,0,0,0,0,0,0,0,0,0,17,1,8.0166666666667,0,14.15033333333,14.15033333333,14.15033333333,0,-10,3,0,-13,0.00033333332999064,14.15,0.00,18,18,0,0,0,0,11.466666666667,8.0166666666667
,Toraman,"Tolga Can",Fest,Nein,Ja,2025-09-15,5.00,40.00,0.00,20,22,183.1333,0,0,0,0,22,183.13333333333,21.667,173.333,9.80033333333,0.00033333332999064,9.8,,0,0,0,0,0,0,1,9.5,0,0,17,2,16.3,0,9.80033333333,9.80033333333,9.80033333333,0,0,0,0,0,0.00033333332999064,9.8,0.00,20,0,0,0,9.5,0,6.3666666666667,16.3
14,Haugg,Uwe,Fest,Ja,Ja,2025-01-01,4.00,32.00,0.00,18,6,50.6167,8,64,4,32,18,146.61666666667,17.333,138.667,7.94966666667,7.94966666667,0,6,0,0,0,0,0,0,0,0,0,0,5,1,6.9666666666667,-0.2,7.94966666667,7.94966666667,7.74966666667,0,-10,8,0,-18,7.74966666667,0,0.00,5,6,0,0,0,0,3.65,6.9666666666667
231,Shokripour,Vahid,Fest,Ja,Ja,2025-01-01,5.00,40.00,0.00,20,24,208.1167,0,0,0,0,24,208.11666666667,21.667,173.333,34.78366666667,0.00033333332999064,34.78333333334,24,0,0,0,0,0,0,1,9.5,2,16.166666666667,18,3,25.333333333333,0,34.78366666667,34.78366666667,34.78366666667,0,35,0,0,35,0.00033333332999064,34.78333333334,0.00,24,24,0,0,9.5,16.166666666667,13.116666666667,25.333333333333
226,Konak,Zidar,Fest,Nein,Ja,2025-05-01,5.00,40.00,0.00,20,22,183.65,0,0,0,0,22,183.65,21.667,173.333,10.317,0.00033333332999064,10.31666666667,,0,0,0,0,0,0,2,18.5,1,7,0,2,16.433333333333,0,10.317,10.317,10.317,0,16,0,0,16,0.00033333332999064,10.31666666667,0.00,15,0,0,0,18.5,7,0,16.433333333333
,,,,,,,,,,,1293,11086.0499,101,799,32,256.5,1426,12141.55,1438.687,11344.646,970.23699999999,,,,9,73,175,515.91666666667,67,159.08333333333,20,181.8,66,479.58333333333,841,91,738.55,,970.23699999999,970.23699999999,1267.0006666666,-2939.2536666667,,101,241,709,211.53366666651,1055.4670000001,0,1209,,515.91666666667,159.08333333333,181.8,479.58333333333,607.15,738.55
1 Vertrag Vertrag Vertrag Vertrag Vertrag Vertrag Vertrag Vertrag Stunden innerhalb der Soll-Stunden Stunden innerhalb der Soll-Stunden Stunden innerhalb der Soll-Stunden Stunden innerhalb der Soll-Stunden Stunden innerhalb der Soll-Stunden Stunden innerhalb der Soll-Stunden Ist-/Soll-Stunden Ist-/Soll-Stunden Ist-/Soll-Stunden Ist-/Soll-Stunden Berechnung Berechnung Berechnung Berechnung Berechnung Berechnung Berechnung Berechnung Berechnung Berechnung Berechnung Berechnung Berechnung Berechnung Berechnung Berechnung Berechnung Salden Salden Salden Salden Salden Salden Salden Salden Salden Auszahlen Auszahlen Auszahlen Auszahlen Auszahlen Auszahlen Auszahlen Auszahlen Auszahlen Auszahlen Auszahlen
2 Gearbeitete Stunden Gearbeitete Stunden Urlaub nehmen Urlaub nehmen Krankheitsstunden Krankheitsstunden Gesamt Gesamt Vertrag Vertrag Plus/Min Mehrstunden Überstunden Zuschlag P-Schein Krank ohne AU Krank ohne AU Nachtzuschlag 25 (25%) Nachtzuschlag 25 (25%) Nachtzuschlag 40 (40%) Nachtzuschlag 40 (40%) Sonntagszuschlag (50%) Sonntagszuschlag (50%) Feiertagszuschlag (125%) Feiertagszuschlag (125%) Verpfl.-pauschale (14%) Samstagzuschlag (15%) Samstagzuschlag (15%) Zeitausgleich Zeitausgleich Zeitausgleich Zeitausgleich Zeitausgleich Urlaub Urlaub Urlaub Urlaub Mehrstunden Überstunden auszahlen Fahrtkosten Verpflegungspauschale Zuschlag P-Schein 25% 40% 50% 125% 14% 15%
3 Persnr. Nachname Vorname Art P-Schein Recht auf Zuschläge? Start Tage Stunden Max Urlaubstage pro Jahr Tage Stunden Tage Stunden Tage Stunden Tage Stunden Tage Stunden Stunden Stunden Stunden anzahl Tage Stunden Tage Stunden Tage Stunden Tage Stunden Tage Stunden Tage Tage Stunden Start +/- Änderungen Auszahlen Jetzt Start Abbau Korrektur Jetzt Stunden Stunden km Tage Anzahl Stunden Stunden Stunden Stunden Stunden Stunden
4 169 Safari Abdul Latif Fest Ja Ja 2025-09-01 5.00 40.00 0.00 20 20 180.4167 5 40 0 0 25 220.41666666667 21.667 173.333 47.08366666667 0.00033333332999064 47.08333333334 20 0 0 0 0 0 0 0 0 2 16.166666666667 16 2 17.35 0.334 47.08366666667 47.08366666667 47.08366666667 0.334 19 5 0 14 0.00033333332999064 47.08333333334 0.00 20 20 0 0 0 16.166666666667 18.9 17.35
5 93 Moshtagh Askari Adel Fest Ja Ja 2025-01-01 5.00 40.00 0.00 20 25 218.4167 0 0 0 0 25 218.41666666667 21.667 173.333 45.08366666667 0.00033333332999064 45.08333333334 25 0 0 0 0 0 0 0 0 2 18.65 20 3 24.95 0.007 45.08366666667 45.08366666667 45.08366666667 0.0069999999999979 31 0 0 31 0.00033333332999064 45.08333333334 0.00 25 25 0 0 0 18.65 14.816666666667 24.95
6 18 Mihailescu Adrian Lucian Fest Ja Ja 2025-09-01 5.00 40.00 0.00 20 21 177.5667 0 0 0 0 21 177.56666666667 21.667 173.333 4.23366666667 0.00033333332999064 4.23333333334 21 0 0 0 0 0 0 0 0 1 8.6333333333333 15 3 24.116666666667 0 4.23366666667 4.23366666667 4.23366666667 0 20 0 0 20 0.00033333332999064 4.23333333334 0.00 18 21 0 0 0 8.6333333333333 11.1 24.116666666667
7 Esmaeili Dashtbayaz Amin Fest Nein Ja 2025-04-01 5.00 40.00 0.00 20 171.2667 0 0 0 0 20 171.26666666667 21.667 173.333 -2.06633333333 0 0 0 0 0 0 0 0 0 0 0 0 18 2 16.083333333333 0 -2.06633333333 -2.06633333333 0 -2.06633333333 -3 0 15 12 0 0 0.00 20 0 0 0 0 0 11.183333333333 16.083333333333
8 Ahmadpour Amirhossein Fest Ja Ja 2025-09-01 5.00 40.00 0.00 24 21 193.6333 2 16 0 0 23 209.63333333333 21.667 173.333 36.30033333333 0.00033333332999064 36.3 21 0 0 0 0 0 0 1 8.4666666666667 1 8.7833333333333 19 0 0 0 36.30033333333 36.30033333333 36.30033333333 0 -2 2 13 9 0.00033333332999064 36.3 0.00 21 21 0 0 8.4666666666667 8.7833333333333 24.383333333333 0
9 188 Malani Karizi Arasch Fest Nein Ja 2025-09-01 5.00 40.00 0.00 20 24 211.9833 0 0 0 0 24 211.98333333333 21.667 173.333 38.65033333333 0.00033333332999064 38.65 0 0 6 2.6 0 0 0 0 1 8.25 20 3 24.7 -147.647 38.65033333333 38.65033333333 0 -108.99666666667 5 0 0 5 0 0 0.00 24 0 2.6 0 0 8.25 16.433333333333 24.7
10 113 Ziaudin Atiq Fest Ja Ja 2025-02-24 5.00 40.00 0.00 20 23 194.3167 0 0 0 0 23 194.31666666667 21.667 173.333 20.98366666667 0.00033333332999064 20.98333333334 23 0 0 0 0 0 0 0 0 1 6.4333333333333 20 2 15.05 14.047 20.98366666667 20.98366666667 20.98366666667 14.047 9 0 0 9 0.00033333332999064 20.98333333334 0.00 21 23 0 0 0 6.4333333333333 12.833333333333 15.05
11 Rahmati Behnam Fest Ja Ja 2025-04-01 5.00 40.00 0.00 20 176.05 0 0 0 0 20 176.05 21.667 173.333 2.717 0.00033333332999064 2.71666666667 20 1 8.5 0 0 0 0 0 0 0 0 20 0 0 0 2.717 2.717 2.717 0 -2 0 15 13 0.00033333332999064 2.71666666667 0.00 20 20 0 0 0 0 16.05 0
12 Altner Christian Van Manh Fest Ja Ja 2025-10-01 5.00 40.00 0.00 20 22 184.0833 0 0 0 0 22 184.08333333333 21.667 173.333 10.75033333333 0.00033333332999064 10.75 22 0 0 1 0.2 0 0 0 0 1 7.9333333333333 20 1 8.0166666666667 0 10.75033333333 10.75033333333 10.75033333333 0 0 0 2 2 0.00033333332999064 10.75 0.00 21 22 0.2 0 0 7.9333333333333 7.9333333333333 8.0166666666667
13 Aba Cuan Fest Nein Ja 2025-08-01 5.00 40.00 0.00 8 50.25 6 39 0 0 14 89.25 21.667 173.333 -84.083 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -144.25 -84.083 -84.083 0 -228.333 0 6 10 4 0 0 0.00 0 0 0 0 0 0 0
14 Haghighivand Danial Fest Nein Ja 2025-04-01 5.00 40.00 0.00 19 166.0333 2 16 0 0 21 182.03333333333 21.667 173.333 8.70033333333 0.00033333332999064 8.7 0 0 0 0 0 0 0 0 0 0 19 0 0 0 8.70033333333 8.70033333333 8.70033333333 0 -10 2 15 3 0.00033333332999064 8.7 0.00 19 0 0 0 0 0 14.033333333333 0
15 Mohammadi Danial Fest Ja Ja 2025-01-01 5.00 40.00 0.00 20 21 182.8667 0 0 0 0 21 182.86666666667 21.667 173.333 9.53366666667 0.00033333332999064 9.53333333334 21 0 0 0 0 0 0 0 0 1 8.3333333333333 18 2 16.616666666667 0 9.53366666667 9.53366666667 9.53366666667 0 7 0 0 7 0.00033333332999064 9.53333333334 0.00 20 21 0 0 0 8.3333333333333 13.916666666667 16.616666666667
16 Shahrasebi Danial Fest 2021-05-01 0.00 0.00 0.00 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 5 0 0 5 0 0 0 0 0 0 0 0 0
17 Emkani Daniel Fest Nein Ja 2025-07-15 3.00 25.00 0.00 14 118.45 0 0 0 0 14 118.45 13 108.333 10.117 10.117 0 0 0 0 0 0 0 0 0 1 8.6833333333333 0 2 13.3 0 10.117 10.117 10.117 0 0 0 7 7 10.117 0 0.00 12 0 0 0 0 8.6833333333333 0 13.3
18 Lolies Danny Fest Ja Ja 2025-03-01 5.00 40.00 0.00 19 158.9 0 0 1 8.5 20 167.4 21.667 173.333 -5.933 0 0 19 3 24 0 0 0 0 0 0 1 5.55 15 2 17.05 0 -5.933 -5.933 0 -5.933 10 0 0 10 0 0 0.00 17 19 0 0 0 5.55 11.05 17.05
19 Hastedt David Fest Nein Ja 2025-01-27 4.00 32.00 0.00 20 14 112.3167 0 0 4 32 18 144.31666666667 17.333 138.667 5.64966666667 5.64966666667 0 0 0 0 0 0 0 0 0 0 0 11 0 0 0 5.64966666667 5.64966666667 5.64966666667 0 2 0 0 2 5.64966666667 0 0.00 11 0 0 0 0 0 7.4166666666667 0
20 2 Shahrasebi David Fest Ja Ja 2022-10-01 5.00 40.00 0.00 20 0 0 0 0 0 0 0 21.667 173.333 0 0 0 1 8.5 0 0 0 0 0 0 0 0 0 0 0 151.34 0 0 0 151.34 2 0 0 2 0 0 0 0 0 0 0 0 0
21 Golmohammadi Davood Fest Ja Ja 2025-08-01 5.00 40.00 0.00 20 20 182.2167 0 0 0 0 20 182.21666666667 21.667 173.333 8.88366666667 0.00033333332999064 8.88333333334 20 1 8 0 0 0 0 0 0 0 0 20 0 0 0 8.88366666667 8.88366666667 8.88366666667 0 -6 0 8 2 0.00033333332999064 8.88333333334 0.00 20 20 0 0 0 0 22.216666666667 0
22 202 Pouzesh Esmail Fest Ja Ja 2025-09-01 5.00 40.00 0.00 20 24 202.0833 0 0 0 0 24 202.08333333333 21.667 173.333 28.75033333333 0.00033333332999064 28.75 24 0 0 21 70.416666666667 9 20.6 1 9.5 4 17.816666666667 0 0 0 0 28.75033333333 28.75033333333 28.75033333333 0 12 0 0 12 0.00033333332999064 28.75 0.00 24 24 70.416666666667 20.6 9.5 17.816666666667 0 0
23 187 Sajaei Khouzestani Farid Fest Ja Ja 2025-04-07 5.00 40.00 0.00 20 22 197.4333 0 0 0 0 22 197.43333333333 21.667 173.333 24.10033333333 0.00033333332999064 24.1 22 0 0 0 0 0 0 0 0 2 17.316666666667 18 2 16.366666666667 0 24.10033333333 24.10033333333 24.10033333333 0 19 0 0 19 0.00033333332999064 24.1 0.00 22 22 0 0 0 17.316666666667 19.75 16.366666666667
24 191 Faridani Rad Farzad Fest Ja Ja 2025-02-01 5.00 40.00 0.00 20 21 174.0833 0 0 2 16 23 190.08333333333 21.667 173.333 16.75033333333 0.00033333332999064 16.75 21 0 0 14 44.5 2 5.6666666666667 0 0 3 16.15 5 0 0 0 16.75033333333 16.75033333333 16.75033333333 0 0 0 0 0 0.00033333332999064 16.75 0.00 21 21 44.5 5.6666666666667 0 16.15 1.7666666666667 0
25 Movafagh Farzad Fest Ja Ja 2025-02-03 5.00 40.00 0.00 20 23 202.6167 0 0 1 8 24 210.61666666667 21.667 173.333 37.28366666667 0.00033333332999064 37.28333333334 23 0 0 0 0 0 0 0 0 0 0 20 3 24.55 0 37.28366666667 37.28366666667 37.28366666667 0 17 0 0 17 0.00033333332999064 37.28333333334 0.00 23 23 0 0 0 0 18.066666666667 24.55
26 89 Hashimazada Firuz Fest Ja Ja 2025-01-01 5.00 40.00 0.00 20 18 155.35 0 0 3 24 21 179.35 21.667 173.333 6.017 0.00033333332999064 6.01666666667 18 1 8 0 0 0 0 0 0 1 8.5333333333333 17 0 0 0 6.017 6.017 6.017 0 8 0 0 8 0.00033333332999064 6.01666666667 0.00 18 18 0 0 0 8.5333333333333 10.816666666667 0
27 Saeedpour Fouad Fest Ja Ja 2025-09-01 5.00 40.00 0.00 23 203.2833 0 0 0 0 23 203.28333333333 21.667 173.333 29.95033333333 0.00033333332999064 29.95 23 0 0 2 4.25 0 0 3 26.8 1 9 16 3 25.25 0 29.95033333333 29.95033333333 29.95033333333 0 26 0 0 26 0.00033333332999064 29.95 0.00 23 23 4.25 0 26.8 9 9.9833333333333 25.25
28 55 Khodayaran Hossein Fest Ja Ja 2025-01-01 5.00 40.00 0.00 20 13 105.3 10 80 0 0 23 185.3 21.667 173.333 11.967 0.00033333332999064 11.96666666667 13 0 0 11 31 2 5.6666666666667 0 0 2 8.1333333333333 0 0 0 0 11.967 11.967 11.967 0 16 10 0 6 0.00033333332999064 11.96666666667 0.00 12 13 31 5.6666666666667 0 8.1333333333333 0 0
29 206 Rasulkhani Hossein Fest Ja Ja 2025-03-06 5.00 40.00 0.00 20 23 194.8667 0 0 0 0 23 194.86666666667 21.667 173.333 21.53366666667 0.00033333332999064 21.53333333334 23 0 0 4 10.433333333333 0 0 1 9.25 1 9 13 4 32.366666666667 0 21.53366666667 21.53366666667 21.53366666667 0 29 0 0 29 0.00033333332999064 21.53333333334 0.00 23 23 10.433333333333 0 9.25 9 7.8166666666667 32.366666666667
30 Mohamed Salah Hussin Noureldin Fest Ja Ja 2025-06-06 5.00 40.00 0.00 20 22 186.5833 0 0 0 0 22 186.58333333333 21.667 173.333 13.25033333333 0.00033333332999064 13.25 22 0 0 0 0 0 0 0 0 1 8.5166666666667 18 3 24.55 0 13.25033333333 13.25033333333 13.25033333333 0 -11 0 11 0 0.00033333332999064 13.25 0.00 21 22 0 0 0 8.5166666666667 9.5166666666667 24.55
31 50 Rohany Oskoee Iman Fest Nein Ja 2025-01-01 5.00 40.00 0.00 20 22 190.3833 0 0 0 0 22 190.38333333333 21.667 173.333 17.05033333333 0.00033333332999064 17.05 0 0 0 0 0 0 0 0 1 8.7333333333333 20 1 8.0333333333333 0 17.05033333333 17.05033333333 17.05033333333 0 36 0 0 36 0.00033333332999064 17.05 0.00 22 0 0 0 0 8.7333333333333 13.616666666667 8.0333333333333
32 7 Salehi Iman Fest Ja Ja 2025-02-01 5.00 40.00 0.00 20 26 220.1 0 0 0 0 26 220.1 21.667 173.333 46.767 0.00033333332999064 46.76666666667 26 0 0 21 68.766666666667 7 13.483333333333 2 18.2 2 16.383333333333 0 1 8.0166666666667 -14.233 46.767 46.767 32.534 7.105427357601E-15 0 0 0 0 0.00033333332999064 32.53366666667 0.00 26 26 68.766666666667 13.483333333333 18.2 16.383333333333 0 8.0166666666667
33 Krüger Julian Fest Ja Ja 2025-01-15 5.00 40.00 0.00 20 23 197.35 0 0 0 0 23 197.35 21.667 173.333 24.017 0.00033333332999064 24.01666666667 23 0 0 0 0 0 0 0 0 2 14.283333333333 0 2 15.116666666667 0 24.017 24.017 24.017 0 -30 0 38 8 0.00033333332999064 24.01666666667 0.00 19 23 0 0 0 14.283333333333 0 15.116666666667
34 Hazrati Kamyar Fest Ja 2025-02-24 5.00 40.00 0.00 20 21 182.75 0 0 0 0 21 182.75 21.667 173.333 9.417 0.00033333332999064 9.41666666667 0 0 0 0 0 0 0 0 0 0 19 2 16.616666666667 0 9.417 9.417 9.417 0 10 0 0 10 0.00033333332999064 9.41666666667 0.00 20 0 0 0 0 0 14.133333333333 16.616666666667
35 Saei Kasra Fest Nein Ja 2025-08-01 5.00 40.00 0.00 6 37.2667 10 80 0 0 16 117.26666666667 21.667 173.333 -56.06633333333 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -130.782 -56.06633333333 -56.06633333333 0 -186.84833333333 0 10 10 0 0 0 0.00 0 0 0 0 0 0 0
36 182 Kalantari Kaveh Fest Ja Ja 2025-03-01 5.00 40.00 0.00 20 18 159.05 5 40 0 0 23 199.05 21.667 173.333 25.717 0.00033333332999064 25.71666666667 18 0 0 2 4.25 0 0 1 10.25 1 9.9 14 2 16.916666666667 0 25.717 25.717 25.717 0 22 5 0 17 0.00033333332999064 25.71666666667 0.00 18 18 4.25 0 10.25 9.9 5.7333333333333 16.916666666667
37 Sahem Khalid Fest Ja Ja 2025-06-16 5.00 40.00 0.00 20 24 204.35 0 0 0 0 24 204.35 21.667 173.333 31.017 0.00033333332999064 31.01666666667 24 0 0 0 0 0 0 0 0 2 16.833333333333 19 3 26.033333333333 0 31.017 31.017 31.017 0 15 0 0 15 0.00033333332999064 31.01666666667 0.00 24 24 0 0 0 16.833333333333 9.4833333333333 26.033333333333
38 194 Saffarnajib Majid Fest Ja Ja 2025-04-01 3.00 21.00 0.00 16 16 133.3667 0 0 0 0 16 133.36666666667 13 91 42.36666666667 42.36666666667 0 16 0 0 0 0 0 0 0 0 0 0 16 0 0 0 42.36666666667 42.36666666667 42.36666666667 0 0 0 16 16 42.36666666667 0 0.00 16 16 0 0 0 0 5.3666666666667 0
39 177 Juyande Masoud Fest Nein Ja 2025-01-01 5.00 40.00 0.00 20 22 182.9 0 0 0 0 22 182.9 21.667 173.333 9.567 0.00033333332999064 9.56666666667 0 0 21 68.1 10 23.783333333333 0 0 2 8.5166666666667 0 0 0 0.009 9.567 9.567 9.567 0.0090000000000003 -7 0 0 -7 0.00033333332999064 9.56666666667 0.00 22 0 68.1 23.783333333333 0 8.5166666666667 0 0
40 53 Eggerstedt Matthias Fest Ja Ja 2025-01-01 5.00 40.00 0.00 20 22 190.1333 0 0 0 0 22 190.13333333333 21.667 173.333 16.80033333333 0.00033333332999064 16.8 22 0 0 0 0 0 0 0 0 1 8.6333333333333 20 0 0 0 16.80033333333 16.80033333333 16.80033333333 0 8 0 2 10 0.00033333332999064 16.8 0.00 21 22 0 0 0 8.6333333333333 15.233333333333 0
41 Arabnia Mehdi Fest Ja Ja 2025-05-05 5.00 40.00 0.00 22 194.3833 0 0 0 0 22 194.38333333333 21.667 173.333 21.05033333333 0.00033333332999064 21.05 22 0 0 0 0 0 0 0 0 0 0 20 2 15.783333333333 0 21.05033333333 21.05033333333 21.05033333333 0 -5 0 12 7 0.00033333332999064 21.05 0.00 21 22 0 0 0 0 18.6 15.783333333333
42 Jalali Wagener Mehdi Fest Ja Ja 2025-02-01 5.00 40.00 0.00 24 204.4833 0 0 0 0 24 204.48333333333 21.667 173.333 31.15033333333 0.00033333332999064 31.15 24 0 0 0 0 0 0 0 0 1 8.5333333333333 21 2 16.35 0 31.15033333333 31.15033333333 31.15033333333 0 -2 0 1 -1 0.00033333332999064 31.15 0.00 24 24 0 0 0 8.5333333333333 11.6 16.35
43 Taghadosi Mehdi Fest Ja Ja 2025-08-08 5.00 40.00 0.00 22 188.9333 0 0 0 0 22 188.93333333333 21.667 173.333 15.60033333333 0.00033333332999064 15.6 22 0 0 0 0 0 0 0 0 1 6.3 19 2 16.6 0 15.60033333333 15.60033333333 15.60033333333 0 10 0 0 10 0.00033333332999064 15.6 0.00 21 22 0 0 0 6.3 14.033333333333 16.6
44 Mazidi Mehrdad Fest Ja Ja 2025-07-01 5.00 40.00 0.00 20 21 183.1667 0 0 0 0 21 183.16666666667 21.667 173.333 9.83366666667 0.00033333332999064 9.83333333334 21 0 0 0 0 0 0 0 0 1 8.1166666666667 18 2 17.033333333333 0 9.83366666667 9.83366666667 9.83366666667 0 0 0 10 10 0.00033333332999064 9.83333333334 0.00 21 21 0 0 0 8.1166666666667 14.016666666667 17.033333333333
45 165 Koell Melanie Nadine Fest Nein Ja 2025-01-01 5.00 40.00 0.00 20 0 0 0 4 32 4 32 21.667 173.333 -141.333 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -165.263 -141.333 -141.333 0 -306.596 12 0 0 12 0 0 0 0 0 0 0 0 0
46 119 Pourhossein Milad Fest Ja Ja 2025-09-01 5.00 40.00 0.00 20 15 125.6167 7 56 0 0 22 181.61666666667 21.667 173.333 8.28366666667 0.00033333332999064 8.28333333334 15 0 0 0 0 0 0 0 0 1 7.9333333333333 12 2 16.6 -57.848 8.28366666667 8.28366666667 0 -49.56433333333 -6 7 23 10 0 0 0.00 14 15 0 0 0 7.9333333333333 5.0833333333333 16.6
47 144 Maleki Mohammad Fest Ja Ja 2025-09-01 3.00 24.00 0.00 20 5 41.1167 0 0 0 0 5 41.116666666667 13 104 -62.883333333333 0 0 5 2 16 0 0 0 0 0 0 0 0 3 0 0 -250.962 -62.883333333333 -62.883333333333 0 -313.84533333333 -1 0 0 -1 0 0 0.00 3 5 0 0 0 0 1.4 0
48 217 Rasul Khani Mohammad Fest Ja Ja 2025-03-03 5.00 40.00 0.00 20 24 209.4833 0 0 0 0 24 209.48333333333 21.667 173.333 36.15033333333 0.00033333332999064 36.15 24 0 0 1 2.25 0 0 2 17.45 1 8.25 20 1 8.0166666666667 0 36.15033333333 36.15033333333 36.15033333333 0 20 0 0 20 0.00033333332999064 36.15 0.00 24 24 2.25 0 17.45 8.25 13.516666666667 8.0166666666667
49 96 Sabahi Mohammad Fest Ja Ja 2025-01-01 5.00 40.00 0.00 20 18 152.5833 1 8 4 32 23 192.58333333333 21.667 173.333 19.25033333333 0.00033333332999064 19.25 18 0 0 0 0 0 0 0 0 2 16.583333333333 16 0 0 0 19.25033333333 19.25033333333 19.25033333333 0 10 1 0 9 0.00033333332999064 19.25 0.00 18 18 0 0 0 16.583333333333 8 0
50 78 Tajikzadeh Mohammad Fest Ja Ja 2025-01-01 5.00 40.00 0.00 20 18 152.3167 5 40 0 0 23 192.31666666667 21.667 173.333 18.98366666667 0.00033333332999064 18.98333333334 18 0 0 0 0 0 0 0 0 1 8.25 16 1 8.0833333333333 0 18.98366666667 18.98366666667 18.98366666667 0 7 5 0 2 0.00033333332999064 18.98333333334 0.00 18 18 0 0 0 8.25 7.9833333333333 8.0833333333333
51 Atrafi Mohammad Nasir Fest Ja Ja 2025-10-01 5.00 4.00 0.00 20 19 162.9667 0 0 0 0 19 162.96666666667 21.667 17.333 145.63366666667 145.63366666667 0 19 0 0 0 0 0 0 0 0 0 0 16 3 22.5 0 145.63366666667 145.63366666667 145.63366666667 0 0 0 5 5 145.63366666667 0 0.00 17 19 0 0 0 0 12.466666666667 22.5
52 Amiri Mohammad Samir Fest Ja Ja 2025-09-01 5.00 40.00 0.00 20 22 190.15 0 0 3 24 25 214.15 21.667 173.333 40.817 0.00033333332999064 40.81666666667 22 0 0 5 17.25 5 12.633333333333 1 10.166666666667 0 0 0 2 12.316666666667 0 40.817 40.817 40.817 0 11 0 0 11 0.00033333332999064 40.81666666667 0.00 20 22 17.25 12.633333333333 10.166666666667 0 0 12.316666666667
53 El Moustaiqid Najlae Fest Ja 2025-09-01 5.00 40.00 0.00 20 151.7 1 8 0 0 21 159.7 21.667 173.333 -13.633 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -1140.63 -13.633 -13.633 0 -1154.263 -5 1 15 9 0 0 0.00 0 0 0 0 0 0 0
54 Jasarevic Noel Fest Nein Ja 2025-09-15 5.00 40.00 0.00 14 116.6333 7 56 0 0 21 172.63333333333 21.667 173.333 -0.69966666667 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0.69966666667 -0.69966666667 0 -0.69966666667 0 7 6 -1 0 0 0.00 12 0 0 0 0 0 0 0
55 Sharh Ghaboos Peyman Fest Ja Ja 2025-03-01 5.00 40.00 0.00 24 212.6333 0 0 0 0 24 212.63333333333 21.667 173.333 39.30033333333 0.00033333332999064 39.3 24 0 0 6 2.6 0 0 0 0 1 8.25 21 2 16.683333333333 0 39.30033333333 39.30033333333 39.30033333333 0 20 0 0 20 0.00033333332999064 39.3 0.00 24 24 2.6 0 0 8.25 17.1 16.683333333333
56 227 Feilbach Raoul Nikolai Fest Nein Ja 2025-09-01 5.00 40.00 0.00 20 24 215.8833 0 0 0 0 24 215.88333333333 21.667 173.333 42.55033333333 0.00033333332999064 42.55 0 0 16 55.5 16 40.75 1 8.7 4 17.483333333333 0 0 0 0.007 42.55033333333 42.55033333333 42.55033333333 0.0069999999999979 -14 0 20 6 0.00033333332999064 42.55 0.00 24 0 55.5 40.75 8.7 17.483333333333 0 0
57 Hatami Reza Fest Ja Ja 2025-09-15 5.00 40.00 0.00 20 20 173.9667 0 0 0 0 20 173.96666666667 21.667 173.333 0.63366666667 0.00033333332999064 0.63333333334 20 0 0 0 0 0 0 0 0 0 0 18 2 16.6 0 0.63366666667 0.63366666667 0.63366666666999 9.9920072216264E-15 0 0 6 6 0.00033333332999064 0.63333333334 0.00 20 20 0 0 0 0 13.366666666667 16.6
58 137 Pourhossein Reza Fest Ja Ja 2025-04-01 5.00 40.00 0.00 20 21 181.55 0 0 0 0 21 181.55 21.667 173.333 8.217 0.00033333332999064 8.21666666667 21 0 0 12 36.533333333333 7 15.016666666667 0 0 2 16.316666666667 7 1 8.0166666666667 0 8.217 8.217 8.217 0 29 0 0 29 0.00033333332999064 8.21666666667 0.00 20 21 36.533333333333 15.016666666667 0 16.316666666667 4.1666666666667 8.0166666666667
59 225 Hosseini Jahed Sajad Fest Ja Ja 2025-03-03 5.00 40.00 0.00 20 21 193.3833 2 16 0 0 23 209.38333333333 21.667 173.333 36.05033333333 0.00033333332999064 36.05 21 0 0 0 0 0 0 1 8.4666666666667 1 8.7833333333333 18 0 0 0 36.05033333333 36.05033333333 36.05033333333 0 39 2 -17 20 0.00033333332999064 36.05 0.00 20 21 0 0 8.4666666666667 8.7833333333333 24.3 0
60 Saki Sara Fest Ja Ja 2025-02-01 5.00 40.00 0.00 14 117.6333 10 80 0 0 24 197.63333333333 21.667 173.333 24.30033333333 0.00033333332999064 24.3 14 0 0 11 35.05 0 0 1 8.7 2 16.383333333333 0 0 0 0 24.30033333333 24.30033333333 24.30033333333 0 23 10 -2 11 0.00033333332999064 24.3 0.00 14 14 35.05 0 8.7 16.383333333333 0 0
61 Perez Y Waldt Sebastian Fest Nein Ja 2024-09-23 5.00 40.00 0.00 22 181.9 0 0 0 0 22 181.9 21.667 173.333 8.567 0.00033333332999064 8.56666666667 0 0 4 14.1 0 0 0 0 1 8.0166666666667 11 3 24.666666666667 -756.446 8.567 8.567 0 -747.879 14 0 0 14 0 0 0.00 17 0 14.1 0 0 8.0166666666667 6.5166666666667 24.666666666667
62 38 Hosseini Seyed Hamid Fest Ja Ja 2025-03-10 5.00 40.00 0.00 20 18 153.4167 0 0 5 40 23 193.41666666667 21.667 173.333 20.08366666667 0.00033333332999064 20.08333333334 18 0 0 0 0 0 0 0 0 2 16.5 15 1 8.0833333333333 0 20.08366666667 20.08366666667 20.08366666667 0 26 0 0 26 0.00033333332999064 20.08333333334 0.00 18 18 0 0 0 16.5 8.8333333333333 8.0833333333333
63 Jabbari Shahram Fest Nein Ja 2025-04-01 5.00 40.00 0.00 20 175.2167 0 0 0 0 20 175.21666666667 21.667 173.333 1.88366666667 0.00033333332999064 1.88333333334 0 0 0 0 0 0 0 0 0 0 18 2 16.95 0 1.88366666667 1.88366666667 1.88366666667 0 -5 0 0 -5 0.00033333332999064 1.88333333334 0.00 20 0 0 0 0 0 14.266666666667 16.95
64 124 Imanian Shahriyar Fest Ja Ja 2025-03-10 5.00 40.00 0.00 20 12 104.2833 12 96 0 0 24 200.28333333333 21.667 173.333 26.95033333333 0.00033333332999064 26.95 12 0 0 1 0.083333333333333 0 0 1 8.35 0 0 10 1 8.1666666666667 0 26.95033333333 26.95033333333 26.95033333333 0 36 12 0 24 0.00033333332999064 26.95 0.00 12 12 0.083333333333333 0 8.35 0 7.6833333333333 8.1666666666667
65 164 Tokhi Shokrollah Fest Ja Ja 2025-03-04 5.00 40.00 0.00 20 18 152.6 5 40 0 0 23 192.6 21.667 173.333 19.267 0.00033333332999064 19.26666666667 18 0 0 14 47.75 9 21.483333333333 0 0 2 9.3 2 1 8.6333333333333 0 19.267 19.267 19.267 0 14 5 0 9 0.00033333332999064 19.26666666667 0.00 18 18 47.75 21.483333333333 0 9.3 0.93333333333333 8.6333333333333
66 210 Rahimzadeh Siamak Fest Ja Ja 2025-03-01 5.00 40.00 0.00 20 23 197.5 0 0 0 0 23 197.5 21.667 173.333 24.167 0.00033333332999064 24.16666666667 23 0 0 0 0 0 0 0 0 2 16.25 18 3 25.016666666667 0.027 24.167 24.167 24.167 0.027000000000001 39 0 0 39 0.00033333332999064 24.16666666667 0.00 23 23 0 0 0 16.25 12.233333333333 25.016666666667
67 Florescu Silviu-Cezar Fest Nein Ja 2025-10-01 5.00 40.00 0.00 20 173.5333 0 0 0 0 20 173.53333333333 21.667 173.333 0.20033333333001 0.00033333332999064 0.20000000000002 0 0 2 0.28333333333333 0 0 0 0 0 0 19 1 8.35 0 0.20033333333001 0.20033333333001 0.20033333333001 0 9 0 0 9 0.00033333332999064 0.20000000000002 0.00 20 0 0.28333333333333 0 0 0 12.9 8.35
68 Vafaei Soheil Fest Ja Ja 2025-04-01 5.00 40.00 0.00 18 155.4833 3 24 1 8 22 187.48333333333 21.667 173.333 14.15033333333 0.00033333332999064 14.15 18 0 0 0 0 0 0 0 0 0 0 17 1 8.0166666666667 0 14.15033333333 14.15033333333 14.15033333333 0 -10 3 0 -13 0.00033333332999064 14.15 0.00 18 18 0 0 0 0 11.466666666667 8.0166666666667
69 Toraman Tolga Can Fest Nein Ja 2025-09-15 5.00 40.00 0.00 20 22 183.1333 0 0 0 0 22 183.13333333333 21.667 173.333 9.80033333333 0.00033333332999064 9.8 0 0 0 0 0 0 1 9.5 0 0 17 2 16.3 0 9.80033333333 9.80033333333 9.80033333333 0 0 0 0 0 0.00033333332999064 9.8 0.00 20 0 0 0 9.5 0 6.3666666666667 16.3
70 14 Haugg Uwe Fest Ja Ja 2025-01-01 4.00 32.00 0.00 18 6 50.6167 8 64 4 32 18 146.61666666667 17.333 138.667 7.94966666667 7.94966666667 0 6 0 0 0 0 0 0 0 0 0 0 5 1 6.9666666666667 -0.2 7.94966666667 7.94966666667 7.74966666667 0 -10 8 0 -18 7.74966666667 0 0.00 5 6 0 0 0 0 3.65 6.9666666666667
71 231 Shokripour Vahid Fest Ja Ja 2025-01-01 5.00 40.00 0.00 20 24 208.1167 0 0 0 0 24 208.11666666667 21.667 173.333 34.78366666667 0.00033333332999064 34.78333333334 24 0 0 0 0 0 0 1 9.5 2 16.166666666667 18 3 25.333333333333 0 34.78366666667 34.78366666667 34.78366666667 0 35 0 0 35 0.00033333332999064 34.78333333334 0.00 24 24 0 0 9.5 16.166666666667 13.116666666667 25.333333333333
72 226 Konak Zidar Fest Nein Ja 2025-05-01 5.00 40.00 0.00 20 22 183.65 0 0 0 0 22 183.65 21.667 173.333 10.317 0.00033333332999064 10.31666666667 0 0 0 0 0 0 2 18.5 1 7 0 2 16.433333333333 0 10.317 10.317 10.317 0 16 0 0 16 0.00033333332999064 10.31666666667 0.00 15 0 0 0 18.5 7 0 16.433333333333
73 1293 11086.0499 101 799 32 256.5 1426 12141.55 1438.687 11344.646 970.23699999999 9 73 175 515.91666666667 67 159.08333333333 20 181.8 66 479.58333333333 841 91 738.55 970.23699999999 970.23699999999 1267.0006666666 -2939.2536666667 101 241 709 211.53366666651 1055.4670000001 0 1209 515.91666666667 159.08333333333 181.8 479.58333333333 607.15 738.55

View File

@ -0,0 +1,84 @@
Nachname;Vorname;Verpflegungspauschale;Vertr. Stundenlohn;Arbeitsstunden lt. Vertrag;Gearbeitet Stunden;Gearbeitet Tage;Pünktlichkeitsprämie anzahl;Krankheitstage x 8 Stunden;davon Überstunden;Tourengeld Anzahl;Tourengeld-Bonus (Platz 1-3;Gesundheitsbonus abzuziehen? (ja/nein);Urlaubsgeld (44 EUR pro tag);P-schein Zulage;Wochenendzuschlag 15%;Sonntagszuschlag 50%;Feiertagszuschlag 125%;Feiertagszuschlag 150%;Nachtzuschlag 25%;Nachtzuschlag 40%
Malani Karizi;Arasch;24;14.42;173.32;211.98333333333332;24;24;0;38.651333333333305;41.5;500;;0;0;24.7;0;8.25;0;2.6;0
Saeedpour;Fouad;23;14.42;173.32;203.28333333333333;23;23;0;29.951333333333316;41;400;;0;23;25.25;26.8;9;0;4.25;0
Sharh Ghaboos;Peyman;24;14.42;173.32;212.63333333333333;24;24;0;39.30133333333331;37;300;;0;24;16.683333333333;0;8.25;0;2.6;0
Kalantari;Kaveh;18;14.42;173.32;159.05;18;18;0;25.717999999999996;31;0;;5;18;16.916666666667;10.25;9.9;0;4.25;0
Ahmadpour;Amirhossein;21;14.42;173.32;193.63333333333333;21;21;0;36.30133333333331;31;0;;2;21;0;8.4666666666667;8.7833333333333;0;0;0
Hosseini Jahed;Sajad;20;14.42;173.32;193.38333333333333;21;21;0;44.05133333333331;30;0;;3;21;0;8.4666666666667;8.7833333333333;0;0;0
Rasul Khani;Mohammad;24;14.42;173.32;209.48333333333332;24;24;0;36.151333333333305;29.5;0;;0;24;8.0166666666667;17.45;8.25;0;2.25;0
Moshtagh Askari;Adel;25;15.58;173.32;218.41666666666666;25;23;0;45.08466666666664;29;0;;0;25;24.95;0;18.65;0;0;0
Rasulkhani;Hossein;23;14.42;173.32;194.86666666666667;23;22;0;21.53466666666666;28;0;;0;23;32.366666666667;9.25;9;0;10.433333333333;0
Sajaei Khouzestani;Farid;22;14.42;173.32;197.43333333333334;22;22;0;24.101333333333322;26;0;;0;22;16.366666666667;0;17.316666666667;0;0;0
Perez Y Waldt;Sebastian;17;14.42;173.32;181.89999999999998;22;15;0;8.567999999999962;25;0;;0;0;24.666666666667;0;8.0166666666667;0;14.1;0
Rahimzadeh;Siamak;23;14.42;173.32;197.5;23;23;0;24.167999999999985;25;0;;0;23;25.016666666667;0;16.25;0;0;0
Shokripour;Vahid;24;15;173.32;208.11666666666667;24;24;0;34.78466666666666;23.5;0;;0;24;25.333333333333;9.5;16.166666666667;0;0;0
Hosseini;Seyed Hamid;18;14.42;173.32;153.41666666666666;18;18;5;20.084666666666642;23;0;ja;0;18;8.0833333333333;0;16.5;0;0;0
Safari;Abdul Latif ;20;14.42;173.32;180.41666666666669;20;20;0;47.08466666666667;23;0;;5;20;17.35;0;16.166666666667;0;0;0
Pourhossein;Reza;20;14.42;173.32;181.55;21;21;0;8.217999999999996;20.5;0;;0;21;8.0166666666667;0;16.316666666667;0;36.533333333333;15.016666666667
Salehi;Iman;26;17.31;173.32;220.1;26;26;0;46.76799999999998;20;0;;0;26;8.0166666666667;18.2;16.383333333333;0;68.766666666667;13.483333333333
Rohany Oskoee;Iman;22;16.15;173.32;190.38333333333333;22;10;0;17.05133333333331;20;0;;0;0;8.0333333333333;0;8.7333333333333;0;0;0
Sahem;Khalid;24;14.42;173.32;204.35000000000002;24;24;0;31.018000000000008;18.5;0;;0;24;26.033333333333;0;16.833333333333;0;0;0
Toraman;Tolga Can;20;14.42;173.32;183.13333333333333;22;19;0;9.80133333333331;18;0;;0;0;16.3;9.5;0;0;0;0
Tokhi;Shokrollah;18;14.42;173.32;152.6;18;18;0;19.26799999999998;17;0;;5;18;8.6333333333333;0;9.3;0;47.75;21.483333333333
Eggerstedt;Matthias;21;15.58;173.32;190.13333333333333;22;22;0;16.80133333333331;16;0;;0;22;0;0;8.6333333333333;0;0;0
Mihailescu;Adrian Lucian;18;16.73;173.32;177.56666666666666;21;4;0;4.234666666666648;14.5;0;;0;21;24.116666666667;0;8.6333333333333;0;0;0
Hastedt;David;11;14.93;138.656;112.31666666666666;14;11;4;5.651066666666651;14;0;ja;0;0;0;0;0;0;0;0
Faridani Rad;Farzad;21;15;173.32;174.08333333333334;21;21;2;16.751333333333328;13.5;0;ja;0;21;0;0;16.15;0;44.5;5.6666666666667
Tajikzadeh;Mohammad;18;16.15;173.32;152.31666666666666;18;18;0;18.984666666666648;13.5;0;;5;18;8.0833333333333;0;8.25;0;0;0
Saki;Sara;14;15;173.32;117.63333333333333;14;14;0;32.30133333333331;13;0;;11;14;0;8.7;16.383333333333;0;35.05;0
Imanian;Shahriyar;12;14.42;173.32;104.28333333333333;12;12;0;26.951333333333316;12.5;0;;12;12;8.1666666666667;8.35;0;0;0.083333333333333;0
Sabahi;Mohammad;18;16.15;173.32;152.58333333333334;18;17;4;19.251333333333328;11;0;ja;1;18;0;0;16.583333333333;0;0;0
Florescu;Silviu-Cezar;20;15;173.32;173.53333333333336;20;20;0;0.2013333333333449;10.5;0;;0;0;8.35;0;0;0;0.28333333333333;0
Florescu;Silviu-Cezar;20;15;173.32;173.53333333333336;20;20;0;0.2013333333333449;10.5;0;;0;0;8.35;0;0;0;0.28333333333333;0
Altner;Christian Van Manh;21;15;173.32;184.08333333333334;22;22;0;10.751333333333328;9;0;;0;22;8.0166666666667;0;7.9333333333333;0;0.2;0
Pourhossein;Milad;14;14.42;173.32;125.61666666666665;15;15;0;8.28466666666663;8.5;0;;7;15;16.6;0;7.9333333333333;0;0;0
Pouzesh;Esmail;24;16.15;173.32;202.08333333333334;24;24;0;28.751333333333328;7;0;;0;24;0;9.5;17.816666666667;0;70.416666666667;20.6
Khodayaran;Hossein;12;16.15;173.32;105.30000000000001;13;13;0;11.967999999999996;7;0;;10;13;0;0;8.1333333333333;0;31;5.6666666666667
Juyande;Masoud;22;15;173.32;182.9;22;22;0;9.56799999999999;7;0;;0;0;0;0;8.5166666666667;0;68.1;23.783333333333
Maleki;Mohammad;3;14.44;103.992;41.11666666666667;5;1;2;-46.88253333333334;3.5;0;ja;0;5;0;0;0;0;0;0
Saffarnajib;Majid;16;15.38;90.99300000000001;133.36666666666665;16;16;0;42.36736666666664;1;0;;0;16;0;0;0;0;0;0
Jasarevic;Noel;12;14.42;173.32;116.63333333333333;14;0;0;-0.6986666666666892;0;0;ja;0;0;0;0;0;0;0;0
Movafagh;Farzad ;23;12.82;173.32;202.61666666666667;23;0;1;37.28466666666666;0;0;ja;0;0;24.55;0;0;0;0;0
Haralambous;Martin;0;12.41;173.32;0;0;0;0;-173.33200000000002;0;0;ja;0;0;0;0;0;0;0;0
Hazrati;Kamyar;20;12.82;173.32;182.75;21;0;0;9.417999999999985;0;0;ja;0;0;16.616666666667;0;0;0;0;0
Krüger;Julian;19;22.5;173.32;197.35;23;0;0;24.01799999999998;0;0;ja;0;0;15.116666666667;0;14.283333333333;0;0;0
Arabnia;Mehdi;21;12.82;173.32;194.38333333333333;22;0;0;21.05133333333331;0;0;ja;0;0;15.783333333333;0;0;0;0;0
Taghadosi;Mehdi;21;12.82;173.32;188.93333333333334;22;0;0;15.601333333333322;0;0;ja;0;0;16.6;0;6.3;0;0;0
Jalali Wagener;Mehdi;24;12.82;173.32;204.48333333333332;24;0;0;31.151333333333305;0;0;ja;0;0;16.35;0;8.5333333333333;0;0;0
Mazidi;Mehrdad;21;12.82;173.32;183.16666666666666;21;0;0;9.834666666666642;0;0;ja;0;0;17.033333333333;0;8.1166666666667;0;0;0
Koell;Melanie Nadine;0;15.58;173.32;0;0;0;0;-173.33200000000002;0;0;ja;0;0;0;0;0;0;0;0
Esmaeili Miandehi;Milad;0;12.82;173.32;0;0;0;0;-173.33200000000002;0;0;ja;0;0;0;0;0;0;0;0
Mohamed Salah;Hussin Noureldin;21;12.82;173.32;186.58333333333334;22;0;0;13.251333333333328;0;0;ja;0;0;24.55;0;8.5166666666667;0;0;0
Tokhi;Haroun;0;14.42;173.32;0;0;0;0;-173.33200000000002;0;0;ja;0;0;0;0;0;0;0;0
Feilbach;Raoul Nikolai ;24;19.62;173.32;215.88333333333335;24;0;0;42.55133333333334;0;0;ja;0;0;0;8.7;17.483333333333;0;55.5;40.75
Hashimazada;Firuz;18;12.82;173.32;155.35;18;0;4;14.01799999999998;0;0;ja;0;0;0;0;8.5333333333333;0;0;0
Esser;Detlef;0;12;121.32400000000001;0;0;0;0;-121.3324;0;0;ja;0;0;0;0;0;0;0;0
Atrafi;Mohammad Nasir;17;12.82;17.332;162.96666666666667;19;0;0;145.63346666666666;0;0;ja;0;0;22.5;0;0;0;0;0
Amiri;Mohammad Samir;20;19.62;173.32;190.14999999999998;22;0;0;16.817999999999962;0;0;ja;0;0;12.316666666667;10.166666666667;0;0;17.25;12.633333333333
Babaei Dezghi;Mojtaba;0;15;160.321;0;0;0;0;-162.49875;0;0;ja;0;0;0;0;0;0;0;0
Aydin;Muhammet Ali;0;14.51;73.661;0;0;0;0;-73.6661;0;0;ja;0;0;0;0;0;0;0;0
El Moustaiqid;Najlae;9;12.82;173.32;151.7;20;0;0;-5.632000000000026;0;0;ja;0;0;0;0;0;0;0;0
Heucke;Niklas;0;15;173.32;0;0;0;0;-173.33200000000002;0;0;ja;0;0;0;0;0;0;0;0
Saei;Kasra;0;5.65;173.32;37.266666666666666;6;0;0;-56.06533333333335;0;0;ja;0;0;0;0;0;0;0;0
Latza;Lelia Eleonore;0;6.35;173.32;0;0;0;0;-173.33200000000002;0;0;ja;0;0;0;0;0;0;0;0
Karimloo;Hamidreza;0;12.82;173.32;0;0;0;0;-173.33200000000002;0;0;ja;0;0;0;0;0;0;0;0
Hatami;Reza;20;12.82;173.32;173.9666666666667;20;0;0;0.634666666666682;0;0;ja;0;0;16.6;0;0;0;0;0
Abdallah;Delshad;0;12.82;173.32;0;0;0;0;-173.33200000000002;0;0;ja;0;0;0;0;0;0;0;0
Sidiqi;Sahel;0;14.42;173.32;0;0;0;0;-173.33200000000002;0;0;ja;0;0;0;0;0;0;0;0
Golmohammadi;Davood ;20;12.82;173.32;182.2166666666667;20;0;0;8.884666666666682;0;0;ja;0;0;0;0;0;0;0;0
Shahrasebi;David;0;0;173.32;0;0;0;0;-173.33200000000002;0;0;ja;0;0;0;0;0;0;0;0
Lolies;Danny;17;12.82;173.32;158.9;19;0;4;18.06799999999999;0;0;ja;0;0;17.05;0;5.55;0;0;0
Emkani;Daniel;12;22.48;108.325;118.45;14;0;0;10.117499999999993;0;0;ja;0;0;13.3;0;8.6833333333333;0;0;0
Askarian Pour;Seyed Hosein;0;15.58;173.32;0;0;0;0;-173.33200000000002;0;0;ja;0;0;0;0;0;0;0;0
Jabbari;Shahram;20;12.82;173.32;175.21666666666667;20;0;0;1.8846666666666536;0;0;ja;0;0;16.95;0;0;0;0;0
Haghighivand;Danial;19;12.82;173.32;166.03333333333333;19;0;0;8.701333333333316;0;0;ja;0;0;0;0;0;0;0;0
Alami;Shoaib;0;12.82;173.32;0;0;0;0;-173.33200000000002;0;0;ja;0;0;0;0;0;0;0;0
Mohammadi;Danial;20;12.82;173.32;182.86666666666665;21;0;0;9.53466666666663;0;0;ja;0;0;16.616666666667;0;8.3333333333333;0;0;0
Aba;Cuan;0;5.65;173.32;50.25;8;0;0;-75.08200000000002;0;0;ja;0;0;0;0;0;0;0;0
Modrau;Cara Marleen;0;14.42;173.32;0;0;0;0;-173.33200000000002;0;0;ja;0;0;0;0;0;0;0;0
Rahmati;Behnam;20;12.82;173.32;176.04999999999998;20;0;1;11.217999999999968;0;0;ja;0;0;0;0;0;0;0;0
Vafaei;Soheil;18;12.82;173.32;155.48333333333332;18;0;1;14.151333333333305;0;0;ja;0;0;8.0166666666667;0;0;0;0;0
Ziaudin;Atiq;21;12.82;173.32;194.31666666666666;23;0;0;20.984666666666648;0;0;ja;0;0;15.05;0;6.4333333333333;0;0;0
Haugg;Uwe;5;12.82;138.656;50.616666666666674;6;0;4;-4.848933333333349;0;0;ja;0;0;6.9666666666667;0;0;0;0;0
Esmaeili Dashtbayaz;Amin;20;12.82;173.32;171.26666666666665;20;0;0;-2.0653333333333634;0;0;ja;0;0;16.083333333333;0;0;0;0;0
Konak;Zidar;15;22.5;173.32;183.65000000000003;22;0;0;10.31800000000002;0;0;ja;0;0;16.433333333333;18.5;7;0;0;0
1 Nachname Vorname Verpflegungspauschale Vertr. Stundenlohn Arbeitsstunden lt. Vertrag Gearbeitet Stunden Gearbeitet Tage Pünktlichkeitsprämie anzahl Krankheitstage x 8 Stunden davon Überstunden Tourengeld Anzahl Tourengeld-Bonus (Platz 1-3 Gesundheitsbonus abzuziehen? (ja/nein) Urlaubsgeld (44 EUR pro tag) P-schein Zulage Wochenendzuschlag 15% Sonntagszuschlag 50% Feiertagszuschlag 125% Feiertagszuschlag 150% Nachtzuschlag 25% Nachtzuschlag 40%
2 Malani Karizi Arasch 24 14.42 173.32 211.98333333333332 24 24 0 38.651333333333305 41.5 500 0 0 24.7 0 8.25 0 2.6 0
3 Saeedpour Fouad 23 14.42 173.32 203.28333333333333 23 23 0 29.951333333333316 41 400 0 23 25.25 26.8 9 0 4.25 0
4 Sharh Ghaboos Peyman 24 14.42 173.32 212.63333333333333 24 24 0 39.30133333333331 37 300 0 24 16.683333333333 0 8.25 0 2.6 0
5 Kalantari Kaveh 18 14.42 173.32 159.05 18 18 0 25.717999999999996 31 0 5 18 16.916666666667 10.25 9.9 0 4.25 0
6 Ahmadpour Amirhossein 21 14.42 173.32 193.63333333333333 21 21 0 36.30133333333331 31 0 2 21 0 8.4666666666667 8.7833333333333 0 0 0
7 Hosseini Jahed Sajad 20 14.42 173.32 193.38333333333333 21 21 0 44.05133333333331 30 0 3 21 0 8.4666666666667 8.7833333333333 0 0 0
8 Rasul Khani Mohammad 24 14.42 173.32 209.48333333333332 24 24 0 36.151333333333305 29.5 0 0 24 8.0166666666667 17.45 8.25 0 2.25 0
9 Moshtagh Askari Adel 25 15.58 173.32 218.41666666666666 25 23 0 45.08466666666664 29 0 0 25 24.95 0 18.65 0 0 0
10 Rasulkhani Hossein 23 14.42 173.32 194.86666666666667 23 22 0 21.53466666666666 28 0 0 23 32.366666666667 9.25 9 0 10.433333333333 0
11 Sajaei Khouzestani Farid 22 14.42 173.32 197.43333333333334 22 22 0 24.101333333333322 26 0 0 22 16.366666666667 0 17.316666666667 0 0 0
12 Perez Y Waldt Sebastian 17 14.42 173.32 181.89999999999998 22 15 0 8.567999999999962 25 0 0 0 24.666666666667 0 8.0166666666667 0 14.1 0
13 Rahimzadeh Siamak 23 14.42 173.32 197.5 23 23 0 24.167999999999985 25 0 0 23 25.016666666667 0 16.25 0 0 0
14 Shokripour Vahid 24 15 173.32 208.11666666666667 24 24 0 34.78466666666666 23.5 0 0 24 25.333333333333 9.5 16.166666666667 0 0 0
15 Hosseini Seyed Hamid 18 14.42 173.32 153.41666666666666 18 18 5 20.084666666666642 23 0 ja 0 18 8.0833333333333 0 16.5 0 0 0
16 Safari Abdul Latif 20 14.42 173.32 180.41666666666669 20 20 0 47.08466666666667 23 0 5 20 17.35 0 16.166666666667 0 0 0
17 Pourhossein Reza 20 14.42 173.32 181.55 21 21 0 8.217999999999996 20.5 0 0 21 8.0166666666667 0 16.316666666667 0 36.533333333333 15.016666666667
18 Salehi Iman 26 17.31 173.32 220.1 26 26 0 46.76799999999998 20 0 0 26 8.0166666666667 18.2 16.383333333333 0 68.766666666667 13.483333333333
19 Rohany Oskoee Iman 22 16.15 173.32 190.38333333333333 22 10 0 17.05133333333331 20 0 0 0 8.0333333333333 0 8.7333333333333 0 0 0
20 Sahem Khalid 24 14.42 173.32 204.35000000000002 24 24 0 31.018000000000008 18.5 0 0 24 26.033333333333 0 16.833333333333 0 0 0
21 Toraman Tolga Can 20 14.42 173.32 183.13333333333333 22 19 0 9.80133333333331 18 0 0 0 16.3 9.5 0 0 0 0
22 Tokhi Shokrollah 18 14.42 173.32 152.6 18 18 0 19.26799999999998 17 0 5 18 8.6333333333333 0 9.3 0 47.75 21.483333333333
23 Eggerstedt Matthias 21 15.58 173.32 190.13333333333333 22 22 0 16.80133333333331 16 0 0 22 0 0 8.6333333333333 0 0 0
24 Mihailescu Adrian Lucian 18 16.73 173.32 177.56666666666666 21 4 0 4.234666666666648 14.5 0 0 21 24.116666666667 0 8.6333333333333 0 0 0
25 Hastedt David 11 14.93 138.656 112.31666666666666 14 11 4 5.651066666666651 14 0 ja 0 0 0 0 0 0 0 0
26 Faridani Rad Farzad 21 15 173.32 174.08333333333334 21 21 2 16.751333333333328 13.5 0 ja 0 21 0 0 16.15 0 44.5 5.6666666666667
27 Tajikzadeh Mohammad 18 16.15 173.32 152.31666666666666 18 18 0 18.984666666666648 13.5 0 5 18 8.0833333333333 0 8.25 0 0 0
28 Saki Sara 14 15 173.32 117.63333333333333 14 14 0 32.30133333333331 13 0 11 14 0 8.7 16.383333333333 0 35.05 0
29 Imanian Shahriyar 12 14.42 173.32 104.28333333333333 12 12 0 26.951333333333316 12.5 0 12 12 8.1666666666667 8.35 0 0 0.083333333333333 0
30 Sabahi Mohammad 18 16.15 173.32 152.58333333333334 18 17 4 19.251333333333328 11 0 ja 1 18 0 0 16.583333333333 0 0 0
31 Florescu Silviu-Cezar 20 15 173.32 173.53333333333336 20 20 0 0.2013333333333449 10.5 0 0 0 8.35 0 0 0 0.28333333333333 0
32 Florescu Silviu-Cezar 20 15 173.32 173.53333333333336 20 20 0 0.2013333333333449 10.5 0 0 0 8.35 0 0 0 0.28333333333333 0
33 Altner Christian Van Manh 21 15 173.32 184.08333333333334 22 22 0 10.751333333333328 9 0 0 22 8.0166666666667 0 7.9333333333333 0 0.2 0
34 Pourhossein Milad 14 14.42 173.32 125.61666666666665 15 15 0 8.28466666666663 8.5 0 7 15 16.6 0 7.9333333333333 0 0 0
35 Pouzesh Esmail 24 16.15 173.32 202.08333333333334 24 24 0 28.751333333333328 7 0 0 24 0 9.5 17.816666666667 0 70.416666666667 20.6
36 Khodayaran Hossein 12 16.15 173.32 105.30000000000001 13 13 0 11.967999999999996 7 0 10 13 0 0 8.1333333333333 0 31 5.6666666666667
37 Juyande Masoud 22 15 173.32 182.9 22 22 0 9.56799999999999 7 0 0 0 0 0 8.5166666666667 0 68.1 23.783333333333
38 Maleki Mohammad 3 14.44 103.992 41.11666666666667 5 1 2 -46.88253333333334 3.5 0 ja 0 5 0 0 0 0 0 0
39 Saffarnajib Majid 16 15.38 90.99300000000001 133.36666666666665 16 16 0 42.36736666666664 1 0 0 16 0 0 0 0 0 0
40 Jasarevic Noel 12 14.42 173.32 116.63333333333333 14 0 0 -0.6986666666666892 0 0 ja 0 0 0 0 0 0 0 0
41 Movafagh Farzad 23 12.82 173.32 202.61666666666667 23 0 1 37.28466666666666 0 0 ja 0 0 24.55 0 0 0 0 0
42 Haralambous Martin 0 12.41 173.32 0 0 0 0 -173.33200000000002 0 0 ja 0 0 0 0 0 0 0 0
43 Hazrati Kamyar 20 12.82 173.32 182.75 21 0 0 9.417999999999985 0 0 ja 0 0 16.616666666667 0 0 0 0 0
44 Krüger Julian 19 22.5 173.32 197.35 23 0 0 24.01799999999998 0 0 ja 0 0 15.116666666667 0 14.283333333333 0 0 0
45 Arabnia Mehdi 21 12.82 173.32 194.38333333333333 22 0 0 21.05133333333331 0 0 ja 0 0 15.783333333333 0 0 0 0 0
46 Taghadosi Mehdi 21 12.82 173.32 188.93333333333334 22 0 0 15.601333333333322 0 0 ja 0 0 16.6 0 6.3 0 0 0
47 Jalali Wagener Mehdi 24 12.82 173.32 204.48333333333332 24 0 0 31.151333333333305 0 0 ja 0 0 16.35 0 8.5333333333333 0 0 0
48 Mazidi Mehrdad 21 12.82 173.32 183.16666666666666 21 0 0 9.834666666666642 0 0 ja 0 0 17.033333333333 0 8.1166666666667 0 0 0
49 Koell Melanie Nadine 0 15.58 173.32 0 0 0 0 -173.33200000000002 0 0 ja 0 0 0 0 0 0 0 0
50 Esmaeili Miandehi Milad 0 12.82 173.32 0 0 0 0 -173.33200000000002 0 0 ja 0 0 0 0 0 0 0 0
51 Mohamed Salah Hussin Noureldin 21 12.82 173.32 186.58333333333334 22 0 0 13.251333333333328 0 0 ja 0 0 24.55 0 8.5166666666667 0 0 0
52 Tokhi Haroun 0 14.42 173.32 0 0 0 0 -173.33200000000002 0 0 ja 0 0 0 0 0 0 0 0
53 Feilbach Raoul Nikolai 24 19.62 173.32 215.88333333333335 24 0 0 42.55133333333334 0 0 ja 0 0 0 8.7 17.483333333333 0 55.5 40.75
54 Hashimazada Firuz 18 12.82 173.32 155.35 18 0 4 14.01799999999998 0 0 ja 0 0 0 0 8.5333333333333 0 0 0
55 Esser Detlef 0 12 121.32400000000001 0 0 0 0 -121.3324 0 0 ja 0 0 0 0 0 0 0 0
56 Atrafi Mohammad Nasir 17 12.82 17.332 162.96666666666667 19 0 0 145.63346666666666 0 0 ja 0 0 22.5 0 0 0 0 0
57 Amiri Mohammad Samir 20 19.62 173.32 190.14999999999998 22 0 0 16.817999999999962 0 0 ja 0 0 12.316666666667 10.166666666667 0 0 17.25 12.633333333333
58 Babaei Dezghi Mojtaba 0 15 160.321 0 0 0 0 -162.49875 0 0 ja 0 0 0 0 0 0 0 0
59 Aydin Muhammet Ali 0 14.51 73.661 0 0 0 0 -73.6661 0 0 ja 0 0 0 0 0 0 0 0
60 El Moustaiqid Najlae 9 12.82 173.32 151.7 20 0 0 -5.632000000000026 0 0 ja 0 0 0 0 0 0 0 0
61 Heucke Niklas 0 15 173.32 0 0 0 0 -173.33200000000002 0 0 ja 0 0 0 0 0 0 0 0
62 Saei Kasra 0 5.65 173.32 37.266666666666666 6 0 0 -56.06533333333335 0 0 ja 0 0 0 0 0 0 0 0
63 Latza Lelia Eleonore 0 6.35 173.32 0 0 0 0 -173.33200000000002 0 0 ja 0 0 0 0 0 0 0 0
64 Karimloo Hamidreza 0 12.82 173.32 0 0 0 0 -173.33200000000002 0 0 ja 0 0 0 0 0 0 0 0
65 Hatami Reza 20 12.82 173.32 173.9666666666667 20 0 0 0.634666666666682 0 0 ja 0 0 16.6 0 0 0 0 0
66 Abdallah Delshad 0 12.82 173.32 0 0 0 0 -173.33200000000002 0 0 ja 0 0 0 0 0 0 0 0
67 Sidiqi Sahel 0 14.42 173.32 0 0 0 0 -173.33200000000002 0 0 ja 0 0 0 0 0 0 0 0
68 Golmohammadi Davood 20 12.82 173.32 182.2166666666667 20 0 0 8.884666666666682 0 0 ja 0 0 0 0 0 0 0 0
69 Shahrasebi David 0 0 173.32 0 0 0 0 -173.33200000000002 0 0 ja 0 0 0 0 0 0 0 0
70 Lolies Danny 17 12.82 173.32 158.9 19 0 4 18.06799999999999 0 0 ja 0 0 17.05 0 5.55 0 0 0
71 Emkani Daniel 12 22.48 108.325 118.45 14 0 0 10.117499999999993 0 0 ja 0 0 13.3 0 8.6833333333333 0 0 0
72 Askarian Pour Seyed Hosein 0 15.58 173.32 0 0 0 0 -173.33200000000002 0 0 ja 0 0 0 0 0 0 0 0
73 Jabbari Shahram 20 12.82 173.32 175.21666666666667 20 0 0 1.8846666666666536 0 0 ja 0 0 16.95 0 0 0 0 0
74 Haghighivand Danial 19 12.82 173.32 166.03333333333333 19 0 0 8.701333333333316 0 0 ja 0 0 0 0 0 0 0 0
75 Alami Shoaib 0 12.82 173.32 0 0 0 0 -173.33200000000002 0 0 ja 0 0 0 0 0 0 0 0
76 Mohammadi Danial 20 12.82 173.32 182.86666666666665 21 0 0 9.53466666666663 0 0 ja 0 0 16.616666666667 0 8.3333333333333 0 0 0
77 Aba Cuan 0 5.65 173.32 50.25 8 0 0 -75.08200000000002 0 0 ja 0 0 0 0 0 0 0 0
78 Modrau Cara Marleen 0 14.42 173.32 0 0 0 0 -173.33200000000002 0 0 ja 0 0 0 0 0 0 0 0
79 Rahmati Behnam 20 12.82 173.32 176.04999999999998 20 0 1 11.217999999999968 0 0 ja 0 0 0 0 0 0 0 0
80 Vafaei Soheil 18 12.82 173.32 155.48333333333332 18 0 1 14.151333333333305 0 0 ja 0 0 8.0166666666667 0 0 0 0 0
81 Ziaudin Atiq 21 12.82 173.32 194.31666666666666 23 0 0 20.984666666666648 0 0 ja 0 0 15.05 0 6.4333333333333 0 0 0
82 Haugg Uwe 5 12.82 138.656 50.616666666666674 6 0 4 -4.848933333333349 0 0 ja 0 0 6.9666666666667 0 0 0 0 0
83 Esmaeili Dashtbayaz Amin 20 12.82 173.32 171.26666666666665 20 0 0 -2.0653333333333634 0 0 ja 0 0 16.083333333333 0 0 0 0 0
84 Konak Zidar 15 22.5 173.32 183.65000000000003 22 0 0 10.31800000000002 0 0 ja 0 0 16.433333333333 18.5 7 0 0 0

View File

@ -3,6 +3,9 @@
"collection": "@nestjs/schematics", "collection": "@nestjs/schematics",
"sourceRoot": "src", "sourceRoot": "src",
"compilerOptions": { "compilerOptions": {
"deleteOutDir": true "deleteOutDir": true,
"assets": [
"**/*.hbs"
]
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,7 @@
"start:dev": "nest start --watch", "start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch", "start:debug": "nest start --debug --watch",
"start:prod": "node dist/main", "start:prod": "node dist/main",
"prisma": "prisma",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest", "test": "jest",
"test:watch": "jest --watch", "test:watch": "jest --watch",
@ -25,6 +26,7 @@
"dependencies": { "dependencies": {
"@apollo/server": "^4.12.0", "@apollo/server": "^4.12.0",
"@faker-js/faker": "^9.6.0", "@faker-js/faker": "^9.6.0",
"@nestjs-modules/mailer": "^2.0.2",
"@nestjs/apollo": "^13.0.4", "@nestjs/apollo": "^13.0.4",
"@nestjs/axios": "^4.0.0", "@nestjs/axios": "^4.0.0",
"@nestjs/bullmq": "^11.0.2", "@nestjs/bullmq": "^11.0.2",
@ -43,14 +45,18 @@
"bullmq": "^5.56.0", "bullmq": "^5.56.0",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0", "date-fns-tz": "^3.2.0",
"form-data": "^4.0.4",
"graphql": "^16.10.0", "graphql": "^16.10.0",
"graphql-subscriptions": "^3.0.0", "graphql-subscriptions": "^3.0.0",
"graphql-ws": "^6.0.4", "graphql-ws": "^6.0.4",
"handlebars": "^4.7.8",
"kysely": "^0.27.6", "kysely": "^0.27.6",
"nodemailer": "^7.0.10",
"pg": "^8.14.1", "pg": "^8.14.1",
"prisma-kysely": "^1.8.0", "prisma-kysely": "^1.8.0",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2" "rxjs": "^7.8.2",
"uuid": "^8.3.2"
}, },
"devDependencies": { "devDependencies": {
"@nestjs/cli": "^11.0.6", "@nestjs/cli": "^11.0.6",
@ -58,7 +64,9 @@
"@nestjs/testing": "^11.0.15", "@nestjs/testing": "^11.0.15",
"@types/express": "^5.0.1", "@types/express": "^5.0.1",
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
"@types/multer": "^2.0.0",
"@types/node": "^22.14.0", "@types/node": "^22.14.0",
"@types/nodemailer": "^7.0.3",
"@types/supertest": "^6.0.3", "@types/supertest": "^6.0.3",
"@typescript-eslint/eslint-plugin": "^8.29.1", "@typescript-eslint/eslint-plugin": "^8.29.1",
"@typescript-eslint/parser": "^8.29.1", "@typescript-eslint/parser": "^8.29.1",

View File

@ -0,0 +1,17 @@
-- CreateTable
CREATE TABLE "TourFile" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"tourId" TEXT NOT NULL,
"ticketId" TEXT NOT NULL,
"storagePath" TEXT NOT NULL,
"originalFilename" TEXT NOT NULL,
"mimeType" TEXT NOT NULL,
"fileSizeBytes" INTEGER NOT NULL,
"dlUploadStatus" TEXT NOT NULL DEFAULT 'pending',
"createdAt" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "TourFile_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "TourFile_tourId_ticketId_idx" ON "TourFile"("tourId", "ticketId");

View File

@ -421,6 +421,23 @@ model Tour {
ticketId String? @db.Uuid ticketId String? @db.Uuid
} }
model TourFile {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
tourId String
ticketId String
storagePath String
originalFilename String
mimeType String
fileSizeBytes Int
dlUploadStatus String @default("pending")
createdAt DateTime? @default(now())
@@index([tourId, ticketId])
}
model Patient { model Patient {
id String @id id String @id
surname String? surname String?

View File

@ -1,7 +1,6 @@
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo'; import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { HttpModule } from '@nestjs/axios';
import { BullModule } from '@nestjs/bullmq'; import { BullModule } from '@nestjs/bullmq';
import { Module } from '@nestjs/common'; import { Logger, MiddlewareConsumer, Module } from '@nestjs/common';
import { EventEmitterModule } from '@nestjs/event-emitter'; import { EventEmitterModule } from '@nestjs/event-emitter';
import { GraphQLModule } from '@nestjs/graphql'; import { GraphQLModule } from '@nestjs/graphql';
import { ScheduleModule } from '@nestjs/schedule'; import { ScheduleModule } from '@nestjs/schedule';
@ -11,8 +10,11 @@ import { Pool } from 'pg';
import { AppController } from './app.controller'; import { AppController } from './app.controller';
import { AppService } from './app.service'; import { AppService } from './app.service';
import { ConfigModule } from './core/config/config.module'; import { ConfigModule } from './core/config/config.module';
import { ConfigService } from './core/config/config.service';
import { DatabaseModule } from './core/database/database.module'; import { DatabaseModule } from './core/database/database.module';
import { PrismaModule } from './core/database/prisma.module'; import { PrismaModule } from './core/database/prisma.module';
import { MailModule } from './core/mail/mail.module';
import { RequestLoggerMiddleware } from './core/middleware/request-logger.middleware';
import { ApplicationAccountingModule } from './modules/app-accounting/app-accounting.module'; import { ApplicationAccountingModule } from './modules/app-accounting/app-accounting.module';
import { ApplicationControlCenterModule } from './modules/app-control-center/app-control-center.module'; import { ApplicationControlCenterModule } from './modules/app-control-center/app-control-center.module';
import { AppHomeModule } from './modules/app-home/app-home.module'; import { AppHomeModule } from './modules/app-home/app-home.module';
@ -23,59 +25,36 @@ import { FeatCalculationModule } from './modules/feat-calculation/feat-calculati
import { FeatureEtlModule } from './modules/feat-etl/feat-etl.module'; import { FeatureEtlModule } from './modules/feat-etl/feat-etl.module';
import { FeatReportsModule } from './modules/feat-reports/feat-reports.module'; import { FeatReportsModule } from './modules/feat-reports/feat-reports.module';
import { FeatTicketSystemModule } from './modules/feat-tickets/ticket-system.module'; import { FeatTicketSystemModule } from './modules/feat-tickets/ticket-system.module';
import { FeatBusinessObjectsModule } from './modules/feat-business-objects/feat-business-objects.module'; import { OtelMetricsService } from './core/service/otel-metrics.service';
@Module({ @Module({
imports: [ imports: [
ConfigModule, ConfigModule,
// DatabaseModule.forRootAsync({ DatabaseModule.forRootAsync({
// isGlobal: true, imports: [ConfigModule],
// inject: [ConfigService], useFactory: async (config: ConfigService) => ({
// useFactory(config: ConfigService) { dialect: new PostgresDialect({
// return { pool: new Pool({
// config: { host: config.databaseHost(),
// dialect: new PostgresDialect({ port: config.databasePort(),
// pool: new Pool({ database: config.databaseName(),
// host: config.databaseHost, user: config.databaseUser(),
// port: config.databasePort, password: config.databasePassword(),
// database: config.databaseName, }),
// user: config.databaseUser,
// password: config.databasePassword,
// }),
// }),
// },
// };
// },
// }),
DatabaseModule.forRoot({
// isGlobal: true,
dialect: new PostgresDialect({
pool: new Pool({
host: 'localhost',
port: 5432,
database: 'avicenna',
user: 'johndoe',
password: 'randompassword',
}), }),
}), }),
inject: [ConfigService],
}), }),
BullModule.forRoot({ BullModule.forRootAsync({
connection: { imports: [ConfigModule],
host: 'localhost', useFactory: async (config: ConfigService) => ({
port: 6379, connection: {
}, host: config.redisHost(),
port: config.redisPort(),
},
}),
inject: [ConfigService],
}), }),
// DatabaseModule.forRoot({
// dialect: new PostgresDialect({
// pool: new Pool({
// host: 'localhost',
// port: 54322,
// database: 'postgres',
// user: 'postgres',
// password: 'postgres',
// }),
// }),
// }),
PrismaModule.forRoot(), PrismaModule.forRoot(),
ScheduleModule.forRoot(), ScheduleModule.forRoot(),
EventEmitterModule.forRoot(), EventEmitterModule.forRoot(),
@ -88,6 +67,7 @@ import { FeatBusinessObjectsModule } from './modules/feat-business-objects/feat-
'graphql-ws': true, 'graphql-ws': true,
}, },
}), }),
MailModule,
FeatureEtlModule, FeatureEtlModule,
FeatReportsModule, FeatReportsModule,
FeatCalculationModule, FeatCalculationModule,
@ -100,6 +80,10 @@ import { FeatBusinessObjectsModule } from './modules/feat-business-objects/feat-
AppHomeModule, AppHomeModule,
], ],
controllers: [AppController], controllers: [AppController],
providers: [AppService], providers: [AppService, Logger, OtelMetricsService],
}) })
export class AppModule {} export class AppModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(RequestLoggerMiddleware).forRoutes('*');
}
}

View File

@ -39,8 +39,8 @@ export class ConfigService extends NestConfigService {
dyflexisAppPassword(): string { dyflexisAppPassword(): string {
return this.get('DYFLEXIS_APP_PASSWORD'); return this.get('DYFLEXIS_APP_PASSWORD');
} }
databasePort(): string { databasePort(): number {
return this.get('DATABASE_PORT'); return +this.get('DATABASE_PORT');
} }
databaseHost(): string { databaseHost(): string {
return this.get('DATABASE_HOST'); return this.get('DATABASE_HOST');
@ -54,4 +54,28 @@ export class ConfigService extends NestConfigService {
databaseName(): string { databaseName(): string {
return this.get('DATABASE_DATABASE'); return this.get('DATABASE_DATABASE');
} }
redisPort(): number {
return +this.get('REDIS_PORT');
}
redisHost(): string {
return this.get('REDIS_HOST');
}
storagePath(): string {
return this.get('STORAGE_PATH');
}
mailHost(): string {
return this.get('SMTP_HOST');
}
mailPort(): string {
return this.get('SMTP_PORT');
}
mailUser(): string {
return this.get('SMTP_USER');
}
mailPass(): string {
return this.get('SMTP_PASS');
}
mailFrom(): string {
return this.get('MAIL_FROM');
}
} }

View File

@ -7,7 +7,7 @@ import {
Type, Type,
} from '@nestjs/common'; } from '@nestjs/common';
import { KyselyConfig } from 'kysely'; import { KyselyConfig } from 'kysely';
import { createKyselyProvider } from './kysely-client.factory'; import { createKyselyClient, createKyselyProvider } from './kysely-client.factory';
export const KYSELY_CLIENT_PROVIDER_TOKEN = 'KyselyClientProviderToken'; export const KYSELY_CLIENT_PROVIDER_TOKEN = 'KyselyClientProviderToken';
export const KYSELY_CONFIG_TOKEN = 'KYSELY_CONFIG_TOKEN'; export const KYSELY_CONFIG_TOKEN = 'KYSELY_CONFIG_TOKEN';
@ -21,11 +21,10 @@ export interface KyselyModuleAsyncOptions
isGlobal?: boolean; isGlobal?: boolean;
useExisting?: Type<KyselyConfigFactory>; useExisting?: Type<KyselyConfigFactory>;
useClass?: Type<KyselyConfigFactory>; useClass?: Type<KyselyConfigFactory>;
// FIX 1: Erlaube Promise im Rückgabetyp
useFactory?: ( useFactory?: (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
...args: any[] ...args: any[]
) => KyselyConfig; ) => Promise<KyselyConfig> | KyselyConfig;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
inject?: any[]; inject?: any[];
} }
@ -43,11 +42,22 @@ export class DatabaseModule {
} }
public static forRootAsync(options: KyselyModuleAsyncOptions): DynamicModule { public static forRootAsync(options: KyselyModuleAsyncOptions): DynamicModule {
// Wir erstellen den Client-Provider, der auf den Config-Provider wartet
const clientProvider: Provider = {
provide: KYSELY_CLIENT_PROVIDER_TOKEN,
useFactory: (config: KyselyConfig) => createKyselyClient(config),
inject: [KYSELY_CONFIG_TOKEN], // Injiziert das Ergebnis von createAsyncOptionsProvider
};
return { return {
global: options.isGlobal, global: options.isGlobal,
module: DatabaseModule, module: DatabaseModule,
imports: options.imports || [], imports: options.imports || [],
providers: this.createAsyncProviders(options), providers: [
...this.createAsyncProviders(options),
clientProvider,
],
exports: [clientProvider],
}; };
} }
@ -55,12 +65,12 @@ export class DatabaseModule {
options: KyselyModuleAsyncOptions, options: KyselyModuleAsyncOptions,
): Provider[] { ): Provider[] {
if (options.useExisting || options.useFactory) { if (options.useExisting || options.useFactory) {
return this.createAsyncOptionsProvider(options); return [this.createAsyncOptionsProvider(options)];
} }
if (options.useClass) { if (options.useClass) {
return [ return [
...this.createAsyncOptionsProvider(options), this.createAsyncOptionsProvider(options),
{ {
provide: options.useClass, provide: options.useClass,
useClass: options.useClass, useClass: options.useClass,
@ -68,56 +78,28 @@ export class DatabaseModule {
]; ];
} }
return [...this.createAsyncOptionsProvider(options)]; throw new Error('Invalid configuration for DatabaseModule: One of useClass, useExisting, or useFactory must be provided.');
} }
// Dieser Provider stellt NUR die Konfiguration (KyselyConfig) bereit
private static createAsyncOptionsProvider( private static createAsyncOptionsProvider(
options: KyselyModuleAsyncOptions, options: KyselyModuleAsyncOptions,
): Provider[] { ): Provider {
if (options.useFactory) { if (options.useFactory) {
const config = options.useFactory(options.inject); return {
return [
// {
// provide: KYSELY_CONFIG_TOKEN,
// useFactory: options.useFactory,
// inject: options.inject || [],
// },
{
provide: KYSELY_CLIENT_PROVIDER_TOKEN,
useExisting: createKyselyProvider(config)
}
];
}
if (options.useExisting) {
return [
{
provide: KYSELY_CONFIG_TOKEN,
useFactory: async (optionsFactory: KyselyConfigFactory) =>
await optionsFactory.createKyselyConfig(),
inject: [options.useExisting],
},
];
}
if (options.useClass) {
return [
{
provide: KYSELY_CONFIG_TOKEN,
useFactory: async (optionsFactory: KyselyConfigFactory) =>
await optionsFactory.createKyselyConfig(),
inject: [options.useClass],
},
];
}
return [
{
provide: KYSELY_CONFIG_TOKEN, provide: KYSELY_CONFIG_TOKEN,
useFactory: async (optionsFactory: KyselyConfigFactory) => useFactory: options.useFactory,
await optionsFactory.createKyselyConfig(), inject: options.inject || [],
inject: [], };
}, }
];
const inject = [options.useClass || options.useExisting];
return {
provide: KYSELY_CONFIG_TOKEN,
useFactory: async (optionsFactory: KyselyConfigFactory) =>
await optionsFactory.createKyselyConfig(),
inject: inject,
};
} }
} }

View File

@ -10,6 +10,7 @@ input AccountingFilterArgs {
enum AccountingState { enum AccountingState {
Billable Billable
Billed Billed
Pending
PreparedForBilling PreparedForBilling
} }
@ -138,6 +139,7 @@ type Metric {
type Mutation { type Mutation {
acceptAnomaly(id: String!): Anomaly! acceptAnomaly(id: String!): Anomaly!
sendApprovalRequest(email: String!, ticketId: String!): Boolean!
setAnomalyToSleep(id: String!): Anomaly! setAnomalyToSleep(id: String!): Anomaly!
setDocumentInfo(approval: ApprovalInfoArgs, tdLocation: String, ticketId: String!): Ticket! setDocumentInfo(approval: ApprovalInfoArgs, tdLocation: String, ticketId: String!): Ticket!
} }

View File

@ -0,0 +1,39 @@
import { MailerModule } from '@nestjs-modules/mailer';
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter';
import { Module } from '@nestjs/common';
import { join } from 'path';
import { ConfigService } from '../config/config.service';
import { MailService } from './mail.service';
@Module({
imports: [
MailerModule.forRootAsync({
useFactory: (config: ConfigService) => ({
transport: {
host: config.mailHost(),
port: +config.mailPort(),
secure: false,
auth: {
user: config.mailUser(),
pass: config.mailPass(),
},
pool: true,
},
defaults: {
from: config.mailFrom(),
},
template: {
dir: join(process.cwd(), 'dist/core/mail/templates'),
adapter: new HandlebarsAdapter(),
options: {
strict: true,
},
},
}),
inject: [ConfigService],
}),
],
providers: [MailService],
exports: [MailService],
})
export class MailModule {}

View File

@ -0,0 +1,35 @@
import { MailerService } from '@nestjs-modules/mailer';
import { Injectable, Logger } from '@nestjs/common';
import { TourFile } from '@prisma/client';
import { ConfigService } from '../config/config.service';
@Injectable()
export class MailService {
private readonly logger = new Logger(MailService.name);
constructor(private readonly mailerService: MailerService, private readonly config: ConfigService) {}
async sendGenehmigungsanfrage(recipient: string, transportschein: TourFile) {
this.logger.log(
`sending Genehmigungsanfrage mail to ${recipient} with Transportschein ${transportschein.originalFilename} for Tour ${transportschein.tourId}`,
);
try {
await this.mailerService.sendMail({
to: recipient,
bcc: 'info@avicenna.hamburg',
subject: `Antrag auf Kostenübernahme`,
template: 'genehmigungsanfrage',
attachments: [
{
filename: transportschein.originalFilename,
path: transportschein.storagePath,
contentType: transportschein.mimeType,
},
],
});
} catch (error) {
this.logger.error('failed to send mail', error)
}
}
}

View File

@ -0,0 +1 @@
export const MAILING_QUEUE = 'mailing_queue';

View File

@ -0,0 +1,18 @@
<div style="font-family: sans-serif; white-space: pre-wrap;">
Sehr geehrte Damen und Herren,
für den betreffenden Transport wurde bereits eine Kostenübernahme gemäß § 6 Abs. 3 der Krankentransport-Richtlinie beantragt. Leider liegt uns bislang keine Genehmigung vor.
Da uns ohne Genehmigung keine Abrechnung mit der Krankenkasse möglich ist, verzögert sich der gesamte Abrechnungsprozess. Um eine private Rechnungsstellung an den Patienten zu vermeiden, bitten wir Sie um zeitnahe Übermittlung der Genehmigung.
Bitte senden Sie die Unterlagen an eine der folgenden Stellen:
• E-Mail: info@avicenna.hamburg
• Fax: 040 460006820
• Postanschrift: Thomas-Mann-Straße 21, 22175 Hamburg
• Institutionskennzeichen: 600224841
Für Rückfragen stehen wir Ihnen selbstverständlich gerne zur Verfügung.
Vielen Dank vorab.
Mit freundlichen Grüßen
Ihr Team von Avicenna
</div>

View File

@ -0,0 +1,15 @@
<div style="font-family: sans-serif; white-space: pre-wrap;">
Sehr geehrte Damen und Herren,<br>
<br>
hiermit beantragen wir im Auftrag unseres Versicherten die Fahrtkostenerstattung gemäß § 6 Abs. 3 der Krankentransport-Richtlinie.<br>
<br>
Die notwendigen Details entnehmen Sie bitte der beigefügten ärztlichen Verordnung (PDF-Datei).<br>
<br>
Bitte senden Sie uns die Genehmigung an:<br>
• E-Mail: info@avicenna.hamburg<br>
• Fax: 040 460006820<br>
• Postanschrift: Thomas-Mann-Straße 21, 22175 Hamburg<br>
• Institutionskennzeichen: 600224841<br>
<br>
Ihr Team der Avicenna Ambulance<br>
</div>

View File

@ -0,0 +1,20 @@
import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
import { ConfigService } from '../config/config.service';
import { diskStorage } from 'multer';
import * as path from 'path';
import { v4 as uuidv4 } from 'uuid';
export function createMulterOptions(config: ConfigService): MulterOptions {
const storagePath = config.storagePath();
return {
storage: diskStorage({
destination: (req, file, cb) => cb(null, storagePath),
filename: (req, file, cb) => {
const extension = path.extname(file.originalname);
const filename = `${uuidv4()}${extension}`;
cb(null, filename);
},
}),
};
}

View File

@ -1,4 +1,5 @@
export enum AccountingState { export enum AccountingState {
Pending = 'Pending',
Billable = 'Billable', Billable = 'Billable',
PreparedForBilling = 'PreparedForBilling', PreparedForBilling = 'PreparedForBilling',
Billed = 'Billed', Billed = 'Billed',

View File

@ -5,10 +5,34 @@ import { FeatTicketSystemModule } from '../feat-tickets/ticket-system.module';
import { TicketRepository } from './ticket.repository'; import { TicketRepository } from './ticket.repository';
import { TicketsResolver } from './tickets.resolver'; import { TicketsResolver } from './tickets.resolver';
import { TicketSystemService } from './ticket-system.service'; import { TicketSystemService } from './ticket-system.service';
import { TicketsController } from './tickets.controller';
import { MulterModule } from '@nestjs/platform-express';
import { ConfigModule } from 'src/core/config/config.module';
import { ConfigService } from 'src/core/config/config.service';
import { createMulterOptions } from 'src/core/storage/multer.config';
import { BullModule } from '@nestjs/bullmq';
import { TOUR_FILE_UPLOAD_QUEUE } from '../feat-business-objects/entities/tour/tour.tokens';
import { MAILING_QUEUE } from 'src/core/mail/mail.tokens';
@Module({ @Module({
imports: [PrismaModule, FeatTicketSystemModule, FeatBusinessObjectsModule], imports: [
PrismaModule,
FeatTicketSystemModule,
FeatBusinessObjectsModule,
MulterModule.registerAsync({
imports: [ConfigModule],
useFactory: (config: ConfigService) => createMulterOptions(config),
inject: [ConfigService],
}),
BullModule.registerQueue({
name: TOUR_FILE_UPLOAD_QUEUE,
}),
BullModule.registerQueue({
name: MAILING_QUEUE,
}),
],
providers: [TicketRepository, TicketSystemService, TicketsResolver], providers: [TicketRepository, TicketSystemService, TicketsResolver],
controllers: [TicketsController],
exports: [TicketSystemService], exports: [TicketSystemService],
}) })
export class ApplicationTicketSystemModule {} export class ApplicationTicketSystemModule {}

View File

@ -1,18 +1,21 @@
import { Injectable } from '@nestjs/common'; import { InjectQueue } from '@nestjs/bullmq';
import { Injectable, NotFoundException } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter'; import { OnEvent } from '@nestjs/event-emitter';
import { Ticket, TicketValidationState, Tour } from '@prisma/client'; import { Ticket, TicketValidationState, Tour } from '@prisma/client';
import { Queue } from 'bullmq';
import { PubSub } from 'graphql-subscriptions'; import { PubSub } from 'graphql-subscriptions';
import { import {
decodeOffsetCursor, decodeOffsetCursor,
encodeCursor, encodeCursor,
} from 'src/core/base/pagination.util'; } from 'src/core/base/pagination.util';
import { MAILING_QUEUE } from 'src/core/mail/mail.tokens';
import { TicketService } from '../feat-business-objects/entities/ticket/ticket.service'; import { TicketService } from '../feat-business-objects/entities/ticket/ticket.service';
import { TourService } from '../feat-business-objects/entities/tour/tour.service'; import { TourService } from '../feat-business-objects/entities/tour/tour.service';
import { TicketLifecycleService } from '../feat-tickets/ticket-lifecycle.service'; import { TicketLifecycleService } from '../feat-tickets/ticket-lifecycle.service';
import { import {
TicketSystemEventTypes, TicketSystemEventTypes,
TicketSystemValidatedEvent, TicketSystemValidatedEvent,
} from '../feat-tickets/ticket-system.events'; } from '../feat-business-objects/entities/ticket/ticket-system.events';
import { DocumentInfoArgs, TicketFilterArgs } from './ticket-arg.types'; import { DocumentInfoArgs, TicketFilterArgs } from './ticket-arg.types';
import { import {
PaginatedTicketObjectType, PaginatedTicketObjectType,
@ -20,6 +23,7 @@ import {
TicketStateMetaObjectType, TicketStateMetaObjectType,
} from './ticket.object-type'; } from './ticket.object-type';
import { TicketRepository } from './ticket.repository'; import { TicketRepository } from './ticket.repository';
import { TourFilesService } from '../feat-business-objects/entities/tour-files/tour-files.service';
@Injectable() @Injectable()
export class TicketSystemService { export class TicketSystemService {
@ -36,13 +40,33 @@ export class TicketSystemService {
private readonly repository: TicketRepository, private readonly repository: TicketRepository,
private readonly ticketService: TicketService, private readonly ticketService: TicketService,
private readonly tourService: TourService, private readonly tourService: TourService,
private readonly tourFileService: TourFilesService,
private readonly lifecycle: TicketLifecycleService, private readonly lifecycle: TicketLifecycleService,
@InjectQueue(MAILING_QUEUE) private readonly approvalQueue: Queue,
) {} ) {}
async updateDocumentInfo(ticketId: string, documentInfo: DocumentInfoArgs) { async updateDocumentInfo(ticketId: string, documentInfo: DocumentInfoArgs) {
return this.lifecycle.validateDocumentInfoUpdate(ticketId, documentInfo); return this.lifecycle.validateDocumentInfoUpdate(ticketId, documentInfo);
} }
async addApprovalRequestJob(ticketId: string, recipientEmail: string): Promise<boolean> {
const attachment = await this.tourFileService.findByTicketId(ticketId)
if (!attachment) {
throw new NotFoundException(
`Kein Dokument für Ticket ${ticketId} gefunden. Bitte erst hochladen.`,
);
}
await this.approvalQueue.add('approval_request', {
ticketId,
tourFileId: attachment.id,
recipientEmail,
});
return true;
}
findById(id: string): Promise<Ticket> { findById(id: string): Promise<Ticket> {
return this.ticketService.findById(id); return this.ticketService.findById(id);
} }

View File

@ -0,0 +1,62 @@
import { InjectQueue } from '@nestjs/bullmq';
import {
Controller,
HttpCode,
HttpStatus,
Logger,
Param,
Post,
UploadedFile,
UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { Queue } from 'bullmq';
import { TourService } from '../feat-business-objects/entities/tour/tour.service';
import { TOUR_FILE_UPLOAD_QUEUE } from '../feat-business-objects/entities/tour/tour.tokens';
import { TourFilesService } from '../feat-business-objects/entities/tour-files/tour-files.service';
@Controller('api/tickets')
export class TicketsController {
private readonly logger = new Logger(TicketsController.name);
constructor(
private readonly tourService: TourService,
private readonly tourFilesService: TourFilesService,
@InjectQueue(TOUR_FILE_UPLOAD_QUEUE)
private readonly transportscheinQueue: Queue,
) {}
@Post(':ticketId/files')
@UseInterceptors(FileInterceptor('file'))
@HttpCode(HttpStatus.ACCEPTED)
async uploadFile(
@Param('ticketId') ticketId: string,
@UploadedFile() file: Express.Multer.File,
) {
this.logger.log(
`file received for ticket ${ticketId ?? ''}: ${file.filename}`,
);
const existingFile = await this.tourFilesService.findByTicketId(ticketId);
if (existingFile) {
return {message: 'File already exists!'}
}
const storage = file.path;
const tours = await this.tourService.findByTicketId(ticketId);
for (const tour of tours) {
const tourFile = await this.tourFilesService.createTourFile({
ticketId,
tourId: tour.id,
storagePath: storage,
file: file,
});
await this.transportscheinQueue.add('Transportschein', {
tourFileId: tourFile.id,
});
}
return { message: 'File accepted and processing started' };
}
}

View File

@ -61,6 +61,14 @@ export class TicketsResolver {
return this.ticketsService.getTicketStateKeyFigures(ticketState); return this.ticketsService.getTicketStateKeyFigures(ticketState);
} }
@Mutation(() => Boolean)
sendApprovalRequest(
@Args('ticketId', {type: () => String}) ticketId: string,
@Args('email', {type: () => String}) email: string,
) {
return this.ticketsService.addApprovalRequestJob(ticketId, email)
}
@Mutation(() => TicketObjectType) @Mutation(() => TicketObjectType)
setDocumentInfo( setDocumentInfo(
@Args('ticketId', { type: () => String }) ticketId: string, @Args('ticketId', { type: () => String }) ticketId: string,

View File

@ -19,7 +19,6 @@ export class EmployeeStillWorkingDetector extends AnomalyDetector {
const longWorkingEmployees = const longWorkingEmployees =
await this.dataService.findEmployeeStillWorking(day); await this.dataService.findEmployeeStillWorking(day);
console.log(longWorkingEmployees)
if (longWorkingEmployees.length > 0) { if (longWorkingEmployees.length > 0) {
anomalies.push( anomalies.push(
this.createAnomaly( this.createAnomaly(

View File

@ -17,7 +17,6 @@ export class IsWorkTimePlannedDetector extends AnomalyDetector {
for (const day of eachDayOfInterval(inInterval)){ for (const day of eachDayOfInterval(inInterval)){
const workTimeWithoutPlan = await this.dataService.findWorkForPlannedTime(day) const workTimeWithoutPlan = await this.dataService.findWorkForPlannedTime(day)
console.log(day, workTimeWithoutPlan)
} }
return return

View File

@ -44,11 +44,7 @@ export class TicketRepository {
async findAllCreated(): Promise<(Ticket & { tours: Tour[] })[]> { async findAllCreated(): Promise<(Ticket & { tours: Tour[] })[]> {
return await this.prisma.ticket.findMany({ return await this.prisma.ticket.findMany({
where: { where: {
state: { currentState: TicketValidationState.CREATED,
none: {
state: TicketValidationState.USER_INTERACTION_REQUIRED,
},
},
}, },
include: { include: {
tours: true, tours: true,
@ -185,7 +181,9 @@ export class TicketRepository {
} }
async createTicket(tours: Tour[]): Promise<Ticket> { async createTicket(tours: Tour[]): Promise<Ticket> {
this.logger.log(`Creating Ticket for ${tours.length} tours`); this.logger.log(
`Creating Ticket for tours: ${tours.map(({ operationId }) => operationId).join(', ')}`,
);
try { try {
return await this.db.transaction().execute(async (trx) => { return await this.db.transaction().execute(async (trx) => {
const ticket = await trx const ticket = await trx
@ -224,6 +222,9 @@ export class TicketRepository {
} }
async addToursToTicket(ticketId: string, tours: Tour[]): Promise<Ticket> { async addToursToTicket(ticketId: string, tours: Tour[]): Promise<Ticket> {
this.logger.log(
`Adding tours to ticket: ${tours.map(({ operationId }) => operationId).join(', ')}`,
);
await this.db await this.db
.updateTable('Tour') .updateTable('Tour')
.set({ .set({
@ -288,6 +289,9 @@ export class TicketRepository {
approvalPath?: string; approvalPath?: string;
}, },
): Promise<Ticket & { tours: Tour[] }> { ): Promise<Ticket & { tours: Tour[] }> {
this.logger.log(
`updating ticket (${ticketId}) with updateset ${JSON.stringify(data)}`,
);
const { state, ...ticketData } = data; const { state, ...ticketData } = data;
await this.db await this.db

View File

@ -1,20 +1,22 @@
import { Injectable } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { import {
ApprovalState, ApprovalState,
Ticket, Ticket,
TicketStateHistory, TicketStateHistory,
TicketValidationState, TicketValidationState,
Tour, Tour
} from '@prisma/client'; } from '@prisma/client';
import { import {
TicketFilterDto, TicketFilterDto,
UpdateDocumentInfoDto, UpdateDocumentInfoDto,
} from 'src/modules/feat-business-objects/entities/ticket/tickets.dto'; } from 'src/modules/feat-business-objects/entities/ticket/tickets.dto';
import { TicketRepository } from './ticket.repository'; import { TicketRepository } from './ticket.repository';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { TicketSystemEventTypes, TicketSystemValidatedEvent } from './ticket-system.events';
@Injectable() @Injectable()
export class TicketService { export class TicketService {
constructor(private readonly repository: TicketRepository) {} constructor(private readonly repository: TicketRepository, private readonly eventEmitter: EventEmitter2) {}
async findById(ticketId: string): Promise<Ticket> { async findById(ticketId: string): Promise<Ticket> {
return this.repository.findById(ticketId); return this.repository.findById(ticketId);
@ -78,6 +80,24 @@ export class TicketService {
return this.repository.setStateOfTicket(ticketId, state); return this.repository.setStateOfTicket(ticketId, state);
} }
async approvalRequestedForTicket(ticketId: string) {
return this.repository.updateTicket(ticketId, {
state: TicketValidationState.DOCUMENTS_MISSING,
approvalState: ApprovalState.REQUESTED,
documentInfo: {
tdLocation: 'unknown',
approval: {
whoRequested: 'Avicenna',
answeredFrom: 'System',
isNeeded: true,
},
},
}).then(() => this.eventEmitter.emit(
TicketSystemEventTypes.Validated,
new TicketSystemValidatedEvent(0)
));
}
async updateTicket( async updateTicket(
ticketId: string, ticketId: string,
data: { data: {

View File

@ -0,0 +1,6 @@
export interface TourFileDto {
tourId: string;
ticketId: string;
storagePath: string;
file: Express.Multer.File
}

View File

@ -0,0 +1,60 @@
import { Inject, Injectable } from '@nestjs/common';
import { KYSELY_CLIENT_PROVIDER_TOKEN } from 'src/core/database/database.module';
import { DB } from 'src/core/database/types';
import { TourFileDto } from './tour-files.dto';
import { Kysely, UpdateResult } from 'kysely';
import { TourFile } from '@prisma/client';
@Injectable()
export class TourFilesRepository {
constructor(
@Inject(KYSELY_CLIENT_PROVIDER_TOKEN) private readonly db: Kysely<DB>,
) {}
createTourFile(tourFileDto: TourFileDto): Promise<TourFile> {
return this.db
.insertInto('TourFile')
.values({
tourId: tourFileDto.tourId,
ticketId: tourFileDto.ticketId,
storagePath: tourFileDto.storagePath,
originalFilename: tourFileDto.file.originalname,
mimeType: tourFileDto.file.mimetype,
fileSizeBytes: tourFileDto.file.size,
dlUploadStatus: 'pending',
})
.returningAll()
.executeTakeFirst();
}
findById(id: string): Promise<TourFile> {
return this.db
.selectFrom('TourFile')
.selectAll()
.where('TourFile.id', '=', id)
.executeTakeFirst();
}
findByTicketId(ticketId: string): Promise<TourFile> {
return this.db
.selectFrom('TourFile')
.selectAll()
.where('TourFile.ticketId', '=', ticketId)
.executeTakeFirst();
}
findByTourId(tourId: string): Promise<TourFile> {
return this.db
.selectFrom('TourFile')
.selectAll()
.where('TourFile.tourId', '=', tourId)
.executeTakeFirst();
}
updateStatus(id: string, newStatus: string): Promise<UpdateResult> {
return this.db
.updateTable('TourFile')
.where('id', '=', id)
.set('dlUploadStatus', newStatus)
.executeTakeFirst();
}
}

View File

@ -0,0 +1,29 @@
import { Injectable } from '@nestjs/common';
import { TourFileDto } from './tour-files.dto';
import { TourFilesRepository } from './tour-files.repository';
import { TourFile } from '@prisma/client';
@Injectable()
export class TourFilesService {
constructor(private readonly repository: TourFilesRepository) {}
createTourFile(tourFileDto: TourFileDto): Promise<TourFile> {
return this.repository.createTourFile(tourFileDto);
}
findById(tourFileId: string): Promise<TourFile> {
return this.repository.findById(tourFileId);
}
findByTicketId(ticketId: string): Promise<TourFile> {
return this.repository.findByTicketId(ticketId);
}
findByTourId(tourId: string): Promise<TourFile> {
return this.repository.findByTourId(tourId);
}
updateStatus(id: string, newStatus: 'pending' | 'failed' | 'success') {
return this.repository.updateStatus(id, newStatus);
}
}

View File

@ -1,16 +1,13 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Prisma, Tour } from '@prisma/client'; import { Prisma, TicketValidationState, Tour } from '@prisma/client';
import { endOfDay, Interval, startOfDay } from 'date-fns'; import { endOfDay, Interval, startOfDay } from 'date-fns';
import { ExpressionBuilder, Kysely } from 'kysely'; import { ExpressionBuilder, ExpressionWrapper, Kysely, SqlBool } 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';
import { AccountingState } from 'src/modules/app-accounting/accounting-state'; import { AccountingState } from 'src/modules/app-accounting/accounting-state';
import { chunk } from 'src/utils'; import { chunk } from 'src/utils';
import { import { TourAccountingFilterDto } from './tours.filter';
createPrismaQueryFromAccountingFilters,
TourAccountingFilterDto,
} from './tours.filter';
type TourEb = ExpressionBuilder<DB, 'Tour'>; type TourEb = ExpressionBuilder<DB, 'Tour'>;
@ -120,35 +117,27 @@ export class TourRepository {
interval: Interval, interval: Interval,
ordinanceTypes: string[] = [], ordinanceTypes: string[] = [],
): Promise<Tour[]> { ): Promise<Tour[]> {
return await this.prisma.tour.findMany({ return await this.db
where: { .selectFrom('Tour')
AND: [ .selectAll()
{ .where((eb) =>
startDate: { eb.and([
gte: new Date(interval.start), eb('Tour.startDate', '>=', new Date(interval.start)),
}, eb('Tour.startDate', '<', new Date(interval.end)),
}, ...(ordinanceTypes.length > 0
{ ? [
startDate: { eb.or(
lt: new Date(interval.end), ordinanceTypes.map((ot) =>
}, eb('Tour.ordinanceType', 'like', `%${ot}%`),
}, ),
{ ),
OR: [ ]
...(ordinanceTypes.length : []),
? ordinanceTypes.map((ot) => ({ onlyRealToursKysely(eb),
ordinanceType: { contains: ot }, ]),
})) )
: []), .orderBy('Tour.startDate asc')
], .execute();
},
...onlyRealTours(),
],
},
orderBy: {
startDate: 'desc',
},
});
} }
async findUpdatedAtBetween( async findUpdatedAtBetween(
@ -289,60 +278,45 @@ export class TourRepository {
async findFirstRecurring(tour: Tour): Promise<Tour | null> { async findFirstRecurring(tour: Tour): Promise<Tour | null> {
const institutionStreet = const institutionStreet =
tour.direction === 'Hinfahrt' ? tour.targetStreet : tour.startStreet; tour.direction === 'Hinfahrt' ? tour.targetStreet : tour.startStreet;
return await this.prisma.tour.findFirst({ return await this.db
where: { .selectFrom('Tour')
type: 'Serie', .selectAll()
category: tour.category, .where('type', '=', 'Serie')
ordinanceType: tour.ordinanceType, .where('category', '=', tour.category)
patientId: tour.patientId, .where('ordinanceType', '=', tour.ordinanceType)
patientName: tour.patientName, .where('patientId', '=', tour.patientId)
patientSurname: tour.patientSurname, .where('patientName', '=', tour.patientName)
patientStreet: tour.patientStreet, .where('patientSurname', '=', tour.patientSurname)
patientCity: tour.patientCity, .where('patientStreet', '=', tour.patientStreet)
patientZip: tour.patientZip, .where('patientCity', '=', tour.patientCity)
OR: [ .where('patientZip', '=', tour.patientZip)
{ targetStreet: institutionStreet }, .where((eb) =>
{ startStreet: institutionStreet }, eb.or([
], eb('targetStreet', '=', institutionStreet),
rangeEndDate: { eb('startStreet', '=', institutionStreet),
gte: tour.startDate, ]),
}, )
ticketId: { .where('rangeEndDate', '>=', tour.startDate)
not: null, .where('ticketId', 'is not', null)
}, .orderBy('startDate', 'desc')
}, .executeTakeFirst();
orderBy: {
startDate: 'desc',
},
});
} }
async findOnSameDayWithOppositeDirection(tour: Tour): Promise<Tour | null> { async findOnSameDayWithOppositeDirection(tour: Tour): Promise<Tour | null> {
return await this.prisma.tour.findFirst({ const dayStart = startOfDay(new Date(tour.startDate));
where: { const dayEnd = endOfDay(new Date(tour.startDate));
AND: [ const oppositeDirection =
{ tour.direction === 'Hinfahrt' ? 'Rückfahrt' : 'Hinfahrt';
startDate: {
gt: startOfDay(new Date(tour.startDate)), return await this.db
}, .selectFrom('Tour')
}, .selectAll()
{ .where('startDate', '>', dayStart)
startDate: { .where('startDate', '<', dayEnd)
lt: endOfDay(new Date(tour.startDate)), .where('startStreet', '=', tour.targetStreet)
}, .where('targetStreet', '=', tour.startStreet)
}, .where('direction', '=', oppositeDirection)
{ .executeTakeFirst();
startStreet: tour.targetStreet,
},
{
targetStreet: tour.startStreet,
},
{
direction: tour.direction === 'Hinfahrt' ? 'Rückfahrt' : 'Hinfahrt',
},
],
},
});
} }
async findWithTicket(interval: Interval): Promise<Tour[]> { async findWithTicket(interval: Interval): Promise<Tour[]> {
@ -389,40 +363,36 @@ export class TourRepository {
take?: number, take?: number,
skip?: number, skip?: number,
): Promise<{ tours: Tour[]; total: number }> { ): Promise<{ tours: Tour[]; total: number }> {
const checkValue = this.getCheckValueForAccountingState(state); return await this.db.transaction().execute(async (trx) => {
const query: Prisma.TourFindManyArgs = { const tours = await trx
...{ .selectFrom('Tour')
...(skip ? { skip, take: 10 } : {}), .selectAll()
...(take ? { take } : {}), .where((eb) =>
...(take && !skip ? { skip: 0 } : {}), eb.and([
}, this.getFilterForAccountingState(eb, state),
where: { onlyRealToursKysely(eb),
check: checkValue, ]),
...(checkValue === 2 && !filter?.deviation )
? { revenueDeviation: { not: 0 } } .offset(skip)
: {}), .limit(take)
empty: { .execute();
not: null,
},
ticketId: {
not: null,
},
AND: [
...createPrismaQueryFromAccountingFilters(filter),
...onlyRealTours(),
],
},
orderBy: { createdAt: 'desc' },
};
const [tours, count] = await this.prisma.$transaction([ const total = await trx
this.prisma.tour.findMany({ ...query }), .selectFrom('Tour')
this.prisma.tour.count({ where: query.where }), .where((eb) =>
]); eb.and([
return { this.getFilterForAccountingState(eb, state),
tours, onlyRealToursKysely(eb),
total: count, ]),
}; )
.select((eb) => eb.fn.countAll().as('total'))
.executeTakeFirst();
return {
tours,
total: Number(total.total),
};
});
} }
async calculateAccountingStateMeta(state: AccountingState): Promise<{ async calculateAccountingStateMeta(state: AccountingState): Promise<{
@ -431,78 +401,90 @@ export class TourRepository {
positiveRevenueDeviation?: number; positiveRevenueDeviation?: number;
negativeRevenueDeviation?: number; negativeRevenueDeviation?: number;
}> { }> {
const checkValue = this.getCheckValueForAccountingState(state); const result = await this.db
const ticketWithCurrentStateAndTours: Prisma.TourWhereInput = { .selectFrom('Tour')
check: checkValue, .where((eb) => {
...(checkValue === 2 ? { revenueDeviation: { not: 0 } } : {}), const conditions = [
empty: { this.getFilterForAccountingState(eb, state),
not: null, onlyRealToursKysely(eb),
}, eb('empty', 'is not', null),
ticketId: { eb('ticketId', 'is not', null),
not: null, ];
}, return eb.and(conditions);
AND: [...onlyRealTours()], })
}; .select((eb) => [
eb.fn.countAll().as('count'),
const [ eb.fn.sum<number>('revenue').as('revenueSum'),
count, eb.fn
revenueSum, .sum<number>(
positiveRevenueDeviation, eb
negativeRevenueDeviation, .case()
] = await this.prisma.$transaction([ .when('revenueDeviation', '>', 0)
this.prisma.tour.count({ .then(eb.ref('revenueDeviation'))
where: { ...ticketWithCurrentStateAndTours }, .else(0)
}), .end(),
this.prisma.tour.aggregate({ )
_sum: { .as('positiveRevenueDeviation'),
revenue: true, eb.fn
}, .sum<number>(
where: { eb
...ticketWithCurrentStateAndTours, .case()
}, .when('revenueDeviation', '<', 0)
}), .then(eb.ref('revenueDeviation'))
this.prisma.tour.aggregate({ .else(0)
_sum: { .end(),
revenueDeviation: true, )
}, .as('negativeRevenueDeviation'),
where: { ])
...ticketWithCurrentStateAndTours, .executeTakeFirstOrThrow();
revenueDeviation: {
gt: 0,
},
},
}),
this.prisma.tour.aggregate({
_sum: {
revenueDeviation: true,
},
where: {
...ticketWithCurrentStateAndTours,
revenueDeviation: {
lt: 0,
},
},
}),
]);
return { return {
count, count: Number(result.count),
revenueSum: revenueSum._sum.revenue ?? 0, revenueSum: Number(result.revenueSum ?? 0),
positiveRevenueDeviation: positiveRevenueDeviation: Number(result.positiveRevenueDeviation ?? 0),
positiveRevenueDeviation._sum.revenueDeviation ?? 0, negativeRevenueDeviation: Number(result.negativeRevenueDeviation ?? 0),
negativeRevenueDeviation:
negativeRevenueDeviation._sum.revenueDeviation ?? 0,
}; };
} }
private getCheckValueForAccountingState(state: AccountingState): number { private getFilterForAccountingState(
eb: ExpressionBuilder<DB, 'Tour'>,
state: AccountingState,
): ExpressionWrapper<DB, 'Tour', SqlBool> {
switch (state) { switch (state) {
case AccountingState.Pending:
return eb.and([
eb('Tour.check', '=', 0),
eb('Tour.revenueDeviation', '<>', 0),
eb.exists(
eb
.selectFrom('Ticket')
.select('Ticket.id')
.whereRef('Ticket.id', '=', 'Tour.ticketId')
.where(
'Ticket.currentState',
'<>',
TicketValidationState.ARCHIVED,
),
),
]);
case AccountingState.Billable: case AccountingState.Billable:
return 0; return eb.and([
eb('Tour.check', '=', 0),
eb('Tour.revenueDeviation', '=', 0),
eb.exists(
eb
.selectFrom('Ticket')
.select('Ticket.id')
.whereRef('Ticket.id', '=', 'Tour.ticketId')
.where(
'Ticket.currentState',
'=',
TicketValidationState.ARCHIVED,
),
),
]);
case AccountingState.PreparedForBilling: case AccountingState.PreparedForBilling:
return 1; return eb.and([eb('Tour.check', '=', 1)]);
case AccountingState.Billed:
return 2;
} }
} }
} }

View File

@ -1 +1,2 @@
export const TOURS_QUEUE = 'tours'; export const TOURS_QUEUE = 'tours';
export const TOUR_FILE_UPLOAD_QUEUE = 'tour_file_upload'

View File

@ -19,6 +19,8 @@ import { TourRepository } from './entities/tour/tour.repository';
import { TourService } from './entities/tour/tour.service'; import { TourService } from './entities/tour/tour.service';
import { WorkTimeRepository } from './entities/work-time/work-time.repository'; import { WorkTimeRepository } from './entities/work-time/work-time.repository';
import { WorkTimeService } from './entities/work-time/work-time.service'; import { WorkTimeService } from './entities/work-time/work-time.service';
import { TourFilesRepository } from './entities/tour-files/tour-files.repository';
import { TourFilesService } from './entities/tour-files/tour-files.service';
@Module({ @Module({
imports: [PrismaModule], imports: [PrismaModule],
@ -32,6 +34,7 @@ import { WorkTimeService } from './entities/work-time/work-time.service';
TariffService, TariffService,
TicketService, TicketService,
TourService, TourService,
TourFilesService,
WorkTimeService, WorkTimeService,
], ],
providers: [ providers: [
@ -52,6 +55,8 @@ import { WorkTimeService } from './entities/work-time/work-time.service';
TicketService, TicketService,
TourRepository, TourRepository,
TourService, TourService,
TourFilesRepository,
TourFilesService,
WorkTimeRepository, WorkTimeRepository,
WorkTimeService, WorkTimeService,
], ],

View File

@ -19,12 +19,16 @@ import {
of, of,
reduce, reduce,
switchMap, switchMap,
tap,
} from 'rxjs'; } from 'rxjs';
import { EmployeeDispoLiveRaw, PatientRaw, TourRaw } from './source.interface'; import { EmployeeDispoLiveRaw, PatientRaw, TourRaw } from './source.interface';
import { SourceRepository } from './source.repository'; import { SourceRepository } from './source.repository';
import { chunk } from 'src/utils'; import { chunk } from 'src/utils';
import { ConfigService } from 'src/core/config/config.service'; import { ConfigService } from 'src/core/config/config.service';
import { TourExtractor } from './extractors/tour.extractor'; import { TourExtractor } from './extractors/tour.extractor';
import * as fs from 'fs';
import * as FormData from 'form-data';
import * as path from 'path';
@Injectable() @Injectable()
export class DispoLiveExtractorsService { export class DispoLiveExtractorsService {
@ -36,6 +40,39 @@ export class DispoLiveExtractorsService {
private readonly config: ConfigService, private readonly config: ConfigService,
) {} ) {}
uploadFileToTour(tourId: string, storagePath: string, filename: string) {
const fileUploadEndpoint = 'custom/upload/userFiles';
const fileStream = fs.createReadStream(storagePath);
const fileid = path.basename(storagePath);
const formData = new FormData();
formData.append('_id', fileid);
formData.append('coll', 'userFiles');
formData.append('uploadFromColl', 'fahrberichte');
formData.append('dataId', tourId);
formData.append('file', fileStream, filename);
return this.dispoliveLogin(
this.config.dispoliveUsername(),
this.config.dispolivePassword(),
).pipe(
switchMap((authCookies) =>
this.http.post(
`${this.config.dispoliveBaseUrl()}/${fileUploadEndpoint}`,
formData,
{
headers: {
...formData.getHeaders(),
Cookie: authCookies,
},
},
),
),
tap((res) => console.log(res.data)),
);
}
extractTour(id: string, jobId: string) { extractTour(id: string, jobId: string) {
return this.fetchTour(id).pipe(map((tour) => ({ ...tour, jobId }))); return this.fetchTour(id).pipe(map((tour) => ({ ...tour, jobId })));
} }

View File

@ -11,6 +11,10 @@ import { ToursConsumer } from './processors/tours.consumer';
import { SchichtplanungSynchronizer } from './producer/schichtplanung'; import { SchichtplanungSynchronizer } from './producer/schichtplanung';
import { TourUpdateChecker } from './producer/tour-update'; import { TourUpdateChecker } from './producer/tour-update';
import { SCHICHTPLANUNG_QUEUE } from './tokens'; import { SCHICHTPLANUNG_QUEUE } from './tokens';
import { TourFileUploadConsumer } from './processors/tour-file-upload.consumer';
import { MAILING_QUEUE } from 'src/core/mail/mail.tokens';
import { MailConsumer } from './processors/mail.consumer';
import { MailModule } from 'src/core/mail/mail.module';
@Module({ @Module({
imports: [ imports: [
@ -18,8 +22,10 @@ import { SCHICHTPLANUNG_QUEUE } from './tokens';
DataAccessExtractorsModule, DataAccessExtractorsModule,
DataAccessTransformersModule, DataAccessTransformersModule,
DataAccessLoadersModule, DataAccessLoadersModule,
MailModule,
BullModule.registerQueue({ name: TOURS_QUEUE }), BullModule.registerQueue({ name: TOURS_QUEUE }),
BullModule.registerQueue({ name: SCHICHTPLANUNG_QUEUE }), BullModule.registerQueue({ name: SCHICHTPLANUNG_QUEUE }),
BullModule.registerQueue({ name: MAILING_QUEUE }),
], ],
providers: [ providers: [
ImporterService, ImporterService,
@ -27,6 +33,8 @@ import { SCHICHTPLANUNG_QUEUE } from './tokens';
TourUpdateChecker, TourUpdateChecker,
SchichtplanungSynchronizer, SchichtplanungSynchronizer,
SchichtplanungConsumer, SchichtplanungConsumer,
TourFileUploadConsumer,
MailConsumer
], ],
exports: [ImporterService], exports: [ImporterService],
}) })

View File

@ -0,0 +1,49 @@
import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Job } from 'bullmq';
import { Logger } from '@nestjs/common';
import { MAILING_QUEUE } from 'src/core/mail/mail.tokens';
import { MailService } from 'src/core/mail/mail.service';
import { TourFilesService } from 'src/modules/feat-business-objects/entities/tour-files/tour-files.service';
import { TicketService } from 'src/modules/feat-business-objects/entities/ticket/ticket.service';
interface ApprovalJobPayload {
ticketId: string;
tourFileId: string;
recipientEmail: string;
}
@Processor(MAILING_QUEUE)
export class MailConsumer extends WorkerHost {
private readonly logger = new Logger(MailConsumer.name);
constructor(
private readonly mailService: MailService,
private readonly tourFileService: TourFilesService,
private readonly ticketService: TicketService,
) {
super();
}
async process(job: Job<ApprovalJobPayload>): Promise<void> {
const { ticketId, tourFileId, recipientEmail } = job.data;
this.logger.log(
`Processing approval request for Ticket ${ticketId} -> ${recipientEmail}`,
);
try {
const tourFile = await this.tourFileService.findById(tourFileId);
await this.mailService.sendGenehmigungsanfrage(recipientEmail, tourFile);
await this.ticketService.approvalRequestedForTicket(ticketId);
this.logger.log(
`Approval email sent successfully for Ticket ${ticketId}`,
);
} catch (error) {
this.logger.error(
`Failed to send approval email for Ticket ${ticketId}`,
error,
);
throw error;
}
}
}

View File

@ -0,0 +1,43 @@
import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Logger } from '@nestjs/common';
import { Job } from 'bullmq';
import { lastValueFrom } from 'rxjs';
import { TOUR_FILE_UPLOAD_QUEUE } from 'src/modules/feat-business-objects/entities/tour/tour.tokens';
import { DispoLiveExtractorsService } from '../../feat-extractors/dispo-live-extractors.service';
import { TourFilesService } from 'src/modules/feat-business-objects/entities/tour-files/tour-files.service';
@Processor(TOUR_FILE_UPLOAD_QUEUE)
export class TourFileUploadConsumer extends WorkerHost {
private readonly logger = new Logger(TourFileUploadConsumer.name);
constructor(
private readonly tourFileService: TourFilesService,
private readonly dlService: DispoLiveExtractorsService,
) {
super();
}
async process(job: Job, token?: string): Promise<any> {
const { tourFileId } = job.data;
try {
const tourFile = await this.tourFileService.findById(tourFileId);
this.logger.log(`starting upload for tourId: ${tourFile.tourId}`);
const res = await lastValueFrom(
this.dlService.uploadFileToTour(
tourFile.tourId,
tourFile.storagePath,
tourFile.originalFilename,
),
);
this.logger.log(`file upload success (Status: ${res.status})`);
await this.tourFileService.updateStatus(tourFile.id, 'success');
} catch (error) {
console.error('upload failed', error);
await this.tourFileService.updateStatus(tourFileId, 'failed');
throw error;
}
}
}

View File

@ -12,7 +12,7 @@ import {
set, set,
subDays, subDays,
subHours, subHours,
subWeeks subWeeks,
} from 'date-fns'; } from 'date-fns';
import { PrismaService } from 'src/core/database/prisma.service'; import { PrismaService } from 'src/core/database/prisma.service';
import { generateIntervalForTimeRange } from 'src/utils'; import { generateIntervalForTimeRange } from 'src/utils';
@ -253,13 +253,13 @@ export class KpiService {
ticketState: TicketValidationState, ticketState: TicketValidationState,
): Promise<KpiInfoObjectType> { ): Promise<KpiInfoObjectType> {
return { return {
main: await this.fastMovingTicketPercentMetric(ticketState), main: { label: '', value: 0 }, // await this.fastMovingTicketPercentMetric(ticketState),
secondary: await this.averageTimePerStateMetric(ticketState), secondary: { label: '', value: 0 }, // await this.averageTimePerStateMetric(ticketState),
additional: [ additional: [
await this.ticketCountPerStateMetric(ticketState), await this.ticketCountPerStateMetric(ticketState),
...(ticketState === TicketValidationState.USER_INTERACTION_REQUIRED // ...(ticketState === TicketValidationState.USER_INTERACTION_REQUIRED
? [await this.ticketApprovalRequiredCountMetric(ticketState)] // ? [await this.ticketApprovalRequiredCountMetric(ticketState)]
: []), // : []),
await this.tourCountPerTicketStateMetric(ticketState), await this.tourCountPerTicketStateMetric(ticketState),
], ],
}; };
@ -317,9 +317,9 @@ export class KpiService {
state: true, state: true,
}, },
orderBy: { orderBy: {
id: 'asc' id: 'asc',
}, },
take: 1000 take: 1000,
}); });
const timePerTickets = movedTickets const timePerTickets = movedTickets
.map(({ state }) => { .map(({ state }) => {

View File

@ -59,13 +59,19 @@ export class TicketLifecycleService {
switch (ticket.currentState) { switch (ticket.currentState) {
case TicketValidationState.USER_INTERACTION_REQUIRED: case TicketValidationState.USER_INTERACTION_REQUIRED:
const approvalStateUpdate = documentInfo.approval.isNeeded let approvalStateUpdate: ApprovalState;
? ApprovalState.REQUESTED if (documentInfo.approval.whoRequested == 'no') {
: ApprovalState.FREE; approvalStateUpdate = ApprovalState.REQUIRED;
} else {
approvalStateUpdate = documentInfo.approval.isNeeded
? ApprovalState.REQUESTED
: ApprovalState.FREE;
}
let stateUpdate: TicketValidationState = let stateUpdate: TicketValidationState =
TicketValidationState.USER_INTERACTION_REQUIRED; TicketValidationState.USER_INTERACTION_REQUIRED;
if ( if (approvalStateUpdate === ApprovalState.REQUIRED) {
} else if (
documentInfo.tdLocation === 'company' && documentInfo.tdLocation === 'company' &&
approvalStateUpdate === ApprovalState.FREE approvalStateUpdate === ApprovalState.FREE
) )
@ -137,29 +143,12 @@ export class TicketLifecycleService {
: ApprovalState.REQUIRED, : ApprovalState.REQUIRED,
}); });
} else { } else {
// NOTE: Condition required to get these tickets out of USER_INTERACTION_REQUIRED await this.ticketService.updateTicket(ticket.id, {
if ( errors,
ticket.tours.every( notes,
({ ordinanceType }) => state: TicketValidationState.TD_UNCERTAIN,
ordinanceType.indexOf('BTW') > -1 || approvalState: ApprovalState.FREE,
ordinanceType.indexOf('TSW') > -1, });
) &&
ticket.currentState != TicketValidationState.TD_UNCERTAIN
) {
await this.ticketService.updateTicket(ticket.id, {
errors,
notes,
state: TicketValidationState.TD_UNCERTAIN,
approvalState: ApprovalState.FREE,
});
} else {
await this.ticketService.updateTicket(ticket.id, {
errors,
notes,
state: TicketValidationState.USER_INTERACTION_REQUIRED,
approvalState: ApprovalState.FREE,
});
}
} }
} else { } else {
await this.ticketService.setActionablesOnTicket( await this.ticketService.setActionablesOnTicket(
@ -260,13 +249,17 @@ export class TicketLifecycleService {
if ( if (
[ [
'Einweisung', '1. Entlassung',
'Entlassung', '2. Entlassung Vorbestellung',
'Entlassung Vorbestellung', '3. Einweisung',
'Konsil KH - Dia', '5. Verlegung',
'6. Barzahlung',
'Privatfahrt Rechnung', 'Privatfahrt Rechnung',
'Tagesklinik', 'Reha',
'Verlegung', 'Tagesklinik - AKW',
'Tagesklinik - ASK',
'Tagesklinik - MKH',
'Transportschein abholen',
].includes(tour.category) || ].includes(tour.category) ||
tour.healthInsurance === 'KOSTENTRÄGER PATIENT' tour.healthInsurance === 'KOSTENTRÄGER PATIENT'
) { ) {

View File

@ -5,7 +5,7 @@ import { TicketLifecycleService } from './ticket-lifecycle.service';
import { import {
TicketSystemEventTypes, TicketSystemEventTypes,
TicketSystemValidatedEvent, TicketSystemValidatedEvent,
} from './ticket-system.events'; } from '../feat-business-objects/entities/ticket/ticket-system.events';
@Injectable() @Injectable()
export class TicketService { export class TicketService {

View File

@ -0,0 +1,168 @@
id: syn-extraction
namespace: dev
description: Run Avicenna extraction with synapse
labels:
env: dev
project: avicenna
# triggers:
# - id: schedule
# type: io.kestra.plugin.core.trigger.Schedule
# cron: "*/5 * * * *"
tasks:
- id: synapseConfig
type: io.kestra.plugin.core.storage.Write
content: |
pipelines:
- name: "patient"
source:
type: "rest_api"
config:
base_url: "https://avicenna.dispolive.de"
path: "/data/patientenStammdaten"
method: POST
records_path: "data"
auth:
strategy: "dispolive"
dispolive:
username: Marcel
password: arndt1.!
rate_limit:
requests_per_second: 5
burst: 1
destination:
type: "csv"
config:
path: "./patient.csv"
transform:
- strategy: keep_fields
config:
fields:
- "_id"
- "name"
- "surname"
- "street"
- "zip"
- "city"
- "birthday"
- "kkId"
- "krankenkasse"
- strategy: rename_fields
config:
mapping:
_id: id
- strategy: add_fields
config:
fields: |
{
"jobId": "{{ execution.id }}"
}
- name: "attendanceRegistration"
sync:
strategy: "time_range_iteration"
time_range_iteration:
start_date: "2025-02-01"
end_date: "2025-02-28"
date_format: "2006-01-02"
interval: 1d
source:
type: "rest_api"
config:
base_url: "https://app.planning.nu"
{% raw %}path: "/avicennaambulance/api2/attendance-registration/{{.SliceStart}}"{% endraw %}
records_path: "attendanceRegistration"
auth:
strategy: "header"
header:
header: "authorization"
token: "Token ${SYN_DYFLEXIS_V2_TOKEN}"
rate_limit:
requests_per_second: 5
burst: 1
destination:
type: "csv"
config:
path: "./_output/02-2025-attendance-registrations.csv"
- id: run
type: io.kestra.plugin.docker.Run
containerImage: thedevilisdero/synapse:latest
pullPolicy: ALWAYS
inputFiles:
synapse.yaml: "{{ outputs.synapseConfig.uri }}"
commands:
- --config
- synapse.yaml
- run
- patient
outputFiles:
- patient.csv
- id: log
type: io.kestra.plugin.core.log.Log
message: "{{ read(outputs.run.outputFiles['patient.csv']) }}"
- id: deleteSource
type: io.kestra.plugin.jdbc.postgresql.Query
url: jdbc:postgresql://avicenna_db:5432/avicenna
username: johndoe
password: randompassword
sql: DELETE FROM "PatientSource";
- id: copyin
type: io.kestra.plugin.jdbc.postgresql.CopyIn
url: jdbc:postgresql://avicenna_db:5432/avicenna
username: johndoe
password: randompassword
format: CSV
from: "{{ outputs.run.outputFiles['patient.csv'] }}"
table: "\"PatientSource\""
header: true
columns:
- birthday
- city
- id
- "\"jobId\""
- "\"kkId\""
- krankenkasse
- name
- street
- surname
- zip
# - id: loadStatic
# type: io.kestra.plugin.core.flow.EachSequential
# value: '{{ outputs.getPartitions.vars.partitions }}'
# - id: getPartitions
# type: io.kestra.plugin.scripts.python.Script
# taskRunner:
# type: io.kestra.plugin.scripts.runner.docker.Docker
# containerImage: ghcr.io/kestra-io/pydata:latest
# beforeCommands:
# - pip install -U arrow
# script: |
# from kestra import Kestra
# import arrow
# partitions = []
# now = arrow.utcnow() #.shift(months=-1)
# partitions.append("from={}&to={}".format(now.span('month')[0].format('YYYY-MM-DD'), now.shift(days=14).format('YYYY-MM-DD')))
# lastMonth = now.shift(months=-1)
# partitions.append("from={}&to={}".format(lastMonth.span('month')[0].format('YYYY-MM-DD'), lastMonth.span('month')[-1].format('YYYY-MM-DD')))
# monthBeforeLast = lastMonth.shift(months=-1)
# partitions.append("from={}&to={}".format(monthBeforeLast.span('month')[0].format('YYYY-MM-DD'), monthBeforeLast.span('month')[-1].format('YYYY-MM-DD')))
# monthBeforeLast = monthBeforeLast.shift(months=-1)
# partitions.append("from={}&to={}".format(monthBeforeLast.span('month')[0].format('YYYY-MM-DD'), monthBeforeLast.span('month')[-1].format('YYYY-MM-DD')))
# Kestra.outputs({'partitions': partitions})
# - id: loadThreeMonths
# type: io.kestra.plugin.core.flow.EachSequential
# value: '{{ outputs.getPartitions.vars.partitions }}'
# tasks:
# - id: loadTours
# type: "io.kestra.plugin.docker.Run"
# containerImage: thedevilisdero/synapse:latest
# - id: "request"
# type: "io.kestra.plugin.core.http.Request"
# uri: "http://avicenna_data:3000/etl?{{ taskrun.value }}"
# headers:
# user-agent: "kestra-io"
# options:
# readTimeout: "PT180S"
# method: "GET"

View File

@ -0,0 +1,99 @@
id: main_sync_workflow
namespace: ceo.genius.etl
tasks:
# Schritt 1: Definiere alle Entitäten und berechne die Zeiträume für die Synchronisation
- id: setup_parameters
type: io.kestra.plugin.scripts.python.Script
containerImage: python:3.11-slim
beforeCommands:
- "pip install python-dateutil"
- "pip install kestra"
script: |
from kestra import Kestra
import json
from datetime import date
from dateutil.relativedelta import relativedelta
today = date.today()
# 1. Definiere die Zeiträume: aktueller Monat + die letzten 3 vollen Monate
date_ranges = []
# Aktueller Monat (vom 1. bis heute)
date_ranges.append({
"start": today.replace(day=1).strftime("%Y-%m-%d"),
"end": today.strftime("%Y-%m-%d")
})
# Letzte 3 volle Monate
for i in range(1, 4):
month = today - relativedelta(months=i)
start_of_month = month.replace(day=1)
end_of_month = (start_of_month + relativedelta(months=1)) - relativedelta(days=1)
date_ranges.append({
"start": start_of_month.strftime("%Y-%m-%d"),
"end": end_of_month.strftime("%Y-%m-%d")
})
# 2. Definiere die Entitäten mit ihrem Schema.
# HIER IST DER ZENTRALE ORT, UM NEUE ENTITÄTEN HINZUZUFÜGEN ODER ZU ÄNDERN.
config = {
"static_entities": [
{
"name": "patient",
"db_table": "PatientSource",
"columns": ["birthday", "city", "id", "\"jobId\"", "\"kkId\"", "krankenkasse", "name", "street", "surname", "zip"]
}
# FÜGE HIER WEITERE STATISCHE ENTITÄTEN HINZU
# {
# "name": "neue_entitaet",
# "db_table": "NeueEntitaetSource",
# "columns": ["spalte1", "spalte2"]
# }
],
"dynamic_entities": [
{
"name": "attendanceRegistration",
"db_table": "AttendanceRegistrationSource",
"columns": ["\"attendanceRegistrationId\"", "\"dateTime\"", "\"employeeId\"", "event", "\"jobId\"", "\"personnelNumber\""]
}
# FÜGE HIER WEITERE DYNAMISCHE ENTITÄTEN HINZU
],
"date_ranges": date_ranges
}
Kestra.outputs({'entity_config': config})
print(json.dumps(config))
- id: sync_static_entities
type: io.kestra.plugin.core.flow.EachParallel
value: "{{ outputs.setup_parameters.vars.entity_config.static_entities }}"
tasks:
- id: run_static_subflow
type: io.kestra.plugin.core.flow.Subflow
namespace: ceo.genius.etl
flowId: subflow_static_entity
inputs:
entity_name: "{{ taskrun.value.name }}"
db_table: "{{ taskrun.value.db_table }}"
columns: "{{ taskrun.value.columns }}"
- id: sync_dynamic_entities
type: io.kestra.plugin.core.flow.EachParallel
value: |
{%- set result = [] -%}
{%- for entity in outputs.setup_parameters.output.dynamic_entities -%}
{%- for range in outputs.setup_parameters.output.date_ranges -%}
{%- do result.append({ "entity": entity, "range": range }) -%}
{%- endfor -%}
{%- endfor -%}
{{ result | json }}
tasks:
- id: run_dynamic_subflow
type: io.kestra.plugin.core.flow.Subflow
namespace: ceo.genius.etl # Passe dies an, falls die Subflows in einem anderen Namespace liegen
flowId: subflow_dynamic_entity
inputs:
entity_name: "{{ taskrun.value.entity.name }}"
db_table: "{{ taskrun.value.entity.db_table }}"
columns: "{{ taskrun.value.entity.columns }}"
startDate: "{{ taskrun.value.range.start }}"
endDate: "{{ taskrun.value.range.end }}"

View File

@ -0,0 +1,51 @@
id: subflow_static_entity
namespace: ceo.genius.etl
inputs:
- id: entity_name
type: STRING
- id: db_table
type: STRING
- id: columns
type: JSON
tasks:
# - id: render_synapse_config
# type: io.kestra.core.tasks.storages.template
# templateUri: "kestra://{{ flow.namespace }}/_templates/{{ inputs.entity_name }}.yml"
- id: render_synapse_config
type: io.kestra.plugin.core.templating.TemplatedTask
spec: |
{{ read('nsfile://' ~ flow.namespace ~ '/configs/' ~ inputs.entity_name ~ '.yml') }}
- id: run_synapse
type: io.kestra.plugin.docker.Run
containerImage: thedevilisdero/synapse:latest
pullPolicy: ALWAYS
inputFiles:
synapse.yaml: "{{ read('nsfile://' ~ flow.namespace ~ '/configs/' ~ inputs.entity_name ~ '.yml') }}"
commands:
- --config
- synapse.yaml
- run
- "{{ inputs.entity_name }}"
outputFiles:
- "{{ inputs.entity_name }}.csv"
- id: delete_from_table
type: io.kestra.plugin.jdbc.postgresql.Query
url: jdbc:postgresql://avicenna_db:5432/avicenna # ANPASSEN
username: johndoe # ANPASSEN
password: randompassword # ANPASSEN
sql: 'DELETE FROM "{{ inputs.db_table }}";'
- id: copy_to_table
type: io.kestra.plugin.jdbc.postgresql.CopyIn
url: jdbc:postgresql://avicenna_db:5432/avicenna # ANPASSEN
username: johndoe # ANPASSEN
password: randompassword # ANPASSEN
from: "{{ outputs.run_synapse.outputFiles[inputs.entity_name ~ '.csv'] }}"
table: '"{{ inputs.db_table }}"'
header: true
columns: "{{ inputs.columns }}"

View File

@ -58,8 +58,7 @@ async function main() {
const schichtplanung = await getSchichtplanungForDay(new Date('2025-08-11')).then(response => response.json()); const schichtplanung = await getSchichtplanungForDay(new Date('2025-08-11')).then(response => response.json());
const chunks = chunk(schichtplanung.map(({id}) => id), 1) const chunks = chunk(schichtplanung.map(({id}) => id), 1)
for(const chunk of chunks) { for(const chunk of chunks) {
const res = await deleteSchichtplanungItems(chunk as string[]) await deleteSchichtplanungItems(chunk as string[])
console.log(res)
await sleep(200) await sleep(200)
} }
} }