ソースを参照

user trip events

master
Daniel 10ヶ月前
コミット
60a4302d94
11個のファイルの変更208行の追加371行の削除
  1. +1
    -2
      angular/src/app/_views/trip/trip-detail/trip-detail.component.html
  2. +1
    -280
      angular/src/app/_views/trip/trip-detail/trip-detail.component.ts
  3. +72
    -0
      angular/src/app/_views/user-trip-event/user-trip-event-form/user-trip-event-form.component.html
  4. +0
    -0
      angular/src/app/_views/user-trip-event/user-trip-event-form/user-trip-event-form.component.scss
  5. +23
    -0
      angular/src/app/_views/user-trip-event/user-trip-event-form/user-trip-event-form.component.spec.ts
  6. +80
    -0
      angular/src/app/_views/user-trip-event/user-trip-event-form/user-trip-event-form.component.ts
  7. +4
    -0
      angular/src/app/_views/user-trip-event/user-trip-event-list/user-trip-event-list.component.html
  8. +18
    -3
      angular/src/app/_views/user-trip-event/user-trip-event-list/user-trip-event-list.component.ts
  9. +5
    -85
      angular/src/app/_views/user-trip/user-trip-detail/user-trip-detail.component.html
  10. +2
    -0
      angular/src/app/app.module.ts
  11. +2
    -1
      angular/src/assets/i18n/en.json

+ 1
- 2
angular/src/app/_views/trip/trip-detail/trip-detail.component.html ファイルの表示

@@ -9,7 +9,6 @@
<div>
<app-trip-form
[data]="trip"
(submit)="onFormUpdate($event)"
></app-trip-form>
</div>
</mat-tab>
@@ -33,10 +32,10 @@
<div>
<app-user-trip-event-list
[trip]="trip"
[showCreateButton]="false"
>
</app-user-trip-event-list>
</div>
</mat-tab>
</mat-tab-group>

}

+ 1
- 280
angular/src/app/_views/trip/trip-detail/trip-detail.component.ts ファイルの表示

@@ -24,34 +24,15 @@ import {TranslateService} from "@ngx-translate/core";
templateUrl: './trip-detail.component.html',
styleUrl: './trip-detail.component.scss'
})
export class TripDetailComponent implements OnInit, AfterViewInit {
export class TripDetailComponent implements OnInit {
protected trip!: TripJsonld;
protected readonly FormMode = FormMode;
protected originalUserTrips: UserTripJsonld[] = [];
protected tripLocations: TripLocationJsonld[] = [];
protected userTrips: UserTripJsonld[] = [];
protected userTripEvents: UserTripEventJsonld[] = [];
protected users: UserJsonld[] = [];
protected locationForms: FormGroup[] = [];
protected locationColDefinitions: ListColDefinition[] = SearchSelectComponent.getDefaultColDefLocations();
protected userForms: FormGroup[] = [];
protected userColDefinitions: ListColDefinition[] = SearchSelectComponent.getDefaultColDefUsers();
protected originalTripLocations: TripLocation[] = [];


@ViewChildren(SearchSelectComponent) searchSelects!: QueryList<SearchSelectComponent>;

constructor(
private tripService: TripService,
private tripLocationService: TripLocationService,
private locationService: LocationService,
private userTripService: UserTripService,
private userTripEventService: UserTripEventService,
private userService: UserService,
protected appHelperService: AppHelperService,
protected translateService: TranslateService,
private route: ActivatedRoute,
private fb: FormBuilder
) {}

ngOnInit() {
@@ -59,268 +40,8 @@ export class TripDetailComponent implements OnInit, AfterViewInit {
this.tripService.tripsIdGet(params['id']).subscribe(
data => {
this.trip = data;
this.loadTripLocations();
this.loadUserTrips();
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();
}
});
});
}

loadTripLocations() {
this.tripLocationService.tripLocationsGetCollection(
1,
200,
this.trip !== undefined ? this.trip.id : undefined,
).subscribe(
data => {
this.tripLocations = data.member;
// Create a form for each trip location
this.locationForms = [];
this.originalTripLocations = JSON.parse(JSON.stringify(this.tripLocations));
this.tripLocations.forEach((tripLocation) => {
this.locationForms.push(this.createLocationForm(tripLocation.location?.id ?? null));
});
}
);
}

createLocationForm(locationIri: string | null): FormGroup {
return this.fb.group({
location: [locationIri]
});
}

createUserForm(): FormGroup {
return this.fb.group({
user: [null]
});
}

getLocations = (page: number, pageSize: number, term?: string): Observable<any> => {
return this.locationService.locationsGetCollection(page, pageSize, term);
}

getUsers = (page: number, pageSize: number, term?: string): Observable<any> => {
// Sammeln Sie alle aktuell ausgewählten Benutzer-IDs (sowohl gespeicherte als auch nicht gespeicherte)
const assignedUserIds: string[] = [];

// IDs aus bestehenden UserTrips (mit gültiger ID)
this.userTrips
.filter(ut => ut.user && ut.user.id)
.forEach(ut => assignedUserIds.push(this.appHelperService.extractId(ut.user?.id!)));

// IDs aus aktuellen Formularwerten (auch für noch nicht gespeicherte Einträge)
this.userForms.forEach(form => {
const userValue = form.get('user')?.value;
if (userValue) {
assignedUserIds.push(this.appHelperService.extractId(userValue));
}
});

// Filtere Benutzer, die bereits ausgewählt sind (gespeichert oder nicht)
return this.userService.usersGetCollection(
page,
pageSize,
undefined,
undefined,
term,
'{"isPilot":true}'
).pipe(
map(response => {
response.member = response.member.filter(user =>
!assignedUserIds.includes(this.appHelperService.extractId(user.id!))
);
return response;
})
);
}

onUserSelectChange(index: number) {
// Erzwingen Sie eine Aktualisierung aller anderen SearchSelect-Komponenten
setTimeout(() => {
if (this.searchSelects) {
this.searchSelects.forEach((component, i) => {
// Überspringen Sie das aktuelle SearchSelect (es muss nicht aktualisiert werden)
if (i !== index) {
component.ngAfterViewInit();
}
});
}
});
}

onFormUpdate(event: FormSubmitEvent<TripJsonld>) {
if (event.status === ModalStatus.Submitted && event.data) {
this.trip = event.data;
}
}

addNewUserTrip() {
// Erstelle ein unvollständiges Objekt (ohne user-Property)
const newUserTrip: UserTripJsonld = {
tripIri: this.trip.id!,
userIri: null,
completed: false,
approved: false,
};

// Füge es als UserTripJsonld hinzu
this.userTrips.push(newUserTrip as UserTripJsonld);
this.userForms.push(this.createUserForm());

// Rest unverändert...
setTimeout(() => {
if (this.searchSelects) {
const lastSelect = this.searchSelects.last;
if (lastSelect) {
lastSelect.ngAfterViewInit();
}
}
});
}

loadUserTrips() {
this.userTripService.userTripsGetCollection(
1,
200,
this.trip !== undefined ? this.trip.id : undefined,
).subscribe({
next: (data) => {
this.userTrips = data.member;
this.originalUserTrips = [...data.member]; // Kopie der ursprünglichen UserTrips speichern

// Formulare für jeden UserTrip erstellen
this.userForms = [];
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);
}
});
}

loadUserTripEvents() {
this.userTripEventService.userTripEventsGetCollection(
1,
200,
undefined,
undefined,
this.trip.dbId!,
).subscribe({
next: (data) => {
this.userTripEvents = data.member;
}
})
}

saveAllUserTrips() {
// 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;
}

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

// 2. Gelöschte UserTrips von der Datenbank entfernen
originalUserTripIds.forEach(id => {
allPromises.push(
firstValueFrom(this.userTripService.userTripsIdDelete(id))
);
});

// 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.');
}
});
}
}

+ 72
- 0
angular/src/app/_views/user-trip-event/user-trip-event-form/user-trip-event-form.component.html ファイルの表示

@@ -0,0 +1,72 @@
<div class="spt-container">
@if (!isEditMode()) {
<div class="spt-headline d-flex justify-content-between align-items-start">
<h2>{{ ('basic.create') | translate }} {{ 'model.event' | translate }}</h2>
</div>
} @else {
<div class="spt-headline d-flex justify-content-between align-items-start">
<h2>{{ ('basic.edit') | translate }} {{ 'model.trip' | translate }}: {{ data?.event?.name }}</h2>
</div>
}
<div class="spt-form">
@if (data !== undefined) {
<form [formGroup]="userTripEventForm" (ngSubmit)="onSubmit()">
<input id="userTripIri" type="hidden" formControlName="userTripIri" required/>
<div class="col-12 mb-3">
<label for="eventIri" class="form-label">{{ 'model.event' | translate }}*:</label>
<app-search-select #eventSearchSelect
[formId]="'eventIri'"
[formLabelLangKey]="'model.event'"
[documentForm]="form"
[getDataFunction]="getEvents"
[displayedDataField]="'name'"
[listColDefinitions]="eventColDefinitions"
[dataSet]="data.event"
>
</app-search-select>
<input id="eventIri" type="hidden" formControlName="eventIri" required/>
</div>
<div class="col-12 mb-3">
<label for="locationIri" class="form-label">{{ 'model.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]="'common.date' | translate"
[inputId]="'startDate'"
[initialValue]="form.get('date')?.value ?? null"
(dateTimeChange)="onDateChange($event, 'date')"
></app-datetime-picker>
</div>
<div class="col-12 col-lg-6 mb-3">
<label for="note" class="form-label">{{ 'common.note' | translate }}:</label>
<textarea class="form-control" id="note" formControlName="note"></textarea>
</div>
</form>
}
</div>

<div class="row">
<div class="col-12 mb-3">
<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_event' | translate }}
</button>
}
</div>
</div>
</div>

+ 0
- 0
angular/src/app/_views/user-trip-event/user-trip-event-form/user-trip-event-form.component.scss ファイルの表示


+ 23
- 0
angular/src/app/_views/user-trip-event/user-trip-event-form/user-trip-event-form.component.spec.ts ファイルの表示

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

import { UserTripEventFormComponent } from './user-trip-event-form.component';

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

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

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

+ 80
- 0
angular/src/app/_views/user-trip-event/user-trip-event-form/user-trip-event-form.component.ts ファイルの表示

@@ -0,0 +1,80 @@
import { Component } from '@angular/core';
import {
EventService, LocationService,
UserTripEventJsonld,
UserTripEventService, UserTripJsonld,
} from "@app/core/api/v1";
import {tripForm, userTripEventForm} from "@app/_forms/apiForms";
import {AbstractDataFormComponent} from "@app/_components/_abstract/abstract-data-form-component";
import {AppHelperService} from "@app/_helpers/app-helper.service";
import {TranslateService} from "@ngx-translate/core";
import {Router} from "@angular/router";
import {ListGetDataFunctionType} from "@app/_components/list/list-get-data-function-type";
import {SearchSelectComponent} from "@app/_components/search-select/search-select.component";
import {ListColDefinition} from "@app/_components/list/list-col-definition";

@Component({
selector: 'app-user-trip-event-form',
templateUrl: './user-trip-event-form.component.html',
styleUrl: './user-trip-event-form.component.scss'
})
export class UserTripEventFormComponent extends AbstractDataFormComponent<UserTripEventJsonld> {

protected readonly SearchSelectComponent = SearchSelectComponent;
protected readonly userTripEventForm = userTripEventForm;
protected userTrip?: UserTripJsonld;
protected eventColDefinitions: ListColDefinition[] = SearchSelectComponent.getDefaultColDefEvents();
protected locationColDefinitions: ListColDefinition[] = SearchSelectComponent.getDefaultColDefLocations();

constructor(
protected userTripEventService: UserTripEventService,
protected eventService: EventService,
protected locationService: LocationService,
appHelperService: AppHelperService,
translateService: TranslateService,
router: Router
) {
super(
userTripEventForm,
appHelperService,
(data: UserTripEventJsonld) => {
return this.userTripEventService.userTripEventsPost(data);
},
(id: string | number, data: UserTripEventJsonld) =>
this.userTripEventService.userTripEventsIdPatch(
id.toString(),
this.appHelperService.convertJsonldToJson(data)
),
(id: string | number) => this.userTripEventService.userTripEventsIdDelete(id.toString()),
translateService,
router
);

//this.redirectAfterDelete = '/' + ROUTE_USER_TRIPS;
}

override ngOnInit() {
super.ngOnInit();
this.form.get('userTripIri')?.setValue(this.userTrip?.id!);
}

getEvents: ListGetDataFunctionType = (
index: number,
pageSize: number,
term?: string,
) => {
return this.eventService.eventsGetCollection(
index,
pageSize,
term
);
}

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

+ 4
- 0
angular/src/app/_views/user-trip-event/user-trip-event-list/user-trip-event-list.component.html ファイルの表示

@@ -2,6 +2,10 @@
<app-list #listComponent
[listId]="'userTripEventList'"
[getDataFunction]="getData"
[showCreateButton]="showCreateButton"
[listColDefinitions]="listColDefinitions"
[dataFormComponent]="userTripEventFormComponent"
[dataFormComponentData]="dataFormComponentData"
[deleteItemFunction]="deleteItemFunction"
></app-list>
</div>

+ 18
- 3
angular/src/app/_views/user-trip-event/user-trip-event-list/user-trip-event-list.component.ts ファイルの表示

@@ -1,10 +1,14 @@
import {Component, Input, ViewChild} from '@angular/core';
import {ListComponent} from "@app/_components/list/list.component";
import {ListColDefinition} from "@app/_components/list/list-col-definition";
import {TripJsonld, UserTripEventService} from "@app/core/api/v1";
import {TripJsonld, UserTripEventService, UserTripJsonld} 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 {
UserTripEventFormComponent
} from "@app/_views/user-trip-event/user-trip-event-form/user-trip-event-form.component";
import {Observable} from "rxjs";

@Component({
selector: 'app-user-trip-event-list',
@@ -13,9 +17,13 @@ import {ListGetDataFunctionType} from "@app/_components/list/list-get-data-funct
})
export class UserTripEventListComponent {
@ViewChild("listComponent", {static: false}) listComponent!: ListComponent;
@Input() public userTrip?: UserTripJsonld;
@Input() public trip?: TripJsonld;
@Input() public showCreateButton: boolean;

protected readonly userTripEventFormComponent = UserTripEventFormComponent;
protected listColDefinitions!: ListColDefinition[];
protected dataFormComponentData: any;

constructor(
private userTripEventService: UserTripEventService,
@@ -75,10 +83,13 @@ export class UserTripEventListComponent {
filterType: FilterBarComponent.FILTER_TYPE_DATE,
} as ListColDefinition,
];
this.showCreateButton = true;
}

ngOnInit() {

this.dataFormComponentData = {
userTrip: this.userTrip,
};
}

ngAfterViewInit(): void {
@@ -93,11 +104,15 @@ export class UserTripEventListComponent {
return this.userTripEventService.userTripEventsGetCollection(
index,
pageSize,
undefined,
this.userTrip !== undefined ? this.userTrip.id! : undefined,
undefined,
this.trip !== undefined ? this.trip.dbId! : undefined,
this.listComponent.getFilterJsonString(),
this.listComponent.getSortingJsonString()
);
}

get deleteItemFunction(): (id: string) => Observable<any> {
return (id: string) => this.userTripEventService.userTripEventsIdDelete(id);
}
}

+ 5
- 85
angular/src/app/_views/user-trip/user-trip-detail/user-trip-detail.component.html ファイルの表示

@@ -15,91 +15,11 @@
</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="p-2 mb-2 changing-list">
<div class="row">
<div class="col-12 col-md-6 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-6 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-3 mb-1">
<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-3 mb-1">
<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-4 mb-1">
<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 class="col-12 col-md-2 mb-1 d-flex align-items-end">
<button type="button" class="btn btn-danger" (click)="removeUserTripEvent(i)">X</button>
</div>
</div>
</div>

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

<div class="row">
<div class="col-12 col-lg-6 mb-3">
<button type="button" class="btn btn-primary" (click)="saveAllUserTripEvents()">
{{ 'basic.save' | translate }}
</button>
</div>
</div>
<app-user-trip-event-list
[userTrip]="userTrip"
[showCreateButton]="true"
>
</app-user-trip-event-list>
</div>
</mat-tab>
</mat-tab-group>

+ 2
- 0
angular/src/app/app.module.ts ファイルの表示

@@ -73,6 +73,7 @@ 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';
import { UserTripEventFormComponent } from './_views/user-trip-event/user-trip-event-form/user-trip-event-form.component';

registerLocaleData(localeDe, 'de-DE');

@@ -171,6 +172,7 @@ export function HttpLoaderFactory(http: HttpClient) {
ImageUploadComponent,
TripLocationListComponent,
TripLocationFormComponent,
UserTripEventFormComponent,
],
providers: [
{provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true},


+ 2
- 1
angular/src/assets/i18n/en.json ファイルの表示

@@ -75,6 +75,7 @@
"trip_location": "Itinerary location",
"user": "User",
"user_trip": "Pilotage",
"user_trip_event": "Pilotage event",
"vessel": "Vessel",
"zone": "Zone"
},
@@ -89,7 +90,7 @@
"customer_reference": "Customer reference",
"end_date": "End date",
"end_location": "End location",
"events": "Events",
"events": "Events (accumulated)",
"is_arrival": "Arrival",
"is_departure": "Departure",
"is_transit": "Transit",


読み込み中…
キャンセル
保存