| @@ -1659,6 +1659,56 @@ paths: | |||
| style: form | |||
| explode: false | |||
| allowReserved: false | |||
| - | |||
| name: userTrip | |||
| in: query | |||
| description: '' | |||
| required: false | |||
| deprecated: false | |||
| allowEmptyValue: false | |||
| schema: | |||
| type: string | |||
| style: form | |||
| explode: false | |||
| allowReserved: false | |||
| - | |||
| name: 'userTrip[]' | |||
| in: query | |||
| description: '' | |||
| required: false | |||
| deprecated: false | |||
| allowEmptyValue: false | |||
| schema: | |||
| type: array | |||
| items: | |||
| type: string | |||
| style: form | |||
| explode: true | |||
| allowReserved: false | |||
| - | |||
| name: custom_json_filter | |||
| in: query | |||
| description: '' | |||
| required: false | |||
| deprecated: false | |||
| allowEmptyValue: false | |||
| schema: | |||
| type: string | |||
| style: form | |||
| explode: false | |||
| allowReserved: false | |||
| - | |||
| name: custom_json_order | |||
| in: query | |||
| description: '' | |||
| required: false | |||
| deprecated: false | |||
| allowEmptyValue: false | |||
| schema: | |||
| type: string | |||
| style: form | |||
| explode: false | |||
| allowReserved: false | |||
| deprecated: false | |||
| post: | |||
| operationId: api_user_trip_events_post | |||
| @@ -1717,6 +1767,31 @@ paths: | |||
| explode: false | |||
| allowReserved: false | |||
| deprecated: false | |||
| delete: | |||
| operationId: api_user_trip_events_id_delete | |||
| tags: | |||
| - UserTripEvent | |||
| responses: | |||
| '204': | |||
| description: 'UserTripEvent resource deleted' | |||
| '404': | |||
| description: 'Resource not found' | |||
| summary: 'Removes the UserTripEvent resource.' | |||
| description: 'Removes the UserTripEvent resource.' | |||
| parameters: | |||
| - | |||
| name: id | |||
| in: path | |||
| description: 'UserTripEvent identifier' | |||
| required: true | |||
| deprecated: false | |||
| allowEmptyValue: false | |||
| schema: | |||
| type: string | |||
| style: simple | |||
| explode: false | |||
| allowReserved: false | |||
| deprecated: false | |||
| patch: | |||
| operationId: api_user_trip_events_id_patch | |||
| tags: | |||
| @@ -241,4 +241,17 @@ export class SearchSelectComponent implements OnInit, AfterViewInit { | |||
| } as ListColDefinition, | |||
| ]; | |||
| } | |||
| public static getDefaultColDefEvents(subResource?: string): ListColDefinition[] { | |||
| return [ | |||
| { | |||
| name: 'name', | |||
| text: 'common.name', | |||
| type: ListComponent.COLUMN_TYPE_TEXT, | |||
| field: 'name', | |||
| sortable: true, | |||
| filterType: FilterBarComponent.FILTER_TYPE_TEXT, | |||
| } as ListColDefinition, | |||
| ]; | |||
| } | |||
| } | |||
| @@ -104,7 +104,7 @@ | |||
| [getDataFunction]="getUsers" | |||
| [displayedDataField]="'fullName'" | |||
| [listColDefinitions]="userColDefinitions" | |||
| [dataSet]="userTrip.userIri" | |||
| [dataSet]="userTrip.user" | |||
| (change)="onUserSelectChange(i)" | |||
| > | |||
| </app-search-select> | |||
| @@ -121,8 +121,8 @@ | |||
| </div> | |||
| <div class="mt-4"> | |||
| <button type="button" class="btn btn-success" (click)="saveAllUserTrips()"> | |||
| {{ 'trip.save_user_assignments' | translate }} | |||
| <button type="button" class="btn btn-primary" (click)="saveAllUserTrips()"> | |||
| {{ 'basic.save' | translate }} | |||
| </button> | |||
| </div> | |||
| </div> | |||
| @@ -26,6 +26,7 @@ import {map} from "rxjs/operators"; | |||
| export class TripDetailComponent implements OnInit, AfterViewInit { | |||
| protected trip!: TripJsonld; | |||
| protected readonly FormMode = FormMode; | |||
| protected originalUserTrips: UserTripJsonld[] = []; | |||
| protected tripLocations: TripLocationJsonld[] = []; | |||
| protected userTrips: UserTripJsonld[] = []; | |||
| protected users: UserJsonld[] = []; | |||
| @@ -212,11 +213,10 @@ export class TripDetailComponent implements OnInit, AfterViewInit { | |||
| addNewUserTrip() { | |||
| // Erstelle ein unvollständiges Objekt (ohne user-Property) | |||
| const newUserTrip: any = { | |||
| trip: { | |||
| 'id': this.trip.id | |||
| } as TripJsonld, | |||
| user: undefined | |||
| const newUserTrip: UserTripJsonld = { | |||
| tripIri: this.trip.id!, | |||
| userIri: null, | |||
| completed: false | |||
| }; | |||
| // Füge es als UserTripJsonld hinzu | |||
| @@ -239,16 +239,28 @@ export class TripDetailComponent implements OnInit, AfterViewInit { | |||
| 1, | |||
| 200, | |||
| this.trip !== undefined ? this.trip.id : undefined, | |||
| ).subscribe( | |||
| data => { | |||
| ).subscribe({ | |||
| next: (data) => { | |||
| this.userTrips = data.member; | |||
| // Create a form for each user trip | |||
| this.originalUserTrips = [...data.member]; // Kopie der ursprünglichen UserTrips speichern | |||
| // Formulare für jeden UserTrip erstellen | |||
| this.userForms = []; | |||
| this.userTrips.forEach(() => { | |||
| this.userForms.push(this.createUserForm()); | |||
| this.userTrips.forEach((userTrip, index) => { | |||
| const form = this.createUserForm(); | |||
| // Formularwerte initialisieren | |||
| if (userTrip.userIri || (userTrip.user && userTrip.user.id)) { | |||
| form.get('user')?.setValue(userTrip.userIri || userTrip.user?.id); | |||
| } | |||
| this.userForms.push(form); | |||
| }); | |||
| }, | |||
| error: (error) => { | |||
| console.error('Fehler beim Laden der Benutzerzuweisungen:', error); | |||
| } | |||
| ); | |||
| }); | |||
| } | |||
| removeTripLocation(index: number) { | |||
| @@ -270,21 +282,9 @@ export class TripDetailComponent implements OnInit, AfterViewInit { | |||
| } | |||
| removeUserTrip(index: number) { | |||
| const userTripId = this.userTrips[index].id; | |||
| if (userTripId) { | |||
| // If it exists on the server, delete it | |||
| this.userTripService.userTripsIdDelete(this.appHelperService.extractId(userTripId)).subscribe( | |||
| () => { | |||
| this.userTrips.splice(index, 1); | |||
| this.userForms.splice(index, 1); | |||
| } | |||
| ); | |||
| } else { | |||
| // If it's only local, just remove it from the array | |||
| this.userTrips.splice(index, 1); | |||
| this.userForms.splice(index, 1); | |||
| } | |||
| // Nur aus dem Array entfernen, tatsächliches Löschen erfolgt beim Speichern | |||
| this.userTrips.splice(index, 1); | |||
| this.userForms.splice(index, 1); | |||
| } | |||
| saveAllTripLocations() { | |||
| @@ -296,9 +296,7 @@ export class TripDetailComponent implements OnInit, AfterViewInit { | |||
| if (locationFormValue) { | |||
| // If just an ID was set, create a proper location object | |||
| if (typeof locationFormValue === 'string') { | |||
| tripLocation.location = { | |||
| 'id': locationFormValue | |||
| } as LocationJsonld; | |||
| tripLocation.locationIri = locationFormValue; | |||
| } | |||
| } else { | |||
| // If no location is set, show an error | |||
| @@ -338,50 +336,91 @@ export class TripDetailComponent implements OnInit, AfterViewInit { | |||
| } | |||
| saveAllUserTrips() { | |||
| // First update the user objects in our userTrips array | |||
| this.userTrips.forEach((userTrip, index) => { | |||
| const userFormValue = this.userForms[index].get('user')?.value; | |||
| // Ensure we have a user | |||
| if (userFormValue) { | |||
| // If just an ID was set, create a proper user object | |||
| if (typeof userFormValue === 'string') { | |||
| userTrip.userIri = userFormValue; | |||
| // Aktuelle IDs speichern, um später gelöschte Einträge zu identifizieren | |||
| let originalUserTripIds: string[] = []; | |||
| // Zuerst alle existierenden UserTrips vom Server holen | |||
| this.userTripService.userTripsGetCollection( | |||
| 1, 200, this.trip.dbId?.toString() | |||
| ).subscribe({ | |||
| next: (existingUserTrips) => { | |||
| // IDs der existierenden UserTrips speichern | |||
| originalUserTripIds = existingUserTrips.member | |||
| .filter(userTrip => userTrip.id) | |||
| .map(userTrip => this.appHelperService.extractId(userTrip.id!)); | |||
| // Aktualisieren der user objects in unserem userTrips-Array | |||
| this.userTrips.forEach((userTrip, index) => { | |||
| const userFormValue = this.userForms[index].get('user')?.value; | |||
| // User setzen, wenn verfügbar | |||
| if (userFormValue) { | |||
| if (typeof userFormValue === 'string') { | |||
| userTrip.userIri = userFormValue; | |||
| } | |||
| } | |||
| }); | |||
| // Filtern: Nur UserTrips mit gültigen User-Zuweisungen behalten | |||
| const validUserTrips = this.userTrips.filter(ut => ut.userIri || (ut.user && ut.user.id)); | |||
| if (validUserTrips.length === 0 && this.userTrips.length > 0) { | |||
| window.alert('Bitte wähle für jeden Eintrag einen Benutzer aus oder entferne ungenutzte Einträge.'); | |||
| return; | |||
| } | |||
| } | |||
| }); | |||
| // Check if there are any invalid user trips (without a proper user selection) | |||
| const invalidEntries = this.userTrips.filter(ut => !ut.user || !ut.user.id); | |||
| // Array für alle Operationen erstellen | |||
| const allPromises: Promise<any>[] = []; | |||
| // 1. Neue UserTrips erstellen und existierende aktualisieren | |||
| validUserTrips.forEach(userTrip => { | |||
| if (userTrip.id) { | |||
| // Existierenden UserTrip aktualisieren | |||
| const id = this.appHelperService.extractId(userTrip.id); | |||
| // ID aus der Liste der zu löschenden IDs entfernen | |||
| const idIndex = originalUserTripIds.indexOf(id); | |||
| if (idIndex > -1) { | |||
| originalUserTripIds.splice(idIndex, 1); | |||
| } | |||
| // Update-Promise hinzufügen | |||
| allPromises.push( | |||
| firstValueFrom(this.userTripService.userTripsIdPatch( | |||
| id, | |||
| this.appHelperService.convertJsonldToJson(userTrip) | |||
| )) | |||
| ); | |||
| } else { | |||
| // Neuen UserTrip erstellen | |||
| allPromises.push( | |||
| firstValueFrom(this.userTripService.userTripsPost(userTrip)) | |||
| ); | |||
| } | |||
| }); | |||
| if (invalidEntries.length > 0) { | |||
| // Show alert to the user | |||
| window.alert('Please select a user for all entries or remove unused entries before saving.'); | |||
| return; | |||
| } | |||
| // 2. Gelöschte UserTrips von der Datenbank entfernen | |||
| originalUserTripIds.forEach(id => { | |||
| allPromises.push( | |||
| firstValueFrom(this.userTripService.userTripsIdDelete(id)) | |||
| ); | |||
| }); | |||
| // At this point, all entries are valid | |||
| const savePromises = this.userTrips.map(userTrip => { | |||
| if (userTrip.id) { | |||
| // Update existing | |||
| return firstValueFrom(this.userTripService.userTripsIdPatch( | |||
| this.appHelperService.extractId(userTrip.id), | |||
| this.appHelperService.convertJsonldToJson(userTrip) | |||
| )); | |||
| } else { | |||
| // Create new | |||
| return firstValueFrom(this.userTripService.userTripsPost(userTrip)); | |||
| // Alle Operationen ausführen | |||
| Promise.all(allPromises) | |||
| .then(() => { | |||
| // Nach dem Speichern alle UserTrips neu laden | |||
| this.loadUserTrips(); | |||
| }) | |||
| .catch(error => { | |||
| console.error('Fehler beim Speichern der Benutzerzuweisungen:', error); | |||
| window.alert('Beim Speichern der Benutzerzuweisungen ist ein Fehler aufgetreten. Bitte versuche es erneut.'); | |||
| }); | |||
| }, | |||
| error: (error) => { | |||
| console.error('Fehler beim Laden der existierenden Benutzerzuweisungen:', error); | |||
| window.alert('Die aktuellen Benutzerzuweisungen konnten nicht geladen werden. Bitte versuche es erneut.'); | |||
| } | |||
| }); | |||
| Promise.all(savePromises) | |||
| .then(() => { | |||
| // Reload user trips to get updated data | |||
| this.loadUserTrips(); | |||
| }) | |||
| .catch(error => { | |||
| console.error('Error saving user trips:', error); | |||
| window.alert('An error occurred while saving user assignments. Please try again.'); | |||
| }); | |||
| } | |||
| } | |||
| @@ -16,7 +16,91 @@ | |||
| </app-user-trip-form> | |||
| </mat-tab> | |||
| <mat-tab label="{{ 'user_trip.events' | translate }}"> | |||
| <div> | |||
| <h4 class="mb-4">{{ 'user_trip.events' | translate }}</h4> | |||
| <div *ngFor="let userTripEvent of userTripEvents; let i = index" class="mb-4"> | |||
| <div class="row"> | |||
| <div class="col-12 col-md-3 mb-3"> | |||
| <label [for]="'event_' + i" class="form-label">{{ 'model.event' | translate }}*:</label> | |||
| <app-search-select | |||
| [formId]="'eventIri'" | |||
| [formLabelLangKey]="'model.event'" | |||
| [documentForm]="eventForms[i]" | |||
| [getDataFunction]="getEvents" | |||
| [displayedDataField]="'name'" | |||
| [listColDefinitions]="eventColDefinitions" | |||
| [dataSet]="userTripEvent.event" | |||
| > | |||
| </app-search-select> | |||
| </div> | |||
| <div class="col-12 col-md-3 mb-3"> | |||
| <label [for]="'location_' + i" class="form-label">{{ 'model.location' | translate }}*:</label> | |||
| <app-search-select | |||
| [formId]="'locationIri'" | |||
| [formLabelLangKey]="'model.location'" | |||
| [documentForm]="eventForms[i]" | |||
| [getDataFunction]="getLocations" | |||
| [displayedDataField]="'name'" | |||
| [listColDefinitions]="locationColDefinitions" | |||
| [dataSet]="userTripEvent.location" | |||
| > | |||
| </app-search-select> | |||
| </div> | |||
| <div class="col-12 col-md-2 mb-3"> | |||
| <label class="form-label">{{ 'user_trip.event_date' | translate }} ({{ 'common.date' | translate }}):</label> | |||
| <div> | |||
| <input | |||
| type="date" | |||
| class="form-control" | |||
| [value]="formatDateForInput(userTripEvent.date)" | |||
| (change)="onDateInputChange($event, i)" | |||
| /> | |||
| </div> | |||
| </div> | |||
| <div class="col-12 col-md-2 mb-3"> | |||
| <label class="form-label">{{ 'user_trip.event_date' | translate }} ({{ 'common.time' | translate }}):</label> | |||
| <div> | |||
| <input | |||
| type="time" | |||
| class="form-control" | |||
| [value]="formatTimeForInput(userTripEvent.date)" | |||
| (change)="onTimeInputChange($event, i)" | |||
| /> | |||
| </div> | |||
| </div> | |||
| <div class="col-12 col-md-1 mb-3 d-flex align-items-end"> | |||
| <button type="button" class="btn btn-danger" (click)="removeUserTripEvent(i)">X</button> | |||
| </div> | |||
| </div> | |||
| <div class="row"> | |||
| <div class="col-12 mb-3"> | |||
| <label [for]="'note_' + i" class="form-label">{{ 'common.note' | translate }}:</label> | |||
| <textarea | |||
| class="form-control" | |||
| [id]="'note_' + i" | |||
| rows="2" | |||
| [(ngModel)]="userTripEvent.note" | |||
| ></textarea> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <div class="my-3"> | |||
| <button type="button" class="btn btn-primary" (click)="addNewUserTripEvent()">+</button> | |||
| </div> | |||
| <div class="mt-4"> | |||
| <button type="button" class="btn btn-success" (click)="saveAllUserTripEvents()"> | |||
| {{ 'basic.save' | translate }} | |||
| </button> | |||
| </div> | |||
| </div> | |||
| </mat-tab> | |||
| </mat-tab-group> | |||
| } | |||
| } | |||
| @@ -1,52 +1,294 @@ | |||
| import { Component } from '@angular/core'; | |||
| import { Component, OnInit, AfterViewInit, QueryList, ViewChildren } from '@angular/core'; | |||
| import { FormBuilder, FormGroup } from '@angular/forms'; | |||
| import { | |||
| LocationService, | |||
| TripJsonld, | |||
| TripLocationService, | |||
| TripService, UserService, | |||
| LocationJsonld, | |||
| UserService, | |||
| UserTripJsonld, | |||
| UserTripService | |||
| UserTripService, | |||
| UserTripEventJsonld, | |||
| UserTripEventService, | |||
| EventService, | |||
| EventJsonld | |||
| } from "@app/core/api/v1"; | |||
| import {AppHelperService} from "@app/_helpers/app-helper.service"; | |||
| import {ActivatedRoute} from "@angular/router"; | |||
| import {FormBuilder} from "@angular/forms"; | |||
| import {FormMode, FormSubmitEvent} from "@app/_components/_abstract/abstract-data-form-component"; | |||
| import {ModalStatus} from "@app/_helpers/modal.states"; | |||
| import { AppHelperService } from "@app/_helpers/app-helper.service"; | |||
| import { ActivatedRoute } from "@angular/router"; | |||
| import { FormMode, FormSubmitEvent } from "@app/_components/_abstract/abstract-data-form-component"; | |||
| import { ModalStatus } from "@app/_helpers/modal.states"; | |||
| import { firstValueFrom, Observable } from 'rxjs'; | |||
| import { ListColDefinition } from '@app/_components/list/list-col-definition'; | |||
| import { SearchSelectComponent } from '@app/_components/search-select/search-select.component'; | |||
| import { ListComponent } from '@app/_components/list/list.component'; | |||
| import { FilterBarComponent } from "@app/_components/filter-bar/filter-bar.component"; | |||
| @Component({ | |||
| selector: 'app-user-trip-detail', | |||
| templateUrl: './user-trip-detail.component.html', | |||
| styleUrl: './user-trip-detail.component.scss' | |||
| selector: 'app-user-trip-detail', | |||
| templateUrl: './user-trip-detail.component.html', | |||
| styleUrl: './user-trip-detail.component.scss' | |||
| }) | |||
| export class UserTripDetailComponent { | |||
| export class UserTripDetailComponent implements OnInit, AfterViewInit { | |||
| protected userTrip!: UserTripJsonld; | |||
| protected readonly FormMode = FormMode; | |||
| // Events related properties | |||
| protected userTripEvents: UserTripEventJsonld[] = []; | |||
| protected originalUserTripEvents: UserTripEventJsonld[] = []; // Original-Events zum Vergleich | |||
| protected eventForms: FormGroup[] = []; | |||
| protected locationColDefinitions: ListColDefinition[] = SearchSelectComponent.getDefaultColDefLocations(); | |||
| protected eventColDefinitions: ListColDefinition[] = []; | |||
| @ViewChildren(SearchSelectComponent) searchSelects!: QueryList<SearchSelectComponent>; | |||
| constructor( | |||
| private tripService: TripService, | |||
| private tripLocationService: TripLocationService, | |||
| private locationService: LocationService, | |||
| private userTripService: UserTripService, | |||
| private userService: UserService, | |||
| private userTripEventService: UserTripEventService, | |||
| private eventService: EventService, | |||
| protected appHelperService: AppHelperService, | |||
| private route: ActivatedRoute, | |||
| private fb: FormBuilder | |||
| ) {} | |||
| ) { | |||
| // Initialize event column definitions | |||
| this.eventColDefinitions = SearchSelectComponent.getDefaultColDefEvents(); | |||
| } | |||
| ngOnInit() { | |||
| this.route.params.subscribe(params => { | |||
| this.userTripService.userTripsIdGet(params['id']).subscribe( | |||
| data => { | |||
| this.userTrip = data; | |||
| this.loadUserTripEvents(); | |||
| } | |||
| ); | |||
| }); | |||
| } | |||
| ngAfterViewInit() { | |||
| // Reinitialize search selects when they change | |||
| this.searchSelects.changes.subscribe(components => { | |||
| components.forEach((component: SearchSelectComponent) => { | |||
| // Force search selects to initialize | |||
| if (component.dataSet) { | |||
| component.ngAfterViewInit(); | |||
| } | |||
| }); | |||
| }); | |||
| } | |||
| onFormUpdate(event: FormSubmitEvent<UserTripJsonld>) { | |||
| if (event.status === ModalStatus.Submitted && event.data) { | |||
| this.userTrip = event.data; | |||
| } | |||
| } | |||
| } | |||
| loadUserTripEvents() { | |||
| if (!this.userTrip || !this.userTrip.id) return; | |||
| this.userTripEventService.userTripEventsGetCollection( | |||
| 1, | |||
| 200, | |||
| this.userTrip.id | |||
| ).subscribe({ | |||
| next: (data) => { | |||
| this.userTripEvents = data.member; | |||
| this.originalUserTripEvents = [...data.member]; // Kopie der ursprünglichen Events speichern | |||
| // Formular für jedes Event erstellen | |||
| this.eventForms = []; | |||
| this.userTripEvents.forEach((event, index) => { | |||
| const form = this.createEventForm(); | |||
| // Formularwerte initialisieren | |||
| if (event.eventIri) { | |||
| form.get('eventIri')?.setValue(event.eventIri); | |||
| } | |||
| if (event.locationIri) { | |||
| form.get('locationIri')?.setValue(event.locationIri); | |||
| } | |||
| this.eventForms.push(form); | |||
| }); | |||
| }, | |||
| error: (error) => { | |||
| console.error('Fehler beim Laden der Events:', error); | |||
| } | |||
| }); | |||
| } | |||
| createEventForm(): FormGroup { | |||
| return this.fb.group({ | |||
| eventIri: [null], | |||
| locationIri: [null] | |||
| }); | |||
| } | |||
| getEvents = (page: number, pageSize: number, term?: string): Observable<any> => { | |||
| return this.eventService.eventsGetCollection(page, pageSize, undefined, term); | |||
| } | |||
| getLocations = (page: number, pageSize: number, term?: string): Observable<any> => { | |||
| return this.locationService.locationsGetCollection(page, pageSize, undefined, term); | |||
| } | |||
| formatDateForInput(dateString: string): string { | |||
| if (!dateString) return ''; | |||
| const date = new Date(dateString); | |||
| return date.toISOString().split('T')[0]; | |||
| } | |||
| formatTimeForInput(dateString: string): string { | |||
| if (!dateString) return ''; | |||
| const date = new Date(dateString); | |||
| return date.toTimeString().slice(0, 5); | |||
| } | |||
| onDateInputChange(event: Event, index: number) { | |||
| const input = event.target as HTMLInputElement; | |||
| const currentDate = new Date(this.userTripEvents[index].date || new Date()); | |||
| const [year, month, day] = input.value.split('-').map(Number); | |||
| currentDate.setFullYear(year, month - 1, day); | |||
| this.userTripEvents[index].date = currentDate.toISOString(); | |||
| } | |||
| onTimeInputChange(event: Event, index: number) { | |||
| const input = event.target as HTMLInputElement; | |||
| const currentDate = new Date(this.userTripEvents[index].date || new Date()); | |||
| const [hours, minutes] = input.value.split(':').map(Number); | |||
| currentDate.setHours(hours, minutes); | |||
| this.userTripEvents[index].date = currentDate.toISOString(); | |||
| } | |||
| addNewUserTripEvent() { | |||
| // Neues leeres User-Trip-Event erstellen | |||
| const newUserTripEvent: UserTripEventJsonld = { | |||
| userTripIri: this.userTrip.id!, | |||
| date: new Date().toISOString(), | |||
| eventIri: null, | |||
| locationIri: null, | |||
| note: null | |||
| }; | |||
| this.userTripEvents.push(newUserTripEvent); | |||
| this.eventForms.push(this.createEventForm()); | |||
| // Im nächsten Event-Loop erzwingen, dass die search-select-Komponenten initialisiert werden | |||
| setTimeout(() => { | |||
| if (this.searchSelects) { | |||
| const lastSelects = this.searchSelects.toArray().slice(-2); | |||
| lastSelects.forEach(select => { | |||
| if (select) { | |||
| select.ngAfterViewInit(); | |||
| } | |||
| }); | |||
| } | |||
| }); | |||
| } | |||
| removeUserTripEvent(index: number) { | |||
| // Nur aus dem Array entfernen, tatsächliches Löschen erfolgt beim Speichern | |||
| this.userTripEvents.splice(index, 1); | |||
| this.eventForms.splice(index, 1); | |||
| } | |||
| saveAllUserTripEvents() { | |||
| // Aktuelle IDs speichern, um später gelöschte Einträge zu identifizieren | |||
| let originalEventIds: string[] = []; | |||
| // Zuerst alle existierenden Events vom Server holen | |||
| this.userTripEventService.userTripEventsGetCollection( | |||
| 1, 200, this.userTrip.id | |||
| ).subscribe({ | |||
| next: (existingEvents) => { | |||
| // IDs der existierenden Events speichern | |||
| originalEventIds = existingEvents.member | |||
| .filter(event => event.id) | |||
| .map(event => this.appHelperService.extractId(event.id!)); | |||
| // Aktualisieren der Event- und Location-Objekte in unserem userTripEvents-Array | |||
| this.userTripEvents.forEach((userTripEvent, index) => { | |||
| const eventFormValue = this.eventForms[index].get('eventIri')?.value; | |||
| const locationFormValue = this.eventForms[index].get('locationIri')?.value; | |||
| // Event setzen, wenn verfügbar | |||
| if (eventFormValue) { | |||
| if (typeof eventFormValue === 'string') { | |||
| userTripEvent.eventIri = eventFormValue; | |||
| } | |||
| } | |||
| // Location setzen, wenn verfügbar | |||
| if (locationFormValue) { | |||
| if (typeof locationFormValue === 'string') { | |||
| userTripEvent.locationIri = locationFormValue; | |||
| } | |||
| } | |||
| }); | |||
| // Events filtern, die sowohl Event als auch Location haben | |||
| const validUserTripEvents = this.userTripEvents.filter( | |||
| ute => ute.eventIri && ute.locationIri | |||
| ); | |||
| if (validUserTripEvents.length === 0 && this.userTripEvents.length > 0) { | |||
| window.alert('Bitte stelle sicher, dass alle Events sowohl einen Event-Typ als auch einen Ort haben.'); | |||
| return; | |||
| } | |||
| // Array für alle Operationen erstellen | |||
| const allPromises: Promise<any>[] = []; | |||
| // 1. Neue Events erstellen und existierende aktualisieren | |||
| validUserTripEvents.forEach(userTripEvent => { | |||
| if (userTripEvent.id) { | |||
| // Existierendes Event aktualisieren | |||
| const id = this.appHelperService.extractId(userTripEvent.id); | |||
| // ID aus der Liste der zu löschenden IDs entfernen | |||
| const idIndex = originalEventIds.indexOf(id); | |||
| if (idIndex > -1) { | |||
| originalEventIds.splice(idIndex, 1); | |||
| } | |||
| // Update-Promise hinzufügen | |||
| allPromises.push( | |||
| firstValueFrom(this.userTripEventService.userTripEventsIdPatch( | |||
| id, | |||
| this.appHelperService.convertJsonldToJson(userTripEvent) | |||
| )) | |||
| ); | |||
| } else { | |||
| // Neues Event erstellen | |||
| allPromises.push( | |||
| firstValueFrom(this.userTripEventService.userTripEventsPost(userTripEvent)) | |||
| ); | |||
| } | |||
| }); | |||
| // 2. Gelöschte Events von der Datenbank entfernen | |||
| originalEventIds.forEach(id => { | |||
| allPromises.push( | |||
| firstValueFrom(this.userTripEventService.userTripEventsIdDelete(id)) | |||
| ); | |||
| }); | |||
| // Alle Operationen ausführen | |||
| Promise.all(allPromises) | |||
| .then(() => { | |||
| // Nach dem Speichern alle Events neu laden | |||
| this.loadUserTripEvents(); | |||
| }) | |||
| .catch(error => { | |||
| console.error('Fehler beim Speichern der Events:', error); | |||
| window.alert('Beim Speichern der Events ist ein Fehler aufgetreten. Bitte versuche es erneut.'); | |||
| }); | |||
| }, | |||
| error: (error) => { | |||
| console.error('Fehler beim Laden der existierenden Events:', error); | |||
| window.alert('Die aktuellen Events konnten nicht geladen werden. Bitte versuche es erneut.'); | |||
| } | |||
| }); | |||
| } | |||
| } | |||
| @@ -100,13 +100,17 @@ export class UserTripEventService { | |||
| * Retrieves the collection of UserTripEvent resources. | |||
| * @param page The collection page number | |||
| * @param itemsPerPage The number of items per page | |||
| * @param userTrip | |||
| * @param userTrip2 | |||
| * @param customJsonFilter | |||
| * @param customJsonOrder | |||
| * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. | |||
| * @param reportProgress flag to report request and response progress. | |||
| */ | |||
| public userTripEventsGetCollection(page?: number, itemsPerPage?: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/ld+json', context?: HttpContext, transferCache?: boolean}): Observable<ApiUserTripEventsGetCollection200Response>; | |||
| public userTripEventsGetCollection(page?: number, itemsPerPage?: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/ld+json', context?: HttpContext, transferCache?: boolean}): Observable<HttpResponse<ApiUserTripEventsGetCollection200Response>>; | |||
| public userTripEventsGetCollection(page?: number, itemsPerPage?: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/ld+json', context?: HttpContext, transferCache?: boolean}): Observable<HttpEvent<ApiUserTripEventsGetCollection200Response>>; | |||
| public userTripEventsGetCollection(page?: number, itemsPerPage?: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/ld+json', context?: HttpContext, transferCache?: boolean}): Observable<any> { | |||
| public userTripEventsGetCollection(page?: number, itemsPerPage?: number, userTrip?: string, userTrip2?: Array<string>, customJsonFilter?: string, customJsonOrder?: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/ld+json', context?: HttpContext, transferCache?: boolean}): Observable<ApiUserTripEventsGetCollection200Response>; | |||
| public userTripEventsGetCollection(page?: number, itemsPerPage?: number, userTrip?: string, userTrip2?: Array<string>, customJsonFilter?: string, customJsonOrder?: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/ld+json', context?: HttpContext, transferCache?: boolean}): Observable<HttpResponse<ApiUserTripEventsGetCollection200Response>>; | |||
| public userTripEventsGetCollection(page?: number, itemsPerPage?: number, userTrip?: string, userTrip2?: Array<string>, customJsonFilter?: string, customJsonOrder?: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/ld+json', context?: HttpContext, transferCache?: boolean}): Observable<HttpEvent<ApiUserTripEventsGetCollection200Response>>; | |||
| public userTripEventsGetCollection(page?: number, itemsPerPage?: number, userTrip?: string, userTrip2?: Array<string>, customJsonFilter?: string, customJsonOrder?: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/ld+json', context?: HttpContext, transferCache?: boolean}): Observable<any> { | |||
| let localVarQueryParameters = new HttpParams({encoder: this.encoder}); | |||
| if (page !== undefined && page !== null) { | |||
| @@ -117,6 +121,24 @@ export class UserTripEventService { | |||
| localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, | |||
| <any>itemsPerPage, 'itemsPerPage'); | |||
| } | |||
| if (userTrip !== undefined && userTrip !== null) { | |||
| localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, | |||
| <any>userTrip, 'userTrip'); | |||
| } | |||
| if (userTrip2) { | |||
| userTrip2.forEach((element) => { | |||
| localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, | |||
| <any>element, 'userTrip[]'); | |||
| }) | |||
| } | |||
| if (customJsonFilter !== undefined && customJsonFilter !== null) { | |||
| localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, | |||
| <any>customJsonFilter, 'custom_json_filter'); | |||
| } | |||
| if (customJsonOrder !== undefined && customJsonOrder !== null) { | |||
| localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, | |||
| <any>customJsonOrder, 'custom_json_order'); | |||
| } | |||
| let localVarHeaders = this.defaultHeaders; | |||
| @@ -176,6 +198,77 @@ export class UserTripEventService { | |||
| ); | |||
| } | |||
| /** | |||
| * Removes the UserTripEvent resource. | |||
| * Removes the UserTripEvent resource. | |||
| * @param id UserTripEvent identifier | |||
| * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. | |||
| * @param reportProgress flag to report request and response progress. | |||
| */ | |||
| public userTripEventsIdDelete(id: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable<any>; | |||
| public userTripEventsIdDelete(id: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable<HttpResponse<any>>; | |||
| public userTripEventsIdDelete(id: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable<HttpEvent<any>>; | |||
| public userTripEventsIdDelete(id: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable<any> { | |||
| if (id === null || id === undefined) { | |||
| throw new Error('Required parameter id was null or undefined when calling userTripEventsIdDelete.'); | |||
| } | |||
| let localVarHeaders = this.defaultHeaders; | |||
| let localVarCredential: string | undefined; | |||
| // authentication (JWT) required | |||
| localVarCredential = this.configuration.lookupCredential('JWT'); | |||
| if (localVarCredential) { | |||
| localVarHeaders = localVarHeaders.set('Authorization', 'Bearer ' + localVarCredential); | |||
| } | |||
| let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; | |||
| if (localVarHttpHeaderAcceptSelected === undefined) { | |||
| // to determine the Accept header | |||
| const httpHeaderAccepts: string[] = [ | |||
| ]; | |||
| localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); | |||
| } | |||
| if (localVarHttpHeaderAcceptSelected !== undefined) { | |||
| localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); | |||
| } | |||
| let localVarHttpContext: HttpContext | undefined = options && options.context; | |||
| if (localVarHttpContext === undefined) { | |||
| localVarHttpContext = new HttpContext(); | |||
| } | |||
| let localVarTransferCache: boolean | undefined = options && options.transferCache; | |||
| if (localVarTransferCache === undefined) { | |||
| localVarTransferCache = true; | |||
| } | |||
| let responseType_: 'text' | 'json' | 'blob' = 'json'; | |||
| if (localVarHttpHeaderAcceptSelected) { | |||
| if (localVarHttpHeaderAcceptSelected.startsWith('text')) { | |||
| responseType_ = 'text'; | |||
| } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { | |||
| responseType_ = 'json'; | |||
| } else { | |||
| responseType_ = 'blob'; | |||
| } | |||
| } | |||
| let localVarPath = `/api/user_trip_events/${this.configuration.encodeParam({name: "id", value: id, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: undefined})}`; | |||
| return this.httpClient.request<any>('delete', `${this.configuration.basePath}${localVarPath}`, | |||
| { | |||
| context: localVarHttpContext, | |||
| responseType: <any>responseType_, | |||
| withCredentials: this.configuration.withCredentials, | |||
| headers: localVarHeaders, | |||
| observe: observe, | |||
| transferCache: localVarTransferCache, | |||
| reportProgress: reportProgress | |||
| } | |||
| ); | |||
| } | |||
| /** | |||
| * Retrieves a UserTripEvent resource. | |||
| * Retrieves a UserTripEvent resource. | |||
| @@ -59,7 +59,8 @@ | |||
| "shipping_company": "Shipping Company", | |||
| "trip": "Trip", | |||
| "user": "User", | |||
| "user_trip": "User Trip" | |||
| "user_trip": "User Trip", | |||
| "event": "Event" | |||
| }, | |||
| "location": | |||
| { | |||
| @@ -88,7 +88,7 @@ class UserApi | |||
| */ | |||
| #[ApiProperty(readable: false)] | |||
| #[Assert\NotBlank(groups: ['postValidation'])] | |||
| public string $password; | |||
| 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). | |||
| @@ -2,8 +2,11 @@ | |||
| namespace App\ApiResource; | |||
| use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; | |||
| use ApiPlatform\Metadata\ApiFilter; | |||
| use ApiPlatform\Metadata\ApiProperty; | |||
| use ApiPlatform\Metadata\ApiResource; | |||
| use ApiPlatform\Metadata\Delete; | |||
| use ApiPlatform\Metadata\Get; | |||
| use ApiPlatform\Metadata\GetCollection; | |||
| use ApiPlatform\Metadata\Patch; | |||
| @@ -11,6 +14,8 @@ use ApiPlatform\Metadata\Post; | |||
| use ApiPlatform\Doctrine\Orm\State\Options; | |||
| use App\Entity\UserTrip; | |||
| use App\Entity\UserTripEvent; | |||
| use App\Filter\CustomJsonFilter; | |||
| use App\Filter\CustomJsonOrderFilter; | |||
| use App\State\EntityClassDtoStateProcessor; | |||
| use App\State\EntityToDtoStateProvider; | |||
| use Symfony\Component\Validator\Constraints as Assert; | |||
| @@ -32,12 +37,19 @@ use Symfony\Component\Validator\Constraints\NotBlank; | |||
| new Patch( | |||
| security: 'is_granted("ROLE_ADMIN")' | |||
| ), | |||
| new Delete( | |||
| security: 'is_granted("ROLE_ADMIN")' | |||
| ) | |||
| ], | |||
| security: 'is_granted("ROLE_USER")', | |||
| provider: EntityToDtoStateProvider::class, | |||
| processor: EntityClassDtoStateProcessor::class, | |||
| stateOptions: new Options(entityClass: UserTripEvent::class), | |||
| )] | |||
| #[ApiFilter(SearchFilter::class, properties: ['userTrip' => 'exact'])] | |||
| #[ApiFilter(CustomJsonFilter::class)] | |||
| #[ApiFilter(CustomJsonOrderFilter::class)] | |||
| class UserTripEventApi | |||
| { | |||
| #[ApiProperty(readable: false, writable: false, identifier: true)] | |||
| @@ -40,10 +40,12 @@ class UserTripEvent | |||
| public function __construct( | |||
| UserTrip $userTrip, | |||
| Event $event, | |||
| Location $location, | |||
| DateTimeImmutable $date | |||
| ) { | |||
| $this->userTrip = $userTrip; | |||
| $this->event = $event; | |||
| $this->location = $location; | |||
| $this->date = $date; | |||
| $this->createdAt = new DateTimeImmutable(); | |||
| } | |||
| @@ -0,0 +1,49 @@ | |||
| <?php | |||
| namespace App\Mapper; | |||
| use App\ApiResource\EventApi; | |||
| use App\Entity\Event; | |||
| use App\Repository\EventRepository; | |||
| use Symfonycasts\MicroMapper\AsMapper; | |||
| use Symfonycasts\MicroMapper\MapperInterface; | |||
| #[AsMapper(from: EventApi::class, to: Event::class)] | |||
| class EventApiToEntityMapper implements MapperInterface | |||
| { | |||
| public function __construct( | |||
| private EventRepository $repository, | |||
| ) { | |||
| } | |||
| public function load(object $from, string $toClass, array $context): object | |||
| { | |||
| $dto = $from; | |||
| assert($dto instanceof EventApi); | |||
| if ($dto->id) { | |||
| $entity = $this->repository->find($dto->id); | |||
| if (!$entity) { | |||
| throw new \Exception('Event not found'); | |||
| } | |||
| return $entity; | |||
| } | |||
| return new Event(); | |||
| } | |||
| public function populate(object $from, object $to, array $context): object | |||
| { | |||
| $dto = $from; | |||
| $entity = $to; | |||
| assert($dto instanceof EventApi); | |||
| assert($entity instanceof Event); | |||
| $entity->setName($dto->name); | |||
| $entity->setIdentifier($dto->identifier); | |||
| $entity->setSequence($dto->sequence); | |||
| $entity->setMandatory($dto->mandatory); | |||
| return $entity; | |||
| } | |||
| } | |||
| @@ -3,6 +3,8 @@ | |||
| namespace App\Mapper; | |||
| use App\ApiResource\UserTripApi; | |||
| use App\Entity\Trip; | |||
| use App\Entity\User; | |||
| use App\Entity\UserTrip; | |||
| use App\Repository\MediaObjectRepository; | |||
| use App\Repository\UserTripRepository; | |||
| @@ -19,7 +21,8 @@ class UserTripApiToEntityMapper implements MapperInterface | |||
| private UserTripRepository $repository, | |||
| private TripRepository $tripRepository, | |||
| private UserRepository $userRepository, | |||
| private MediaObjectRepository $mediaObjectRepository | |||
| private MediaObjectRepository $mediaObjectRepository, | |||
| private MicroMapperInterface $microMapper | |||
| ) { | |||
| } | |||
| @@ -37,24 +40,25 @@ class UserTripApiToEntityMapper implements MapperInterface | |||
| } | |||
| // For new user trips, we need trip and user | |||
| if (!$dto->trip?->id || !$dto->user?->id) { | |||
| if (!$dto->tripIri || !$dto->userIri) { | |||
| throw new \Exception('Trip and User are required for new user trips'); | |||
| } | |||
| $trip = $this->tripRepository->find($dto->trip->id); | |||
| $trip = $this->microMapper->map($dto->tripIri, Trip::class, [ | |||
| MicroMapperInterface::MAX_DEPTH => 1, | |||
| ]); | |||
| if (!$trip) { | |||
| throw new \Exception('Trip not found'); | |||
| } | |||
| $user = $this->userRepository->find($dto->user->id); | |||
| $user = $this->microMapper->map($dto->userIri, User::class, [ | |||
| MicroMapperInterface::MAX_DEPTH => 1, | |||
| ]); | |||
| if (!$user) { | |||
| throw new \Exception('User not found'); | |||
| } | |||
| return new UserTrip( | |||
| $trip, | |||
| $user, | |||
| ); | |||
| return new UserTrip($trip, $user); | |||
| } | |||
| public function populate(object $from, object $to, array $context): object | |||
| @@ -2,11 +2,13 @@ | |||
| namespace App\Mapper; | |||
| use App\ApiResource\UserTripApi; | |||
| use App\ApiResource\UserTripEventApi; | |||
| use App\Entity\Event; | |||
| use App\Entity\Location; | |||
| use App\Entity\UserTrip; | |||
| use App\Entity\UserTripEvent; | |||
| use App\Repository\UserTripEventRepository; | |||
| use App\Repository\UserTripRepository; | |||
| use App\Repository\EventRepository; | |||
| use Symfonycasts\MicroMapper\AsMapper; | |||
| use Symfonycasts\MicroMapper\MapperInterface; | |||
| use Symfonycasts\MicroMapper\MicroMapperInterface; | |||
| @@ -16,8 +18,7 @@ class UserTripEventApiToEntityMapper implements MapperInterface | |||
| { | |||
| public function __construct( | |||
| private UserTripEventRepository $repository, | |||
| private UserTripRepository $userTripRepository, | |||
| private EventRepository $eventRepository, | |||
| private MicroMapperInterface $microMapper | |||
| ) { | |||
| } | |||
| @@ -35,21 +36,32 @@ class UserTripEventApiToEntityMapper implements MapperInterface | |||
| } | |||
| // For new user trip events, we need userTrip and event | |||
| if (!$dto->userTrip?->id || !$dto->event?->id) { | |||
| if (!$dto->userTripIri || !$dto->eventIri || !$dto->locationIri) { | |||
| throw new \Exception('UserTrip and Event are required for new user trip events'); | |||
| } | |||
| $userTrip = $this->userTripRepository->find($dto->userTrip->id); | |||
| $userTrip = $this->microMapper->map($dto->userTripIri, UserTrip::class, [ | |||
| MicroMapperInterface::MAX_DEPTH => 1, | |||
| ]); | |||
| if (!$userTrip) { | |||
| throw new \Exception('UserTrip not found'); | |||
| } | |||
| $event = $this->eventRepository->find($dto->event->id); | |||
| $event = $this->microMapper->map($dto->eventIri, Event::class, [ | |||
| MicroMapperInterface::MAX_DEPTH => 1, | |||
| ]); | |||
| if (!$event) { | |||
| throw new \Exception('Event not found'); | |||
| } | |||
| return new UserTripEvent($userTrip, $event, $dto->date); | |||
| $location = $this->microMapper->map($dto->locationIri, Location::class, [ | |||
| MicroMapperInterface::MAX_DEPTH => 1, | |||
| ]); | |||
| if (!$location) { | |||
| throw new \Exception('Location not found'); | |||
| } | |||
| return new UserTripEvent($userTrip, $event, $location, $dto->date); | |||
| } | |||
| public function populate(object $from, object $to, array $context): object | |||
| @@ -59,21 +71,29 @@ class UserTripEventApiToEntityMapper implements MapperInterface | |||
| assert($dto instanceof UserTripEventApi); | |||
| assert($entity instanceof UserTripEvent); | |||
| if ($dto->event) { | |||
| $event = $this->eventRepository->find($dto->event->id); | |||
| if (!$event) { | |||
| throw new \Exception('Event not found'); | |||
| } | |||
| $entity->setEvent($event); | |||
| $userTrip = $this->microMapper->map($dto->userTripIri, UserTrip::class, [ | |||
| MicroMapperInterface::MAX_DEPTH => 1, | |||
| ]); | |||
| if (!$userTrip) { | |||
| throw new \Exception('UserTrip not found'); | |||
| } | |||
| $entity->setUserTrip($userTrip); | |||
| if ($dto->userTrip) { | |||
| $userTrip = $this->userTripRepository->find($dto->userTrip->id); | |||
| if (!$userTrip) { | |||
| throw new \Exception('UserTrip not found'); | |||
| } | |||
| $entity->setUserTrip($userTrip); | |||
| $event = $this->microMapper->map($dto->eventIri, Event::class, [ | |||
| MicroMapperInterface::MAX_DEPTH => 1, | |||
| ]); | |||
| if (!$event) { | |||
| throw new \Exception('Event not found'); | |||
| } | |||
| $entity->setEvent($event); | |||
| $location = $this->microMapper->map($dto->locationIri, Location::class, [ | |||
| MicroMapperInterface::MAX_DEPTH => 1, | |||
| ]); | |||
| if (!$location) { | |||
| throw new \Exception('Location not found'); | |||
| } | |||
| $entity->setLocation($location); | |||
| $entity->setNote($dto->note); | |||
| $entity->setDate($dto->date); | |||
| @@ -50,7 +50,7 @@ class UserTripEventEntityToApiMapper implements MapperInterface | |||
| MicroMapperInterface::MAX_DEPTH => 1, | |||
| ]); | |||
| $dto->locationIri = $dto->location = $this->microMapper->map($entity->getEvent(), LocationApi::class, [ | |||
| $dto->locationIri = $dto->location = $this->microMapper->map($entity->getLocation(), LocationApi::class, [ | |||
| MicroMapperInterface::MAX_DEPTH => 1, | |||
| ]); | |||