parent
c33ab89354
commit
b1a70673a4
|
|
@ -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.
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
FROM node:20.15.1-alpine AS builder
|
||||
FROM node:20.19.1-alpine AS builder
|
||||
|
||||
WORKDIR /app/builder
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"componentsPath": "src/app/core/base-components",
|
||||
"importAlias": "@spartan-ng/helm"
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
#!/bin/bash
|
||||
set -xe
|
||||
: "${API_BASEURL?API_BASEURL is not provided}"
|
||||
: "${GRAPHQL_ENDPOINT?GRAPHQL_ENDPOINT is not provided}"
|
||||
: "${GRAPHQL_WS_ENDPOINT?GRAPHQL_WS_ENDPOINT 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}"
|
||||
: "${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/
|
||||
|
||||
for var in "${env_vars[@]}"
|
||||
do
|
||||
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
|
||||
|
||||
exec "$@"
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -25,8 +25,8 @@
|
|||
"@apollo/client": "^3.13.8",
|
||||
"@azure/msal-angular": "^3.0.22",
|
||||
"@azure/msal-browser": "^3.19.1",
|
||||
"@grafana/faro-web-sdk": "^1.15.0",
|
||||
"@grafana/faro-web-tracing": "^1.15.0",
|
||||
"@ng-icons/core": "^31.4.0",
|
||||
"@ng-icons/lucide": "^31.4.0",
|
||||
"@ngneat/dialog": "^5.1.1",
|
||||
"@ngneat/overview": "^6.0.0",
|
||||
"@ngxpert/hot-toast": "^3.0.0",
|
||||
|
|
@ -41,12 +41,16 @@
|
|||
"@opentelemetry/resources": "^1.30.1",
|
||||
"@opentelemetry/sdk-trace-web": "^1.30.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.34.0",
|
||||
"@spartan-ng/brain": "^0.0.1-alpha.561",
|
||||
"apollo-angular": "^10.0.3",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^3.6.0",
|
||||
"echarts": "^5.5.1",
|
||||
"graphql": "^16.11.0",
|
||||
"graphql-ws": "^5.16.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
|
|
@ -60,6 +64,7 @@
|
|||
"@graphql-codegen/typescript-apollo-angular": "^4.0.0",
|
||||
"@graphql-codegen/typescript-operations": "^4.6.0",
|
||||
"@material/material-color-utilities": "^0.3.0",
|
||||
"@spartan-ng/cli": "^0.0.1-alpha.561",
|
||||
"@tailwindcss/postcss": "^4.1.4",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
|
|
@ -71,6 +76,7 @@
|
|||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"postcss": "^8.4.39",
|
||||
"tailwindcss": "^4.1.4",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "~5.5.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,10 +17,6 @@ import { graphqlProvider } from './graphql.provider';
|
|||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
// provideAppInitializer(() => {
|
||||
// const initializerFn = faroInitializer();
|
||||
// return initializerFn();
|
||||
// }),
|
||||
{
|
||||
provide: ErrorHandler,
|
||||
useClass: GlobalErrorHandler,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
import { HlmInput } from './lib/hlm-input';
|
||||
|
||||
export * from './lib/hlm-input';
|
||||
|
||||
export const HlmInputImports = [HlmInput] as const;
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { HlmLabel } from './lib/hlm-label';
|
||||
|
||||
export * from './lib/hlm-label';
|
||||
|
||||
export const HlmLabelImports = [HlmLabel] as const;
|
||||
|
|
@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './lib/hlm';
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { type ClassValue, clsx } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function hlm(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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")
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -27,6 +27,7 @@ export type AccountingFilterArgs = {
|
|||
export enum AccountingState {
|
||||
Billable = 'Billable',
|
||||
Billed = 'Billed',
|
||||
Pending = 'Pending',
|
||||
PreparedForBilling = 'PreparedForBilling'
|
||||
}
|
||||
|
||||
|
|
@ -165,6 +166,7 @@ export type Metric = {
|
|||
export type Mutation = {
|
||||
__typename?: 'Mutation';
|
||||
acceptAnomaly: Anomaly;
|
||||
sendApprovalRequest: Scalars['Boolean']['output'];
|
||||
setAnomalyToSleep: Anomaly;
|
||||
setDocumentInfo: Ticket;
|
||||
};
|
||||
|
|
@ -175,6 +177,12 @@ export type MutationAcceptAnomalyArgs = {
|
|||
};
|
||||
|
||||
|
||||
export type MutationSendApprovalRequestArgs = {
|
||||
email: Scalars['String']['input'];
|
||||
ticketId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationSetAnomalyToSleepArgs = {
|
||||
id: Scalars['String']['input'];
|
||||
};
|
||||
|
|
@ -523,6 +531,7 @@ export type Tour = {
|
|||
totalKm: Scalars['Float']['output'];
|
||||
transportType?: Maybe<Scalars['String']['output']>;
|
||||
type: Scalars['String']['output'];
|
||||
updatedAt?: Maybe<Scalars['DateTime']['output']>;
|
||||
};
|
||||
|
||||
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 SendApprovalRequestMutationVariables = Exact<{
|
||||
email: Scalars['String']['input'];
|
||||
ticketId: Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type SendApprovalRequestMutation = { __typename?: 'Mutation', sendApprovalRequest: boolean };
|
||||
|
||||
export type GetUpcomingToursQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
|
|
@ -1513,6 +1530,22 @@ export const SetDocumentInfoDocument = gql`
|
|||
export class SetDocumentInfoGQL extends Apollo.Mutation<SetDocumentInfoMutation, SetDocumentInfoMutationVariables> {
|
||||
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) {
|
||||
super(apollo);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -217,3 +217,7 @@ mutation SetDocumentInfo(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
mutation SendApprovalRequest($email: String!, $ticketId: String!) {
|
||||
sendApprovalRequest(email: $email, ticketId: $ticketId)
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
|
@ -17,7 +17,7 @@ import { DashboardAccountingService } from '../dashboard-accounting.service';
|
|||
<div class="flex justify-around items-center">
|
||||
<div class="flex flex-col justify-center items-center">
|
||||
<div class="text-xl">{{ kpi()?.count }}</div>
|
||||
<div class="text-sm">Tickets</div>
|
||||
<div class="text-sm">Touren</div>
|
||||
</div>
|
||||
@if(kpi()?.revenueSum && accountingState() !== AccountingState.Billed) {
|
||||
<div class="flex flex-col justify-center items-center">
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ import {
|
|||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 'dks-accounting-item',
|
||||
template: ` <div class="flex justify-between">
|
||||
template: `
|
||||
<div class="flex justify-between">
|
||||
<div class="flex items-center">
|
||||
<button
|
||||
mat-icon-button
|
||||
|
|
@ -31,7 +32,13 @@ import {
|
|||
>
|
||||
<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 }}
|
||||
</a>
|
||||
</div>
|
||||
<span>{{ tour.ordinanceType }}</span>
|
||||
</div>
|
||||
|
|
@ -71,7 +78,7 @@ import {
|
|||
MatBadgeModule,
|
||||
ClipboardModule,
|
||||
NgClass,
|
||||
CurrencyPipe
|
||||
CurrencyPipe,
|
||||
],
|
||||
})
|
||||
export class AccountingItemComponent implements AfterViewInit {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
|
|
@ -11,30 +10,28 @@ import {
|
|||
AccountingState,
|
||||
} from '../../../core/data-access/graphql/generated/generated';
|
||||
import { AccountingLaneHeaderFilterComponent } from './accounting-lane-header-filter.component';
|
||||
import { RoleDirective } from 'src/app/core/components/ms-authentication/role.directive';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 'dks-accounting-lane-header',
|
||||
template: `<div></div>
|
||||
@switch (state) {
|
||||
@case (AccountingState.Billable) {
|
||||
@switch (state) { @case(AccountingState.Pending) {
|
||||
<div>Offen</div>
|
||||
} @case (AccountingState.Billable) {
|
||||
<div>Abrechenbar</div>
|
||||
}
|
||||
@case (AccountingState.PreparedForBilling) {
|
||||
} @case (AccountingState.PreparedForBilling) {
|
||||
<div>für Abrechnung vorbereitet</div>
|
||||
} @case (AccountingState.Billed) {
|
||||
<div>Abgerechnet (mit Abweichung)</div>
|
||||
} }
|
||||
<div>
|
||||
für Abrechnung vorbereitet
|
||||
</div>
|
||||
}
|
||||
@case (AccountingState.Billed) {
|
||||
<div>
|
||||
Abgerechnet (mit Abweichung)
|
||||
</div>
|
||||
}
|
||||
}
|
||||
<dks-accounting-lane-header-filter
|
||||
*dksRole="['admin']"
|
||||
[state]="state"
|
||||
(filterChange)="filterChanged($event)"
|
||||
></dks-accounting-lane-header-filter>`,
|
||||
></dks-accounting-lane-header-filter>
|
||||
</div>`,
|
||||
host: {
|
||||
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 {
|
||||
@Input() state!: AccountingState;
|
||||
|
|
|
|||
|
|
@ -98,7 +98,6 @@ export class AccountingTourCostBreakdownComponent {
|
|||
servicePositions: any[] | undefined,
|
||||
idx: number
|
||||
): number {
|
||||
console.log(servicePositions, idx);
|
||||
return (servicePositions ?? [])
|
||||
.slice(0, idx + 1)
|
||||
.reduce((sum, { value }) => (sum += value), 0);
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
<!-- Action Buttons -->
|
||||
</div>
|
||||
<app-accounting-info-card
|
||||
class="self-center"
|
||||
[accountingState]="AccountingState.Pending"
|
||||
></app-accounting-info-card>
|
||||
<app-accounting-info-card
|
||||
class="self-center"
|
||||
[accountingState]="AccountingState.Billable"
|
||||
|
|
@ -18,20 +22,16 @@ import { AccountingLaneComponent } from './accounting-lane/accounting-lane.compo
|
|||
class="self-center"
|
||||
[accountingState]="AccountingState.PreparedForBilling"
|
||||
></app-accounting-info-card>
|
||||
<app-accounting-info-card
|
||||
class="self-center"
|
||||
[accountingState]="AccountingState.Billed"
|
||||
></app-accounting-info-card>
|
||||
<div>
|
||||
<dks-accounting-lane
|
||||
[accountingState]="AccountingState.Billable"
|
||||
[accountingState]="AccountingState.Pending"
|
||||
></dks-accounting-lane>
|
||||
</div>
|
||||
<dks-accounting-lane
|
||||
[accountingState]="AccountingState.PreparedForBilling"
|
||||
[accountingState]="AccountingState.Billable"
|
||||
></dks-accounting-lane>
|
||||
<dks-accounting-lane
|
||||
[accountingState]="AccountingState.Billed"
|
||||
[accountingState]="AccountingState.PreparedForBilling"
|
||||
></dks-accounting-lane>
|
||||
</section>`,
|
||||
styles: [
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import {
|
|||
PaginatedAnomaly,
|
||||
PaginatedAnomalyGroup,
|
||||
} from '../../core/data-access/graphql/generated/generated';
|
||||
import { faro, LogLevel } from '@grafana/faro-web-sdk';
|
||||
|
||||
@Injectable()
|
||||
export class AnomaliesService {
|
||||
|
|
@ -90,12 +89,6 @@ export class AnomaliesService {
|
|||
'Leider ist etwas schief gelaufen, versuchen Sie es bitte nochmal'
|
||||
);
|
||||
} else {
|
||||
faro.api.pushLog([`Sleep anomaly ${anomalyId}`], {
|
||||
context: {
|
||||
payload: anomalyId,
|
||||
},
|
||||
level: LogLevel.TRACE,
|
||||
})
|
||||
this.toast.success(
|
||||
'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'
|
||||
);
|
||||
} else {
|
||||
faro.api.pushLog([`Accept anomaly ${anomalyId}`], {
|
||||
context: {
|
||||
payload: anomalyId,
|
||||
},
|
||||
level: LogLevel.TRACE,
|
||||
})
|
||||
this.toast.success('Die Anomalie wurde akzeptiert!');
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -76,6 +76,5 @@ export class AnomalyViewComponent {
|
|||
}
|
||||
|
||||
ngOnInit() {
|
||||
console.log(this.anomalyId());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import {
|
|||
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 { LinkCardComponent } from './link-card/link-card.component';
|
||||
import { SkeletonDirective } from '../../core/components/skeleton-rect/skeleton.directive';
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
|
|
@ -22,7 +21,6 @@ import { SkeletonDirective } from '../../core/components/skeleton-rect/skeleton.
|
|||
LinkCardComponent,
|
||||
MatDividerModule,
|
||||
CurrencyPipe,
|
||||
SkeletonDirective,
|
||||
],
|
||||
template: `
|
||||
<div class="flex flex-col m-0 md:m-4">
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ export class TrendGraphComponent {
|
|||
private chart?: ECharts;
|
||||
|
||||
public readonly options = computed<EChartsOption>(() => {
|
||||
console.log(this.data());
|
||||
return this.generateEchartsOptions();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -7,11 +7,10 @@ import {
|
|||
Metric,
|
||||
} from '../../../../core/data-access/graphql/generated/generated';
|
||||
import { ManagerDashboardService } from '../../manager-dashboard.service';
|
||||
import { SkeletonDirective } from '../../../../core/components/skeleton-rect/skeleton.directive';
|
||||
|
||||
@Component({
|
||||
selector: 'app-manager-info-card',
|
||||
imports: [InfoCardModule, DecimalPipe, SkeletonDirective],
|
||||
imports: [InfoCardModule, DecimalPipe],
|
||||
templateUrl: './manager-info-card.component.html',
|
||||
styleUrl: './manager-info-card.component.scss',
|
||||
})
|
||||
|
|
|
|||
|
|
@ -16,14 +16,10 @@
|
|||
}
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
>{{ ticket.tours.at(0)?.patientName }}
|
||||
{{ ticket.tours.at(0)?.patientSurname }}</span
|
||||
>
|
||||
<span
|
||||
>{{ getToursLength() }} Tour{{
|
||||
getToursLength() > 1 ? 'en' : ''
|
||||
}}</span
|
||||
>
|
||||
<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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
HostBinding,
|
||||
inject,
|
||||
input,
|
||||
Input,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
|
|
@ -24,6 +25,10 @@ import CategoryColors from './category-colors';
|
|||
host: {
|
||||
class:
|
||||
'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: [
|
||||
`
|
||||
|
|
@ -35,11 +40,10 @@ import CategoryColors from './category-colors';
|
|||
imports: [MatIconModule, MatBadgeModule],
|
||||
})
|
||||
export class TicketItemComponent implements OnInit {
|
||||
hasActivity = input(false)
|
||||
@Input() ticket!: Ticket & { tours: Tour[] };
|
||||
|
||||
@HostBinding('style.border-left-color')
|
||||
leftBorderColor = '#ffffff';
|
||||
@HostBinding('style.border-right-color')
|
||||
rightBorderColor = '#ffffff';
|
||||
|
||||
@HostBinding('style.background-color')
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
|
|
@ -11,38 +10,30 @@ import {
|
|||
TicketValidationState,
|
||||
} from '../../../core/data-access/graphql/generated/generated';
|
||||
import { TicketLaneHeaderFilterComponent } from './ticket-lane-header-filter.component';
|
||||
import { RoleDirective } from 'src/app/core/components/ms-authentication/role.directive';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 'dks-ticket-lane-header',
|
||||
template: `<div></div>
|
||||
@switch (state) {
|
||||
@case (TicketValidationState.Created) {
|
||||
<div>Touren angenommen</div>
|
||||
}
|
||||
@case (TicketValidationState.UserInteractionRequired) {
|
||||
<div>
|
||||
Genehmigung/Transportschein
|
||||
</div>
|
||||
}
|
||||
@case (TicketValidationState.TdUncertain) {
|
||||
<div>
|
||||
Transportschein unklar
|
||||
</div>
|
||||
}
|
||||
@case (TicketValidationState.DocumentsMissing) {
|
||||
<div>
|
||||
Dokumente fehlen
|
||||
</div>
|
||||
}
|
||||
@case (TicketValidationState.Archived) {
|
||||
@switch (state) { @case (TicketValidationState.Created) {
|
||||
<div>Daten ergänzen</div>
|
||||
} @case (TicketValidationState.UserInteractionRequired) {
|
||||
<div>Genehmigung einleiten</div>
|
||||
} @case (TicketValidationState.TdUncertain) {
|
||||
<div>Transportschein organisieren</div>
|
||||
} @case (TicketValidationState.DocumentsMissing) {
|
||||
<div>Dokumente prüfen</div>
|
||||
} @case (TicketValidationState.Archived) {
|
||||
<div>Archiv</div>
|
||||
}
|
||||
}
|
||||
} }
|
||||
<div>
|
||||
<dks-ticket-lane-header-filter
|
||||
*dksRole="['admin']"
|
||||
[state]="state"
|
||||
(filterChange)="filterChanged($event)"
|
||||
></dks-ticket-lane-header-filter>`,
|
||||
></dks-ticket-lane-header-filter>
|
||||
</div>`,
|
||||
host: {
|
||||
class: 'flex items-center justify-between p-2',
|
||||
},
|
||||
|
|
@ -54,7 +45,7 @@ import { TicketLaneHeaderFilterComponent } from './ticket-lane-header-filter.com
|
|||
}
|
||||
`,
|
||||
],
|
||||
imports: [TicketLaneHeaderFilterComponent]
|
||||
imports: [TicketLaneHeaderFilterComponent, RoleDirective],
|
||||
})
|
||||
export class TicketLaneHeaderComponent {
|
||||
@Input() state!: TicketValidationState;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
class="mat-elevation-z4 animate-fadeIn"
|
||||
(click)="openTicketView(ticket)"
|
||||
[ticket]="ticket"
|
||||
[hasActivity]="activityService.isDirty(ticket.id)"
|
||||
>
|
||||
</dks-ticket-item>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
import { AsyncPipe } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
Input,
|
||||
ViewChild,
|
||||
|
|
@ -31,6 +32,7 @@ import { TicketItemComponent } from '../ticket-item/ticket-item.component';
|
|||
import { TicketViewComponent } from '../ticket-view/ticket-view.component';
|
||||
import { TicketsService } from '../tickets.service';
|
||||
import { TicketLaneHeaderComponent } from './ticket-lane-header.component';
|
||||
import { ActivityService } from '../../../core/services/activity.service';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
|
@ -58,6 +60,8 @@ import { TicketLaneHeaderComponent } from './ticket-lane-header.component';
|
|||
export class TicketLaneComponent {
|
||||
private dialog = inject(DialogService);
|
||||
private ticketsService = inject(TicketsService);
|
||||
activityService = inject(ActivityService);
|
||||
private cdr = inject(ChangeDetectorRef);
|
||||
|
||||
private ticketState$ = new BehaviorSubject<TicketValidationState | undefined>(
|
||||
undefined
|
||||
|
|
@ -129,10 +133,15 @@ export class TicketLaneComponent {
|
|||
}
|
||||
|
||||
openTicketView(ticket: Ticket & { tours: Tour[] }): void {
|
||||
this.activityService.markAsDirty(ticket.id);
|
||||
const dialogRef = this.dialog.open(TicketViewComponent, {
|
||||
data: ticket,
|
||||
width: '40vw',
|
||||
minHeight: '400px',
|
||||
enableClose: {
|
||||
backdrop: true,
|
||||
escape: false,
|
||||
},
|
||||
});
|
||||
|
||||
dialogRef.afterClosed$
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<!-- <ng-container
|
||||
*skeleton="isLoading(); repeat: 5; className: 'mr-2'; height: '50px'"
|
||||
*skeleton="true; repeat: 1; className: 'mr-2'; height: '100px'"
|
||||
></ng-container> -->
|
||||
@if (kpi()?.main?.label) {
|
||||
@if (kpi()?.main?.label != '') {
|
||||
<app-info-card
|
||||
[chartValue]="kpi()?.main?.value!"
|
||||
[chartLabel]="kpi()?.main?.label!"
|
||||
|
|
@ -19,4 +19,15 @@
|
|||
</app-info-card-footer>
|
||||
}
|
||||
</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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,11 +8,10 @@ import {
|
|||
TicketValidationState,
|
||||
} from '../../../core/data-access/graphql/generated/generated';
|
||||
import { TicketsService } from '../tickets.service';
|
||||
import { SkeletonDirective } from '../../../core/components/skeleton-rect/skeleton.directive';
|
||||
|
||||
@Component({
|
||||
selector: 'app-ticket-state-info-card',
|
||||
imports: [InfoCardModule, DecimalPipe, SkeletonDirective],
|
||||
imports: [InfoCardModule, DecimalPipe],
|
||||
templateUrl: './ticket-state-info-card.component.html',
|
||||
styleUrl: './ticket-state-info-card.component.scss',
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
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 { MatListModule } from '@angular/material/list';
|
||||
import {
|
||||
|
|
@ -17,60 +17,40 @@ import {
|
|||
<mat-list-item role="listitem">
|
||||
Typ:
|
||||
<span class="font-bold">
|
||||
{{ ticket.tours[0].type }}
|
||||
{{ ticket().tours[0].type }}
|
||||
</span>
|
||||
</mat-list-item>
|
||||
<mat-list-item role="listitem">
|
||||
Kategorie:
|
||||
<span class="font-bold">
|
||||
{{ ticket.tours[0].category }}
|
||||
{{ ticket().tours[0].category }}
|
||||
</span>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
@if (ticket.approvalState) {
|
||||
@if (ticket().approvalState) {
|
||||
<mat-list class="col-span-2" role="list">
|
||||
<div mat-subheader class="text-sm">Gehnemigungsinformationen</div>
|
||||
<mat-list-item role="listitem">
|
||||
@switch (ticket.approvalState) {
|
||||
@case (ApprovalState.Free) {
|
||||
@switch (ticket().approvalState) { @case (ApprovalState.Free) {
|
||||
<span>Genehmigungsfrei</span>
|
||||
}
|
||||
@case (ApprovalState.Unknown) {
|
||||
<span
|
||||
title="Automatische Prüfung nicht möglich"
|
||||
} @case (ApprovalState.Unknown) {
|
||||
<span title="Automatische Prüfung nicht möglich"
|
||||
>Automatische Prüfung nicht möglich</span
|
||||
>
|
||||
}
|
||||
@case (ApprovalState.Required) {
|
||||
<span
|
||||
>Genehmigung benötigt</span
|
||||
>
|
||||
}
|
||||
@case (ApprovalState.Requested) {
|
||||
<span
|
||||
>Genehmigung angefragt</span
|
||||
>
|
||||
}
|
||||
@case (ApprovalState.Approved) {
|
||||
<span
|
||||
>Genehmigung eingegangen</span
|
||||
>
|
||||
}
|
||||
}
|
||||
} @case (ApprovalState.Required) {
|
||||
<span>Genehmigung benötigt</span>
|
||||
} @case (ApprovalState.Requested) {
|
||||
<span>Genehmigung angefragt</span>
|
||||
} @case (ApprovalState.Approved) {
|
||||
<span>Genehmigung eingegangen</span>
|
||||
} }
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
}
|
||||
@if (ticket.errors.length > 0 || ticket.notes.length > 0) {
|
||||
<mat-list
|
||||
class="col-span-2 max-h-96 overflow-y-auto"
|
||||
role="list"
|
||||
>
|
||||
} @if (ticket().errors.length > 0 || ticket().notes.length > 0) {
|
||||
<mat-list class="col-span-2 max-h-96 overflow-y-auto" role="list">
|
||||
<div mat-subheader class="text-sm">Ticket Anmerkungen</div>
|
||||
@for (error of ticket.errors; track error) {
|
||||
<mat-list-item
|
||||
lines="2"
|
||||
role="listitem"
|
||||
>
|
||||
@for (error of ticket().errors; track error) {
|
||||
<mat-list-item lines="2" 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)">{{
|
||||
|
|
@ -78,26 +58,20 @@ import {
|
|||
}}</span>
|
||||
</mat-list-item>
|
||||
}
|
||||
@for (note of ticket.notes; track note) {
|
||||
<mat-list-item
|
||||
lines="2"
|
||||
role="listitem"
|
||||
>
|
||||
<!-- @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
|
||||
]
|
||||
imports: [MatListModule, MatIconModule],
|
||||
})
|
||||
export class TicketOverviewComponent {
|
||||
@Input() ticket!: Ticket & { tours: Tour[] };
|
||||
ticket = input.required<Ticket & { tours: Tour[] }>()
|
||||
|
||||
ApprovalState = ApprovalState;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import {
|
|||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Input,
|
||||
OnInit
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
|
|
@ -14,6 +14,7 @@ import {
|
|||
Ticket,
|
||||
Tour,
|
||||
} from '../../../core/data-access/graphql/generated/generated';
|
||||
import { TransportDocumentUploadComponent } from '../../../core/components/transport-document-upload/transport-document-upload.component';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
|
@ -41,7 +42,14 @@ import {
|
|||
|
||||
<ng-container matColumnDef="operationId">
|
||||
<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 matColumnDef="startDate">
|
||||
<th mat-header-cell *matHeaderCellDef>Anfahrt</th>
|
||||
|
|
@ -84,7 +92,7 @@ import {
|
|||
MatButtonModule,
|
||||
MatCheckboxModule,
|
||||
MatTableModule,
|
||||
]
|
||||
],
|
||||
})
|
||||
export class TicketToursComponent implements OnInit {
|
||||
@Input() ticket!: Ticket & { tours: Tour[] };
|
||||
|
|
@ -107,7 +115,6 @@ export class TicketToursComponent implements OnInit {
|
|||
);
|
||||
|
||||
ngOnInit(): void {
|
||||
console.log(this.ticket.tours)
|
||||
this.dataSource = [...this.ticket.tours]
|
||||
.filter(({ check }) => check === 0)
|
||||
.sort(
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
computed,
|
||||
effect,
|
||||
EventEmitter,
|
||||
inject,
|
||||
Input,
|
||||
|
|
@ -19,6 +16,7 @@ import { MatFormFieldModule } from '@angular/material/form-field';
|
|||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatRadioModule } from '@angular/material/radio';
|
||||
import { AuthService } from '../../../core/components/ms-authentication/auth.service';
|
||||
import {
|
||||
ApprovalState,
|
||||
DocumentInfo,
|
||||
|
|
@ -26,7 +24,6 @@ import {
|
|||
TicketValidationState,
|
||||
Tour,
|
||||
} from '../../../core/data-access/graphql/generated/generated';
|
||||
import { AuthService } from '../../../core/components/ms-authentication/auth.service';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
|
@ -57,21 +54,19 @@ import { AuthService } from '../../../core/components/ms-authentication/auth.ser
|
|||
<mat-radio-button [value]="true"> Ja </mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</div>
|
||||
@if (
|
||||
(ticket.approvalState === ApprovalState.Required &&
|
||||
formModel().approval.isNeeded) ||
|
||||
(ticket.approvalState === ApprovalState.Unknown &&
|
||||
formModel().approval.isNeeded)
|
||||
) {
|
||||
@if ( (ticket.approvalState === ApprovalState.Required &&
|
||||
formModel().approval.isNeeded) || (ticket.approvalState ===
|
||||
ApprovalState.Unknown && formModel().approval.isNeeded) ) {
|
||||
<div class="flex flex-col p-2">
|
||||
<label>Wer fragte die Genehmigung an?</label>
|
||||
<label>Wurde die Genehmigung bereits angefragt?</label>
|
||||
<mat-radio-group
|
||||
[(ngModel)]="formModel().approval.whoRequested"
|
||||
name="who"
|
||||
required
|
||||
>
|
||||
<mat-radio-button value="Avicenna"> Avicenna </mat-radio-button>
|
||||
<mat-radio-button value="Kunde"> Kunde </mat-radio-button>
|
||||
<mat-radio-button value="Avicenna"> Ja, durch Avicenna </mat-radio-button>
|
||||
<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-group>
|
||||
</div>
|
||||
<mat-form-field class="p-2">
|
||||
|
|
@ -88,7 +83,7 @@ import { AuthService } from '../../../core/components/ms-authentication/auth.ser
|
|||
}
|
||||
|
||||
<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
|
||||
[(ngModel)]="formModel().tdLocation"
|
||||
name="tdLocation"
|
||||
|
|
@ -112,7 +107,7 @@ import { AuthService } from '../../../core/components/ms-authentication/auth.ser
|
|||
MatFormFieldModule,
|
||||
MatRadioModule,
|
||||
MatInputModule,
|
||||
FormsModule
|
||||
FormsModule,
|
||||
],
|
||||
})
|
||||
export class TicketUserActionRequiredComponent implements OnInit {
|
||||
|
|
|
|||
|
|
@ -4,29 +4,35 @@
|
|||
</h3>
|
||||
<hr />
|
||||
<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">
|
||||
<ng-container>
|
||||
@switch (ref.data.currentState) {
|
||||
@case (TicketState.UserInteractionRequired) {
|
||||
@switch (ref.data.currentState) { @case
|
||||
(TicketState.UserInteractionRequired) {
|
||||
<dks-ticket-user-action-required
|
||||
[ticket]="ref.data"
|
||||
(saveFormData)="setDocumentInfo($event)"
|
||||
></dks-ticket-user-action-required>
|
||||
}
|
||||
@case (TicketState.TdUncertain) {
|
||||
} @case (TicketState.TdUncertain) {
|
||||
<dks-ticket-td-uncertain
|
||||
[ticket]="ref.data"
|
||||
(saveFormData)="setDocumentInfo($event)"
|
||||
></dks-ticket-td-uncertain>
|
||||
}
|
||||
@case (TicketState.DocumentsMissing) {
|
||||
} @case (TicketState.DocumentsMissing) {
|
||||
<dks-ticket-documents-missing
|
||||
[ticket]="ref.data"
|
||||
(saveFormData)="setDocumentInfo($event)"
|
||||
></dks-ticket-documents-missing>
|
||||
}
|
||||
}
|
||||
} }
|
||||
</ng-container>
|
||||
</mat-tab>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { TicketOverviewComponent } from './ticket-overview.component';
|
|||
import { TicketTdUncertainComponent } from './ticket-td-uncertain.component';
|
||||
import { TicketToursComponent } from './ticket-tours.component';
|
||||
import { TicketUserActionRequiredComponent } from './ticket-user-action-required.component';
|
||||
import { TicketApprovalRequestComponent } from './ticket-approval-request.component';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
|
@ -27,6 +28,7 @@ import { TicketUserActionRequiredComponent } from './ticket-user-action-required
|
|||
TicketOverviewComponent,
|
||||
TicketToursComponent,
|
||||
TicketUserActionRequiredComponent,
|
||||
TicketApprovalRequestComponent,
|
||||
TicketTdUncertainComponent,
|
||||
TicketDocumentsMissingComponent,
|
||||
],
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {
|
|||
GetTicketStateMetaQuery,
|
||||
SearchTicketsGQL,
|
||||
SearchTicketsQuery,
|
||||
SendApprovalRequestGQL,
|
||||
SetDocumentInfoGQL,
|
||||
SetDocumentInfoMutationVariables,
|
||||
TicketFilterArgs,
|
||||
|
|
@ -53,11 +54,33 @@ export class TicketsService {
|
|||
private readonly searchTicketsGql: SearchTicketsGQL,
|
||||
private readonly setDocumentInfoGql: SetDocumentInfoGQL,
|
||||
private readonly ticketStateKpiGql: TicketStateKpiGQL,
|
||||
private readonly sendApprovalRequestGql: SendApprovalRequestGQL,
|
||||
private readonly toast: HotToastService
|
||||
) {
|
||||
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(
|
||||
state: TicketValidationState,
|
||||
cursor: string,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,19 @@
|
|||
export const environment = {
|
||||
production: false,
|
||||
api: {
|
||||
baseUrl: 'http://localhost:3000/'
|
||||
},
|
||||
graphQl: {
|
||||
endpoint: 'http://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: {
|
||||
clientId: '9a4a1e7a-9544-439f-9cd3-3c20066a8a82',
|
||||
authority:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
export const environment = {
|
||||
production: true,
|
||||
api: {
|
||||
baseUrl: '{{API_BASEURL}}'
|
||||
},
|
||||
graphQl: {
|
||||
endpoint: '{{GRAPHQL_ENDPOINT}}',
|
||||
ws: '{{GRAPHQL_WS_ENDPOINT}}'
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
export const environment = {
|
||||
production: false,
|
||||
api: {
|
||||
baseUrl: ''
|
||||
},
|
||||
graphQl: {
|
||||
endpoint: '',
|
||||
ws: '',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
/* You can add global styles to this file, and also import other style files */
|
||||
@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,
|
||||
body {
|
||||
|
|
@ -12,7 +19,8 @@ body {
|
|||
color: var(--mat-sys-on-surface);
|
||||
}
|
||||
|
||||
.dispolive-btn, .dyflexis-btn {
|
||||
.dispolive-btn,
|
||||
.dyflexis-btn {
|
||||
a[target="_blank"]::after {
|
||||
content: " \2924"; /* Unicode-Pfeil */
|
||||
font-size: 0.8em;
|
||||
|
|
@ -38,3 +46,83 @@ body {
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,19 @@
|
|||
"ES2022",
|
||||
"dom",
|
||||
"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": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -3,6 +3,9 @@
|
|||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
"deleteOutDir": true,
|
||||
"assets": [
|
||||
"**/*.hbs"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -12,6 +12,7 @@
|
|||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"prisma": "prisma",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
|
|
@ -25,6 +26,7 @@
|
|||
"dependencies": {
|
||||
"@apollo/server": "^4.12.0",
|
||||
"@faker-js/faker": "^9.6.0",
|
||||
"@nestjs-modules/mailer": "^2.0.2",
|
||||
"@nestjs/apollo": "^13.0.4",
|
||||
"@nestjs/axios": "^4.0.0",
|
||||
"@nestjs/bullmq": "^11.0.2",
|
||||
|
|
@ -43,14 +45,18 @@
|
|||
"bullmq": "^5.56.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"date-fns-tz": "^3.2.0",
|
||||
"form-data": "^4.0.4",
|
||||
"graphql": "^16.10.0",
|
||||
"graphql-subscriptions": "^3.0.0",
|
||||
"graphql-ws": "^6.0.4",
|
||||
"handlebars": "^4.7.8",
|
||||
"kysely": "^0.27.6",
|
||||
"nodemailer": "^7.0.10",
|
||||
"pg": "^8.14.1",
|
||||
"prisma-kysely": "^1.8.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.2"
|
||||
"rxjs": "^7.8.2",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^11.0.6",
|
||||
|
|
@ -58,7 +64,9 @@
|
|||
"@nestjs/testing": "^11.0.15",
|
||||
"@types/express": "^5.0.1",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/multer": "^2.0.0",
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/nodemailer": "^7.0.3",
|
||||
"@types/supertest": "^6.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^8.29.1",
|
||||
"@typescript-eslint/parser": "^8.29.1",
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
@ -421,6 +421,23 @@ model Tour {
|
|||
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 {
|
||||
id String @id
|
||||
surname String?
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { BullModule } from '@nestjs/bullmq';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { Logger, MiddlewareConsumer, Module } from '@nestjs/common';
|
||||
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||
import { GraphQLModule } from '@nestjs/graphql';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
|
|
@ -11,8 +10,11 @@ import { Pool } from 'pg';
|
|||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { ConfigModule } from './core/config/config.module';
|
||||
import { ConfigService } from './core/config/config.service';
|
||||
import { DatabaseModule } from './core/database/database.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 { ApplicationControlCenterModule } from './modules/app-control-center/app-control-center.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 { FeatReportsModule } from './modules/feat-reports/feat-reports.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({
|
||||
imports: [
|
||||
ConfigModule,
|
||||
// DatabaseModule.forRootAsync({
|
||||
// isGlobal: true,
|
||||
// inject: [ConfigService],
|
||||
// useFactory(config: ConfigService) {
|
||||
// return {
|
||||
// config: {
|
||||
// dialect: new PostgresDialect({
|
||||
// pool: new Pool({
|
||||
// host: config.databaseHost,
|
||||
// port: config.databasePort,
|
||||
// database: config.databaseName,
|
||||
// user: config.databaseUser,
|
||||
// password: config.databasePassword,
|
||||
// }),
|
||||
// }),
|
||||
// },
|
||||
// };
|
||||
// },
|
||||
// }),
|
||||
DatabaseModule.forRoot({
|
||||
// isGlobal: true,
|
||||
DatabaseModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
useFactory: async (config: ConfigService) => ({
|
||||
dialect: new PostgresDialect({
|
||||
pool: new Pool({
|
||||
host: 'localhost',
|
||||
port: 5432,
|
||||
database: 'avicenna',
|
||||
user: 'johndoe',
|
||||
password: 'randompassword',
|
||||
host: config.databaseHost(),
|
||||
port: config.databasePort(),
|
||||
database: config.databaseName(),
|
||||
user: config.databaseUser(),
|
||||
password: config.databasePassword(),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
BullModule.forRoot({
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
BullModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
useFactory: async (config: ConfigService) => ({
|
||||
connection: {
|
||||
host: 'localhost',
|
||||
port: 6379,
|
||||
host: config.redisHost(),
|
||||
port: config.redisPort(),
|
||||
},
|
||||
}),
|
||||
// DatabaseModule.forRoot({
|
||||
// dialect: new PostgresDialect({
|
||||
// pool: new Pool({
|
||||
// host: 'localhost',
|
||||
// port: 54322,
|
||||
// database: 'postgres',
|
||||
// user: 'postgres',
|
||||
// password: 'postgres',
|
||||
// }),
|
||||
// }),
|
||||
// }),
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
PrismaModule.forRoot(),
|
||||
ScheduleModule.forRoot(),
|
||||
EventEmitterModule.forRoot(),
|
||||
|
|
@ -88,6 +67,7 @@ import { FeatBusinessObjectsModule } from './modules/feat-business-objects/feat-
|
|||
'graphql-ws': true,
|
||||
},
|
||||
}),
|
||||
MailModule,
|
||||
FeatureEtlModule,
|
||||
FeatReportsModule,
|
||||
FeatCalculationModule,
|
||||
|
|
@ -100,6 +80,10 @@ import { FeatBusinessObjectsModule } from './modules/feat-business-objects/feat-
|
|||
AppHomeModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
providers: [AppService, Logger, OtelMetricsService],
|
||||
})
|
||||
export class AppModule {}
|
||||
export class AppModule {
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
consumer.apply(RequestLoggerMiddleware).forRoutes('*');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,8 +39,8 @@ export class ConfigService extends NestConfigService {
|
|||
dyflexisAppPassword(): string {
|
||||
return this.get('DYFLEXIS_APP_PASSWORD');
|
||||
}
|
||||
databasePort(): string {
|
||||
return this.get('DATABASE_PORT');
|
||||
databasePort(): number {
|
||||
return +this.get('DATABASE_PORT');
|
||||
}
|
||||
databaseHost(): string {
|
||||
return this.get('DATABASE_HOST');
|
||||
|
|
@ -54,4 +54,28 @@ export class ConfigService extends NestConfigService {
|
|||
databaseName(): string {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {
|
|||
Type,
|
||||
} from '@nestjs/common';
|
||||
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_CONFIG_TOKEN = 'KYSELY_CONFIG_TOKEN';
|
||||
|
|
@ -21,11 +21,10 @@ export interface KyselyModuleAsyncOptions
|
|||
isGlobal?: boolean;
|
||||
useExisting?: Type<KyselyConfigFactory>;
|
||||
useClass?: Type<KyselyConfigFactory>;
|
||||
// FIX 1: Erlaube Promise im Rückgabetyp
|
||||
useFactory?: (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
...args: any[]
|
||||
) => KyselyConfig;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
) => Promise<KyselyConfig> | KyselyConfig;
|
||||
inject?: any[];
|
||||
}
|
||||
|
||||
|
|
@ -43,11 +42,22 @@ export class DatabaseModule {
|
|||
}
|
||||
|
||||
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 {
|
||||
global: options.isGlobal,
|
||||
module: DatabaseModule,
|
||||
imports: options.imports || [],
|
||||
providers: this.createAsyncProviders(options),
|
||||
providers: [
|
||||
...this.createAsyncProviders(options),
|
||||
clientProvider,
|
||||
],
|
||||
exports: [clientProvider],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -55,12 +65,12 @@ export class DatabaseModule {
|
|||
options: KyselyModuleAsyncOptions,
|
||||
): Provider[] {
|
||||
if (options.useExisting || options.useFactory) {
|
||||
return this.createAsyncOptionsProvider(options);
|
||||
return [this.createAsyncOptionsProvider(options)];
|
||||
}
|
||||
|
||||
if (options.useClass) {
|
||||
return [
|
||||
...this.createAsyncOptionsProvider(options),
|
||||
this.createAsyncOptionsProvider(options),
|
||||
{
|
||||
provide: 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(
|
||||
options: KyselyModuleAsyncOptions,
|
||||
): Provider[] {
|
||||
): Provider {
|
||||
if (options.useFactory) {
|
||||
const config = options.useFactory(options.inject);
|
||||
return [
|
||||
// {
|
||||
// provide: KYSELY_CONFIG_TOKEN,
|
||||
// useFactory: options.useFactory,
|
||||
// inject: options.inject || [],
|
||||
// },
|
||||
{
|
||||
provide: KYSELY_CLIENT_PROVIDER_TOKEN,
|
||||
useExisting: createKyselyProvider(config)
|
||||
}
|
||||
];
|
||||
return {
|
||||
provide: KYSELY_CONFIG_TOKEN,
|
||||
useFactory: options.useFactory,
|
||||
inject: options.inject || [],
|
||||
};
|
||||
}
|
||||
|
||||
if (options.useExisting) {
|
||||
return [
|
||||
{
|
||||
const inject = [options.useClass || 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,
|
||||
useFactory: async (optionsFactory: KyselyConfigFactory) =>
|
||||
await optionsFactory.createKyselyConfig(),
|
||||
inject: [],
|
||||
},
|
||||
];
|
||||
inject: inject,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ input AccountingFilterArgs {
|
|||
enum AccountingState {
|
||||
Billable
|
||||
Billed
|
||||
Pending
|
||||
PreparedForBilling
|
||||
}
|
||||
|
||||
|
|
@ -138,6 +139,7 @@ type Metric {
|
|||
|
||||
type Mutation {
|
||||
acceptAnomaly(id: String!): Anomaly!
|
||||
sendApprovalRequest(email: String!, ticketId: String!): Boolean!
|
||||
setAnomalyToSleep(id: String!): Anomaly!
|
||||
setDocumentInfo(approval: ApprovalInfoArgs, tdLocation: String, ticketId: String!): Ticket!
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export const MAILING_QUEUE = 'mailing_queue';
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
export enum AccountingState {
|
||||
Pending = 'Pending',
|
||||
Billable = 'Billable',
|
||||
PreparedForBilling = 'PreparedForBilling',
|
||||
Billed = 'Billed',
|
||||
|
|
|
|||
|
|
@ -5,10 +5,34 @@ import { FeatTicketSystemModule } from '../feat-tickets/ticket-system.module';
|
|||
import { TicketRepository } from './ticket.repository';
|
||||
import { TicketsResolver } from './tickets.resolver';
|
||||
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({
|
||||
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],
|
||||
controllers: [TicketsController],
|
||||
exports: [TicketSystemService],
|
||||
})
|
||||
export class ApplicationTicketSystemModule {}
|
||||
|
|
|
|||
|
|
@ -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 { Ticket, TicketValidationState, Tour } from '@prisma/client';
|
||||
import { Queue } from 'bullmq';
|
||||
import { PubSub } from 'graphql-subscriptions';
|
||||
import {
|
||||
decodeOffsetCursor,
|
||||
encodeCursor,
|
||||
} 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 { TourService } from '../feat-business-objects/entities/tour/tour.service';
|
||||
import { TicketLifecycleService } from '../feat-tickets/ticket-lifecycle.service';
|
||||
import {
|
||||
TicketSystemEventTypes,
|
||||
TicketSystemValidatedEvent,
|
||||
} from '../feat-tickets/ticket-system.events';
|
||||
} from '../feat-business-objects/entities/ticket/ticket-system.events';
|
||||
import { DocumentInfoArgs, TicketFilterArgs } from './ticket-arg.types';
|
||||
import {
|
||||
PaginatedTicketObjectType,
|
||||
|
|
@ -20,6 +23,7 @@ import {
|
|||
TicketStateMetaObjectType,
|
||||
} from './ticket.object-type';
|
||||
import { TicketRepository } from './ticket.repository';
|
||||
import { TourFilesService } from '../feat-business-objects/entities/tour-files/tour-files.service';
|
||||
|
||||
@Injectable()
|
||||
export class TicketSystemService {
|
||||
|
|
@ -36,13 +40,33 @@ export class TicketSystemService {
|
|||
private readonly repository: TicketRepository,
|
||||
private readonly ticketService: TicketService,
|
||||
private readonly tourService: TourService,
|
||||
private readonly tourFileService: TourFilesService,
|
||||
private readonly lifecycle: TicketLifecycleService,
|
||||
@InjectQueue(MAILING_QUEUE) private readonly approvalQueue: Queue,
|
||||
) {}
|
||||
|
||||
async updateDocumentInfo(ticketId: string, documentInfo: DocumentInfoArgs) {
|
||||
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> {
|
||||
return this.ticketService.findById(id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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' };
|
||||
}
|
||||
}
|
||||
|
|
@ -61,6 +61,14 @@ export class TicketsResolver {
|
|||
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)
|
||||
setDocumentInfo(
|
||||
@Args('ticketId', { type: () => String }) ticketId: string,
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ export class EmployeeStillWorkingDetector extends AnomalyDetector {
|
|||
const longWorkingEmployees =
|
||||
await this.dataService.findEmployeeStillWorking(day);
|
||||
|
||||
console.log(longWorkingEmployees)
|
||||
if (longWorkingEmployees.length > 0) {
|
||||
anomalies.push(
|
||||
this.createAnomaly(
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ export class IsWorkTimePlannedDetector extends AnomalyDetector {
|
|||
|
||||
for (const day of eachDayOfInterval(inInterval)){
|
||||
const workTimeWithoutPlan = await this.dataService.findWorkForPlannedTime(day)
|
||||
console.log(day, workTimeWithoutPlan)
|
||||
}
|
||||
|
||||
return
|
||||
|
|
|
|||
|
|
@ -44,11 +44,7 @@ export class TicketRepository {
|
|||
async findAllCreated(): Promise<(Ticket & { tours: Tour[] })[]> {
|
||||
return await this.prisma.ticket.findMany({
|
||||
where: {
|
||||
state: {
|
||||
none: {
|
||||
state: TicketValidationState.USER_INTERACTION_REQUIRED,
|
||||
},
|
||||
},
|
||||
currentState: TicketValidationState.CREATED,
|
||||
},
|
||||
include: {
|
||||
tours: true,
|
||||
|
|
@ -185,7 +181,9 @@ export class TicketRepository {
|
|||
}
|
||||
|
||||
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 {
|
||||
return await this.db.transaction().execute(async (trx) => {
|
||||
const ticket = await trx
|
||||
|
|
@ -224,6 +222,9 @@ export class TicketRepository {
|
|||
}
|
||||
|
||||
async addToursToTicket(ticketId: string, tours: Tour[]): Promise<Ticket> {
|
||||
this.logger.log(
|
||||
`Adding tours to ticket: ${tours.map(({ operationId }) => operationId).join(', ')}`,
|
||||
);
|
||||
await this.db
|
||||
.updateTable('Tour')
|
||||
.set({
|
||||
|
|
@ -288,6 +289,9 @@ export class TicketRepository {
|
|||
approvalPath?: string;
|
||||
},
|
||||
): Promise<Ticket & { tours: Tour[] }> {
|
||||
this.logger.log(
|
||||
`updating ticket (${ticketId}) with updateset ${JSON.stringify(data)}`,
|
||||
);
|
||||
const { state, ...ticketData } = data;
|
||||
|
||||
await this.db
|
||||
|
|
|
|||
|
|
@ -1,20 +1,22 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import {
|
||||
ApprovalState,
|
||||
Ticket,
|
||||
TicketStateHistory,
|
||||
TicketValidationState,
|
||||
Tour,
|
||||
Tour
|
||||
} from '@prisma/client';
|
||||
import {
|
||||
TicketFilterDto,
|
||||
UpdateDocumentInfoDto,
|
||||
} from 'src/modules/feat-business-objects/entities/ticket/tickets.dto';
|
||||
import { TicketRepository } from './ticket.repository';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { TicketSystemEventTypes, TicketSystemValidatedEvent } from './ticket-system.events';
|
||||
|
||||
@Injectable()
|
||||
export class TicketService {
|
||||
constructor(private readonly repository: TicketRepository) {}
|
||||
constructor(private readonly repository: TicketRepository, private readonly eventEmitter: EventEmitter2) {}
|
||||
|
||||
async findById(ticketId: string): Promise<Ticket> {
|
||||
return this.repository.findById(ticketId);
|
||||
|
|
@ -78,6 +80,24 @@ export class TicketService {
|
|||
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(
|
||||
ticketId: string,
|
||||
data: {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
export interface TourFileDto {
|
||||
tourId: string;
|
||||
ticketId: string;
|
||||
storagePath: string;
|
||||
file: Express.Multer.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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,13 @@
|
|||
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 { ExpressionBuilder, Kysely } from 'kysely';
|
||||
import { ExpressionBuilder, ExpressionWrapper, Kysely, SqlBool } from 'kysely';
|
||||
import { KYSELY_CLIENT_PROVIDER_TOKEN } from 'src/core/database/database.module';
|
||||
import { PrismaService } from 'src/core/database/prisma.service';
|
||||
import { DB } from 'src/core/database/types';
|
||||
import { AccountingState } from 'src/modules/app-accounting/accounting-state';
|
||||
import { chunk } from 'src/utils';
|
||||
import {
|
||||
createPrismaQueryFromAccountingFilters,
|
||||
TourAccountingFilterDto,
|
||||
} from './tours.filter';
|
||||
import { TourAccountingFilterDto } from './tours.filter';
|
||||
|
||||
type TourEb = ExpressionBuilder<DB, 'Tour'>;
|
||||
|
||||
|
|
@ -120,35 +117,27 @@ export class TourRepository {
|
|||
interval: Interval,
|
||||
ordinanceTypes: string[] = [],
|
||||
): Promise<Tour[]> {
|
||||
return await this.prisma.tour.findMany({
|
||||
where: {
|
||||
AND: [
|
||||
{
|
||||
startDate: {
|
||||
gte: new Date(interval.start),
|
||||
},
|
||||
},
|
||||
{
|
||||
startDate: {
|
||||
lt: new Date(interval.end),
|
||||
},
|
||||
},
|
||||
{
|
||||
OR: [
|
||||
...(ordinanceTypes.length
|
||||
? ordinanceTypes.map((ot) => ({
|
||||
ordinanceType: { contains: ot },
|
||||
}))
|
||||
return await this.db
|
||||
.selectFrom('Tour')
|
||||
.selectAll()
|
||||
.where((eb) =>
|
||||
eb.and([
|
||||
eb('Tour.startDate', '>=', new Date(interval.start)),
|
||||
eb('Tour.startDate', '<', new Date(interval.end)),
|
||||
...(ordinanceTypes.length > 0
|
||||
? [
|
||||
eb.or(
|
||||
ordinanceTypes.map((ot) =>
|
||||
eb('Tour.ordinanceType', 'like', `%${ot}%`),
|
||||
),
|
||||
),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
...onlyRealTours(),
|
||||
],
|
||||
},
|
||||
orderBy: {
|
||||
startDate: 'desc',
|
||||
},
|
||||
});
|
||||
onlyRealToursKysely(eb),
|
||||
]),
|
||||
)
|
||||
.orderBy('Tour.startDate asc')
|
||||
.execute();
|
||||
}
|
||||
|
||||
async findUpdatedAtBetween(
|
||||
|
|
@ -289,60 +278,45 @@ export class TourRepository {
|
|||
async findFirstRecurring(tour: Tour): Promise<Tour | null> {
|
||||
const institutionStreet =
|
||||
tour.direction === 'Hinfahrt' ? tour.targetStreet : tour.startStreet;
|
||||
return await this.prisma.tour.findFirst({
|
||||
where: {
|
||||
type: 'Serie',
|
||||
category: tour.category,
|
||||
ordinanceType: tour.ordinanceType,
|
||||
patientId: tour.patientId,
|
||||
patientName: tour.patientName,
|
||||
patientSurname: tour.patientSurname,
|
||||
patientStreet: tour.patientStreet,
|
||||
patientCity: tour.patientCity,
|
||||
patientZip: tour.patientZip,
|
||||
OR: [
|
||||
{ targetStreet: institutionStreet },
|
||||
{ startStreet: institutionStreet },
|
||||
],
|
||||
rangeEndDate: {
|
||||
gte: tour.startDate,
|
||||
},
|
||||
ticketId: {
|
||||
not: null,
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
startDate: 'desc',
|
||||
},
|
||||
});
|
||||
return await this.db
|
||||
.selectFrom('Tour')
|
||||
.selectAll()
|
||||
.where('type', '=', 'Serie')
|
||||
.where('category', '=', tour.category)
|
||||
.where('ordinanceType', '=', tour.ordinanceType)
|
||||
.where('patientId', '=', tour.patientId)
|
||||
.where('patientName', '=', tour.patientName)
|
||||
.where('patientSurname', '=', tour.patientSurname)
|
||||
.where('patientStreet', '=', tour.patientStreet)
|
||||
.where('patientCity', '=', tour.patientCity)
|
||||
.where('patientZip', '=', tour.patientZip)
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb('targetStreet', '=', institutionStreet),
|
||||
eb('startStreet', '=', institutionStreet),
|
||||
]),
|
||||
)
|
||||
.where('rangeEndDate', '>=', tour.startDate)
|
||||
.where('ticketId', 'is not', null)
|
||||
.orderBy('startDate', 'desc')
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async findOnSameDayWithOppositeDirection(tour: Tour): Promise<Tour | null> {
|
||||
return await this.prisma.tour.findFirst({
|
||||
where: {
|
||||
AND: [
|
||||
{
|
||||
startDate: {
|
||||
gt: startOfDay(new Date(tour.startDate)),
|
||||
},
|
||||
},
|
||||
{
|
||||
startDate: {
|
||||
lt: endOfDay(new Date(tour.startDate)),
|
||||
},
|
||||
},
|
||||
{
|
||||
startStreet: tour.targetStreet,
|
||||
},
|
||||
{
|
||||
targetStreet: tour.startStreet,
|
||||
},
|
||||
{
|
||||
direction: tour.direction === 'Hinfahrt' ? 'Rückfahrt' : 'Hinfahrt',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const dayStart = startOfDay(new Date(tour.startDate));
|
||||
const dayEnd = endOfDay(new Date(tour.startDate));
|
||||
const oppositeDirection =
|
||||
tour.direction === 'Hinfahrt' ? 'Rückfahrt' : 'Hinfahrt';
|
||||
|
||||
return await this.db
|
||||
.selectFrom('Tour')
|
||||
.selectAll()
|
||||
.where('startDate', '>', dayStart)
|
||||
.where('startDate', '<', dayEnd)
|
||||
.where('startStreet', '=', tour.targetStreet)
|
||||
.where('targetStreet', '=', tour.startStreet)
|
||||
.where('direction', '=', oppositeDirection)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async findWithTicket(interval: Interval): Promise<Tour[]> {
|
||||
|
|
@ -389,40 +363,36 @@ export class TourRepository {
|
|||
take?: number,
|
||||
skip?: number,
|
||||
): Promise<{ tours: Tour[]; total: number }> {
|
||||
const checkValue = this.getCheckValueForAccountingState(state);
|
||||
const query: Prisma.TourFindManyArgs = {
|
||||
...{
|
||||
...(skip ? { skip, take: 10 } : {}),
|
||||
...(take ? { take } : {}),
|
||||
...(take && !skip ? { skip: 0 } : {}),
|
||||
},
|
||||
where: {
|
||||
check: checkValue,
|
||||
...(checkValue === 2 && !filter?.deviation
|
||||
? { revenueDeviation: { not: 0 } }
|
||||
: {}),
|
||||
empty: {
|
||||
not: null,
|
||||
},
|
||||
ticketId: {
|
||||
not: null,
|
||||
},
|
||||
AND: [
|
||||
...createPrismaQueryFromAccountingFilters(filter),
|
||||
...onlyRealTours(),
|
||||
],
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
};
|
||||
return await this.db.transaction().execute(async (trx) => {
|
||||
const tours = await trx
|
||||
.selectFrom('Tour')
|
||||
.selectAll()
|
||||
.where((eb) =>
|
||||
eb.and([
|
||||
this.getFilterForAccountingState(eb, state),
|
||||
onlyRealToursKysely(eb),
|
||||
]),
|
||||
)
|
||||
.offset(skip)
|
||||
.limit(take)
|
||||
.execute();
|
||||
|
||||
const total = await trx
|
||||
.selectFrom('Tour')
|
||||
.where((eb) =>
|
||||
eb.and([
|
||||
this.getFilterForAccountingState(eb, state),
|
||||
onlyRealToursKysely(eb),
|
||||
]),
|
||||
)
|
||||
.select((eb) => eb.fn.countAll().as('total'))
|
||||
.executeTakeFirst();
|
||||
|
||||
const [tours, count] = await this.prisma.$transaction([
|
||||
this.prisma.tour.findMany({ ...query }),
|
||||
this.prisma.tour.count({ where: query.where }),
|
||||
]);
|
||||
return {
|
||||
tours,
|
||||
total: count,
|
||||
total: Number(total.total),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async calculateAccountingStateMeta(state: AccountingState): Promise<{
|
||||
|
|
@ -431,78 +401,90 @@ export class TourRepository {
|
|||
positiveRevenueDeviation?: number;
|
||||
negativeRevenueDeviation?: number;
|
||||
}> {
|
||||
const checkValue = this.getCheckValueForAccountingState(state);
|
||||
const ticketWithCurrentStateAndTours: Prisma.TourWhereInput = {
|
||||
check: checkValue,
|
||||
...(checkValue === 2 ? { revenueDeviation: { not: 0 } } : {}),
|
||||
empty: {
|
||||
not: null,
|
||||
},
|
||||
ticketId: {
|
||||
not: null,
|
||||
},
|
||||
AND: [...onlyRealTours()],
|
||||
};
|
||||
|
||||
const [
|
||||
count,
|
||||
revenueSum,
|
||||
positiveRevenueDeviation,
|
||||
negativeRevenueDeviation,
|
||||
] = await this.prisma.$transaction([
|
||||
this.prisma.tour.count({
|
||||
where: { ...ticketWithCurrentStateAndTours },
|
||||
}),
|
||||
this.prisma.tour.aggregate({
|
||||
_sum: {
|
||||
revenue: true,
|
||||
},
|
||||
where: {
|
||||
...ticketWithCurrentStateAndTours,
|
||||
},
|
||||
}),
|
||||
this.prisma.tour.aggregate({
|
||||
_sum: {
|
||||
revenueDeviation: true,
|
||||
},
|
||||
where: {
|
||||
...ticketWithCurrentStateAndTours,
|
||||
revenueDeviation: {
|
||||
gt: 0,
|
||||
},
|
||||
},
|
||||
}),
|
||||
this.prisma.tour.aggregate({
|
||||
_sum: {
|
||||
revenueDeviation: true,
|
||||
},
|
||||
where: {
|
||||
...ticketWithCurrentStateAndTours,
|
||||
revenueDeviation: {
|
||||
lt: 0,
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
const result = await this.db
|
||||
.selectFrom('Tour')
|
||||
.where((eb) => {
|
||||
const conditions = [
|
||||
this.getFilterForAccountingState(eb, state),
|
||||
onlyRealToursKysely(eb),
|
||||
eb('empty', 'is not', null),
|
||||
eb('ticketId', 'is not', null),
|
||||
];
|
||||
return eb.and(conditions);
|
||||
})
|
||||
.select((eb) => [
|
||||
eb.fn.countAll().as('count'),
|
||||
eb.fn.sum<number>('revenue').as('revenueSum'),
|
||||
eb.fn
|
||||
.sum<number>(
|
||||
eb
|
||||
.case()
|
||||
.when('revenueDeviation', '>', 0)
|
||||
.then(eb.ref('revenueDeviation'))
|
||||
.else(0)
|
||||
.end(),
|
||||
)
|
||||
.as('positiveRevenueDeviation'),
|
||||
eb.fn
|
||||
.sum<number>(
|
||||
eb
|
||||
.case()
|
||||
.when('revenueDeviation', '<', 0)
|
||||
.then(eb.ref('revenueDeviation'))
|
||||
.else(0)
|
||||
.end(),
|
||||
)
|
||||
.as('negativeRevenueDeviation'),
|
||||
])
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
return {
|
||||
count,
|
||||
revenueSum: revenueSum._sum.revenue ?? 0,
|
||||
positiveRevenueDeviation:
|
||||
positiveRevenueDeviation._sum.revenueDeviation ?? 0,
|
||||
negativeRevenueDeviation:
|
||||
negativeRevenueDeviation._sum.revenueDeviation ?? 0,
|
||||
count: Number(result.count),
|
||||
revenueSum: Number(result.revenueSum ?? 0),
|
||||
positiveRevenueDeviation: Number(result.positiveRevenueDeviation ?? 0),
|
||||
negativeRevenueDeviation: Number(result.negativeRevenueDeviation ?? 0),
|
||||
};
|
||||
}
|
||||
|
||||
private getCheckValueForAccountingState(state: AccountingState): number {
|
||||
private getFilterForAccountingState(
|
||||
eb: ExpressionBuilder<DB, 'Tour'>,
|
||||
state: AccountingState,
|
||||
): ExpressionWrapper<DB, 'Tour', SqlBool> {
|
||||
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:
|
||||
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:
|
||||
return 1;
|
||||
case AccountingState.Billed:
|
||||
return 2;
|
||||
return eb.and([eb('Tour.check', '=', 1)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
export const TOURS_QUEUE = 'tours';
|
||||
export const TOUR_FILE_UPLOAD_QUEUE = 'tour_file_upload'
|
||||
|
|
@ -19,6 +19,8 @@ import { TourRepository } from './entities/tour/tour.repository';
|
|||
import { TourService } from './entities/tour/tour.service';
|
||||
import { WorkTimeRepository } from './entities/work-time/work-time.repository';
|
||||
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({
|
||||
imports: [PrismaModule],
|
||||
|
|
@ -32,6 +34,7 @@ import { WorkTimeService } from './entities/work-time/work-time.service';
|
|||
TariffService,
|
||||
TicketService,
|
||||
TourService,
|
||||
TourFilesService,
|
||||
WorkTimeService,
|
||||
],
|
||||
providers: [
|
||||
|
|
@ -52,6 +55,8 @@ import { WorkTimeService } from './entities/work-time/work-time.service';
|
|||
TicketService,
|
||||
TourRepository,
|
||||
TourService,
|
||||
TourFilesRepository,
|
||||
TourFilesService,
|
||||
WorkTimeRepository,
|
||||
WorkTimeService,
|
||||
],
|
||||
|
|
|
|||
|
|
@ -19,12 +19,16 @@ import {
|
|||
of,
|
||||
reduce,
|
||||
switchMap,
|
||||
tap,
|
||||
} from 'rxjs';
|
||||
import { EmployeeDispoLiveRaw, PatientRaw, TourRaw } from './source.interface';
|
||||
import { SourceRepository } from './source.repository';
|
||||
import { chunk } from 'src/utils';
|
||||
import { ConfigService } from 'src/core/config/config.service';
|
||||
import { TourExtractor } from './extractors/tour.extractor';
|
||||
import * as fs from 'fs';
|
||||
import * as FormData from 'form-data';
|
||||
import * as path from 'path';
|
||||
|
||||
@Injectable()
|
||||
export class DispoLiveExtractorsService {
|
||||
|
|
@ -36,6 +40,39 @@ export class DispoLiveExtractorsService {
|
|||
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) {
|
||||
return this.fetchTour(id).pipe(map((tour) => ({ ...tour, jobId })));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ import { ToursConsumer } from './processors/tours.consumer';
|
|||
import { SchichtplanungSynchronizer } from './producer/schichtplanung';
|
||||
import { TourUpdateChecker } from './producer/tour-update';
|
||||
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({
|
||||
imports: [
|
||||
|
|
@ -18,8 +22,10 @@ import { SCHICHTPLANUNG_QUEUE } from './tokens';
|
|||
DataAccessExtractorsModule,
|
||||
DataAccessTransformersModule,
|
||||
DataAccessLoadersModule,
|
||||
MailModule,
|
||||
BullModule.registerQueue({ name: TOURS_QUEUE }),
|
||||
BullModule.registerQueue({ name: SCHICHTPLANUNG_QUEUE }),
|
||||
BullModule.registerQueue({ name: MAILING_QUEUE }),
|
||||
],
|
||||
providers: [
|
||||
ImporterService,
|
||||
|
|
@ -27,6 +33,8 @@ import { SCHICHTPLANUNG_QUEUE } from './tokens';
|
|||
TourUpdateChecker,
|
||||
SchichtplanungSynchronizer,
|
||||
SchichtplanungConsumer,
|
||||
TourFileUploadConsumer,
|
||||
MailConsumer
|
||||
],
|
||||
exports: [ImporterService],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ import {
|
|||
set,
|
||||
subDays,
|
||||
subHours,
|
||||
subWeeks
|
||||
subWeeks,
|
||||
} from 'date-fns';
|
||||
import { PrismaService } from 'src/core/database/prisma.service';
|
||||
import { generateIntervalForTimeRange } from 'src/utils';
|
||||
|
|
@ -253,13 +253,13 @@ export class KpiService {
|
|||
ticketState: TicketValidationState,
|
||||
): Promise<KpiInfoObjectType> {
|
||||
return {
|
||||
main: await this.fastMovingTicketPercentMetric(ticketState),
|
||||
secondary: await this.averageTimePerStateMetric(ticketState),
|
||||
main: { label: '', value: 0 }, // await this.fastMovingTicketPercentMetric(ticketState),
|
||||
secondary: { label: '', value: 0 }, // await this.averageTimePerStateMetric(ticketState),
|
||||
additional: [
|
||||
await this.ticketCountPerStateMetric(ticketState),
|
||||
...(ticketState === TicketValidationState.USER_INTERACTION_REQUIRED
|
||||
? [await this.ticketApprovalRequiredCountMetric(ticketState)]
|
||||
: []),
|
||||
// ...(ticketState === TicketValidationState.USER_INTERACTION_REQUIRED
|
||||
// ? [await this.ticketApprovalRequiredCountMetric(ticketState)]
|
||||
// : []),
|
||||
await this.tourCountPerTicketStateMetric(ticketState),
|
||||
],
|
||||
};
|
||||
|
|
@ -317,9 +317,9 @@ export class KpiService {
|
|||
state: true,
|
||||
},
|
||||
orderBy: {
|
||||
id: 'asc'
|
||||
id: 'asc',
|
||||
},
|
||||
take: 1000
|
||||
take: 1000,
|
||||
});
|
||||
const timePerTickets = movedTickets
|
||||
.map(({ state }) => {
|
||||
|
|
|
|||
|
|
@ -59,13 +59,19 @@ export class TicketLifecycleService {
|
|||
|
||||
switch (ticket.currentState) {
|
||||
case TicketValidationState.USER_INTERACTION_REQUIRED:
|
||||
const approvalStateUpdate = documentInfo.approval.isNeeded
|
||||
let approvalStateUpdate: ApprovalState;
|
||||
if (documentInfo.approval.whoRequested == 'no') {
|
||||
approvalStateUpdate = ApprovalState.REQUIRED;
|
||||
} else {
|
||||
approvalStateUpdate = documentInfo.approval.isNeeded
|
||||
? ApprovalState.REQUESTED
|
||||
: ApprovalState.FREE;
|
||||
}
|
||||
|
||||
let stateUpdate: TicketValidationState =
|
||||
TicketValidationState.USER_INTERACTION_REQUIRED;
|
||||
if (
|
||||
if (approvalStateUpdate === ApprovalState.REQUIRED) {
|
||||
} else if (
|
||||
documentInfo.tdLocation === 'company' &&
|
||||
approvalStateUpdate === ApprovalState.FREE
|
||||
)
|
||||
|
|
@ -137,29 +143,12 @@ export class TicketLifecycleService {
|
|||
: ApprovalState.REQUIRED,
|
||||
});
|
||||
} else {
|
||||
// NOTE: Condition required to get these tickets out of USER_INTERACTION_REQUIRED
|
||||
if (
|
||||
ticket.tours.every(
|
||||
({ ordinanceType }) =>
|
||||
ordinanceType.indexOf('BTW') > -1 ||
|
||||
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 {
|
||||
await this.ticketService.setActionablesOnTicket(
|
||||
|
|
@ -260,13 +249,17 @@ export class TicketLifecycleService {
|
|||
|
||||
if (
|
||||
[
|
||||
'Einweisung',
|
||||
'Entlassung',
|
||||
'Entlassung Vorbestellung',
|
||||
'Konsil KH - Dia',
|
||||
'1. Entlassung',
|
||||
'2. Entlassung Vorbestellung',
|
||||
'3. Einweisung',
|
||||
'5. Verlegung',
|
||||
'6. Barzahlung',
|
||||
'Privatfahrt Rechnung',
|
||||
'Tagesklinik',
|
||||
'Verlegung',
|
||||
'Reha',
|
||||
'Tagesklinik - AKW',
|
||||
'Tagesklinik - ASK',
|
||||
'Tagesklinik - MKH',
|
||||
'Transportschein abholen',
|
||||
].includes(tour.category) ||
|
||||
tour.healthInsurance === 'KOSTENTRÄGER PATIENT'
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { TicketLifecycleService } from './ticket-lifecycle.service';
|
|||
import {
|
||||
TicketSystemEventTypes,
|
||||
TicketSystemValidatedEvent,
|
||||
} from './ticket-system.events';
|
||||
} from '../feat-business-objects/entities/ticket/ticket-system.events';
|
||||
|
||||
@Injectable()
|
||||
export class TicketService {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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 }}"
|
||||
|
|
@ -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 }}"
|
||||
|
|
@ -58,8 +58,7 @@ async function main() {
|
|||
const schichtplanung = await getSchichtplanungForDay(new Date('2025-08-11')).then(response => response.json());
|
||||
const chunks = chunk(schichtplanung.map(({id}) => id), 1)
|
||||
for(const chunk of chunks) {
|
||||
const res = await deleteSchichtplanungItems(chunk as string[])
|
||||
console.log(res)
|
||||
await deleteSchichtplanungItems(chunk as string[])
|
||||
await sleep(200)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue