feat: Add route editing functionality (#20)
fix(formations): Don't overwrite existing formations in local storage
This commit is contained in:
parent
7e44fed534
commit
7a583d81a6
|
@ -197,10 +197,15 @@
|
||||||
},
|
},
|
||||||
"cli": {
|
"cli": {
|
||||||
"schematicCollections": [
|
"schematicCollections": [
|
||||||
|
"@schematics/angular",
|
||||||
"@ionic/angular-toolkit"
|
"@ionic/angular-toolkit"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"schematics": {
|
"schematics": {
|
||||||
|
"@schematics/angular:component": {
|
||||||
|
"standalone": true,
|
||||||
|
"style": "scss"
|
||||||
|
},
|
||||||
"@ionic/angular-toolkit:component": {
|
"@ionic/angular-toolkit:component": {
|
||||||
"styleext": "scss"
|
"styleext": "scss"
|
||||||
},
|
},
|
||||||
|
|
|
@ -45,6 +45,7 @@
|
||||||
"@angular/localize": "^17.0.2",
|
"@angular/localize": "^17.0.2",
|
||||||
"@capacitor/cli": "5.5.1",
|
"@capacitor/cli": "5.5.1",
|
||||||
"@ionic/angular-toolkit": "^9.0.0",
|
"@ionic/angular-toolkit": "^9.0.0",
|
||||||
|
"@schematics/angular": "^17.0.5",
|
||||||
"@types/jasmine": "~4.3.0",
|
"@types/jasmine": "~4.3.0",
|
||||||
"@types/node": "^12.11.1",
|
"@types/node": "^12.11.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
||||||
|
@ -1317,6 +1318,22 @@
|
||||||
"yarn": ">= 1.13.0"
|
"yarn": ">= 1.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@angular/cli/node_modules/@schematics/angular": {
|
||||||
|
"version": "17.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.0.0.tgz",
|
||||||
|
"integrity": "sha512-9jKU5x/WzaBsfSkUowK1X74FqtMXa6+A60XgW4ACO8i6fwKfPeS+tIrAieeYOX80/njBh7I5CvcpHmWA2SbcXQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@angular-devkit/core": "17.0.0",
|
||||||
|
"@angular-devkit/schematics": "17.0.0",
|
||||||
|
"jsonc-parser": "3.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.13.0 || >=20.9.0",
|
||||||
|
"npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
|
||||||
|
"yarn": ">= 1.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@angular/common": {
|
"node_modules/@angular/common": {
|
||||||
"version": "17.0.2",
|
"version": "17.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@angular/common/-/common-17.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/common/-/common-17.0.2.tgz",
|
||||||
|
@ -4562,13 +4579,13 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@schematics/angular": {
|
"node_modules/@schematics/angular": {
|
||||||
"version": "17.0.0",
|
"version": "17.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.0.5.tgz",
|
||||||
"integrity": "sha512-9jKU5x/WzaBsfSkUowK1X74FqtMXa6+A60XgW4ACO8i6fwKfPeS+tIrAieeYOX80/njBh7I5CvcpHmWA2SbcXQ==",
|
"integrity": "sha512-sOc1UG4NiV+7cGwrbWPnyW71O+NgsKaFb2agSrVduRL7o4neMDeqF04ik4Kv1jKA7sZOQfPV+3cn6XI49Mumrw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular-devkit/core": "17.0.0",
|
"@angular-devkit/core": "17.0.5",
|
||||||
"@angular-devkit/schematics": "17.0.0",
|
"@angular-devkit/schematics": "17.0.5",
|
||||||
"jsonc-parser": "3.2.0"
|
"jsonc-parser": "3.2.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
@ -4577,6 +4594,63 @@
|
||||||
"yarn": ">= 1.13.0"
|
"yarn": ">= 1.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@schematics/angular/node_modules/@angular-devkit/core": {
|
||||||
|
"version": "17.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.0.5.tgz",
|
||||||
|
"integrity": "sha512-e1evgRabAfOZBnmFCe8E0oufcu+FzBe5hBzS94Dm42GlxdX965/M4yVKQxIMpjivQTmjl+AWb6cF1ltBdSGZeQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"ajv": "8.12.0",
|
||||||
|
"ajv-formats": "2.1.1",
|
||||||
|
"jsonc-parser": "3.2.0",
|
||||||
|
"picomatch": "3.0.1",
|
||||||
|
"rxjs": "7.8.1",
|
||||||
|
"source-map": "0.7.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.13.0 || >=20.9.0",
|
||||||
|
"npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
|
||||||
|
"yarn": ">= 1.13.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"chokidar": "^3.5.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"chokidar": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@schematics/angular/node_modules/@angular-devkit/schematics": {
|
||||||
|
"version": "17.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.0.5.tgz",
|
||||||
|
"integrity": "sha512-KYPku0qTb8B+TtRbFqXGYpJOPg1k6d5bNHV6n8jTc35mlEUUghOd7HkovdfkQ3cgGNQM56a74D1CvSeruZEGsA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@angular-devkit/core": "17.0.5",
|
||||||
|
"jsonc-parser": "3.2.0",
|
||||||
|
"magic-string": "0.30.5",
|
||||||
|
"ora": "5.4.1",
|
||||||
|
"rxjs": "7.8.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.13.0 || >=20.9.0",
|
||||||
|
"npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
|
||||||
|
"yarn": ">= 1.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@schematics/angular/node_modules/picomatch": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@sigstore/bundle": {
|
"node_modules/@sigstore/bundle": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-2.1.0.tgz",
|
||||||
|
@ -18261,6 +18335,19 @@
|
||||||
"semver": "7.5.4",
|
"semver": "7.5.4",
|
||||||
"symbol-observable": "4.0.0",
|
"symbol-observable": "4.0.0",
|
||||||
"yargs": "17.7.2"
|
"yargs": "17.7.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@schematics/angular": {
|
||||||
|
"version": "17.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.0.0.tgz",
|
||||||
|
"integrity": "sha512-9jKU5x/WzaBsfSkUowK1X74FqtMXa6+A60XgW4ACO8i6fwKfPeS+tIrAieeYOX80/njBh7I5CvcpHmWA2SbcXQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@angular-devkit/core": "17.0.0",
|
||||||
|
"@angular-devkit/schematics": "17.0.0",
|
||||||
|
"jsonc-parser": "3.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@angular/common": {
|
"@angular/common": {
|
||||||
|
@ -20503,14 +20590,49 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@schematics/angular": {
|
"@schematics/angular": {
|
||||||
"version": "17.0.0",
|
"version": "17.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.0.5.tgz",
|
||||||
"integrity": "sha512-9jKU5x/WzaBsfSkUowK1X74FqtMXa6+A60XgW4ACO8i6fwKfPeS+tIrAieeYOX80/njBh7I5CvcpHmWA2SbcXQ==",
|
"integrity": "sha512-sOc1UG4NiV+7cGwrbWPnyW71O+NgsKaFb2agSrVduRL7o4neMDeqF04ik4Kv1jKA7sZOQfPV+3cn6XI49Mumrw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@angular-devkit/core": "17.0.0",
|
"@angular-devkit/core": "17.0.5",
|
||||||
"@angular-devkit/schematics": "17.0.0",
|
"@angular-devkit/schematics": "17.0.5",
|
||||||
"jsonc-parser": "3.2.0"
|
"jsonc-parser": "3.2.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@angular-devkit/core": {
|
||||||
|
"version": "17.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.0.5.tgz",
|
||||||
|
"integrity": "sha512-e1evgRabAfOZBnmFCe8E0oufcu+FzBe5hBzS94Dm42GlxdX965/M4yVKQxIMpjivQTmjl+AWb6cF1ltBdSGZeQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"ajv": "8.12.0",
|
||||||
|
"ajv-formats": "2.1.1",
|
||||||
|
"jsonc-parser": "3.2.0",
|
||||||
|
"picomatch": "3.0.1",
|
||||||
|
"rxjs": "7.8.1",
|
||||||
|
"source-map": "0.7.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@angular-devkit/schematics": {
|
||||||
|
"version": "17.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.0.5.tgz",
|
||||||
|
"integrity": "sha512-KYPku0qTb8B+TtRbFqXGYpJOPg1k6d5bNHV6n8jTc35mlEUUghOd7HkovdfkQ3cgGNQM56a74D1CvSeruZEGsA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@angular-devkit/core": "17.0.5",
|
||||||
|
"jsonc-parser": "3.2.0",
|
||||||
|
"magic-string": "0.30.5",
|
||||||
|
"ora": "5.4.1",
|
||||||
|
"rxjs": "7.8.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"picomatch": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@sigstore/bundle": {
|
"@sigstore/bundle": {
|
||||||
|
|
|
@ -67,6 +67,7 @@
|
||||||
"@angular/localize": "^17.0.2",
|
"@angular/localize": "^17.0.2",
|
||||||
"@capacitor/cli": "5.5.1",
|
"@capacitor/cli": "5.5.1",
|
||||||
"@ionic/angular-toolkit": "^9.0.0",
|
"@ionic/angular-toolkit": "^9.0.0",
|
||||||
|
"@schematics/angular": "^17.0.5",
|
||||||
"@types/jasmine": "~4.3.0",
|
"@types/jasmine": "~4.3.0",
|
||||||
"@types/node": "^12.11.1",
|
"@types/node": "^12.11.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
||||||
|
|
|
@ -6,6 +6,8 @@ import {provideEffects} from "@ngrx/effects";
|
||||||
import {formationsReducer} from "./formations/store/formations.reducer";
|
import {formationsReducer} from "./formations/store/formations.reducer";
|
||||||
import {subscriptionReducer} from "./subscription/store/subscription.reducer";
|
import {subscriptionReducer} from "./subscription/store/subscription.reducer";
|
||||||
import {subscriptionEffects, subscriptionFeature} from "./subscription/store";
|
import {subscriptionEffects, subscriptionFeature} from "./subscription/store";
|
||||||
|
import {routesReducer} from "./routes/store/routes.reducer";
|
||||||
|
import {featureStateName as routesFeature, routesEffects} from "./routes/store";
|
||||||
|
|
||||||
export const ROOT_ROUTES: Routes = [
|
export const ROOT_ROUTES: Routes = [
|
||||||
{
|
{
|
||||||
|
@ -46,7 +48,12 @@ export const ROOT_ROUTES: Routes = [
|
||||||
{
|
{
|
||||||
path: 'routes',
|
path: 'routes',
|
||||||
loadComponent: () => import("./routes/routes.component").then(mod => mod.RoutesComponent),
|
loadComponent: () => import("./routes/routes.component").then(mod => mod.RoutesComponent),
|
||||||
canActivate: [AppAuthGuard]
|
canActivate: [AppAuthGuard],
|
||||||
|
providers: [
|
||||||
|
provideState(routesFeature, routesReducer),
|
||||||
|
provideState(formationsFeature, formationsReducer),
|
||||||
|
provideEffects(routesEffects, formationsEffects)
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'timetables',
|
path: 'timetables',
|
||||||
|
@ -68,7 +75,8 @@ export const ROOT_ROUTES: Routes = [
|
||||||
canActivate: [AppAuthGuard],
|
canActivate: [AppAuthGuard],
|
||||||
providers: [
|
providers: [
|
||||||
provideState(formationsFeature, formationsReducer),
|
provideState(formationsFeature, formationsReducer),
|
||||||
provideEffects(formationsEffects)
|
provideState(routesFeature, routesReducer),
|
||||||
|
provideEffects(formationsEffects, routesEffects)
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -9,7 +9,12 @@
|
||||||
<ion-content [fullscreen]="true">
|
<ion-content [fullscreen]="true">
|
||||||
<ion-list>
|
<ion-list>
|
||||||
<ion-list-header>
|
<ion-list-header>
|
||||||
<ion-label i18n>Routes</ion-label>
|
<ion-label>
|
||||||
|
<span i18n>Routes</span>
|
||||||
|
@if (!(hasPersonalPlan$ | async)) {
|
||||||
|
({{ (routes$ | async)?.length }} / 1)
|
||||||
|
}
|
||||||
|
</ion-label>
|
||||||
</ion-list-header>
|
</ion-list-header>
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-label i18n class="bold">Name</ion-label>
|
<ion-label i18n class="bold">Name</ion-label>
|
||||||
|
@ -18,7 +23,7 @@
|
||||||
<ion-label i18n class="bold">Last Station</ion-label>
|
<ion-label i18n class="bold">Last Station</ion-label>
|
||||||
<ion-label i18n class="bold ion-text-end"># Stations</ion-label>
|
<ion-label i18n class="bold ion-text-end"># Stations</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item-sliding *ngFor="let route of routes">
|
<ion-item-sliding *ngFor="let route of routes$ | async">
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-label>{{ route.name }}</ion-label>
|
<ion-label>{{ route.name }}</ion-label>
|
||||||
<ion-label>{{ route.country.name }}</ion-label>
|
<ion-label>{{ route.country.name }}</ion-label>
|
||||||
|
@ -27,7 +32,11 @@
|
||||||
<ion-label class="ion-text-end">{{ route.numberOfStations }}</ion-label>
|
<ion-label class="ion-text-end">{{ route.numberOfStations }}</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item-options side="end">
|
<ion-item-options side="end">
|
||||||
<ion-item-option color="danger" i18n>
|
<ion-item-option (click)="updateRoute(route)" color="secondary" i18n>
|
||||||
|
<ion-icon slot="start" [ios]="'pencil-outline'" [md]="'pencil-sharp'"></ion-icon>
|
||||||
|
Update
|
||||||
|
</ion-item-option>
|
||||||
|
<ion-item-option (click)="deleteRoute(route)" color="danger" i18n>
|
||||||
<ion-icon slot="start" [ios]="'trash-outline'" [md]="'trash-sharp'"></ion-icon>
|
<ion-icon slot="start" [ios]="'trash-outline'" [md]="'trash-sharp'"></ion-icon>
|
||||||
Delete
|
Delete
|
||||||
</ion-item-option>
|
</ion-item-option>
|
||||||
|
@ -99,14 +108,19 @@
|
||||||
<ion-fab-button i18n-aria-label aria-label="Add timetable" [show]="true">
|
<ion-fab-button i18n-aria-label aria-label="Add timetable" [show]="true">
|
||||||
<ion-icon [ios]="'time-outline'" [md]="'time-sharp'"></ion-icon>
|
<ion-icon [ios]="'time-outline'" [md]="'time-sharp'"></ion-icon>
|
||||||
</ion-fab-button>
|
</ion-fab-button>
|
||||||
<ion-fab-button i18n-aria-label aria-label="Add route" [show]="true">
|
@if ((routes$ | async)?.length == 0 || (hasPersonalPlan$ | async)) {
|
||||||
<ion-icon [ios]="'map-outline'" [md]="'map-sharp'"></ion-icon>
|
<ion-fab-button i18n-aria-label aria-label="Add route" [show]="true" (click)="addRoute()">
|
||||||
</ion-fab-button>
|
<ion-icon [ios]="'map-outline'" [md]="'map-sharp'"></ion-icon>
|
||||||
|
</ion-fab-button>
|
||||||
|
}
|
||||||
</ion-fab-list>
|
</ion-fab-list>
|
||||||
</ion-fab>
|
</ion-fab>
|
||||||
<app-create-formation [isOpen]="isCreateFormationModalOpen"
|
<app-create-formation [isOpen]="isCreateFormationModalOpen"
|
||||||
(dismissed)="isCreateFormationModalOpen = false"></app-create-formation>
|
(dismissed)="isCreateFormationModalOpen = false"></app-create-formation>
|
||||||
<app-update-formation [isOpen]="isUpdateFormationModalOpen" [updatedFormation]="updatedFormation!"
|
<app-update-formation [isOpen]="isUpdateFormationModalOpen" [updatedFormation]="updatedFormation!"
|
||||||
(dismissed)="isUpdateFormationModalOpen = false"></app-update-formation>
|
(dismissed)="isUpdateFormationModalOpen = false"></app-update-formation>
|
||||||
|
<app-create-route [isOpen]="isCreateRouteModalOpen" (dismissed)="isCreateRouteModalOpen = false"></app-create-route>
|
||||||
|
<app-update-route [isOpen]="isUpdateRouteModalOpen" [updatedRoute]="updatedRoute"
|
||||||
|
(dismissed)="isUpdateRouteModalOpen = false"></app-update-route>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
<ion-footer id="footer"></ion-footer>
|
<ion-footer id="footer"></ion-footer>
|
||||||
|
|
|
@ -40,7 +40,7 @@ import {
|
||||||
trashOutline,
|
trashOutline,
|
||||||
trashSharp
|
trashSharp
|
||||||
} from "ionicons/icons";
|
} from "ionicons/icons";
|
||||||
import {Route} from "../routes/model/route";
|
import {DEFAULT_ROUTE, Route} from "../routes/model/route";
|
||||||
import {Timetable} from "../timetables/model/timetable";
|
import {Timetable} from "../timetables/model/timetable";
|
||||||
import {FormationsStoreService} from "../formations/service/formations-store.service";
|
import {FormationsStoreService} from "../formations/service/formations-store.service";
|
||||||
import {DEFAULT_FORMATION, Formation} from "../formations/model/formation";
|
import {DEFAULT_FORMATION, Formation} from "../formations/model/formation";
|
||||||
|
@ -53,6 +53,12 @@ import {ActivatedRoute, EventType, NavigationEnd, Router} from "@angular/router"
|
||||||
import {addMessageAction} from "../messages/store/messages.actions";
|
import {addMessageAction} from "../messages/store/messages.actions";
|
||||||
import {Message} from "../messages/model/message";
|
import {Message} from "../messages/model/message";
|
||||||
import {MessagesState} from "../messages/store/messages.reducer";
|
import {MessagesState} from "../messages/store/messages.reducer";
|
||||||
|
import {CreateRouteComponent} from "../routes/create-route/create-route.component";
|
||||||
|
import {UpdateRouteComponent} from "../routes/update-route/update-route.component";
|
||||||
|
import {RoutesStoreService} from "../routes/service/routes-store.service";
|
||||||
|
import {RoutesState} from "../routes/store/routes.reducer";
|
||||||
|
import {deleteRouteAction} from "../routes/store/routes.actions";
|
||||||
|
import {AuthService} from "../auth/service/auth.service";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-dashboard',
|
selector: 'app-dashboard',
|
||||||
|
@ -85,19 +91,12 @@ import {MessagesState} from "../messages/store/messages.reducer";
|
||||||
IonFabList,
|
IonFabList,
|
||||||
IonFooter,
|
IonFooter,
|
||||||
CreateFormationComponent,
|
CreateFormationComponent,
|
||||||
UpdateFormationComponent
|
UpdateFormationComponent,
|
||||||
|
CreateRouteComponent,
|
||||||
|
UpdateRouteComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class DashboardComponent implements OnDestroy {
|
export class DashboardComponent implements OnDestroy {
|
||||||
routes: Route[] = [
|
|
||||||
{
|
|
||||||
name: 'Köln-Aachen',
|
|
||||||
country: {name: $localize`Germany`, code: 'de'},
|
|
||||||
firstStation: {name: 'Köln Hbf'},
|
|
||||||
lastStation: {name: 'Aachen Hbf'},
|
|
||||||
numberOfStations: 30
|
|
||||||
}
|
|
||||||
];
|
|
||||||
timetables: Timetable[] = [];
|
timetables: Timetable[] = [];
|
||||||
|
|
||||||
isCreateFormationModalOpen = false;
|
isCreateFormationModalOpen = false;
|
||||||
|
@ -106,6 +105,19 @@ export class DashboardComponent implements OnDestroy {
|
||||||
private readonly formationsStoreService: FormationsStoreService = inject(FormationsStoreService);
|
private readonly formationsStoreService: FormationsStoreService = inject(FormationsStoreService);
|
||||||
formations$ = this.formationsStoreService.getFormations$();
|
formations$ = this.formationsStoreService.getFormations$();
|
||||||
|
|
||||||
|
isCreateRouteModalOpen = false;
|
||||||
|
isUpdateRouteModalOpen = false;
|
||||||
|
updatedRoute: Route = DEFAULT_ROUTE;
|
||||||
|
private readonly routesStoreService: RoutesStoreService = inject(RoutesStoreService);
|
||||||
|
routes$ = this.routesStoreService.getRoutes$();
|
||||||
|
|
||||||
|
private readonly authService: AuthService = inject(AuthService);
|
||||||
|
readonly user$ = this.authService.getUser$();
|
||||||
|
readonly hasPersonalPlan$ = this.user$.pipe(
|
||||||
|
map(user => user.roles),
|
||||||
|
filter(roles => roles.includes('PERSONAL_PLAN'))
|
||||||
|
);
|
||||||
|
|
||||||
private messages: Record<string, Message> = {
|
private messages: Record<string, Message> = {
|
||||||
success: {
|
success: {
|
||||||
text: $localize`You have successfully subscribed. The subscription can be managed from the account settings.`,
|
text: $localize`You have successfully subscribed. The subscription can be managed from the account settings.`,
|
||||||
|
@ -117,6 +129,7 @@ export class DashboardComponent implements OnDestroy {
|
||||||
private subscription: Subscription;
|
private subscription: Subscription;
|
||||||
|
|
||||||
constructor(private readonly formationsStore: Store<FormationsState>,
|
constructor(private readonly formationsStore: Store<FormationsState>,
|
||||||
|
private readonly routesStore: Store<RoutesState>,
|
||||||
private readonly messagesStore: Store<MessagesState>,
|
private readonly messagesStore: Store<MessagesState>,
|
||||||
private readonly activatedRoute: ActivatedRoute,
|
private readonly activatedRoute: ActivatedRoute,
|
||||||
private readonly router: Router) {
|
private readonly router: Router) {
|
||||||
|
@ -160,6 +173,19 @@ export class DashboardComponent implements OnDestroy {
|
||||||
this.formationsStore.dispatch(deleteFormationAction({payload: formation}));
|
this.formationsStore.dispatch(deleteFormationAction({payload: formation}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addRoute() {
|
||||||
|
this.isCreateRouteModalOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRoute(route: Route) {
|
||||||
|
this.isUpdateRouteModalOpen = true;
|
||||||
|
this.updatedRoute = route;
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteRoute(route: Route) {
|
||||||
|
this.routesStore.dispatch(deleteRouteAction({payload: route}));
|
||||||
|
}
|
||||||
|
|
||||||
private triggerFeedbackMessage() {
|
private triggerFeedbackMessage() {
|
||||||
const state = this.activatedRoute.snapshot.queryParamMap.get('state') || '';
|
const state = this.activatedRoute.snapshot.queryParamMap.get('state') || '';
|
||||||
if (state in this.messages) {
|
if (state in this.messages) {
|
||||||
|
|
|
@ -61,7 +61,7 @@ import {FormationsState} from "./store/formations.reducer";
|
||||||
export class FormationsComponent {
|
export class FormationsComponent {
|
||||||
isCreateModalOpen = false;
|
isCreateModalOpen = false;
|
||||||
isUpdateModalOpen = false;
|
isUpdateModalOpen = false;
|
||||||
updatedFormation: Formation = DEFAULT_FORMATION;
|
updatedFormation: Formation = {...DEFAULT_FORMATION};
|
||||||
|
|
||||||
private readonly storeService: FormationsStoreService = inject(FormationsStoreService);
|
private readonly storeService: FormationsStoreService = inject(FormationsStoreService);
|
||||||
formations$ = this.storeService.getFormations$();
|
formations$ = this.storeService.getFormations$();
|
||||||
|
@ -82,8 +82,8 @@ export class FormationsComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFormation(formation: Formation) {
|
updateFormation(formation: Formation) {
|
||||||
this.isUpdateModalOpen = true;
|
|
||||||
this.updatedFormation = formation;
|
this.updatedFormation = formation;
|
||||||
|
this.isUpdateModalOpen = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteFormation(formation: Formation) {
|
deleteFormation(formation: Formation) {
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
export interface Formation {
|
import {Item} from "../../typeahead/item";
|
||||||
|
|
||||||
|
export interface Formation extends Item {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
trainSimWorldFormation?: Formation;
|
trainSimWorldFormation?: Formation;
|
||||||
|
|
|
@ -25,6 +25,7 @@ export class FormationsService {
|
||||||
fetchFormations(): Observable<Formation[]> {
|
fetchFormations(): Observable<Formation[]> {
|
||||||
if (environment.mockNetwork) {
|
if (environment.mockNetwork) {
|
||||||
this.formations = JSON.parse(localStorage.getItem("formations") || '[]');
|
this.formations = JSON.parse(localStorage.getItem("formations") || '[]');
|
||||||
|
this.formations.forEach(formation => this.knownFormations.set(formation.id, formation));
|
||||||
return of(this.formations);
|
return of(this.formations);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -48,10 +48,10 @@ import {FormationsState} from "../store/formations.reducer";
|
||||||
export class UpdateFormationComponent {
|
export class UpdateFormationComponent {
|
||||||
@ViewChild(IonModal) modal: IonModal | undefined;
|
@ViewChild(IonModal) modal: IonModal | undefined;
|
||||||
|
|
||||||
@Input({required: true}) isOpen: boolean = false;
|
|
||||||
@Output() dismissed: EventEmitter<boolean> = new EventEmitter<boolean>();
|
@Output() dismissed: EventEmitter<boolean> = new EventEmitter<boolean>();
|
||||||
|
|
||||||
formation: Formation = {...DEFAULT_FORMATION};
|
formation: Formation = {...DEFAULT_FORMATION};
|
||||||
|
formationOnOpen: Formation = {...this.formation};
|
||||||
|
|
||||||
private readonly store: Store<FormationsState> = inject(Store<FormationsState>);
|
private readonly store: Store<FormationsState> = inject(Store<FormationsState>);
|
||||||
private readonly storeService: FormationsStoreService = inject(FormationsStoreService);
|
private readonly storeService: FormationsStoreService = inject(FormationsStoreService);
|
||||||
|
@ -60,13 +60,26 @@ export class UpdateFormationComponent {
|
||||||
constructor() {
|
constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _isOpen = false;
|
||||||
|
@Input({required: true}) set isOpen(newValue: boolean) {
|
||||||
|
if (!this._isOpen && newValue) {
|
||||||
|
this.formationOnOpen = {...this.formation};
|
||||||
|
}
|
||||||
|
this._isOpen = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isOpen() {
|
||||||
|
return this._isOpen;
|
||||||
|
}
|
||||||
|
|
||||||
@Input({required: true}) set updatedFormation(newValue: Formation) {
|
@Input({required: true}) set updatedFormation(newValue: Formation) {
|
||||||
this.formation = {...newValue};
|
this.formation = {...newValue};
|
||||||
|
this.formationOnOpen = {...newValue};
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel() {
|
cancel() {
|
||||||
this.dismissed.emit(true);
|
this.dismissed.emit(true);
|
||||||
this.formation = {...DEFAULT_FORMATION};
|
this.formation = {...this.formationOnOpen};
|
||||||
}
|
}
|
||||||
|
|
||||||
confirm() {
|
confirm() {
|
||||||
|
@ -74,7 +87,6 @@ export class UpdateFormationComponent {
|
||||||
payload: {...this.formation}
|
payload: {...this.formation}
|
||||||
}))
|
}))
|
||||||
this.dismissed.emit(true);
|
this.dismissed.emit(true);
|
||||||
this.formation = {...DEFAULT_FORMATION};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
compareWith(formation1: Formation, formation2: Formation) {
|
compareWith(formation1: Formation, formation2: Formation) {
|
||||||
|
|
|
@ -0,0 +1,116 @@
|
||||||
|
import {Station} from "../model/station";
|
||||||
|
import {InputCustomEvent, SelectCustomEvent} from "@ionic/angular";
|
||||||
|
import {Formation} from "../../formations/model/formation";
|
||||||
|
import {TravelDuration} from "../model/travel-duration";
|
||||||
|
import {DEFAULT_DEPOT, Depot, Track} from "../model/depot";
|
||||||
|
import {FormationsStoreService} from "../../formations/service/formations-store.service";
|
||||||
|
import {inject} from "@angular/core";
|
||||||
|
import {map, Observable} from "rxjs";
|
||||||
|
|
||||||
|
export class DepotComponent {
|
||||||
|
depot: Depot = {...DEFAULT_DEPOT};
|
||||||
|
usedFormations: Formation[] = [];
|
||||||
|
isFormationPopoverOpen = false;
|
||||||
|
clickEvent: MouseEvent = new MouseEvent('mouseup');
|
||||||
|
travelDurationIndex?: number;
|
||||||
|
unusedFormations$!: Observable<Formation[]>;
|
||||||
|
private readonly storeService: FormationsStoreService = inject(FormationsStoreService);
|
||||||
|
readonly formations$ = this.storeService.getFormations$();
|
||||||
|
|
||||||
|
compareWithStation(station1: Station, station2: Station) {
|
||||||
|
return station1 && station2 ? station1.id === station2.id : station1 === station2;
|
||||||
|
}
|
||||||
|
|
||||||
|
compareWithFormation(formation1: Formation, formation2: Formation) {
|
||||||
|
return formation1 && formation2 ? formation1.id == formation2.id : formation1 === formation2;
|
||||||
|
}
|
||||||
|
|
||||||
|
addTrack() {
|
||||||
|
const newTracks = [...this.depot.tracks];
|
||||||
|
const newId = Math.max(...newTracks.map(track => track.id)) + 1;
|
||||||
|
newTracks.push({id: newId, name: '', capacity: 0});
|
||||||
|
this.depot.tracks = newTracks;
|
||||||
|
}
|
||||||
|
|
||||||
|
changeName(index: number, event: InputCustomEvent) {
|
||||||
|
if (event.detail.value != null) {
|
||||||
|
const newTracks = [...this.depot.tracks];
|
||||||
|
newTracks[index] = {...newTracks[index], name: event.detail.value};
|
||||||
|
this.depot.tracks = newTracks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
changeCapacity(index: number, event: InputCustomEvent) {
|
||||||
|
if (event.detail.value != null) {
|
||||||
|
const newTracks = [...this.depot.tracks];
|
||||||
|
newTracks[index] = {...newTracks[index], capacity: +event.detail.value};
|
||||||
|
this.depot.tracks = newTracks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteTrack(deletedTrack: Track) {
|
||||||
|
this.depot.tracks = this.depot.tracks.filter(
|
||||||
|
track => track.id !== deletedTrack.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
trackByTrack(_: number, item: Track) {
|
||||||
|
return item.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
addTravelDuration(event: MouseEvent) {
|
||||||
|
this.clickEvent = event;
|
||||||
|
this.isFormationPopoverOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
changeTime(index: number, event: InputCustomEvent) {
|
||||||
|
if (event.detail.value != null) {
|
||||||
|
const newDurations = [...this.depot.travelDurations];
|
||||||
|
newDurations[index] = {...newDurations[index], time: +event.detail.value};
|
||||||
|
this.depot.travelDurations = newDurations;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelectFormation(event: SelectCustomEvent, index: number) {
|
||||||
|
if (event.detail.value != null) {
|
||||||
|
this.travelDurationIndex = index;
|
||||||
|
this.selectFormation(event.detail.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectFormation(formation: Formation) {
|
||||||
|
const newDurations = [...this.depot.travelDurations];
|
||||||
|
if (this.travelDurationIndex !== undefined) {
|
||||||
|
newDurations[this.travelDurationIndex].formation = formation;
|
||||||
|
} else {
|
||||||
|
newDurations.push({
|
||||||
|
formation: formation,
|
||||||
|
time: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.depot.travelDurations = newDurations;
|
||||||
|
this.usedFormations = this.depot.travelDurations.map(duration => duration.formation);
|
||||||
|
this.updateUnusedFormations();
|
||||||
|
this.travelDurationIndex = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteTravelDuration(travelDuration: TravelDuration) {
|
||||||
|
this.depot.travelDurations = this.depot.travelDurations.filter(
|
||||||
|
duration => duration.formation.id !== travelDuration.formation.id
|
||||||
|
);
|
||||||
|
this.usedFormations = this.depot.travelDurations.map(duration => duration.formation);
|
||||||
|
this.updateUnusedFormations();
|
||||||
|
}
|
||||||
|
|
||||||
|
trackByTravelDuration(_: number, item: TravelDuration) {
|
||||||
|
return item.formation.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUnusedFormations() {
|
||||||
|
this.unusedFormations$ = this.formations$.pipe(
|
||||||
|
map(formations => formations.filter(
|
||||||
|
formation => !this.usedFormations.some(usedFormation => usedFormation.id == formation.id)
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
import {Station} from "../model/station";
|
||||||
|
import {InputCustomEvent, SelectCustomEvent} from "@ionic/angular";
|
||||||
|
import {Formation} from "../../formations/model/formation";
|
||||||
|
import {TravelDuration} from "../model/travel-duration";
|
||||||
|
import {DEFAULT_PORTAL, Portal} from "../model/portal";
|
||||||
|
import {FormationsStoreService} from "../../formations/service/formations-store.service";
|
||||||
|
import {inject} from "@angular/core";
|
||||||
|
import {map, Observable} from "rxjs";
|
||||||
|
|
||||||
|
export class PortalComponent {
|
||||||
|
portal: Portal = {...DEFAULT_PORTAL};
|
||||||
|
usedFormations: Formation[] = [];
|
||||||
|
isFormationPopoverOpen = false;
|
||||||
|
clickEvent: MouseEvent = new MouseEvent('mouseup');
|
||||||
|
travelDurationIndex?: number;
|
||||||
|
|
||||||
|
private readonly storeService: FormationsStoreService = inject(FormationsStoreService);
|
||||||
|
readonly formations$ = this.storeService.getFormations$();
|
||||||
|
unusedFormations$!: Observable<Formation[]>;
|
||||||
|
|
||||||
|
compareWithStation(station1: Station, station2: Station) {
|
||||||
|
return station1 && station2 ? station1.id === station2.id : station1 === station2;
|
||||||
|
}
|
||||||
|
|
||||||
|
compareWithFormation(formation1: Formation, formation2: Formation) {
|
||||||
|
return formation1 && formation2 ? formation1.id == formation2.id : formation1 === formation2;
|
||||||
|
}
|
||||||
|
|
||||||
|
changeTime(index: number, event: InputCustomEvent) {
|
||||||
|
if (event.detail.value != null) {
|
||||||
|
const newDurations = [...this.portal.travelDurations];
|
||||||
|
newDurations[index] = {...newDurations[index], time: +event.detail.value};
|
||||||
|
this.portal.travelDurations = newDurations;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addTravelDuration(event: MouseEvent) {
|
||||||
|
this.clickEvent = event;
|
||||||
|
this.isFormationPopoverOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelectFormation(event: SelectCustomEvent, index: number) {
|
||||||
|
if (event.detail.value != null) {
|
||||||
|
this.travelDurationIndex = index;
|
||||||
|
this.selectFormation(event.detail.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectFormation(formation: Formation) {
|
||||||
|
const newDurations: TravelDuration[] = [...this.portal.travelDurations];
|
||||||
|
|
||||||
|
if (this.travelDurationIndex !== undefined) {
|
||||||
|
newDurations[this.travelDurationIndex] = {...newDurations[this.travelDurationIndex], formation};
|
||||||
|
} else if (!newDurations.some(duration => duration.formation.id == formation.id)) {
|
||||||
|
newDurations.push({
|
||||||
|
formation: formation,
|
||||||
|
time: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.portal.travelDurations = newDurations;
|
||||||
|
this.usedFormations = this.portal.travelDurations.map(duration => duration.formation);
|
||||||
|
this.updateUnusedFormations();
|
||||||
|
this.travelDurationIndex = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteTravelDuration(travelDuration: TravelDuration) {
|
||||||
|
this.portal.travelDurations = this.portal.travelDurations.filter(
|
||||||
|
duration => duration.formation.id !== travelDuration.formation.id
|
||||||
|
);
|
||||||
|
this.usedFormations = this.portal.travelDurations.map(duration => duration.formation);
|
||||||
|
this.updateUnusedFormations();
|
||||||
|
}
|
||||||
|
|
||||||
|
trackBy(_: number, item: TravelDuration) {
|
||||||
|
return item.formation.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUnusedFormations() {
|
||||||
|
this.unusedFormations$ = this.formations$.pipe(
|
||||||
|
map(formations => formations.filter(
|
||||||
|
formation => !this.usedFormations.some(usedFormation => usedFormation.id == formation.id)
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,160 @@
|
||||||
|
import {ItemReorderCustomEvent} from "@ionic/angular";
|
||||||
|
import {Station} from "../model/station";
|
||||||
|
import {DEFAULT_PORTAL, Portal} from "../model/portal";
|
||||||
|
import {DEFAULT_ROUTE, Route} from "../model/route";
|
||||||
|
import {DEFAULT_DEPOT, Depot} from "../model/depot";
|
||||||
|
import {Country} from "../model/country";
|
||||||
|
import {Item} from "../../typeahead/item";
|
||||||
|
import {Store} from "@ngrx/store";
|
||||||
|
import {RoutesState} from "../store/routes.reducer";
|
||||||
|
import {inject} from "@angular/core";
|
||||||
|
import {allCountries} from "../store";
|
||||||
|
import {RoutesStoreService} from "../service/routes-store.service";
|
||||||
|
import {map, Observable} from "rxjs";
|
||||||
|
|
||||||
|
export class RouteComponent {
|
||||||
|
route: Route = {...DEFAULT_ROUTE};
|
||||||
|
|
||||||
|
isStationPopoverOpen = false;
|
||||||
|
clickEvent: MouseEvent = new MouseEvent('mouseup');
|
||||||
|
stationIndex?: number;
|
||||||
|
|
||||||
|
isCreatePortalModalOpen = false;
|
||||||
|
isUpdatePortalModalOpen = false;
|
||||||
|
portalIndex?: number;
|
||||||
|
_updatedPortal: Portal = {...DEFAULT_PORTAL};
|
||||||
|
|
||||||
|
isCreateDepotModalOpen = false;
|
||||||
|
isUpdateDepotModalOpen = false;
|
||||||
|
depotIndex?: number;
|
||||||
|
_updatedDepot: Depot = {...DEFAULT_DEPOT};
|
||||||
|
|
||||||
|
protected readonly store: Store<RoutesState> = inject(Store<RoutesState>);
|
||||||
|
readonly countries$ = this.store.select(allCountries());
|
||||||
|
private readonly storeService: RoutesStoreService = inject(RoutesStoreService);
|
||||||
|
readonly stations$ = this.storeService.getStations$();
|
||||||
|
unusedStations$!: Observable<Station[]>;
|
||||||
|
|
||||||
|
compareWithCountry(country1: Country, country2: Country) {
|
||||||
|
return country1 && country2 ? country1.code === country2.code : country1 === country2;
|
||||||
|
}
|
||||||
|
|
||||||
|
trackBy<T extends Item>(_: number, item: T) {
|
||||||
|
return item.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleReorderStations(event: ItemReorderCustomEvent) {
|
||||||
|
const newStations = [...this.route.stations];
|
||||||
|
const movedItem = newStations[event.detail.from];
|
||||||
|
if (event.detail.from > event.detail.to) {
|
||||||
|
for (let i = event.detail.from - 1; i >= event.detail.to; i--) {
|
||||||
|
newStations[i + 1] = newStations[i];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (let i = event.detail.from + 1; i <= event.detail.to; i++) {
|
||||||
|
newStations[i - 1] = newStations[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newStations[event.detail.to] = movedItem;
|
||||||
|
this.route.stations = newStations;
|
||||||
|
this.updateUnusedStations();
|
||||||
|
event.detail.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteStation(deletedStation: Station) {
|
||||||
|
this.route.stations = this.route.stations.filter(station => station.id !== deletedStation.id);
|
||||||
|
this.updateUnusedStations();
|
||||||
|
}
|
||||||
|
|
||||||
|
selectStation(newStation: Station) {
|
||||||
|
const newStations = [...this.route.stations];
|
||||||
|
if (this.stationIndex !== undefined) {
|
||||||
|
newStations[this.stationIndex] = newStation;
|
||||||
|
} else if (!newStations.some(station => station.id == newStation.id)) {
|
||||||
|
newStations.push(newStation);
|
||||||
|
}
|
||||||
|
this.route.stations = newStations;
|
||||||
|
this.updateUnusedStations();
|
||||||
|
this.stationIndex = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
openPopoverStation(event: MouseEvent, index?: number) {
|
||||||
|
this.stationIndex = index;
|
||||||
|
this.clickEvent = event;
|
||||||
|
this.isStationPopoverOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
addPortal() {
|
||||||
|
this.isCreatePortalModalOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
insertPortal(portal: Portal) {
|
||||||
|
const newPortals = [...this.route.portals];
|
||||||
|
newPortals.push(portal);
|
||||||
|
this.route.portals = newPortals;
|
||||||
|
}
|
||||||
|
|
||||||
|
get updatedPortal() {
|
||||||
|
return this._updatedPortal;
|
||||||
|
}
|
||||||
|
|
||||||
|
set updatedPortal(changedPortal: Portal) {
|
||||||
|
this._updatedPortal = changedPortal;
|
||||||
|
if (this.portalIndex != null) {
|
||||||
|
const newPortals = [...this.route.portals];
|
||||||
|
newPortals[this.portalIndex] = changedPortal;
|
||||||
|
this.route.portals = newPortals;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePortal(portal: Portal, index: number) {
|
||||||
|
this._updatedPortal = portal;
|
||||||
|
this.portalIndex = index;
|
||||||
|
this.isUpdatePortalModalOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
deletePortal(deletedPortal: Portal) {
|
||||||
|
this.route.portals = this.route.portals.filter(portal => portal.id !== deletedPortal.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
addDepot() {
|
||||||
|
this.isCreateDepotModalOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
insertDepot(depot: Depot) {
|
||||||
|
const newDepots = [...this.route.depots];
|
||||||
|
newDepots.push(depot);
|
||||||
|
this.route.depots = newDepots;
|
||||||
|
}
|
||||||
|
|
||||||
|
get updatedDepot() {
|
||||||
|
return this._updatedDepot;
|
||||||
|
}
|
||||||
|
|
||||||
|
set updatedDepot(changedDepot: Depot) {
|
||||||
|
this._updatedDepot = changedDepot;
|
||||||
|
if (this.depotIndex != null) {
|
||||||
|
const newDepots = [...this.route.depots];
|
||||||
|
newDepots[this.depotIndex] = changedDepot;
|
||||||
|
this.route.depots = newDepots;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDepot(depot: Depot, index: number) {
|
||||||
|
this._updatedDepot = depot;
|
||||||
|
this.isUpdateDepotModalOpen = true;
|
||||||
|
this.depotIndex = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteDepot(deletedDepot: Depot) {
|
||||||
|
this.route.depots = this.route.depots.filter(depot => depot.id !== deletedDepot.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUnusedStations() {
|
||||||
|
this.unusedStations$ = this.stations$.pipe(
|
||||||
|
map(stations => stations.filter(
|
||||||
|
station => !this.route.stations.some(routeStation => routeStation.id == station.id)
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,139 @@
|
||||||
|
<ion-modal aria-labelledby="create-depot-title" [isOpen]="isOpen"
|
||||||
|
(keyup.escape)="cancel()"
|
||||||
|
backdrop-dismiss="false">
|
||||||
|
<ng-template>
|
||||||
|
<ion-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-title id="create-depot-title" i18n>Create Depot</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content class="ion-padding">
|
||||||
|
<form id="createDepotForm" #createDepot="ngForm" (ngSubmit)="confirm()">
|
||||||
|
<ion-input
|
||||||
|
i18n-label
|
||||||
|
label="ID"
|
||||||
|
labelPlacement="stacked"
|
||||||
|
type="text"
|
||||||
|
[required]="true"
|
||||||
|
name="id"
|
||||||
|
i18n-placeholder
|
||||||
|
placeholder="unique-id"
|
||||||
|
i18n-helper-text
|
||||||
|
helper-text="You cannot change this value later on. Uniquely identifies this depot."
|
||||||
|
pattern="^[\w\-]+$"
|
||||||
|
i18n-title
|
||||||
|
title="Only letters and dashes (-) are allowed."
|
||||||
|
[(ngModel)]="depot.id"
|
||||||
|
></ion-input>
|
||||||
|
<ion-input
|
||||||
|
i18n-label
|
||||||
|
label="Name"
|
||||||
|
labelPlacement="stacked"
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
[required]="true"
|
||||||
|
i18n-placeholder
|
||||||
|
placeholder="Name of the depot"
|
||||||
|
[(ngModel)]="depot.name"
|
||||||
|
></ion-input>
|
||||||
|
<ion-select
|
||||||
|
i18n-label
|
||||||
|
label="Nearest Station"
|
||||||
|
labelPlacement="stacked"
|
||||||
|
i18n-placeholder
|
||||||
|
placeholder="Closest station of the route"
|
||||||
|
name="nearestStation"
|
||||||
|
interface="popover"
|
||||||
|
[required]="true"
|
||||||
|
[compareWith]="compareWithStation"
|
||||||
|
[(ngModel)]="depot.nearestStation"
|
||||||
|
>
|
||||||
|
<ion-select-option *ngFor="let station of stations"
|
||||||
|
[value]="station">
|
||||||
|
{{ station.name }}
|
||||||
|
</ion-select-option>
|
||||||
|
</ion-select>
|
||||||
|
|
||||||
|
<h6 i18n>Tracks</h6>
|
||||||
|
<ion-list>
|
||||||
|
<ion-item>
|
||||||
|
<ion-label i18n class="bold">Name</ion-label>
|
||||||
|
<ion-label i18n class="bold">Capacity</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item-sliding *ngFor="let track of depot.tracks; index as i; trackBy: trackByTrack">
|
||||||
|
<ion-item>
|
||||||
|
<ion-input i18n-aria-label aria-label="Name" type="text" [value]="track.name"
|
||||||
|
(ionChange)="changeName(i, $event)"></ion-input>
|
||||||
|
<ion-input i18n-aria-label aria-label="Capacity" type="number" [value]="track.capacity"
|
||||||
|
min="0"
|
||||||
|
(ionChange)="changeCapacity(i, $event)"></ion-input>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item-options>
|
||||||
|
<ion-item-option (click)="deleteTrack(track)" color="danger" i18n>
|
||||||
|
<ion-icon slot="start" [ios]="'trash-outline'" [md]="'trash-sharp'"></ion-icon>
|
||||||
|
Delete
|
||||||
|
</ion-item-option>
|
||||||
|
</ion-item-options>
|
||||||
|
</ion-item-sliding>
|
||||||
|
</ion-list>
|
||||||
|
<ion-button (click)="addTrack()">
|
||||||
|
<ion-label i18n>Add track</ion-label>
|
||||||
|
</ion-button>
|
||||||
|
|
||||||
|
<h6 i18n>Travel Durations to nearest station</h6>
|
||||||
|
<ion-list>
|
||||||
|
<ion-item>
|
||||||
|
<ion-label i18n class="bold">Formation</ion-label>
|
||||||
|
<ion-label i18n class="bold">Time (s)</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item-sliding
|
||||||
|
*ngFor="let travelDuration of depot.travelDurations; index as i; trackBy: trackByTravelDuration">
|
||||||
|
<ion-item>
|
||||||
|
<ion-select
|
||||||
|
interface="popover"
|
||||||
|
[value]="travelDuration.formation"
|
||||||
|
(ionChange)="onSelectFormation($event, i)"
|
||||||
|
[compareWith]="compareWithFormation"
|
||||||
|
>
|
||||||
|
<ion-select-option [value]="travelDuration.formation">
|
||||||
|
{{ travelDuration.formation.name }}
|
||||||
|
</ion-select-option>
|
||||||
|
<ion-select-option *ngFor="let formation of unusedFormations$ | async" [value]="formation">
|
||||||
|
{{ formation.name }}
|
||||||
|
</ion-select-option>
|
||||||
|
</ion-select>
|
||||||
|
<ion-input i18n-aria-label aria-label="Time in seconds" type="number" [value]="travelDuration.time"
|
||||||
|
min="0"
|
||||||
|
(ionChange)="changeTime(i, $event)"></ion-input>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item-options>
|
||||||
|
<ion-item-option (click)="deleteTravelDuration(travelDuration)" color="danger" i18n>
|
||||||
|
<ion-icon slot="start" [ios]="'trash-outline'" [md]="'trash-sharp'"></ion-icon>
|
||||||
|
Delete
|
||||||
|
</ion-item-option>
|
||||||
|
</ion-item-options>
|
||||||
|
</ion-item-sliding>
|
||||||
|
</ion-list>
|
||||||
|
<ion-button (click)="addTravelDuration($event)" [disabled]="(unusedFormations$ | async)?.length == 0">
|
||||||
|
<ion-label i18n>Add travel duration</ion-label>
|
||||||
|
</ion-button>
|
||||||
|
</form>
|
||||||
|
</ion-content>
|
||||||
|
<ion-footer>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-buttons slot="start">
|
||||||
|
<ion-button (click)="cancel()" i18n>Cancel</ion-button>
|
||||||
|
</ion-buttons>
|
||||||
|
<ion-buttons slot="end">
|
||||||
|
<ion-button form="createDepotForm" type="submit" button-type="submit" [strong]="true" i18n
|
||||||
|
[disabled]="!createDepot.form.valid">Confirm
|
||||||
|
</ion-button>
|
||||||
|
</ion-buttons>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-footer>
|
||||||
|
</ng-template>
|
||||||
|
</ion-modal>
|
||||||
|
|
||||||
|
<app-typeahead [isOpen]="isFormationPopoverOpen" (dismissed)="isFormationPopoverOpen = false"
|
||||||
|
(itemSelected)="selectFormation($event)" [items$]="formations$" [usedItems]="usedFormations"
|
||||||
|
[event]="clickEvent"></app-typeahead>
|
|
@ -0,0 +1,3 @@
|
||||||
|
.bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
|
import {CreateDepotComponent} from './create-depot.component';
|
||||||
|
|
||||||
|
describe('CreateDepotComponent', () => {
|
||||||
|
let component: CreateDepotComponent;
|
||||||
|
let fixture: ComponentFixture<CreateDepotComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [CreateDepotComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(CreateDepotComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,100 @@
|
||||||
|
import {Component, EventEmitter, Input, Output, ViewChild} from '@angular/core';
|
||||||
|
import {AsyncPipe, NgForOf} from '@angular/common';
|
||||||
|
import {DepotComponent} from "../common/depot-component";
|
||||||
|
import {addIcons} from "ionicons";
|
||||||
|
import {trashOutline, trashSharp} from "ionicons/icons";
|
||||||
|
import {FormsModule} from "@angular/forms";
|
||||||
|
import {
|
||||||
|
IonButton,
|
||||||
|
IonButtons,
|
||||||
|
IonContent,
|
||||||
|
IonFooter,
|
||||||
|
IonHeader,
|
||||||
|
IonIcon,
|
||||||
|
IonInput,
|
||||||
|
IonItem,
|
||||||
|
IonItemOption,
|
||||||
|
IonItemOptions,
|
||||||
|
IonItemSliding,
|
||||||
|
IonLabel,
|
||||||
|
IonList,
|
||||||
|
IonModal,
|
||||||
|
IonSelect,
|
||||||
|
IonSelectOption,
|
||||||
|
IonTitle,
|
||||||
|
IonToolbar
|
||||||
|
} from "@ionic/angular/standalone";
|
||||||
|
import {TypeaheadComponent} from "../../typeahead/typeahead.component";
|
||||||
|
import {Station} from "../model/station";
|
||||||
|
import {DEFAULT_DEPOT, Depot} from "../model/depot";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-create-depot',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
NgForOf,
|
||||||
|
FormsModule,
|
||||||
|
IonButton,
|
||||||
|
IonButtons,
|
||||||
|
IonContent,
|
||||||
|
IonFooter,
|
||||||
|
IonHeader,
|
||||||
|
IonIcon,
|
||||||
|
IonInput,
|
||||||
|
IonItem,
|
||||||
|
IonItemOption,
|
||||||
|
IonItemOptions,
|
||||||
|
IonItemSliding,
|
||||||
|
IonLabel,
|
||||||
|
IonList,
|
||||||
|
IonModal,
|
||||||
|
IonSelect,
|
||||||
|
IonSelectOption,
|
||||||
|
IonTitle,
|
||||||
|
IonToolbar,
|
||||||
|
TypeaheadComponent,
|
||||||
|
AsyncPipe,
|
||||||
|
],
|
||||||
|
templateUrl: './create-depot.component.html',
|
||||||
|
styleUrl: './create-depot.component.scss'
|
||||||
|
})
|
||||||
|
export class CreateDepotComponent extends DepotComponent {
|
||||||
|
@Input({required: true}) isOpen: boolean = false;
|
||||||
|
@Output() dismissed: EventEmitter<boolean> = new EventEmitter<boolean>();
|
||||||
|
@Output() createdDepot: EventEmitter<Depot> = new EventEmitter<Depot>();
|
||||||
|
@ViewChild(IonModal) modal: IonModal | undefined;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
addIcons({
|
||||||
|
trashOutline,
|
||||||
|
trashSharp,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_stations?: Station[];
|
||||||
|
@Input({required: true}) set stations(newStations: Station[]) {
|
||||||
|
if (newStations !== null) {
|
||||||
|
this._stations = newStations;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get stations() {
|
||||||
|
return this._stations || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
this.dismissed.emit(true);
|
||||||
|
this.depot = {...DEFAULT_DEPOT};
|
||||||
|
this.usedFormations = [];
|
||||||
|
this.updateUnusedFormations();
|
||||||
|
}
|
||||||
|
|
||||||
|
confirm() {
|
||||||
|
this.createdDepot.emit({...this.depot});
|
||||||
|
this.dismissed.emit(true);
|
||||||
|
this.depot = {...DEFAULT_DEPOT};
|
||||||
|
this.usedFormations = [];
|
||||||
|
this.updateUnusedFormations();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,111 @@
|
||||||
|
<ion-modal aria-labelledby="create-portal-title" [isOpen]="isOpen"
|
||||||
|
(keyup.escape)="cancel()"
|
||||||
|
backdrop-dismiss="false">
|
||||||
|
<ng-template>
|
||||||
|
<ion-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-title id="create-portal-title" i18n>Create Portal</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content class="ion-padding">
|
||||||
|
<form id="createPortalForm" #createPortal="ngForm" (ngSubmit)="confirm()">
|
||||||
|
<ion-input
|
||||||
|
i18n-label
|
||||||
|
label="ID"
|
||||||
|
labelPlacement="stacked"
|
||||||
|
type="text"
|
||||||
|
[required]="true"
|
||||||
|
name="id"
|
||||||
|
i18n-placeholder
|
||||||
|
placeholder="unique-id"
|
||||||
|
i18n-helper-text
|
||||||
|
helper-text="You cannot change this value later on. Uniquely identifies this portal."
|
||||||
|
pattern="^[\w\-]+$"
|
||||||
|
i18n-title
|
||||||
|
title="Only letters and dashes (-) are allowed."
|
||||||
|
[(ngModel)]="portal.id"
|
||||||
|
></ion-input>
|
||||||
|
<ion-input
|
||||||
|
i18n-label
|
||||||
|
label="Name"
|
||||||
|
labelPlacement="stacked"
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
[required]="true"
|
||||||
|
i18n-placeholder
|
||||||
|
placeholder="Name of the portal"
|
||||||
|
[(ngModel)]="portal.name"
|
||||||
|
></ion-input>
|
||||||
|
<ion-select
|
||||||
|
i18n-label
|
||||||
|
label="Nearest Station"
|
||||||
|
labelPlacement="stacked"
|
||||||
|
i18n-placeholder
|
||||||
|
placeholder="Closest station of the route"
|
||||||
|
name="nearestStation"
|
||||||
|
interface="popover"
|
||||||
|
[required]="true"
|
||||||
|
[compareWith]="compareWithStation"
|
||||||
|
[(ngModel)]="portal.nearestStation"
|
||||||
|
>
|
||||||
|
<ion-select-option *ngFor="let station of stations"
|
||||||
|
[value]="station">
|
||||||
|
{{ station.name }}
|
||||||
|
</ion-select-option>
|
||||||
|
</ion-select>
|
||||||
|
|
||||||
|
<h6 i18n>Travel Durations to nearest station</h6>
|
||||||
|
<ion-list>
|
||||||
|
<ion-item>
|
||||||
|
<ion-label i18n class="bold">Formation</ion-label>
|
||||||
|
<ion-label i18n class="bold">Time (s)</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item-sliding *ngFor="let travelDuration of portal.travelDurations; index as i; trackBy: trackBy">
|
||||||
|
<ion-item>
|
||||||
|
<ion-select
|
||||||
|
interface="popover"
|
||||||
|
[value]="travelDuration.formation"
|
||||||
|
(ionChange)="onSelectFormation($event, i)"
|
||||||
|
[compareWith]="compareWithFormation"
|
||||||
|
>
|
||||||
|
<ion-select-option [value]="travelDuration.formation">
|
||||||
|
{{ travelDuration.formation.name }}
|
||||||
|
</ion-select-option>
|
||||||
|
<ion-select-option *ngFor="let formation of unusedFormations$ | async" [value]="formation">
|
||||||
|
{{ formation.name }}
|
||||||
|
</ion-select-option>
|
||||||
|
</ion-select>
|
||||||
|
<ion-input i18n-aria-label aria-label="Time in seconds" type="number"
|
||||||
|
(ionChange)="changeTime(i, $event)"></ion-input>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item-options>
|
||||||
|
<ion-item-option (click)="deleteTravelDuration(travelDuration)" color="danger" i18n>
|
||||||
|
<ion-icon slot="start" [ios]="'trash-outline'" [md]="'trash-sharp'"></ion-icon>
|
||||||
|
Delete
|
||||||
|
</ion-item-option>
|
||||||
|
</ion-item-options>
|
||||||
|
</ion-item-sliding>
|
||||||
|
</ion-list>
|
||||||
|
<ion-button (click)="addTravelDuration($event)" [disabled]="(unusedFormations$ | async)?.length == 0">
|
||||||
|
<ion-label i18n>Add travel duration</ion-label>
|
||||||
|
</ion-button>
|
||||||
|
</form>
|
||||||
|
</ion-content>
|
||||||
|
<ion-footer>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-buttons slot="start">
|
||||||
|
<ion-button (click)="cancel()" i18n>Cancel</ion-button>
|
||||||
|
</ion-buttons>
|
||||||
|
<ion-buttons slot="end">
|
||||||
|
<ion-button form="createPortalForm" type="submit" button-type="submit" [strong]="true" i18n
|
||||||
|
[disabled]="!createPortal.form.valid">Confirm
|
||||||
|
</ion-button>
|
||||||
|
</ion-buttons>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-footer>
|
||||||
|
</ng-template>
|
||||||
|
</ion-modal>
|
||||||
|
|
||||||
|
<app-typeahead [isOpen]="isFormationPopoverOpen" (dismissed)="isFormationPopoverOpen = false"
|
||||||
|
(itemSelected)="selectFormation($event)" [items$]="formations$" [usedItems]="usedFormations"
|
||||||
|
[event]="clickEvent"></app-typeahead>
|
|
@ -0,0 +1,3 @@
|
||||||
|
.bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
|
import {CreatePortalComponent} from './create-portal.component';
|
||||||
|
|
||||||
|
describe('CreatePortalComponent', () => {
|
||||||
|
let component: CreatePortalComponent;
|
||||||
|
let fixture: ComponentFixture<CreatePortalComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [CreatePortalComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(CreatePortalComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,84 @@
|
||||||
|
import {Component, EventEmitter, Input, Output, ViewChild} from '@angular/core';
|
||||||
|
import {AsyncPipe, NgForOf} from '@angular/common';
|
||||||
|
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
|
||||||
|
import {
|
||||||
|
IonButton,
|
||||||
|
IonButtons,
|
||||||
|
IonContent,
|
||||||
|
IonDatetime,
|
||||||
|
IonFooter,
|
||||||
|
IonHeader,
|
||||||
|
IonIcon,
|
||||||
|
IonInput,
|
||||||
|
IonItem,
|
||||||
|
IonItemOption,
|
||||||
|
IonItemOptions,
|
||||||
|
IonItemSliding,
|
||||||
|
IonLabel,
|
||||||
|
IonList,
|
||||||
|
IonModal,
|
||||||
|
IonSelect,
|
||||||
|
IonSelectOption,
|
||||||
|
IonTitle,
|
||||||
|
IonToolbar
|
||||||
|
} from "@ionic/angular/standalone";
|
||||||
|
import {DEFAULT_PORTAL, Portal} from "../model/portal";
|
||||||
|
import {Station} from "../model/station";
|
||||||
|
import {TypeaheadComponent} from "../../typeahead/typeahead.component";
|
||||||
|
import {PortalComponent} from "../common/portal-component";
|
||||||
|
import {addIcons} from "ionicons";
|
||||||
|
import {trashOutline, trashSharp} from "ionicons/icons";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-create-portal',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
FormsModule, IonContent, IonHeader, IonInput, IonModal, IonTitle, IonToolbar,
|
||||||
|
ReactiveFormsModule, IonSelect, IonSelectOption, IonButton, IonButtons, IonFooter, IonIcon, IonItem, IonItemOption,
|
||||||
|
IonItemOptions, IonItemSliding, IonLabel, IonList, TypeaheadComponent, IonDatetime, NgForOf, AsyncPipe
|
||||||
|
],
|
||||||
|
templateUrl: './create-portal.component.html',
|
||||||
|
styleUrl: './create-portal.component.scss'
|
||||||
|
})
|
||||||
|
export class CreatePortalComponent extends PortalComponent {
|
||||||
|
@Input({required: true}) isOpen: boolean = false;
|
||||||
|
@Output() dismissed: EventEmitter<boolean> = new EventEmitter<boolean>();
|
||||||
|
@Output() createdPortal: EventEmitter<Portal> = new EventEmitter<Portal>();
|
||||||
|
@ViewChild(IonModal) modal: IonModal | undefined;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
addIcons({
|
||||||
|
trashOutline,
|
||||||
|
trashSharp,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_stations?: Station[];
|
||||||
|
@Input({required: true}) set stations(newStations: Station[]) {
|
||||||
|
if (newStations !== null) {
|
||||||
|
this._stations = newStations;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get stations() {
|
||||||
|
return this._stations || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
this.dismissed.emit(true);
|
||||||
|
this.portal = {...DEFAULT_PORTAL};
|
||||||
|
this.usedFormations = [];
|
||||||
|
this.updateUnusedFormations();
|
||||||
|
}
|
||||||
|
|
||||||
|
confirm() {
|
||||||
|
this.createdPortal.emit({...this.portal});
|
||||||
|
this.dismissed.emit(true);
|
||||||
|
this.portal = {...DEFAULT_PORTAL};
|
||||||
|
this.usedFormations = [];
|
||||||
|
this.updateUnusedFormations();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,148 @@
|
||||||
|
<ion-modal aria-labelledby="create-route-title" [isOpen]="isOpen"
|
||||||
|
(keyup.escape)="cancel()"
|
||||||
|
backdrop-dismiss="false">
|
||||||
|
<ng-template>
|
||||||
|
<ion-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-title id="create-route-title" i18n>Create Route</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content class="ion-padding">
|
||||||
|
<form id="createRouteForm" #createRoute="ngForm" (ngSubmit)="confirm()">
|
||||||
|
<ion-input
|
||||||
|
i18n-label
|
||||||
|
label="ID"
|
||||||
|
labelPlacement="stacked"
|
||||||
|
type="text"
|
||||||
|
[required]="true"
|
||||||
|
name="id"
|
||||||
|
i18n-placeholder
|
||||||
|
placeholder="unique-id"
|
||||||
|
i18n-helper-text
|
||||||
|
helper-text="You cannot change this value later on. Uniquely identifies this route."
|
||||||
|
pattern="^[\w\-]+$"
|
||||||
|
i18n-title
|
||||||
|
title="Only letters and dashes (-) are allowed."
|
||||||
|
[(ngModel)]="route.id"
|
||||||
|
></ion-input>
|
||||||
|
<ion-input
|
||||||
|
i18n-label
|
||||||
|
label="Name"
|
||||||
|
labelPlacement="stacked"
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
[required]="true"
|
||||||
|
i18n-placeholder
|
||||||
|
placeholder="Name of the route"
|
||||||
|
[(ngModel)]="route.name"
|
||||||
|
></ion-input>
|
||||||
|
<ion-select
|
||||||
|
i18n-label
|
||||||
|
label="Country"
|
||||||
|
labelPlacement="stacked"
|
||||||
|
i18n-placeholder
|
||||||
|
placeholder="Country of the route"
|
||||||
|
name="country"
|
||||||
|
interface="popover"
|
||||||
|
[compareWith]="compareWithCountry"
|
||||||
|
[(ngModel)]="route.country"
|
||||||
|
>
|
||||||
|
<ion-select-option *ngFor="let country of countries$ | async"
|
||||||
|
[value]="country">
|
||||||
|
{{ country.name }}
|
||||||
|
</ion-select-option>
|
||||||
|
</ion-select>
|
||||||
|
<h6 i18n>Stations</h6>
|
||||||
|
<ion-list>
|
||||||
|
<ion-reorder-group [disabled]="false" (ionItemReorder)="handleReorderStations($event)">
|
||||||
|
<ion-item-sliding *ngFor="let station of route.stations; index as i; trackBy: trackBy">
|
||||||
|
<ion-item [button]="true" (click)="openPopoverStation($event, i)">
|
||||||
|
<ion-reorder slot="start"></ion-reorder>
|
||||||
|
<ion-label class="no-pointer-events">{{ station.name }}</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item-options>
|
||||||
|
<ion-item-option (click)="deleteStation(station)" color="danger" i18n>
|
||||||
|
<ion-icon slot="start" [ios]="'trash-outline'" [md]="'trash-sharp'"></ion-icon>
|
||||||
|
Delete
|
||||||
|
</ion-item-option>
|
||||||
|
</ion-item-options>
|
||||||
|
</ion-item-sliding>
|
||||||
|
</ion-reorder-group>
|
||||||
|
</ion-list>
|
||||||
|
<ion-button (click)="openPopoverStation($event)" [disabled]="(unusedStations$ | async)?.length == 0">
|
||||||
|
<ion-label i18n class="no-pointer-events">Add station</ion-label>
|
||||||
|
</ion-button>
|
||||||
|
|
||||||
|
<h6 i18n>Portals</h6>
|
||||||
|
<ion-list>
|
||||||
|
<ion-item-sliding *ngFor="let portal of route.portals; index as i; trackBy: trackBy">
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>{{ portal.name }}</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item-options>
|
||||||
|
<ion-item-option (click)="updatePortal(portal, i)" color="secondary" i18n>
|
||||||
|
<ion-icon slot="start" [ios]="'pencil-outline'" [md]="'pencil-sharp'"></ion-icon>
|
||||||
|
Update
|
||||||
|
</ion-item-option>
|
||||||
|
<ion-item-option (click)="deletePortal(portal)" color="danger" i18n>
|
||||||
|
<ion-icon slot="start" [ios]="'trash-outline'" [md]="'trash-sharp'"></ion-icon>
|
||||||
|
Delete
|
||||||
|
</ion-item-option>
|
||||||
|
</ion-item-options>
|
||||||
|
</ion-item-sliding>
|
||||||
|
</ion-list>
|
||||||
|
<ion-button (click)="addPortal()">
|
||||||
|
<ion-label i18n>Add portal</ion-label>
|
||||||
|
</ion-button>
|
||||||
|
|
||||||
|
<h6 i18n>Depots</h6>
|
||||||
|
<ion-list>
|
||||||
|
<ion-item-sliding *ngFor="let depot of route.depots; index as i; trackBy: trackBy">
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>{{ depot.name }}</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item-options>
|
||||||
|
<ion-item-option (click)="updateDepot(depot, i)" color="secondary" i18n>
|
||||||
|
<ion-icon slot="start" [ios]="'pencil-outline'" [md]="'pencil-sharp'"></ion-icon>
|
||||||
|
Update
|
||||||
|
</ion-item-option>
|
||||||
|
<ion-item-option (click)="deleteDepot(depot)" color="danger" i18n>
|
||||||
|
<ion-icon slot="start" [ios]="'trash-outline'" [md]="'trash-sharp'"></ion-icon>
|
||||||
|
Delete
|
||||||
|
</ion-item-option>
|
||||||
|
</ion-item-options>
|
||||||
|
</ion-item-sliding>
|
||||||
|
</ion-list>
|
||||||
|
<ion-button (click)="addDepot()">
|
||||||
|
<ion-label i18n>Add depot</ion-label>
|
||||||
|
</ion-button>
|
||||||
|
</form>
|
||||||
|
</ion-content>
|
||||||
|
<ion-footer>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-buttons slot="start">
|
||||||
|
<ion-button (click)="cancel()" i18n>Cancel</ion-button>
|
||||||
|
</ion-buttons>
|
||||||
|
<ion-buttons slot="end">
|
||||||
|
<ion-button form="createRouteForm" type="submit" button-type="submit" [strong]="true" i18n
|
||||||
|
[disabled]="!createRoute.form.valid || route.stations.length == 0">Confirm
|
||||||
|
</ion-button>
|
||||||
|
</ion-buttons>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-footer>
|
||||||
|
</ng-template>
|
||||||
|
</ion-modal>
|
||||||
|
|
||||||
|
<app-create-portal [isOpen]="isCreatePortalModalOpen" (dismissed)="isCreatePortalModalOpen = false"
|
||||||
|
[stations]="route.stations" (createdPortal)="insertPortal($event)"></app-create-portal>
|
||||||
|
<app-update-portal [isOpen]="isUpdatePortalModalOpen" (dismissed)="isUpdatePortalModalOpen = false"
|
||||||
|
[stations]="route.stations" [(updatedPortal)]="updatedPortal"></app-update-portal>
|
||||||
|
|
||||||
|
<app-create-depot [isOpen]="isCreateDepotModalOpen" (dismissed)="isCreateDepotModalOpen = false"
|
||||||
|
[stations]="route.stations" (createdDepot)="insertDepot($event)"></app-create-depot>
|
||||||
|
<app-update-depot [isOpen]="isUpdateDepotModalOpen" (dismissed)="isUpdateDepotModalOpen = false"
|
||||||
|
[stations]="route.stations" [(updatedDepot)]="updatedDepot"></app-update-depot>
|
||||||
|
|
||||||
|
<app-typeahead [isOpen]="isStationPopoverOpen" (dismissed)="isStationPopoverOpen = false"
|
||||||
|
(itemSelected)="selectStation($event)" [items$]="stations$" [usedItems]="route.stations"
|
||||||
|
[event]="clickEvent"></app-typeahead>
|
|
@ -0,0 +1,3 @@
|
||||||
|
.no-pointer-events {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
|
import {CreateRouteComponent} from './create-route.component';
|
||||||
|
|
||||||
|
describe('CreateRouteComponent', () => {
|
||||||
|
let component: CreateRouteComponent;
|
||||||
|
let fixture: ComponentFixture<CreateRouteComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [CreateRouteComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(CreateRouteComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,113 @@
|
||||||
|
import {Component, EventEmitter, Input, Output, ViewChild} from '@angular/core';
|
||||||
|
import {
|
||||||
|
IonButton,
|
||||||
|
IonButtons,
|
||||||
|
IonContent,
|
||||||
|
IonFooter,
|
||||||
|
IonHeader,
|
||||||
|
IonIcon,
|
||||||
|
IonInput,
|
||||||
|
IonItem,
|
||||||
|
IonItemOption,
|
||||||
|
IonItemOptions,
|
||||||
|
IonItemSliding,
|
||||||
|
IonLabel,
|
||||||
|
IonList,
|
||||||
|
IonListHeader,
|
||||||
|
IonModal,
|
||||||
|
IonPopover,
|
||||||
|
IonReorder,
|
||||||
|
IonReorderGroup,
|
||||||
|
IonSearchbar,
|
||||||
|
IonSelect,
|
||||||
|
IonSelectOption,
|
||||||
|
IonTitle,
|
||||||
|
IonToolbar
|
||||||
|
} from "@ionic/angular/standalone";
|
||||||
|
import {DEFAULT_ROUTE} from "../model/route";
|
||||||
|
import {addRouteAction} from "../store/routes.actions";
|
||||||
|
import {AsyncPipe, NgForOf} from "@angular/common";
|
||||||
|
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
|
||||||
|
import {TypeaheadComponent} from "../../typeahead/typeahead.component";
|
||||||
|
import {addIcons} from "ionicons";
|
||||||
|
import {pencilOutline, pencilSharp, trashOutline, trashSharp} from "ionicons/icons";
|
||||||
|
import {CreatePortalComponent} from "../create-portal/create-portal.component";
|
||||||
|
import {RouteComponent} from "../common/route-component";
|
||||||
|
import {UpdatePortalComponent} from "../update-portal/update-portal.component";
|
||||||
|
import {CreateDepotComponent} from "../create-depot/create-depot.component";
|
||||||
|
import {UpdateDepotComponent} from "../update-depot/update-depot.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-create-route',
|
||||||
|
standalone: true,
|
||||||
|
templateUrl: './create-route.component.html',
|
||||||
|
imports: [
|
||||||
|
AsyncPipe,
|
||||||
|
FormsModule,
|
||||||
|
IonButton,
|
||||||
|
IonButtons,
|
||||||
|
IonContent,
|
||||||
|
IonFooter,
|
||||||
|
IonHeader,
|
||||||
|
IonInput,
|
||||||
|
IonItem,
|
||||||
|
IonModal,
|
||||||
|
IonSelect,
|
||||||
|
IonSelectOption,
|
||||||
|
IonTitle,
|
||||||
|
IonToolbar,
|
||||||
|
NgForOf,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
IonList,
|
||||||
|
IonReorderGroup,
|
||||||
|
IonLabel,
|
||||||
|
IonReorder,
|
||||||
|
IonListHeader,
|
||||||
|
IonPopover,
|
||||||
|
IonSearchbar,
|
||||||
|
IonItemSliding,
|
||||||
|
IonItemOption,
|
||||||
|
IonItemOptions,
|
||||||
|
IonIcon,
|
||||||
|
TypeaheadComponent,
|
||||||
|
CreatePortalComponent,
|
||||||
|
UpdatePortalComponent,
|
||||||
|
CreateDepotComponent,
|
||||||
|
UpdateDepotComponent
|
||||||
|
],
|
||||||
|
styleUrl: './create-route.component.scss'
|
||||||
|
})
|
||||||
|
export class CreateRouteComponent extends RouteComponent {
|
||||||
|
@Input() isOpen: boolean = false;
|
||||||
|
@Output() dismissed: EventEmitter<boolean> = new EventEmitter<boolean>();
|
||||||
|
|
||||||
|
@ViewChild(IonModal) modal: IonModal | undefined;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
addIcons({
|
||||||
|
trashOutline,
|
||||||
|
trashSharp,
|
||||||
|
pencilOutline,
|
||||||
|
pencilSharp,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
this.dismissed.emit(true);
|
||||||
|
this.route = {...DEFAULT_ROUTE};
|
||||||
|
this.updateUnusedStations();
|
||||||
|
}
|
||||||
|
|
||||||
|
confirm() {
|
||||||
|
this.route.firstStation = this.route.stations[0];
|
||||||
|
this.route.lastStation = this.route.stations[this.route.stations.length - 1];
|
||||||
|
this.route.numberOfStations = this.route.stations.length;
|
||||||
|
this.store.dispatch(addRouteAction({
|
||||||
|
payload: {...this.route}
|
||||||
|
}))
|
||||||
|
this.dismissed.emit(true);
|
||||||
|
this.route = {...DEFAULT_ROUTE};
|
||||||
|
this.updateUnusedStations();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
import {Item} from "../../typeahead/item";
|
||||||
|
import {DEFAULT_STATION, Station} from "./station";
|
||||||
|
import {TravelDuration} from "./travel-duration";
|
||||||
|
|
||||||
|
export interface Depot extends Item {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
nearestStation: Station;
|
||||||
|
tracks: Track[];
|
||||||
|
travelDurations: TravelDuration[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Track {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
capacity: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_DEPOT: Depot = {
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
nearestStation: DEFAULT_STATION,
|
||||||
|
tracks: [],
|
||||||
|
travelDurations: []
|
||||||
|
};
|
|
@ -0,0 +1,17 @@
|
||||||
|
import {Item} from "../../typeahead/item";
|
||||||
|
import {DEFAULT_STATION, Station} from "./station";
|
||||||
|
import {TravelDuration} from "./travel-duration";
|
||||||
|
|
||||||
|
export interface Portal extends Item {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
nearestStation: Station;
|
||||||
|
travelDurations: TravelDuration[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_PORTAL: Portal = {
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
nearestStation: DEFAULT_STATION,
|
||||||
|
travelDurations: []
|
||||||
|
};
|
|
@ -1,14 +1,31 @@
|
||||||
import {Station} from "./station";
|
import {DEFAULT_STATION, Station} from "./station";
|
||||||
import {Country} from "./country";
|
import {Country} from "./country";
|
||||||
|
import {Depot} from "./depot";
|
||||||
|
import {Portal} from "./portal";
|
||||||
|
|
||||||
export interface Route {
|
export interface Route {
|
||||||
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
country: Country;
|
country: Country;
|
||||||
|
stations: Station[];
|
||||||
firstStation: Station;
|
firstStation: Station;
|
||||||
lastStation: Station;
|
lastStation: Station;
|
||||||
numberOfStations: number;
|
numberOfStations: number;
|
||||||
|
depots: Depot[];
|
||||||
|
portals: Portal[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EditedRoute extends Route {
|
export const DEFAULT_ROUTE: Route = {
|
||||||
stations: Station[];
|
id: "",
|
||||||
|
name: "",
|
||||||
|
country: {
|
||||||
|
code: 'de',
|
||||||
|
name: 'Germany'
|
||||||
|
},
|
||||||
|
firstStation: DEFAULT_STATION,
|
||||||
|
lastStation: DEFAULT_STATION,
|
||||||
|
stations: [],
|
||||||
|
numberOfStations: 0,
|
||||||
|
depots: [],
|
||||||
|
portals: []
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,11 @@
|
||||||
export interface Station {
|
import {Item} from "../../typeahead/item";
|
||||||
|
|
||||||
|
export interface Station extends Item {
|
||||||
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_STATION: Station = {
|
||||||
|
id: '',
|
||||||
|
name: ''
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
import {Formation} from "../../formations/model/formation";
|
||||||
|
|
||||||
|
export interface TravelDuration {
|
||||||
|
formation: Formation;
|
||||||
|
time: number;
|
||||||
|
}
|
|
@ -3,7 +3,12 @@
|
||||||
<ion-buttons slot="start">
|
<ion-buttons slot="start">
|
||||||
<ion-menu-button></ion-menu-button>
|
<ion-menu-button></ion-menu-button>
|
||||||
</ion-buttons>
|
</ion-buttons>
|
||||||
<ion-title i18n="page title">Routes</ion-title>
|
<ion-title>
|
||||||
|
<span i18n="page title">Routes</span>
|
||||||
|
@if (!(hasPersonalPlan$ | async)) {
|
||||||
|
({{ (routes$ | async)?.length }} / 1)
|
||||||
|
}
|
||||||
|
</ion-title>
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
<ion-content [fullscreen]="true">
|
<ion-content [fullscreen]="true">
|
||||||
|
@ -15,8 +20,8 @@
|
||||||
<ion-label i18n class="bold">Last Station</ion-label>
|
<ion-label i18n class="bold">Last Station</ion-label>
|
||||||
<ion-label i18n class="bold"># Stations</ion-label>
|
<ion-label i18n class="bold"># Stations</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item-sliding>
|
<ion-item-sliding *ngFor="let route of routes$ | async">
|
||||||
<ion-item *ngFor="let route of routes">
|
<ion-item>
|
||||||
<ion-label>{{ route.name }}</ion-label>
|
<ion-label>{{ route.name }}</ion-label>
|
||||||
<ion-label>{{ route.country.name }}</ion-label>
|
<ion-label>{{ route.country.name }}</ion-label>
|
||||||
<ion-label>{{ route.firstStation.name }}</ion-label>
|
<ion-label>{{ route.firstStation.name }}</ion-label>
|
||||||
|
@ -24,17 +29,26 @@
|
||||||
<ion-label>{{ route.numberOfStations }}</ion-label>
|
<ion-label>{{ route.numberOfStations }}</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item-options side="end">
|
<ion-item-options side="end">
|
||||||
<ion-item-option color="danger" i18n>
|
<ion-item-option (click)="updateRoute(route)" color="secondary" i18n>
|
||||||
|
<ion-icon slot="start" [ios]="'pencil-outline'" [md]="'pencil-sharp'"></ion-icon>
|
||||||
|
Update
|
||||||
|
</ion-item-option>
|
||||||
|
<ion-item-option (click)="deleteRoute(route)" color="danger" i18n>
|
||||||
<ion-icon slot="start" [ios]="'trash-outline'" [md]="'trash-sharp'"></ion-icon>
|
<ion-icon slot="start" [ios]="'trash-outline'" [md]="'trash-sharp'"></ion-icon>
|
||||||
Delete
|
Delete
|
||||||
</ion-item-option>
|
</ion-item-option>
|
||||||
</ion-item-options>
|
</ion-item-options>
|
||||||
</ion-item-sliding>
|
</ion-item-sliding>
|
||||||
</ion-list>
|
</ion-list>
|
||||||
<ion-fab slot="fixed" horizontal="end" vertical="bottom">
|
@if ((routes$ | async)?.length == 0 || (hasPersonalPlan$ | async)) {
|
||||||
<ion-fab-button i18n-aria-label aria-label="Add route">
|
<ion-fab slot="fixed" horizontal="end" vertical="bottom">
|
||||||
<ion-icon [ios]="'add-outline'" [md]="'add-sharp'"></ion-icon>
|
<ion-fab-button i18n-aria-label aria-label="Add route" (click)="addRoute()">
|
||||||
</ion-fab-button>
|
<ion-icon [ios]="'add-outline'" [md]="'add-sharp'"></ion-icon>
|
||||||
</ion-fab>
|
</ion-fab-button>
|
||||||
|
</ion-fab>
|
||||||
|
}
|
||||||
|
<app-create-route [isOpen]="isCreateModalOpen" (dismissed)="isCreateModalOpen = false"></app-create-route>
|
||||||
|
<app-update-route [isOpen]="isUpdateModalOpen" [updatedRoute]="updatedRoute!"
|
||||||
|
(dismissed)="isUpdateModalOpen = false"></app-update-route>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
<ion-footer></ion-footer>
|
<ion-footer id="footer"></ion-footer>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import {Component} from '@angular/core';
|
import {Component, inject} from '@angular/core';
|
||||||
import {Route} from "./model/route";
|
import {DEFAULT_ROUTE, Route} from "./model/route";
|
||||||
import {addIcons} from "ionicons";
|
import {addIcons} from "ionicons";
|
||||||
import {addOutline, addSharp, trashOutline, trashSharp} from "ionicons/icons";
|
import {addOutline, addSharp, pencilOutline, pencilSharp, trashOutline, trashSharp} from "ionicons/icons";
|
||||||
import {NgForOf} from "@angular/common";
|
import {AsyncPipe, NgForOf} from "@angular/common";
|
||||||
import {
|
import {
|
||||||
IonButtons,
|
IonButtons,
|
||||||
IonContent,
|
IonContent,
|
||||||
|
@ -22,6 +22,16 @@ import {
|
||||||
IonTitle,
|
IonTitle,
|
||||||
IonToolbar
|
IonToolbar
|
||||||
} from "@ionic/angular/standalone";
|
} from "@ionic/angular/standalone";
|
||||||
|
import {RoutesStoreService} from "./service/routes-store.service";
|
||||||
|
import {deleteRouteAction} from "./store/routes.actions";
|
||||||
|
import {Store} from "@ngrx/store";
|
||||||
|
import {RoutesState} from "./store/routes.reducer";
|
||||||
|
import {CreateFormationComponent} from "../formations/create-formation/create-formation.component";
|
||||||
|
import {UpdateFormationComponent} from "../formations/update-formation/update-formation.component";
|
||||||
|
import {CreateRouteComponent} from "./create-route/create-route.component";
|
||||||
|
import {UpdateRouteComponent} from "./update-route/update-route.component";
|
||||||
|
import {AuthService} from "../auth/service/auth.service";
|
||||||
|
import {filter, map} from "rxjs";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-routes',
|
selector: 'app-routes',
|
||||||
|
@ -46,27 +56,50 @@ import {
|
||||||
IonIcon,
|
IonIcon,
|
||||||
IonFab,
|
IonFab,
|
||||||
IonFabButton,
|
IonFabButton,
|
||||||
IonFooter
|
IonFooter,
|
||||||
|
AsyncPipe,
|
||||||
|
CreateFormationComponent,
|
||||||
|
UpdateFormationComponent,
|
||||||
|
CreateRouteComponent,
|
||||||
|
UpdateRouteComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class RoutesComponent {
|
export class RoutesComponent {
|
||||||
|
isCreateModalOpen = false;
|
||||||
|
isUpdateModalOpen = false;
|
||||||
|
updatedRoute: Route = {...DEFAULT_ROUTE};
|
||||||
|
|
||||||
routes: Route[] = [
|
private readonly storeService: RoutesStoreService = inject(RoutesStoreService);
|
||||||
{
|
routes$ = this.storeService.getRoutes$();
|
||||||
name: 'Köln-Aachen',
|
|
||||||
country: {name: $localize`Germany`, code: 'de'},
|
|
||||||
firstStation: {name: 'Köln Hbf'},
|
|
||||||
lastStation: {name: 'Aachen Hbf'},
|
|
||||||
numberOfStations: 30
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
constructor() {
|
private readonly authService: AuthService = inject(AuthService);
|
||||||
|
readonly user$ = this.authService.getUser$();
|
||||||
|
readonly hasPersonalPlan$ = this.user$.pipe(
|
||||||
|
map(user => user.roles),
|
||||||
|
filter(roles => roles.includes('PERSONAL_PLAN'))
|
||||||
|
);
|
||||||
|
|
||||||
|
constructor(private readonly store: Store<RoutesState>) {
|
||||||
addIcons({
|
addIcons({
|
||||||
addOutline,
|
addOutline,
|
||||||
addSharp,
|
addSharp,
|
||||||
trashOutline,
|
trashOutline,
|
||||||
trashSharp,
|
trashSharp,
|
||||||
|
pencilOutline,
|
||||||
|
pencilSharp,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addRoute() {
|
||||||
|
this.isCreateModalOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRoute(route: Route) {
|
||||||
|
this.updatedRoute = route;
|
||||||
|
this.isUpdateModalOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteRoute(route: Route) {
|
||||||
|
this.store.dispatch(deleteRouteAction({payload: route}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
|
import {RouteService} from './route.service';
|
||||||
|
|
||||||
|
describe('RoutesService', () => {
|
||||||
|
let service: RouteService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(RouteService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,75 @@
|
||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import {HttpClient, HttpHeaders} from "@angular/common/http";
|
||||||
|
import {environment} from "../../../environments/environment";
|
||||||
|
import {Route} from "../model/route";
|
||||||
|
import {ErrorService} from "../../errors/error.service";
|
||||||
|
import {catchError, Observable, of} from "rxjs";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class RouteService {
|
||||||
|
|
||||||
|
httpOptions = {
|
||||||
|
headers: new HttpHeaders({'Content-Type': 'application/json'})
|
||||||
|
};
|
||||||
|
private routesURL = environment.backendURL + '/route';
|
||||||
|
|
||||||
|
private knownRoutes: Map<string, Route> = new Map<string, Route>();
|
||||||
|
private routes: Route[] = [];
|
||||||
|
|
||||||
|
constructor(private readonly http: HttpClient,
|
||||||
|
private readonly errorService: ErrorService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchRoutes(): Observable<Route[]> {
|
||||||
|
if (environment.mockNetwork) {
|
||||||
|
this.routes = JSON.parse(localStorage.getItem("routes") || '[]');
|
||||||
|
return of(this.routes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.http.get<Route[]>(this.routesURL, this.httpOptions)
|
||||||
|
.pipe(
|
||||||
|
catchError(this.errorService.handleError<Route[]>('Routes',
|
||||||
|
'fetchRoutes', []))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
storeRoute(route: Route): Observable<Route> {
|
||||||
|
if (environment.mockNetwork) {
|
||||||
|
this.knownRoutes.set(route.id, route);
|
||||||
|
this.storeRoutesInLocalStorage();
|
||||||
|
return of(route);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.http.put<Route>(
|
||||||
|
this.routesURL + '/' + encodeURIComponent(route.id),
|
||||||
|
route,
|
||||||
|
this.httpOptions
|
||||||
|
).pipe(
|
||||||
|
catchError(this.errorService.handleError<Route>('Routes',
|
||||||
|
'storeRoute', route))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteRoute(route: Route): Observable<ArrayBuffer> {
|
||||||
|
if (environment.mockNetwork) {
|
||||||
|
this.knownRoutes.delete(route.id);
|
||||||
|
this.storeRoutesInLocalStorage();
|
||||||
|
return of(new ArrayBuffer(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.http.delete<ArrayBuffer>(
|
||||||
|
this.routesURL + '/' + encodeURIComponent(route.id),
|
||||||
|
this.httpOptions
|
||||||
|
).pipe(
|
||||||
|
catchError(this.errorService.handleError<ArrayBuffer>('Route',
|
||||||
|
'deleteRoute', new ArrayBuffer(0)))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private storeRoutesInLocalStorage() {
|
||||||
|
this.routes = Array.from(this.knownRoutes.values());
|
||||||
|
localStorage.setItem("routes", JSON.stringify(this.routes));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
|
import {RoutesStoreService} from './routes-store.service';
|
||||||
|
|
||||||
|
describe('RoutesStoreService', () => {
|
||||||
|
let service: RoutesStoreService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(RoutesStoreService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,64 @@
|
||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import {Store} from "@ngrx/store";
|
||||||
|
import {RouteService} from "./route.service";
|
||||||
|
import {RoutesState} from "../store/routes.reducer";
|
||||||
|
import {filter, finalize, Observable, share, switchMap, tap, using} from "rxjs";
|
||||||
|
import {allRoutes, allStations, needRoutes, needStations} from "../store";
|
||||||
|
import {
|
||||||
|
loadAllRoutesAction,
|
||||||
|
loadAllRoutesCancelledAction,
|
||||||
|
loadAllRoutesFinishedAction,
|
||||||
|
loadAllStationsAction,
|
||||||
|
loadAllStationsCancelledAction,
|
||||||
|
loadAllStationsFinishedAction
|
||||||
|
} from "../store/routes.actions";
|
||||||
|
import {Route} from "../model/route";
|
||||||
|
import {Station} from "../model/station";
|
||||||
|
import {StationService} from "./station.service";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class RoutesStoreService {
|
||||||
|
|
||||||
|
constructor(private readonly routeService: RouteService,
|
||||||
|
private readonly stationService: StationService,
|
||||||
|
private readonly store: Store<RoutesState>) {
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoutes$() {
|
||||||
|
return using(
|
||||||
|
() => this.loadRoutes$().subscribe(),
|
||||||
|
() => this.store.select(allRoutes())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadRoutes$(): Observable<Route[]> {
|
||||||
|
return this.store.select(needRoutes()).pipe(
|
||||||
|
filter(needRoutes => needRoutes),
|
||||||
|
tap(() => this.store.dispatch(loadAllRoutesAction())),
|
||||||
|
switchMap(() => this.routeService.fetchRoutes()),
|
||||||
|
tap((routes) => this.store.dispatch(loadAllRoutesFinishedAction({payload: routes}))),
|
||||||
|
finalize(() => this.store.dispatch(loadAllRoutesCancelledAction())),
|
||||||
|
share()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getStations$() {
|
||||||
|
return using(
|
||||||
|
() => this.loadStations$().subscribe(),
|
||||||
|
() => this.store.select(allStations())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadStations$(): Observable<Station[]> {
|
||||||
|
return this.store.select(needStations()).pipe(
|
||||||
|
filter(needStations => needStations),
|
||||||
|
tap(() => this.store.dispatch(loadAllStationsAction())),
|
||||||
|
switchMap(() => this.stationService.fetchStations()),
|
||||||
|
tap((stations) => this.store.dispatch(loadAllStationsFinishedAction({payload: stations}))),
|
||||||
|
finalize(() => this.store.dispatch(loadAllStationsCancelledAction())),
|
||||||
|
share()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
|
import {StationService} from './station.service';
|
||||||
|
|
||||||
|
describe('StationService', () => {
|
||||||
|
let service: StationService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(StationService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,38 @@
|
||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import {HttpClient, HttpHeaders} from "@angular/common/http";
|
||||||
|
import {environment} from "../../../environments/environment";
|
||||||
|
import {ErrorService} from "../../errors/error.service";
|
||||||
|
import {catchError, Observable, of} from "rxjs";
|
||||||
|
import {Route} from "../model/route";
|
||||||
|
import {Station} from "../model/station";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class StationService {
|
||||||
|
|
||||||
|
httpOptions = {
|
||||||
|
headers: new HttpHeaders({'Content-Type': 'application/json'})
|
||||||
|
};
|
||||||
|
private stationsURL = environment.backendURL + '/station';
|
||||||
|
|
||||||
|
private knownStations: Map<string, Station> = new Map<string, Station>();
|
||||||
|
private stations: Station[] = [];
|
||||||
|
|
||||||
|
constructor(private readonly http: HttpClient,
|
||||||
|
private readonly errorService: ErrorService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchStations(): Observable<Station[]> {
|
||||||
|
if (environment.mockNetwork) {
|
||||||
|
this.stations = JSON.parse(localStorage.getItem("stations") || '[]');
|
||||||
|
return of(this.stations);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.http.get<Route[]>(this.stationsURL, this.httpOptions)
|
||||||
|
.pipe(
|
||||||
|
catchError(this.errorService.handleError<Route[]>('Routes',
|
||||||
|
'fetchStations', []))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
import {createFeatureSelector, createSelector} from "@ngrx/store";
|
||||||
|
import {RoutesState} from "./routes.reducer";
|
||||||
|
import {FunctionalEffect} from "@ngrx/effects";
|
||||||
|
import {deleteRoute, storeRoute} from "./routes.effects";
|
||||||
|
|
||||||
|
export const featureStateName = 'routes';
|
||||||
|
|
||||||
|
export const routesEffects: Record<string, FunctionalEffect> = {
|
||||||
|
storeRoute: storeRoute,
|
||||||
|
deleteRoute: deleteRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getRoutesFeatureState = createFeatureSelector<RoutesState>(
|
||||||
|
featureStateName
|
||||||
|
);
|
||||||
|
|
||||||
|
export const needRoutes = () => createSelector(
|
||||||
|
getRoutesFeatureState,
|
||||||
|
(state: RoutesState) => state.needRoutes
|
||||||
|
);
|
||||||
|
|
||||||
|
export const allRoutes = () => createSelector(
|
||||||
|
getRoutesFeatureState,
|
||||||
|
(state: RoutesState) => state.routes
|
||||||
|
);
|
||||||
|
|
||||||
|
export const allCountries = () => createSelector(
|
||||||
|
getRoutesFeatureState,
|
||||||
|
(state: RoutesState) => state.countries
|
||||||
|
);
|
||||||
|
|
||||||
|
export const needStations = () => createSelector(
|
||||||
|
getRoutesFeatureState,
|
||||||
|
(state: RoutesState) => state.needStations
|
||||||
|
);
|
||||||
|
|
||||||
|
export const allStations = () => createSelector(
|
||||||
|
getRoutesFeatureState,
|
||||||
|
(state: RoutesState) => state.stations
|
||||||
|
);
|
|
@ -0,0 +1,71 @@
|
||||||
|
import {createAction, props} from "@ngrx/store";
|
||||||
|
import {Route} from "../model/route";
|
||||||
|
import {Station} from "../model/station";
|
||||||
|
|
||||||
|
export enum ActionTypes {
|
||||||
|
LoadAllRoutes = '[Routes] Load All Routes',
|
||||||
|
LoadAllRoutesFinished = '[Routes] Load All Routes Finished',
|
||||||
|
LoadAllRoutesCancelled = '[Routes] Load All Routes Cancelled',
|
||||||
|
|
||||||
|
LoadAllStations = '[Routes] Load All Stations',
|
||||||
|
LoadAllStationsFinished = '[Routes] Load All Stations Finished',
|
||||||
|
LoadAllStationsCancelled = '[Routes] Load All Stations Cancelled',
|
||||||
|
|
||||||
|
LoadSingleRoute = '[Routes] Load Single Route',
|
||||||
|
LoadSingleRouteFinished = '[Routes] Load Single Route Finished',
|
||||||
|
|
||||||
|
AddRoute = '[Routes] Add Route',
|
||||||
|
UpdateRoute = '[Routes] Update Route',
|
||||||
|
DeleteRoute = '[Routes] Delete Route',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadAllRoutesAction = createAction(
|
||||||
|
ActionTypes.LoadAllRoutes
|
||||||
|
);
|
||||||
|
|
||||||
|
export const loadAllRoutesFinishedAction = createAction(
|
||||||
|
ActionTypes.LoadAllRoutesFinished,
|
||||||
|
props<{ payload: Route[] }>()
|
||||||
|
);
|
||||||
|
|
||||||
|
export const loadAllRoutesCancelledAction = createAction(
|
||||||
|
ActionTypes.LoadAllRoutesCancelled
|
||||||
|
);
|
||||||
|
|
||||||
|
export const loadSingleRouteAction = createAction(
|
||||||
|
ActionTypes.LoadSingleRoute,
|
||||||
|
props<{ payload: string }>()
|
||||||
|
);
|
||||||
|
|
||||||
|
export const loadSingleRouteFinishedAction = createAction(
|
||||||
|
ActionTypes.LoadSingleRouteFinished,
|
||||||
|
props<{ payload: Route }>()
|
||||||
|
);
|
||||||
|
|
||||||
|
export const addRouteAction = createAction(
|
||||||
|
ActionTypes.AddRoute,
|
||||||
|
props<{ payload: Route }>()
|
||||||
|
);
|
||||||
|
|
||||||
|
export const updateRouteAction = createAction(
|
||||||
|
ActionTypes.UpdateRoute,
|
||||||
|
props<{ payload: Route }>()
|
||||||
|
);
|
||||||
|
|
||||||
|
export const deleteRouteAction = createAction(
|
||||||
|
ActionTypes.DeleteRoute,
|
||||||
|
props<{ payload: Route }>()
|
||||||
|
);
|
||||||
|
|
||||||
|
export const loadAllStationsAction = createAction(
|
||||||
|
ActionTypes.LoadAllStations
|
||||||
|
);
|
||||||
|
|
||||||
|
export const loadAllStationsFinishedAction = createAction(
|
||||||
|
ActionTypes.LoadAllStationsFinished,
|
||||||
|
props<{ payload: Station[] }>()
|
||||||
|
);
|
||||||
|
|
||||||
|
export const loadAllStationsCancelledAction = createAction(
|
||||||
|
ActionTypes.LoadAllStationsCancelled
|
||||||
|
);
|
|
@ -0,0 +1,29 @@
|
||||||
|
import {Actions, createEffect, ofType} from "@ngrx/effects";
|
||||||
|
import {inject} from "@angular/core";
|
||||||
|
import {RouteService} from "../service/route.service";
|
||||||
|
import {addRouteAction, deleteRouteAction, updateRouteAction} from "./routes.actions";
|
||||||
|
import {map, switchMap} from "rxjs";
|
||||||
|
|
||||||
|
export const storeRoute = createEffect((
|
||||||
|
actions$ = inject(Actions),
|
||||||
|
routesService = inject(RouteService)
|
||||||
|
) => {
|
||||||
|
return actions$.pipe(
|
||||||
|
ofType(addRouteAction, updateRouteAction),
|
||||||
|
map(action => action.payload),
|
||||||
|
switchMap((route) => routesService.storeRoute(route))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{functional: true, dispatch: false});
|
||||||
|
|
||||||
|
export const deleteRoute = createEffect((
|
||||||
|
actions$ = inject(Actions),
|
||||||
|
routesService = inject(RouteService)
|
||||||
|
) => {
|
||||||
|
return actions$.pipe(
|
||||||
|
ofType(deleteRouteAction),
|
||||||
|
map(action => action.payload),
|
||||||
|
switchMap((route) => routesService.deleteRoute(route))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{functional: true, dispatch: false});
|
|
@ -0,0 +1,82 @@
|
||||||
|
import {Route} from "../model/route";
|
||||||
|
import {createReducer, on} from "@ngrx/store";
|
||||||
|
import {
|
||||||
|
addRouteAction,
|
||||||
|
deleteRouteAction,
|
||||||
|
loadAllRoutesCancelledAction,
|
||||||
|
loadAllRoutesFinishedAction,
|
||||||
|
loadAllStationsCancelledAction,
|
||||||
|
loadAllStationsFinishedAction,
|
||||||
|
loadSingleRouteFinishedAction,
|
||||||
|
updateRouteAction
|
||||||
|
} from "./routes.actions";
|
||||||
|
import {Country} from "../model/country";
|
||||||
|
import {Station} from "../model/station";
|
||||||
|
|
||||||
|
export interface RoutesState {
|
||||||
|
needRoutes: boolean;
|
||||||
|
routes: Route[];
|
||||||
|
countries: Country[];
|
||||||
|
needStations: boolean;
|
||||||
|
stations: Station[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initialState: RoutesState = {
|
||||||
|
needRoutes: true,
|
||||||
|
routes: [],
|
||||||
|
countries: [
|
||||||
|
{
|
||||||
|
code: 'de',
|
||||||
|
name: $localize`Germany`
|
||||||
|
}
|
||||||
|
],
|
||||||
|
needStations: true,
|
||||||
|
stations: []
|
||||||
|
};
|
||||||
|
|
||||||
|
export const routesReducer = createReducer(
|
||||||
|
initialState,
|
||||||
|
on(loadAllRoutesFinishedAction, (state,
|
||||||
|
action) => ({
|
||||||
|
...state,
|
||||||
|
routes: [...action.payload]
|
||||||
|
})),
|
||||||
|
on(loadSingleRouteFinishedAction, (state,
|
||||||
|
action) => ({
|
||||||
|
...state,
|
||||||
|
selectedItem: action.payload
|
||||||
|
})),
|
||||||
|
on(loadAllRoutesFinishedAction, loadAllRoutesCancelledAction, (state, _) => ({
|
||||||
|
...state,
|
||||||
|
needRoutes: false
|
||||||
|
})),
|
||||||
|
on(loadAllStationsFinishedAction, (state,
|
||||||
|
action) => ({
|
||||||
|
...state,
|
||||||
|
stations: [...action.payload]
|
||||||
|
})),
|
||||||
|
on(loadAllStationsFinishedAction, loadAllStationsCancelledAction, (state, _) => ({
|
||||||
|
...state,
|
||||||
|
needStations: false
|
||||||
|
})),
|
||||||
|
on(addRouteAction, (state, action) => ({
|
||||||
|
...state,
|
||||||
|
routes: [...state.routes, action.payload]
|
||||||
|
})),
|
||||||
|
on(updateRouteAction, (state, action) => ({
|
||||||
|
...state,
|
||||||
|
routes: state.routes.map((oldRoute) => {
|
||||||
|
if (oldRoute.id == action.payload.id) {
|
||||||
|
return action.payload;
|
||||||
|
} else {
|
||||||
|
return oldRoute;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})),
|
||||||
|
on(deleteRouteAction, (state, action) => ({
|
||||||
|
...state,
|
||||||
|
routes: state.routes.filter((route) => {
|
||||||
|
return route.id != action.payload.id
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
);
|
|
@ -0,0 +1,133 @@
|
||||||
|
<ion-modal aria-labelledby="update-depot-title" [isOpen]="isOpen"
|
||||||
|
(keyup.escape)="cancel()"
|
||||||
|
backdrop-dismiss="false">
|
||||||
|
<ng-template>
|
||||||
|
<ion-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-title id="update-depot-title" i18n>Update Depot</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content class="ion-padding">
|
||||||
|
<form id="updateDepotForm" #updateDepot="ngForm" (ngSubmit)="confirm()">
|
||||||
|
<ion-input
|
||||||
|
i18n-label
|
||||||
|
label="ID"
|
||||||
|
labelPlacement="stacked"
|
||||||
|
type="text"
|
||||||
|
[required]="true"
|
||||||
|
name="id"
|
||||||
|
[readonly]="true"
|
||||||
|
[(ngModel)]="depot.id"
|
||||||
|
></ion-input>
|
||||||
|
<ion-input
|
||||||
|
i18n-label
|
||||||
|
label="Name"
|
||||||
|
labelPlacement="stacked"
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
[required]="true"
|
||||||
|
i18n-placeholder
|
||||||
|
placeholder="Name of the depot"
|
||||||
|
[(ngModel)]="depot.name"
|
||||||
|
></ion-input>
|
||||||
|
<ion-select
|
||||||
|
i18n-label
|
||||||
|
label="Nearest Station"
|
||||||
|
labelPlacement="stacked"
|
||||||
|
i18n-placeholder
|
||||||
|
placeholder="Closest station of the route"
|
||||||
|
name="nearestStation"
|
||||||
|
interface="popover"
|
||||||
|
[required]="true"
|
||||||
|
[compareWith]="compareWithStation"
|
||||||
|
[(ngModel)]="depot.nearestStation"
|
||||||
|
>
|
||||||
|
<ion-select-option *ngFor="let station of stations"
|
||||||
|
[value]="station">
|
||||||
|
{{ station.name }}
|
||||||
|
</ion-select-option>
|
||||||
|
</ion-select>
|
||||||
|
|
||||||
|
<h6 i18n>Tracks</h6>
|
||||||
|
<ion-list>
|
||||||
|
<ion-item>
|
||||||
|
<ion-label i18n class="bold">Name</ion-label>
|
||||||
|
<ion-label i18n class="bold">Capacity</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item-sliding *ngFor="let track of depot.tracks; index as i; trackBy: trackByTrack">
|
||||||
|
<ion-item>
|
||||||
|
<ion-input i18n-aria-label aria-label="Name" type="text" [value]="track.name"
|
||||||
|
(ionChange)="changeName(i, $event)"></ion-input>
|
||||||
|
<ion-input i18n-aria-label aria-label="Capacity" type="number" [value]="track.capacity"
|
||||||
|
(ionChange)="changeCapacity(i, $event)"></ion-input>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item-options>
|
||||||
|
<ion-item-option (click)="deleteTrack(track)" color="danger" i18n>
|
||||||
|
<ion-icon slot="start" [ios]="'trash-outline'" [md]="'trash-sharp'"></ion-icon>
|
||||||
|
Delete
|
||||||
|
</ion-item-option>
|
||||||
|
</ion-item-options>
|
||||||
|
</ion-item-sliding>
|
||||||
|
</ion-list>
|
||||||
|
<ion-button (click)="addTrack()">
|
||||||
|
<ion-label i18n>Add track</ion-label>
|
||||||
|
</ion-button>
|
||||||
|
|
||||||
|
<h6 i18n>Travel Durations to nearest station</h6>
|
||||||
|
<ion-list>
|
||||||
|
<ion-item>
|
||||||
|
<ion-label i18n class="bold">Formation</ion-label>
|
||||||
|
<ion-label i18n class="bold">Time (s)</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item-sliding
|
||||||
|
*ngFor="let travelDuration of depot.travelDurations; index as i; trackBy: trackByTravelDuration">
|
||||||
|
<ion-item>
|
||||||
|
<ion-select
|
||||||
|
i18n-aria-label
|
||||||
|
aria-label="Formation"
|
||||||
|
interface="popover"
|
||||||
|
[value]="travelDuration.formation"
|
||||||
|
(ionChange)="onSelectFormation($event, i)"
|
||||||
|
[compareWith]="compareWithFormation"
|
||||||
|
>
|
||||||
|
<ion-select-option [value]="travelDuration.formation">
|
||||||
|
{{ travelDuration.formation.name }}
|
||||||
|
</ion-select-option>
|
||||||
|
<ion-select-option *ngFor="let formation of unusedFormations$ | async" [value]="formation">
|
||||||
|
{{ formation.name }}
|
||||||
|
</ion-select-option>
|
||||||
|
</ion-select>
|
||||||
|
<ion-input i18n-aria-label aria-label="Time in seconds" type="number" [value]="travelDuration.time"
|
||||||
|
(ionChange)="changeTime(i, $event)"></ion-input>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item-options>
|
||||||
|
<ion-item-option (click)="deleteTravelDuration(travelDuration)" color="danger" i18n>
|
||||||
|
<ion-icon slot="start" [ios]="'trash-outline'" [md]="'trash-sharp'"></ion-icon>
|
||||||
|
Delete
|
||||||
|
</ion-item-option>
|
||||||
|
</ion-item-options>
|
||||||
|
</ion-item-sliding>
|
||||||
|
</ion-list>
|
||||||
|
<ion-button (click)="addTravelDuration($event)" [disabled]="(unusedFormations$ | async)?.length == 0">
|
||||||
|
<ion-label i18n>Add travel duration</ion-label>
|
||||||
|
</ion-button>
|
||||||
|
</form>
|
||||||
|
</ion-content>
|
||||||
|
<ion-footer>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-buttons slot="start">
|
||||||
|
<ion-button (click)="cancel()" i18n>Cancel</ion-button>
|
||||||
|
</ion-buttons>
|
||||||
|
<ion-buttons slot="end">
|
||||||
|
<ion-button form="updateDepotForm" type="submit" button-type="submit" [strong]="true" i18n
|
||||||
|
[disabled]="!updateDepot.form.valid">Confirm
|
||||||
|
</ion-button>
|
||||||
|
</ion-buttons>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-footer>
|
||||||
|
</ng-template>
|
||||||
|
</ion-modal>
|
||||||
|
|
||||||
|
<app-typeahead [isOpen]="isFormationPopoverOpen" (dismissed)="isFormationPopoverOpen = false"
|
||||||
|
(itemSelected)="selectFormation($event)" [items$]="formations$" [usedItems]="usedFormations"
|
||||||
|
[event]="clickEvent"></app-typeahead>
|
|
@ -0,0 +1,3 @@
|
||||||
|
.bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
|
import {UpdateDepotComponent} from './update-depot.component';
|
||||||
|
|
||||||
|
describe('UpdateDepotComponent', () => {
|
||||||
|
let component: UpdateDepotComponent;
|
||||||
|
let fixture: ComponentFixture<UpdateDepotComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [UpdateDepotComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(UpdateDepotComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,84 @@
|
||||||
|
import {Component, EventEmitter, Input, Output, ViewChild} from '@angular/core';
|
||||||
|
import {CommonModule} from '@angular/common';
|
||||||
|
import {DepotComponent} from "../common/depot-component";
|
||||||
|
import {FormsModule} from "@angular/forms";
|
||||||
|
import {
|
||||||
|
IonButton,
|
||||||
|
IonButtons,
|
||||||
|
IonContent,
|
||||||
|
IonFooter,
|
||||||
|
IonHeader,
|
||||||
|
IonIcon,
|
||||||
|
IonInput,
|
||||||
|
IonItem,
|
||||||
|
IonItemOption,
|
||||||
|
IonItemOptions,
|
||||||
|
IonItemSliding,
|
||||||
|
IonLabel,
|
||||||
|
IonList,
|
||||||
|
IonModal,
|
||||||
|
IonSelect,
|
||||||
|
IonSelectOption,
|
||||||
|
IonTitle,
|
||||||
|
IonToolbar
|
||||||
|
} from "@ionic/angular/standalone";
|
||||||
|
import {TypeaheadComponent} from "../../typeahead/typeahead.component";
|
||||||
|
import {addIcons} from "ionicons";
|
||||||
|
import {trashOutline, trashSharp} from "ionicons/icons";
|
||||||
|
import {Station} from "../model/station";
|
||||||
|
import {Depot} from "../model/depot";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-update-depot',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, FormsModule, IonButton, IonButtons, IonContent, IonFooter, IonHeader, IonIcon, IonInput, IonItem, IonItemOption, IonItemOptions, IonItemSliding, IonLabel, IonList, IonModal, IonSelect, IonSelectOption, IonTitle, IonToolbar, TypeaheadComponent],
|
||||||
|
templateUrl: './update-depot.component.html',
|
||||||
|
styleUrl: './update-depot.component.scss'
|
||||||
|
})
|
||||||
|
export class UpdateDepotComponent extends DepotComponent {
|
||||||
|
@Input({required: true}) isOpen: boolean = false;
|
||||||
|
@Output() dismissed: EventEmitter<boolean> = new EventEmitter<boolean>();
|
||||||
|
@Output() updatedDepotChange: EventEmitter<Depot> = new EventEmitter();
|
||||||
|
|
||||||
|
@ViewChild(IonModal) modal: IonModal | undefined;
|
||||||
|
|
||||||
|
depotOnOpen: Depot = {...this.depot};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
addIcons({
|
||||||
|
trashOutline,
|
||||||
|
trashSharp,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Input({required: true}) set updatedDepot(newValue: Depot) {
|
||||||
|
this.depot = {...newValue};
|
||||||
|
this.depotOnOpen = {...newValue};
|
||||||
|
this.usedFormations = this.depot.travelDurations.map(duration => duration.formation);
|
||||||
|
this.updateUnusedFormations();
|
||||||
|
}
|
||||||
|
|
||||||
|
_stations?: Station[];
|
||||||
|
@Input({required: true}) set stations(newStations: Station[]) {
|
||||||
|
if (newStations !== null) {
|
||||||
|
this._stations = newStations;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get stations() {
|
||||||
|
return this._stations || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
this.dismissed.emit(true);
|
||||||
|
this.depot = this.depotOnOpen;
|
||||||
|
this.usedFormations = this.depot.travelDurations.map(duration => duration.formation);
|
||||||
|
this.updateUnusedFormations();
|
||||||
|
}
|
||||||
|
|
||||||
|
confirm() {
|
||||||
|
this.updatedDepotChange.emit({...this.depot});
|
||||||
|
this.dismissed.emit(true);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,107 @@
|
||||||
|
<ion-modal aria-labelledby="update-portal-title" [isOpen]="isOpen"
|
||||||
|
(keyup.escape)="cancel()"
|
||||||
|
backdrop-dismiss="false">
|
||||||
|
<ng-template>
|
||||||
|
<ion-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-title id="update-portal-title" i18n>Update Portal</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content class="ion-padding">
|
||||||
|
<form id="updatePortalForm" #updatePortal="ngForm" (ngSubmit)="confirm()">
|
||||||
|
<ion-input
|
||||||
|
i18n-label
|
||||||
|
label="ID"
|
||||||
|
labelPlacement="stacked"
|
||||||
|
type="text"
|
||||||
|
[required]="true"
|
||||||
|
name="id"
|
||||||
|
[readonly]="true"
|
||||||
|
[(ngModel)]="portal.id"
|
||||||
|
></ion-input>
|
||||||
|
<ion-input
|
||||||
|
i18n-label
|
||||||
|
label="Name"
|
||||||
|
labelPlacement="stacked"
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
[required]="true"
|
||||||
|
i18n-placeholder
|
||||||
|
placeholder="Name of the portal"
|
||||||
|
[(ngModel)]="portal.name"
|
||||||
|
></ion-input>
|
||||||
|
<ion-select
|
||||||
|
i18n-label
|
||||||
|
label="Nearest Station"
|
||||||
|
labelPlacement="stacked"
|
||||||
|
i18n-placeholder
|
||||||
|
placeholder="Closest station of the route"
|
||||||
|
name="nearestStation"
|
||||||
|
interface="popover"
|
||||||
|
[required]="true"
|
||||||
|
[compareWith]="compareWithStation"
|
||||||
|
[(ngModel)]="portal.nearestStation"
|
||||||
|
>
|
||||||
|
<ion-select-option *ngFor="let station of stations"
|
||||||
|
[value]="station">
|
||||||
|
{{ station.name }}
|
||||||
|
</ion-select-option>
|
||||||
|
</ion-select>
|
||||||
|
|
||||||
|
<h6 i18n>Travel Durations to nearest station</h6>
|
||||||
|
<ion-list>
|
||||||
|
<ion-item>
|
||||||
|
<ion-label i18n class="bold">Formation</ion-label>
|
||||||
|
<ion-label i18n class="bold">Time (s)</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item-sliding *ngFor="let travelDuration of portal.travelDurations; index as i; trackBy: trackBy">
|
||||||
|
<ion-item>
|
||||||
|
<ion-select
|
||||||
|
i18n-aria-label
|
||||||
|
aria-label="Formation"
|
||||||
|
interface="popover"
|
||||||
|
[value]="travelDuration.formation"
|
||||||
|
(ionChange)="onSelectFormation($event, i)"
|
||||||
|
[compareWith]="compareWithFormation"
|
||||||
|
>
|
||||||
|
<ion-select-option [value]="travelDuration.formation">
|
||||||
|
{{ travelDuration.formation.name }}
|
||||||
|
</ion-select-option>
|
||||||
|
<ion-select-option *ngFor="let formation of unusedFormations$ | async" [value]="formation">
|
||||||
|
{{ formation.name }}
|
||||||
|
</ion-select-option>
|
||||||
|
</ion-select>
|
||||||
|
<ion-input i18n-aria-label aria-label="Time in seconds" type="number" [value]="travelDuration.time"
|
||||||
|
(ionChange)="changeTime(i, $event)"></ion-input>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item-options>
|
||||||
|
<ion-item-option (click)="deleteTravelDuration(travelDuration)" color="danger" i18n>
|
||||||
|
<ion-icon slot="start" [ios]="'trash-outline'" [md]="'trash-sharp'"></ion-icon>
|
||||||
|
Delete
|
||||||
|
</ion-item-option>
|
||||||
|
</ion-item-options>
|
||||||
|
</ion-item-sliding>
|
||||||
|
</ion-list>
|
||||||
|
<ion-button (click)="addTravelDuration($event)" [disabled]="(unusedFormations$ | async)?.length == 0">
|
||||||
|
<ion-label i18n>Add travel duration</ion-label>
|
||||||
|
</ion-button>
|
||||||
|
</form>
|
||||||
|
</ion-content>
|
||||||
|
<ion-footer>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-buttons slot="start">
|
||||||
|
<ion-button (click)="cancel()" i18n>Cancel</ion-button>
|
||||||
|
</ion-buttons>
|
||||||
|
<ion-buttons slot="end">
|
||||||
|
<ion-button form="updatePortalForm" type="submit" button-type="submit" [strong]="true" i18n
|
||||||
|
[disabled]="!updatePortal.form.valid">Confirm
|
||||||
|
</ion-button>
|
||||||
|
</ion-buttons>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-footer>
|
||||||
|
</ng-template>
|
||||||
|
</ion-modal>
|
||||||
|
|
||||||
|
<app-typeahead [isOpen]="isFormationPopoverOpen" (dismissed)="isFormationPopoverOpen = false"
|
||||||
|
(itemSelected)="selectFormation($event)" [items$]="formations$" [usedItems]="usedFormations"
|
||||||
|
[event]="clickEvent"></app-typeahead>
|
|
@ -0,0 +1,3 @@
|
||||||
|
.bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
|
import {UpdatePortalComponent} from './update-portal.component';
|
||||||
|
|
||||||
|
describe('UpdatePortalComponent', () => {
|
||||||
|
let component: UpdatePortalComponent;
|
||||||
|
let fixture: ComponentFixture<UpdatePortalComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [UpdatePortalComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(UpdatePortalComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,108 @@
|
||||||
|
import {Component, EventEmitter, Input, Output, ViewChild} from '@angular/core';
|
||||||
|
import {PortalComponent} from "../common/portal-component";
|
||||||
|
import {addIcons} from "ionicons";
|
||||||
|
import {trashOutline, trashSharp} from "ionicons/icons";
|
||||||
|
import {AsyncPipe, NgForOf} from "@angular/common";
|
||||||
|
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
|
||||||
|
import {
|
||||||
|
IonButton,
|
||||||
|
IonButtons,
|
||||||
|
IonContent,
|
||||||
|
IonFooter,
|
||||||
|
IonHeader,
|
||||||
|
IonIcon,
|
||||||
|
IonInput,
|
||||||
|
IonItem,
|
||||||
|
IonItemOption,
|
||||||
|
IonItemOptions,
|
||||||
|
IonItemSliding,
|
||||||
|
IonLabel,
|
||||||
|
IonList,
|
||||||
|
IonModal,
|
||||||
|
IonSelect,
|
||||||
|
IonSelectOption,
|
||||||
|
IonTitle,
|
||||||
|
IonToolbar
|
||||||
|
} from "@ionic/angular/standalone";
|
||||||
|
import {TypeaheadComponent} from "../../typeahead/typeahead.component";
|
||||||
|
import {Portal} from "../model/portal";
|
||||||
|
import {Station} from "../model/station";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-update-portal',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
NgForOf,
|
||||||
|
FormsModule,
|
||||||
|
IonButton,
|
||||||
|
IonButtons,
|
||||||
|
IonContent,
|
||||||
|
IonFooter,
|
||||||
|
IonHeader,
|
||||||
|
IonIcon,
|
||||||
|
IonInput,
|
||||||
|
IonItem,
|
||||||
|
IonItemOption,
|
||||||
|
IonItemOptions,
|
||||||
|
IonItemSliding,
|
||||||
|
IonLabel,
|
||||||
|
IonList,
|
||||||
|
IonModal,
|
||||||
|
IonSelect,
|
||||||
|
IonSelectOption,
|
||||||
|
IonTitle,
|
||||||
|
IonToolbar,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
TypeaheadComponent,
|
||||||
|
AsyncPipe,
|
||||||
|
],
|
||||||
|
templateUrl: './update-portal.component.html',
|
||||||
|
styleUrl: './update-portal.component.scss'
|
||||||
|
})
|
||||||
|
export class UpdatePortalComponent extends PortalComponent {
|
||||||
|
@Input({required: true}) isOpen: boolean = false;
|
||||||
|
@Output() dismissed: EventEmitter<boolean> = new EventEmitter<boolean>();
|
||||||
|
@Output() updatedPortalChange: EventEmitter<Portal> = new EventEmitter();
|
||||||
|
|
||||||
|
@ViewChild(IonModal) modal: IonModal | undefined;
|
||||||
|
|
||||||
|
portalOnOpen: Portal = {...this.portal};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
addIcons({
|
||||||
|
trashOutline,
|
||||||
|
trashSharp,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Input({required: true}) set updatedPortal(newValue: Portal) {
|
||||||
|
this.portal = {...newValue};
|
||||||
|
this.portalOnOpen = {...newValue};
|
||||||
|
this.usedFormations = this.portal.travelDurations.map(duration => duration.formation);
|
||||||
|
this.updateUnusedFormations();
|
||||||
|
}
|
||||||
|
|
||||||
|
_stations?: Station[];
|
||||||
|
@Input({required: true}) set stations(newStations: Station[]) {
|
||||||
|
if (newStations !== null) {
|
||||||
|
this._stations = newStations;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get stations() {
|
||||||
|
return this._stations || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
this.dismissed.emit(true);
|
||||||
|
this.portal = this.portalOnOpen;
|
||||||
|
this.usedFormations = this.portal.travelDurations.map(duration => duration.formation);
|
||||||
|
this.updateUnusedFormations();
|
||||||
|
}
|
||||||
|
|
||||||
|
confirm() {
|
||||||
|
this.updatedPortalChange.emit(this.portal);
|
||||||
|
this.dismissed.emit(true);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,142 @@
|
||||||
|
<ion-modal aria-labelledby="update-route-title" [isOpen]="isOpen"
|
||||||
|
(keyup.escape)="cancel()"
|
||||||
|
backdrop-dismiss="false">
|
||||||
|
<ng-template>
|
||||||
|
<ion-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-title id="update-route-title" i18n>Update Route</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content class="ion-padding">
|
||||||
|
<form id="updateRouteForm" #updateRoute="ngForm" (ngSubmit)="confirm()">
|
||||||
|
<ion-input
|
||||||
|
i18n-label
|
||||||
|
label="ID"
|
||||||
|
labelPlacement="stacked"
|
||||||
|
type="text"
|
||||||
|
[required]="true"
|
||||||
|
name="id"
|
||||||
|
[readonly]="true"
|
||||||
|
[(ngModel)]="route.id"
|
||||||
|
></ion-input>
|
||||||
|
<ion-input
|
||||||
|
i18n-label
|
||||||
|
label="Name"
|
||||||
|
labelPlacement="stacked"
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
[required]="true"
|
||||||
|
i18n-placeholder
|
||||||
|
placeholder="Name of the route"
|
||||||
|
[(ngModel)]="route.name"
|
||||||
|
></ion-input>
|
||||||
|
<ion-select
|
||||||
|
i18n-label
|
||||||
|
label="Country"
|
||||||
|
labelPlacement="stacked"
|
||||||
|
i18n-placeholder
|
||||||
|
placeholder="Country of the route"
|
||||||
|
name="country"
|
||||||
|
interface="popover"
|
||||||
|
[compareWith]="compareWithCountry"
|
||||||
|
[(ngModel)]="route.country"
|
||||||
|
>
|
||||||
|
<ion-select-option *ngFor="let country of countries$ | async"
|
||||||
|
[value]="country">
|
||||||
|
{{ country.name }}
|
||||||
|
</ion-select-option>
|
||||||
|
</ion-select>
|
||||||
|
<h6 i18n>Stations</h6>
|
||||||
|
<ion-list>
|
||||||
|
<ion-reorder-group [disabled]="false" (ionItemReorder)="handleReorderStations($event)">
|
||||||
|
<ion-item-sliding *ngFor="let station of route.stations; index as i; trackBy: trackBy">
|
||||||
|
<ion-item [button]="true" (click)="openPopoverStation($event, i)">
|
||||||
|
<ion-reorder slot="start"></ion-reorder>
|
||||||
|
<ion-label class="no-pointer-events">{{ station.name }}</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item-options>
|
||||||
|
<ion-item-option (click)="deleteStation(station)" color="danger" i18n>
|
||||||
|
<ion-icon slot="start" [ios]="'trash-outline'" [md]="'trash-sharp'"></ion-icon>
|
||||||
|
Delete
|
||||||
|
</ion-item-option>
|
||||||
|
</ion-item-options>
|
||||||
|
</ion-item-sliding>
|
||||||
|
</ion-reorder-group>
|
||||||
|
</ion-list>
|
||||||
|
<ion-button (click)="openPopoverStation($event)" [disabled]="(unusedStations$ | async)?.length == 0">
|
||||||
|
<ion-label i18n class="no-pointer-events">Add station</ion-label>
|
||||||
|
</ion-button>
|
||||||
|
|
||||||
|
<h6 i18n>Portals</h6>
|
||||||
|
<ion-list>
|
||||||
|
<ion-item-sliding *ngFor="let portal of route.portals; index as i; trackBy: trackBy">
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>{{ portal.name }}</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item-options>
|
||||||
|
<ion-item-option (click)="updatePortal(portal, i)" color="secondary" i18n>
|
||||||
|
<ion-icon slot="start" [ios]="'pencil-outline'" [md]="'pencil-sharp'"></ion-icon>
|
||||||
|
Update
|
||||||
|
</ion-item-option>
|
||||||
|
<ion-item-option (click)="deletePortal(portal)" color="danger" i18n>
|
||||||
|
<ion-icon slot="start" [ios]="'trash-outline'" [md]="'trash-sharp'"></ion-icon>
|
||||||
|
Delete
|
||||||
|
</ion-item-option>
|
||||||
|
</ion-item-options>
|
||||||
|
</ion-item-sliding>
|
||||||
|
</ion-list>
|
||||||
|
<ion-button (click)="addPortal()">
|
||||||
|
<ion-label i18n>Add portal</ion-label>
|
||||||
|
</ion-button>
|
||||||
|
|
||||||
|
<h6 i18n>Depots</h6>
|
||||||
|
<ion-list>
|
||||||
|
<ion-item-sliding *ngFor="let depot of route.depots; index as i; trackBy: trackBy">
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>{{ depot.name }}</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item-options>
|
||||||
|
<ion-item-option (click)="updateDepot(depot, i)" color="secondary" i18n>
|
||||||
|
<ion-icon slot="start" [ios]="'pencil-outline'" [md]="'pencil-sharp'"></ion-icon>
|
||||||
|
Update
|
||||||
|
</ion-item-option>
|
||||||
|
<ion-item-option (click)="deleteDepot(depot)" color="danger" i18n>
|
||||||
|
<ion-icon slot="start" [ios]="'trash-outline'" [md]="'trash-sharp'"></ion-icon>
|
||||||
|
Delete
|
||||||
|
</ion-item-option>
|
||||||
|
</ion-item-options>
|
||||||
|
</ion-item-sliding>
|
||||||
|
</ion-list>
|
||||||
|
<ion-button (click)="addDepot()">
|
||||||
|
<ion-label i18n>Add depot</ion-label>
|
||||||
|
</ion-button>
|
||||||
|
</form>
|
||||||
|
</ion-content>
|
||||||
|
<ion-footer>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-buttons slot="start">
|
||||||
|
<ion-button (click)="cancel()" i18n>Cancel</ion-button>
|
||||||
|
</ion-buttons>
|
||||||
|
<ion-buttons slot="end">
|
||||||
|
<ion-button form="updateRouteForm" type="submit" button-type="submit" [strong]="true" i18n
|
||||||
|
[disabled]="!updateRoute.form.valid || route.stations.length == 0">Confirm
|
||||||
|
</ion-button>
|
||||||
|
</ion-buttons>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-footer>
|
||||||
|
</ng-template>
|
||||||
|
</ion-modal>
|
||||||
|
|
||||||
|
<app-create-portal [isOpen]="isCreatePortalModalOpen" (dismissed)="isCreatePortalModalOpen = false"
|
||||||
|
[stations]="route.stations" (createdPortal)="insertPortal($event)"></app-create-portal>
|
||||||
|
<app-update-portal [isOpen]="isUpdatePortalModalOpen" (dismissed)="isUpdatePortalModalOpen = false"
|
||||||
|
[stations]="route.stations" [(updatedPortal)]="updatedPortal"></app-update-portal>
|
||||||
|
|
||||||
|
<app-create-depot [isOpen]="isCreateDepotModalOpen" (dismissed)="isCreateDepotModalOpen = false"
|
||||||
|
[stations]="route.stations" (createdDepot)="insertDepot($event)"></app-create-depot>
|
||||||
|
<app-update-depot [isOpen]="isUpdateDepotModalOpen" (dismissed)="isUpdateDepotModalOpen = false"
|
||||||
|
[stations]="route.stations" [(updatedDepot)]="updatedDepot"></app-update-depot>
|
||||||
|
|
||||||
|
<app-typeahead [isOpen]="isStationPopoverOpen" (dismissed)="isStationPopoverOpen = false"
|
||||||
|
(itemSelected)="selectStation($event)" [usedItems]="route.stations" [items$]="stations$"
|
||||||
|
[event]="clickEvent"></app-typeahead>
|
|
@ -0,0 +1,3 @@
|
||||||
|
.no-pointer-events {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
|
import {UpdateRouteComponent} from './update-route.component';
|
||||||
|
|
||||||
|
describe('UpdateRouteComponent', () => {
|
||||||
|
let component: UpdateRouteComponent;
|
||||||
|
let fixture: ComponentFixture<UpdateRouteComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [UpdateRouteComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(UpdateRouteComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,123 @@
|
||||||
|
import {Component, EventEmitter, Input, Output, ViewChild} from '@angular/core';
|
||||||
|
import {
|
||||||
|
IonButton,
|
||||||
|
IonButtons,
|
||||||
|
IonContent,
|
||||||
|
IonFooter,
|
||||||
|
IonHeader,
|
||||||
|
IonIcon,
|
||||||
|
IonInput,
|
||||||
|
IonItem,
|
||||||
|
IonItemOption,
|
||||||
|
IonItemOptions,
|
||||||
|
IonItemSliding,
|
||||||
|
IonLabel,
|
||||||
|
IonList,
|
||||||
|
IonModal,
|
||||||
|
IonReorder,
|
||||||
|
IonReorderGroup,
|
||||||
|
IonSelect,
|
||||||
|
IonSelectOption,
|
||||||
|
IonTitle,
|
||||||
|
IonToolbar
|
||||||
|
} from "@ionic/angular/standalone";
|
||||||
|
import {Route} from "../model/route";
|
||||||
|
import {updateRouteAction} from "../store/routes.actions";
|
||||||
|
import {AsyncPipe, NgForOf} from "@angular/common";
|
||||||
|
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
|
||||||
|
import {CreatePortalComponent} from "../create-portal/create-portal.component";
|
||||||
|
import {TypeaheadComponent} from "../../typeahead/typeahead.component";
|
||||||
|
import {addIcons} from "ionicons";
|
||||||
|
import {pencilOutline, pencilSharp, trashOutline, trashSharp} from "ionicons/icons";
|
||||||
|
import {RouteComponent} from "../common/route-component";
|
||||||
|
import {UpdatePortalComponent} from "../update-portal/update-portal.component";
|
||||||
|
import {CreateDepotComponent} from "../create-depot/create-depot.component";
|
||||||
|
import {UpdateDepotComponent} from "../update-depot/update-depot.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-update-route',
|
||||||
|
standalone: true,
|
||||||
|
templateUrl: './update-route.component.html',
|
||||||
|
imports: [
|
||||||
|
AsyncPipe,
|
||||||
|
FormsModule,
|
||||||
|
IonButton,
|
||||||
|
IonButtons,
|
||||||
|
IonContent,
|
||||||
|
IonFooter,
|
||||||
|
IonHeader,
|
||||||
|
IonInput,
|
||||||
|
IonItem,
|
||||||
|
IonModal,
|
||||||
|
IonSelect,
|
||||||
|
IonSelectOption,
|
||||||
|
IonTitle,
|
||||||
|
IonToolbar,
|
||||||
|
NgForOf,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
CreatePortalComponent,
|
||||||
|
IonIcon,
|
||||||
|
IonItemOption,
|
||||||
|
IonItemOptions,
|
||||||
|
IonItemSliding,
|
||||||
|
IonLabel,
|
||||||
|
IonList,
|
||||||
|
IonReorder,
|
||||||
|
IonReorderGroup,
|
||||||
|
TypeaheadComponent,
|
||||||
|
UpdatePortalComponent,
|
||||||
|
CreateDepotComponent,
|
||||||
|
UpdateDepotComponent
|
||||||
|
],
|
||||||
|
styleUrl: './update-route.component.scss'
|
||||||
|
})
|
||||||
|
export class UpdateRouteComponent extends RouteComponent {
|
||||||
|
@Output() dismissed: EventEmitter<boolean> = new EventEmitter<boolean>();
|
||||||
|
|
||||||
|
@ViewChild(IonModal) modal: IonModal | undefined;
|
||||||
|
routeOnOpen: Route = {...this.route};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
addIcons({
|
||||||
|
trashOutline,
|
||||||
|
trashSharp,
|
||||||
|
pencilOutline,
|
||||||
|
pencilSharp,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private _isOpen: boolean = false;
|
||||||
|
@Input({required: true}) set isOpen(newValue: boolean) {
|
||||||
|
if (!this._isOpen && newValue) {
|
||||||
|
this.routeOnOpen = {...this.route};
|
||||||
|
}
|
||||||
|
this._isOpen = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isOpen() {
|
||||||
|
return this._isOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Input({required: true}) set updatedRoute(newValue: Route) {
|
||||||
|
this.route = {...newValue};
|
||||||
|
this.routeOnOpen = {...newValue};
|
||||||
|
this.updateUnusedStations();
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
this.dismissed.emit(true);
|
||||||
|
this.route = this.routeOnOpen;
|
||||||
|
this.updateUnusedStations();
|
||||||
|
}
|
||||||
|
|
||||||
|
confirm() {
|
||||||
|
this.route.firstStation = this.route.stations[0];
|
||||||
|
this.route.lastStation = this.route.stations[this.route.stations.length - 1];
|
||||||
|
this.route.numberOfStations = this.route.stations.length;
|
||||||
|
this.store.dispatch(updateRouteAction({
|
||||||
|
payload: {...this.route}
|
||||||
|
}))
|
||||||
|
this.dismissed.emit(true);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
export interface Item {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
<ion-popover [isOpen]="isOpen" (ionPopoverDidDismiss)="dismissed.emit(true)" [event]="event" [side]="side"
|
||||||
|
[alignment]="alignment">
|
||||||
|
<ng-template>
|
||||||
|
<ion-content class="ion-padding">
|
||||||
|
<ion-searchbar [debounce]="debounce" (ionInput)="filterItems($event)"></ion-searchbar>
|
||||||
|
<ion-list [inset]="true">
|
||||||
|
<ion-item [button]="true" *ngFor="let item of filteredItems$ | async" (click)="selectItem(item)">
|
||||||
|
<ion-label>{{ item.name }}</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
@if ((filteredItems$ | async)?.length == 0) {
|
||||||
|
<ion-item>
|
||||||
|
<ion-label i18n>No items available</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
}
|
||||||
|
</ion-list>
|
||||||
|
</ion-content>
|
||||||
|
</ng-template>
|
||||||
|
</ion-popover>
|
|
@ -0,0 +1,23 @@
|
||||||
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
|
import {TypeaheadComponent} from './typeahead.component';
|
||||||
|
|
||||||
|
describe('TypeaheadComponent', () => {
|
||||||
|
let component: TypeaheadComponent;
|
||||||
|
let fixture: ComponentFixture<TypeaheadComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [TypeaheadComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(TypeaheadComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,64 @@
|
||||||
|
import {Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, ViewChild} from '@angular/core';
|
||||||
|
import {AsyncPipe, NgForOf} from '@angular/common';
|
||||||
|
import {IonContent, IonItem, IonLabel, IonList, IonPopover, IonSearchbar} from "@ionic/angular/standalone";
|
||||||
|
import {Item} from "./item";
|
||||||
|
import {SearchbarCustomEvent} from "@ionic/angular";
|
||||||
|
import {map, Observable} from "rxjs";
|
||||||
|
|
||||||
|
type PositionAlign = "start" | "center" | "end";
|
||||||
|
type PositionSide = "top" | "right" | "bottom" | "left" | "start" | "end";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-typeahead',
|
||||||
|
standalone: true,
|
||||||
|
imports: [IonContent, IonItem, IonLabel, IonList, IonPopover, IonSearchbar, AsyncPipe, NgForOf],
|
||||||
|
templateUrl: './typeahead.component.html',
|
||||||
|
styleUrl: './typeahead.component.scss'
|
||||||
|
})
|
||||||
|
export class TypeaheadComponent<T extends Item> implements OnChanges {
|
||||||
|
@Input() debounce: number = 300;
|
||||||
|
@Input() event: Event = new Event('');
|
||||||
|
@Input() alignment: PositionAlign = 'center';
|
||||||
|
@Input() side: PositionSide = 'bottom';
|
||||||
|
@Output() dismissed: EventEmitter<boolean> = new EventEmitter<boolean>();
|
||||||
|
@Output() itemSelected: EventEmitter<T> = new EventEmitter<T>();
|
||||||
|
@ViewChild('popover') popover: any;
|
||||||
|
|
||||||
|
@Input({required: true}) isOpen: boolean = false;
|
||||||
|
@Input({required: true}) items$!: Observable<T[]>;
|
||||||
|
@Input() usedItems: T[] = [];
|
||||||
|
_filteredItems!: Observable<T[]>;
|
||||||
|
|
||||||
|
ngOnChanges(changes: SimpleChanges) {
|
||||||
|
const readyForUpdate = this.items$ != null && this.usedItems != null && this.isOpen != null;
|
||||||
|
if (readyForUpdate
|
||||||
|
&& changes['isOpen']?.currentValue
|
||||||
|
&& !changes['isOpen']?.previousValue) {
|
||||||
|
this.updateFilteredItems();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get filteredItems$() {
|
||||||
|
return this._filteredItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateFilteredItems() {
|
||||||
|
this._filteredItems = this.items$.pipe(
|
||||||
|
map(items => items.filter(item => !this.usedItems.some(usedItem => usedItem.id == item.id)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
filterItems(event: SearchbarCustomEvent) {
|
||||||
|
if (typeof event.detail.value === "string") {
|
||||||
|
const searchValue = event.detail.value.toLowerCase();
|
||||||
|
this._filteredItems = this.items$.pipe(
|
||||||
|
map(items => items.filter(item => item.name.toLowerCase().includes(searchValue)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectItem(selectedItem: T) {
|
||||||
|
this.itemSelected.emit(selectedItem);
|
||||||
|
this.dismissed.emit(true);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue