| @@ -2,66 +2,68 @@ import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; | |||||
| import {FormBuilder, FormGroup} from "@angular/forms"; | import {FormBuilder, FormGroup} from "@angular/forms"; | ||||
| @Component({ | @Component({ | ||||
| selector: 'app-datetime-picker', | |||||
| templateUrl: './datetime-picker.component.html', | |||||
| styleUrl: './datetime-picker.component.scss' | |||||
| selector: 'app-datetime-picker', | |||||
| templateUrl: './datetime-picker.component.html', | |||||
| styleUrl: './datetime-picker.component.scss' | |||||
| }) | }) | ||||
| export class DatetimePickerComponent implements OnInit { | export class DatetimePickerComponent implements OnInit { | ||||
| @Input() label: string = 'Date and Time'; | |||||
| @Input() inputId: string = 'myId'; | |||||
| @Input() initialValue: string | null = null; | |||||
| @Input() readonly: boolean = false; | |||||
| @Input() showSeconds: boolean = false; | |||||
| @Output() dateTimeChange = new EventEmitter<string | null>(); | |||||
| @Input() label: string = 'Date and Time'; | |||||
| @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; | |||||
| form: FormGroup; | |||||
| constructor(private fb: FormBuilder) { | |||||
| this.form = this.fb.group({ | |||||
| date: [''], | |||||
| time: [''] | |||||
| }); | |||||
| } | |||||
| ngOnInit() { | |||||
| if (this.initialValue) { | |||||
| const date = new Date(this.initialValue); | |||||
| this.form.patchValue({ | |||||
| date: this.formatDate(date), | |||||
| time: this.formatTime(date) | |||||
| }); | |||||
| constructor(private fb: FormBuilder) { | |||||
| this.form = this.fb.group({ | |||||
| date: [''], | |||||
| time: [''] | |||||
| }); | |||||
| } | } | ||||
| if (this.readonly) { | |||||
| this.form.disable(); | |||||
| } | |||||
| ngOnInit() { | |||||
| if (this.initialValue) { | |||||
| // Datum als UTC interpretieren | |||||
| const date = new Date(this.initialValue); | |||||
| this.form.patchValue({ | |||||
| date: this.formatDate(date), | |||||
| time: this.formatTime(date) | |||||
| }); | |||||
| } | |||||
| this.form.valueChanges.subscribe(() => { | |||||
| if (!this.readonly) { | |||||
| this.emitDateTime(); | |||||
| } | |||||
| }); | |||||
| } | |||||
| if (this.readonly) { | |||||
| this.form.disable(); | |||||
| } | |||||
| private formatDate(date: Date): string { | |||||
| return date.toLocaleDateString('en-CA'); | |||||
| } | |||||
| this.form.valueChanges.subscribe(() => { | |||||
| if (!this.readonly) { | |||||
| this.emitDateTime(); | |||||
| } | |||||
| }); | |||||
| } | |||||
| private formatDate(date: Date): string { | |||||
| // ISO String verwenden und nur den Datumsteil extrahieren (YYYY-MM-DD) | |||||
| return date.toISOString().split('T')[0]; | |||||
| } | |||||
| private formatTime(date: Date): string { | private formatTime(date: Date): string { | ||||
| // Zeit aus ISO String extrahieren | |||||
| const timeString = date.toISOString().split('T')[1]; | |||||
| if (this.showSeconds) { | if (this.showSeconds) { | ||||
| return date.toLocaleTimeString('en-GB', { hour12: false }); | |||||
| // Rückgabe mit Sekunden (HH:MM:SS) | |||||
| return timeString.substring(0, 8); | |||||
| } else { | } else { | ||||
| // Nur Stunden und Minuten zurückgeben | |||||
| return date.toLocaleTimeString('en-GB', { | |||||
| hour12: false, | |||||
| hour: '2-digit', | |||||
| minute: '2-digit' | |||||
| }); | |||||
| // Nur Stunden und Minuten zurückgeben (HH:MM) | |||||
| return timeString.substring(0, 5); | |||||
| } | } | ||||
| } | } | ||||
| private emitDateTime() { | private emitDateTime() { | ||||
| const { date, time } = this.form.value; | |||||
| const {date, time} = this.form.value; | |||||
| if (date && time) { | if (date && time) { | ||||
| const [year, month, day] = date.split('-'); | const [year, month, day] = date.split('-'); | ||||
| @@ -74,21 +76,24 @@ export class DatetimePickerComponent implements OnInit { | |||||
| seconds = '00'; | seconds = '00'; | ||||
| } | } | ||||
| // Datum in UTC erstellen | |||||
| const dateTime = new Date( | const dateTime = new Date( | ||||
| Number(year), | |||||
| Number(month) - 1, | |||||
| Number(day), | |||||
| Number(hours), | |||||
| Number(minutes), | |||||
| Number(seconds || 0) | |||||
| Date.UTC( | |||||
| Number(year), | |||||
| Number(month) - 1, | |||||
| Number(day), | |||||
| Number(hours), | |||||
| Number(minutes), | |||||
| Number(seconds || 0) | |||||
| ) | |||||
| ); | ); | ||||
| // Format the date to match the loaded format | |||||
| const formattedDate = dateTime.toLocaleString('sv-SE', { timeZone: 'Europe/Berlin' }).replace(' ', 'T') + '+02:00'; | |||||
| // Als ISO-String formatieren, der automatisch in UTC ist | |||||
| const formattedDate = dateTime.toISOString(); | |||||
| this.dateTimeChange.emit(formattedDate); | this.dateTimeChange.emit(formattedDate); | ||||
| } else { | } else { | ||||
| this.dateTimeChange.emit(null); | this.dateTimeChange.emit(null); | ||||
| } | } | ||||
| } | } | ||||
| } | |||||
| } | |||||
| @@ -405,7 +405,6 @@ export class ListComponent implements OnInit, AfterViewInit, OnDestroy { | |||||
| } | } | ||||
| public onFilterChanged(filterData: {filters: any, activeCount: number}) { | public onFilterChanged(filterData: {filters: any, activeCount: number}) { | ||||
| console.log(filterData); | |||||
| const filterJson = JSON.stringify(filterData.filters); | const filterJson = JSON.stringify(filterData.filters); | ||||
| const currentFilterJson = JSON.stringify(this.filterObj); | const currentFilterJson = JSON.stringify(this.filterObj); | ||||
| @@ -417,6 +416,12 @@ export class ListComponent implements OnInit, AfterViewInit, OnDestroy { | |||||
| } | } | ||||
| public onCreateData() { | public onCreateData() { | ||||
| // Important: Reset the data property to prevent using previous edit data | |||||
| if (this.dataFormComponentData) { | |||||
| this.dataFormComponentData = {...this.dataFormComponentData}; | |||||
| delete this.dataFormComponentData.data; | |||||
| } | |||||
| this.appHelperService.openModal( | this.appHelperService.openModal( | ||||
| this.dataFormComponent, | this.dataFormComponent, | ||||
| this.dataFormComponentData, | this.dataFormComponentData, | ||||
| @@ -425,7 +430,11 @@ export class ListComponent implements OnInit, AfterViewInit, OnDestroy { | |||||
| } | } | ||||
| public onEditData(element: any) { | public onEditData(element: any) { | ||||
| this.dataFormComponentData.data = element; | |||||
| // Create a new object instead of modifying the existing one | |||||
| this.dataFormComponentData = { | |||||
| ...this.dataFormComponentData, | |||||
| data: element | |||||
| }; | |||||
| this.appHelperService.openModal( | this.appHelperService.openModal( | ||||
| this.dataFormComponent, | this.dataFormComponent, | ||||
| this.dataFormComponentData, | this.dataFormComponentData, | ||||
| @@ -5,7 +5,7 @@ | |||||
| </div> | </div> | ||||
| } @else { | } @else { | ||||
| <div class="spt-headline d-flex justify-content-between align-items-start"> | <div class="spt-headline d-flex justify-content-between align-items-start"> | ||||
| <h2>{{ ('basic.edit') | translate }} {{ 'model.trip_location' | translate }}: {{ data?.location?.name }}</h2> | |||||
| <h2>{{ ('basic.edit') | translate }} {{ 'model.trip_location' | translate }}</h2> | |||||
| </div> | </div> | ||||
| } | } | ||||
| <div class="spt-form"> | <div class="spt-form"> | ||||
| @@ -1,4 +1,4 @@ | |||||
| import {Component, Input} from '@angular/core'; | |||||
| import {Component} from '@angular/core'; | |||||
| import {AbstractDataFormComponent} from "@app/_components/_abstract/abstract-data-form-component"; | import {AbstractDataFormComponent} from "@app/_components/_abstract/abstract-data-form-component"; | ||||
| import { | import { | ||||
| LocationService, TripJsonld, | LocationService, TripJsonld, | ||||
| @@ -21,11 +21,13 @@ import {SearchSelectComponent} from "@app/_components/search-select/search-selec | |||||
| }) | }) | ||||
| export class TripLocationFormComponent extends AbstractDataFormComponent<TripLocationJsonld> { | export class TripLocationFormComponent extends AbstractDataFormComponent<TripLocationJsonld> { | ||||
| protected trip!: TripJsonld; | |||||
| protected arrivalDateTime?: string; | |||||
| protected departureDateTime?: string; | |||||
| protected readonly tripLocationForm = tripLocationForm; | |||||
| protected readonly SearchSelectComponent = SearchSelectComponent; | protected readonly SearchSelectComponent = SearchSelectComponent; | ||||
| protected locationColDefinitions: ListColDefinition[]; | protected locationColDefinitions: ListColDefinition[]; | ||||
| protected trip!: TripJsonld; | |||||
| protected newArrivalDateTime?: string; | |||||
| protected newDepartureDateTime?: string; | |||||
| protected tripLocationEntries: TripLocationJsonld[]; | |||||
| constructor( | constructor( | ||||
| private tripLocationService: TripLocationService, | private tripLocationService: TripLocationService, | ||||
| @@ -49,7 +51,7 @@ export class TripLocationFormComponent extends AbstractDataFormComponent<TripLoc | |||||
| translateService, | translateService, | ||||
| router | router | ||||
| ); | ); | ||||
| this.tripLocationEntries = []; | |||||
| this.redirectAfterDelete = '/' + ROUTE_BASE_DATA; | this.redirectAfterDelete = '/' + ROUTE_BASE_DATA; | ||||
| this.locationColDefinitions = SearchSelectComponent.getDefaultColDefLocations(); | this.locationColDefinitions = SearchSelectComponent.getDefaultColDefLocations(); | ||||
| } | } | ||||
| @@ -58,8 +60,11 @@ export class TripLocationFormComponent extends AbstractDataFormComponent<TripLoc | |||||
| super.ngOnInit(); | super.ngOnInit(); | ||||
| this.form.get('tripIri')?.setValue(this.trip.id); | this.form.get('tripIri')?.setValue(this.trip.id); | ||||
| if (!this.isEditMode()) { | if (!this.isEditMode()) { | ||||
| this.form.get('arrivalDateTime')?.setValue(this.arrivalDateTime); | |||||
| this.form.get('departureDateTime')?.setValue(this.departureDateTime); | |||||
| this.form.get('newArrivalDateTime')?.setValue(this.newArrivalDateTime); | |||||
| this.form.get('newDepartureDateTime')?.setValue(this.newDepartureDateTime); | |||||
| } else { | |||||
| this.form.get('newArrivalDateTime')?.setValue(this.data?.arrivalDateTime); | |||||
| this.form.get('newDepartureDateTime')?.setValue(this.data?.departureDateTime); | |||||
| } | } | ||||
| } | } | ||||
| @@ -72,8 +77,115 @@ export class TripLocationFormComponent extends AbstractDataFormComponent<TripLoc | |||||
| } | } | ||||
| override onSubmit() { | override onSubmit() { | ||||
| const formData = this.form.value as TripLocationJsonld; | |||||
| if (this.hasDateOverlap(formData)) { | |||||
| this.translateService?.get('trip_location.overlap_error').subscribe((error: string) => { | |||||
| alert(error); | |||||
| }); | |||||
| return; | |||||
| } | |||||
| super.onSubmit(); | super.onSubmit(); | ||||
| } | } | ||||
| protected readonly tripLocationForm = tripLocationForm; | |||||
| private hasDateOverlap(formData: TripLocationJsonld): boolean { | |||||
| // Wenn kein Datum gesetzt ist, kann keine Überlappung stattfinden | |||||
| if (!formData.arrivalDateTime && !formData.departureDateTime) { | |||||
| return false; | |||||
| } | |||||
| // Wenn wir im Edit-Modus sind, entferne den aktuellen Eintrag aus der Überprüfung | |||||
| const entriesToCheck = this.isEditMode() | |||||
| ? this.tripLocationEntries.filter(entry => entry.id !== this.data?.id) | |||||
| : this.tripLocationEntries; | |||||
| for (const entry of entriesToCheck) { | |||||
| // Fall 1: Nur ein Datum ist gesetzt in formData | |||||
| if (formData.arrivalDateTime && !formData.departureDateTime) { | |||||
| // Arrival date darf nicht mit anderen arrival dates übereinstimmen | |||||
| if (entry.arrivalDateTime === formData.arrivalDateTime) { | |||||
| return true; | |||||
| } | |||||
| // Überprüfe, ob arrival innerhalb eines existierenden Zeitraums liegt | |||||
| // ABER: arrival darf gleich mit einem departure sein | |||||
| if (entry.arrivalDateTime && entry.departureDateTime) { | |||||
| const arrivalDate = new Date(formData.arrivalDateTime); | |||||
| const entryArrival = new Date(entry.arrivalDateTime); | |||||
| const entryDeparture = new Date(entry.departureDateTime); | |||||
| // Liegt innerhalb des Zeitraums, aber ist NICHT gleich dem departure | |||||
| if (arrivalDate > entryArrival && arrivalDate < entryDeparture) { | |||||
| return true; | |||||
| } | |||||
| } | |||||
| } | |||||
| else if (!formData.arrivalDateTime && formData.departureDateTime) { | |||||
| // Departure date darf nicht mit anderen departure dates übereinstimmen | |||||
| if (entry.departureDateTime === formData.departureDateTime) { | |||||
| return true; | |||||
| } | |||||
| // Überprüfe, ob departure innerhalb eines existierenden Zeitraums liegt | |||||
| // ABER: departure darf gleich mit einem arrival sein | |||||
| if (entry.arrivalDateTime && entry.departureDateTime) { | |||||
| const departureDate = new Date(formData.departureDateTime); | |||||
| const entryArrival = new Date(entry.arrivalDateTime); | |||||
| const entryDeparture = new Date(entry.departureDateTime); | |||||
| // Liegt innerhalb des Zeitraums, aber ist NICHT gleich dem arrival | |||||
| if (departureDate > entryArrival && departureDate < entryDeparture) { | |||||
| return true; | |||||
| } | |||||
| } | |||||
| } | |||||
| // Fall 2: Beide Daten sind gesetzt in formData, überprüfe auf Überlappung | |||||
| else if (formData.arrivalDateTime && formData.departureDateTime) { | |||||
| const formArrival = new Date(formData.arrivalDateTime); | |||||
| const formDeparture = new Date(formData.departureDateTime); | |||||
| // Wenn der andere Eintrag beide Daten hat | |||||
| if (entry.arrivalDateTime && entry.departureDateTime) { | |||||
| const entryArrival = new Date(entry.arrivalDateTime); | |||||
| const entryDeparture = new Date(entry.departureDateTime); | |||||
| // Überprüfung auf Überlappung der Zeiträume, erlaubt grenzen zu teilen | |||||
| // Formular-Zeitraum darf vor oder nach dem Entry-Zeitraum liegen, | |||||
| // oder die Grenzen dürfen gleich sein (departure gleich arrival oder umgekehrt) | |||||
| if ( | |||||
| // Überlappung, aber nicht nur am Rand | |||||
| (formArrival < entryDeparture && formDeparture > entryArrival) && | |||||
| // Erlaubt: formDeparture === entryArrival ODER formArrival === entryDeparture | |||||
| !(formDeparture.getTime() === entryArrival.getTime() || | |||||
| formArrival.getTime() === entryDeparture.getTime()) | |||||
| ) { | |||||
| return true; | |||||
| } | |||||
| } | |||||
| // Wenn der andere Eintrag nur arrival hat | |||||
| else if (entry.arrivalDateTime && !entry.departureDateTime) { | |||||
| const entryArrival = new Date(entry.arrivalDateTime); | |||||
| // Das arrival des anderen Eintrags darf nicht innerhalb des neuen Zeitraums liegen | |||||
| // ABER: Es darf am Rand liegen (gleich mit formDeparture) | |||||
| if (entryArrival > formArrival && entryArrival < formDeparture) { | |||||
| return true; | |||||
| } | |||||
| } | |||||
| // Wenn der andere Eintrag nur departure hat | |||||
| else if (!entry.arrivalDateTime && entry.departureDateTime) { | |||||
| const entryDeparture = new Date(entry.departureDateTime); | |||||
| // Das departure des anderen Eintrags darf nicht innerhalb des neuen Zeitraums liegen | |||||
| // ABER: Es darf am Rand liegen (gleich mit formArrival) | |||||
| if (entryDeparture > formArrival && entryDeparture < formDeparture) { | |||||
| return true; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| return false; | |||||
| } | |||||
| } | } | ||||
| @@ -100,7 +100,6 @@ export class TripLocationListComponent { | |||||
| map(response => { | map(response => { | ||||
| // Set default arrival and departure time before return | // Set default arrival and departure time before return | ||||
| let arrivalDateTime = this.appHelperService.getDateTimeWithoutTimezone(); | let arrivalDateTime = this.appHelperService.getDateTimeWithoutTimezone(); | ||||
| console.log(arrivalDateTime); | |||||
| let departureDateTime = this.appHelperService.getDateTimeWithoutTimezone(new Date(new Date().setHours(new Date().getHours() + 1))); | let departureDateTime = this.appHelperService.getDateTimeWithoutTimezone(new Date(new Date().setHours(new Date().getHours() + 1))); | ||||
| if (response.member && response.member.length > 0) { | if (response.member && response.member.length > 0) { | ||||
| const lastEntry = response.member[response.member.length - 1]; | const lastEntry = response.member[response.member.length - 1]; | ||||
| @@ -116,8 +115,9 @@ export class TripLocationListComponent { | |||||
| } | } | ||||
| this.dataFormComponentData = { | this.dataFormComponentData = { | ||||
| trip: this.trip, | trip: this.trip, | ||||
| arrivalDateTime: arrivalDateTime, | |||||
| departureDateTime: departureDateTime | |||||
| newArrivalDateTime: arrivalDateTime, | |||||
| newDepartureDateTime: departureDateTime, | |||||
| tripLocationEntries: response.member | |||||
| }; | }; | ||||
| return response; | return response; | ||||
| @@ -44,7 +44,7 @@ | |||||
| }, | }, | ||||
| "common": { | "common": { | ||||
| "code": "Code", | "code": "Code", | ||||
| "created_at": "Created at", | |||||
| "created_at": "Created at (UTC)", | |||||
| "date": "Date", | "date": "Date", | ||||
| "name": "Name", | "name": "Name", | ||||
| "note": "Note", | "note": "Note", | ||||
| @@ -107,7 +107,8 @@ | |||||
| }, | }, | ||||
| "trip_location": { | "trip_location": { | ||||
| "arrival_date_time": "ETA", | "arrival_date_time": "ETA", | ||||
| "departure_date_time": "ETD" | |||||
| "departure_date_time": "ETD", | |||||
| "overlap_error": "These dates and times overlap with other entries, please check!!" | |||||
| }, | }, | ||||
| "user_trip": { | "user_trip": { | ||||
| "approved": "Approved", | "approved": "Approved", | ||||