Repository: hpi-sam/digital-fuesim-manv
Pull request #589: Add UI for `VehicleTemplate` editing
Author: login: ClFeSc
Labels: name: enhancement
description: New feature or request
name: frontend
description: Issues mainly related to the frontend
Created: 2022-12-26T19:03:39Z | Merged: 2025-04-12T15:41:11Z

Closes #588 

Linked issue #588: UI to edit and create vehicle templates
There should be a UI to edit and create vehicle templates, similar to the one for map image templates.

Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b0e670647..fd3cd9f09 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,7 @@ and this project does **not** adhere to [Semantic Versioning](https://semver.org
 - Add possibility to export only templates from an exercise and import them in another one.
     - Patients, vehicles, and image templates can be selected.
     - Imports can either overwrite existing templates or be added to the exercise.
+- Vehicle templates can now be edited, added, and deleted from within an exercise.
 
 ### Changed
 
diff --git a/frontend/src/app/pages/exercises/exercise/exercise.module.ts b/frontend/src/app/pages/exercises/exercise/exercise.module.ts
index 0c0701600..0af791e69 100644
--- a/frontend/src/app/pages/exercises/exercise/exercise.module.ts
+++ b/frontend/src/app/pages/exercises/exercise/exercise.module.ts
@@ -14,8 +14,11 @@ import { ExerciseComponent } from './exercise/exercise.component';
 import { AlarmGroupOverviewModule } from './shared/alarm-group-overview/alarm-group-overview.module';
 import { ClientOverviewModule } from './shared/client-overview/client-overview.module';
 import { CreateImageTemplateModalComponent } from './shared/editor-panel/create-image-template-modal/create-image-template-modal.component';
+import { CreateVehicleTemplateModalComponent } from './shared/editor-panel/create-vehicle-template-modal/create-vehicle-template-modal.component';
 import { EditImageTemplateModalComponent } from './shared/editor-panel/edit-image-template-modal/edit-image-template-modal.component';
+import { EditVehicleTemplateModalComponent } from './shared/editor-panel/edit-vehicle-template-modal/edit-vehicle-template-modal.component';
 import { ImageTemplateFormComponent } from './shared/editor-panel/image-template-form/image-template-form.component';
+import { VehicleTemplateFormComponent } from './shared/editor-panel/vehicle-template-form/vehicle-template-form.component';
 import { EmergencyOperationsCenterModule } from './shared/emergency-operations-center/emergency-operations-center.module';
 import { ExerciseMapModule } from './shared/exercise-map/exercise-map.module';
 import { ExerciseSettingsModalComponent } from './shared/exercise-settings/exercise-settings-modal/exercise-settings-modal.component';
@@ -29,6 +32,8 @@ import { TrainerMapEditorComponent } from './shared/trainer-map-editor/trainer-m
 import { TrainerToolbarComponent } from './shared/trainer-toolbar/trainer-toolbar.component';
 import { TransferOverviewModule } from './shared/transfer-overview/transfer-overview.module';
 import { CoordinatePickerModule } from './shared/coordinate-picker/coordinate-picker.module';
+import { PersonnelTemplateDisplayComponent } from './shared/editor-panel/personnel-template-display/personnel-template-display.component';
+import { MaterialTemplateDisplayComponent } from './shared/editor-panel/material-template-display/material-template-display.component';
 
 @NgModule({
     declarations: [
@@ -39,8 +44,13 @@ import { CoordinatePickerModule } from './shared/coordinate-picker/coordinate-pi
         ExerciseSettingsModalComponent,
         TimeTravelComponent,
         CreateImageTemplateModalComponent,
+        CreateVehicleTemplateModalComponent,
         EditImageTemplateModalComponent,
+        EditVehicleTemplateModalComponent,
         ImageTemplateFormComponent,
+        VehicleTemplateFormComponent,
+        PersonnelTemplateDisplayComponent,
+        MaterialTemplateDisplayComponent,
         PartialExportModalComponent,
         PartialImportModalComponent,
     ],
diff --git a/frontend/src/app/pages/exercises/exercise/shared/editor-panel/create-vehicle-template-modal/create-vehicle-template-modal.component.html b/frontend/src/app/pages/exercises/exercise/shared/editor-panel/create-vehicle-template-modal/create-vehicle-template-modal.component.html
new file mode 100644
index 000000000..0ad9bf361
--- /dev/null
+++ b/frontend/src/app/pages/exercises/exercise/shared/editor-panel/create-vehicle-template-modal/create-vehicle-template-modal.component.html
@@ -0,0 +1,19 @@
+<div class="modal-header">
+    <h4 class="modal-title">Fahrzeugvorlage hinzufügen</h4>
+    <button type="button" class="btn-close" (click)="close()"></button>
+</div>
+<div class="modal-body">
+    <app-vehicle-template-form
+        [initialValues]="{
+            url: editableVehicleTemplateValues.url,
+            height: editableVehicleTemplateValues.height,
+            name: editableVehicleTemplateValues.name,
+            type: editableVehicleTemplateValues.type,
+            patientCapacity: editableVehicleTemplateValues.patientCapacity,
+            materialTypes: editableVehicleTemplateValues.materialTypes,
+            personnelTypes: editableVehicleTemplateValues.personnelTypes,
+        }"
+        (submitVehicleTemplate)="createVehicleTemplate($event)"
+        [btnText]="'Fahrzeugvorlage hinzufügen'"
+    ></app-vehicle-template-form>
+</div>
diff --git a/frontend/src/app/pages/exercises/exercise/shared/editor-panel/create-vehicle-template-modal/create-vehicle-template-modal.component.scss b/frontend/src/app/pages/exercises/exercise/shared/editor-panel/create-vehicle-template-modal/create-vehicle-template-modal.component.scss
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/src/app/pages/exercises/exercise/shared/editor-panel/create-vehicle-template-modal/create-vehicle-template-modal.component.ts b/frontend/src/app/pages/exercises/exercise/shared/editor-panel/create-vehicle-template-modal/create-vehicle-template-modal.component.ts
new file mode 100644
index 000000000..00feee123
--- /dev/null
+++ b/frontend/src/app/pages/exercises/exercise/shared/editor-panel/create-vehicle-template-modal/create-vehicle-template-modal.component.ts
@@ -0,0 +1,71 @@
+import { Component } from '@angular/core';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { uuid } from 'digital-fuesim-manv-shared';
+import { ExerciseService } from 'src/app/core/exercise.service';
+import type {
+    ChangedVehicleTemplateValues,
+    EditableVehicleTemplateValues,
+} from '../vehicle-template-form/vehicle-template-form.component';
+
+@Component({
+    selector: 'app-create-vehicle-template-modal',
+    templateUrl: './create-vehicle-template-modal.component.html',
+    styleUrls: ['./create-vehicle-template-modal.component.scss'],
+    standalone: false,
+})
+export class CreateVehicleTemplateModalComponent {
+    public readonly editableVehicleTemplateValues: EditableVehicleTemplateValues =
+        {
+            url: null,
+            height: 100,
+            name: null,
+            patientCapacity: 1,
+            type: null,
+            materialTypes: [],
+            personnelTypes: [],
+        };
+
+    constructor(
+        private readonly activeModal: NgbActiveModal,
+        private readonly exerciseService: ExerciseService
+    ) {}
+
+    public createVehicleTemplate({
+        url,
+        height,
+        name,
+        aspectRatio,
+        patientCapacity,
+        type,
+        materialTypes,
+        personnelTypes,
+    }: ChangedVehicleTemplateValues) {
+        this.exerciseService
+            .proposeAction({
+                type: '[VehicleTemplate] Add vehicleTemplate',
+                vehicleTemplate: {
+                    id: uuid(),
+                    type: 'vehicleTemplate',
+                    image: {
+                        url,
+                        height,
+                        aspectRatio,
+                    },
+                    name,
+                    materials: materialTypes,
+                    patientCapacity,
+                    personnel: personnelTypes,
+                    vehicleType: type,
+                },
+            })
+            .then((response) => {
+                if (response.success) {
+                    this.close();
+                }
+            });
+    }
+
+    public close() {
+        this.activeModal.close();
+    }
+}
diff --git a/frontend/src/app/pages/exercises/exercise/shared/editor-panel/create-vehicle-template-modal/open-create-vehicle-template-modal.ts b/frontend/src/app/pages/exercises/exercise/shared/editor-panel/create-vehicle-template-modal/open-create-vehicle-template-modal.ts
new file mode 100644
index 000000000..a825aba5f
--- /dev/null
+++ b/frontend/src/app/pages/exercises/exercise/shared/editor-panel/create-vehicle-template-modal/open-create-vehicle-template-modal.ts
@@ -0,0 +1,8 @@
+import type { NgbModal } from '@ng-bootstrap/ng-bootstrap';
+import { CreateVehicleTemplateModalComponent } from './create-vehicle-template-modal.component';
+
+export async function openCreateVehicleTemplateModal(
+    ngbModalService: NgbModal
+) {
+    ngbModalService.open(CreateVehicleTemplateModalComponent, { size: 'lg' });
+}
diff --git a/frontend/src/app/pages/exercises/exercise/shared/editor-panel/edit-vehicle-template-modal/edit-vehicle-template-modal.component.html b/frontend/src/app/pages/exercises/exercise/shared/editor-panel/edit-vehicle-template-modal/edit-vehicle-template-modal.component.html
new file mode 100644
index 000000000..f81d9df36
--- /dev/null
+++ b/frontend/src/app/pages/exercises/exercise/shared/editor-panel/edit-vehicle-template-modal/edit-vehicle-template-modal.component.html
@@ -0,0 +1,24 @@
+<div class="modal-header">
+    <h4 class="modal-title">Fahrzeugvorlage bearbeiten</h4>
+    <button type="button" class="btn-close" (click)="close()"></button>
+</div>
+<div *ngIf="vehicleTemplate" class="modal-body">
+    <app-vehicle-template-form
+        [initialValues]="{
+            name: vehicleTemplate.name,
+            type: vehicleTemplate.vehicleType,
+            url: vehicleTemplate.image.url,
+            height: vehicleTemplate.image.height,
+            patientCapacity: vehicleTemplate.patientCapacity,
+            materialTypes: vehicleTemplate.materials,
+            personnelTypes: vehicleTemplate.personnel,
+        }"
+        (submitVehicleTemplate)="editVehicleTemplate($event)"
+        [btnText]="'Speichern'"
+    ></app-vehicle-template-form>
+
+    <button class="btn btn-danger float-end" (click)="deleteVehicleTemplate()">
+        <i class="bi bi-trash-fill"></i>
+        Löschen
+    </button>
+</div>
diff --git a/frontend/src/app/pages/exercises/exercise/shared/editor-panel/edit-vehicle-template-modal/edit-vehicle-template-modal.component.scss b/frontend/src/app/pages/exercises/exercise/shared/editor-panel/edit-vehicle-template-modal/edit-vehicle-template-modal.component.scss
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/src/app/pages/exercises/exercise/shared/editor-panel/edit-vehicle-template-modal/edit-vehicle-template-modal.component.ts b/frontend/src/app/pages/exercises/exercise/shared/editor-panel/edit-vehicle-template-modal/edit-vehicle-template-modal.component.ts
new file mode 100644
index 000000000..53d644b90
--- /dev/null
+++ b/frontend/src/app/pages/exercises/exercise/shared/editor-panel/edit-vehicle-template-modal/edit-vehicle-template-modal.component.ts
@@ -0,0 +1,105 @@
+import type { OnInit } from '@angular/core';
+import { Component } from '@angular/core';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { Store } from '@ngrx/store';
+import type {
+    UUID,
+    Mutable,
+    VehicleTemplate,
+} from 'digital-fuesim-manv-shared';
+import { cloneDeepMutable } from 'digital-fuesim-manv-shared';
+import { ExerciseService } from 'src/app/core/exercise.service';
+import type { AppState } from 'src/app/state/app.state';
+import { createSelectVehicleTemplate } from 'src/app/state/application/selectors/exercise.selectors';
+import { selectStateSnapshot } from 'src/app/state/get-state-snapshot';
+import { ConfirmationModalService } from 'src/app/core/confirmation-modal/confirmation-modal.service';
+import type { ChangedVehicleTemplateValues } from '../vehicle-template-form/vehicle-template-form.component';
+
+@Component({
+    selector: 'app-edit-vehicle-template-modal',
+    templateUrl: './edit-vehicle-template-modal.component.html',
+    styleUrls: ['./edit-vehicle-template-modal.component.scss'],
+    standalone: false,
+})
+export class EditVehicleTemplateModalComponent implements OnInit {
+    // This is set after the modal creation and therefore accessible in ngOnInit
+    public vehicleTemplateId!: UUID;
+
+    public vehicleTemplate?: Mutable<VehicleTemplate>;
+
+    constructor(
+        private readonly exerciseService: ExerciseService,
+        private readonly store: Store<AppState>,
+        private readonly activeModal: NgbActiveModal,
+        private readonly confirmationModalService: ConfirmationModalService
+    ) {}
+
+    ngOnInit(): void {
+        this.vehicleTemplate = cloneDeepMutable(
+            selectStateSnapshot(
+                createSelectVehicleTemplate(this.vehicleTemplateId),
+                this.store
+            )
+        );
+    }
+
+    public async deleteVehicleTemplate(): Promise<void> {
+        const confirmDelete = await this.confirmationModalService.confirm({
+            title: 'Fahrzeug-Vorlage löschen',
+            description: `Möchten Sie die Fahrzeug-Vorlage "${this.vehicleTemplate?.vehicleType}" wirklich löschen? Diese Vorlage wird auch aus allen Alarmgruppen gelöscht werden. Bereit auf der Karte existierende Fahrzeuge, Fahrzeuge im Transfer und bereits alarmierte Fahrzeuge sind davon nicht betroffen.`,
+        });
+        if (!confirmDelete) {
+            return;
+        }
+        this.exerciseService
+            .proposeAction({
+                type: '[VehicleTemplate] Delete vehicleTemplate',
+                id: this.vehicleTemplateId,
+            })
+            .then((response) => {
+                if (response.success) {
+                    this.close();
+                }
+            });
+    }
+
+    public editVehicleTemplate({
+        url,
+        height,
+        name,
+        aspectRatio,
+        patientCapacity,
+        type,
+        materialTypes,
+        personnelTypes,
+    }: ChangedVehicleTemplateValues): void {
+        if (!this.vehicleTemplate) {
+            console.error("VehicleTemplate wasn't initialized yet");
+            return;
+        }
+        this.exerciseService
+            .proposeAction({
+                type: '[VehicleTemplate] Edit vehicleTemplate',
+                id: this.vehicleTemplateId,
+                name,
+                image: {
+                    url,
+                    height,
+                    aspectRatio,
+                },
+                materials: materialTypes,
+                patientCapacity,
+                personnelTypes,
+                vehicleType: type,
+            })
+            .then((response) => {
+                if (response.success) {
+                    this.close();
+                }
+            });
+    }
+
+    public close(): void {
+        this.activeModal.close();
+    }
+}
diff --git a/frontend/src/app/pages/exercises/exercise/shared/editor-panel/edit-vehicle-template-modal/open-edit-vehicle-template-modal.ts b/frontend/src/app/pages/exercises/exercise/shared/editor-panel/edit-vehicle-template-modal/open-edit-vehicle-template-modal.ts
new file mode 100644
index 000000000..cc1261a0a
--- /dev/null
+++ b/frontend/src/app/pages/exercises/exercise/shared/editor-panel/edit-vehicle-template-modal/open-edit-vehicle-template-modal.ts
@@ -0,0 +1,15 @@
+import type { NgbModal } from '@ng-bootstrap/ng-bootstrap';
+import type { UUID } from 'digital-fuesim-manv-shared';
+import { EditVehicleTemplateModalComponent } from './edit-vehicle-template-modal.component';
+
+export async function openEditVehicleTemplateModal(
+    ngbModalService: NgbModal,
+    vehicleTemplateId: UUID
+) {
+    const modalRef = ngbModalService.open(EditVehicleTemplateModalComponent, {
+        size: 'lg',
+    });
+    const componentInstance =
+        modalRef.componentInstance as EditVehicleTemplateModalComponent;
+    componentInstance.vehicleTemplateId = vehicleTemplateId;
+}
diff --git a/frontend/src/app/pages/exercises/exercise/shared/editor-panel/material-template-display/material-template-display.component.html b/frontend/src/app/pages/exercises/exercise/shared/editor-panel/material-template-display/material-template-display.component.html
new file mode 100644
index 000000000..d0ad50061
--- /dev/null
+++ b/frontend/src/app/pages/exercises/exercise/shared/editor-panel/material-template-display/material-template-display.component.html
@@ -0,0 +1,6 @@
+<ng-container *ngIf="materialTemplate$ | async as materialTemplate">
+    <div class="preview-image-container pb-0">
+        <img [src]="materialTemplate.image.url" />
+    </div>
+    {{ materialTypeNames[materialTemplate.materialType] }}
+</ng-container>
diff --git a/frontend/src/app/pages/exercises/exercise/shared/editor-panel/material-template-display/material-template-display.component.scss b/frontend/src/app/pages/exercises/exercise/shared/editor-panel/material-template-display/material-template-display.component.scss
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/src/app/pages/exercises/exercise/shared/editor-panel/material-template-display/material-template-display.component.ts b/frontend/src/app/pages/exercises/exercise/shared/editor-panel/material-template-display/material-template-display.component.ts
new file mode 100644
index 000000000..c82ea5631
--- /dev/null
+++ b/frontend/src/app/pages/exercises/exercise/shared/editor-panel/material-template-display/material-template-display.component.ts
@@ -0,0 +1,35 @@
+import type { OnChanges } from '@angular/core';
+import { Component, Input } from '@angular/core';
+import { Store } from '@ngrx/store';
+import type {
+    MaterialTemplate,
+    MaterialType,
+} from 'digital-fuesim-manv-shared';
+import { materialTypeNames } from 'digital-fuesim-manv-shared';
+import type { Observable } from 'rxjs';
+import type { AppState } from 'src/app/state/app.state';
+import { createSelectMaterialTemplate } from 'src/app/state/application/selectors/exercise.selectors';
+
+@Component({
+    selector: 'app-material-template-display',
+    templateUrl: './material-template-display.component.html',
+    styleUrls: ['./material-template-display.component.scss'],
+    standalone: false,
+})
+export class MaterialTemplateDisplayComponent implements OnChanges {
+    @Input() materialTemplateType!: MaterialType;
+
+    public materialTemplate$?: Observable<MaterialTemplate>;
+
+    ngOnChanges() {
+        this.materialTemplate$ = this.store.select(
+            createSelectMaterialTemplate(this.materialTemplateType)
+        );
+    }
+
+    constructor(private readonly store: Store<AppState>) {}
+
+    public get materialTypeNames() {
+        return materialTypeNames;
+    }
+}
diff --git a/frontend/src/app/pages/exercises/exercise/shared/editor-panel/personnel-template-display/personnel-template-display.component.html b/frontend/src/app/pages/exercises/exercise/shared/editor-panel/personnel-template-display/personnel-template-display.component.html
new file mode 100644
index 000000000..218c49d41
--- /dev/null
+++ b/frontend/src/app/pages/exercises/exercise/shared/editor-panel/personnel-template-display/personnel-template-display.component.html
@@ -0,0 +1,6 @@
+<ng-container *ngIf="personnelTemplate$ | async as personnelTemplate">
+    <div class="preview-image-container pb-0">
+        <img [src]="personnelTemplate.image.url" />
+    </div>
+    {{ personnelTypeNames[personnelTemplate.personnelType] }}
+</ng-container>
diff --git a/frontend/src/app/pages/exercises/exercise/shared/editor-panel/personnel-template-display/personnel-template-display.component.scss b/frontend/src/app/pages/exercises/exercise/shared/editor-panel/personnel-template-display/personnel-template-display.component.scss
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/src/app/pages/exercises/exercise/shared/editor-panel/personnel-template-display/personnel-template-display.component.ts b/frontend/src/app/pages/exercises/exercise/shared/editor-panel/personnel-template-display/personnel-template-display.component.ts
new file mode 100644
index 000000000..d3c5d3532
--- /dev/null
+++ b/frontend/src/app/pages/exercises/exercise/shared/editor-panel/personnel-template-display/personnel-template-display.component.ts
@@ -0,0 +1,35 @@
+import type { OnChanges } from '@angular/core';
+import { Component, Input } from '@angular/core';
+import { Store } from '@ngrx/store';
+import type {
+    PersonnelTemplate,
+    PersonnelType,
+} from 'digital-fuesim-manv-shared';
+import { personnelTypeNames } from 'digital-fuesim-manv-shared';
+import type { Observable } from 'rxjs';
+import type { AppState } from 'src/app/state/app.state';
+import { createSelectPersonnelTemplate } from 'src/app/state/application/selectors/exercise.selectors';
+
+@Component({
+    selector: 'app-personnel-template-display',
+    templateUrl: './personnel-template-display.component.html',
+    styleUrls: ['./personnel-template-display.component.scss'],
+    standalone: false,
+})
+export class PersonnelTemplateDisplayComponent implements OnChanges {
+    @Input() personnelTemplateType!: PersonnelType;
+
+    public personnelTemplate$?: Observable<PersonnelTemplate>;
+
+    ngOnChanges() {
+        this.personnelTemplate$ = this.store.select(
+            createSelectPersonnelTemplate(this.personnelTemplateType)
+        );
+    }
+
+    constructor(private readonly store: Store<AppState>) {}
+
+    public get personnelTypeNames() {
+        return personnelTypeNames;
+    }
+}
diff --git a/frontend/src/app/pages/exercises/exercise/shared/editor-panel/vehicle-template-form/vehicle-template-form.component.html b/frontend/src/app/pages/exercises/exercise/shared/editor-panel/vehicle-template-form/vehicle-template-form.component.html
new file mode 100644
index 000000000..90eb50e5d
--- /dev/null
+++ b/frontend/src/app/pages/exercises/exercise/shared/editor-panel/vehicle-template-form/vehicle-template-form.component.html
@@ -0,0 +1,215 @@
+<ng-container *ngIf="values">
+    <div class="form-group mb-3">
+        <label class="form-label">Standardname von neuen Fahrzeugen:</label>
+        <input
+            #nameInput="ngModel"
+            [(ngModel)]="values.name"
+            required
+            type="text"
+            class="form-control"
+            placeholder="Fahrzeugname"
+        />
+        <app-display-validation
+            [ngModelInput]="nameInput"
+        ></app-display-validation>
+    </div>
+    <div class="form-group mb-3">
+        <label class="form-label">Typ:</label>
+        <input
+            #typeInput="ngModel"
+            [(ngModel)]="values.type"
+            required
+            type="text"
+            class="form-control"
+            placeholder="Fahrzeugtyp"
+        />
+        <app-display-validation
+            [ngModelInput]="typeInput"
+        ></app-display-validation>
+    </div>
+    <div class="form-group mb-3">
+        <label class="form-label">Bildadresse:</label>
+        <input
+            #imageUrlInput="ngModel"
+            [(ngModel)]="values.url"
+            [appAutofocus]="true"
+            appImageExistsValidator
+            required
+            type="url"
+            class="form-control"
+            placeholder="https://example.com/image.png"
+        />
+        <app-display-validation
+            [ngModelInput]="imageUrlInput"
+        ></app-display-validation>
+    </div>
+    <div class="form-group mb-3">
+        <label class="form-label">Bildhöhe:</label>
+        <input
+            #heightInput="ngModel"
+            [(ngModel)]="values.height"
+            required
+            min="1"
+            step="1"
+            appIntegerValidator
+            type="number"
+            class="form-control"
+            placeholder="100"
+        />
+        <app-display-validation
+            [ngModelInput]="heightInput"
+        ></app-display-validation>
+    </div>
+    <div class="form-group mb-3">
+        <label class="form-label">Patientenkapazität:</label>
+        <input
+            #patientCapacityInput="ngModel"
+            [(ngModel)]="values.patientCapacity"
+            required
+            min="0"
+            step="1"
+            appIntegerValidator
+            type="number"
+            class="form-control"
+            placeholder="1"
+        />
+        <app-display-validation
+            [ngModelInput]="patientCapacityInput"
+        ></app-display-validation>
+    </div>
+    <div class="form-group mb-3">
+        <label class="form-label"
+            >Materialien
+            <div ngbDropdown autoClose="outside" class="d-inline-block">
+                <button
+                    ngbDropdownToggle
+                    type="button"
+                    class="btn btn-outline-primary ms-3"
+                >
+                    Material hinzufügen
+                </button>
+                <div ngbDropdownMenu>
+                    <button
+                        *ngFor="
+                            let materialTemplate of materialTemplates$
+                                | async
+                                | values
+                        "
+                        ngbDropdownItem
+                        (click)="addMaterial(materialTemplate.materialType)"
+                        type="button"
+                        class="btn"
+                    >
+                        <i class="bi bi-plus"></i>
+                        {{ materialTypeNames[materialTemplate.materialType] }}
+                    </button>
+                </div>
+            </div></label
+        >
+        <div
+            *ngIf="values.materialTypes.length > 0; else noMaterialTemplates"
+            class="d-flex flex-wrap"
+        >
+            <div
+                *ngFor="
+                    let materialTypes of values.materialTypes;
+                    let index = index
+                "
+                class="card text-center m-1"
+                style="width: 10rem; text-overflow: ellipsis; height: auto"
+            >
+                <div>
+                    <button
+                        (click)="removeMaterial(index)"
+                        class="btn btn-sm btn-outline-danger float-end"
+                    >
+                        <i class="bi bi-trash"></i>
+                    </button>
+                </div>
+                <app-material-template-display
+                    [materialTemplateType]="materialTypes"
+                >
+                </app-material-template-display>
+            </div>
+        </div>
+        <ng-template #noMaterialTemplates>
+            <div class="text-muted mt-3 mb-3">Keine Materialien zugewiesen</div>
+        </ng-template>
+    </div>
+    <div class="form-group mb-3">
+        <label class="form-label"
+            >Personal
+            <div ngbDropdown autoClose="outside" class="d-inline-block">
+                <button
+                    ngbDropdownToggle
+                    type="button"
+                    class="btn btn-outline-primary ms-3"
+                >
+                    Personal hinzufügen
+                </button>
+                <div ngbDropdownMenu>
+                    <button
+                        *ngFor="
+                            let personnelTemplate of personnelTemplates$
+                                | async
+                                | values
+                        "
+                        ngbDropdownItem
+                        (click)="addPersonnel(personnelTemplate.personnelType)"
+                        type="button"
+                        class="btn"
+                    >
+                        <i class="bi bi-plus"></i>
+                        {{
+                            personnelTypeNames[personnelTemplate.personnelType]
+                        }}
+                    </button>
+                </div>
+            </div></label
+        >
+        <div
+            *ngIf="values.personnelTypes.length > 0; else noPersonnelTemplates"
+            class="d-flex flex-wrap"
+        >
+            <div
+                *ngFor="
+                    let personnelType of values.personnelTypes;
+                    let index = index
+                "
+                class="card text-center m-1"
+                style="width: 10rem; text-overflow: ellipsis; height: auto"
+            >
+                <div>
+                    <button
+                        (click)="removePersonnel(index)"
+                        class="btn btn-sm btn-outline-danger float-end"
+                    >
+                        <i class="bi bi-trash"></i>
+                    </button>
+                </div>
+                <app-personnel-template-display
+                    [personnelTemplateType]="personnelType"
+                >
+                </app-personnel-template-display>
+            </div>
+        </div>
+        <ng-template #noPersonnelTemplates>
+            <div class="text-muted mt-3 mb-3">Kein Personal zugewiesen</div>
+        </ng-template>
+    </div>
+
+    <!-- If the button is not disabled the vehicleTemplate is correct -->
+    <button
+        (click)="submit()"
+        [disabled]="
+            nameInput.invalid ||
+            typeInput.invalid ||
+            imageUrlInput.invalid ||
+            heightInput.invalid ||
+            patientCapacityInput.invalid
+        "
+        class="btn btn-primary"
+    >
+        {{ btnText }}
+    </button>
+</ng-container>
diff --git a/frontend/src/app/pages/exercises/exercise/shared/editor-panel/vehicle-template-form/vehicle-template-form.component.scss b/frontend/src/app/pages/exercises/exercise/shared/editor-panel/vehicle-template-form/vehicle-template-form.component.scss
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/src/app/pages/exercises/exercise/shared/editor-panel/vehicle-template-form/vehicle-template-form.component.ts b/frontend/src/app/pages/exercises/exercise/shared/editor-panel/vehicle-template-form/vehicle-template-form.component.ts
new file mode 100644
index 000000000..23e4f234a
--- /dev/null
+++ b/frontend/src/app/pages/exercises/exercise/shared/editor-panel/vehicle-template-form/vehicle-template-form.component.ts
@@ -0,0 +1,148 @@
+import type { OnChanges } from '@angular/core';
+import { Component, EventEmitter, Input, Output } from '@angular/core';
+import { Store } from '@ngrx/store';
+import type {
+    MaterialTemplate,
+    MaterialType,
+    PersonnelTemplate,
+    PersonnelType,
+} from 'digital-fuesim-manv-shared';
+import {
+    materialTypeNames,
+    personnelTypeNames,
+} from 'digital-fuesim-manv-shared';
+import { cloneDeep } from 'lodash-es';
+import type { Observable } from 'rxjs';
+import { MessageService } from 'src/app/core/messages/message.service';
+import { getImageAspectRatio } from 'src/app/shared/functions/get-image-aspect-ratio';
+import type { SimpleChangesGeneric } from 'src/app/shared/types/simple-changes-generic';
+import type { AppState } from 'src/app/state/app.state';
+import {
+    selectMaterialTemplates,
+    selectPersonnelTemplates,
+} from 'src/app/state/application/selectors/exercise.selectors';
+
+@Component({
+    selector: 'app-vehicle-template-form',
+    templateUrl: './vehicle-template-form.component.html',
+    styleUrls: ['./vehicle-template-form.component.scss'],
+    standalone: false,
+})
+export class VehicleTemplateFormComponent implements OnChanges {
+    @Input() initialValues!: EditableVehicleTemplateValues;
+    @Input() btnText!: string;
+
+    /**
+     * Emits the changed values
+     */
+    @Output() readonly submitVehicleTemplate =
+        new EventEmitter<ChangedVehicleTemplateValues>();
+
+    public values?: EditableVehicleTemplateValues;
+
+    public materialTemplates$: Observable<{
+        readonly [type in MaterialType]: MaterialTemplate;
+    }> = this.store.select(selectMaterialTemplates);
+    public personnelTemplates$: Observable<{
+        readonly [type in PersonnelType]: PersonnelTemplate;
+    }> = this.store.select(selectPersonnelTemplates);
+
+    constructor(
+        private readonly messageService: MessageService,
+        private readonly store: Store<AppState>
+    ) {}
+
+    ngOnChanges(_changes: SimpleChangesGeneric<this>): void {
+        this.values = {
+            ...this.initialValues,
+            ...this.values,
+        };
+    }
+
+    /**
+     * Emits the changed values via submitVehicleTemplate
+     * This method must only be called if all values are valid
+     */
+    public async submit() {
+        if (!this.values) {
+            return;
+        }
+        const valuesOnSubmit = cloneDeep(this.values);
+        getImageAspectRatio(valuesOnSubmit.url!)
+            .then((aspectRatio) => {
+                this.submitVehicleTemplate.emit({
+                    name: valuesOnSubmit.name!,
+                    type: valuesOnSubmit.type!,
+                    url: valuesOnSubmit.url!,
+                    aspectRatio,
+                    height: valuesOnSubmit.height,
+                    patientCapacity: valuesOnSubmit.patientCapacity,
+                    materialTypes: valuesOnSubmit.materialTypes,
+                    personnelTypes: valuesOnSubmit.personnelTypes,
+                });
+            })
+            .catch((error) => {
+                this.messageService.postError({
+                    title: 'Ungültige URL',
+                    body: 'Bitte überprüfen Sie die Bildadresse.',
+                    error,
+                });
+            });
+    }
+
+    public addPersonnel(type: PersonnelType) {
+        if (!this.values) {
+            return;
+        }
+        this.values.personnelTypes.push(type);
+    }
+
+    public removePersonnel(index: number) {
+        if (!this.values) {
+            return;
+        }
+        this.values.personnelTypes.splice(index, 1);
+    }
+
+    public addMaterial(type: MaterialType) {
+        if (!this.values) {
+            return;
+        }
+        this.values.materialTypes.push(type);
+    }
+
+    public removeMaterial(index: number) {
+        if (!this.values) {
+            return;
+        }
+        this.values.materialTypes.splice(index, 1);
+    }
+
+    public get personnelTypeNames() {
+        return personnelTypeNames;
+    }
+    public get materialTypeNames() {
+        return materialTypeNames;
+    }
+}
+
+export interface EditableVehicleTemplateValues {
+    name: string | null;
+    type: string | null;
+    url: string | null;
+    height: number;
+    patientCapacity: number;
+    materialTypes: MaterialType[];
+    personnelTypes: PersonnelType[];
+}
+
+export interface ChangedVehicleTemplateValues {
+    name: string;
+    type: string;
+    url: string;
+    aspectRatio: number;
+    height: number;
+    patientCapacity: number;
+    materialTypes: MaterialType[];
+    personnelTypes: PersonnelType[];
+}
diff --git a/frontend/src/app/pages/exercises/exercise/shared/trainer-map-editor/trainer-map-editor.component.html b/frontend/src/app/pages/exercises/exercise/shared/trainer-map-editor/trainer-map-editor.component.html
index 89c749907..25c1ecaef 100644
--- a/frontend/src/app/pages/exercises/exercise/shared/trainer-map-editor/trainer-map-editor.component.html
+++ b/frontend/src/app/pages/exercises/exercise/shared/trainer-map-editor/trainer-map-editor.component.html
@@ -261,7 +261,16 @@ <h5>
                     </div>
                 </li>
                 <li class="list-group-item">
-                    <h5>Fahrzeuge</h5>
+                    <h5>
+                        Fahrzeuge
+                        <button
+                            class="ms-3 btn btn-sm btn-link text-decoration-none"
+                            (click)="addVehicleTemplate()"
+                        >
+                            <i class="bi bi-plus"></i>
+                            Hinzufügen
+                        </button>
+                    </h5>
                     <div
                         class="overflow-auto w-100"
                         style="white-space: nowrap"
@@ -280,6 +289,18 @@ <h5>Fahrzeuge</h5>
                             <div class="preview-image-container card-img-top">
                                 <img [src]="vehicleTemplate.image.url" />
                             </div>
+
+                            <!-- (mousedown) instead of (click) to be able to stop the propagation-->
+                            <button
+                                (mousedown)="
+                                    editVehicleTemplate(vehicleTemplate.id);
+                                    $event.stopPropagation()
+                                "
+                                class="btn btn-outline-warning position-absolute"
+                                style="top: 0; right: 0"
+                            >
+                                <i class="bi bi-pencil-fill"></i>
+                            </button>
                             <div class="card-body">
                                 <h5
                                     class="card-title text-truncate"
diff --git a/frontend/src/app/pages/exercises/exercise/shared/trainer-map-editor/trainer-map-editor.component.ts b/frontend/src/app/pages/exercises/exercise/shared/trainer-map-editor/trainer-map-editor.component.ts
index 6ec6ce8c3..99597db67 100644
--- a/frontend/src/app/pages/exercises/exercise/shared/trainer-map-editor/trainer-map-editor.component.ts
+++ b/frontend/src/app/pages/exercises/exercise/shared/trainer-map-editor/trainer-map-editor.component.ts
@@ -25,9 +25,11 @@ import {
 import { DragElementService } from '../core/drag-element.service';
 import { TransferLinesService } from '../core/transfer-lines.service';
 import { openCreateImageTemplateModal } from '../editor-panel/create-image-template-modal/open-create-image-template-modal';
+import { openCreateVehicleTemplateModal } from '../editor-panel/create-vehicle-template-modal/open-create-vehicle-template-modal';
 import { openEditImageTemplateModal } from '../editor-panel/edit-image-template-modal/open-edit-image-template-modal';
 import { openPartialImportOverwriteModal } from '../partial-import/open-partial-import-overwrite-modal';
 import { simulatedRegionDragTemplates } from '../editor-panel/templates/simulated-region';
+import { openEditVehicleTemplateModal } from '../editor-panel/edit-vehicle-template-modal/open-edit-vehicle-template-modal';
 
 @Component({
     selector: 'app-trainer-map-editor',
@@ -81,10 +83,18 @@ export class TrainerMapEditorComponent {
         openCreateImageTemplateModal(this.ngbModalService);
     }
 
+    public addVehicleTemplate() {
+        openCreateVehicleTemplateModal(this.ngbModalService);
+    }
+
     public editMapImageTemplate(mapImageTemplateId: UUID) {
         openEditImageTemplateModal(this.ngbModalService, mapImageTemplateId);
     }
 
+    public editVehicleTemplate(mapImageTemplateId: UUID) {
+        openEditVehicleTemplateModal(this.ngbModalService, mapImageTemplateId);
+    }
+
     public setCurrentCategory(category: ColorCode) {
         this.currentCategory = category;
     }
diff --git a/frontend/src/app/state/application/selectors/exercise.selectors.ts b/frontend/src/app/state/application/selectors/exercise.selectors.ts
index 2ffec6106..facb14632 100644
--- a/frontend/src/app/state/application/selectors/exercise.selectors.ts
+++ b/frontend/src/app/state/application/selectors/exercise.selectors.ts
@@ -101,6 +101,12 @@ export function createSelectRadiogram<R extends ExerciseRadiogram>(id: UUID) {
         (radiograms) => radiograms[id] as R
     );
 }
+export const createSelectMaterialTemplate = createSelectElementFromMapFactory(
+    selectMaterialTemplates
+);
+export const createSelectPersonnelTemplate = createSelectElementFromMapFactory(
+    selectPersonnelTemplates
+);
 
 function createSelectElementFromArrayFactory<Element extends { id: UUID }>(
     elementsSelector: (state: AppState) => readonly Element[]
diff --git a/shared/src/models/index.ts b/shared/src/models/index.ts
index 66ca90ad7..e21925b76 100644
--- a/shared/src/models/index.ts
+++ b/shared/src/models/index.ts
@@ -21,3 +21,5 @@ export { ExerciseConfiguration } from './exercise-configuration.js';
 export * from './utils/index.js';
 export * from './log-entry.js';
 export * from './tag.js';
+export { MaterialTemplate } from './material-template.js';
+export { PersonnelTemplate } from './personnel-template.js';
diff --git a/shared/src/models/utils/index.ts b/shared/src/models/utils/index.ts
index 37078b464..3526e9373 100644
--- a/shared/src/models/utils/index.ts
+++ b/shared/src/models/utils/index.ts
@@ -27,3 +27,4 @@ export * from './rescue-resource.js';
 export * from './request-target/index.js';
 export * from './occupations/index.js';
 export * from './tag-helpers.js';
+export * from './material-type.js';
diff --git a/shared/src/models/utils/material-type.ts b/shared/src/models/utils/material-type.ts
index 963c52820..4b479fb74 100644
--- a/shared/src/models/utils/material-type.ts
+++ b/shared/src/models/utils/material-type.ts
@@ -6,3 +6,10 @@ export const materialTypeAllowedValues: AllowedValues<MaterialType> = {
     big: true,
     standard: true,
 };
+
+export const materialTypeNames: {
+    [key in MaterialType]: string;
+} = {
+    big: 'Groß',
+    standard: 'Standard',
+};
diff --git a/shared/src/store/action-reducers/action-reducers.ts b/shared/src/store/action-reducers/action-reducers.ts
index cf2afa69f..746ae3a83 100644
--- a/shared/src/store/action-reducers/action-reducers.ts
+++ b/shared/src/store/action-reducers/action-reducers.ts
@@ -16,6 +16,7 @@ import { EmergencyOperationCenterActionReducers } from './emergency-operation-ce
 import { SimulatedRegionActionReducers } from './simulated-region.js';
 import { SimulationActionReducers } from './simulation.js';
 import { RadiogramActionReducers } from './radiogram.js';
+import { VehicleTemplateActionReducers } from './vehicle-templates.js';
 
 /**
  * All action reducers of the exercise must be registered here
@@ -39,6 +40,7 @@ const actionReducers = {
     ...SimulatedRegionActionReducers,
     ...SimulationActionReducers,
     ...RadiogramActionReducers,
+    ...VehicleTemplateActionReducers,
 };
 
 type ExerciseActionReducer =
diff --git a/shared/src/store/action-reducers/index.ts b/shared/src/store/action-reducers/index.ts
index ee9dfe0ea..ebd897955 100644
--- a/shared/src/store/action-reducers/index.ts
+++ b/shared/src/store/action-reducers/index.ts
@@ -7,5 +7,6 @@ export * from './vehicle.js';
 export * from './viewport.js';
 export * from './map-images.js';
 export * from './action-reducers.js';
+export * from './vehicle-templates.js';
 
 // Don't forget to register the actionReducer in `action-reducers.ts`
diff --git a/shared/src/store/action-reducers/vehicle-templates.ts b/shared/src/store/action-reducers/vehicle-templates.ts
new file mode 100644
index 000000000..f2f76ace6
--- /dev/null
+++ b/shared/src/store/action-reducers/vehicle-templates.ts
@@ -0,0 +1,149 @@
+import { Type } from 'class-transformer';
+import {
+    IsString,
+    ValidateNested,
+    IsUUID,
+    IsInt,
+    IsArray,
+} from 'class-validator';
+import { VehicleTemplate } from '../../models/index.js';
+import type { PersonnelType } from '../../models/utils/index.js';
+import { ImageProperties } from '../../models/utils/index.js';
+import type { MaterialType } from '../../models/utils/material-type.js';
+import type { ExerciseState } from '../../state.js';
+import type { Mutable, UUID } from '../../utils/index.js';
+import { uuidValidationOptions, cloneDeepMutable } from '../../utils/index.js';
+import type { Action, ActionReducer } from '../action-reducer.js';
+import { ReducerError } from '../reducer-error.js';
+import { IsValue } from '../../utils/validators/is-value.js';
+
+export class AddVehicleTemplateAction implements Action {
+    @IsValue('[VehicleTemplate] Add vehicleTemplate')
+    public readonly type = '[VehicleTemplate] Add vehicleTemplate';
+
+    @ValidateNested()
+    @Type(() => VehicleTemplate)
+    public readonly vehicleTemplate!: VehicleTemplate;
+}
+
+export class EditVehicleTemplateAction implements Action {
+    @IsValue('[VehicleTemplate] Edit vehicleTemplate')
+    public readonly type = '[VehicleTemplate] Edit vehicleTemplate';
+
+    @IsUUID(4, uuidValidationOptions)
+    public readonly id!: UUID;
+
+    @IsString()
+    public readonly name!: string;
+
+    @IsString()
+    public readonly vehicleType!: string;
+
+    @IsInt()
+    public readonly patientCapacity!: number;
+
+    @ValidateNested()
+    @Type(() => ImageProperties)
+    public readonly image!: ImageProperties;
+
+    @IsString({ each: true })
+    @IsArray()
+    public readonly materials!: readonly MaterialType[];
+
+    @IsString({ each: true })
+    @IsArray()
+    public readonly personnelTypes!: readonly PersonnelType[];
+}
+
+export class DeleteVehicleTemplateAction implements Action {
+    @IsValue('[VehicleTemplate] Delete vehicleTemplate')
+    public readonly type = '[VehicleTemplate] Delete vehicleTemplate';
+
+    @IsUUID(4, uuidValidationOptions)
+    public readonly id!: UUID;
+}
+
+export namespace VehicleTemplateActionReducers {
+    export const addVehicleTemplate: ActionReducer<AddVehicleTemplateAction> = {
+        action: AddVehicleTemplateAction,
+        reducer: (draftState, { vehicleTemplate }) => {
+            if (
+                draftState.vehicleTemplates.some(
+                    (template) => template.id === vehicleTemplate.id
+                )
+            ) {
+                throw new ReducerError(
+                    `VehicleTemplate with id ${vehicleTemplate.id} already exists`
+                );
+            }
+            draftState.vehicleTemplates.push(cloneDeepMutable(vehicleTemplate));
+            return draftState;
+        },
+        rights: 'trainer',
+    };
+
+    export const editVehicleTemplate: ActionReducer<EditVehicleTemplateAction> =
+        {
+            action: EditVehicleTemplateAction,
+            reducer: (
+                draftState,
+                {
+                    id,
+                    name,
+                    vehicleType,
+                    image,
+                    patientCapacity,
+                    materials,
+                    personnelTypes,
+                }
+            ) => {
+                const vehicleTemplate = getVehicleTemplate(draftState, id);
+                vehicleTemplate.image = cloneDeepMutable(image);
+                vehicleTemplate.name = name;
+                vehicleTemplate.patientCapacity = patientCapacity;
+                vehicleTemplate.vehicleType = vehicleType;
+                vehicleTemplate.materials = cloneDeepMutable(materials);
+                vehicleTemplate.personnel = cloneDeepMutable(personnelTypes);
+
+                return draftState;
+            },
+            rights: 'trainer',
+        };
+
+    export const deleteVehicleTemplate: ActionReducer<DeleteVehicleTemplateAction> =
+        {
+            action: DeleteVehicleTemplateAction,
+            reducer: (draftState, { id }) => {
+                getVehicleTemplate(draftState, id);
+                draftState.vehicleTemplates =
+                    draftState.vehicleTemplates.filter(
+                        (template) => template.id !== id
+                    );
+                // Delete this template from every alarm group
+                for (const alarmGroup of Object.values(
+                    draftState.alarmGroups
+                )) {
+                    alarmGroup.alarmGroupVehicles = Object.fromEntries(
+                        Object.entries(alarmGroup.alarmGroupVehicles).filter(
+                            ([_, vehicle]) => vehicle.vehicleTemplateId !== id
+                        )
+                    );
+                }
+                return draftState;
+            },
+            rights: 'trainer',
+        };
+}
+
+function getVehicleTemplate(
+    state: Mutable<ExerciseState>,
+    id: UUID
+): Mutable<VehicleTemplate> {
+    const vehicleTemplate = state.vehicleTemplates.find(
+        (template) => template.id === id
+    );
+    if (!vehicleTemplate) {
+        throw new ReducerError(`VehicleTemplate with id ${id} does not exist`);
+    }
+    return vehicleTemplate;
+}


Reviews:
user: login: lukasrad02
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-10T12:28:57Z
user: login: ClFeSc
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-10T12:36:25Z
user: login: lukasrad02
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-10T12:39:54Z
user: login: lukasrad02
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-10T12:43:42Z
user: login: ClFeSc
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-10T12:48:14Z
user: login: ClFeSc
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-11T10:13:42Z
user: login: ClFeSc
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-11T17:41:02Z
user: login: lukasrad02
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-12T07:00:54Z
user: login: ClFeSc
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-12T09:36:47Z
user: login: lukasrad02
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-12T14:07:34Z

Review comments:
diff_hunk: @@ -0,0 +1,70 @@
+import { Component } from '@angular/core';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { uuid } from 'digital-fuesim-manv-shared';
+import { ExerciseService } from 'src/app/core/exercise.service';
+import type {
+    ChangedVehicleTemplateValues,
+    EditableVehicleTemplateValues,
+} from '../vehicle-template-form/vehicle-template-form.component';
+
+@Component({
+    selector: 'app-create-vehicle-template-modal',
+    templateUrl: './create-vehicle-template-modal.component.html',
+    styleUrls: ['./create-vehicle-template-modal.component.scss'],
+})
+export class CreateVehicleTemplateModalComponent {
+    public readonly editableVehicleTemplateValues: EditableVehicleTemplateValues =
+        {
+            url: null,
+            height: 100,
+            name: null,
+            patientCapacity: 1,
+            type: null,
+            materialTypes: [],
+            personnelTypes: [],
+        };
+
+    constructor(
+        public readonly activeModal: NgbActiveModal,
+        private readonly exerciseService: ExerciseService
path: frontend/src/app/pages/exercises/exercise/shared/editor-panel/create-vehicle-template-modal/create-vehicle-template-modal.component.ts
user: login: lukasrad02
body: Is there a reason for the different visibilities?
created_at: 2025-04-10T11:54:54Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,70 @@
+import { Component } from '@angular/core';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { uuid } from 'digital-fuesim-manv-shared';
+import { ExerciseService } from 'src/app/core/exercise.service';
+import type {
+    ChangedVehicleTemplateValues,
+    EditableVehicleTemplateValues,
+} from '../vehicle-template-form/vehicle-template-form.component';
+
+@Component({
+    selector: 'app-create-vehicle-template-modal',
+    templateUrl: './create-vehicle-template-modal.component.html',
+    styleUrls: ['./create-vehicle-template-modal.component.scss'],
+})
+export class CreateVehicleTemplateModalComponent {
+    public readonly editableVehicleTemplateValues: EditableVehicleTemplateValues =
+        {
+            url: null,
+            height: 100,
+            name: null,
+            patientCapacity: 1,
+            type: null,
+            materialTypes: [],
+            personnelTypes: [],
+        };
+
+    constructor(
+        public readonly activeModal: NgbActiveModal,
+        private readonly exerciseService: ExerciseService
+    ) {}
+
+    public createImageTemplate({
path: frontend/src/app/pages/exercises/exercise/shared/editor-panel/create-vehicle-template-modal/create-vehicle-template-modal.component.ts
user: login: lukasrad02
body: I think this should be `createVehicleTemplate`
created_at: 2025-04-10T11:55:20Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,148 @@
+import { Type } from 'class-transformer';
+import {
+    IsString,
+    ValidateNested,
+    IsUUID,
+    IsInt,
+    IsArray,
+} from 'class-validator';
+import { VehicleTemplate } from '../../models/index.js';
+import type { PersonnelType } from '../../models/utils/index.js';
+import { ImageProperties } from '../../models/utils/index.js';
+import type { MaterialType } from '../../models/utils/material-type.js';
+import type { ExerciseState } from '../../state.js';
+import type { Mutable, UUID } from '../../utils/index.js';
+import { uuidValidationOptions, cloneDeepMutable } from '../../utils/index.js';
+import type { Action, ActionReducer } from '../action-reducer.js';
+import { ReducerError } from '../reducer-error.js';
+
+export class AddVehicleTemplateAction implements Action {
+    @IsString()
path: shared/src/store/action-reducers/vehicle-templates.ts
user: login: lukasrad02
body: Please use `@IsValue('[VehicleTemplate] Add vehicleTemplate' as const)` instead.

(Also applies to the other two actions
created_at: 2025-04-10T12:23:28Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,148 @@
+import { Type } from 'class-transformer';
+import {
+    IsString,
+    ValidateNested,
+    IsUUID,
+    IsInt,
+    IsArray,
+} from 'class-validator';
+import { VehicleTemplate } from '../../models/index.js';
+import type { PersonnelType } from '../../models/utils/index.js';
+import { ImageProperties } from '../../models/utils/index.js';
+import type { MaterialType } from '../../models/utils/material-type.js';
+import type { ExerciseState } from '../../state.js';
+import type { Mutable, UUID } from '../../utils/index.js';
+import { uuidValidationOptions, cloneDeepMutable } from '../../utils/index.js';
+import type { Action, ActionReducer } from '../action-reducer.js';
+import { ReducerError } from '../reducer-error.js';
+
+export class AddVehicleTemplateAction implements Action {
+    @IsString()
+    public readonly type = '[VehicleTemplate] Add vehicleTemplate';
+
+    @ValidateNested()
+    @Type(() => VehicleTemplate)
+    public readonly vehicleTemplate!: VehicleTemplate;
+}
+
+export class EditVehicleTemplateAction implements Action {
+    @IsString()
+    public readonly type = '[VehicleTemplate] Edit vehicleTemplate';
+
+    @IsUUID(4, uuidValidationOptions)
+    public readonly id!: UUID;
+
+    @IsString()
+    public readonly name!: string;
+
+    @IsString()
+    public readonly vehicleType!: string;
+
+    @IsInt()
+    public readonly patientCapacity!: number;
+
+    @ValidateNested()
+    @Type(() => ImageProperties)
+    public readonly image!: ImageProperties;
+
+    @IsString({ each: true })
+    @IsArray()
+    public readonly materials!: readonly MaterialType[];
+
+    @IsString({ each: true })
+    @IsArray()
+    public readonly personnelTypes!: readonly PersonnelType[];
+}
+
+export class DeleteVehicleTemplateAction implements Action {
+    @IsString()
+    public readonly type = '[VehicleTemplate] Delete vehicleTemplates';
path: shared/src/store/action-reducers/vehicle-templates.ts
user: login: lukasrad02
body: ```suggestion
    public readonly type = '[VehicleTemplate] Delete vehicleTemplate';
```
created_at: 2025-04-10T12:24:08Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,148 @@
+import { Type } from 'class-transformer';
+import {
+    IsString,
+    ValidateNested,
+    IsUUID,
+    IsInt,
+    IsArray,
+} from 'class-validator';
+import { VehicleTemplate } from '../../models/index.js';
+import type { PersonnelType } from '../../models/utils/index.js';
+import { ImageProperties } from '../../models/utils/index.js';
+import type { MaterialType } from '../../models/utils/material-type.js';
+import type { ExerciseState } from '../../state.js';
+import type { Mutable, UUID } from '../../utils/index.js';
+import { uuidValidationOptions, cloneDeepMutable } from '../../utils/index.js';
+import type { Action, ActionReducer } from '../action-reducer.js';
+import { ReducerError } from '../reducer-error.js';
+
+export class AddVehicleTemplateAction implements Action {
+    @IsString()
+    public readonly type = '[VehicleTemplate] Add vehicleTemplate';
+
+    @ValidateNested()
+    @Type(() => VehicleTemplate)
+    public readonly vehicleTemplate!: VehicleTemplate;
+}
+
+export class EditVehicleTemplateAction implements Action {
+    @IsString()
+    public readonly type = '[VehicleTemplate] Edit vehicleTemplate';
+
+    @IsUUID(4, uuidValidationOptions)
+    public readonly id!: UUID;
+
+    @IsString()
+    public readonly name!: string;
+
+    @IsString()
+    public readonly vehicleType!: string;
+
+    @IsInt()
+    public readonly patientCapacity!: number;
+
+    @ValidateNested()
+    @Type(() => ImageProperties)
+    public readonly image!: ImageProperties;
+
+    @IsString({ each: true })
+    @IsArray()
+    public readonly materials!: readonly MaterialType[];
+
+    @IsString({ each: true })
+    @IsArray()
+    public readonly personnelTypes!: readonly PersonnelType[];
+}
+
+export class DeleteVehicleTemplateAction implements Action {
+    @IsString()
+    public readonly type = '[VehicleTemplate] Delete vehicleTemplates';
+
+    @IsUUID(4, uuidValidationOptions)
+    public readonly id!: UUID;
+}
+
+export namespace VehicleTemplateActionReducers {
+    export const addVehicleTemplate: ActionReducer<AddVehicleTemplateAction> = {
+        action: AddVehicleTemplateAction,
+        reducer: (draftState, { vehicleTemplate }) => {
+            if (
+                draftState.vehicleTemplates.some(
+                    (template) => template.id === vehicleTemplate.id
+                )
+            ) {
+                throw new ReducerError(
+                    `VehicleTemplate with id ${vehicleTemplate.id} already exists`
+                );
+            }
+            draftState.vehicleTemplates.push(cloneDeepMutable(vehicleTemplate));
+            return draftState;
+        },
+        rights: 'trainer',
+    };
+
+    export const editVehicleTemplate: ActionReducer<EditVehicleTemplateAction> =
+        {
+            action: EditVehicleTemplateAction,
+            reducer: (
+                draftState,
+                {
+                    id,
+                    name,
+                    vehicleType,
+                    image,
+                    patientCapacity,
+                    materials,
+                    personnelTypes,
+                }
+            ) => {
+                const vehicleTemplate = getVehicleTemplate(draftState, id);
+                vehicleTemplate.image = cloneDeepMutable(image);
+                vehicleTemplate.name = name;
+                vehicleTemplate.patientCapacity = patientCapacity;
+                vehicleTemplate.vehicleType = vehicleType;
+                vehicleTemplate.materials = cloneDeepMutable(materials);
+                vehicleTemplate.personnel = cloneDeepMutable(personnelTypes);
+
+                return draftState;
+            },
+            rights: 'trainer',
+        };
+
+    export const deleteVehicleTemplate: ActionReducer<DeleteVehicleTemplateAction> =
+        {
+            action: DeleteVehicleTemplateAction,
+            reducer: (draftState, { id }) => {
+                getVehicleTemplate(draftState, id);
+                draftState.vehicleTemplates =
+                    draftState.vehicleTemplates.filter(
+                        (template) => template.id !== id
+                    );
+                // Delete every alarm group that uses this template
path: shared/src/store/action-reducers/vehicle-templates.ts
user: login: lukasrad02
body: I think we should add a warning about this side effect to the frontend
created_at: 2025-04-10T12:26:08Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,94 @@
+import type { OnInit } from '@angular/core';
+import { Component } from '@angular/core';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { Store } from '@ngrx/store';
+import type {
+    UUID,
+    Mutable,
+    VehicleTemplate,
+} from 'digital-fuesim-manv-shared';
+import { cloneDeepMutable } from 'digital-fuesim-manv-shared';
+import { ExerciseService } from 'src/app/core/exercise.service';
+import type { AppState } from 'src/app/state/app.state';
+import { createSelectVehicleTemplate } from 'src/app/state/application/selectors/exercise.selectors';
+import { selectStateSnapshot } from 'src/app/state/get-state-snapshot';
+import type { ChangedVehicleTemplateValues } from '../vehicle-template-form/vehicle-template-form.component';
+
+@Component({
+    selector: 'app-edit-vehicle-template-modal',
+    templateUrl: './edit-vehicle-template-modal.component.html',
+    styleUrls: ['./edit-vehicle-template-modal.component.scss'],
+    standalone: false,
+})
+export class EditVehicleTemplateModalComponent implements OnInit {
+    // This is set after the modal creation and therefore accessible in ngOnInit
+    public vehicleTemplateId!: UUID;
+
+    public vehicleTemplate?: Mutable<VehicleTemplate>;
+
+    constructor(
+        private readonly exerciseService: ExerciseService,
+        private readonly store: Store<AppState>,
+        public readonly activeModal: NgbActiveModal
+    ) {}
+
+    ngOnInit(): void {
+        this.vehicleTemplate = cloneDeepMutable(
+            selectStateSnapshot(
+                createSelectVehicleTemplate(this.vehicleTemplateId),
+                this.store
+            )
+        );
+    }
+
+    public deleteVehicleTemplate(): void {
+        this.exerciseService
+            .proposeAction({
+                type: '[VehicleTemplate] Delete vehicleTemplates',
+                id: this.vehicleTemplateId,
+            })
+            .then((response) => {
+                if (response.success) {
+                    this.close();
+                }
+            });
+    }
+
+    public editVehicleTemplate({
+        url,
+        height,
+        name,
+        aspectRatio,
+        patientCapacity,
+        type,
+    }: ChangedVehicleTemplateValues): void {
+        if (!this.vehicleTemplate) {
+            console.error("VehicleTemplate wasn't initialized yet");
+            return;
+        }
+        this.exerciseService
+            .proposeAction({
+                type: '[VehicleTemplate] Edit vehicleTemplate',
+                id: this.vehicleTemplateId,
+                name,
+                image: {
+                    url,
+                    height,
+                    aspectRatio,
+                },
+                materials: this.vehicleTemplate.materials,
+                patientCapacity,
+                personnelTypes: this.vehicleTemplate.personnel,
path: frontend/src/app/pages/exercises/exercise/shared/editor-panel/edit-vehicle-template-modal/edit-vehicle-template-modal.component.ts
user: login: lukasrad02
body: I think you should use the values passed as parameters for the personnel and material templates

Edit: Also works in the current way (references to the rescue). But its still a bit confusing
created_at: 2025-04-10T12:28:35Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,70 @@
+import { Component } from '@angular/core';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { uuid } from 'digital-fuesim-manv-shared';
+import { ExerciseService } from 'src/app/core/exercise.service';
+import type {
+    ChangedVehicleTemplateValues,
+    EditableVehicleTemplateValues,
+} from '../vehicle-template-form/vehicle-template-form.component';
+
+@Component({
+    selector: 'app-create-vehicle-template-modal',
+    templateUrl: './create-vehicle-template-modal.component.html',
+    styleUrls: ['./create-vehicle-template-modal.component.scss'],
+})
+export class CreateVehicleTemplateModalComponent {
+    public readonly editableVehicleTemplateValues: EditableVehicleTemplateValues =
+        {
+            url: null,
+            height: 100,
+            name: null,
+            patientCapacity: 1,
+            type: null,
+            materialTypes: [],
+            personnelTypes: [],
+        };
+
+    constructor(
+        public readonly activeModal: NgbActiveModal,
+        private readonly exerciseService: ExerciseService
path: frontend/src/app/pages/exercises/exercise/shared/editor-panel/create-vehicle-template-modal/create-vehicle-template-modal.component.ts
user: login: ClFeSc
body: I don't know, to be honest. The `exerciseService` is `private` because it can be, but out `NgbActiveModal`s are `public` everywhere we use them.
created_at: 2025-04-10T12:36:25Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,8 @@
+import type { NgbModal } from '@ng-bootstrap/ng-bootstrap';
+import { CreateVehicleTemplateModalComponent } from './create-vehicle-template-modal.component';
+
+export async function openCreateVehicleTemplateModal(
+    ngbModalService: NgbModal
+) {
+    ngbModalService.open(CreateVehicleTemplateModalComponent);
path: frontend/src/app/pages/exercises/exercise/shared/editor-panel/create-vehicle-template-modal/open-create-vehicle-template-modal.ts
user: login: lukasrad02
body: Would you mind using an a bit larger size (maybe `lg`) for both the create and edit modal?

When editing a more complex template (e.g. GW San) with lots of material and personnel, one has to scroll down quite a bit to reach the buttons, as only two items fit next to each other.
created_at: 2025-04-10T12:39:54Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,70 @@
+import { Component } from '@angular/core';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { uuid } from 'digital-fuesim-manv-shared';
+import { ExerciseService } from 'src/app/core/exercise.service';
+import type {
+    ChangedVehicleTemplateValues,
+    EditableVehicleTemplateValues,
+} from '../vehicle-template-form/vehicle-template-form.component';
+
+@Component({
+    selector: 'app-create-vehicle-template-modal',
+    templateUrl: './create-vehicle-template-modal.component.html',
+    styleUrls: ['./create-vehicle-template-modal.component.scss'],
+})
+export class CreateVehicleTemplateModalComponent {
+    public readonly editableVehicleTemplateValues: EditableVehicleTemplateValues =
+        {
+            url: null,
+            height: 100,
+            name: null,
+            patientCapacity: 1,
+            type: null,
+            materialTypes: [],
+            personnelTypes: [],
+        };
+
+    constructor(
+        public readonly activeModal: NgbActiveModal,
+        private readonly exerciseService: ExerciseService
path: frontend/src/app/pages/exercises/exercise/shared/editor-panel/create-vehicle-template-modal/create-vehicle-template-modal.component.ts
user: login: lukasrad02
body: > `NgbActiveModals` are public everywhere we use them

They only have to be public if you want to use them from the template (e.g. calling `activeModal.close()` directly in an event handler). I think some people just copied the `public` modifier from such a component and never changed it.
created_at: 2025-04-10T12:43:42Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,70 @@
+import { Component } from '@angular/core';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { uuid } from 'digital-fuesim-manv-shared';
+import { ExerciseService } from 'src/app/core/exercise.service';
+import type {
+    ChangedVehicleTemplateValues,
+    EditableVehicleTemplateValues,
+} from '../vehicle-template-form/vehicle-template-form.component';
+
+@Component({
+    selector: 'app-create-vehicle-template-modal',
+    templateUrl: './create-vehicle-template-modal.component.html',
+    styleUrls: ['./create-vehicle-template-modal.component.scss'],
+})
+export class CreateVehicleTemplateModalComponent {
+    public readonly editableVehicleTemplateValues: EditableVehicleTemplateValues =
+        {
+            url: null,
+            height: 100,
+            name: null,
+            patientCapacity: 1,
+            type: null,
+            materialTypes: [],
+            personnelTypes: [],
+        };
+
+    constructor(
+        public readonly activeModal: NgbActiveModal,
+        private readonly exerciseService: ExerciseService
path: frontend/src/app/pages/exercises/exercise/shared/editor-panel/create-vehicle-template-modal/create-vehicle-template-modal.component.ts
user: login: ClFeSc
body: Ah, I see. Will make it `private`.
created_at: 2025-04-10T12:48:14Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,148 @@
+import { Type } from 'class-transformer';
+import {
+    IsString,
+    ValidateNested,
+    IsUUID,
+    IsInt,
+    IsArray,
+} from 'class-validator';
+import { VehicleTemplate } from '../../models/index.js';
+import type { PersonnelType } from '../../models/utils/index.js';
+import { ImageProperties } from '../../models/utils/index.js';
+import type { MaterialType } from '../../models/utils/material-type.js';
+import type { ExerciseState } from '../../state.js';
+import type { Mutable, UUID } from '../../utils/index.js';
+import { uuidValidationOptions, cloneDeepMutable } from '../../utils/index.js';
+import type { Action, ActionReducer } from '../action-reducer.js';
+import { ReducerError } from '../reducer-error.js';
+
+export class AddVehicleTemplateAction implements Action {
+    @IsString()
+    public readonly type = '[VehicleTemplate] Add vehicleTemplate';
+
+    @ValidateNested()
+    @Type(() => VehicleTemplate)
+    public readonly vehicleTemplate!: VehicleTemplate;
+}
+
+export class EditVehicleTemplateAction implements Action {
+    @IsString()
+    public readonly type = '[VehicleTemplate] Edit vehicleTemplate';
+
+    @IsUUID(4, uuidValidationOptions)
+    public readonly id!: UUID;
+
+    @IsString()
+    public readonly name!: string;
+
+    @IsString()
+    public readonly vehicleType!: string;
+
+    @IsInt()
+    public readonly patientCapacity!: number;
+
+    @ValidateNested()
+    @Type(() => ImageProperties)
+    public readonly image!: ImageProperties;
+
+    @IsString({ each: true })
+    @IsArray()
+    public readonly materials!: readonly MaterialType[];
+
+    @IsString({ each: true })
+    @IsArray()
+    public readonly personnelTypes!: readonly PersonnelType[];
+}
+
+export class DeleteVehicleTemplateAction implements Action {
+    @IsString()
+    public readonly type = '[VehicleTemplate] Delete vehicleTemplates';
+
+    @IsUUID(4, uuidValidationOptions)
+    public readonly id!: UUID;
+}
+
+export namespace VehicleTemplateActionReducers {
+    export const addVehicleTemplate: ActionReducer<AddVehicleTemplateAction> = {
+        action: AddVehicleTemplateAction,
+        reducer: (draftState, { vehicleTemplate }) => {
+            if (
+                draftState.vehicleTemplates.some(
+                    (template) => template.id === vehicleTemplate.id
+                )
+            ) {
+                throw new ReducerError(
+                    `VehicleTemplate with id ${vehicleTemplate.id} already exists`
+                );
+            }
+            draftState.vehicleTemplates.push(cloneDeepMutable(vehicleTemplate));
+            return draftState;
+        },
+        rights: 'trainer',
+    };
+
+    export const editVehicleTemplate: ActionReducer<EditVehicleTemplateAction> =
+        {
+            action: EditVehicleTemplateAction,
+            reducer: (
+                draftState,
+                {
+                    id,
+                    name,
+                    vehicleType,
+                    image,
+                    patientCapacity,
+                    materials,
+                    personnelTypes,
+                }
+            ) => {
+                const vehicleTemplate = getVehicleTemplate(draftState, id);
+                vehicleTemplate.image = cloneDeepMutable(image);
+                vehicleTemplate.name = name;
+                vehicleTemplate.patientCapacity = patientCapacity;
+                vehicleTemplate.vehicleType = vehicleType;
+                vehicleTemplate.materials = cloneDeepMutable(materials);
+                vehicleTemplate.personnel = cloneDeepMutable(personnelTypes);
+
+                return draftState;
+            },
+            rights: 'trainer',
+        };
+
+    export const deleteVehicleTemplate: ActionReducer<DeleteVehicleTemplateAction> =
+        {
+            action: DeleteVehicleTemplateAction,
+            reducer: (draftState, { id }) => {
+                getVehicleTemplate(draftState, id);
+                draftState.vehicleTemplates =
+                    draftState.vehicleTemplates.filter(
+                        (template) => template.id !== id
+                    );
+                // Delete every alarm group that uses this template
path: shared/src/store/action-reducers/vehicle-templates.ts
user: login: ClFeSc
body: I've tried to rework it in a way that only the deleted vehicle gets deleted from the alarm groups (and also have a confirmation beforehand). But this is broken for some reason. The way I've implemented the reducer successfully removes the vehicle template with id `id` from all alarm groups in the backend. In the frontend, however, it removes exactly all vehicles that are *not* `id`, thus breaking many things. Reloading fixes this as the backend never had the issue in the first place. I've pushed `backend-after.json` and `frontend-after.json` files showing the respective states right after applying the remove vehicle template action.
created_at: 2025-04-11T10:13:42Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,148 @@
+import { Type } from 'class-transformer';
+import {
+    IsString,
+    ValidateNested,
+    IsUUID,
+    IsInt,
+    IsArray,
+} from 'class-validator';
+import { VehicleTemplate } from '../../models/index.js';
+import type { PersonnelType } from '../../models/utils/index.js';
+import { ImageProperties } from '../../models/utils/index.js';
+import type { MaterialType } from '../../models/utils/material-type.js';
+import type { ExerciseState } from '../../state.js';
+import type { Mutable, UUID } from '../../utils/index.js';
+import { uuidValidationOptions, cloneDeepMutable } from '../../utils/index.js';
+import type { Action, ActionReducer } from '../action-reducer.js';
+import { ReducerError } from '../reducer-error.js';
+
+export class AddVehicleTemplateAction implements Action {
+    @IsString()
+    public readonly type = '[VehicleTemplate] Add vehicleTemplate';
+
+    @ValidateNested()
+    @Type(() => VehicleTemplate)
+    public readonly vehicleTemplate!: VehicleTemplate;
+}
+
+export class EditVehicleTemplateAction implements Action {
+    @IsString()
+    public readonly type = '[VehicleTemplate] Edit vehicleTemplate';
+
+    @IsUUID(4, uuidValidationOptions)
+    public readonly id!: UUID;
+
+    @IsString()
+    public readonly name!: string;
+
+    @IsString()
+    public readonly vehicleType!: string;
+
+    @IsInt()
+    public readonly patientCapacity!: number;
+
+    @ValidateNested()
+    @Type(() => ImageProperties)
+    public readonly image!: ImageProperties;
+
+    @IsString({ each: true })
+    @IsArray()
+    public readonly materials!: readonly MaterialType[];
+
+    @IsString({ each: true })
+    @IsArray()
+    public readonly personnelTypes!: readonly PersonnelType[];
+}
+
+export class DeleteVehicleTemplateAction implements Action {
+    @IsString()
+    public readonly type = '[VehicleTemplate] Delete vehicleTemplates';
+
+    @IsUUID(4, uuidValidationOptions)
+    public readonly id!: UUID;
+}
+
+export namespace VehicleTemplateActionReducers {
+    export const addVehicleTemplate: ActionReducer<AddVehicleTemplateAction> = {
+        action: AddVehicleTemplateAction,
+        reducer: (draftState, { vehicleTemplate }) => {
+            if (
+                draftState.vehicleTemplates.some(
+                    (template) => template.id === vehicleTemplate.id
+                )
+            ) {
+                throw new ReducerError(
+                    `VehicleTemplate with id ${vehicleTemplate.id} already exists`
+                );
+            }
+            draftState.vehicleTemplates.push(cloneDeepMutable(vehicleTemplate));
+            return draftState;
+        },
+        rights: 'trainer',
+    };
+
+    export const editVehicleTemplate: ActionReducer<EditVehicleTemplateAction> =
+        {
+            action: EditVehicleTemplateAction,
+            reducer: (
+                draftState,
+                {
+                    id,
+                    name,
+                    vehicleType,
+                    image,
+                    patientCapacity,
+                    materials,
+                    personnelTypes,
+                }
+            ) => {
+                const vehicleTemplate = getVehicleTemplate(draftState, id);
+                vehicleTemplate.image = cloneDeepMutable(image);
+                vehicleTemplate.name = name;
+                vehicleTemplate.patientCapacity = patientCapacity;
+                vehicleTemplate.vehicleType = vehicleType;
+                vehicleTemplate.materials = cloneDeepMutable(materials);
+                vehicleTemplate.personnel = cloneDeepMutable(personnelTypes);
+
+                return draftState;
+            },
+            rights: 'trainer',
+        };
+
+    export const deleteVehicleTemplate: ActionReducer<DeleteVehicleTemplateAction> =
+        {
+            action: DeleteVehicleTemplateAction,
+            reducer: (draftState, { id }) => {
+                getVehicleTemplate(draftState, id);
+                draftState.vehicleTemplates =
+                    draftState.vehicleTemplates.filter(
+                        (template) => template.id !== id
+                    );
+                // Delete every alarm group that uses this template
path: shared/src/store/action-reducers/vehicle-templates.ts
user: login: ClFeSc
body: This should be working. I think I had the same problem as when switching branches with the new `vite` approach from `angular`: It did not update the `shared` package correctly in the `frontend`, and the version it kept was the one where I had accidentally swapped the condition around. And then it never updated...

Btw: `vite` is a great piece of software: https://github.com/hpi-sam/digital-fuesim-manv/actions/runs/14408881083/job/40411947000?pr=589#step:7:9
created_at: 2025-04-11T17:41:02Z
author_association: COLLABORATOR
diff_hunk: @@ -29,7 +30,8 @@ export class EditVehicleTemplateModalComponent implements OnInit {
     constructor(
         private readonly exerciseService: ExerciseService,
         private readonly store: Store<AppState>,
-        public readonly activeModal: NgbActiveModal
+        public readonly activeModal: NgbActiveModal,
path: frontend/src/app/pages/exercises/exercise/shared/editor-panel/edit-vehicle-template-modal/edit-vehicle-template-modal.component.ts
user: login: lukasrad02
body: ```suggestion
        private readonly activeModal: NgbActiveModal,
```

Sorry, didn't notice this in the first review
created_at: 2025-04-12T06:55:49Z
author_association: COLLABORATOR
diff_hunk: @@ -29,7 +30,8 @@ export class EditVehicleTemplateModalComponent implements OnInit {
     constructor(
         private readonly exerciseService: ExerciseService,
         private readonly store: Store<AppState>,
-        public readonly activeModal: NgbActiveModal
+        public readonly activeModal: NgbActiveModal,
path: frontend/src/app/pages/exercises/exercise/shared/editor-panel/edit-vehicle-template-modal/edit-vehicle-template-modal.component.ts
user: login: ClFeSc
body: Changed it.
created_at: 2025-04-12T09:36:47Z
author_association: COLLABORATOR

PR comments:
user: login: ClFeSc
created_at: 2025-04-10T12:15:56Z
body: @lukasrad02 This should now be ready for review.
author_association: COLLABORATOR
user: login: lukasrad02
created_at: 2025-04-12T07:01:31Z
body: Apart from the one change request above, this PR looks fine now
author_association: COLLABORATOR
user: login: lukasrad02
created_at: 2025-04-12T14:10:01Z
body: I've noticed that you merged #522 with a merge commit. The previous PRs have all been squashed.

I don't have a strong opinion on this, but would prefer continuing squash merging.
author_association: COLLABORATOR
user: login: ClFeSc
created_at: 2025-04-12T15:45:52Z
body: > I've noticed that you merged #522 with a merge commit. The previous PRs have all been squashed.
> 
> I don't have a strong opinion on this, but would prefer continuing squash merging.

Ah, missed that. I've had to look up which merge strategy was used in another repo before and must've just kept my assumption without checking again. I've used a squash merge here. In repos where we don't usually rebase I like this more than adding all those commits to the default branch.
author_association: COLLABORATOR