modernize ticket and accounting
iteration 0
This commit is contained in:
@@ -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 "$@"
|
||||
+7276
-1343
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));
|
||||
}
|
||||
+12
@@ -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>
|
||||
+23
@@ -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();
|
||||
});
|
||||
});
|
||||
+63
@@ -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")
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
+27
@@ -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));
|
||||
}
|
||||
}
|
||||
Vendored
-68
@@ -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();
|
||||
}
|
||||
}
|
||||
Vendored
-50
@@ -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);
|
||||
};
|
||||
}
|
||||
+1
-1
@@ -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">
|
||||
|
||||
+35
-28
@@ -22,34 +22,41 @@ import {
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 'dks-accounting-item',
|
||||
template: ` <div class="flex justify-between">
|
||||
<div class="flex items-center">
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="notifyCopy($event)"
|
||||
[cdkCopyToClipboard]="tour.operationId"
|
||||
>
|
||||
<mat-icon class="text-sm">content_copy</mat-icon>
|
||||
</button>
|
||||
template: `
|
||||
<div class="flex justify-between">
|
||||
<div class="flex items-center">
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="notifyCopy($event)"
|
||||
[cdkCopyToClipboard]="tour.operationId"
|
||||
>
|
||||
<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 }}
|
||||
</div>
|
||||
<span>{{ tour.ordinanceType }}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
@if (accountingState !== AccountingState.Billed) {
|
||||
<span
|
||||
[title]="!tour.healthInsurance ? 'Keine Krankenkasse hinterlegt' : ''"
|
||||
[ngClass]="{ 'bg-yellow-200': !tour.healthInsurance }"
|
||||
>
|
||||
{{ tour.revenue | currency : 'EUR' : 'symbol' : '1.2-2' : 'de-DE' }}
|
||||
</span>
|
||||
} @else {
|
||||
{{
|
||||
tour.revenueDeviation | currency : 'EUR' : 'symbol' : '1.2-2' : 'de-DE'
|
||||
}}
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
<span>{{ tour.ordinanceType }}</span>
|
||||
</div>
|
||||
<div>
|
||||
@if (accountingState !== AccountingState.Billed) {
|
||||
<span
|
||||
[title]="!tour.healthInsurance ? 'Keine Krankenkasse hinterlegt' : ''"
|
||||
[ngClass]="{ 'bg-yellow-200': !tour.healthInsurance }"
|
||||
>
|
||||
{{ tour.revenue | currency : 'EUR' : 'symbol' : '1.2-2' : 'de-DE' }}
|
||||
</span>
|
||||
} @else {
|
||||
{{
|
||||
tour.revenueDeviation | currency : 'EUR' : 'symbol' : '1.2-2' : 'de-DE'
|
||||
}}
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
host: {
|
||||
class: 'flex flex-col rounded border p-2 m-1 bg-white cursor-pointer',
|
||||
},
|
||||
@@ -71,8 +78,8 @@ import {
|
||||
MatBadgeModule,
|
||||
ClipboardModule,
|
||||
NgClass,
|
||||
CurrencyPipe
|
||||
],
|
||||
CurrencyPipe,
|
||||
],
|
||||
})
|
||||
export class AccountingItemComponent implements AfterViewInit {
|
||||
toast = inject(HotToastService);
|
||||
|
||||
+16
-19
@@ -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
|
||||
[state]="state"
|
||||
(filterChange)="filterChanged($event)"
|
||||
></dks-accounting-lane-header-filter>`,
|
||||
<dks-accounting-lane-header-filter
|
||||
*dksRole="['admin']"
|
||||
[state]="state"
|
||||
(filterChange)="filterChanged($event)"
|
||||
></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;
|
||||
|
||||
-1
@@ -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);
|
||||
|
||||
+7
-7
@@ -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!');
|
||||
}
|
||||
})
|
||||
|
||||
-1
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
+1
-2
@@ -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',
|
||||
})
|
||||
|
||||
+22
-26
@@ -1,29 +1,25 @@
|
||||
@if (ticket) {
|
||||
<div class="flex justify-between">
|
||||
<span class="text-base">
|
||||
{{ ticket.tours.at(0)?.ordinanceType }}
|
||||
</span>
|
||||
<div>
|
||||
@if (ticket.errors.length) {
|
||||
<mat-icon
|
||||
[matBadge]="ticket.errors.length"
|
||||
matBadgeColor="warn"
|
||||
[title]="ticket.errors.length + ' Anmerkungen'"
|
||||
aria-hidden="false"
|
||||
>
|
||||
error
|
||||
</mat-icon>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
>{{ ticket.tours.at(0)?.patientName }}
|
||||
{{ ticket.tours.at(0)?.patientSurname }}</span
|
||||
<div class="flex justify-between">
|
||||
<span class="text-base">
|
||||
{{ ticket.tours.at(0)?.ordinanceType }}
|
||||
</span>
|
||||
<div>
|
||||
@if (ticket.errors.length) {
|
||||
<mat-icon
|
||||
[matBadge]="ticket.errors.length"
|
||||
matBadgeColor="warn"
|
||||
[title]="ticket.errors.length + ' Anmerkungen'"
|
||||
aria-hidden="false"
|
||||
>
|
||||
<span
|
||||
>{{ getToursLength() }} Tour{{
|
||||
getToursLength() > 1 ? 'en' : ''
|
||||
}}</span
|
||||
>
|
||||
<span class="text-xs self-end">{{ getDistance(ticket.createdAt) }}</span>
|
||||
error
|
||||
</mat-icon>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<span>
|
||||
{{ ticket.tours.at(0)?.patientName }}
|
||||
{{ ticket.tours.at(0)?.patientSurname }}
|
||||
</span>
|
||||
<span>{{ getToursLength() }} Tour{{ getToursLength() > 1 ? "en" : "" }}</span>
|
||||
<span class="text-xs self-end">{{ getDistance(ticket.createdAt) }}</span>
|
||||
}
|
||||
|
||||
+7
-3
@@ -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')
|
||||
|
||||
+28
-37
@@ -1,4 +1,3 @@
|
||||
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
@@ -11,50 +10,42 @@ 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) {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 'dks-ticket-lane-header',
|
||||
template: `<div></div>
|
||||
@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>
|
||||
}
|
||||
}
|
||||
<dks-ticket-lane-header-filter
|
||||
[state]="state"
|
||||
(filterChange)="filterChanged($event)"
|
||||
></dks-ticket-lane-header-filter>`,
|
||||
host: {
|
||||
class: 'flex items-center justify-between p-2',
|
||||
},
|
||||
styles: [
|
||||
`
|
||||
} }
|
||||
<div>
|
||||
<dks-ticket-lane-header-filter
|
||||
*dksRole="['admin']"
|
||||
[state]="state"
|
||||
(filterChange)="filterChanged($event)"
|
||||
></dks-ticket-lane-header-filter>
|
||||
</div>`,
|
||||
host: {
|
||||
class: 'flex items-center justify-between p-2',
|
||||
},
|
||||
styles: [
|
||||
`
|
||||
:host {
|
||||
color: var(--mat-sys-on-secondary-container);
|
||||
background-color: var(--mat-sys-secondary-container);
|
||||
}
|
||||
`,
|
||||
],
|
||||
imports: [TicketLaneHeaderFilterComponent]
|
||||
],
|
||||
imports: [TicketLaneHeaderFilterComponent, RoleDirective],
|
||||
})
|
||||
export class TicketLaneHeaderComponent {
|
||||
@Input() state!: TicketValidationState;
|
||||
|
||||
+1
@@ -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>
|
||||
|
||||
+9
@@ -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$
|
||||
|
||||
+13
-2
@@ -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>
|
||||
}
|
||||
|
||||
+1
-2
@@ -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',
|
||||
})
|
||||
|
||||
+133
@@ -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);
|
||||
}
|
||||
}
|
||||
+62
-88
@@ -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 {
|
||||
@@ -9,95 +9,69 @@ import {
|
||||
} from '../../../core/data-access/graphql/generated/generated';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 'dks-ticket-overview',
|
||||
template: `<div class="grid grid-cols-4">
|
||||
<mat-list class="col-span-2" role="list">
|
||||
<div mat-subheader class="text-sm">Ticket Informationen</div>
|
||||
<mat-list-item role="listitem">
|
||||
Typ:
|
||||
<span class="font-bold">
|
||||
{{ ticket.tours[0].type }}
|
||||
</span>
|
||||
</mat-list-item>
|
||||
<mat-list-item role="listitem">
|
||||
Kategorie:
|
||||
<span class="font-bold">
|
||||
{{ ticket.tours[0].category }}
|
||||
</span>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
@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) {
|
||||
<span>Genehmigungsfrei</span>
|
||||
}
|
||||
@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
|
||||
>
|
||||
}
|
||||
}
|
||||
</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"
|
||||
>
|
||||
<div mat-subheader class="text-sm">Ticket Anmerkungen</div>
|
||||
@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)">{{
|
||||
error.split(':').at(1)
|
||||
}}</span>
|
||||
</mat-list-item>
|
||||
}
|
||||
@for (note of ticket.notes; track note) {
|
||||
<mat-list-item
|
||||
lines="2"
|
||||
role="listitem"
|
||||
>
|
||||
<mat-icon matListItemIcon class="!text-yellow-400">warning</mat-icon>
|
||||
<span matListItemTitle>{{ note.split(':').at(0) }}</span>
|
||||
<span [title]="note.split(':').at(1)">{{ note.split(':').at(1) }}</span>
|
||||
</mat-list-item>
|
||||
}
|
||||
</mat-list>
|
||||
}
|
||||
</div>`,
|
||||
imports: [
|
||||
MatListModule,
|
||||
MatIconModule
|
||||
]
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 'dks-ticket-overview',
|
||||
template: `<div class="grid grid-cols-4">
|
||||
<mat-list class="col-span-2" role="list">
|
||||
<div mat-subheader class="text-sm">Ticket Informationen</div>
|
||||
<mat-list-item role="listitem">
|
||||
Typ:
|
||||
<span class="font-bold">
|
||||
{{ ticket().tours[0].type }}
|
||||
</span>
|
||||
</mat-list-item>
|
||||
<mat-list-item role="listitem">
|
||||
Kategorie:
|
||||
<span class="font-bold">
|
||||
{{ ticket().tours[0].category }}
|
||||
</span>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
@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) {
|
||||
<span>Genehmigungsfrei</span>
|
||||
} @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>
|
||||
} }
|
||||
</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">
|
||||
<div mat-subheader class="text-sm">Ticket Anmerkungen</div>
|
||||
@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)">{{
|
||||
error.split(':').at(1)
|
||||
}}</span>
|
||||
</mat-list-item>
|
||||
}
|
||||
<!-- @for (note of ticket.notes; track note) {
|
||||
<mat-list-item lines="2" role="listitem">
|
||||
<mat-icon matListItemIcon class="!text-yellow-400">warning</mat-icon>
|
||||
<span matListItemTitle>{{ note.split(':').at(0) }}</span>
|
||||
<span [title]="note.split(':').at(1)">{{ note.split(':').at(1) }}</span>
|
||||
</mat-list-item>
|
||||
} -->
|
||||
</mat-list>
|
||||
}
|
||||
</div>`,
|
||||
imports: [MatListModule, MatIconModule],
|
||||
})
|
||||
export class TicketOverviewComponent {
|
||||
@Input() ticket!: Ticket & { tours: Tour[] };
|
||||
ticket = input.required<Ticket & { tours: Tour[] }>()
|
||||
|
||||
ApprovalState = ApprovalState;
|
||||
}
|
||||
|
||||
+20
-13
@@ -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,11 +14,12 @@ 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,
|
||||
selector: 'dks-ticket-tours',
|
||||
template: ` <div class="max-h-96 overflow-y-auto">
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 'dks-ticket-tours',
|
||||
template: ` <div class="max-h-96 overflow-y-auto">
|
||||
<table mat-table [dataSource]="dataSource" class="w-full">
|
||||
<ng-container matColumnDef="select">
|
||||
<th mat-header-cell *matHeaderCellDef>
|
||||
@@ -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>
|
||||
@@ -78,13 +86,13 @@ import {
|
||||
></tr>
|
||||
</table>
|
||||
</div>`,
|
||||
imports: [
|
||||
NgClass,
|
||||
DatePipe,
|
||||
MatButtonModule,
|
||||
MatCheckboxModule,
|
||||
MatTableModule,
|
||||
]
|
||||
imports: [
|
||||
NgClass,
|
||||
DatePipe,
|
||||
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(
|
||||
|
||||
+75
-80
@@ -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,94 +24,91 @@ import {
|
||||
TicketValidationState,
|
||||
Tour,
|
||||
} from '../../../core/data-access/graphql/generated/generated';
|
||||
import { AuthService } from '../../../core/components/ms-authentication/auth.service';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 'dks-ticket-user-action-required',
|
||||
template: `<div class="flex flex-col p-3">
|
||||
<form #form="ngForm" class="flex flex-col w-full">
|
||||
<div class="flex justify-end pt-4">
|
||||
<button mat-stroked-button class="mr-2" (click)="form.resetForm()">
|
||||
Zurücksetzen
|
||||
</button>
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
[disabled]="form.invalid"
|
||||
(click)="saveForm()"
|
||||
>
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col p-2">
|
||||
<label>Wird eine Genehmigung benötigt?</label>
|
||||
<mat-radio-group
|
||||
[(ngModel)]="formModel().approval.isNeeded"
|
||||
name="approvalNeeded"
|
||||
required
|
||||
>
|
||||
<mat-radio-button [value]="false"> Nein </mat-radio-button>
|
||||
<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)
|
||||
) {
|
||||
<div class="flex flex-col p-2">
|
||||
<label>Wer fragte die Genehmigung an?</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-group>
|
||||
</div>
|
||||
<mat-form-field class="p-2">
|
||||
<mat-label>Name des Sachbearbeiters?</mat-label>
|
||||
<input
|
||||
type="text"
|
||||
matInput
|
||||
id="staff"
|
||||
[(ngModel)]="formModel().approval.answeredFrom"
|
||||
name="staff"
|
||||
required
|
||||
/>
|
||||
</mat-form-field>
|
||||
}
|
||||
|
||||
<div class="flex flex-col p-2">
|
||||
<label>Wo befindet sich der Transportschein?</label>
|
||||
<mat-radio-group
|
||||
[(ngModel)]="formModel().tdLocation"
|
||||
name="tdLocation"
|
||||
required
|
||||
>
|
||||
<mat-radio-button [value]="'company'"> Im Betrieb </mat-radio-button>
|
||||
<mat-radio-button [value]="'practice'">
|
||||
In der Praxis
|
||||
</mat-radio-button>
|
||||
<mat-radio-button [value]="'customer'">
|
||||
Beim Kunden
|
||||
</mat-radio-button>
|
||||
<mat-radio-button [value]="'uncertain'"> Unklar </mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</div>
|
||||
</form>
|
||||
</div>`,
|
||||
<form #form="ngForm" class="flex flex-col w-full">
|
||||
<div class="flex justify-end pt-4">
|
||||
<button mat-stroked-button class="mr-2" (click)="form.resetForm()">
|
||||
Zurücksetzen
|
||||
</button>
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
[disabled]="form.invalid"
|
||||
(click)="saveForm()"
|
||||
>
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col p-2">
|
||||
<label>Wird eine Genehmigung benötigt?</label>
|
||||
<mat-radio-group
|
||||
[(ngModel)]="formModel().approval.isNeeded"
|
||||
name="approvalNeeded"
|
||||
required
|
||||
>
|
||||
<mat-radio-button [value]="false"> Nein </mat-radio-button>
|
||||
<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) ) {
|
||||
<div class="flex flex-col p-2">
|
||||
<label>Wurde die Genehmigung bereits angefragt?</label>
|
||||
<mat-radio-group
|
||||
[(ngModel)]="formModel().approval.whoRequested"
|
||||
name="who"
|
||||
required
|
||||
>
|
||||
<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">
|
||||
<mat-label>Name des Sachbearbeiters?</mat-label>
|
||||
<input
|
||||
type="text"
|
||||
matInput
|
||||
id="staff"
|
||||
[(ngModel)]="formModel().approval.answeredFrom"
|
||||
name="staff"
|
||||
required
|
||||
/>
|
||||
</mat-form-field>
|
||||
}
|
||||
|
||||
<div class="flex flex-col p-2">
|
||||
<label>Wo befindet sich der Transportschein im Original?</label>
|
||||
<mat-radio-group
|
||||
[(ngModel)]="formModel().tdLocation"
|
||||
name="tdLocation"
|
||||
required
|
||||
>
|
||||
<mat-radio-button [value]="'company'"> Im Betrieb </mat-radio-button>
|
||||
<mat-radio-button [value]="'practice'">
|
||||
In der Praxis
|
||||
</mat-radio-button>
|
||||
<mat-radio-button [value]="'customer'">
|
||||
Beim Kunden
|
||||
</mat-radio-button>
|
||||
<mat-radio-button [value]="'uncertain'"> Unklar </mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</div>
|
||||
</form>
|
||||
</div>`,
|
||||
imports: [
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
MatFormFieldModule,
|
||||
MatRadioModule,
|
||||
MatInputModule,
|
||||
FormsModule
|
||||
],
|
||||
FormsModule,
|
||||
],
|
||||
})
|
||||
export class TicketUserActionRequiredComponent implements OnInit {
|
||||
@Input() ticket!: Ticket & { tours: Tour[] };
|
||||
|
||||
+27
-21
@@ -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) {
|
||||
<dks-ticket-user-action-required
|
||||
[ticket]="ref.data"
|
||||
(saveFormData)="setDocumentInfo($event)"
|
||||
></dks-ticket-user-action-required>
|
||||
}
|
||||
@case (TicketState.TdUncertain) {
|
||||
<dks-ticket-td-uncertain
|
||||
[ticket]="ref.data"
|
||||
(saveFormData)="setDocumentInfo($event)"
|
||||
></dks-ticket-td-uncertain>
|
||||
}
|
||||
@case (TicketState.DocumentsMissing) {
|
||||
<dks-ticket-documents-missing
|
||||
[ticket]="ref.data"
|
||||
(saveFormData)="setDocumentInfo($event)"
|
||||
></dks-ticket-documents-missing>
|
||||
}
|
||||
}
|
||||
@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) {
|
||||
<dks-ticket-td-uncertain
|
||||
[ticket]="ref.data"
|
||||
(saveFormData)="setDocumentInfo($event)"
|
||||
></dks-ticket-td-uncertain>
|
||||
} @case (TicketState.DocumentsMissing) {
|
||||
<dks-ticket-documents-missing
|
||||
[ticket]="ref.data"
|
||||
(saveFormData)="setDocumentInfo($event)"
|
||||
></dks-ticket-documents-missing>
|
||||
} }
|
||||
</ng-container>
|
||||
</mat-tab>
|
||||
}
|
||||
|
||||
+2
@@ -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:
|
||||
@@ -16,4 +26,4 @@ export const environment = {
|
||||
otlpEndpoint: 'https://alloy.genius.ceo/collect',
|
||||
serviceName: 'avicenna.dashboard.dev'
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
@@ -28,4 +40,4 @@
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user