Przeglądaj źródła

user form & trips done

master
Daniel 11 miesięcy temu
rodzic
commit
78f0871daf
24 zmienionych plików z 377 dodań i 237 usunięć
  1. +1
    -1
      angular/openapi.json
  2. +2
    -0
      angular/openapi.yaml
  3. +136
    -0
      angular/src/app/_components/_abstract/abstract-image-form-component.ts
  4. +19
    -0
      angular/src/app/_components/image-upload/image-upload.component.html
  5. +0
    -0
      angular/src/app/_components/image-upload/image-upload.component.scss
  6. +23
    -0
      angular/src/app/_components/image-upload/image-upload.component.spec.ts
  7. +38
    -0
      angular/src/app/_components/image-upload/image-upload.component.ts
  8. +2
    -2
      angular/src/app/_forms/apiForms.ts
  9. +41
    -0
      angular/src/app/_services/image-upload.service.ts
  10. +9
    -21
      angular/src/app/_views/user-trip/user-trip-form/user-trip-form.component.html
  11. +19
    -159
      angular/src/app/_views/user-trip/user-trip-form/user-trip-form.component.ts
  12. +7
    -3
      angular/src/app/_views/user-trip/user-trip-list/user-trip-list.component.ts
  13. +3
    -21
      angular/src/app/_views/user/user-detail/user-detail.component.html
  14. +8
    -2
      angular/src/app/_views/user/user-detail/user-detail.component.ts
  15. +21
    -2
      angular/src/app/_views/user/user-form/user-form.component.html
  16. +28
    -16
      angular/src/app/_views/user/user-form/user-form.component.ts
  17. +9
    -0
      angular/src/app/_views/user/user-list/user-list.component.ts
  18. +2
    -0
      angular/src/app/app.module.ts
  19. +1
    -1
      angular/src/app/core/api/v1/model/user.ts
  20. +1
    -1
      angular/src/app/core/api/v1/model/userJsonld.ts
  21. +3
    -1
      angular/src/assets/i18n/en.json
  22. +2
    -7
      httpdocs/src/ApiResource/UserApi.php
  23. +1
    -0
      httpdocs/src/Mapper/UserApiToEntityMapper.php
  24. +1
    -0
      httpdocs/src/Mapper/UserEntityToApiMapper.php

+ 1
- 1
angular/openapi.json
Plik diff jest za duży
Wyświetl plik


+ 2
- 0
angular/openapi.yaml Wyświetl plik

@@ -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: ''


+ 136
- 0
angular/src/app/_components/_abstract/abstract-image-form-component.ts Wyświetl plik

@@ -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
});
}
}
});
}
}

+ 19
- 0
angular/src/app/_components/image-upload/image-upload.component.html Wyświetl plik

@@ -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
angular/src/app/_components/image-upload/image-upload.component.scss Wyświetl plik


+ 23
- 0
angular/src/app/_components/image-upload/image-upload.component.spec.ts Wyświetl plik

@@ -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();
});
});

+ 38
- 0
angular/src/app/_components/image-upload/image-upload.component.ts Wyświetl plik

@@ -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;
}
}

+ 2
- 2
angular/src/app/_forms/apiForms.ts Wyświetl plik

@@ -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, [])
});


+ 41
- 0
angular/src/app/_services/image-upload.service.ts Wyświetl plik

@@ -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);
})
);
}
}

+ 9
- 21
angular/src/app/_views/user-trip/user-trip-form/user-trip-form.component.html Wyświetl plik

@@ -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>


+ 19
- 159
angular/src/app/_views/user-trip/user-trip-form/user-trip-form.component.ts Wyświetl plik

@@ -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
});
}
});
}
}

+ 7
- 3
angular/src/app/_views/user-trip/user-trip-list/user-trip-list.component.ts Wyświetl plik

@@ -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()


+ 3
- 21
angular/src/app/_views/user/user-detail/user-detail.component.html Wyświetl plik

@@ -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>

+ 8
- 2
angular/src/app/_views/user/user-detail/user-detail.component.ts Wyświetl plik

@@ -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;
}
}
}

+ 21
- 2
angular/src/app/_views/user/user-form/user-form.component.html Wyświetl plik

@@ -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>

+ 28
- 16
angular/src/app/_views/user/user-form/user-form.component.ts Wyświetl plik

@@ -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;
}
}
}

+ 9
- 0
angular/src/app/_views/user/user-list/user-list.component.ts Wyświetl plik

@@ -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,
];
}



+ 2
- 0
angular/src/app/app.module.ts Wyświetl plik

@@ -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},


+ 1
- 1
angular/src/app/core/api/v1/model/user.ts Wyświetl plik

@@ -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;
}


+ 1
- 1
angular/src/app/core/api/v1/model/userJsonld.ts Wyświetl plik

@@ -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;
}


+ 3
- 1
angular/src/assets/i18n/en.json Wyświetl plik

@@ -121,7 +121,9 @@
"lastname": "Lastname",
"userTrips": "User trips",
"pilotIdNo": "#Pilot id",
"password": "Password"
"password": "Password",
"active": "Active",
"image": "Image"
},
"form":
{


+ 2
- 7
httpdocs/src/ApiResource/UserApi.php Wyświetl plik

@@ -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)]


+ 1
- 0
httpdocs/src/Mapper/UserApiToEntityMapper.php Wyświetl plik

@@ -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));
}


+ 1
- 0
httpdocs/src/Mapper/UserEntityToApiMapper.php Wyświetl plik

@@ -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) {


Ładowanie…
Anuluj
Zapisz