diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7c1c9a1e7..0d505a13c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -28,6 +28,9 @@ and this project does **not** adhere to [Semantic Versioning](https://semver.org
- Trainers can see the current occupation of a vehicle and cancel it.
- Simulated regions are prefixed with "\[Simuliert\]" in the request target selection for the requests behavior.
- The patient, whose popup is open, is now highlighted.
+- There is now a behavior that transfers patients. It has configurable load times and delay between transfers.
+ - Its user interface can be used to transfer specific patients in specific vehicles.
+ - Its user interface displays what vehicles are being loaded and what vehicles are waiting for transfer.
### Changed
diff --git a/frontend/src/app/pages/exercises/exercise/shared/editor-panel/templates/simulated-region.ts b/frontend/src/app/pages/exercises/exercise/shared/editor-panel/templates/simulated-region.ts
index 8387be2a0..bba9b49ba 100644
--- a/frontend/src/app/pages/exercises/exercise/shared/editor-panel/templates/simulated-region.ts
+++ b/frontend/src/app/pages/exercises/exercise/shared/editor-panel/templates/simulated-region.ts
@@ -5,6 +5,7 @@ import type {
Mutable,
} from 'digital-fuesim-manv-shared';
import {
+ TransferBehaviorState,
cloneDeepMutable,
MapPosition,
SimulatedRegion,
@@ -76,6 +77,7 @@ const stereotypes: SimulatedRegion[] = [
AutomaticallyDistributeVehiclesBehaviorState.create(),
AnswerRequestsBehaviorState.create(),
ReportBehaviorState.create(),
+ TransferBehaviorState.create(),
],
inEvents: [],
position,
diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/simulated-region-overview.module.ts b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/simulated-region-overview.module.ts
index 4437f6dea..d80eec5fa 100644
--- a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/simulated-region-overview.module.ts
+++ b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/simulated-region-overview.module.ts
@@ -48,6 +48,7 @@ import { RequestVehiclesComponent } from './tabs/behavior-tab/behaviors/request-
import { SimulatedRegionOverviewPatientInteractionBarComponent } from './tabs/patients-tab/simulated-region-overview-patient-interaction-bar/simulated-region-overview-patient-interaction-bar.component';
import { SimulatedRegionOverviewVehiclesTabComponent } from './tabs/vehicles-tab/simulated-region-overview-vehicles-tab.component';
import { SimulatedRegionOverviewPatientsTableComponent } from './patients-table/simulated-region-overview-patients-table.component';
+import { SimulatedRegionOverviewBehaviorTransferVehiclesComponent } from './tabs/behavior-tab/behaviors/transfer-vehicles/simulated-region-overview-behavior-transfer-vehicles.component';
@NgModule({
declarations: [
@@ -87,6 +88,7 @@ import { SimulatedRegionOverviewPatientsTableComponent } from './patients-table/
SimulatedRegionOverviewPatientInteractionBarComponent,
SimulatedRegionOverviewVehiclesTabComponent,
SimulatedRegionOverviewPatientsTableComponent,
+ SimulatedRegionOverviewBehaviorTransferVehiclesComponent,
],
imports: [
FormsModule,
diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/request-vehicles/simulated-region-overview-behavior-request-vehicles.component.ts b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/request-vehicles/simulated-region-overview-behavior-request-vehicles.component.ts
index 0357ce9a6..c58203c9b 100644
--- a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/request-vehicles/simulated-region-overview-behavior-request-vehicles.component.ts
+++ b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/request-vehicles/simulated-region-overview-behavior-request-vehicles.component.ts
@@ -61,7 +61,7 @@ export class RequestVehiclesComponent implements OnChanges {
`[Simuliert] ${simulatedRegion.name}`,
])
.filter(([id, _name]) => id !== this.simulatedRegionId)
- .sort(([id1, name1], [id2, name2]) =>
+ .sort(([_id1, name1], [_id2, name2]) =>
name1 === name2 ? 0 : name1! < name2! ? -1 : 1
);
options.unshift(['trainees', 'Die Trainierenden']);
diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/transfer-vehicles/simulated-region-overview-behavior-transfer-vehicles.component.html b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/transfer-vehicles/simulated-region-overview-behavior-transfer-vehicles.component.html
new file mode 100644
index 000000000..dadae6f21
--- /dev/null
+++ b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/transfer-vehicles/simulated-region-overview-behavior-transfer-vehicles.component.html
@@ -0,0 +1,421 @@
+
+
+
+
+
+
+ Informationen
+
+
+
+
+
+ Auf Transfer wartende Fahrzeuge
+
+ 0; else noneBuffered"
+ class="table table-striped"
+ >
+
+
+ Fahrzeug
+ Ziel
+ Patienten
+
+
+
+
+
+
+ {{ bufferedTransfer.vehicleName }}
+
+
+ {{ bufferedTransfer.destination }}
+
+
+ {{ bufferedTransfer.numberOfPatients }}
+
+
+
+
+
+
+ Derzeit warten keine Fahrzeuge auf den Transfer.
+
+
+
+
+
+ Fahrzeuge, die gerade für den Transfer beladen werden
+
+ 0; else noActivities"
+ class="table table-striped"
+ >
+
+
+ Fahrzeug
+ Ziel
+ Patienten
+
+
+
+
+
+
+ {{ activeActivity.vehicleName }}
+
+
+ {{ activeActivity.destination }}
+
+
+ {{ activeActivity.numberOfPatients }}
+
+
+
+
+
+
+ Derzeit werden keine Fahrzeuge für den Transfer beladen.
+
+
+
+
+
+
+
+
+
+ Einstellungen
+
+
+
+
+
+
+
+ Ladezeit pro Patient
+
+
+
+
+
+ Ladezeit für Personal
+
+
+
+
+
+ Zeit zwischen zwei Ausfahrten
+
+
+
+
+
+
+
+
+
+
+ Transfer initiieren
+
+
+
+
+
+
+ Es können nur so viele Patienten ausgewählt werden, wie in das
+ ausgewählte Fahrzeug passen. Das Fahrzeug kann nur zu einem Fahrzeug
+ geändert werden, in welches alle ausgewählten Patienten passen.
+
+
+
+ {{
+ vehicleToSend.name
+ }}
+
+ Kein Fahrzeug ausgewählt
+
+
+
+
+ = minPatients"
+ >
+
+
+
+ {{ useableVehicle.name }}
+
+
+
+
+
+
+
+
+
+ SK
+ Gesichtet
+ Verlauf
+ Ausgewählt
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Es befinden sich keine Patienten in der Region.
+
+
+
+
+
+
+
+
+ {{
+ selectedDestination.name
+ ? selectedDestination.name
+ : selectedDestination.externalName
+ }}
+
+
+ Kein Ziel ausgewählt
+
+
+
+
+
+
+
+
+ {{ reachableTransferPoint.externalName }}
+
+
+
+
+
+
+
+ {{ reachableHospital.name }}
+
+
+
+
+
+
+
+
+
+ Fahrzeug versenden
+
+
+
+
diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/transfer-vehicles/simulated-region-overview-behavior-transfer-vehicles.component.scss b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/transfer-vehicles/simulated-region-overview-behavior-transfer-vehicles.component.scss
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/transfer-vehicles/simulated-region-overview-behavior-transfer-vehicles.component.ts b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/transfer-vehicles/simulated-region-overview-behavior-transfer-vehicles.component.ts
new file mode 100644
index 000000000..5794d3c8d
--- /dev/null
+++ b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/transfer-vehicles/simulated-region-overview-behavior-transfer-vehicles.component.ts
@@ -0,0 +1,394 @@
+import type { OnDestroy, OnInit } from '@angular/core';
+import { Component, Input } from '@angular/core';
+import type { MemoizedSelector } from '@ngrx/store';
+import { Store, createSelector } from '@ngrx/store';
+import type {
+ Hospital,
+ PatientStatus,
+ TransferBehaviorState,
+ TransferPoint,
+ UUIDSet,
+ Vehicle,
+} from 'digital-fuesim-manv-shared';
+import {
+ isInSpecificSimulatedRegion,
+ Patient,
+ UUID,
+} from 'digital-fuesim-manv-shared';
+import type { Observable } from 'rxjs';
+import { Subject, takeUntil } from 'rxjs';
+import { ExerciseService } from 'src/app/core/exercise.service';
+import type { AppState } from 'src/app/state/app.state';
+import {
+ createSelectActivityStatesByType,
+ createSelectBehaviorState,
+ createSelectElementsInSimulatedRegion,
+ selectConfiguration,
+ selectHospitals,
+ selectPatients,
+ selectTransferPoints,
+ selectVehicles,
+} from 'src/app/state/application/selectors/exercise.selectors';
+import { comparePatientsByVisibleStatus } from '../../../compare-patients';
+
+let globalLastInformationCollapsed = true;
+let globalLastSettingsCollapsed = true;
+let globalLastTransferCollapsed = true;
+
+@Component({
+ selector: 'app-simulated-region-overview-behavior-transfer-vehicles',
+ templateUrl:
+ './simulated-region-overview-behavior-transfer-vehicles.component.html',
+ styleUrls: [
+ './simulated-region-overview-behavior-transfer-vehicles.component.scss',
+ ],
+})
+export class SimulatedRegionOverviewBehaviorTransferVehiclesComponent
+ implements OnInit, OnDestroy
+{
+ @Input() simulatedRegionId!: UUID;
+ @Input() transferBehaviorId!: UUID;
+
+ private readonly destroy$ = new Subject();
+ public minPatients = 0;
+
+ public clickedPatients: { [key: UUID]: boolean } = {};
+
+ public bufferedTransfers$!: Observable<
+ { vehicleName: string; destination: string; numberOfPatients: number }[]
+ >;
+ public activeActivities$!: Observable<
+ { vehicleName: string; destination: string; numberOfPatients: number }[]
+ >;
+
+ public vehicleToSend?: Vehicle;
+
+ public useableVehicles$!: Observable;
+
+ public reachableTransferPoints$!: Observable;
+ public reachableHospitals$!: Observable;
+
+ public selectedDestination?: {
+ name?: string | undefined;
+ externalName?: string | undefined;
+ } & (Hospital | TransferPoint);
+
+ private _informationCollapsed: boolean;
+ transferBehaviorState$!: Observable;
+ patients$!: Observable<
+ (Patient & {
+ visibleStatus: PatientStatus;
+ })[]
+ >;
+
+ public get informationCollapsed(): boolean {
+ return this._informationCollapsed;
+ }
+ public set informationCollapsed(value: boolean) {
+ this._informationCollapsed = value;
+ globalLastInformationCollapsed = value;
+ }
+
+ private _settingsCollapsed: boolean;
+
+ public get settingsCollapsed(): boolean {
+ return this._settingsCollapsed;
+ }
+ public set settingsCollapsed(value: boolean) {
+ this._settingsCollapsed = value;
+ globalLastSettingsCollapsed = value;
+ }
+
+ private _transferCollapsed: boolean;
+
+ public get transferCollapsed(): boolean {
+ return this._transferCollapsed;
+ }
+ public set transferCollapsed(value: boolean) {
+ this._transferCollapsed = value;
+ globalLastTransferCollapsed = value;
+ }
+
+ constructor(
+ private readonly store: Store,
+ private readonly exerciseService: ExerciseService
+ ) {
+ this._informationCollapsed = globalLastInformationCollapsed;
+ this._settingsCollapsed = globalLastSettingsCollapsed;
+ this._transferCollapsed = globalLastTransferCollapsed;
+ }
+
+ ngOnInit(): void {
+ const transferBehaviorStateSelector: MemoizedSelector<
+ AppState,
+ TransferBehaviorState
+ > = createSelectBehaviorState(
+ this.simulatedRegionId,
+ this.transferBehaviorId
+ );
+
+ const bufferedTransferEventQueueSelector = createSelector(
+ transferBehaviorStateSelector,
+ (transferBehaviorState) =>
+ transferBehaviorState.startTransferEventQueue
+ );
+
+ const bufferedTransfersSelector = createSelector(
+ bufferedTransferEventQueueSelector,
+ selectVehicles,
+ selectHospitals,
+ selectTransferPoints,
+ (bufferedTransferEventQueue, vehicles, hospitals, transferPoints) =>
+ bufferedTransferEventQueue.map((bufferedTransferEvent) => ({
+ vehicleName:
+ vehicles[bufferedTransferEvent.vehicleId]?.name ??
+ 'Gelöschtes Fahrzeug',
+ destination:
+ bufferedTransferEvent.transferDestinationType ===
+ 'hospital'
+ ? hospitals[
+ bufferedTransferEvent.transferDestinationId
+ ]?.name ?? 'Gelöschtes Krankenhaus'
+ : transferPoints[
+ bufferedTransferEvent.transferDestinationId
+ ]?.externalName ?? 'Gelöschtes Transferziel',
+ numberOfPatients: Object.keys(
+ vehicles[bufferedTransferEvent.vehicleId]?.patientIds ??
+ {}
+ ).length,
+ }))
+ );
+
+ const activeActivityStatesSelector = createSelectActivityStatesByType(
+ this.simulatedRegionId,
+ 'loadVehicleActivity'
+ );
+
+ const activeActivitiesSelector = createSelector(
+ activeActivityStatesSelector,
+ selectVehicles,
+ selectHospitals,
+ selectTransferPoints,
+ (activeActivityStates, vehicles, hospitals, transferPoints) =>
+ activeActivityStates.map((activeActivityState) => ({
+ vehicleName:
+ vehicles[activeActivityState.vehicleId]?.name ??
+ 'Gelöschtes Fahrzeug',
+ destination:
+ activeActivityState.transferDestinationType ===
+ 'hospital'
+ ? hospitals[
+ activeActivityState.transferDestinationId
+ ]?.name ?? 'Gelöschtes Krankenhaus'
+ : transferPoints[
+ activeActivityState.transferDestinationId
+ ]?.externalName ?? 'Gelöschtes Transferziel',
+ numberOfPatients: Object.keys(
+ activeActivityState.patientsToBeLoaded
+ ).length,
+ }))
+ );
+
+ const ownTransferPointSelector = createSelector(
+ selectTransferPoints,
+ (transferPoints) =>
+ Object.values(transferPoints).find((transferPoint) =>
+ isInSpecificSimulatedRegion(
+ transferPoint,
+ this.simulatedRegionId
+ )
+ )!
+ );
+
+ const vehiclesInSimulatedRegionSelector =
+ createSelectElementsInSimulatedRegion(
+ selectVehicles,
+ this.simulatedRegionId
+ );
+
+ const patientsInSimulatedRegionSelector =
+ createSelectElementsInSimulatedRegion(
+ selectPatients,
+ this.simulatedRegionId
+ );
+
+ const reachableTransferPointsSelector = createSelector(
+ selectTransferPoints,
+ ownTransferPointSelector,
+ (transferPoints, transferPoint) =>
+ Object.keys(transferPoint.reachableTransferPoints).map(
+ (id) => transferPoints[id]!
+ )
+ );
+
+ const reachableHospitalsSelector = createSelector(
+ selectHospitals,
+ ownTransferPointSelector,
+ (hospitals, transferPoint) =>
+ Object.keys(transferPoint.reachableHospitals).map(
+ (id) => hospitals[id]!
+ )
+ );
+
+ this.patients$ = this.store.select(
+ createSelector(
+ patientsInSimulatedRegionSelector,
+ selectConfiguration,
+ (patients, configuration) =>
+ patients
+ .sort((patientA, patientB) =>
+ comparePatientsByVisibleStatus(
+ patientA,
+ patientB,
+ configuration
+ )
+ )
+ .map((patient) => ({
+ visibleStatus: Patient.getVisibleStatus(
+ patient,
+ configuration.pretriageEnabled,
+ configuration.bluePatientsEnabled
+ ),
+ ...patient,
+ }))
+ )
+ );
+ this.bufferedTransfers$ = this.store.select(bufferedTransfersSelector);
+ this.activeActivities$ = this.store.select(activeActivitiesSelector);
+ this.transferBehaviorState$ = this.store.select(
+ transferBehaviorStateSelector
+ );
+ this.useableVehicles$ = this.store.select(
+ vehiclesInSimulatedRegionSelector
+ );
+
+ this.reachableTransferPoints$ = this.store.select(
+ reachableTransferPointsSelector
+ );
+ this.reachableHospitals$ = this.store.select(
+ reachableHospitalsSelector
+ );
+
+ // remove selected elements if deleted
+
+ this.useableVehicles$
+ .pipe(takeUntil(this.destroy$))
+ .subscribe((useableVehicles) => {
+ if (
+ this.vehicleToSend &&
+ !useableVehicles.includes(this.vehicleToSend)
+ ) {
+ this.vehicleToSend = undefined;
+ }
+ });
+
+ this.patients$.pipe(takeUntil(this.destroy$)).subscribe((patients) => {
+ Object.entries(this.clickedPatients).forEach(
+ ([patientId, clicked]) => {
+ if (
+ !patients
+ .map((patient) => patient.id)
+ .includes(patientId)
+ ) {
+ if (clicked) {
+ this.minPatients--;
+ }
+ delete this.clickedPatients[patientId];
+ }
+ }
+ );
+ });
+
+ this.reachableHospitals$
+ .pipe(takeUntil(this.destroy$))
+ .subscribe((reachableHospitals) => {
+ if (
+ this.selectedDestination?.type === 'hospital' &&
+ !reachableHospitals.includes(this.selectedDestination)
+ ) {
+ this.selectedDestination = undefined;
+ }
+ });
+
+ this.reachableTransferPoints$
+ .pipe(takeUntil(this.destroy$))
+ .subscribe((reachableTransferPoints) => {
+ if (
+ this.selectedDestination?.type === 'transferPoint' &&
+ !reachableTransferPoints.includes(this.selectedDestination)
+ ) {
+ this.selectedDestination = undefined;
+ }
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.destroy$.next();
+ }
+
+ public updatePatientLoadTime(loadTimePerPatient: number) {
+ this.exerciseService.proposeAction({
+ type: '[TransferBehavior] Update Patient Load Time',
+ simulatedRegionId: this.simulatedRegionId,
+ behaviorId: this.transferBehaviorId,
+ loadTimePerPatient,
+ });
+ }
+ public updatePersonnelLoadTime(personnelLoadTime: number) {
+ this.exerciseService.proposeAction({
+ type: '[TransferBehavior] Update Personnel Load Time',
+ simulatedRegionId: this.simulatedRegionId,
+ behaviorId: this.transferBehaviorId,
+ personnelLoadTime,
+ });
+ }
+ public updateSendDelay(delayBetweenSends: number) {
+ this.exerciseService.proposeAction({
+ type: '[TransferBehavior] Update Delay Between Sends',
+ simulatedRegionId: this.simulatedRegionId,
+ behaviorId: this.transferBehaviorId,
+ delayBetweenSends,
+ });
+ }
+
+ public togglePatientSelection(patient: Patient) {
+ if (
+ !this.clickedPatients[patient.id] &&
+ this.minPatients + 1 >
+ (this.vehicleToSend?.patientCapacity ??
+ Number.POSITIVE_INFINITY)
+ ) {
+ return;
+ }
+ this.clickedPatients[patient.id] = !this.clickedPatients[patient.id];
+ this.minPatients = Object.values(this.clickedPatients).filter(
+ (clicked) => clicked
+ ).length;
+ }
+
+ public sendVehicle() {
+ if (!this.vehicleToSend || !this.selectedDestination) {
+ return;
+ }
+
+ const patients: UUIDSet = Object.fromEntries(
+ Object.entries(this.clickedPatients)
+ .filter(([_patientId, clicked]) => clicked)
+ .map(([patientId, _clicked]) => [patientId, true])
+ );
+
+ this.exerciseService.proposeAction({
+ type: '[TransferBehavior] Send Transfer Request Event',
+ simulatedRegionId: this.simulatedRegionId,
+ behaviorId: this.transferBehaviorId,
+ vehicleId: this.vehicleToSend.id,
+ destinationType: this.selectedDestination.type,
+ destinationId: this.selectedDestination.id,
+ patients,
+ });
+
+ this.clickedPatients = {};
+ this.vehicleToSend = undefined;
+ this.minPatients = 0;
+ }
+}
diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/simulated-region-overview-behavior-tab.component.html b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/simulated-region-overview-behavior-tab.component.html
index 27e86e4ba..c73d67d0d 100644
--- a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/simulated-region-overview-behavior-tab.component.html
+++ b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/simulated-region-overview-behavior-tab.component.html
@@ -67,6 +67,11 @@
[simulatedRegionId]="simulatedRegion.id"
[requestBehaviorId]="selectedBehavior!.id"
>
+
Es ist noch keine Verhaltensweise ausgewählt.
diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/utils/behavior-to-german-name.pipe.ts b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/utils/behavior-to-german-name.pipe.ts
index f55f1dc62..ff8dd9014 100644
--- a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/utils/behavior-to-german-name.pipe.ts
+++ b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/utils/behavior-to-german-name.pipe.ts
@@ -13,6 +13,7 @@ const behaviorTypeToGermanNameDictionary: {
answerRequestsBehavior: 'Fahrzeuganfragen beantworten',
automaticallyDistributeVehiclesBehavior: 'Fahrzeuge verteilen',
requestBehavior: 'Fahrzeuge anfordern',
+ transferBehavior: 'Fahrzeuge versenden',
};
@Pipe({
name: 'behaviorTypeToGermanName',
diff --git a/frontend/src/app/shared/components/vehicle-occupation-editor/vehicle-occupation-editor.component.html b/frontend/src/app/shared/components/vehicle-occupation-editor/vehicle-occupation-editor.component.html
index 178c3a559..a5a70ed56 100644
--- a/frontend/src/app/shared/components/vehicle-occupation-editor/vehicle-occupation-editor.component.html
+++ b/frontend/src/app/shared/components/vehicle-occupation-editor/vehicle-occupation-editor.component.html
@@ -12,6 +12,12 @@
Das Fahrzeug wird gerade ausgeladen.
+
+ Das Fahrzeug wird gerade beladen.
+
+
+ Das Fahrzeug wartet auf den Transfer.
+
Die Tätigkeit ist unbekannt.
diff --git a/shared/src/models/simulated-region.ts b/shared/src/models/simulated-region.ts
index 1a54ac306..f4e5580da 100644
--- a/shared/src/models/simulated-region.ts
+++ b/shared/src/models/simulated-region.ts
@@ -38,6 +38,20 @@ export class SimulatedRegion {
@IsString()
public readonly borderColor: string;
+ @Type(...simulationEventTypeOptions)
+ @ValidateNested()
+ public readonly inEvents: readonly ExerciseSimulationEvent[] = [];
+
+ @Type(...simulationBehaviorTypeOptions)
+ @ValidateNested()
+ public readonly behaviors: readonly ExerciseSimulationBehaviorState[] = [];
+
+ @IsMultiTypedIdMap(getSimulationActivityConstructor)
+ @ValidateNested()
+ public readonly activities: {
+ readonly [stateId: UUID]: ExerciseSimulationActivityState;
+ } = {};
+
/**
* @param position top-left position
* @deprecated Use {@link create} instead
@@ -54,20 +68,6 @@ export class SimulatedRegion {
this.borderColor = borderColor;
}
- @Type(...simulationEventTypeOptions)
- @ValidateNested()
- public readonly inEvents: readonly ExerciseSimulationEvent[] = [];
-
- @Type(...simulationBehaviorTypeOptions)
- @ValidateNested()
- public readonly behaviors: readonly ExerciseSimulationBehaviorState[] = [];
-
- @IsMultiTypedIdMap(getSimulationActivityConstructor)
- @ValidateNested()
- public readonly activities: {
- readonly [stateId: UUID]: ExerciseSimulationActivityState;
- } = {};
-
static readonly create = getCreate(this);
static image: ImageProperties = {
diff --git a/shared/src/models/utils/occupations/exercise-occupation.ts b/shared/src/models/utils/occupations/exercise-occupation.ts
index 6f4a76eb7..2261ab1c7 100644
--- a/shared/src/models/utils/occupations/exercise-occupation.ts
+++ b/shared/src/models/utils/occupations/exercise-occupation.ts
@@ -3,11 +3,15 @@ import type { Constructor } from '../../../utils';
import { IntermediateOccupation } from './intermediate-occupation';
import { Occupation } from './occupation';
import { NoOccupation } from './no-occupation';
+import { LoadOccupation } from './load-occupation';
+import { WaitForTransferOccupation } from './wait-for-transfer-occupation';
import { UnloadingOccupation } from './unloading-occupation';
export const occupations = {
IntermediateOccupation,
NoOccupation,
+ LoadOccupation,
+ WaitForTransferOccupation,
UnloadingOccupation,
};
@@ -24,6 +28,8 @@ export type ExerciseOccupationType = ExerciseOccupation['type'];
export const occupationDictionary: ExerciseOccupationDictionary = {
intermediateOccupation: IntermediateOccupation,
noOccupation: NoOccupation,
+ loadOccupation: LoadOccupation,
+ waitForTransferOccupation: WaitForTransferOccupation,
unloadingOccupation: UnloadingOccupation,
};
diff --git a/shared/src/models/utils/occupations/load-occupation.ts b/shared/src/models/utils/occupations/load-occupation.ts
new file mode 100644
index 000000000..c4082820f
--- /dev/null
+++ b/shared/src/models/utils/occupations/load-occupation.ts
@@ -0,0 +1,19 @@
+import { IsUUID } from 'class-validator';
+import { IsValue } from '../../../utils/validators';
+import { getCreate } from '../get-create';
+import { UUID, uuidValidationOptions } from '../../../utils';
+import type { Occupation } from './occupation';
+
+export class LoadOccupation implements Occupation {
+ @IsValue('loadOccupation')
+ readonly type = 'loadOccupation';
+
+ @IsUUID(4, uuidValidationOptions)
+ readonly loadingActivityId: UUID;
+
+ constructor(loadingActivityId: UUID) {
+ this.loadingActivityId = loadingActivityId;
+ }
+
+ static readonly create = getCreate(this);
+}
diff --git a/shared/src/models/utils/occupations/wait-for-transfer-occupation.ts b/shared/src/models/utils/occupations/wait-for-transfer-occupation.ts
new file mode 100644
index 000000000..afcb5b837
--- /dev/null
+++ b/shared/src/models/utils/occupations/wait-for-transfer-occupation.ts
@@ -0,0 +1,10 @@
+import { IsValue } from '../../../utils/validators';
+import { getCreate } from '../get-create';
+import type { Occupation } from './occupation';
+
+export class WaitForTransferOccupation implements Occupation {
+ @IsValue('waitForTransferOccupation')
+ readonly type = 'waitForTransferOccupation';
+
+ static readonly create = getCreate(this);
+}
diff --git a/shared/src/simulation/activities/exercise-simulation-activity.ts b/shared/src/simulation/activities/exercise-simulation-activity.ts
index aa4c1141b..c7fb8c757 100644
--- a/shared/src/simulation/activities/exercise-simulation-activity.ts
+++ b/shared/src/simulation/activities/exercise-simulation-activity.ts
@@ -6,8 +6,10 @@ import { unloadVehicleActivity } from './unload-vehicle';
import { recurringEventActivity } from './recurring-event';
import { generateReportActivity } from './generate-report';
import { providePersonnelFromVehiclesActivity } from './provide-personnel-from-vehicles';
-import { transferVehiclesActivity } from './transfer-vehicles';
import { createRequestActivity } from './create-request';
+import { loadVehicleActivity } from './load-vehicle';
+import { sendRemoteEventActivity } from './send-remote-event';
+import { transferVehicleActivity } from './transfer-vehicle';
export const simulationActivities = {
reassignTreatmentsActivity,
@@ -16,8 +18,10 @@ export const simulationActivities = {
recurringEventActivity,
generateReportActivity,
providePersonnelFromVehiclesActivity,
- transferVehiclesActivity,
createRequestActivity,
+ loadVehicleActivity,
+ sendRemoteEventActivity,
+ transferVehicleActivity,
};
export type ExerciseSimulationActivity =
diff --git a/shared/src/simulation/activities/index.ts b/shared/src/simulation/activities/index.ts
index 0a173f713..5e047c279 100644
--- a/shared/src/simulation/activities/index.ts
+++ b/shared/src/simulation/activities/index.ts
@@ -4,4 +4,6 @@ export * from './reassign-treatments';
export * from './unload-vehicle';
export * from './recurring-event';
export * from './provide-personnel-from-vehicles';
-export * from './transfer-vehicles';
+export * from './load-vehicle';
+export * from './send-remote-event';
+export * from './transfer-vehicle';
diff --git a/shared/src/simulation/activities/load-vehicle.ts b/shared/src/simulation/activities/load-vehicle.ts
new file mode 100644
index 000000000..c9b8c3f6d
--- /dev/null
+++ b/shared/src/simulation/activities/load-vehicle.ts
@@ -0,0 +1,250 @@
+import {
+ IsBoolean,
+ IsInt,
+ IsOptional,
+ IsString,
+ IsUUID,
+ Min,
+} from 'class-validator';
+import {
+ SimulatedRegionPosition,
+ VehiclePosition,
+ getCreate,
+ isInSpecificSimulatedRegion,
+} from '../../models/utils';
+import {
+ UUID,
+ UUIDSet,
+ cloneDeepMutable,
+ uuidValidationOptions,
+} from '../../utils';
+import { IsLiteralUnion, IsUUIDSet, IsValue } from '../../utils/validators';
+import {
+ TransferDestination,
+ transferDestinationTypeAllowedValues,
+} from '../utils/transfer-destination';
+import { getElement } from '../../store/action-reducers/utils';
+import { sendSimulationEvent } from '../events/utils';
+import {
+ MaterialRemovedEvent,
+ NewPatientEvent,
+ PatientRemovedEvent,
+ PersonnelRemovedEvent,
+ StartTransferEvent,
+} from '../events';
+import { completelyLoadVehicle } from '../../store/action-reducers/utils/completely-load-vehicle';
+import { IntermediateOccupation } from '../../models/utils/occupations/intermediate-occupation';
+import { changePositionWithId } from '../../models/utils/position/position-helpers-mutable';
+import type {
+ SimulationActivity,
+ SimulationActivityState,
+} from './simulation-activity';
+
+export class LoadVehicleActivityState implements SimulationActivityState {
+ @IsValue('loadVehicleActivity' as const)
+ public readonly type = 'loadVehicleActivity';
+
+ @IsUUID(4, uuidValidationOptions)
+ public readonly id: UUID;
+
+ @IsUUID(4, uuidValidationOptions)
+ public readonly vehicleId: UUID;
+
+ @IsLiteralUnion(transferDestinationTypeAllowedValues)
+ readonly transferDestinationType: TransferDestination;
+
+ @IsUUID(4, uuidValidationOptions)
+ readonly transferDestinationId: UUID;
+
+ @IsUUIDSet()
+ public readonly patientsToBeLoaded: UUIDSet;
+
+ @IsInt()
+ @Min(0)
+ public readonly loadDelay: number;
+
+ @IsOptional()
+ @IsString()
+ public readonly key?: string;
+
+ @IsBoolean()
+ public readonly hasBeenStarted: boolean = false;
+
+ @IsInt()
+ @Min(0)
+ public readonly startTime: number = 0;
+
+ /**
+ * @deprecated Use {@link create} instead
+ */
+ constructor(
+ id: UUID,
+ vehicleId: UUID,
+ transferDestinationType: TransferDestination,
+ transferDestinationId: UUID,
+ patientsToBeLoaded: UUIDSet,
+ loadDelay: number,
+ key?: string
+ ) {
+ this.id = id;
+ this.vehicleId = vehicleId;
+ this.transferDestinationType = transferDestinationType;
+ this.transferDestinationId = transferDestinationId;
+ this.patientsToBeLoaded = patientsToBeLoaded;
+ this.loadDelay = loadDelay;
+ this.key = key;
+ }
+
+ static readonly create = getCreate(this);
+}
+
+export const loadVehicleActivity: SimulationActivity =
+ {
+ activityState: LoadVehicleActivityState,
+ tick(
+ draftState,
+ simulatedRegion,
+ activityState,
+ tickInterval,
+ terminate
+ ) {
+ const vehicle = getElement(
+ draftState,
+ 'vehicle',
+ activityState.vehicleId
+ );
+
+ // Start load process only once
+
+ if (!activityState.hasBeenStarted) {
+ // Send remove events
+
+ Object.keys(vehicle.personnelIds).forEach((personnelId) => {
+ const personnel = getElement(
+ draftState,
+ 'personnel',
+ personnelId
+ );
+ if (
+ isInSpecificSimulatedRegion(
+ personnel,
+ simulatedRegion.id
+ )
+ ) {
+ sendSimulationEvent(
+ simulatedRegion,
+ PersonnelRemovedEvent.create(personnelId)
+ );
+ }
+ });
+ Object.keys(vehicle.materialIds).forEach((materialId) => {
+ const material = getElement(
+ draftState,
+ 'material',
+ materialId
+ );
+ if (
+ isInSpecificSimulatedRegion(
+ material,
+ simulatedRegion.id
+ )
+ ) {
+ sendSimulationEvent(
+ simulatedRegion,
+ MaterialRemovedEvent.create(materialId)
+ );
+ }
+ });
+ Object.keys(activityState.patientsToBeLoaded).forEach(
+ (patientId) => {
+ const patient = getElement(
+ draftState,
+ 'patient',
+ patientId
+ );
+ if (
+ isInSpecificSimulatedRegion(
+ patient,
+ simulatedRegion.id
+ )
+ ) {
+ sendSimulationEvent(
+ simulatedRegion,
+ PatientRemovedEvent.create(patientId)
+ );
+ }
+ }
+ );
+
+ // Load material and personnel
+
+ completelyLoadVehicle(draftState, vehicle);
+
+ // Load patients (and unload patients not to be loaded)
+
+ Object.keys(vehicle.patientIds).forEach((patientId) => {
+ changePositionWithId(
+ patientId,
+ SimulatedRegionPosition.create(simulatedRegion.id),
+ 'patient',
+ draftState
+ );
+ // Inform the region that a new patient has left the vehicle
+ // (Only if it actually left the vehicle and will not be instantly re-added)
+ if (!activityState.patientsToBeLoaded[patientId]) {
+ sendSimulationEvent(
+ simulatedRegion,
+ NewPatientEvent.create(patientId)
+ );
+ }
+ });
+
+ vehicle.patientIds = cloneDeepMutable(
+ activityState.patientsToBeLoaded
+ );
+
+ Object.keys(vehicle.patientIds).forEach((patientId) => {
+ changePositionWithId(
+ patientId,
+ VehiclePosition.create(vehicle.id),
+ 'patient',
+ draftState
+ );
+ });
+
+ activityState.hasBeenStarted = true;
+ activityState.startTime = draftState.currentTime;
+ }
+
+ if (
+ activityState.startTime + activityState.loadDelay <=
+ draftState.currentTime
+ ) {
+ // terminate if the occupation has changed
+ if (
+ vehicle.occupation.type !== 'loadOccupation' ||
+ vehicle.occupation.loadingActivityId !== activityState.id
+ ) {
+ terminate();
+ return;
+ }
+ sendSimulationEvent(
+ simulatedRegion,
+ StartTransferEvent.create(
+ activityState.vehicleId,
+ activityState.transferDestinationType,
+ activityState.transferDestinationId,
+ activityState.key
+ )
+ );
+
+ vehicle.occupation = cloneDeepMutable(
+ IntermediateOccupation.create(
+ draftState.currentTime + tickInterval
+ )
+ );
+
+ terminate();
+ }
+ },
+ };
diff --git a/shared/src/simulation/activities/send-remote-event.ts b/shared/src/simulation/activities/send-remote-event.ts
new file mode 100644
index 000000000..b8816838d
--- /dev/null
+++ b/shared/src/simulation/activities/send-remote-event.ts
@@ -0,0 +1,62 @@
+import { Type } from 'class-transformer';
+import { IsUUID, ValidateNested } from 'class-validator';
+import { getCreate } from '../../models/utils';
+import { UUID, uuidValidationOptions } from '../../utils';
+import { IsValue } from '../../utils/validators';
+import { ExerciseSimulationEvent, simulationEventTypeOptions } from '../events';
+import { sendSimulationEvent } from '../events/utils';
+import type {
+ SimulationActivity,
+ SimulationActivityState,
+} from './simulation-activity';
+
+export class SendRemoteEventActivityState implements SimulationActivityState {
+ @IsValue('sendRemoteEventActivity' as const)
+ public readonly type = 'sendRemoteEventActivity';
+
+ @IsUUID(4, uuidValidationOptions)
+ public readonly id: UUID;
+
+ @IsUUID(4, uuidValidationOptions)
+ public readonly targetSimulatedRegionId: UUID;
+
+ @Type(...simulationEventTypeOptions)
+ @ValidateNested()
+ public readonly event: ExerciseSimulationEvent;
+
+ /**
+ * @deprecated Use {@link create} instead
+ */
+ constructor(
+ id: UUID,
+ targetSimulatedRegionId: UUID,
+ event: ExerciseSimulationEvent
+ ) {
+ this.id = id;
+ this.targetSimulatedRegionId = targetSimulatedRegionId;
+ this.event = event;
+ }
+
+ static readonly create = getCreate(this);
+}
+
+export const sendRemoteEventActivity: SimulationActivity =
+ {
+ activityState: SendRemoteEventActivityState,
+ tick(
+ draftState,
+ _simulatedRegion,
+ activityState,
+ _tickInterval,
+ terminate
+ ) {
+ const targetSimulatedRegion =
+ draftState.simulatedRegions[
+ activityState.targetSimulatedRegionId
+ ];
+ if (targetSimulatedRegion) {
+ sendSimulationEvent(targetSimulatedRegion, activityState.event);
+ }
+ terminate();
+ },
+ };
diff --git a/shared/src/simulation/activities/transfer-vehicle.ts b/shared/src/simulation/activities/transfer-vehicle.ts
new file mode 100644
index 000000000..aff9152ba
--- /dev/null
+++ b/shared/src/simulation/activities/transfer-vehicle.ts
@@ -0,0 +1,266 @@
+import { IsOptional, IsString, IsUUID } from 'class-validator';
+import {
+ MissingTransferConnectionRadiogram,
+ RadiogramUnpublishedStatus,
+} from '../../models/radiogram';
+import { publishRadiogram } from '../../models/radiogram/radiogram-helpers-mutable';
+import {
+ getCreate,
+ isInSpecificSimulatedRegion,
+ isInSpecificVehicle,
+ NoOccupation,
+ TransferStartPoint,
+} from '../../models/utils';
+import { VehicleResource } from '../../models/utils/rescue-resource';
+import { TransferActionReducers } from '../../store/action-reducers/transfer';
+import {
+ getElement,
+ getElementByPredicate,
+} from '../../store/action-reducers/utils';
+import { cloneDeepMutable, UUID, uuidValidationOptions } from '../../utils';
+import { IsLiteralUnion, IsValue } from '../../utils/validators';
+import {
+ TransferConnectionMissingEvent,
+ VehicleTransferSuccessfulEvent,
+} from '../events';
+import { sendSimulationEvent } from '../events/utils';
+import { nextUUID } from '../utils/randomness';
+import {
+ TransferDestination,
+ transferDestinationTypeAllowedValues,
+} from '../utils/transfer-destination';
+import { HospitalActionReducers } from '../../store/action-reducers/hospital';
+import type { ResourceDescription } from '../../models/utils/resource-description';
+import type {
+ SimulationActivity,
+ SimulationActivityState,
+} from './simulation-activity';
+
+export class TransferVehicleActivityState implements SimulationActivityState {
+ @IsValue('transferVehicleActivity' as const)
+ public readonly type = 'transferVehicleActivity';
+
+ @IsUUID(4, uuidValidationOptions)
+ readonly id: UUID;
+
+ @IsUUID(4, uuidValidationOptions)
+ readonly vehicleId: UUID;
+
+ @IsLiteralUnion(transferDestinationTypeAllowedValues)
+ readonly transferDestinationType: TransferDestination;
+
+ @IsUUID(4, uuidValidationOptions)
+ readonly transferDestinationId: UUID;
+
+ @IsOptional()
+ @IsString()
+ readonly key?: string;
+
+ /**
+ * @deprecated Use {@link create} instead
+ */
+ constructor(
+ id: UUID,
+ vehicleId: UUID,
+ transferDestinationType: TransferDestination,
+ transferDestinationId: UUID,
+ key?: string
+ ) {
+ this.id = id;
+ this.vehicleId = vehicleId;
+
+ this.transferDestinationType = transferDestinationType;
+ this.transferDestinationId = transferDestinationId;
+ this.key = key;
+ }
+
+ static readonly create = getCreate(this);
+}
+
+export const transferVehicleActivity: SimulationActivity =
+ {
+ activityState: TransferVehicleActivityState,
+ tick(
+ draftState,
+ simulatedRegion,
+ activityState,
+ _tickInterval,
+ terminate
+ ) {
+ const vehicle = getElement(
+ draftState,
+ 'vehicle',
+ activityState.vehicleId
+ );
+
+ if (activityState.transferDestinationType === 'transferPoint') {
+ const ownTransferPoint = getElementByPredicate(
+ draftState,
+ 'transferPoint',
+ (transferPoint) =>
+ isInSpecificSimulatedRegion(
+ transferPoint,
+ simulatedRegion.id
+ )
+ );
+
+ if (
+ ownTransferPoint.reachableTransferPoints[
+ activityState.transferDestinationId
+ ] === undefined
+ ) {
+ sendSimulationEvent(
+ simulatedRegion,
+ TransferConnectionMissingEvent.create(
+ nextUUID(draftState),
+ activityState.transferDestinationId,
+ activityState.key
+ )
+ );
+ publishRadiogram(
+ draftState,
+ cloneDeepMutable(
+ MissingTransferConnectionRadiogram.create(
+ nextUUID(draftState),
+ simulatedRegion.id,
+ RadiogramUnpublishedStatus.create(),
+ activityState.transferDestinationId
+ )
+ )
+ );
+
+ terminate();
+ return;
+ }
+
+ // If the vehicle is not completely loaded terminate
+ if (
+ Object.keys(vehicle.materialIds).some((materialId) => {
+ const material = getElement(
+ draftState,
+ 'material',
+ materialId
+ );
+ return !isInSpecificVehicle(material, vehicle.id);
+ }) ||
+ Object.keys(vehicle.personnelIds).some((personnelId) => {
+ const personnel = getElement(
+ draftState,
+ 'personnel',
+ personnelId
+ );
+ return !isInSpecificVehicle(personnel, vehicle.id);
+ })
+ ) {
+ terminate();
+ return;
+ }
+
+ // Do transfer and send event
+
+ vehicle.occupation = cloneDeepMutable(NoOccupation.create());
+
+ TransferActionReducers.addToTransfer.reducer(draftState, {
+ type: '[Transfer] Add to transfer',
+ elementType: 'vehicle',
+ elementId: activityState.vehicleId,
+ startPoint: TransferStartPoint.create(ownTransferPoint.id),
+ targetTransferPointId: activityState.transferDestinationId,
+ });
+
+ const vehicleResourceDescription: ResourceDescription = {};
+ vehicleResourceDescription[vehicle.vehicleType] = 1;
+
+ sendSimulationEvent(
+ simulatedRegion,
+ VehicleTransferSuccessfulEvent.create(
+ activityState.transferDestinationId,
+ activityState.key ?? '',
+ VehicleResource.create(vehicleResourceDescription)
+ )
+ );
+
+ terminate();
+ }
+ if (activityState.transferDestinationType === 'hospital') {
+ const ownTransferPoint = getElementByPredicate(
+ draftState,
+ 'transferPoint',
+ (transferPoint) =>
+ isInSpecificSimulatedRegion(
+ transferPoint,
+ simulatedRegion.id
+ )
+ );
+
+ if (
+ ownTransferPoint.reachableHospitals[
+ activityState.transferDestinationId
+ ] === undefined
+ ) {
+ sendSimulationEvent(
+ simulatedRegion,
+ TransferConnectionMissingEvent.create(
+ nextUUID(draftState),
+ activityState.transferDestinationId,
+ activityState.key
+ )
+ );
+ // TODO: Publish Radiogram (The current Radiogram does not support hospitals)
+
+ terminate();
+ return;
+ }
+
+ // If the vehicle is not completely loaded terminate
+ if (
+ Object.keys(vehicle.materialIds).some((materialId) => {
+ const material = getElement(
+ draftState,
+ 'material',
+ materialId
+ );
+ return !isInSpecificVehicle(material, vehicle.id);
+ }) ||
+ Object.keys(vehicle.personnelIds).some((personnelId) => {
+ const personnel = getElement(
+ draftState,
+ 'personnel',
+ personnelId
+ );
+ return !isInSpecificVehicle(personnel, vehicle.id);
+ })
+ ) {
+ terminate();
+ return;
+ }
+
+ // Do transfer and send event
+
+ vehicle.occupation = cloneDeepMutable(NoOccupation.create());
+
+ HospitalActionReducers.transportPatientToHospital.reducer(
+ draftState,
+ {
+ type: '[Hospital] Transport patient to hospital',
+ vehicleId: vehicle.id,
+ hospitalId: activityState.transferDestinationId,
+ }
+ );
+
+ const vehicleResourceDescription: ResourceDescription = {};
+ vehicleResourceDescription[vehicle.vehicleType] = 1;
+
+ sendSimulationEvent(
+ simulatedRegion,
+ VehicleTransferSuccessfulEvent.create(
+ activityState.transferDestinationId,
+ activityState.key ?? '',
+ VehicleResource.create(vehicleResourceDescription)
+ )
+ );
+
+ terminate();
+ }
+ },
+ };
diff --git a/shared/src/simulation/activities/transfer-vehicles.ts b/shared/src/simulation/activities/transfer-vehicles.ts
deleted file mode 100644
index f3f082035..000000000
--- a/shared/src/simulation/activities/transfer-vehicles.ts
+++ /dev/null
@@ -1,221 +0,0 @@
-import { Type } from 'class-transformer';
-import { IsString, IsUUID, ValidateNested } from 'class-validator';
-import { groupBy } from 'lodash-es';
-import {
- MissingTransferConnectionRadiogram,
- RadiogramUnpublishedStatus,
-} from '../../models/radiogram';
-import { publishRadiogram } from '../../models/radiogram/radiogram-helpers-mutable';
-import {
- currentSimulatedRegionIdOf,
- getCreate,
- isInSimulatedRegion,
- isInSpecificSimulatedRegion,
- TransferStartPoint,
-} from '../../models/utils';
-import { amountOfResourcesInVehicle } from '../../models/utils/amount-of-resources-in-vehicle';
-import { VehicleResource } from '../../models/utils/rescue-resource';
-import { TransferActionReducers } from '../../store/action-reducers/transfer';
-import {
- getElement,
- getElementByPredicate,
-} from '../../store/action-reducers/utils';
-import { completelyLoadVehicle } from '../../store/action-reducers/utils/completely-load-vehicle';
-import { cloneDeepMutable, UUID, uuidValidationOptions } from '../../utils';
-import { IsValue } from '../../utils/validators';
-import {
- ResourceRequiredEvent,
- TransferConnectionMissingEvent,
- VehiclesSentEvent,
- VehicleTransferSuccessfulEvent,
-} from '../events';
-import { sendSimulationEvent } from '../events/utils';
-import { nextUUID } from '../utils/randomness';
-import type {
- SimulationActivity,
- SimulationActivityState,
-} from './simulation-activity';
-
-export class TransferVehiclesActivityState implements SimulationActivityState {
- @IsValue('transferVehiclesActivity' as const)
- public readonly type = 'transferVehiclesActivity';
-
- @IsUUID(4, uuidValidationOptions)
- public readonly id: UUID;
-
- @IsUUID(4, uuidValidationOptions)
- public readonly targetTransferPointId: UUID;
-
- @IsString()
- public readonly key: string;
-
- @ValidateNested()
- @Type(() => VehicleResource)
- public readonly vehiclesToBeTransferred: VehicleResource;
-
- /**
- * @deprecated Use {@link create} instead
- */
- constructor(
- id: UUID,
- targetTransferPointId: UUID,
- key: string,
- vehiclesToBeTransferred: VehicleResource
- ) {
- this.id = id;
- this.targetTransferPointId = targetTransferPointId;
- this.key = key;
- this.vehiclesToBeTransferred = vehiclesToBeTransferred;
- }
-
- static readonly create = getCreate(this);
-}
-
-export const transferVehiclesActivity: SimulationActivity =
- {
- activityState: TransferVehiclesActivityState,
- tick(
- draftState,
- simulatedRegion,
- activityState,
- _tickInterval,
- terminate
- ) {
- const ownTransferPoint = getElementByPredicate(
- draftState,
- 'transferPoint',
- (transferPoint) =>
- isInSpecificSimulatedRegion(
- transferPoint,
- simulatedRegion.id
- )
- );
-
- if (
- ownTransferPoint.reachableTransferPoints[
- activityState.targetTransferPointId
- ] === undefined
- ) {
- sendSimulationEvent(
- simulatedRegion,
- TransferConnectionMissingEvent.create(
- nextUUID(draftState),
- activityState.targetTransferPointId
- )
- );
- publishRadiogram(
- draftState,
- cloneDeepMutable(
- MissingTransferConnectionRadiogram.create(
- nextUUID(draftState),
- simulatedRegion.id,
- RadiogramUnpublishedStatus.create(),
- activityState.targetTransferPointId
- )
- )
- );
- terminate();
- return;
- }
-
- const vehicles = Object.values(draftState.vehicles)
- .filter((vehicle) =>
- isInSpecificSimulatedRegion(vehicle, simulatedRegion.id)
- )
- .filter(
- (vehicle) => Object.keys(vehicle.patientIds).length === 0
- );
- const groupedVehicles = groupBy(
- vehicles,
- (vehicle) => vehicle.vehicleType
- );
-
- const missingVehicles: { [key: string]: number } = {};
- Object.entries(
- activityState.vehiclesToBeTransferred.vehicleCounts
- ).forEach(([vehicleType, vehicleCount]) => {
- if (
- (groupedVehicles[vehicleType]?.length ?? 0) < vehicleCount
- ) {
- missingVehicles[vehicleType] =
- vehicleCount -
- (groupedVehicles[vehicleType]?.length ?? 0);
- }
- });
- sendSimulationEvent(
- simulatedRegion,
- ResourceRequiredEvent.create(
- nextUUID(draftState),
- simulatedRegion.id,
- VehicleResource.create(missingVehicles),
- activityState.key
- )
- );
-
- const sentVehicles: { [key: string]: number } = {};
- Object.entries(
- activityState.vehiclesToBeTransferred.vehicleCounts
- ).forEach(([vehicleType, vehicleCount]) => {
- sentVehicles[vehicleType] = Math.min(
- groupedVehicles[vehicleType]?.length ?? 0,
- vehicleCount
- );
- // sort the vehicles by number of loaded resources descending
- groupedVehicles[vehicleType]?.sort(
- (a, b) =>
- amountOfResourcesInVehicle(draftState, b.id) -
- amountOfResourcesInVehicle(draftState, a.id)
- );
- for (let i = 0; i < sentVehicles[vehicleType]!; i++) {
- completelyLoadVehicle(
- draftState,
- groupedVehicles[vehicleType]![i]!
- );
-
- TransferActionReducers.addToTransfer.reducer(draftState, {
- type: '[Transfer] Add to transfer',
- elementType: 'vehicle',
- elementId: groupedVehicles[vehicleType]![i]!.id,
- startPoint: TransferStartPoint.create(
- ownTransferPoint.id
- ),
- targetTransferPointId:
- activityState.targetTransferPointId,
- });
- }
- });
-
- const targetTransferPoint = getElement(
- draftState,
- 'transferPoint',
- activityState.targetTransferPointId
- );
- if (Object.values(sentVehicles).some((value) => value !== 0)) {
- sendSimulationEvent(
- simulatedRegion,
- VehicleTransferSuccessfulEvent.create(
- nextUUID(draftState),
- targetTransferPoint.id,
- activityState.key,
- VehicleResource.create(sentVehicles)
- )
- );
-
- if (isInSimulatedRegion(targetTransferPoint)) {
- sendSimulationEvent(
- getElement(
- draftState,
- 'simulatedRegion',
- currentSimulatedRegionIdOf(targetTransferPoint)
- ),
- VehiclesSentEvent.create(
- nextUUID(draftState),
- VehicleResource.create(sentVehicles)
- )
- );
- }
- }
-
- terminate();
- },
- };
diff --git a/shared/src/simulation/behaviors/answer-requests.ts b/shared/src/simulation/behaviors/answer-requests.ts
index d863c3707..972144a2b 100644
--- a/shared/src/simulation/behaviors/answer-requests.ts
+++ b/shared/src/simulation/behaviors/answer-requests.ts
@@ -1,11 +1,23 @@
-import { IsUUID } from 'class-validator';
-import { getCreate, isInSpecificSimulatedRegion } from '../../models/utils';
+import { IsInt, IsUUID, Min, ValidateNested } from 'class-validator';
+import { Type } from 'class-transformer';
+import {
+ VehicleResource,
+ getCreate,
+ isInSpecificSimulatedRegion,
+} from '../../models/utils';
import { getElementByPredicate } from '../../store/action-reducers/utils';
-import { UUID, uuid, uuidValidationOptions } from '../../utils';
+import {
+ UUID,
+ cloneDeepMutable,
+ uuid,
+ uuidValidationOptions,
+} from '../../utils';
import { IsValue } from '../../utils/validators';
-import { TransferVehiclesActivityState } from '../activities';
import { addActivity } from '../activities/utils';
import { nextUUID } from '../utils/randomness';
+import { DelayEventActivityState } from '../activities';
+import { ResourceRequiredEvent, TransferVehiclesRequestEvent } from '../events';
+import type { ResourceDescription } from '../../models/utils/resource-description';
import type {
SimulationBehavior,
SimulationBehaviorState,
@@ -18,13 +30,22 @@ export class AnswerRequestsBehaviorState implements SimulationBehaviorState {
@IsUUID(4, uuidValidationOptions)
public readonly id: UUID = uuid();
+ @Type(() => TransferVehiclesRequestEvent)
+ @ValidateNested()
+ public readonly receivedEvents: readonly TransferVehiclesRequestEvent[] =
+ [];
+
+ @IsInt()
+ @Min(0)
+ public readonly requestsHandled: number = 0;
+
static readonly create = getCreate(this);
}
export const answerRequestsBehavior: SimulationBehavior =
{
behaviorState: AnswerRequestsBehaviorState,
- handleEvent: (draftState, simulatedRegion, _behaviorState, event) => {
+ handleEvent: (draftState, simulatedRegion, behaviorState, event) => {
switch (event.type) {
case 'resourceRequiredEvent': {
if (
@@ -41,19 +62,82 @@ export const answerRequestsBehavior: SimulationBehavior
+ receivedEvent.key === event.key
+ );
+ const requestEvent =
+ behaviorState.receivedEvents[requestEventIndex];
+ let createEvent = false;
+ const vehiclesNotAvailable: ResourceDescription = {};
+ if (requestEvent) {
+ Object.entries(
+ requestEvent.requestedVehicles
+ ).forEach(
+ ([vehicleType, requestedVehicleAmount]) => {
+ if (
+ (event.availableVehicles[vehicleType] ??
+ 0) < requestedVehicleAmount
+ ) {
+ vehiclesNotAvailable[vehicleType] =
+ requestedVehicleAmount -
+ (event.availableVehicles[
+ vehicleType
+ ] ?? 0);
+ createEvent = true;
+ }
+ }
+ );
+ behaviorState.receivedEvents.splice(
+ requestEventIndex,
+ 1
+ );
+ }
+ if (createEvent) {
+ addActivity(
+ simulatedRegion,
+ DelayEventActivityState.create(
+ nextUUID(draftState),
+ ResourceRequiredEvent.create(
+ '',
+ simulatedRegion.id,
+ VehicleResource.create(
+ vehiclesNotAvailable
+ ),
+ requestEvent!.key ?? ''
+ ),
+ draftState.currentTime
+ )
+ );
+ }
+ }
+ break;
default:
// Ignore event
}
diff --git a/shared/src/simulation/behaviors/automatically-distribute-vehicles.ts b/shared/src/simulation/behaviors/automatically-distribute-vehicles.ts
index f7f6311de..c83eac530 100644
--- a/shared/src/simulation/behaviors/automatically-distribute-vehicles.ts
+++ b/shared/src/simulation/behaviors/automatically-distribute-vehicles.ts
@@ -1,16 +1,18 @@
import { IsInt, IsOptional, IsUUID, Min } from 'class-validator';
-import { getCreate, VehicleResource } from '../../models/utils';
+import { cloneDeep } from 'lodash-es';
+import { getCreate } from '../../models/utils';
import type { Mutable } from '../../utils';
import { cloneDeepMutable, UUID, uuid, UUIDSet } from '../../utils';
import { IsUUIDSet, IsUUIDSetMap, IsValue } from '../../utils/validators';
import { IsResourceDescription } from '../../utils/validators/is-resource-description';
import {
+ DelayEventActivityState,
RecurringEventActivityState,
- TransferVehiclesActivityState,
} from '../activities';
import { addActivity } from '../activities/utils';
import { TryToDistributeEvent } from '../events/try-to-distribute';
import { nextUUID } from '../utils/randomness';
+import { TransferVehiclesRequestEvent } from '../events';
import type {
SimulationBehavior,
SimulationBehaviorState,
@@ -44,7 +46,7 @@ export class AutomaticallyDistributeVehiclesBehaviorState
@IsInt()
@Min(1)
/*
- * This *MUST* be greater that the tick Duration to Ensure that we can wait for a response in the next tick
+ * This *MUST* be greater than about 10 tick durations to Ensure that we can wait for a response
*/
public readonly distributionDelay: number = 60_000; // 1 minute
@@ -180,15 +182,15 @@ export const automaticallyDistributeVehiclesBehavior: SimulationBehavior {
- if (vehicleAmount === 0) {
- return;
- }
- if (
- !behaviorState.distributedLastRound[vehicleType]
- ) {
+ Object.entries(event.availableVehicles).forEach(
+ ([vehicleType, vehicleAmount]) => {
+ if (vehicleAmount === 0) {
+ return;
+ }
+ if (
+ !behaviorState.distributedLastRound[
+ vehicleType
+ ]
+ ) {
+ behaviorState.distributedLastRound[
+ vehicleType
+ ] = 0;
+ }
behaviorState.distributedLastRound[
vehicleType
- ] = 0;
- }
- behaviorState.distributedLastRound[vehicleType]++;
+ ]++;
- if (behaviorState.remainingInNeed[vehicleType]) {
- delete behaviorState.remainingInNeed[
- vehicleType
- ]![event.targetId];
+ if (
+ behaviorState.remainingInNeed[vehicleType]
+ ) {
+ delete behaviorState.remainingInNeed[
+ vehicleType
+ ]![event.transferDestinationId];
+ }
}
- });
+ );
}
break;
diff --git a/shared/src/simulation/behaviors/exercise-simulation-behavior.ts b/shared/src/simulation/behaviors/exercise-simulation-behavior.ts
index 3398a5996..f50b0ab43 100644
--- a/shared/src/simulation/behaviors/exercise-simulation-behavior.ts
+++ b/shared/src/simulation/behaviors/exercise-simulation-behavior.ts
@@ -8,6 +8,7 @@ import { automaticallyDistributeVehiclesBehavior } from './automatically-distrib
import { providePersonnelBehavior } from './provide-personnel';
import { answerRequestsBehavior } from './answer-requests';
import { requestBehavior } from './request';
+import { transferBehavior } from './transfer';
export const simulationBehaviors = {
automaticallyDistributeVehiclesBehavior,
@@ -18,6 +19,7 @@ export const simulationBehaviors = {
providePersonnelBehavior,
answerRequestsBehavior,
requestBehavior,
+ transferBehavior,
};
export type ExerciseSimulationBehavior =
diff --git a/shared/src/simulation/behaviors/index.ts b/shared/src/simulation/behaviors/index.ts
index b23d5b2d9..bda7a4981 100644
--- a/shared/src/simulation/behaviors/index.ts
+++ b/shared/src/simulation/behaviors/index.ts
@@ -8,3 +8,4 @@ export * from './report';
export * from './provide-personnel';
export * from './utils';
export * from './request';
+export * from './transfer';
diff --git a/shared/src/simulation/behaviors/transfer.ts b/shared/src/simulation/behaviors/transfer.ts
new file mode 100644
index 000000000..3a057b05d
--- /dev/null
+++ b/shared/src/simulation/behaviors/transfer.ts
@@ -0,0 +1,421 @@
+import {
+ IsInt,
+ IsOptional,
+ IsUUID,
+ Min,
+ ValidateNested,
+} from 'class-validator';
+import { groupBy } from 'lodash-es';
+import { Type } from 'class-transformer';
+import {
+ VehicleResource,
+ currentSimulatedRegionOf,
+ getCreate,
+ isInSimulatedRegion,
+ isInSpecificSimulatedRegion,
+} from '../../models/utils';
+import {
+ UUID,
+ cloneDeepMutable,
+ uuid,
+ uuidValidationOptions,
+} from '../../utils';
+import { IsValue } from '../../utils/validators';
+import { addActivity, terminateActivity } from '../activities/utils';
+import { nextUUID } from '../utils/randomness';
+import { getElement } from '../../store/action-reducers/utils';
+import {
+ DelayEventActivityState,
+ LoadVehicleActivityState,
+ RecurringEventActivityState,
+ SendRemoteEventActivityState,
+} from '../activities';
+import { isUnoccupied } from '../../models/utils/occupations/occupation-helpers-mutable';
+import { amountOfResourcesInVehicle } from '../../models/utils/amount-of-resources-in-vehicle';
+import type { ResourceDescription } from '../../models/utils/resource-description';
+import {
+ DoTransferEvent,
+ RequestReceivedEvent,
+ StartTransferEvent,
+ VehiclesSentEvent,
+} from '../events';
+import { LoadOccupation } from '../../models/utils/occupations/load-occupation';
+import { WaitForTransferOccupation } from '../../models/utils/occupations/wait-for-transfer-occupation';
+import { TransferVehicleActivityState } from '../activities/transfer-vehicle';
+import type {
+ SimulationBehavior,
+ SimulationBehaviorState,
+} from './simulation-behavior';
+
+export class TransferBehaviorState implements SimulationBehaviorState {
+ @IsValue('transferBehavior' as const)
+ readonly type = 'transferBehavior';
+
+ @IsUUID(4, uuidValidationOptions)
+ public readonly id: UUID = uuid();
+
+ @IsInt()
+ @Min(0)
+ public readonly loadTimePerPatient: number;
+
+ @IsInt()
+ @Min(0)
+ public readonly personnelLoadTime: number;
+
+ @IsInt()
+ @Min(0)
+ public readonly delayBetweenSends: number;
+
+ @Type(() => StartTransferEvent)
+ @ValidateNested()
+ public readonly startTransferEventQueue: readonly StartTransferEvent[] = [];
+
+ @IsUUID(4, uuidValidationOptions)
+ @IsOptional()
+ public readonly recurringActivityId: UUID | undefined;
+
+ /**
+ * @deprecated Use {@link create} instead
+ */
+ constructor(
+ loadTimePerPatient: number = 60_000, // 1 minute
+ personnelLoadTime: number = 120_000, // 2 minutes
+ delayBetweenSends: number = 60_000 // 1 minute
+ ) {
+ this.loadTimePerPatient = loadTimePerPatient;
+ this.personnelLoadTime = personnelLoadTime;
+ this.delayBetweenSends = delayBetweenSends;
+ }
+
+ static readonly create = getCreate(this);
+}
+
+export const transferBehavior: SimulationBehavior = {
+ behaviorState: TransferBehaviorState,
+ handleEvent(draftState, simulatedRegion, behaviorState, event) {
+ switch (event.type) {
+ case 'transferPatientsRequestEvent':
+ {
+ // find a vehicle to use
+
+ const vehicles = Object.values(draftState.vehicles)
+ .filter((vehicle) =>
+ isInSpecificSimulatedRegion(
+ vehicle,
+ simulatedRegion.id
+ )
+ )
+ .filter(
+ (vehicle) =>
+ Object.keys(vehicle.patientIds).length === 0
+ );
+ const vehiclesOfCorrectType = vehicles.filter(
+ (vehicle) => vehicle.type === event.vehicleType
+ );
+
+ // sort the unoccupied vehicles by number of loaded resources descending and use the one with the most
+
+ const vehicleToLoad = vehiclesOfCorrectType
+ .filter((vehicle) =>
+ isUnoccupied(vehicle, draftState.currentTime)
+ )
+ .sort(
+ (vehicle1, vehicle2) =>
+ amountOfResourcesInVehicle(
+ draftState,
+ vehicle2.id
+ ) -
+ amountOfResourcesInVehicle(
+ draftState,
+ vehicle1.id
+ )
+ )[0];
+
+ if (vehicleToLoad) {
+ const activityId = nextUUID(draftState);
+ addActivity(
+ simulatedRegion,
+ LoadVehicleActivityState.create(
+ activityId,
+ vehicleToLoad.id,
+ event.transferDestinationType,
+ event.transferDestinationId,
+ event.patientIds,
+ Math.max(
+ Object.keys(event.patientIds).length *
+ behaviorState.loadTimePerPatient,
+ behaviorState.personnelLoadTime
+ )
+ )
+ );
+ vehicleToLoad.occupation = cloneDeepMutable(
+ LoadOccupation.create(activityId)
+ );
+ }
+ }
+ break;
+ case 'transferPatientsInSpecificVehicleRequestEvent':
+ {
+ const vehicle = getElement(
+ draftState,
+ 'vehicle',
+ event.vehicleId
+ );
+ // Don't do anything if vehicle is occupied
+ if (!isUnoccupied(vehicle, draftState.currentTime)) {
+ return;
+ }
+
+ const activityId = nextUUID(draftState);
+ addActivity(
+ simulatedRegion,
+ LoadVehicleActivityState.create(
+ activityId,
+ vehicle.id,
+ event.transferDestinationType,
+ event.transferDestinationId,
+ event.patientIds,
+ Math.max(
+ Object.keys(event.patientIds).length *
+ behaviorState.loadTimePerPatient,
+ behaviorState.personnelLoadTime
+ )
+ )
+ );
+ vehicle.occupation = cloneDeepMutable(
+ LoadOccupation.create(activityId)
+ );
+ }
+ break;
+ case 'transferSpecificVehicleRequestEvent':
+ {
+ const vehicle = getElement(
+ draftState,
+ 'vehicle',
+ event.vehicleId
+ );
+ // Don't do anything if vehicle is occupied
+ if (!isUnoccupied(vehicle, draftState.currentTime)) {
+ return;
+ }
+
+ const activityId = nextUUID(draftState);
+ addActivity(
+ simulatedRegion,
+ LoadVehicleActivityState.create(
+ activityId,
+ vehicle.id,
+ event.transferDestinationType,
+ event.transferDestinationId,
+ {},
+ behaviorState.personnelLoadTime
+ )
+ );
+ vehicle.occupation = cloneDeepMutable(
+ LoadOccupation.create(activityId)
+ );
+ }
+ break;
+ case 'transferVehiclesRequestEvent':
+ {
+ // group vehicles
+
+ const vehicles = Object.values(draftState.vehicles)
+ .filter((vehicle) =>
+ isInSpecificSimulatedRegion(
+ vehicle,
+ simulatedRegion.id
+ )
+ )
+ .filter(
+ (vehicle) =>
+ Object.keys(vehicle.patientIds).length === 0
+ );
+ const groupedVehicles = groupBy(
+ vehicles,
+ (vehicle) => vehicle.vehicleType
+ );
+
+ const sentVehicles: ResourceDescription = {};
+
+ Object.entries(event.requestedVehicles).forEach(
+ ([vehicleType, vehicleAmount]) => {
+ // sort the unoccupied vehicles by number of loaded resources descending and use the one with the most
+
+ const loadableVehicles = groupedVehicles[
+ vehicleType
+ ]
+ ?.filter((vehicle) =>
+ isUnoccupied(
+ vehicle,
+ draftState.currentTime
+ )
+ )
+ .sort(
+ (vehicle1, vehicle2) =>
+ amountOfResourcesInVehicle(
+ draftState,
+ vehicle2.id
+ ) -
+ amountOfResourcesInVehicle(
+ draftState,
+ vehicle1.id
+ )
+ );
+
+ sentVehicles[vehicleType] = 0;
+
+ for (
+ let index = 0;
+ index <
+ Math.min(
+ loadableVehicles?.length ?? 0,
+ vehicleAmount
+ );
+ index++
+ ) {
+ const activityId = nextUUID(draftState);
+ addActivity(
+ simulatedRegion,
+ LoadVehicleActivityState.create(
+ activityId,
+ loadableVehicles![index]!.id,
+ event.transferDestinationType,
+ event.transferDestinationId,
+ {},
+ behaviorState.personnelLoadTime
+ )
+ );
+ loadableVehicles![index]!.occupation =
+ cloneDeepMutable(
+ LoadOccupation.create(activityId)
+ );
+ sentVehicles[vehicleType]++;
+ }
+ }
+ );
+
+ // Send RequestReceivedEvent into own region
+
+ addActivity(
+ simulatedRegion,
+ DelayEventActivityState.create(
+ nextUUID(draftState),
+ RequestReceivedEvent.create(
+ sentVehicles,
+ event.transferDestinationType,
+ event.transferDestinationId,
+ event.key
+ ),
+ draftState.currentTime
+ )
+ );
+
+ // Send event to destination if it is a simulated region
+
+ if (
+ event.transferDestinationType === 'transferPoint' &&
+ isInSimulatedRegion(
+ getElement(
+ draftState,
+ 'transferPoint',
+ event.transferDestinationId
+ )
+ )
+ ) {
+ addActivity(
+ simulatedRegion,
+ SendRemoteEventActivityState.create(
+ nextUUID(draftState),
+ currentSimulatedRegionOf(
+ draftState,
+ getElement(
+ draftState,
+ 'transferPoint',
+ event.transferDestinationId
+ )
+ ).id,
+ VehiclesSentEvent.create(
+ '',
+ VehicleResource.create(sentVehicles)
+ )
+ )
+ );
+ }
+ }
+ break;
+ case 'startTransferEvent':
+ {
+ const vehicle = getElement(
+ draftState,
+ 'vehicle',
+ event.vehicleId
+ );
+ vehicle.occupation = cloneDeepMutable(
+ WaitForTransferOccupation.create()
+ );
+
+ behaviorState.startTransferEventQueue.push(event);
+ }
+ break;
+ case 'tickEvent':
+ {
+ if (
+ behaviorState.recurringActivityId === undefined &&
+ behaviorState.startTransferEventQueue.length !== 0
+ ) {
+ behaviorState.recurringActivityId =
+ nextUUID(draftState);
+ addActivity(
+ simulatedRegion,
+ RecurringEventActivityState.create(
+ behaviorState.recurringActivityId,
+ DoTransferEvent.create(),
+ draftState.currentTime,
+ behaviorState.delayBetweenSends
+ )
+ );
+ }
+ }
+ break;
+ case 'doTransferEvent':
+ {
+ if (
+ behaviorState.startTransferEventQueue.length === 0 &&
+ behaviorState.recurringActivityId
+ ) {
+ terminateActivity(
+ draftState,
+ simulatedRegion,
+ behaviorState.recurringActivityId
+ );
+ behaviorState.recurringActivityId = undefined;
+ return;
+ }
+
+ const transferEvent =
+ behaviorState.startTransferEventQueue.shift();
+ const vehicle =
+ draftState.vehicles[transferEvent!.vehicleId];
+ if (
+ vehicle?.occupation.type !== 'waitForTransferOccupation'
+ ) {
+ return;
+ }
+ addActivity(
+ simulatedRegion,
+ TransferVehicleActivityState.create(
+ nextUUID(draftState),
+ vehicle.id,
+ transferEvent!.transferDestinationType,
+ transferEvent!.transferDestinationId,
+ transferEvent!.key
+ )
+ );
+ }
+ break;
+ default:
+ // ignore event
+ }
+ },
+};
diff --git a/shared/src/simulation/events/do-transfer.ts b/shared/src/simulation/events/do-transfer.ts
new file mode 100644
index 000000000..90c697811
--- /dev/null
+++ b/shared/src/simulation/events/do-transfer.ts
@@ -0,0 +1,10 @@
+import { getCreate } from '../../models/utils';
+import { IsValue } from '../../utils/validators';
+import type { SimulationEvent } from './simulation-event';
+
+export class DoTransferEvent implements SimulationEvent {
+ @IsValue('doTransferEvent')
+ readonly type = 'doTransferEvent';
+
+ static readonly create = getCreate(this);
+}
diff --git a/shared/src/simulation/events/exercise-simulation-event.ts b/shared/src/simulation/events/exercise-simulation-event.ts
index 8962be043..9ff18a8a5 100644
--- a/shared/src/simulation/events/exercise-simulation-event.ts
+++ b/shared/src/simulation/events/exercise-simulation-event.ts
@@ -21,6 +21,13 @@ import { MaterialRemovedEvent } from './material-removed';
import { PersonnelRemovedEvent } from './personnel-removed';
import { PatientRemovedEvent } from './patient-removed';
import { VehicleRemovedEvent } from './vehicle-removed';
+import { TransferPatientsInSpecificVehicleRequestEvent } from './transfer-patients-in-specific-vehicle-request';
+import { TransferSpecificVehicleRequestEvent } from './transfer-specific-vehicle-request';
+import { TransferVehiclesRequestEvent } from './transfer-vehicles-request';
+import { TransferPatientsRequestEvent } from './transfer-patients-request';
+import { RequestReceivedEvent } from './request-received';
+import { StartTransferEvent } from './start-transfer';
+import { DoTransferEvent } from './do-transfer';
export const simulationEvents = {
MaterialAvailableEvent,
@@ -43,6 +50,13 @@ export const simulationEvents = {
PersonnelRemovedEvent,
PatientRemovedEvent,
VehicleRemovedEvent,
+ TransferPatientsInSpecificVehicleRequestEvent,
+ TransferSpecificVehicleRequestEvent,
+ TransferVehiclesRequestEvent,
+ TransferPatientsRequestEvent,
+ RequestReceivedEvent,
+ StartTransferEvent,
+ DoTransferEvent,
};
export type ExerciseSimulationEvent = InstanceType<
@@ -75,6 +89,14 @@ export const simulationEventDictionary: ExerciseSimulationEventDictionary = {
personnelRemovedEvent: PersonnelRemovedEvent,
patientRemovedEvent: PatientRemovedEvent,
vehicleRemovedEvent: VehicleRemovedEvent,
+ transferPatientsInSpecificVehicleRequestEvent:
+ TransferPatientsInSpecificVehicleRequestEvent,
+ transferSpecificVehicleRequestEvent: TransferSpecificVehicleRequestEvent,
+ transferVehiclesRequestEvent: TransferVehiclesRequestEvent,
+ transferPatientsRequestEvent: TransferPatientsRequestEvent,
+ requestReceivedEvent: RequestReceivedEvent,
+ startTransferEvent: StartTransferEvent,
+ doTransferEvent: DoTransferEvent,
};
export const simulationEventTypeOptions: Parameters = [
diff --git a/shared/src/simulation/events/index.ts b/shared/src/simulation/events/index.ts
index 53848334f..a70406f25 100644
--- a/shared/src/simulation/events/index.ts
+++ b/shared/src/simulation/events/index.ts
@@ -16,3 +16,10 @@ export * from './material-removed';
export * from './personnel-removed';
export * from './patient-removed';
export * from './vehicle-removed';
+export * from './request-received';
+export * from './start-transfer';
+export * from './transfer-patients-in-specific-vehicle-request';
+export * from './transfer-patients-request';
+export * from './transfer-specific-vehicle-request';
+export * from './transfer-vehicles-request';
+export * from './do-transfer';
diff --git a/shared/src/simulation/events/request-received.ts b/shared/src/simulation/events/request-received.ts
new file mode 100644
index 000000000..6dec7ce36
--- /dev/null
+++ b/shared/src/simulation/events/request-received.ts
@@ -0,0 +1,46 @@
+import { IsOptional, IsString, IsUUID } from 'class-validator';
+import { getCreate } from '../../models/utils';
+import { IsLiteralUnion, IsValue } from '../../utils/validators';
+import { ResourceDescription } from '../../models/utils/resource-description';
+import { IsResourceDescription } from '../../utils/validators/is-resource-description';
+import {
+ TransferDestination,
+ transferDestinationTypeAllowedValues,
+} from '../utils/transfer-destination';
+import { UUID, uuidValidationOptions } from '../../utils';
+import type { SimulationEvent } from './simulation-event';
+
+export class RequestReceivedEvent implements SimulationEvent {
+ @IsValue('requestReceivedEvent')
+ readonly type = 'requestReceivedEvent';
+
+ @IsResourceDescription()
+ readonly availableVehicles: ResourceDescription;
+
+ @IsLiteralUnion(transferDestinationTypeAllowedValues)
+ readonly transferDestinationType: TransferDestination;
+
+ @IsUUID(4, uuidValidationOptions)
+ readonly transferDestinationId: UUID;
+
+ @IsString()
+ @IsOptional()
+ readonly key?: string;
+
+ /**
+ * @deprecated Use {@link create} instead
+ */
+ constructor(
+ availableVehicles: ResourceDescription,
+ transferDestinationType: TransferDestination,
+ transferDestinationId: UUID,
+ key?: string
+ ) {
+ this.availableVehicles = availableVehicles;
+ this.transferDestinationType = transferDestinationType;
+ this.transferDestinationId = transferDestinationId;
+ this.key = key;
+ }
+
+ static readonly create = getCreate(this);
+}
diff --git a/shared/src/simulation/events/start-transfer.ts b/shared/src/simulation/events/start-transfer.ts
new file mode 100644
index 000000000..66ca50f05
--- /dev/null
+++ b/shared/src/simulation/events/start-transfer.ts
@@ -0,0 +1,44 @@
+import { IsOptional, IsString, IsUUID } from 'class-validator';
+import { getCreate } from '../../models/utils';
+import { UUID, uuidValidationOptions } from '../../utils';
+import { IsLiteralUnion, IsValue } from '../../utils/validators';
+import {
+ TransferDestination,
+ transferDestinationTypeAllowedValues,
+} from '../utils/transfer-destination';
+import type { SimulationEvent } from './simulation-event';
+
+export class StartTransferEvent implements SimulationEvent {
+ @IsValue('startTransferEvent')
+ readonly type = 'startTransferEvent';
+
+ @IsUUID(4, uuidValidationOptions)
+ readonly vehicleId!: UUID;
+
+ @IsLiteralUnion(transferDestinationTypeAllowedValues)
+ readonly transferDestinationType: TransferDestination;
+
+ @IsUUID(4, uuidValidationOptions)
+ readonly transferDestinationId: UUID;
+
+ @IsOptional()
+ @IsString()
+ readonly key?: string;
+
+ /**
+ * @deprecated Use {@link create} instead
+ */
+ constructor(
+ vehicleId: UUID,
+ transferDestinationType: TransferDestination,
+ transferDestinationId: UUID,
+ key?: string
+ ) {
+ this.vehicleId = vehicleId;
+ this.transferDestinationType = transferDestinationType;
+ this.transferDestinationId = transferDestinationId;
+ this.key = key;
+ }
+
+ static readonly create = getCreate(this);
+}
diff --git a/shared/src/simulation/events/transfer-connection-missing.ts b/shared/src/simulation/events/transfer-connection-missing.ts
index d923bb14f..b71d33eb4 100644
--- a/shared/src/simulation/events/transfer-connection-missing.ts
+++ b/shared/src/simulation/events/transfer-connection-missing.ts
@@ -1,4 +1,4 @@
-import { IsUUID } from 'class-validator';
+import { IsOptional, IsString, IsUUID } from 'class-validator';
import { getCreate } from '../../models/utils';
import { UUID, uuidValidationOptions } from '../../utils';
import { IsValue } from '../../utils/validators';
@@ -14,12 +14,17 @@ export class TransferConnectionMissingEvent implements SimulationEvent {
@IsUUID(4, uuidValidationOptions)
public readonly transferPointId: UUID;
+ @IsOptional()
+ @IsString()
+ public readonly key?: string;
+
/**
* @deprecated Use {@link create} instead
*/
- constructor(id: UUID, transferPointId: UUID) {
+ constructor(id: UUID, transferPointId: UUID, key?: string) {
this.id = id;
this.transferPointId = transferPointId;
+ this.key = key;
}
static readonly create = getCreate(this);
diff --git a/shared/src/simulation/events/transfer-patients-in-specific-vehicle-request.ts b/shared/src/simulation/events/transfer-patients-in-specific-vehicle-request.ts
new file mode 100644
index 000000000..6c935050a
--- /dev/null
+++ b/shared/src/simulation/events/transfer-patients-in-specific-vehicle-request.ts
@@ -0,0 +1,45 @@
+import { IsUUID } from 'class-validator';
+import { getCreate } from '../../models/utils';
+import { UUID, UUIDSet, uuidValidationOptions } from '../../utils';
+import { IsLiteralUnion, IsUUIDSet, IsValue } from '../../utils/validators';
+import {
+ TransferDestination,
+ transferDestinationTypeAllowedValues,
+} from '../utils/transfer-destination';
+import type { SimulationEvent } from './simulation-event';
+
+export class TransferPatientsInSpecificVehicleRequestEvent
+ implements SimulationEvent
+{
+ @IsValue('transferPatientsInSpecificVehicleRequestEvent')
+ readonly type = 'transferPatientsInSpecificVehicleRequestEvent';
+
+ @IsUUIDSet()
+ readonly patientIds: UUIDSet;
+
+ @IsUUID(4, uuidValidationOptions)
+ readonly vehicleId: UUID;
+
+ @IsLiteralUnion(transferDestinationTypeAllowedValues)
+ readonly transferDestinationType: TransferDestination;
+
+ @IsUUID(4, uuidValidationOptions)
+ readonly transferDestinationId: UUID;
+
+ /**
+ * @deprecated Use {@link create} instead
+ */
+ constructor(
+ patientIds: UUIDSet,
+ vehicleId: UUID,
+ transferDestinationType: TransferDestination,
+ transferDestinationId: UUID
+ ) {
+ this.patientIds = patientIds;
+ this.vehicleId = vehicleId;
+ this.transferDestinationType = transferDestinationType;
+ this.transferDestinationId = transferDestinationId;
+ }
+
+ static readonly create = getCreate(this);
+}
diff --git a/shared/src/simulation/events/transfer-patients-request.ts b/shared/src/simulation/events/transfer-patients-request.ts
new file mode 100644
index 000000000..2bc436bf3
--- /dev/null
+++ b/shared/src/simulation/events/transfer-patients-request.ts
@@ -0,0 +1,43 @@
+import { IsString, IsUUID } from 'class-validator';
+import { getCreate } from '../../models/utils';
+import { IsLiteralUnion, IsUUIDSet, IsValue } from '../../utils/validators';
+import { UUID, UUIDSet, uuidValidationOptions } from '../../utils';
+import {
+ TransferDestination,
+ transferDestinationTypeAllowedValues,
+} from '../utils/transfer-destination';
+import type { SimulationEvent } from './simulation-event';
+
+export class TransferPatientsRequestEvent implements SimulationEvent {
+ @IsValue('transferPatientsRequestEvent')
+ readonly type = 'transferPatientsRequestEvent';
+
+ @IsString()
+ readonly vehicleType: string;
+
+ @IsUUIDSet()
+ readonly patientIds: UUIDSet;
+
+ @IsLiteralUnion(transferDestinationTypeAllowedValues)
+ readonly transferDestinationType: TransferDestination;
+
+ @IsUUID(4, uuidValidationOptions)
+ readonly transferDestinationId: UUID;
+
+ /**
+ * @deprecated Use {@link create} instead
+ */
+ constructor(
+ vehicleType: string,
+ patientIds: UUIDSet,
+ transferDestinationType: TransferDestination,
+ transferDestinationId: UUID
+ ) {
+ this.vehicleType = vehicleType;
+ this.patientIds = patientIds;
+ this.transferDestinationType = transferDestinationType;
+ this.transferDestinationId = transferDestinationId;
+ }
+
+ static readonly create = getCreate(this);
+}
diff --git a/shared/src/simulation/events/transfer-specific-vehicle-request.ts b/shared/src/simulation/events/transfer-specific-vehicle-request.ts
new file mode 100644
index 000000000..5571fa196
--- /dev/null
+++ b/shared/src/simulation/events/transfer-specific-vehicle-request.ts
@@ -0,0 +1,38 @@
+import { IsUUID } from 'class-validator';
+import { getCreate } from '../../models/utils';
+import { UUID, uuidValidationOptions } from '../../utils';
+import { IsLiteralUnion, IsValue } from '../../utils/validators';
+import {
+ TransferDestination,
+ transferDestinationTypeAllowedValues,
+} from '../utils/transfer-destination';
+import type { SimulationEvent } from './simulation-event';
+
+export class TransferSpecificVehicleRequestEvent implements SimulationEvent {
+ @IsValue('transferSpecificVehicleRequestEvent')
+ readonly type = 'transferSpecificVehicleRequestEvent';
+
+ @IsUUID(4, uuidValidationOptions)
+ readonly vehicleId: UUID;
+
+ @IsLiteralUnion(transferDestinationTypeAllowedValues)
+ readonly transferDestinationType: TransferDestination;
+
+ @IsUUID(4, uuidValidationOptions)
+ readonly transferDestinationId: UUID;
+
+ /**
+ * @deprecated Use {@link create} instead
+ */
+ constructor(
+ vehicleId: UUID,
+ transferDestinationType: TransferDestination,
+ transferDestinationId: UUID
+ ) {
+ this.vehicleId = vehicleId;
+ this.transferDestinationType = transferDestinationType;
+ this.transferDestinationId = transferDestinationId;
+ }
+
+ static readonly create = getCreate(this);
+}
diff --git a/shared/src/simulation/events/transfer-vehicles-request.ts b/shared/src/simulation/events/transfer-vehicles-request.ts
new file mode 100644
index 000000000..e355cfd6f
--- /dev/null
+++ b/shared/src/simulation/events/transfer-vehicles-request.ts
@@ -0,0 +1,46 @@
+import { IsOptional, IsString, IsUUID } from 'class-validator';
+import { getCreate } from '../../models/utils';
+import { IsLiteralUnion, IsValue } from '../../utils/validators';
+import { ResourceDescription } from '../../models/utils/resource-description';
+import { IsResourceDescription } from '../../utils/validators/is-resource-description';
+import { UUID, uuidValidationOptions } from '../../utils';
+import {
+ TransferDestination,
+ transferDestinationTypeAllowedValues,
+} from '../utils/transfer-destination';
+import type { SimulationEvent } from './simulation-event';
+
+export class TransferVehiclesRequestEvent implements SimulationEvent {
+ @IsValue('transferVehiclesRequestEvent')
+ readonly type = 'transferVehiclesRequestEvent';
+
+ @IsResourceDescription()
+ readonly requestedVehicles: ResourceDescription;
+
+ @IsLiteralUnion(transferDestinationTypeAllowedValues)
+ readonly transferDestinationType: TransferDestination;
+
+ @IsUUID(4, uuidValidationOptions)
+ readonly transferDestinationId: UUID;
+
+ @IsOptional()
+ @IsString()
+ readonly key?: string;
+
+ /**
+ * @deprecated Use {@link create} instead
+ */
+ constructor(
+ requestedVehicles: ResourceDescription,
+ transferDestinationType: TransferDestination,
+ transferDestinationId: UUID,
+ key?: string
+ ) {
+ this.requestedVehicles = requestedVehicles;
+ this.transferDestinationType = transferDestinationType;
+ this.transferDestinationId = transferDestinationId;
+ this.key = key;
+ }
+
+ static readonly create = getCreate(this);
+}
diff --git a/shared/src/simulation/events/vehicle-transfer-successful.ts b/shared/src/simulation/events/vehicle-transfer-successful.ts
index b7532ff3e..bd43dcbc4 100644
--- a/shared/src/simulation/events/vehicle-transfer-successful.ts
+++ b/shared/src/simulation/events/vehicle-transfer-successful.ts
@@ -9,9 +9,6 @@ export class VehicleTransferSuccessfulEvent implements SimulationEvent {
@IsValue('vehicleTransferSuccessfulEvent')
readonly type = 'vehicleTransferSuccessfulEvent';
- @IsUUID(4, uuidValidationOptions)
- public readonly id: UUID;
-
@IsUUID(4, uuidValidationOptions)
public readonly targetId: UUID;
@@ -25,13 +22,7 @@ export class VehicleTransferSuccessfulEvent implements SimulationEvent {
/**
* @deprecated Use {@link create} instead
*/
- constructor(
- id: UUID,
- targetId: UUID,
- key: string,
- vehiclesSent: VehicleResource
- ) {
- this.id = id;
+ constructor(targetId: UUID, key: string, vehiclesSent: VehicleResource) {
this.targetId = targetId;
this.key = key;
this.vehiclesSent = vehiclesSent;
diff --git a/shared/src/simulation/events/vehicles-sent.ts b/shared/src/simulation/events/vehicles-sent.ts
index 9c28f028c..91719142b 100644
--- a/shared/src/simulation/events/vehicles-sent.ts
+++ b/shared/src/simulation/events/vehicles-sent.ts
@@ -1,8 +1,8 @@
import { Type } from 'class-transformer';
-import { IsUUID, ValidateNested } from 'class-validator';
+import { IsString, ValidateNested } from 'class-validator';
import { getCreate } from '../../models/utils';
import { VehicleResource } from '../../models/utils/rescue-resource';
-import { UUID, uuidValidationOptions } from '../../utils';
+import { UUID } from '../../utils';
import { IsValue } from '../../utils/validators';
import type { SimulationEvent } from './simulation-event';
@@ -10,7 +10,8 @@ export class VehiclesSentEvent implements SimulationEvent {
@IsValue('vehiclesSentEvent')
readonly type = 'vehiclesSentEvent';
- @IsUUID(4, uuidValidationOptions)
+ // I want to use '' here because this id should not exist and is never used but if we insert `IsUUID()` this is forbidden
+ @IsString()
public readonly id: UUID;
@Type(() => VehicleResource)
diff --git a/shared/src/simulation/utils/transfer-destination.ts b/shared/src/simulation/utils/transfer-destination.ts
new file mode 100644
index 000000000..45e7eaa2d
--- /dev/null
+++ b/shared/src/simulation/utils/transfer-destination.ts
@@ -0,0 +1,9 @@
+import type { AllowedValues } from '../../utils/validators';
+
+export type TransferDestination = 'hospital' | 'transferPoint';
+
+export const transferDestinationTypeAllowedValues: AllowedValues =
+ {
+ hospital: true,
+ transferPoint: true,
+ };
diff --git a/shared/src/state-migrations/29-remove-transfer-vehicles-activity-and-change-answer-request-behavior.ts b/shared/src/state-migrations/29-remove-transfer-vehicles-activity-and-change-answer-request-behavior.ts
new file mode 100644
index 000000000..a0f2d32b9
--- /dev/null
+++ b/shared/src/state-migrations/29-remove-transfer-vehicles-activity-and-change-answer-request-behavior.ts
@@ -0,0 +1,79 @@
+import type { Migration } from './migration-functions';
+
+export const removeTransferVehiclesActivityAndChangeAnswerRequestBehavior29: Migration =
+ {
+ action: (_intermediaryState, action) => {
+ if (
+ (action as { type: string }).type ===
+ '[SimulatedRegion] Add Behavior'
+ ) {
+ const typedAction = action as {
+ behaviorState: BehaviorStateStub;
+ };
+ if (
+ typedAction.behaviorState.type === 'answerRequestsBehavior'
+ ) {
+ typedAction.behaviorState.receivedEvents = [];
+ typedAction.behaviorState.requestsHandled = 0;
+ }
+ } else if (
+ (action as { type: string }).type ===
+ '[SimulatedRegion] Add simulated region'
+ ) {
+ const typedAction = action as {
+ simulatedRegion: SimulatedRegionStub;
+ };
+ migrateSimulatedRegion(typedAction.simulatedRegion);
+ }
+ return true;
+ },
+ state: (state) => {
+ const typedState = state as {
+ simulatedRegions: {
+ [key: string]: SimulatedRegionStub;
+ };
+ };
+
+ Object.values(typedState.simulatedRegions).forEach(
+ (simulatedRegion) => {
+ migrateSimulatedRegion(simulatedRegion);
+ }
+ );
+ },
+ };
+
+interface SimulatedRegionStub {
+ activities: {
+ [stateId: string]: {
+ type: string | 'transferVehiclesActivity';
+ };
+ };
+ behaviors: BehaviorStateStub[];
+}
+
+type BehaviorStateStub =
+ | {
+ type: 'answerRequestsBehavior';
+ receivedEvents: unknown[] | undefined;
+ requestsHandled: number | undefined;
+ }
+ | {
+ type: Exclude<'answerRequestsBehavior', unknown>;
+ };
+
+function migrateSimulatedRegion(simulatedRegion: SimulatedRegionStub) {
+ Object.keys(simulatedRegion.activities).forEach((key) => {
+ if (
+ simulatedRegion.activities[key]!.type === 'transferVehiclesActivity'
+ ) {
+ delete simulatedRegion.activities[key];
+ }
+ });
+
+ simulatedRegion.behaviors.forEach((behavior) => {
+ if (behavior.type === 'answerRequestsBehavior') {
+ behavior.receivedEvents = [];
+ behavior.requestsHandled = 0;
+ }
+ });
+}
diff --git a/shared/src/state-migrations/migration-functions.ts b/shared/src/state-migrations/migration-functions.ts
index a987bce4b..1dfb9a6fb 100644
--- a/shared/src/state-migrations/migration-functions.ts
+++ b/shared/src/state-migrations/migration-functions.ts
@@ -17,6 +17,7 @@ import { addPatientStatusTags25 } from './25-add-patient-status-tags';
import { addSimulatedRegionBorderColor26 } from './26-add-border-color-to-simulated-region';
import { addOccupationToVehicles27 } from './27-add-occupation-to-vehicles';
import { activitiesToUnloadVehiclesBehavior28 } from './28-add-activities-to-unload-vehicles-behavior';
+import { removeTransferVehiclesActivityAndChangeAnswerRequestBehavior29 } from './29-remove-transfer-vehicles-activity-and-change-answer-request-behavior';
import { updateEocLog3 } from './3-update-eoc-log';
import { removeSetParticipantIdAction4 } from './4-remove-set-participant-id-action';
import { removeStatistics5 } from './5-remove-statistics';
@@ -79,4 +80,5 @@ export const migrations: {
26: addSimulatedRegionBorderColor26,
27: addOccupationToVehicles27,
28: activitiesToUnloadVehiclesBehavior28,
+ 29: removeTransferVehiclesActivityAndChangeAnswerRequestBehavior29,
};
diff --git a/shared/src/state.ts b/shared/src/state.ts
index 7d002dc4b..da63b3b65 100644
--- a/shared/src/state.ts
+++ b/shared/src/state.ts
@@ -160,5 +160,5 @@ export class ExerciseState {
*
* This number MUST be increased every time a change to any object (that is part of the state or the state itself) is made in a way that there may be states valid before that are no longer valid.
*/
- static readonly currentStateVersion = 28;
+ static readonly currentStateVersion = 29;
}
diff --git a/shared/src/store/action-reducers/hospital.ts b/shared/src/store/action-reducers/hospital.ts
index 91abec4bd..69f19db43 100644
--- a/shared/src/store/action-reducers/hospital.ts
+++ b/shared/src/store/action-reducers/hospital.ts
@@ -6,7 +6,7 @@ import {
Min,
ValidateNested,
} from 'class-validator';
-import { Hospital } from '../../models';
+import { Hospital } from '../../models/hospital';
import { HospitalPatient } from '../../models/hospital-patient';
import { cloneDeepMutable, UUID, uuidValidationOptions } from '../../utils';
import { IsValue } from '../../utils/validators';
diff --git a/shared/src/store/action-reducers/patient.ts b/shared/src/store/action-reducers/patient.ts
index 960832c3b..590694c19 100644
--- a/shared/src/store/action-reducers/patient.ts
+++ b/shared/src/store/action-reducers/patient.ts
@@ -1,6 +1,6 @@
import { Type } from 'class-transformer';
import { IsString, IsUUID, MaxLength, ValidateNested } from 'class-validator';
-import { Patient } from '../../models';
+import { Patient } from '../../models/patient';
import {
isOnMap,
MapPosition,
@@ -28,7 +28,7 @@ import {
import { IsLiteralUnion, IsValue } from '../../utils/validators';
import type { Action, ActionReducer } from '../action-reducer';
import { ReducerError } from '../reducer-error';
-import { PatientRemovedEvent } from '../../simulation';
+import { PatientRemovedEvent } from '../../simulation/events';
import { sendSimulationEvent } from '../../simulation/events/utils';
import { updateTreatments } from './utils/calculate-treatments';
import { getElement } from './utils/get-element';
diff --git a/shared/src/store/action-reducers/simulation.ts b/shared/src/store/action-reducers/simulation.ts
index 9ba17fcf4..cea6c035f 100644
--- a/shared/src/store/action-reducers/simulation.ts
+++ b/shared/src/store/action-reducers/simulation.ts
@@ -9,10 +9,13 @@ import {
} from 'class-validator';
import { Type } from 'class-transformer';
import type {
+ ExerciseSimulationEvent,
TreatPatientsBehaviorState,
UnloadArrivingVehiclesBehaviorState,
} from '../../simulation';
import {
+ TransferPatientsInSpecificVehicleRequestEvent,
+ TransferSpecificVehicleRequestEvent,
updateBehaviorsRequestTarget,
updateBehaviorsRequestInterval,
ReportableInformation,
@@ -28,14 +31,19 @@ import {
uuidValidationOptions,
cloneDeepMutable,
uuidArrayValidationOptions,
+ UUIDSet,
} from '../../utils';
-import { IsLiteralUnion, IsValue } from '../../utils/validators';
+import { IsLiteralUnion, IsUUIDSet, IsValue } from '../../utils/validators';
import type { Action, ActionReducer } from '../action-reducer';
import { ExpectedReducerError, ReducerError } from '../reducer-error';
import {
requestTargetTypeOptions,
ExerciseRequestTargetConfiguration,
} from '../../models';
+import {
+ TransferDestination,
+ transferDestinationTypeAllowedValues,
+} from '../../simulation/utils/transfer-destination';
import { getActivityById, getBehaviorById, getElement } from './utils';
export class UpdateTreatPatientsIntervalsAction implements Action {
@@ -262,6 +270,74 @@ export class UpdatePromiseInvalidationIntervalAction implements Action {
public readonly promiseInvalidationInterval!: number;
}
+export class UpdatePatientLoadTimeAction implements Action {
+ @IsValue('[TransferBehavior] Update Patient Load Time')
+ public readonly type = '[TransferBehavior] Update Patient Load Time';
+
+ @IsUUID(4, uuidValidationOptions)
+ public readonly simulatedRegionId!: UUID;
+
+ @IsUUID(4, uuidValidationOptions)
+ public readonly behaviorId!: UUID;
+
+ @IsInt()
+ @Min(0)
+ public readonly loadTimePerPatient!: number;
+}
+
+export class UpdatePersonnelLoadTimeAction implements Action {
+ @IsValue('[TransferBehavior] Update Personnel Load Time')
+ public readonly type = '[TransferBehavior] Update Personnel Load Time';
+
+ @IsUUID(4, uuidValidationOptions)
+ public readonly simulatedRegionId!: UUID;
+
+ @IsUUID(4, uuidValidationOptions)
+ public readonly behaviorId!: UUID;
+
+ @IsInt()
+ @Min(0)
+ public readonly personnelLoadTime!: number;
+}
+
+export class UpdateDelayBetweenSendsAction implements Action {
+ @IsValue('[TransferBehavior] Update Delay Between Sends')
+ public readonly type = '[TransferBehavior] Update Delay Between Sends';
+
+ @IsUUID(4, uuidValidationOptions)
+ public readonly simulatedRegionId!: UUID;
+
+ @IsUUID(4, uuidValidationOptions)
+ public readonly behaviorId!: UUID;
+
+ @IsInt()
+ @Min(0)
+ public readonly delayBetweenSends!: number;
+}
+
+export class SendTransferRequestEventAction implements Action {
+ @IsValue('[TransferBehavior] Send Transfer Request Event')
+ public readonly type = '[TransferBehavior] Send Transfer Request Event';
+
+ @IsUUID(4, uuidValidationOptions)
+ public readonly simulatedRegionId!: UUID;
+
+ @IsUUID(4, uuidValidationOptions)
+ public readonly behaviorId!: UUID;
+
+ @IsUUID(4, uuidValidationOptions)
+ public readonly vehicleId!: UUID;
+
+ @IsLiteralUnion(transferDestinationTypeAllowedValues)
+ public readonly destinationType!: TransferDestination;
+
+ @IsUUID(4, uuidValidationOptions)
+ public readonly destinationId!: UUID;
+
+ @IsUUIDSet()
+ public readonly patients!: UUIDSet;
+}
+
export namespace SimulationActionReducers {
export const updateTreatPatientsIntervals: ActionReducer =
{
@@ -677,4 +753,122 @@ export namespace SimulationActionReducers {
},
rights: 'trainer',
};
+
+ export const updatePatientLoadTime: ActionReducer =
+ {
+ action: UpdatePatientLoadTimeAction,
+ reducer(
+ draftState,
+ { simulatedRegionId, behaviorId, loadTimePerPatient }
+ ) {
+ const behaviorState = getBehaviorById(
+ draftState,
+ simulatedRegionId,
+ behaviorId,
+ 'transferBehavior'
+ );
+
+ behaviorState.loadTimePerPatient = loadTimePerPatient;
+
+ return draftState;
+ },
+ rights: 'trainer',
+ };
+
+ export const updatePersonnelLoadTime: ActionReducer =
+ {
+ action: UpdatePersonnelLoadTimeAction,
+ reducer(
+ draftState,
+ { simulatedRegionId, behaviorId, personnelLoadTime }
+ ) {
+ const behaviorState = getBehaviorById(
+ draftState,
+ simulatedRegionId,
+ behaviorId,
+ 'transferBehavior'
+ );
+
+ behaviorState.personnelLoadTime = personnelLoadTime;
+
+ return draftState;
+ },
+ rights: 'trainer',
+ };
+
+ export const updateDelayBetweenSends: ActionReducer =
+ {
+ action: UpdateDelayBetweenSendsAction,
+ reducer(
+ draftState,
+ { simulatedRegionId, behaviorId, delayBetweenSends }
+ ) {
+ const behaviorState = getBehaviorById(
+ draftState,
+ simulatedRegionId,
+ behaviorId,
+ 'transferBehavior'
+ );
+
+ behaviorState.delayBetweenSends = delayBetweenSends;
+
+ // also update the value inside the activity if one is running already
+
+ if (behaviorState.recurringActivityId) {
+ const reccuringActivity = getActivityById(
+ draftState,
+ simulatedRegionId,
+ behaviorState.recurringActivityId,
+ 'recurringEventActivity'
+ );
+ reccuringActivity.recurrenceIntervalTime =
+ delayBetweenSends;
+ }
+
+ return draftState;
+ },
+ rights: 'trainer',
+ };
+
+ export const sendTransferRequestEvent: ActionReducer =
+ {
+ action: SendTransferRequestEventAction,
+ reducer(
+ draftState,
+ {
+ simulatedRegionId,
+ vehicleId,
+ destinationType,
+ destinationId,
+ patients,
+ }
+ ) {
+ const simulatedRegion = getElement(
+ draftState,
+ 'simulatedRegion',
+ simulatedRegionId
+ );
+
+ let event: ExerciseSimulationEvent;
+ if (Object.keys(cloneDeepMutable(patients)).length === 0) {
+ event = TransferSpecificVehicleRequestEvent.create(
+ vehicleId,
+ destinationType,
+ destinationId
+ );
+ } else {
+ event =
+ TransferPatientsInSpecificVehicleRequestEvent.create(
+ patients,
+ vehicleId,
+ destinationType,
+ destinationId
+ );
+ }
+
+ sendSimulationEvent(simulatedRegion, event);
+ return draftState;
+ },
+ rights: 'trainer',
+ };
}
diff --git a/shared/src/store/action-reducers/vehicle.ts b/shared/src/store/action-reducers/vehicle.ts
index 39602a0c7..c01b16778 100644
--- a/shared/src/store/action-reducers/vehicle.ts
+++ b/shared/src/store/action-reducers/vehicle.ts
@@ -1,6 +1,6 @@
import { Type } from 'class-transformer';
import { IsArray, IsString, IsUUID, ValidateNested } from 'class-validator';
-import { Material, Personnel, Vehicle } from '../../models';
+import { Material } from '../../models/material';
import {
currentCoordinatesOf,
currentSimulatedRegionIdOf,
@@ -22,7 +22,7 @@ import {
changePositionWithId,
} from '../../models/utils/position/position-helpers-mutable';
import type { ExerciseState } from '../../state';
-import { imageSizeToPosition } from '../../state-helpers';
+import { imageSizeToPosition } from '../../state-helpers/image-size-to-position';
import type { Mutable } from '../../utils';
import {
cloneDeepMutable,
@@ -41,7 +41,9 @@ import {
PersonnelAvailableEvent,
PersonnelRemovedEvent,
VehicleRemovedEvent,
-} from '../../simulation';
+} from '../../simulation/events';
+import { Vehicle } from '../../models/vehicle';
+import { Personnel } from '../../models/personnel';
import { deletePatient } from './patient';
import { completelyLoadVehicle as completelyLoadVehicleHelper } from './utils/completely-load-vehicle';
import { getElement } from './utils/get-element';
diff --git a/test-scenarios b/test-scenarios
index dd6572880..17e783104 160000
--- a/test-scenarios
+++ b/test-scenarios
@@ -1 +1 @@
-Subproject commit dd6572880ee480dfdfc5d0b119975d15be34e9d4
+Subproject commit 17e7831048c3c07ce50149d6853584596fab3d58