| @@ -2312,6 +2312,33 @@ components: | |||
| required: | |||
| - name | |||
| - code | |||
| MediaObject: | |||
| type: object | |||
| description: '' | |||
| deprecated: false | |||
| properties: | |||
| dbId: | |||
| readOnly: true | |||
| type: | |||
| - integer | |||
| - 'null' | |||
| contentUrl: | |||
| externalDocs: | |||
| url: 'https://schema.org/contentUrl' | |||
| type: | |||
| - string | |||
| - 'null' | |||
| filePath: | |||
| readOnly: true | |||
| type: | |||
| - string | |||
| - 'null' | |||
| createdAt: | |||
| readOnly: true | |||
| type: | |||
| - string | |||
| - 'null' | |||
| format: date-time | |||
| MediaObject.jsonld: | |||
| type: object | |||
| description: '' | |||
| @@ -2788,6 +2815,8 @@ components: | |||
| - 'null' | |||
| completed: | |||
| type: boolean | |||
| signature: | |||
| $ref: '#/components/schemas/MediaObject' | |||
| signatureUrl: | |||
| readOnly: true | |||
| type: | |||
| @@ -2807,6 +2836,7 @@ components: | |||
| required: | |||
| - trip | |||
| - user | |||
| - completed | |||
| UserTrip.jsonld: | |||
| type: object | |||
| description: '' | |||
| @@ -2850,6 +2880,8 @@ components: | |||
| - 'null' | |||
| completed: | |||
| type: boolean | |||
| signature: | |||
| $ref: '#/components/schemas/MediaObject.jsonld' | |||
| signatureUrl: | |||
| readOnly: true | |||
| type: | |||
| @@ -2869,6 +2901,7 @@ components: | |||
| required: | |||
| - trip | |||
| - user | |||
| - completed | |||
| UserTripEvent: | |||
| type: object | |||
| description: '' | |||
| @@ -31,8 +31,8 @@ export abstract class AbstractDataFormComponent<T extends { [key: string]: any } | |||
| constructor( | |||
| protected formConfig: FormGroup, | |||
| protected createFn: SaveFunction<T>, | |||
| protected updateFn: UpdateFunction<T>, | |||
| protected createFn?: SaveFunction<T>, | |||
| protected updateFn?: UpdateFunction<T>, | |||
| protected deleteFn?: DeleteFunction, | |||
| protected translateService?: TranslateService, | |||
| protected router?: Router | |||
| @@ -58,24 +58,26 @@ export abstract class AbstractDataFormComponent<T extends { [key: string]: any } | |||
| const formData = this.form.value as T; | |||
| const request$ = this.mode === FormMode.Create | |||
| ? this.createFn(formData) | |||
| : this.updateFn(this.id!, formData); | |||
| ? (this.createFn ? this.createFn(formData) : undefined) | |||
| : (this.updateFn ? this.updateFn(this.id!, formData) : undefined); | |||
| request$.subscribe({ | |||
| next: (response) => { | |||
| this.submit.emit({ | |||
| status: ModalStatus.Submitted, | |||
| data: response | |||
| }); | |||
| }, | |||
| error: (error) => { | |||
| console.error('Error saving data:', error); | |||
| this.submit.emit({ | |||
| status: ModalStatus.Cancelled, // Statt Error verwenden wir Cancelled | |||
| data: null | |||
| }); | |||
| } | |||
| }); | |||
| if (request$ !== undefined) { | |||
| request$.subscribe({ | |||
| next: (response) => { | |||
| this.submit.emit({ | |||
| status: ModalStatus.Submitted, | |||
| data: response | |||
| }); | |||
| }, | |||
| error: (error) => { | |||
| console.error('Error saving data:', error); | |||
| this.submit.emit({ | |||
| status: ModalStatus.Cancelled, // Statt Error verwenden wir Cancelled | |||
| data: null | |||
| }); | |||
| } | |||
| }); | |||
| } | |||
| } | |||
| onDelete(): void { | |||
| @@ -31,6 +31,13 @@ export const locationJsonldForm = new FormGroup({ | |||
| createdAt: new FormControl(null, []) | |||
| }); | |||
| export const mediaObjectForm = new FormGroup({ | |||
| dbId: new FormControl(null, []), | |||
| contentUrl: new FormControl(null, []), | |||
| filePath: new FormControl(null, []), | |||
| createdAt: new FormControl(null, []) | |||
| }); | |||
| export const mediaObjectJsonldForm = new FormGroup({ | |||
| dbId: new FormControl(null, []), | |||
| contentUrl: new FormControl(null, []), | |||
| @@ -133,7 +140,8 @@ export const userTripForm = new FormGroup({ | |||
| trip: new FormControl(null, [Validators.required]), | |||
| user: new FormControl(null, [Validators.required]), | |||
| captainName: new FormControl(null, []), | |||
| completed: new FormControl(null, []), | |||
| completed: new FormControl(null, [Validators.required]), | |||
| signature: new FormControl(null, []), | |||
| signatureUrl: new FormControl(null, []), | |||
| completedDate: new FormControl(null, []), | |||
| createdAt: new FormControl(null, []) | |||
| @@ -144,7 +152,8 @@ export const userTripJsonldForm = new FormGroup({ | |||
| trip: new FormControl(null, [Validators.required]), | |||
| user: new FormControl(null, [Validators.required]), | |||
| captainName: new FormControl(null, []), | |||
| completed: new FormControl(null, []), | |||
| completed: new FormControl(null, [Validators.required]), | |||
| signature: new FormControl(null, []), | |||
| signatureUrl: new FormControl(null, []), | |||
| completedDate: new FormControl(null, []), | |||
| createdAt: new FormControl(null, []) | |||
| @@ -5,5 +5,18 @@ | |||
| {{ userTrip.trip.pilotageReference }} ({{ userTrip.user.fullName }})</h2> | |||
| </div> | |||
| </div> | |||
| <mat-tab-group> | |||
| <mat-tab label="{{ 'model.user_trip' | translate }}"> | |||
| <app-user-trip-form | |||
| [data]="userTrip" | |||
| [mode]="FormMode.Edit" | |||
| [id]="appHelperService.extractId(userTrip.id!)" | |||
| (submit)="onFormUpdate($event)" | |||
| > | |||
| </app-user-trip-form> | |||
| </mat-tab> | |||
| <mat-tab label="{{ 'user_trip.events' | translate }}"> | |||
| </mat-tab> | |||
| </mat-tab-group> | |||
| } | |||
| @@ -10,6 +10,8 @@ import { | |||
| 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"; | |||
| @Component({ | |||
| selector: 'app-user-trip-detail', | |||
| @@ -18,6 +20,7 @@ import {FormBuilder} from "@angular/forms"; | |||
| }) | |||
| export class UserTripDetailComponent { | |||
| protected userTrip!: UserTripJsonld; | |||
| protected readonly FormMode = FormMode; | |||
| constructor( | |||
| private tripService: TripService, | |||
| @@ -39,4 +42,11 @@ export class UserTripDetailComponent { | |||
| ); | |||
| }); | |||
| } | |||
| onFormUpdate(event: FormSubmitEvent<UserTripJsonld>) { | |||
| if (event.status === ModalStatus.Submitted && event.data) { | |||
| this.userTrip = event.data; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,48 @@ | |||
| <div class="spt-container"> | |||
| <div class="spt-form"> | |||
| <form [formGroup]="userTripForm" (ngSubmit)="onSubmit()"> | |||
| <input type="hidden" formControlName="trip" /> | |||
| <input type="hidden" formControlName="user" /> | |||
| <input type="hidden" formControlName="signature" /> | |||
| <div class="mb-3"> | |||
| <label for="captainName" class="form-label">{{ 'trip.captain_name' | translate }}:</label> | |||
| <input type="text" class="form-control" id="captainName" formControlName="captainName"/> | |||
| </div> | |||
| <div class="col-12 col-sm-6 col-md-4 col-lg-3 mb-3 switch-widget"> | |||
| <p class="form-label">{{ 'user_trip.completed' | translate }}:</p> | |||
| <label class="switch"> | |||
| <input type="checkbox" formControlName="completed" [disabled]="data ? data.completed : false"> | |||
| <span class="slider round"></span> | |||
| </label> | |||
| </div> | |||
| <!-- Neues File-Upload-Feld für MediaObject --> | |||
| <div class="mb-3"> | |||
| <label for="mediaFile" class="form-label">{{ 'user_trip.signature' | translate }}:</label> | |||
| <input type="file" class="form-control" id="mediaFile" (change)="onFileSelected($event)"/> | |||
| @if (selectedFile) { | |||
| <small class="text-muted">{{ selectedFile.name }}</small> | |||
| } | |||
| @if (data?.signature?.contentUrl) { | |||
| <div class="mt-2"> | |||
| <small>{{ 'user_trip.current_file' | translate }}: {{ getFileNameFromUrl(data?.signature?.contentUrl) }}</small> | |||
| </div> | |||
| } | |||
| </div> | |||
| </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> | |||
| @if (isEditMode()) { | |||
| <button type="button" class="ms-3 btn btn-primary" (click)="onDelete()"> | |||
| {{ 'basic.delete' | translate }} {{ 'model.user_trip' | translate }} | |||
| </button> | |||
| } | |||
| </div> | |||
| </div> | |||
| @@ -0,0 +1,23 @@ | |||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||
| import { UserTripFormComponent } from './user-trip-form.component'; | |||
| describe('UserTripFormComponent', () => { | |||
| let component: UserTripFormComponent; | |||
| let fixture: ComponentFixture<UserTripFormComponent>; | |||
| beforeEach(async () => { | |||
| await TestBed.configureTestingModule({ | |||
| declarations: [UserTripFormComponent] | |||
| }) | |||
| .compileComponents(); | |||
| fixture = TestBed.createComponent(UserTripFormComponent); | |||
| component = fixture.componentInstance; | |||
| fixture.detectChanges(); | |||
| }); | |||
| it('should create', () => { | |||
| expect(component).toBeTruthy(); | |||
| }); | |||
| }); | |||
| @@ -0,0 +1,116 @@ | |||
| import { Component } from '@angular/core'; | |||
| import { | |||
| AbstractDataFormComponent, | |||
| FormMode, | |||
| FormSubmitEvent | |||
| } from "@app/_components/_abstract/abstract-data-form-component"; | |||
| import { | |||
| LocationService, MediaObjectJsonld, MediaObjectService, TripJsonld, | |||
| UserTripJsonld, | |||
| UserTripService, | |||
| VesselService | |||
| } from "@app/core/api/v1"; | |||
| import { tripForm, 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 { firstValueFrom } from 'rxjs'; | |||
| @Component({ | |||
| selector: 'app-user-trip-form', | |||
| templateUrl: './user-trip-form.component.html', | |||
| styleUrl: './user-trip-form.component.scss' | |||
| }) | |||
| export class UserTripFormComponent extends AbstractDataFormComponent<UserTripJsonld> { | |||
| protected readonly userTripForm = userTripForm; | |||
| selectedFile: File | null = null; | |||
| constructor( | |||
| private userTripService: UserTripService, | |||
| private mediaObjectService: MediaObjectService, | |||
| translateService: TranslateService, | |||
| router: Router | |||
| ) { | |||
| super( | |||
| userTripForm, | |||
| undefined, | |||
| (id: string | number, data: UserTripJsonld) => this.userTripService.userTripsIdPatch(id.toString(), data), | |||
| (id: string | number) => this.userTripService.userTripsIdDelete(id.toString()), | |||
| translateService, | |||
| router | |||
| ); | |||
| this.redirectAfterDelete = '/' + ROUTE_USER_TRIPS; | |||
| } | |||
| override ngOnInit(): void { | |||
| super.ngOnInit(); | |||
| // 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]; | |||
| } | |||
| } | |||
| getFileNameFromUrl(url: string | null | undefined): string { | |||
| if (!url) return ''; | |||
| return url.split('/').pop() || ''; | |||
| } | |||
| override onSubmit(): void { | |||
| //if (!this.form.valid) return; | |||
| 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; | |||
| } | |||
| // Wenn keine Datei ausgewählt wurde, rufe direkt die Elternmethode auf | |||
| if (!this.selectedFile) { | |||
| super.onSubmit(); | |||
| return; | |||
| } | |||
| // 1. Handle file upload if a file is selected | |||
| // Blob direkt verwenden, keine FormData erforderlich | |||
| this.mediaObjectService.mediaObjectsPost(this.selectedFile).subscribe({ | |||
| next: (mediaObject) => { | |||
| // 2. Update the form data with the new mediaObject | |||
| this.form.patchValue({ | |||
| signature: mediaObject | |||
| }); | |||
| // 3. Call the parent method to handle the standard save process | |||
| super.onSubmit(); | |||
| }, | |||
| error: (error) => { | |||
| console.error('Error uploading file:', error); | |||
| this.submit.emit({ | |||
| status: ModalStatus.Cancelled, | |||
| data: null | |||
| }); | |||
| } | |||
| }); | |||
| } | |||
| } | |||
| @@ -74,6 +74,22 @@ export class UserTripListComponent { | |||
| sortable: true, | |||
| filterType: FilterBarComponent.FILTER_TYPE_DATE, | |||
| } as ListColDefinition, | |||
| { | |||
| name: 'completed', | |||
| text: 'user_trip.completed', | |||
| type: ListComponent.COLUMN_TYPE_BOOLEAN, | |||
| field: 'completed', | |||
| sortable: true, | |||
| filterType: FilterBarComponent.FILTER_TYPE_BOOLEAN, | |||
| } as ListColDefinition, | |||
| { | |||
| name: 'completedDate', | |||
| text: 'user_trip.completed_at', | |||
| type: ListComponent.COLUMN_TYPE_DATE, | |||
| field: 'completedDate', | |||
| sortable: true, | |||
| filterType: FilterBarComponent.FILTER_TYPE_DATE, | |||
| } as ListColDefinition, | |||
| ]; | |||
| } | |||
| @@ -66,6 +66,7 @@ import {DatetimePickerComponent} from "@app/_components/datetime-picker/datetime | |||
| import { UserTripComponent } from './_views/user-trip/user-trip.component'; | |||
| import { UserTripListComponent } from './_views/user-trip/user-trip-list/user-trip-list.component'; | |||
| import { UserTripDetailComponent } from './_views/user-trip/user-trip-detail/user-trip-detail.component'; | |||
| import { UserTripFormComponent } from './_views/user-trip/user-trip-form/user-trip-form.component'; | |||
| registerLocaleData(localeDe, 'de-DE'); | |||
| @@ -157,6 +158,7 @@ export function HttpLoaderFactory(http: HttpClient) { | |||
| UserTripComponent, | |||
| UserTripListComponent, | |||
| UserTripDetailComponent, | |||
| UserTripFormComponent, | |||
| ], | |||
| providers: [ | |||
| {provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true}, | |||
| @@ -39,6 +39,7 @@ model/eventJsonldContext.ts | |||
| model/eventJsonldContextOneOf.ts | |||
| model/location.ts | |||
| model/locationJsonld.ts | |||
| model/mediaObject.ts | |||
| model/mediaObjectJsonld.ts | |||
| model/models.ts | |||
| model/shippingCompany.ts | |||
| @@ -0,0 +1,23 @@ | |||
| /** | |||
| * Imaq Platform | |||
| * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) | |||
| * | |||
| * The version of the OpenAPI document: 1.0.0 | |||
| * | |||
| * | |||
| * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). | |||
| * https://openapi-generator.tech | |||
| * Do not edit the class manually. | |||
| */ | |||
| /** | |||
| * | |||
| */ | |||
| export interface MediaObject { | |||
| readonly dbId?: number | null; | |||
| contentUrl?: string | null; | |||
| readonly filePath?: string | null; | |||
| readonly createdAt?: string | null; | |||
| } | |||
| @@ -19,6 +19,7 @@ export * from './eventJsonldContext'; | |||
| export * from './eventJsonldContextOneOf'; | |||
| export * from './location'; | |||
| export * from './locationJsonld'; | |||
| export * from './mediaObject'; | |||
| export * from './mediaObjectJsonld'; | |||
| export * from './shippingCompany'; | |||
| export * from './shippingCompanyJsonld'; | |||
| @@ -11,6 +11,7 @@ | |||
| */ | |||
| import { Trip } from './trip'; | |||
| import { User } from './user'; | |||
| import { MediaObject } from './mediaObject'; | |||
| /** | |||
| @@ -21,7 +22,8 @@ export interface UserTrip { | |||
| trip: Trip; | |||
| user: User; | |||
| captainName?: string | null; | |||
| completed?: boolean; | |||
| completed: boolean; | |||
| signature?: MediaObject; | |||
| readonly signatureUrl?: string | null; | |||
| completedDate?: string | null; | |||
| readonly createdAt?: string | null; | |||
| @@ -10,6 +10,7 @@ | |||
| * Do not edit the class manually. | |||
| */ | |||
| import { TripJsonld } from './tripJsonld'; | |||
| import { MediaObjectJsonld } from './mediaObjectJsonld'; | |||
| import { UserJsonld } from './userJsonld'; | |||
| import { EventJsonldContext } from './eventJsonldContext'; | |||
| @@ -25,7 +26,8 @@ export interface UserTripJsonld { | |||
| trip: TripJsonld; | |||
| user: UserJsonld; | |||
| captainName?: string | null; | |||
| completed?: boolean; | |||
| completed: boolean; | |||
| signature?: MediaObjectJsonld; | |||
| readonly signatureUrl?: string | null; | |||
| completedDate?: string | null; | |||
| readonly createdAt?: string | null; | |||
| @@ -38,7 +38,11 @@ | |||
| }, | |||
| "user_trip": | |||
| { | |||
| "view": "User Trips" | |||
| "view": "User Trips", | |||
| "events": "Events", | |||
| "completed": "Completed", | |||
| "completed_at": "Completed at", | |||
| "signature": "Signature Image" | |||
| }, | |||
| "base_data": | |||
| { | |||
| @@ -95,6 +95,7 @@ class UserTripApi | |||
| public ?string $captainName = null; | |||
| #[Assert\NotBlank] | |||
| public bool $completed; | |||
| /** | |||
| @@ -111,8 +112,7 @@ class UserTripApi | |||
| ) | |||
| ] | |||
| )] | |||
| #[Assert\NotBlank] | |||
| public ?MediaObjectApi $mediaObject = null; | |||
| public ?MediaObjectApi $signature = null; | |||
| #[ApiProperty(writable: false)] | |||
| public ?string $signatureUrl = null; | |||
| @@ -2,6 +2,7 @@ | |||
| namespace App\Mapper; | |||
| use App\ApiResource\MediaObjectApi; | |||
| use App\ApiResource\TripApi; | |||
| use App\ApiResource\UserApi; | |||
| use App\ApiResource\UserTripApi; | |||
| @@ -41,6 +42,7 @@ class UserTripEntityToApiMapper implements MapperInterface | |||
| $dto->dbId = $entity->getId(); | |||
| $dto->captainName = $entity->getCaptainName(); | |||
| $dto->signatureUrl = $this->fileUrlService->getFileUrl($entity->getSignature()); | |||
| $dto->completed = $entity->isCompleted(); | |||
| $dto->completedDate = $entity->getCompletedDate(); | |||
| $dto->createdAt = $entity->getCreatedAt(); | |||
| @@ -52,6 +54,14 @@ class UserTripEntityToApiMapper implements MapperInterface | |||
| MicroMapperInterface::MAX_DEPTH => 1, | |||
| ]); | |||
| $dto->signatureUrl = null; | |||
| if ($entity->getSignature() !== null) { | |||
| $dto->signatureUrl = $this->microMapper->map($entity->getSignature(), MediaObjectApi::class, [ | |||
| MicroMapperInterface::MAX_DEPTH => 1, | |||
| ]); | |||
| } | |||
| return $dto; | |||
| } | |||
| } | |||