Bläddra i källkod

wip itinerary entries

master
Daniel 10 månader sedan
förälder
incheckning
acf42de1c2
35 ändrade filer med 472 tillägg och 429 borttagningar
  1. +1
    -1
      angular/openapi.json
  2. +18
    -26
      angular/openapi.yaml
  3. +5
    -2
      angular/src/app/_components/_abstract/abstract-data-form-component.ts
  4. +1
    -1
      angular/src/app/_components/datetime-picker/datetime-picker.component.html
  5. +41
    -17
      angular/src/app/_components/datetime-picker/datetime-picker.component.ts
  6. +5
    -4
      angular/src/app/_components/list/list.component.ts
  7. +4
    -10
      angular/src/app/_forms/apiForms.ts
  8. +1
    -0
      angular/src/app/_helpers/app-helper.service.ts
  9. +4
    -104
      angular/src/app/_views/trip/trip-detail/trip-detail.component.html
  10. +0
    -172
      angular/src/app/_views/trip/trip-detail/trip-detail.component.ts
  11. +1
    -5
      angular/src/app/_views/trip/trip-form/trip-form.component.html
  12. +0
    -8
      angular/src/app/_views/trip/trip-list/trip-list.component.ts
  13. +57
    -0
      angular/src/app/_views/trip/trip-location-form/trip-location-form.component.html
  14. +0
    -0
      angular/src/app/_views/trip/trip-location-form/trip-location-form.component.scss
  15. +23
    -0
      angular/src/app/_views/trip/trip-location-form/trip-location-form.component.spec.ts
  16. +72
    -0
      angular/src/app/_views/trip/trip-location-form/trip-location-form.component.ts
  17. +9
    -0
      angular/src/app/_views/trip/trip-location-list/trip-location-list.component.html
  18. +0
    -0
      angular/src/app/_views/trip/trip-location-list/trip-location-list.component.scss
  19. +23
    -0
      angular/src/app/_views/trip/trip-location-list/trip-location-list.component.spec.ts
  20. +127
    -0
      angular/src/app/_views/trip/trip-location-list/trip-location-list.component.ts
  21. +4
    -0
      angular/src/app/app.module.ts
  22. +0
    -1
      angular/src/app/core/api/v1/model/trip.ts
  23. +0
    -1
      angular/src/app/core/api/v1/model/tripJsonld.ts
  24. +2
    -4
      angular/src/app/core/api/v1/model/tripLocation.ts
  25. +2
    -4
      angular/src/app/core/api/v1/model/tripLocationJsonld.ts
  26. +6
    -0
      angular/src/assets/i18n/en.json
  27. +41
    -0
      httpdocs/migrations/Version20250507154018.php
  28. +0
    -2
      httpdocs/src/ApiResource/TripApi.php
  29. +7
    -12
      httpdocs/src/ApiResource/TripLocationApi.php
  30. +0
    -3
      httpdocs/src/Entity/Trip.php
  31. +13
    -41
      httpdocs/src/Entity/TripLocation.php
  32. +0
    -1
      httpdocs/src/Mapper/TripApiToEntityMapper.php
  33. +0
    -1
      httpdocs/src/Mapper/TripEntityToApiMapper.php
  34. +3
    -5
      httpdocs/src/Mapper/TripLocationApiToEntityMapper.php
  35. +2
    -4
      httpdocs/src/Mapper/TripLocationEntityToApiMapper.php

+ 1
- 1
angular/openapi.json
Filskillnaden har hållits tillbaka eftersom den är för stor
Visa fil


+ 18
- 26
angular/openapi.yaml Visa fil

@@ -4183,10 +4183,6 @@ components:
type:
- string
- 'null'
customerReference:
type:
- string
- 'null'
startLocation:
readOnly: true
type: string
@@ -4280,10 +4276,6 @@ components:
type:
- string
- 'null'
customerReference:
type:
- string
- 'null'
startLocation:
readOnly: true
$ref: '#/components/schemas/Location.jsonld'
@@ -4352,14 +4344,15 @@ components:
- 'null'
format: iri-reference
example: 'https://example.com/'
isArrival:
type: boolean
isTransit:
type: boolean
isDeparture:
type: boolean
date:
type: string
arrivalDateTime:
type:
- string
- 'null'
format: date-time
departureDateTime:
type:
- string
- 'null'
format: date-time
createdAt:
readOnly: true
@@ -4370,7 +4363,6 @@ components:
required:
- tripIri
- locationIri
- date
TripLocation.jsonld:
type: object
description: ''
@@ -4420,14 +4412,15 @@ components:
- 'null'
format: iri-reference
example: 'https://example.com/'
isArrival:
type: boolean
isTransit:
type: boolean
isDeparture:
type: boolean
date:
type: string
arrivalDateTime:
type:
- string
- 'null'
format: date-time
departureDateTime:
type:
- string
- 'null'
format: date-time
createdAt:
readOnly: true
@@ -4438,7 +4431,6 @@ components:
required:
- tripIri
- locationIri
- date
User:
type: object
description: ''


+ 5
- 2
angular/src/app/_components/_abstract/abstract-data-form-component.ts Visa fil

@@ -1,4 +1,4 @@
import { Directive, EventEmitter, Input, Output, OnInit } from '@angular/core';
import {Directive, EventEmitter, Input, Output, OnInit, AfterViewInit} from '@angular/core';
import { FormGroup } from '@angular/forms';
import { Observable } from 'rxjs';
import { Router } from '@angular/router';
@@ -20,7 +20,7 @@ export interface FormSubmitEvent<T> {
}

@Directive()
export abstract class AbstractDataFormComponent<T extends { [key: string]: any }> implements OnInit {
export abstract class AbstractDataFormComponent<T extends { [key: string]: any }> implements OnInit, AfterViewInit {
@Input() data?: T;
@Input() mode: FormMode = FormMode.Create;
@Input() id?: string | number;
@@ -51,6 +51,9 @@ export abstract class AbstractDataFormComponent<T extends { [key: string]: any }
}
}

ngAfterViewInit() {
}

protected getNewDataSet(): T {
return {} as T;
}


+ 1
- 1
angular/src/app/_components/datetime-picker/datetime-picker.component.html Visa fil

@@ -5,6 +5,6 @@
</div>
<div class="col-6">
<label for="{{ inputId }}-time">{{ label }} ({{ 'basic.time' | translate }}):</label>
<input type="time" id="{{ inputId }}-time" class="form-control" formControlName="time" step="1" [readonly]="readonly">
<input type="time" id="{{ inputId }}-time" class="form-control" formControlName="time" [step]="showSeconds ? 1 : 60" [readonly]="readonly">
</div>
</div>

+ 41
- 17
angular/src/app/_components/datetime-picker/datetime-picker.component.ts Visa fil

@@ -11,6 +11,7 @@ export class DatetimePickerComponent implements OnInit {
@Input() inputId: string = 'myId';
@Input() initialValue: string | null = null;
@Input() readonly: boolean = false;
@Input() showSeconds: boolean = false;
@Output() dateTimeChange = new EventEmitter<string | null>();

form: FormGroup;
@@ -46,25 +47,48 @@ export class DatetimePickerComponent implements OnInit {
return date.toLocaleDateString('en-CA');
}

private formatTime(date: Date): string {
return date.toLocaleTimeString('en-GB', { hour12: false });
}
private formatTime(date: Date): string {
if (this.showSeconds) {
return date.toLocaleTimeString('en-GB', { hour12: false });
} else {
// Nur Stunden und Minuten zurückgeben
return date.toLocaleTimeString('en-GB', {
hour12: false,
hour: '2-digit',
minute: '2-digit'
});
}
}

private emitDateTime() {
const { date, time } = this.form.value;
if (date && time) {
const [year, month, day] = date.split('-');

private emitDateTime() {
const { date, time } = this.form.value;
if (date && time) {
const [year, month, day] = date.split('-');
const [hours, minutes, seconds] = time.split(':');
const dateTime = new Date(Number(year), Number(month) - 1, Number(day), Number(hours), Number(minutes), Number(seconds));
// Parse Zeit je nach Format (mit oder ohne Sekunden)
let hours, minutes, seconds;
if (this.showSeconds && time.split(':').length > 2) {
[hours, minutes, seconds] = time.split(':');
} else {
[hours, minutes] = time.split(':');
seconds = '00';
}

// Format the date to match the loaded format
const formattedDate = dateTime.toLocaleString('sv-SE', { timeZone: 'Europe/Berlin' }).replace(' ', 'T') + '+02:00';
const dateTime = new Date(
Number(year),
Number(month) - 1,
Number(day),
Number(hours),
Number(minutes),
Number(seconds || 0)
);

//console.log('Emitting datetime:', formattedDate);
this.dateTimeChange.emit(formattedDate);
} else {
//console.log('Emitting null datetime');
this.dateTimeChange.emit(null);
// Format the date to match the loaded format
const formattedDate = dateTime.toLocaleString('sv-SE', { timeZone: 'Europe/Berlin' }).replace(' ', 'T') + '+02:00';

this.dateTimeChange.emit(formattedDate);
} else {
this.dateTimeChange.emit(null);
}
}
}
}

+ 5
- 4
angular/src/app/_components/list/list.component.ts Visa fil

@@ -29,6 +29,7 @@ export class ListComponent implements OnInit, AfterViewInit, OnDestroy {
@Input() public onRowSelectedFunction!: Function;
@Input() public onUpdateBooleanStateFunction!: ListUpdateElementFunctionType;
@Input() public dataFormComponent!: Type<AbstractDataFormComponent<any>>;
@Input() public dataFormComponentData?: any;
@Input() public searchable: boolean;
@Input() public showDetailButton: boolean;
@Input() public showPosition: boolean;
@@ -122,6 +123,9 @@ export class ListComponent implements OnInit, AfterViewInit, OnDestroy {
this.setupAutoRefresh();
}

ngAfterViewInit(): void {
}

private setupAutoRefresh(): void {
this.clearAutoRefresh();
if (this.refreshIntervalSeconds && this.refreshIntervalSeconds > 0) {
@@ -205,9 +209,6 @@ export class ListComponent implements OnInit, AfterViewInit, OnDestroy {
return visibility;
}

ngAfterViewInit(): void {
}

getData = (): void => {
this.getDataFunction(
this.pagingComponent.getPageIndex(),
@@ -392,7 +393,7 @@ export class ListComponent implements OnInit, AfterViewInit, OnDestroy {
public onCreateData() {
this.appHelperService.openModal(
this.dataFormComponent,
null,
this.dataFormComponentData,
this.getData
);
}


+ 4
- 10
angular/src/app/_forms/apiForms.ts Visa fil

@@ -99,7 +99,6 @@ export const tripForm = new FormGroup({
vessel: new FormControl(null, []),
vesselIri: new FormControl(null, [Validators.required]),
pilotageReference: new FormControl(null, []),
customerReference: new FormControl(null, []),
startLocation: new FormControl(null, []),
startLocationIri: new FormControl(null, [Validators.required]),
endLocation: new FormControl(null, []),
@@ -116,7 +115,6 @@ export const tripJsonldForm = new FormGroup({
vessel: new FormControl(null, []),
vesselIri: new FormControl(null, [Validators.required]),
pilotageReference: new FormControl(null, []),
customerReference: new FormControl(null, []),
startLocation: new FormControl(null, []),
startLocationIri: new FormControl(null, [Validators.required]),
endLocation: new FormControl(null, []),
@@ -134,10 +132,8 @@ export const tripLocationForm = new FormGroup({
tripIri: new FormControl(null, [Validators.required]),
location: new FormControl(null, []),
locationIri: new FormControl(null, [Validators.required]),
isArrival: new FormControl(null, []),
isTransit: new FormControl(null, []),
isDeparture: new FormControl(null, []),
date: new FormControl(null, [Validators.required]),
arrivalDateTime: new FormControl(null, []),
departureDateTime: new FormControl(null, []),
createdAt: new FormControl(null, [])
});

@@ -147,10 +143,8 @@ export const tripLocationJsonldForm = new FormGroup({
tripIri: new FormControl(null, [Validators.required]),
location: new FormControl(null, []),
locationIri: new FormControl(null, [Validators.required]),
isArrival: new FormControl(null, []),
isTransit: new FormControl(null, []),
isDeparture: new FormControl(null, []),
date: new FormControl(null, [Validators.required]),
arrivalDateTime: new FormControl(null, []),
departureDateTime: new FormControl(null, []),
createdAt: new FormControl(null, [])
});



+ 1
- 0
angular/src/app/_helpers/app-helper.service.ts Visa fil

@@ -47,6 +47,7 @@ export class AppHelperService {

public openModal(component: any, data: any, callback?: (callbackParam?: any) => void, callbackParam?: any): Promise<ModalStatus> {
const modalRef = this.modalService.open(component);
console.log(data);
for (const key in data) {
modalRef.componentInstance[key] = data[key];
}


+ 4
- 104
angular/src/app/_views/trip/trip-detail/trip-detail.component.html Visa fil

@@ -16,106 +16,10 @@
<mat-tab label="{{ 'trip.itinerary' | translate }}">
<div>
<h4 class="mb-4">{{ 'trip.itinerary_locations' | translate }}</h4>

<div *ngFor="let tripLocation of tripLocations; let i = index" class="p-2 mb-2 changing-list">
<div class="row">
<div class="col-12 col-md-4 mb-1">
<label [for]="'location_' + i" class="form-label">Location*:</label>
<app-search-select
[formId]="'location'"
[formLabelLangKey]="'model.location'"
[documentForm]="locationForms[i]"
[getDataFunction]="getLocations"
[displayedDataField]="'name'"
[listColDefinitions]="locationColDefinitions"
[dataSet]="tripLocation.location"
>
</app-search-select>
</div>

<div class="col-12 col-md-2 mb-1">
<label class="form-label">trip.date (Date):</label>
<div>
<input
type="date"
class="form-control"
[value]="formatDateForInput(tripLocation.date)"
(change)="onDateInputChange($event, i)"
/>
</div>
</div>

<div class="col-12 col-md-2 mb-1">
<label class="form-label">trip.date (Time):</label>
<div>
<input
type="time"
class="form-control"
[value]="formatTimeForInput(tripLocation.date)"
(change)="onTimeInputChange($event, i)"
/>
</div>
</div>

<div class="col-12 col-md-2 mb-1 d-flex align-items-end">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
[id]="'isArrival_' + i"
[checked]="tripLocation.isArrival"
(change)="onIsArrivalChange($event, i)"
>
<label class="form-check-label" [for]="'isArrival_' + i">
{{ 'trip.is_arrival' | translate }}
</label>
</div>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
[id]="'isTransit_' + i"
[checked]="tripLocation.isTransit"
(change)="onIsTransitChange($event, i)"
>
<label class="form-check-label" [for]="'isTransit_' + i">
{{ 'trip.is_transit' | translate }}
</label>
</div>

<div class="form-check">
<input
class="form-check-input"
type="checkbox"
[id]="'isDeparture_' + i"
[checked]="tripLocation.isDeparture"
(change)="onIsDepartureChange($event, i)"
>
<label class="form-check-label" [for]="'isDeparture_' + i">
{{ 'trip.is_departure' | translate }}
</label>
</div>
</div>

<div class="col-12 col-md-2 mb-1 d-flex align-items-end">
<button type="button" class="btn btn-danger mb-3" (click)="removeTripLocation(i)">X</button>
</div>
</div>
</div>

<div class="row">
<div class="col-12 mb-3">
<button type="button" class="btn btn-primary" (click)="addNewTripLocation()">+</button>
</div>
</div>

<div class="row">
<div class="col-12 mb-3">
<button type="button" class="btn btn-primary" (click)="saveAllTripLocations()">
{{ 'basic.save' | translate }}
</button>
</div>
</div>
<app-trip-location-list
[trip]="trip"
>
</app-trip-location-list>
</div>
</mat-tab>
<mat-tab label="{{ 'trip.assigned_users' | translate }}">
@@ -137,10 +41,6 @@
>
</app-search-select>
</div>

<div class="col-12 col-md-2 d-flex align-items-end">
<button type="button" class="btn btn-danger mb-3" (click)="removeUserTrip(i)">X</button>
</div>
</div>
</div>



+ 0
- 172
angular/src/app/_views/trip/trip-detail/trip-detail.component.ts Visa fil

@@ -168,74 +168,6 @@ export class TripDetailComponent implements OnInit, AfterViewInit {
}
}

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.tripLocations[index].date || new Date());
const [year, month, day] = input.value.split('-').map(Number);

currentDate.setFullYear(year, month - 1, day);
this.tripLocations[index].date = currentDate.toISOString();
}

onTimeInputChange(event: Event, index: number) {
const input = event.target as HTMLInputElement;
const currentDate = new Date(this.tripLocations[index].date || new Date());
const [hours, minutes] = input.value.split(':').map(Number);

currentDate.setHours(hours, minutes);
this.tripLocations[index].date = currentDate.toISOString();
}

onIsArrivalChange(event: Event, index: number) {
const checkbox = event.target as HTMLInputElement;
this.tripLocations[index].isArrival = checkbox.checked;
}

onIsTransitChange(event: Event, index: number) {
const checkbox = event.target as HTMLInputElement;
this.tripLocations[index].isTransit = checkbox.checked;
}

onIsDepartureChange(event: Event, index: number) {
const checkbox = event.target as HTMLInputElement;
this.tripLocations[index].isDeparture = checkbox.checked;
}

addNewTripLocation() {
// Create a new empty trip location
const newTripLocation: TripLocationJsonld = {
tripIri: this.trip.id!,
date: new Date().toISOString(),
isArrival: false,
locationIri: null
};

this.tripLocations.push(newTripLocation);
this.locationForms.push(this.createLocationForm(null));

// Force update in the next event loop to properly initialize the search-select
setTimeout(() => {
if (this.searchSelects) {
const lastSelect = this.searchSelects.last;
if (lastSelect) {
lastSelect.ngAfterViewInit();
}
}
});
}

addNewUserTrip() {
// Erstelle ein unvollständiges Objekt (ohne user-Property)
const newUserTrip: UserTripJsonld = {
@@ -303,110 +235,6 @@ export class TripDetailComponent implements OnInit, AfterViewInit {
})
}

removeTripLocation(index: number): void {
this.translateService.get('basic.delete_confirm').subscribe((confirmMessage: string) => {
if (!confirm(confirmMessage)) {
return;
}
// Just remove from arrays; deletion happens later in saveAllTripLocations()
this.tripLocations.splice(index, 1);
this.locationForms.splice(index, 1);
});
}

removeUserTrip(index: number) {
this.translateService.get('basic.delete_confirm').subscribe((confirmMessage: string) => {
if (!confirm(confirmMessage)) {
return;
}
// Nur aus dem Array entfernen, tatsächliches Löschen erfolgt beim Speichern
this.userTrips.splice(index, 1);
this.userForms.splice(index, 1);
});
}

saveAllTripLocations(): void {
// A) DELETE: find which original dbIds are missing → delete those
const originalDbIds = this.originalTripLocations
.map(o => o.dbId)
// narrow type to number (filter out null/undefined)
.filter((id): id is number => id != null);

const currentDbIds = this.tripLocations
.map(t => t.dbId)
.filter((id): id is number => id != null);

const dbIdsToDelete = originalDbIds.filter(id => !currentDbIds.includes(id));
dbIdsToDelete.forEach(dbId => {
this.tripLocationService
.tripLocationsIdDelete(dbId.toString())
.subscribe({
next: () => console.log(`TripLocation ${dbId} deleted.`),
error: err => console.error(`Error deleting TripLocation ${dbId}:`, err)
});
});

// B) UPSERT: create or update remaining entries
this.tripLocations.forEach((tripLocation, index) => {
const form = this.locationForms[index];
const locationVal = form.get('location')?.value;
const dbId = tripLocation.dbId;
const isNew = dbId == null; // no dbId → brand new

// only for existing entries compare against snapshot
let locationChanged = false;
let modelChanged = false;
if (!isNew) {
const original = this.originalTripLocations[index];
locationChanged = locationVal !== original.locationIri;
modelChanged = (
tripLocation.date !== original.date ||
tripLocation.isArrival !== original.isArrival ||
tripLocation.isTransit !== original.isTransit ||
tripLocation.isDeparture !== original.isDeparture
);
}

// skip if neither new nor changed
if (!(isNew || locationChanged || modelChanged)) {
return;
}

// validate a location was chosen
if (!locationVal) {
console.error(`Location missing for entry #${index}`);
return;
}
if (typeof locationVal === 'string') {
tripLocation.locationIri = locationVal;
}
// always ensure the parent trip IRI is set
tripLocation.tripIri = this.trip?.id ?? null;

// choose correct API call
const save$ = isNew
? this.tripLocationService.tripLocationsPost(tripLocation)
: this.tripLocationService.tripLocationsIdPatch(
dbId!.toString(),
this.appHelperService.convertJsonldToJson(tripLocation)
);

// execute save and log result
save$.subscribe({
next: saved => {
console.log(`TripLocation #${index} ${isNew ? 'created' : 'updated'}.`);
},
error: err => {
console.error(`Error saving TripLocation #${index}:`, err);
}
});
});

// C) Finally: reset original snapshot to current state
this.originalTripLocations = JSON.parse(JSON.stringify(this.tripLocations));
}


saveAllUserTrips() {
// Aktuelle IDs speichern, um später gelöschte Einträge zu identifizieren
let originalUserTripIds: string[] = [];


+ 1
- 5
angular/src/app/_views/trip/trip-form/trip-form.component.html Visa fil

@@ -49,10 +49,6 @@
</app-search-select>
<input id="endLocationIri" type="hidden" formControlName="endLocationIri" required/>
</div>
<div class="col-12 col-lg-6 mb-3">
<label for="customerReference" class="form-label">{{ 'trip.customer_reference' | translate }}:</label>
<input type="text" class="form-control" id="customerReference" formControlName="customerReference"/>
</div>
<div class="col-12 col-lg-6 mb-3">
<app-datetime-picker
[label]="'trip.start_date' | translate"
@@ -65,7 +61,7 @@
<div class="col-12 col-lg-6 mb-3">
<app-datetime-picker
[label]="'trip.end_date' | translate"
[inputId]="'startDate'"
[inputId]="'endDate'"
[initialValue]="tripForm.get('endDate')?.value ?? null"
(dateTimeChange)="onDateChange($event, 'endDate')"
></app-datetime-picker>


+ 0
- 8
angular/src/app/_views/trip/trip-list/trip-list.component.ts Visa fil

@@ -32,14 +32,6 @@ export class TripListComponent {
sortable: true,
filterType: FilterBarComponent.FILTER_TYPE_TEXT,
} as ListColDefinition,
{
name: 'customerReference',
text: 'trip.customer_reference',
type: ListComponent.COLUMN_TYPE_TEXT,
field: 'customerReference',
sortable: true,
filterType: FilterBarComponent.FILTER_TYPE_TEXT,
} as ListColDefinition,
{
name: 'vessel',
text: 'trip.vessel',


+ 57
- 0
angular/src/app/_views/trip/trip-location-form/trip-location-form.component.html Visa fil

@@ -0,0 +1,57 @@
<div class="spt-container">
@if (!isEditMode()) {
<div class="spt-headline d-flex justify-content-between align-items-start">
<h2>{{ ('basic.new') | translate }} {{ 'model.trip_location' | translate }}</h2>
</div>
}
<div class="spt-form">
<form [formGroup]="tripLocationForm" (ngSubmit)="onSubmit()">
<div class="row">
<div class="col-12 mb-3">
<label for="locationIri" class="form-label">{{ 'trip.start_location' | translate }}*:</label>
<app-search-select #locationSearchSelect
[formId]="'locationIri'"
[formLabelLangKey]="'model.location'"
[documentForm]="form"
[getDataFunction]="getLocations"
[displayedDataField]="'name'"
[listColDefinitions]="locationColDefinitions"
[dataSet]="data?.location"
>
</app-search-select>
<input id="locationIri" type="hidden" formControlName="locationIri" required/>
</div>
<div class="col-12 col-lg-6 mb-3">
<app-datetime-picker
[label]="'trip_location.arrival_date_time' | translate"
[inputId]="'arrivalDateTime'"
[initialValue]="tripLocationForm.get('arrivalDateTime')?.value ?? null"
(dateTimeChange)="onDateChange($event, 'arrivalDateTime')"
></app-datetime-picker>
</div>

<div class="col-12 col-lg-6 mb-3">
<app-datetime-picker
[label]="'trip_location.departure_date_time' | translate"
[inputId]="'departureDateTime'"
[initialValue]="tripLocationForm.get('departureDateTime')?.value ?? null"
(dateTimeChange)="onDateChange($event, 'departureDateTime')"
></app-datetime-picker>
</div>
</div>
<div class="row">
<div class="col-12 mb-3">
<button type="submit" class="btn btn-primary" [disabled]="form.invalid">
{{ 'basic.save' | translate }}
</button>

@if (isEditMode()) {
<button type="button" class="ms-3 btn btn-primary" (click)="onDelete()">
{{ 'basic.delete' | translate }} {{ 'model.trip' | translate }}
</button>
}
</div>
</div>
</form>
</div>
</div>

+ 0
- 0
angular/src/app/_views/trip/trip-location-form/trip-location-form.component.scss Visa fil


+ 23
- 0
angular/src/app/_views/trip/trip-location-form/trip-location-form.component.spec.ts Visa fil

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { TripLocationFormComponent } from './trip-location-form.component';

describe('TripLocationFormComponent', () => {
let component: TripLocationFormComponent;
let fixture: ComponentFixture<TripLocationFormComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [TripLocationFormComponent]
})
.compileComponents();
fixture = TestBed.createComponent(TripLocationFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});

+ 72
- 0
angular/src/app/_views/trip/trip-location-form/trip-location-form.component.ts Visa fil

@@ -0,0 +1,72 @@
import {Component, Input} from '@angular/core';
import {AbstractDataFormComponent} from "@app/_components/_abstract/abstract-data-form-component";
import {
LocationService, TripJsonld,
TripLocationJsonld,
TripLocationService,
} 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_BASE_DATA} from "@app/app-routing.module";
import {tripLocationForm} from "@app/_forms/apiForms";
import {ListGetDataFunctionType} from "@app/_components/list/list-get-data-function-type";
import {ListColDefinition} from "@app/_components/list/list-col-definition";
import {SearchSelectComponent} from "@app/_components/search-select/search-select.component";

@Component({
selector: 'app-trip-location-form',
templateUrl: './trip-location-form.component.html',
styleUrl: './trip-location-form.component.scss'
})
export class TripLocationFormComponent extends AbstractDataFormComponent<TripLocationJsonld> {

@Input() public trip!: TripJsonld;
@Input() public arrivalDateTime?: string;
@Input() public departureDateTime?: string;
protected readonly SearchSelectComponent = SearchSelectComponent;
protected locationColDefinitions: ListColDefinition[];

constructor(
private tripLocationService: TripLocationService,
private locationService: LocationService,
private appHelperService: AppHelperService,
translateService: TranslateService,
router: Router
) {
super(
tripLocationForm,
(data: TripLocationJsonld) => {
return this.tripLocationService.tripLocationsPost(data);
},
(id: string | number, data: TripLocationJsonld) =>
this.tripLocationService.tripLocationsIdPatch(
id.toString(),
this.appHelperService.convertJsonldToJson(data)
),
(id: string | number) => this.tripLocationService.tripLocationsIdDelete(id.toString()),
translateService,
router
);

this.redirectAfterDelete = '/' + ROUTE_BASE_DATA;
this.locationColDefinitions = SearchSelectComponent.getDefaultColDefLocations();
}

override ngOnInit() {
super.ngOnInit();
this.form.get('tripIri')?.setValue(this.trip.id);
this.form.get('arrivalDateTime')?.setValue(this.arrivalDateTime);
this.form.get('departureDateTime')?.setValue(this.departureDateTime);
}

getLocations: ListGetDataFunctionType = (index: number, pageSize: number, term?: string) => {
return this.locationService.locationsGetCollection(
index,
pageSize,
term
);
}

protected readonly tripLocationForm = tripLocationForm;
}

+ 9
- 0
angular/src/app/_views/trip/trip-location-list/trip-location-list.component.html Visa fil

@@ -0,0 +1,9 @@
<div class="spt-container">
<app-list #listComponent
[listId]="'tripLocationList'"
[getDataFunction]="getData"
[listColDefinitions]="listColDefinitions"
[dataFormComponent]="tripLocationFormComponent"
[dataFormComponentData]="dataFormComponentData"
></app-list>
</div>

+ 0
- 0
angular/src/app/_views/trip/trip-location-list/trip-location-list.component.scss Visa fil


+ 23
- 0
angular/src/app/_views/trip/trip-location-list/trip-location-list.component.spec.ts Visa fil

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { TripLocationListComponent } from './trip-location-list.component';

describe('TripLocationListComponent', () => {
let component: TripLocationListComponent;
let fixture: ComponentFixture<TripLocationListComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [TripLocationListComponent]
})
.compileComponents();
fixture = TestBed.createComponent(TripLocationListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});

+ 127
- 0
angular/src/app/_views/trip/trip-location-list/trip-location-list.component.ts Visa fil

@@ -0,0 +1,127 @@
import {Component, Input, Type, ViewChild} from '@angular/core';
import {ListComponent} from "@app/_components/list/list.component";
import {ListColDefinition} from "@app/_components/list/list-col-definition";
import {TripJsonld, TripLocationService} 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";
import {TripLocationFormComponent} from "@app/_views/trip/trip-location-form/trip-location-form.component";
import {map, tap} from "rxjs/operators";

@Component({
selector: 'app-trip-location-list',
templateUrl: './trip-location-list.component.html',
styleUrl: './trip-location-list.component.scss'
})
export class TripLocationListComponent {

@Input() public trip!: TripJsonld;
@ViewChild("listComponent", {static: false}) listComponent!: ListComponent;

protected tripLocationFormComponent = TripLocationFormComponent;
protected listColDefinitions!: ListColDefinition[];
protected dataFormComponentData: any;

constructor(
private tripLocationService: TripLocationService,
protected appHelperService: AppHelperService,
) {
this.listColDefinitions = [
{
name: 'location',
text: 'model.location',
type: ListComponent.COLUMN_TYPE_TEXT,
subResource: 'location',
field: 'name',
sortable: true,
filterType: FilterBarComponent.FILTER_TYPE_TEXT,
} as ListColDefinition,
{
name: 'zone',
text: 'model.zone',
type: ListComponent.COLUMN_TYPE_TEXT,
subResource: 'location',
field: 'zoneName',
sortable: true,
filterType: FilterBarComponent.FILTER_TYPE_TEXT,
} as ListColDefinition,
{
name: 'arrivalDateTime',
text: 'trip_location.arrival_date_time',
type: ListComponent.COLUMN_TYPE_DATE,
field: 'arrivalDateTime',
sortable: true,
filterType: FilterBarComponent.FILTER_TYPE_DATE,
} as ListColDefinition,
{
name: 'departureDateTime',
text: 'trip_location.departure_date_time',
type: ListComponent.COLUMN_TYPE_DATE,
field: 'departureDateTime',
sortable: true,
filterType: FilterBarComponent.FILTER_TYPE_DATE,
} as ListColDefinition,
{
name: 'createdAt',
text: 'common.created_at',
type: ListComponent.COLUMN_TYPE_DATE,
field: 'createdAt',
sortable: true,
filterType: FilterBarComponent.FILTER_TYPE_DATE,
} as ListColDefinition,
];
}

ngOnInit() {
this.dataFormComponentData = {
trip: this.trip,
arrivalDateTime: new Date().toISOString(),
departureDateTime: new Date(new Date().setHours(new Date().getHours() + 1))
};
}

ngAfterViewInit(): void {
this.listComponent.getData();
}

getData: ListGetDataFunctionType = (
index: number,
pageSize: number,
term?: string,
) => {
return this.tripLocationService.tripLocationsGetCollection(
index,
pageSize,
this.trip.id,
undefined,
this.listComponent.getFilterJsonString(),
this.listComponent.getSortingJsonString()
).pipe(
map(response => {
// Set default arrival and departure time before return
let arrivalDateTime = new Date().toISOString();
let departureDateTime = new Date(new Date().setHours(new Date().getHours() + 1)).toISOString();
if (response.member && response.member.length > 0) {
const lastEntry = response.member[response.member.length - 1];
if (lastEntry.departureDateTime) {
const departureDate = new Date(lastEntry.departureDateTime);
departureDate.setDate(departureDate.getDate() + 1);
arrivalDateTime = departureDate.toISOString();

const arrivalDate = new Date(arrivalDateTime);
arrivalDate.setHours(arrivalDate.getHours() + 1);
departureDateTime = arrivalDate.toISOString();
}
}
this.dataFormComponentData = {
trip: this.trip,
arrivalDateTime: arrivalDateTime,
departureDateTime: departureDateTime

};

return response;
})
);
}
}

+ 4
- 0
angular/src/app/app.module.ts Visa fil

@@ -71,6 +71,8 @@ import { UserTripEventComponent } from './_views/user-trip-event/user-trip-event
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';
import { TripLocationListComponent } from './_views/trip/trip-location-list/trip-location-list.component';
import { TripLocationFormComponent } from './_views/trip/trip-location-form/trip-location-form.component';

registerLocaleData(localeDe, 'de-DE');

@@ -167,6 +169,8 @@ export function HttpLoaderFactory(http: HttpClient) {
UserTripEventListComponent,
UserFormComponent,
ImageUploadComponent,
TripLocationListComponent,
TripLocationFormComponent,
],
providers: [
{provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true},


+ 0
- 1
angular/src/app/core/api/v1/model/trip.ts Visa fil

@@ -19,7 +19,6 @@ export interface Trip {
readonly vessel?: string;
vesselIri: string | null;
readonly pilotageReference?: string | null;
customerReference?: string | null;
readonly startLocation?: string;
startLocationIri: string | null;
readonly endLocation?: string;


+ 0
- 1
angular/src/app/core/api/v1/model/tripJsonld.ts Visa fil

@@ -24,7 +24,6 @@ export interface TripJsonld {
readonly vessel?: VesselJsonld;
vesselIri: string | null;
readonly pilotageReference?: string | null;
customerReference?: string | null;
readonly startLocation?: LocationJsonld;
startLocationIri: string | null;
readonly endLocation?: LocationJsonld;


+ 2
- 4
angular/src/app/core/api/v1/model/tripLocation.ts Visa fil

@@ -22,10 +22,8 @@ export interface TripLocation {
tripIri: string | null;
location?: Location;
locationIri: string | null;
isArrival?: boolean;
isTransit?: boolean;
isDeparture?: boolean;
date: string;
arrivalDateTime?: string | null;
departureDateTime?: string | null;
readonly createdAt?: string | null;
}


+ 2
- 4
angular/src/app/core/api/v1/model/tripLocationJsonld.ts Visa fil

@@ -25,10 +25,8 @@ export interface TripLocationJsonld {
tripIri: string | null;
location?: LocationJsonld;
locationIri: string | null;
isArrival?: boolean;
isTransit?: boolean;
isDeparture?: boolean;
date: string;
arrivalDateTime?: string | null;
departureDateTime?: string | null;
readonly createdAt?: string | null;
}


+ 6
- 0
angular/src/assets/i18n/en.json Visa fil

@@ -43,6 +43,11 @@
"events": "Events",
"completed": "Completed"
},
"trip_location":
{
"arrival_date_time": "ETA",
"departure_date_time": "ETD"
},
"user_trip":
{
"view": "Pilotage",
@@ -67,6 +72,7 @@
"vessel": "Vessel",
"shipping_company": "Shipping Company",
"trip": "Trip",
"trip_location": "Itinerary location",
"user": "User",
"user_trip": "Pilotage",
"event": "Event"


+ 41
- 0
httpdocs/migrations/Version20250507154018.php Visa fil

@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250507154018 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}

public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE trip DROP customer_reference
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE trip_location ADD arrival_date_time DATETIME DEFAULT NULL COMMENT '(DC2Type:datetime_immutable)', ADD departure_date_time DATETIME DEFAULT NULL COMMENT '(DC2Type:datetime_immutable)', DROP is_arrival, DROP date, DROP is_transit, DROP is_departure
SQL);
}

public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE trip_location ADD is_arrival TINYINT(1) NOT NULL, ADD date DATETIME NOT NULL COMMENT '(DC2Type:datetime_immutable)', ADD is_transit TINYINT(1) NOT NULL, ADD is_departure TINYINT(1) NOT NULL, DROP arrival_date_time, DROP departure_date_time
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE trip ADD customer_reference VARCHAR(255) DEFAULT NULL
SQL);
}
}

+ 0
- 2
httpdocs/src/ApiResource/TripApi.php Visa fil

@@ -81,8 +81,6 @@ class TripApi
#[ApiProperty(writable: false)]
public ?string $pilotageReference = null;

public ?string $customerReference = null;

/**
* @var LocationApi
*/


+ 7
- 12
httpdocs/src/ApiResource/TripLocationApi.php Visa fil

@@ -40,7 +40,7 @@ use Symfony\Component\Validator\Constraints\NotBlank;
security: 'is_granted("ROLE_ADMIN")'
)
],
order: ['date' => 'ASC'],
order: ['arrivalDateTime' => 'ASC', 'departureDateTime' => 'ASC'],
security: 'is_granted("ROLE_USER")',
provider: EntityToDtoStateProvider::class,
processor: EntityClassDtoStateProcessor::class,
@@ -62,9 +62,9 @@ class TripLocationApi
* @var TripApi
*/
#[ApiProperty(
writable: true,
writable: false,
readableLink: true,
writableLink: true,
writableLink: false,
builtinTypes: [
new Type(
'object',
@@ -82,9 +82,9 @@ class TripLocationApi
* @var LocationApi
*/
#[ApiProperty(
writable: true,
writable: false,
readableLink: true,
writableLink: true,
writableLink: false,
builtinTypes: [
new Type(
'object',
@@ -98,14 +98,9 @@ class TripLocationApi
#[ApiProperty(writable: true)]
public ?LocationApi $locationIri = null;

public bool $isArrival = false;
public ?\DateTimeImmutable $arrivalDateTime = null;

public bool $isTransit = false;

public bool $isDeparture = false;

#[Assert\NotBlank]
public \DateTimeImmutable $date;
public ?\DateTimeImmutable $departureDateTime = null;

#[ApiProperty(writable: false)]
public ?\DateTimeImmutable $createdAt = null;

+ 0
- 3
httpdocs/src/Entity/Trip.php Visa fil

@@ -22,9 +22,6 @@ class Trip
#[ORM\JoinColumn(nullable: false)]
private Vessel $vessel;

#[ORM\Column(length: 255, nullable: true)]
private ?string $customerReference = null;

#[ORM\ManyToOne(targetEntity: Location::class)]
#[ORM\JoinColumn(name: 'start_location_id', nullable: false)]
private Location $startLocation;


+ 13
- 41
httpdocs/src/Entity/TripLocation.php Visa fil

@@ -24,26 +24,19 @@ class TripLocation
#[ORM\JoinColumn(nullable: false)]
private Location $location;

#[ORM\Column(nullable: false)]
private bool $isArrival = true;
#[ORM\Column(nullable: true)]
private ?DateTimeImmutable $arrivalDateTime = null;

#[ORM\Column(nullable: false)]
private bool $isTransit = true;

#[ORM\Column(nullable: false)]
private bool $isDeparture = true;

#[ORM\Column]
private DateTimeImmutable $date;
#[ORM\Column(nullable: true)]
private ?DateTimeImmutable $departureDateTime = null;

#[ORM\Column]
private DateTimeImmutable $createdAt;

public function __construct(Trip $trip, Location $location, DateTimeImmutable $date)
public function __construct(Trip $trip, Location $location)
{
$this->trip = $trip;
$this->location = $location;
$this->date = $date;
$this->createdAt = new DateTimeImmutable();
}

@@ -74,45 +67,24 @@ class TripLocation
return $this;
}

public function getDate(): DateTimeImmutable
{
return $this->date;
}

public function setDate(DateTimeImmutable $date): self
{
$this->date = $date;
return $this;
}

public function isArrival(): bool
{
return $this->isArrival;
}

public function setIsArrival(bool $isArrival): void
{
$this->isArrival = $isArrival;
}

public function isTransit(): bool
public function getArrivalDateTime(): ?DateTimeImmutable
{
return $this->isTransit;
return $this->arrivalDateTime;
}

public function setIsTransit(bool $isTransit): void
public function setArrivalDateTime(?DateTimeImmutable $arrivalDateTime): void
{
$this->isTransit = $isTransit;
$this->arrivalDateTime = $arrivalDateTime;
}

public function isDeparture(): bool
public function getDepartureDateTime(): ?DateTimeImmutable
{
return $this->isDeparture;
return $this->departureDateTime;
}

public function setIsDeparture(bool $isDeparture): void
public function setDepartureDateTime(?DateTimeImmutable $departureDateTime): void
{
$this->isDeparture = $isDeparture;
$this->departureDateTime = $departureDateTime;
}

public function getCreatedAt(): DateTimeImmutable


+ 0
- 1
httpdocs/src/Mapper/TripApiToEntityMapper.php Visa fil

@@ -73,7 +73,6 @@ class TripApiToEntityMapper implements MapperInterface
assert($dto instanceof TripApi);
assert($entity instanceof Trip);

$entity->setCustomerReference($dto->customerReference);
$entity->setStartDate($dto->startDate);
$entity->setEndDate($dto->endDate);
$entity->setNote($dto->note);


+ 0
- 1
httpdocs/src/Mapper/TripEntityToApiMapper.php Visa fil

@@ -38,7 +38,6 @@ class TripEntityToApiMapper implements MapperInterface

$dto->dbId = $entity->getId();
$dto->pilotageReference = "P-" . $entity->getId() . "-" . $entity->getStartDate()->format('Y');
$dto->customerReference = $entity->getCustomerReference();
$dto->startDate = $entity->getStartDate();
$dto->endDate = $entity->getEndDate();
$dto->note = $entity->getNote();


+ 3
- 5
httpdocs/src/Mapper/TripLocationApiToEntityMapper.php Visa fil

@@ -49,7 +49,7 @@ class TripLocationApiToEntityMapper implements MapperInterface
throw new \Exception('Location not found');
}

return new TripLocation($trip, $location, $dto->date);
return new TripLocation($trip, $location);
}

public function populate(object $from, object $to, array $context): object
@@ -67,10 +67,8 @@ class TripLocationApiToEntityMapper implements MapperInterface
$entity->setLocation($location);
}

$entity->setIsArrival($dto->isArrival);
$entity->setIsTransit($dto->isTransit);
$entity->setIsDeparture($dto->isDeparture);
$entity->setDate($dto->date);
$entity->setArrivalDateTime($dto->arrivalDateTime);
$entity->setDepartureDateTime($dto->departureDateTime);

return $entity;
}

+ 2
- 4
httpdocs/src/Mapper/TripLocationEntityToApiMapper.php Visa fil

@@ -37,10 +37,8 @@ class TripLocationEntityToApiMapper implements MapperInterface
assert($dto instanceof TripLocationApi);

$dto->dbId = $entity->getId();
$dto->date = $entity->getDate();
$dto->isArrival = $entity->isArrival();
$dto->isTransit = $entity->isTransit();
$dto->isDeparture = $entity->isDeparture();
$dto->arrivalDateTime = $entity->getArrivalDateTime();
$dto->departureDateTime = $entity->getDepartureDateTime();
$dto->createdAt = $entity->getCreatedAt();

$dto->tripIri = $dto->trip = $this->microMapper->map($entity->getTrip(), TripApi::class, [


Laddar…
Avbryt
Spara