| @@ -2886,6 +2886,7 @@ components: | |||
| - firstName | |||
| - referenceId | |||
| - lastName | |||
| - active | |||
| User.jsonld: | |||
| type: object | |||
| description: '' | |||
| @@ -2973,6 +2974,7 @@ components: | |||
| - firstName | |||
| - referenceId | |||
| - lastName | |||
| - active | |||
| UserTrip: | |||
| type: object | |||
| description: '' | |||
| @@ -0,0 +1,136 @@ | |||
| import { Directive, ViewChild } from '@angular/core'; | |||
| import { AbstractDataFormComponent, FormSubmitEvent } from "@app/_components/_abstract/abstract-data-form-component"; | |||
| import { FormGroup } from '@angular/forms'; | |||
| import { ImageUploadComponent } from '@app/_components/image-upload/image-upload.component'; | |||
| import { ImageUploadService } from '@app/_services/image-upload.service'; | |||
| import { ModalStatus } from '@app/_helpers/modal.states'; | |||
| @Directive() | |||
| export abstract class AbstractImageFormComponent<T extends { [key: string]: any }> extends AbstractDataFormComponent<T> { | |||
| // Gemeinsame Eigenschaften für Komponenten mit Bildupload | |||
| selectedFile: File | null = null; | |||
| imageToDelete: string | null = null; | |||
| @ViewChild('imageUpload') imageUpload!: ImageUploadComponent; | |||
| // Abstrakte Eigenschaften, die von abgeleiteten Klassen implementiert werden müssen | |||
| abstract get imageIriControlName(): string; | |||
| abstract get imageDbId(): string | number | undefined | null; | |||
| constructor( | |||
| protected imageUploadService: ImageUploadService, | |||
| formGroup: FormGroup, | |||
| createFunction: ((data: T) => any) | undefined, | |||
| updateFunction: (id: string | number, data: T) => any, | |||
| deleteFunction: (id: string | number) => any, | |||
| ...args: any[] | |||
| ) { | |||
| super(formGroup, createFunction, updateFunction, deleteFunction, ...args); | |||
| this.submit.subscribe((event: FormSubmitEvent<T>) => { | |||
| if (event.status === ModalStatus.Submitted) { | |||
| this.resetImageStatus(); | |||
| } | |||
| }); | |||
| } | |||
| handleFileSelected(file: File): void { | |||
| this.selectedFile = file; | |||
| } | |||
| handleFileDeleted(): void { | |||
| // Speichere die aktuelle imageIri, um sie später zu löschen | |||
| this.imageToDelete = this.form.get(this.imageIriControlName)?.value; | |||
| // Aktualisiere den Formularwert | |||
| const patchValue: any = {}; | |||
| patchValue[this.imageIriControlName] = null; | |||
| this.form.patchValue(patchValue); | |||
| // Falls eine Datei zum Hochladen ausgewählt wurde, entferne diese auch | |||
| this.selectedFile = null; | |||
| } | |||
| resetImageStatus(): void { | |||
| if (this.imageUpload) { | |||
| this.imageUpload.reset(); | |||
| } | |||
| // Zurücksetzen der temporären Variablen | |||
| this.imageToDelete = null; | |||
| this.selectedFile = null; | |||
| } | |||
| override onSubmit(): void { | |||
| if (!this.form.valid) { | |||
| return; | |||
| } | |||
| // Drei Fälle: | |||
| // 1. Ein neues Bild wurde ausgewählt | |||
| // 2. Ein bestehendes Bild soll gelöscht werden | |||
| // 3. Keine Änderungen am Bild | |||
| if (this.selectedFile && this.imageToDelete) { | |||
| // Fall 1a: Ein neues Bild wurde ausgewählt UND ein altes soll gelöscht werden | |||
| // Zuerst das alte Bild löschen, dann das neue hochladen | |||
| const dbId = this.imageDbId; | |||
| if (dbId) { | |||
| this.imageUploadService.deleteImage(dbId).subscribe({ | |||
| next: (success) => { | |||
| // Altes Bild wurde gelöscht, jetzt neues hochladen | |||
| this.uploadNewFile(); | |||
| }, | |||
| error: () => { | |||
| // Fehlerbehandlung bereits im Service, trotzdem neues Bild hochladen | |||
| this.uploadNewFile(); | |||
| } | |||
| }); | |||
| } else { | |||
| this.uploadNewFile(); | |||
| } | |||
| } else if (this.selectedFile) { | |||
| // Fall 1b: Nur ein neues Bild wurde ausgewählt (kein altes vorhanden) | |||
| this.uploadNewFile(); | |||
| } else if (this.imageToDelete && this.imageDbId) { | |||
| // Fall 2: Nur ein bestehendes Bild soll gelöscht werden | |||
| this.imageUploadService.deleteImage(this.imageDbId).subscribe({ | |||
| next: (success) => { | |||
| this.imageToDelete = null; | |||
| super.onSubmit(); | |||
| }, | |||
| error: () => { | |||
| // Fehlerbehandlung bereits im Service | |||
| super.onSubmit(); | |||
| } | |||
| }); | |||
| } else { | |||
| // Fall 3: Keine Änderungen am Bild | |||
| super.onSubmit(); | |||
| } | |||
| } | |||
| private uploadNewFile(): void { | |||
| if (!this.selectedFile) return; | |||
| this.imageUploadService.uploadImage(this.selectedFile).subscribe({ | |||
| next: (imageId) => { | |||
| if (imageId) { | |||
| // Aktualisiere die Formulardaten mit dem neuen mediaObject | |||
| const patchValue: any = {}; | |||
| patchValue[this.imageIriControlName] = imageId; | |||
| this.form.patchValue(patchValue); | |||
| // Rufe die übergeordnete Methode auf, um den Standard-Speicherprozess zu verarbeiten | |||
| super.onSubmit(); | |||
| } else { | |||
| this.submit.emit({ | |||
| status: ModalStatus.Cancelled, | |||
| data: null | |||
| }); | |||
| } | |||
| } | |||
| }); | |||
| } | |||
| } | |||
| @@ -0,0 +1,19 @@ | |||
| <div class="mb-3"> | |||
| <label [for]="fieldId" class="form-label">{{ label }}:</label> | |||
| <!-- File-Input ist deaktiviert, wenn ein Bild existiert und nicht zum Löschen markiert ist --> | |||
| <input type="file" class="form-control" [id]="fieldId" | |||
| [disabled]="disabled || (imageUrl && showImage)" | |||
| (change)="onFileSelected($event)"/> | |||
| @if (selectedFile) { | |||
| <small class="text-muted">{{ selectedFile.name }}</small> | |||
| } | |||
| @if (imageUrl && showImage) { | |||
| <div class="mt-1 d-flex align-items-start gap-2"> | |||
| <img [src]="imageUrl" [alt]="label" class="img-fluid" /> | |||
| <button type="button" class="btn btn-sm btn-danger" (click)="deleteImage()">X</button> | |||
| </div> | |||
| } | |||
| </div> | |||
| @@ -0,0 +1,23 @@ | |||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||
| import { ImageUploadComponent } from './image-upload.component'; | |||
| describe('ImageUploadComponent', () => { | |||
| let component: ImageUploadComponent; | |||
| let fixture: ComponentFixture<ImageUploadComponent>; | |||
| beforeEach(async () => { | |||
| await TestBed.configureTestingModule({ | |||
| declarations: [ImageUploadComponent] | |||
| }) | |||
| .compileComponents(); | |||
| fixture = TestBed.createComponent(ImageUploadComponent); | |||
| component = fixture.componentInstance; | |||
| fixture.detectChanges(); | |||
| }); | |||
| it('should create', () => { | |||
| expect(component).toBeTruthy(); | |||
| }); | |||
| }); | |||
| @@ -0,0 +1,38 @@ | |||
| import { Component, EventEmitter, Input, Output } from '@angular/core'; | |||
| @Component({ | |||
| selector: 'app-image-upload', | |||
| templateUrl: './image-upload.component.html', | |||
| styleUrl: './image-upload.component.scss' | |||
| }) | |||
| export class ImageUploadComponent { | |||
| @Input() imageUrl: string | null | undefined = null; | |||
| @Input() label: string = 'Bild'; | |||
| @Input() disabled: boolean = false; | |||
| @Input() fieldId: string = 'mediaFile'; | |||
| @Output() fileSelected = new EventEmitter<File>(); | |||
| @Output() fileDeleted = new EventEmitter<void>(); | |||
| selectedFile: File | null = null; | |||
| showImage: boolean = true; | |||
| onFileSelected(event: Event): void { | |||
| const element = event.target as HTMLInputElement; | |||
| if (element.files && element.files.length > 0) { | |||
| this.selectedFile = element.files[0]; | |||
| this.fileSelected.emit(this.selectedFile); | |||
| } | |||
| } | |||
| deleteImage(): void { | |||
| this.showImage = false; | |||
| this.fileDeleted.emit(); | |||
| this.selectedFile = null; | |||
| } | |||
| reset(): void { | |||
| this.selectedFile = null; | |||
| this.showImage = this.imageUrl !== null; | |||
| } | |||
| } | |||
| @@ -123,7 +123,7 @@ export const userForm = new FormGroup({ | |||
| imageUrl: new FormControl(null, []), | |||
| fullName: new FormControl(null, []), | |||
| password: new FormControl(null, []), | |||
| active: new FormControl(null, []), | |||
| active: new FormControl(null, [Validators.required]), | |||
| roles: new FormControl(null, []), | |||
| createdAt: new FormControl(null, []) | |||
| }); | |||
| @@ -139,7 +139,7 @@ export const userJsonldForm = new FormGroup({ | |||
| imageUrl: new FormControl(null, []), | |||
| fullName: new FormControl(null, []), | |||
| password: new FormControl(null, []), | |||
| active: new FormControl(null, []), | |||
| active: new FormControl(null, [Validators.required]), | |||
| roles: new FormControl(null, []), | |||
| createdAt: new FormControl(null, []) | |||
| }); | |||
| @@ -0,0 +1,41 @@ | |||
| import { Injectable } from '@angular/core'; | |||
| import { MediaObjectService } from '@app/core/api/v1'; | |||
| import { Observable, catchError, map, of } from 'rxjs'; | |||
| @Injectable({ | |||
| providedIn: 'root' | |||
| }) | |||
| export class ImageUploadService { | |||
| constructor(private mediaObjectService: MediaObjectService) { } | |||
| /** | |||
| * Lädt ein neues Bild hoch | |||
| * @param file Die hochzuladende Datei | |||
| * @returns Observable mit der ID des hochgeladenen Bildes | |||
| */ | |||
| uploadImage(file: File): Observable<string | null | undefined> { | |||
| return this.mediaObjectService.mediaObjectsPost(file).pipe( | |||
| map(mediaObject => mediaObject.id), | |||
| catchError(error => { | |||
| console.error('Fehler beim Hochladen der Datei:', error); | |||
| return of(null); | |||
| }) | |||
| ); | |||
| } | |||
| /** | |||
| * Löscht ein vorhandenes Bild | |||
| * @param id Die ID des zu löschenden Bildes (kann string oder number sein) | |||
| * @returns Observable, das true zurückgibt, wenn das Löschen erfolgreich war | |||
| */ | |||
| deleteImage(id: string | number): Observable<boolean> { | |||
| return this.mediaObjectService.mediaObjectsIdDelete(id.toString()).pipe( | |||
| map(() => true), | |||
| catchError(error => { | |||
| console.error('Fehler beim Löschen des Bildes:', error); | |||
| return of(false); | |||
| }) | |||
| ); | |||
| } | |||
| } | |||
| @@ -18,33 +18,21 @@ | |||
| </label> | |||
| </div> | |||
| <!-- Neues File-Upload-Feld für MediaObject --> | |||
| <div class="mb-3"> | |||
| <label for="mediaFile" class="form-label">{{ 'user_trip.signature' | translate }}:</label> | |||
| <!-- File-Input ist deaktiviert, wenn ein Bild existiert und nicht zum Löschen markiert ist --> | |||
| <input type="file" class="form-control" id="mediaFile" | |||
| [disabled]="data.signatureUrl && showSignatureImage" | |||
| (change)="onFileSelected($event)"/> | |||
| @if (selectedFile) { | |||
| <small class="text-muted">{{ selectedFile.name }}</small> | |||
| } | |||
| @if (data.signatureUrl && showSignatureImage) { | |||
| <div class="mt-1 d-flex align-items-start gap-2"> | |||
| <img [src]="data.signatureUrl" alt="Signatur" class="img-fluid" /> | |||
| <button type="button" class="btn btn-sm btn-danger" (click)="markSignatureForRemoval()">X</button> | |||
| </div> | |||
| } | |||
| </div> | |||
| <!-- Verwende die neue ImageUpload-Komponente --> | |||
| <app-image-upload | |||
| #imageUpload | |||
| [imageUrl]="data.signatureUrl" | |||
| [label]="'user_trip.signature' | translate" | |||
| [disabled]="data.completed" | |||
| (fileSelected)="handleFileSelected($event)" | |||
| (fileDeleted)="handleFileDeleted()"> | |||
| </app-image-upload> | |||
| </form> | |||
| } | |||
| </div> | |||
| <div class="flex gap-2"> | |||
| <!-- <button type="submit" class="btn btn-primary" [disabled]="form.invalid" (click)="onSubmit()">--> | |||
| <button type="submit" class="btn btn-primary" (click)="onSubmit()"> | |||
| {{ 'basic.save' | translate }} | |||
| </button> | |||
| @@ -1,191 +1,51 @@ | |||
| import { Component } from '@angular/core'; | |||
| import { | |||
| AbstractDataFormComponent, FormSubmitEvent, | |||
| } from "@app/_components/_abstract/abstract-data-form-component"; | |||
| import { | |||
| MediaObjectService, | |||
| UserTripJsonld, | |||
| UserTripService, | |||
| } from "@app/core/api/v1"; | |||
| import { UserTripJsonld, UserTripService } from "@app/core/api/v1"; | |||
| import { userTripForm } from "@app/_forms/apiForms"; | |||
| import { TranslateService } from "@ngx-translate/core"; | |||
| import { Router } from "@angular/router"; | |||
| import { ROUTE_USER_TRIPS } from "@app/app-routing.module"; | |||
| import { ModalStatus } from "@app/_helpers/modal.states"; | |||
| import {AppHelperService} from "@app/_helpers/app-helper.service"; | |||
| import { AppHelperService } from "@app/_helpers/app-helper.service"; | |||
| import { AbstractImageFormComponent } from "@app/_components/_abstract/abstract-image-form-component"; | |||
| import { ImageUploadService } from "@app/_services/image-upload.service"; | |||
| @Component({ | |||
| selector: 'app-user-trip-form', | |||
| templateUrl: './user-trip-form.component.html', | |||
| styleUrl: './user-trip-form.component.scss' | |||
| }) | |||
| export class UserTripFormComponent extends AbstractDataFormComponent<UserTripJsonld> { | |||
| export class UserTripFormComponent extends AbstractImageFormComponent<UserTripJsonld> { | |||
| protected readonly userTripForm = userTripForm; | |||
| selectedFile: File | null = null; | |||
| signatureToDelete: string | null = null; | |||
| showSignatureImage: boolean = true; | |||
| // Implementierung der abstrakten Eigenschaften | |||
| get imageIriControlName(): string { | |||
| return 'signatureIri'; | |||
| } | |||
| get imageDbId(): string | undefined | null | number { | |||
| return this.data?.signature?.dbId; | |||
| } | |||
| constructor( | |||
| private userTripService: UserTripService, | |||
| private mediaObjectService: MediaObjectService, | |||
| imageUploadService: ImageUploadService, | |||
| private appHelperService: AppHelperService, | |||
| translateService: TranslateService, | |||
| router: Router | |||
| ) { | |||
| super( | |||
| imageUploadService, | |||
| userTripForm, | |||
| undefined, | |||
| (id: string | number, data: UserTripJsonld) => | |||
| this.userTripService.userTripsIdPatch( | |||
| userTripService.userTripsIdPatch( | |||
| id.toString(), | |||
| this.appHelperService.convertJsonldToJson(data) | |||
| appHelperService.convertJsonldToJson(data) | |||
| ), | |||
| (id: string | number) => this.userTripService.userTripsIdDelete(id.toString()), | |||
| (id: string | number) => userTripService.userTripsIdDelete(id.toString()), | |||
| translateService, | |||
| router | |||
| ); | |||
| this.redirectAfterDelete = '/' + ROUTE_USER_TRIPS; | |||
| } | |||
| override ngOnInit(): void { | |||
| super.ngOnInit(); | |||
| this.submit.subscribe((event: FormSubmitEvent<UserTripJsonld>) => { | |||
| if (event.status === ModalStatus.Submitted) { | |||
| this.updateImageStatus(); | |||
| } | |||
| }); | |||
| // Debug-Ausgabe für Formularvalidierung | |||
| // this.form.statusChanges.subscribe(status => { | |||
| // console.log('Form status:', status); | |||
| // console.log('Form errors:', this.form.errors); | |||
| // | |||
| // Object.keys(this.form.controls).forEach(key => { | |||
| // const control = this.form.get(key); | |||
| // if (control?.invalid) { | |||
| // console.log(`Control '${key}' is invalid:`, control.errors); | |||
| // } | |||
| // }); | |||
| // }); | |||
| } | |||
| onFileSelected(event: Event): void { | |||
| const element = event.target as HTMLInputElement; | |||
| if (element.files && element.files.length > 0) { | |||
| this.selectedFile = element.files[0]; | |||
| } | |||
| } | |||
| markSignatureForRemoval(): void { | |||
| // Speichere die aktuelle signatureIri, um sie später zu löschen | |||
| this.signatureToDelete = this.form.get('signatureIri')?.value; | |||
| // Aktualisiere den Formularwert | |||
| this.form.patchValue({ | |||
| signatureIri: null | |||
| }); | |||
| // Verstecke das Bild in der UI | |||
| this.showSignatureImage = false; | |||
| // Falls eine Datei zum Hochladen ausgewählt wurde, entferne diese auch | |||
| this.selectedFile = null; | |||
| } | |||
| updateImageStatus(): void { | |||
| // Wenn das Formular erfolgreich gespeichert wurde, wird diese Methode aufgerufen | |||
| // Prüfen, ob es ein Bild gibt oder nicht | |||
| if (this.data?.signatureUrl) { | |||
| // Es gibt ein Bild, also zeige es an | |||
| this.showSignatureImage = true; | |||
| } else { | |||
| // Es gibt kein Bild, also verstecke es | |||
| this.showSignatureImage = false; | |||
| } | |||
| // Zurücksetzen der temporären Variablen | |||
| this.signatureToDelete = null; | |||
| this.selectedFile = null; | |||
| } | |||
| override onSubmit(): void { | |||
| if (!this.form.valid) { | |||
| // console.log('Form is invalid:', this.form.errors); | |||
| // Object.keys(this.form.controls).forEach(key => { | |||
| // const control = this.form.get(key); | |||
| // if (control?.invalid) { | |||
| // console.log(`Control '${key}' is invalid:`, control.errors); | |||
| // } | |||
| // }); | |||
| return; | |||
| } | |||
| // Drei Fälle: | |||
| // 1. Ein neues Bild wurde ausgewählt | |||
| // 2. Ein bestehendes Bild soll gelöscht werden | |||
| // 3. Keine Änderungen am Bild | |||
| if (this.selectedFile && this.signatureToDelete) { | |||
| // Fall 1a: Ein neues Bild wurde ausgewählt UND ein altes soll gelöscht werden | |||
| // Zuerst das alte Bild löschen, dann das neue hochladen | |||
| if (this.data?.signature?.dbId) { | |||
| this.mediaObjectService.mediaObjectsIdDelete(this.data?.signature?.dbId?.toString()).subscribe({ | |||
| next: () => { | |||
| // Altes Bild wurde gelöscht, jetzt neues hochladen | |||
| this.uploadNewFile(); | |||
| }, | |||
| error: (error) => { | |||
| console.error('Error deleting signature:', error); | |||
| // Trotz Fehler neues Bild hochladen | |||
| this.uploadNewFile(); | |||
| } | |||
| }); | |||
| } | |||
| } else if (this.selectedFile) { | |||
| // Fall 1b: Nur ein neues Bild wurde ausgewählt (kein altes vorhanden) | |||
| this.uploadNewFile(); | |||
| } else if (this.signatureToDelete && this.data?.signature?.dbId) { | |||
| // Fall 2: Nur ein bestehendes Bild soll gelöscht werden | |||
| this.mediaObjectService.mediaObjectsIdDelete(this.data?.signature?.dbId?.toString()).subscribe({ | |||
| next: () => { | |||
| this.signatureToDelete = null; | |||
| super.onSubmit(); | |||
| }, | |||
| error: (error) => { | |||
| console.error('Error deleting signature:', error); | |||
| super.onSubmit(); | |||
| } | |||
| }); | |||
| } else { | |||
| // Fall 3: Keine Änderungen am Bild | |||
| super.onSubmit(); | |||
| } | |||
| } | |||
| private uploadNewFile(): void { | |||
| if (!this.selectedFile) return; | |||
| this.mediaObjectService.mediaObjectsPost(this.selectedFile).subscribe({ | |||
| next: (mediaObject) => { | |||
| // Aktualisiere die Formulardaten mit dem neuen mediaObject | |||
| this.form.patchValue({ | |||
| signatureIri: mediaObject.id | |||
| }); | |||
| // Rufe die übergeordnete Methode auf, um den Standard-Speicherprozess zu verarbeiten | |||
| super.onSubmit(); | |||
| }, | |||
| error: (error) => { | |||
| console.error('Error uploading file:', error); | |||
| this.submit.emit({ | |||
| status: ModalStatus.Cancelled, | |||
| data: null | |||
| }); | |||
| } | |||
| }); | |||
| } | |||
| } | |||
| @@ -1,7 +1,7 @@ | |||
| import {Component, ViewChild} from '@angular/core'; | |||
| import {Component, Input, ViewChild} from '@angular/core'; | |||
| import {ListComponent} from "@app/_components/list/list.component"; | |||
| import {ListColDefinition} from "@app/_components/list/list-col-definition"; | |||
| import {UserTripService} from "@app/core/api/v1"; | |||
| import {UserJsonld, UserTripService} from "@app/core/api/v1"; | |||
| import {AppHelperService} from "@app/_helpers/app-helper.service"; | |||
| import {FilterBarComponent} from "@app/_components/filter-bar/filter-bar.component"; | |||
| import {ListGetDataFunctionType} from "@app/_components/list/list-get-data-function-type"; | |||
| @@ -12,7 +12,7 @@ import {ListGetDataFunctionType} from "@app/_components/list/list-get-data-funct | |||
| styleUrl: './user-trip-list.component.scss' | |||
| }) | |||
| export class UserTripListComponent { | |||
| @Input() public user?: UserJsonld; | |||
| @ViewChild("listComponent", {static: false}) listComponent!: ListComponent; | |||
| protected listColDefinitions!: ListColDefinition[]; | |||
| @@ -109,6 +109,10 @@ export class UserTripListComponent { | |||
| return this.userTripService.userTripsGetCollection( | |||
| index, | |||
| pageSize, | |||
| undefined, | |||
| undefined, | |||
| this.user ? this.user.id : undefined, | |||
| undefined, | |||
| // term ? Number(term) : undefined, | |||
| this.listComponent.getFilterJsonString(), | |||
| this.listComponent.getSortingJsonString() | |||
| @@ -11,29 +11,11 @@ | |||
| (submit)="onFormUpdate($event)" | |||
| > | |||
| </app-user-form> | |||
| <div class="card contacts-detail"> | |||
| <div class="card-body row"> | |||
| <div class="spt-col col-12 col-sm-6 col-lg-8"> | |||
| <h2>{{ user.firstName }} {{ user.lastName }}</h2> | |||
| <dl class="spt-dl"> | |||
| <dt>{{ ('users.email' | translate) }}</dt> | |||
| <dd><a href="mailto:{{ user.email }}">{{ user.email }}</a></dd> | |||
| </dl> | |||
| </div> | |||
| <div class="col-12 col-sm-6 col-lg-4 has-image"> | |||
| @if (user.imageUrl !== null && user.imageUrl !== undefined) { | |||
| <img src="{{user.imageUrl}}" width="247" height="94" | |||
| alt="{{user.firstName}} {{user.lastName}}" title="{{user.firstName}} {{user.lastName}}"/> | |||
| } | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </mat-tab> | |||
| <mat-tab label="{{ 'users.userTrips' | translate }}"> | |||
| # List of all user trips of this user | |||
| <app-user-trip-list | |||
| [user]="user" | |||
| ></app-user-trip-list> | |||
| </mat-tab> | |||
| </mat-tab-group> | |||
| </div> | |||
| @@ -3,7 +3,8 @@ import {UserJsonld, UserService} from "@app/core/api/v1"; | |||
| import {AccountService} from "@app/_services"; | |||
| import {AppHelperService} from "@app/_helpers/app-helper.service"; | |||
| import {ActivatedRoute} from "@angular/router"; | |||
| import {FormMode} from "@app/_components/_abstract/abstract-data-form-component"; | |||
| import {FormMode, FormSubmitEvent} from "@app/_components/_abstract/abstract-data-form-component"; | |||
| import {ModalStatus} from "@app/_helpers/modal.states"; | |||
| @Component({ | |||
| selector: 'app-user-detail', | |||
| @@ -12,6 +13,7 @@ import {FormMode} from "@app/_components/_abstract/abstract-data-form-component" | |||
| }) | |||
| export class UserDetailComponent implements OnInit, AfterViewInit { | |||
| @Input() public user!: UserJsonld; | |||
| protected readonly FormMode = FormMode; | |||
| protected isCurrentUser: boolean; | |||
| @@ -56,5 +58,9 @@ export class UserDetailComponent implements OnInit, AfterViewInit { | |||
| } | |||
| } | |||
| protected readonly FormMode = FormMode; | |||
| onFormUpdate(event: FormSubmitEvent<UserJsonld>) { | |||
| if (event.status === ModalStatus.Submitted && event.data) { | |||
| this.user = event.data; | |||
| } | |||
| } | |||
| } | |||
| @@ -6,6 +6,8 @@ | |||
| } | |||
| <div class="spt-form"> | |||
| <form [formGroup]="userForm" (ngSubmit)="onSubmit()"> | |||
| <input type="hidden" formControlName="imageIri" /> | |||
| <div class="mb-3"> | |||
| <label for="email" class="form-label">{{ 'users.email' | translate }}:</label> | |||
| <input type="text" class="form-control" id="email" formControlName="email"/> | |||
| @@ -26,6 +28,23 @@ | |||
| <input type="text" class="form-control" id="password" formControlName="password"/> | |||
| </div> | |||
| <div class="col-12 col-sm-6 col-md-4 col-lg-3 mb-3 switch-widget"> | |||
| <p class="form-label">{{ 'users.active' | translate }}:</p> | |||
| <label class="switch"> | |||
| <input type="checkbox" formControlName="active"> | |||
| <span class="slider round"></span> | |||
| </label> | |||
| </div> | |||
| <!-- Verwende die neue ImageUpload-Komponente --> | |||
| <app-image-upload | |||
| #imageUpload | |||
| [imageUrl]="data?.imageUrl" | |||
| [label]="'users.image' | translate" | |||
| (fileSelected)="handleFileSelected($event)" | |||
| (fileDeleted)="handleFileDeleted()"> | |||
| </app-image-upload> | |||
| <div class="flex gap-2"> | |||
| <button type="submit" class="btn btn-primary" [disabled]="form.invalid"> | |||
| {{ 'basic.save' | translate }} | |||
| @@ -33,10 +52,10 @@ | |||
| @if (isEditMode()) { | |||
| <button type="button" class="ms-3 btn btn-primary" (click)="onDelete()"> | |||
| {{ 'basic.delete' | translate }} {{ 'model.trip' | translate }} | |||
| {{ 'basic.delete' | translate }} {{ 'model.user' | translate }} | |||
| </button> | |||
| } | |||
| </div> | |||
| </form> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| @@ -1,40 +1,52 @@ | |||
| import { Component } from '@angular/core'; | |||
| import {AbstractDataFormComponent} from "@app/_components/_abstract/abstract-data-form-component"; | |||
| import {UserJsonld, UserService} from "@app/core/api/v1"; | |||
| import {AppHelperService} from "@app/_helpers/app-helper.service"; | |||
| import {TranslateService} from "@ngx-translate/core"; | |||
| import {Router} from "@angular/router"; | |||
| import {ROUTE_USERS} from "@app/app-routing.module"; | |||
| import {userForm} from "@app/_forms/apiForms"; | |||
| import { UserJsonld, UserService } from "@app/core/api/v1"; | |||
| import { AppHelperService } from "@app/_helpers/app-helper.service"; | |||
| import { TranslateService } from "@ngx-translate/core"; | |||
| import { Router } from "@angular/router"; | |||
| import { ROUTE_USERS } from "@app/app-routing.module"; | |||
| import { userForm } from "@app/_forms/apiForms"; | |||
| import { AbstractImageFormComponent } from "@app/_components/_abstract/abstract-image-form-component"; | |||
| import { ImageUploadService } from "@app/_services/image-upload.service"; | |||
| @Component({ | |||
| selector: 'app-user-form', | |||
| templateUrl: './user-form.component.html', | |||
| styleUrl: './user-form.component.scss' | |||
| selector: 'app-user-form', | |||
| templateUrl: './user-form.component.html', | |||
| styleUrl: './user-form.component.scss' | |||
| }) | |||
| export class UserFormComponent extends AbstractDataFormComponent<UserJsonld> { | |||
| export class UserFormComponent extends AbstractImageFormComponent<UserJsonld> { | |||
| protected readonly userForm = userForm; | |||
| // Implementierung der abstrakten Eigenschaften | |||
| get imageIriControlName(): string { | |||
| return 'imageIri'; | |||
| } | |||
| get imageDbId(): string | number | undefined | null { | |||
| return this.data?.image?.dbId; | |||
| } | |||
| constructor( | |||
| private userService: UserService, | |||
| imageUploadService: ImageUploadService, | |||
| private appHelperService: AppHelperService, | |||
| translateService: TranslateService, | |||
| router: Router | |||
| ) { | |||
| super( | |||
| imageUploadService, | |||
| userForm, | |||
| (data: UserJsonld) => this.userService.usersPost(data), | |||
| (data: UserJsonld) => userService.usersPost(data), | |||
| (id: string | number, data: UserJsonld) => | |||
| this.userService.usersIdPatch( | |||
| userService.usersIdPatch( | |||
| id.toString(), | |||
| this.appHelperService.convertJsonldToJson(data) | |||
| appHelperService.convertJsonldToJson(data) | |||
| ), | |||
| (id: string | number) => this.userService.usersIdDelete(id.toString()), | |||
| (id: string | number) => userService.usersIdDelete(id.toString()), | |||
| translateService, | |||
| router | |||
| ); | |||
| this.redirectAfterDelete = '/' + ROUTE_USERS; | |||
| } | |||
| } | |||
| } | |||
| @@ -7,6 +7,7 @@ import {AppHelperService} from "@app/_helpers/app-helper.service"; | |||
| import {ListComponent} from "@app/_components/list/list.component"; | |||
| import {ListColDefinition} from "@app/_components/list/list-col-definition"; | |||
| import {ListGetDataFunctionType} from "@app/_components/list/list-get-data-function-type"; | |||
| import {FilterBarComponent} from "@app/_components/filter-bar/filter-bar.component"; | |||
| @Component({ | |||
| selector: 'app-user-list', | |||
| @@ -62,6 +63,14 @@ export class UserListComponent implements OnInit, AfterViewInit { | |||
| type: ListComponent.COLUMN_TYPE_TEXT_BOLD, | |||
| field: 'referenceId', | |||
| } as ListColDefinition, | |||
| { | |||
| name: 'active', | |||
| text: 'users.active', | |||
| type: ListComponent.COLUMN_TYPE_BOOLEAN, | |||
| field: 'active', | |||
| sortable: true, | |||
| filterType: FilterBarComponent.FILTER_TYPE_BOOLEAN, | |||
| } as ListColDefinition, | |||
| ]; | |||
| } | |||
| @@ -70,6 +70,7 @@ import { UserTripFormComponent } from './_views/user-trip/user-trip-form/user-tr | |||
| import { UserTripEventComponent } from './_views/user-trip-event/user-trip-event.component'; | |||
| import { UserTripEventListComponent } from './_views/user-trip-event/user-trip-event-list/user-trip-event-list.component'; | |||
| import { UserFormComponent } from './_views/user/user-form/user-form.component'; | |||
| import { ImageUploadComponent } from './_components/image-upload/image-upload.component'; | |||
| registerLocaleData(localeDe, 'de-DE'); | |||
| @@ -165,6 +166,7 @@ export function HttpLoaderFactory(http: HttpClient) { | |||
| UserTripEventComponent, | |||
| UserTripEventListComponent, | |||
| UserFormComponent, | |||
| ImageUploadComponent, | |||
| ], | |||
| providers: [ | |||
| {provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true}, | |||
| @@ -28,7 +28,7 @@ export interface User { | |||
| * The plaintext password when being set or changed. | |||
| */ | |||
| password?: string | null; | |||
| active?: boolean; | |||
| active: boolean; | |||
| roles?: Array<string>; | |||
| readonly createdAt?: string | null; | |||
| } | |||
| @@ -33,7 +33,7 @@ export interface UserJsonld { | |||
| * The plaintext password when being set or changed. | |||
| */ | |||
| password?: string | null; | |||
| active?: boolean; | |||
| active: boolean; | |||
| roles?: Array<string>; | |||
| readonly createdAt?: string | null; | |||
| } | |||
| @@ -121,7 +121,9 @@ | |||
| "lastname": "Lastname", | |||
| "userTrips": "User trips", | |||
| "pilotIdNo": "#Pilot id", | |||
| "password": "Password" | |||
| "password": "Password", | |||
| "active": "Active", | |||
| "image": "Image" | |||
| }, | |||
| "form": | |||
| { | |||
| @@ -17,7 +17,6 @@ use ApiPlatform\Metadata\Get; | |||
| use ApiPlatform\Metadata\GetCollection; | |||
| use ApiPlatform\Metadata\Patch; | |||
| use ApiPlatform\Metadata\Post; | |||
| use App\Entity\MediaObject; | |||
| use App\Entity\User; | |||
| use App\Filter\CustomJsonOrderFilter; | |||
| use App\Filter\UserNameSearchFilter; | |||
| @@ -40,7 +39,7 @@ use Symfony\Component\Validator\Constraints as Assert; | |||
| validationContext: ['groups' => ['Default', 'postValidation']] | |||
| ), | |||
| new Patch( | |||
| security: 'is_granted("is_granted("EDIT", object)")' | |||
| security: 'is_granted("ROLE_ADMIN")' | |||
| ), | |||
| new Delete( | |||
| security: 'is_granted("ROLE_ADMIN")' | |||
| @@ -108,11 +107,7 @@ class UserApi | |||
| #[Assert\NotBlank(groups: ['postValidation'])] | |||
| public ?string $password = null; | |||
| // Object is null ONLY during deserialization: so this allows isPublished | |||
| // to be writable in ALL cases (which is ok because the operations are secured). | |||
| // During serialization, object will always be a DragonTreasureApi, so our | |||
| // voter is called. | |||
| #[ApiProperty(security: 'object === null or is_granted("EDIT", object)')] | |||
| #[Assert\NotNull] | |||
| public bool $active; | |||
| #[ApiProperty(writable: false)] | |||
| @@ -46,6 +46,7 @@ class UserApiToEntityMapper implements MapperInterface | |||
| $entity->setReferenceId($dto->referenceId); | |||
| $entity->setFirstName($dto->firstName); | |||
| $entity->setLastName($dto->lastName); | |||
| $entity->setActive($dto->active); | |||
| if ($dto->password) { | |||
| $entity->setPassword($this->userPasswordHasher->hashPassword($entity, $dto->password)); | |||
| } | |||
| @@ -44,6 +44,7 @@ class UserEntityToApiMapper implements MapperInterface | |||
| $dto->firstName = $entity->getFirstName(); | |||
| $dto->lastName = $entity->getLastName(); | |||
| $dto->roles = $entity->getRoles(); | |||
| $dto->active = $entity->isActive(); | |||
| $dto->imageIri = $dto->image = null; | |||
| if ($entity->getImage() !== null) { | |||