style="
width: 10rem;
text-overflow: ellipsis;
- height: 110px;
+ height: 130px;
"
(mousedown)="
dragElementService.onMouseDown($event, {
diff --git a/frontend/src/app/shared/components/patient-status-displayl/patient-status-display/patient-status-display.component.html b/frontend/src/app/shared/components/patient-status-displayl/patient-status-display/patient-status-display.component.html
index ff5d4f483..374fc6590 100644
--- a/frontend/src/app/shared/components/patient-status-displayl/patient-status-display/patient-status-display.component.html
+++ b/frontend/src/app/shared/components/patient-status-displayl/patient-status-display/patient-status-display.component.html
@@ -1,12 +1,20 @@
-
diff --git a/frontend/src/app/shared/components/patient-status-displayl/patient-status-tags-field/patient-status-tags-field.component.html b/frontend/src/app/shared/components/patient-status-displayl/patient-status-tags-field/patient-status-tags-field.component.html
new file mode 100644
index 000000000..3fd67f56a
--- /dev/null
+++ b/frontend/src/app/shared/components/patient-status-displayl/patient-status-tags-field/patient-status-tags-field.component.html
@@ -0,0 +1,3 @@
+
diff --git a/frontend/src/app/shared/components/patient-status-displayl/patient-status-tags-field/patient-status-tags-field.component.scss b/frontend/src/app/shared/components/patient-status-displayl/patient-status-tags-field/patient-status-tags-field.component.scss
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/src/app/shared/components/patient-status-displayl/patient-status-tags-field/patient-status-tags-field.component.ts b/frontend/src/app/shared/components/patient-status-displayl/patient-status-tags-field/patient-status-tags-field.component.ts
new file mode 100644
index 000000000..c85c01037
--- /dev/null
+++ b/frontend/src/app/shared/components/patient-status-displayl/patient-status-tags-field/patient-status-tags-field.component.ts
@@ -0,0 +1,16 @@
+import type { OnInit } from '@angular/core';
+import { Component, Input } from '@angular/core';
+import { Tags } from 'digital-fuesim-manv-shared';
+
+@Component({
+ selector: 'app-patient-status-tags-field',
+ templateUrl: './patient-status-tags-field.component.html',
+ styleUrls: ['./patient-status-tags-field.component.scss'],
+})
+export class PatientStatusTagsFieldComponent implements OnInit {
+ @Input() patientStatusTagsField!: Tags;
+ isPregnant!: boolean;
+ ngOnInit(): void {
+ this.isPregnant = this.patientStatusTagsField.includes('P');
+ }
+}
diff --git a/frontend/src/app/shared/components/patients-details/patients-details.component.html b/frontend/src/app/shared/components/patients-details/patients-details.component.html
index 1b5e4bc73..a040dafa0 100644
--- a/frontend/src/app/shared/components/patients-details/patients-details.component.html
+++ b/frontend/src/app/shared/components/patients-details/patients-details.component.html
@@ -45,7 +45,9 @@
>
Beschreibung
-
+
diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts
index b67b8c6f8..1e7a169d9 100644
--- a/frontend/src/app/shared/shared.module.ts
+++ b/frontend/src/app/shared/shared.module.ts
@@ -35,6 +35,7 @@ import { FooterComponent } from './components/footer/footer.component';
import { PatientHealthPointDisplayComponent } from './components/patient-health-point-display/patient-health-point-display.component';
import { PatientsDetailsComponent } from './components/patients-details/patients-details.component';
import { PatientStatusColorPipe } from './pipes/patient-status-color.pipe';
+import { PatientStatusTagsFieldComponent } from './components/patient-status-displayl/patient-status-tags-field/patient-status-tags-field.component';
import { PatientBehaviorIconPipe } from './pipes/patient-behavior-icon.pipe';
import { PatientBehaviorDescriptionPipe } from './pipes/patient-behavior-description.pipe';
@@ -68,6 +69,7 @@ import { PatientBehaviorDescriptionPipe } from './pipes/patient-behavior-descrip
PatientHealthPointDisplayComponent,
PatientsDetailsComponent,
PatientStatusColorPipe,
+ PatientStatusTagsFieldComponent,
PatientBehaviorIconPipe,
PatientBehaviorDescriptionPipe,
],
diff --git a/shared/src/data/default-state/patient-templates.ts b/shared/src/data/default-state/patient-templates.ts
index 15536ae8d..231bfce0b 100644
--- a/shared/src/data/default-state/patient-templates.ts
+++ b/shared/src/data/default-state/patient-templates.ts
@@ -810,8 +810,8 @@ const prioRedUntilPhase2State = PatientHealthState.create(
);
export const defaultPatientCategories: readonly PatientCategory[] = [
- // XAXAXA Patients
- PatientCategory.create('XAXAXA', defaultPatientImage, [
+ // XAXAXA Patients - Pregnant
+ PatientCategory.create('XAXAXAP', defaultPatientImage, [
PatientTemplate.create(
{
sex: 'female',
@@ -1308,7 +1308,7 @@ export const defaultPatientCategories: readonly PatientCategory[] = [
),
]),
- // ZBZAZA Patients
+ // ZBZAZA Patients - Not Pregnant
PatientCategory.create('ZBZAZA', defaultPatientImage, [
PatientTemplate.create(
{
@@ -1397,6 +1397,10 @@ export const defaultPatientCategories: readonly PatientCategory[] = [
healthPointsDefaults.redAverage,
redUntilBlackPhase2State.id
),
+ ]),
+
+ // ZBZAZA Patients - Pregnant
+ PatientCategory.create('ZBZAZAP', defaultPatientImage, [
PatientTemplate.create(
{
sex: 'female',
diff --git a/shared/src/models/utils/patient-status-code.ts b/shared/src/models/utils/patient-status-code.ts
index 545102abc..e48776f58 100644
--- a/shared/src/models/utils/patient-status-code.ts
+++ b/shared/src/models/utils/patient-status-code.ts
@@ -1,5 +1,7 @@
import { Type } from 'class-transformer';
-import { ValidateNested } from 'class-validator';
+import { IsArray, ValidateNested } from 'class-validator';
+import type { Immutable } from 'immer';
+import { cloneDeepImmutable } from '../../utils';
import type { AllowedValues } from '../../utils/validators';
import { IsLiteralUnion } from '../../utils/validators';
import { getCreate } from './get-create';
@@ -38,6 +40,13 @@ const behaviourCodeAllowedValues: AllowedValues = {
E: true,
};
+type Tag = 'P';
+const tagAllowedValues: AllowedValues = {
+ P: true,
+};
+
+export type Tags = Immutable;
+
export class PatientStatusDataField {
@IsLiteralUnion(colorCodeAllowedValues)
public readonly colorCode: ColorCode;
@@ -69,6 +78,12 @@ export class PatientStatusCode {
@Type(() => PatientStatusDataField)
public readonly thirdField!: PatientStatusDataField;
+ @IsArray()
+ @IsLiteralUnion(tagAllowedValues, {
+ each: true,
+ })
+ public readonly tags!: Tags;
+
/**
* @deprecated Use {@link create} instead
*/
@@ -89,6 +104,7 @@ export class PatientStatusCode {
code[4] as ColorCode,
code[5] as BehaviourCode
);
+ this.tags = cloneDeepImmutable([...code.slice(6)]) as Tags;
}
static readonly create = getCreate(this);
diff --git a/shared/src/state-migrations/25-add-patient-status-tags.ts b/shared/src/state-migrations/25-add-patient-status-tags.ts
new file mode 100644
index 000000000..baba5c036
--- /dev/null
+++ b/shared/src/state-migrations/25-add-patient-status-tags.ts
@@ -0,0 +1,36 @@
+import type { Migration } from './migration-functions';
+
+export const addPatientStatusTags25: Migration = {
+ action: (_intermediaryState, action) => {
+ const actionType = (action as { type: string } | null)?.type;
+
+ if (actionType === '[Patient] Add patient') {
+ const typedAction = action as {
+ patient: {
+ patientStatusCode: { tags: 'P'[] | undefined };
+ };
+ };
+ typedAction.patient.patientStatusCode.tags = [];
+ }
+
+ return true;
+ },
+ state: (state) => {
+ const typedState = state as {
+ patients: {
+ [patientId: string]: {
+ patientStatusCode: { tags: 'P'[] | undefined };
+ };
+ };
+ patientCategories: { name: { tags: 'P'[] | undefined } }[];
+ };
+
+ Object.values(typedState.patients).forEach((patient) => {
+ patient.patientStatusCode.tags = [];
+ });
+
+ typedState.patientCategories.forEach((status) => {
+ status.name.tags = [];
+ });
+ },
+};
diff --git a/shared/src/state-migrations/migration-functions.ts b/shared/src/state-migrations/migration-functions.ts
index df90da29f..1bf6a0985 100644
--- a/shared/src/state-migrations/migration-functions.ts
+++ b/shared/src/state-migrations/migration-functions.ts
@@ -13,6 +13,7 @@ import { fixTypoInRenameSimulatedRegion21 } from './21-fix-typo-in-rename-simula
import { removeIllegalVehicleMovementActions22 } from './22-remove-illegal-vehicle-movement-actions';
import { addTransferPointToSimulatedRegion23 } from './23-add-transfer-point-to-simulated-region';
import { addRadiograms24 } from './24-add-radiograms';
+import { addPatientStatusTags25 } from './25-add-patient-status-tags';
import { updateEocLog3 } from './3-update-eoc-log';
import { removeSetParticipantIdAction4 } from './4-remove-set-participant-id-action';
import { removeStatistics5 } from './5-remove-statistics';
@@ -71,4 +72,5 @@ export const migrations: {
22: removeIllegalVehicleMovementActions22,
23: addTransferPointToSimulatedRegion23,
24: addRadiograms24,
+ 25: addPatientStatusTags25,
};
diff --git a/shared/src/state.ts b/shared/src/state.ts
index a6c7a5005..aad59b62b 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 = 24;
+ static readonly currentStateVersion = 25;
}
diff --git a/test-scenarios b/test-scenarios
index dac9453dd..4d9cf83fc 160000
--- a/test-scenarios
+++ b/test-scenarios
@@ -1 +1 @@
-Subproject commit dac9453dd1ad1c25ad9e10e7406dc50bc867c269
+Subproject commit 4d9cf83fcf7edba4116baab72dafd1d592835922
From 066d32b1672aa1f319d8b40213e57d2c2ad58ba9 Mon Sep 17 00:00:00 2001
From: benn02 <82985280+benn02@users.noreply.github.com>
Date: Wed, 29 Mar 2023 10:58:56 +0200
Subject: [PATCH 06/31] Add Missing Transfer Connection Radiogram (#835)
---
CHANGELOG.md | 1 +
.../human-readable-radiogram-type.pipe.ts | 1 +
.../models/radiogram/exercise-radiogram.ts | 3 ++
shared/src/models/radiogram/index.ts | 1 +
.../missing-transfer-connection-radiogram.ts | 49 +++++++++++++++++++
5 files changed, 55 insertions(+)
create mode 100644 shared/src/models/radiogram/missing-transfer-connection-radiogram.ts
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e31d0e39e..c7215e258 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@ and this project does **not** adhere to [Semantic Versioning](https://semver.org
- There is now a display for how many different variations of a patient template exists.
- There is now a display for whether a patient is pregnant.
- The patient status display that visualizes the progression of a patient explains its icons via a tooltip
+- There is now a radiogram for missing transfer connections
### Changed
diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/human-readable-radiogram-type.pipe.ts b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/human-readable-radiogram-type.pipe.ts
index 73b5f88b1..a4adb0941 100644
--- a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/human-readable-radiogram-type.pipe.ts
+++ b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/human-readable-radiogram-type.pipe.ts
@@ -4,6 +4,7 @@ import type { ExerciseRadiogram } from 'digital-fuesim-manv-shared';
const map: { [Key in ExerciseRadiogram['type']]: string } = {
materialCountRadiogram: 'Anzahl an Material',
+ missingTransferConnectionRadiogram: 'Es fehlt eine Transferverbindung',
patientCountRadiogram: 'Anzahl an Patienten',
personnelCountRadiogram: 'Anzahl an Personal',
treatmentStatusRadiogram: 'Behandlungsstatus',
diff --git a/shared/src/models/radiogram/exercise-radiogram.ts b/shared/src/models/radiogram/exercise-radiogram.ts
index 7f9e4e65f..76193f794 100644
--- a/shared/src/models/radiogram/exercise-radiogram.ts
+++ b/shared/src/models/radiogram/exercise-radiogram.ts
@@ -1,6 +1,7 @@
import type { Type } from 'class-transformer';
import type { Constructor } from '../../utils';
import { MaterialCountRadiogram } from './material-count-radiogram';
+import { MissingTransferConnectionRadiogram } from './missing-transfer-connection-radiogram';
import { PatientCountRadiogram } from './patient-count-radiogram';
import { PersonnelCountRadiogram } from './personnel-count-radiogram';
import { Radiogram } from './radiogram';
@@ -9,6 +10,7 @@ import { VehicleCountRadiogram } from './vehicle-count-radiogram';
export const radiograms = {
MaterialCountRadiogram,
+ MissingTransferConnectionRadiogram,
PatientCountRadiogram,
PersonnelCountRadiogram,
TreatmentStatusRadiogram,
@@ -25,6 +27,7 @@ type ExerciseRadiogramDictionary = {
export const radiogramDictionary: ExerciseRadiogramDictionary = {
materialCountRadiogram: MaterialCountRadiogram,
+ missingTransferConnectionRadiogram: MissingTransferConnectionRadiogram,
patientCountRadiogram: PatientCountRadiogram,
personnelCountRadiogram: PersonnelCountRadiogram,
treatmentStatusRadiogram: TreatmentStatusRadiogram,
diff --git a/shared/src/models/radiogram/index.ts b/shared/src/models/radiogram/index.ts
index c07d45c2f..3389da6c0 100644
--- a/shared/src/models/radiogram/index.ts
+++ b/shared/src/models/radiogram/index.ts
@@ -1,5 +1,6 @@
export * from './exercise-radiogram';
export * from './material-count-radiogram';
+export * from './missing-transfer-connection-radiogram';
export * from './patient-count-radiogram';
export * from './personnel-count-radiogram';
export * from './radiogram-helpers';
diff --git a/shared/src/models/radiogram/missing-transfer-connection-radiogram.ts b/shared/src/models/radiogram/missing-transfer-connection-radiogram.ts
new file mode 100644
index 000000000..b846feeee
--- /dev/null
+++ b/shared/src/models/radiogram/missing-transfer-connection-radiogram.ts
@@ -0,0 +1,49 @@
+import { IsBoolean, IsUUID, ValidateNested } from 'class-validator';
+import { UUID } from '../../utils';
+import { IsValue } from '../../utils/validators';
+import { IsRadiogramStatus } from '../../utils/validators/is-radiogram-status';
+import { getCreate } from '../utils';
+import type { Radiogram } from './radiogram';
+import { ExerciseRadiogramStatus } from './status/exercise-radiogram-status';
+
+export class MissingTransferConnectionRadiogram implements Radiogram {
+ @IsUUID()
+ readonly id: UUID;
+
+ @IsValue('missingTransferConnectionRadiogram')
+ readonly type = 'missingTransferConnectionRadiogram';
+
+ @IsUUID()
+ readonly simulatedRegionId: UUID;
+
+ /**
+ * @deprecated use the helpers from {@link radiogram-helpers.ts}
+ * or {@link radiogram-helpers-mutable.ts} instead
+ */
+ @IsRadiogramStatus()
+ @ValidateNested()
+ readonly status: ExerciseRadiogramStatus;
+
+ @IsBoolean()
+ readonly informationAvailable: boolean = true;
+
+ @IsUUID()
+ readonly targetTransferPointId: UUID;
+
+ /**
+ * @deprecated Use {@link create} instead
+ */
+ constructor(
+ id: UUID,
+ simulatedRegionId: UUID,
+ status: ExerciseRadiogramStatus,
+ targetTransferPointId: UUID
+ ) {
+ this.id = id;
+ this.simulatedRegionId = simulatedRegionId;
+ this.status = status;
+ this.targetTransferPointId = targetTransferPointId;
+ }
+
+ static readonly create = getCreate(this);
+}
From d4f6e8c6f49debca7848d8835e4d42cb9731e3f6 Mon Sep 17 00:00:00 2001
From: Lukas Hagen <43916057+Greenscreen23@users.noreply.github.com>
Date: Wed, 29 Mar 2023 12:52:57 +0200
Subject: [PATCH 07/31] Remove appending .prettierignore-ci in npm scripts and
add test-scenarios to .prettierignore (#839)
---
.prettierignore | 2 ++
package.json | 6 +++---
2 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/.prettierignore b/.prettierignore
index 20069305f..e172a1a18 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -8,3 +8,5 @@ docker-compose.yml
# The states in here should not be touched by humans
benchmark/data
+
+test-scenarios/
diff --git a/package.json b/package.json
index c680a3d51..cec9b9e4b 100644
--- a/package.json
+++ b/package.json
@@ -16,9 +16,9 @@
"prune:deployment": "npm prune --production && cd shared && npm prune --production && cd ../backend && npm prune --production",
"deployment": "npm run setup:ci && npm run build:deployment && npm run prune:deployment",
"setup:ci": "npm ci && cd shared && npm ci && npm run build && cd ../frontend && npm ci && cd ../backend && npm ci",
- "prettier": "cat .gitignore .prettierignore >> .prettierignore-ci && prettier --ignore-path .prettierignore-ci --loglevel warn --write \"**/*\" --ignore-unknown",
- "prettier:windows": "type .gitignore .prettierignore >> .prettierignore-ci && prettier --ignore-path .prettierignore-ci --loglevel warn --write \"**/*\" --ignore-unknown",
- "prettier:check": "cat .gitignore .prettierignore >> .prettierignore-ci && prettier --ignore-path .prettierignore-ci --loglevel warn --check \"**/*\" --ignore-unknown",
+ "prettier": "cat .gitignore .prettierignore > .prettierignore-ci && prettier --ignore-path .prettierignore-ci --loglevel warn --write \"**/*\" --ignore-unknown",
+ "prettier:windows": "type .gitignore .prettierignore > .prettierignore-ci && prettier --ignore-path .prettierignore-ci --loglevel warn --write \"**/*\" --ignore-unknown",
+ "prettier:check": "cat .gitignore .prettierignore > .prettierignore-ci && prettier --ignore-path .prettierignore-ci --loglevel warn --check \"**/*\" --ignore-unknown",
"lint": "concurrently \"cd shared && npm run lint\" \"cd frontend && npm run lint\" \"cd backend && npm run lint\"",
"lint:fix": "concurrently \"cd shared && npm run lint:fix\" \"cd frontend && npm run lint:fix\" \"cd backend && npm run lint:fix\"",
"test:concurrently": "concurrently \"cd shared && npm run test\" \"cd frontend && npm run test\" \"cd backend && npm run test\"",
From 25427955f019b48085f10b05855e7d5810266fd5 Mon Sep 17 00:00:00 2001
From: Nils1729 <45318774+Nils1729@users.noreply.github.com>
Date: Wed, 29 Mar 2023 13:03:11 +0200
Subject: [PATCH 08/31] Add vehicle resource data type (#837)
* Add vehicle resource data type
* respond to review
---
shared/src/models/utils/vehicle-resource.ts | 22 +++++++++++++++++++
.../validators/is-resource-description.ts | 22 +++++++++++++++++++
2 files changed, 44 insertions(+)
create mode 100644 shared/src/models/utils/vehicle-resource.ts
create mode 100644 shared/src/utils/validators/is-resource-description.ts
diff --git a/shared/src/models/utils/vehicle-resource.ts b/shared/src/models/utils/vehicle-resource.ts
new file mode 100644
index 000000000..dfeff62b5
--- /dev/null
+++ b/shared/src/models/utils/vehicle-resource.ts
@@ -0,0 +1,22 @@
+import { ValidateNested } from 'class-validator';
+import { IsValue } from '../../utils/validators';
+import { IsResourceDescription } from '../../utils/validators/is-resource-description';
+import { getCreate } from './get-create';
+
+export class VehicleResource {
+ @IsValue('vehicleResource' as const)
+ public readonly type = 'vehicleResource';
+
+ @ValidateNested()
+ @IsResourceDescription()
+ public readonly vehicleCounts!: { [key: string]: number };
+
+ /**
+ * @deprecated Use {@link create} instead
+ */
+ constructor(vehicleCounts: { [key: string]: number }) {
+ this.vehicleCounts = vehicleCounts;
+ }
+
+ static readonly create = getCreate(this);
+}
diff --git a/shared/src/utils/validators/is-resource-description.ts b/shared/src/utils/validators/is-resource-description.ts
new file mode 100644
index 000000000..d6b18ca74
--- /dev/null
+++ b/shared/src/utils/validators/is-resource-description.ts
@@ -0,0 +1,22 @@
+import type { ValidationArguments, ValidationOptions } from 'class-validator';
+import { min, isString, isInt } from 'class-validator';
+import { createMapValidator } from './create-map-validator';
+import type { GenericPropertyDecorator } from './generic-property-decorator';
+import { makeValidator } from './make-validator';
+
+export const isResourceDescription = createMapValidator({
+ keyValidator: isString,
+ valueValidator: (value): value is number => isInt(value) && min(value, 0),
+});
+
+// eslint-disable-next-line @typescript-eslint/naming-convention
+export function IsResourceDescription(
+ validationOptions?: ValidationOptions & { each?: Each }
+): GenericPropertyDecorator<{ [key: string]: number }, Each> {
+ return makeValidator<{ [key: string]: number }, Each>(
+ 'isResourceDescription',
+ (value: unknown, args?: ValidationArguments) =>
+ isResourceDescription(value),
+ validationOptions
+ );
+}
From b8ff716f22a27434b3c0c137c148124f919db952 Mon Sep 17 00:00:00 2001
From: benn02 <82985280+benn02@users.noreply.github.com>
Date: Wed, 29 Mar 2023 13:42:10 +0200
Subject: [PATCH 09/31] Add Vehicles Sent Event (#841)
---
.../events/exercise-simulation-event.ts | 3 ++
shared/src/simulation/events/index.ts | 1 +
shared/src/simulation/events/vehicles-sent.ts | 28 +++++++++++++++++++
3 files changed, 32 insertions(+)
create mode 100644 shared/src/simulation/events/vehicles-sent.ts
diff --git a/shared/src/simulation/events/exercise-simulation-event.ts b/shared/src/simulation/events/exercise-simulation-event.ts
index 3031614ae..df741830c 100644
--- a/shared/src/simulation/events/exercise-simulation-event.ts
+++ b/shared/src/simulation/events/exercise-simulation-event.ts
@@ -10,6 +10,7 @@ import { TreatmentsTimerEvent } from './treatments-timer-event';
import { TreatmentProgressChangedEvent } from './treatment-progress-changed';
import { CollectInformationEvent } from './collect';
import { StartCollectingInformationEvent } from './start-collecting';
+import { VehiclesSentEvent } from './vehicles-sent';
export const simulationEvents = {
MaterialAvailableEvent,
@@ -21,6 +22,7 @@ export const simulationEvents = {
VehicleArrivedEvent,
CollectInformationEvent,
StartCollectingInformationEvent,
+ VehiclesSentEvent,
};
export type ExerciseSimulationEvent = InstanceType<
@@ -42,6 +44,7 @@ export const simulationEventDictionary: ExerciseSimulationEventDictionary = {
vehicleArrivedEvent: VehicleArrivedEvent,
collectInformationEvent: CollectInformationEvent,
startCollectingInformationEvent: StartCollectingInformationEvent,
+ vehiclesSentEvent: VehiclesSentEvent,
};
export const simulationEventTypeOptions: Parameters = [
diff --git a/shared/src/simulation/events/index.ts b/shared/src/simulation/events/index.ts
index 3ec43b9c7..15026c1e9 100644
--- a/shared/src/simulation/events/index.ts
+++ b/shared/src/simulation/events/index.ts
@@ -6,3 +6,4 @@ export * from './tick';
export * from './treatment-progress-changed';
export * from './treatments-timer-event';
export * from './vehicle-arrived';
+export * from './vehicles-sent';
diff --git a/shared/src/simulation/events/vehicles-sent.ts b/shared/src/simulation/events/vehicles-sent.ts
new file mode 100644
index 000000000..b0d427244
--- /dev/null
+++ b/shared/src/simulation/events/vehicles-sent.ts
@@ -0,0 +1,28 @@
+import { Type } from 'class-transformer';
+import { IsUUID } from 'class-validator';
+import { getCreate } from '../../models/utils';
+import { VehicleResource } from '../../models/utils/vehicle-resource';
+import { UUID, uuidValidationOptions } from '../../utils';
+import { IsValue } from '../../utils/validators';
+import type { SimulationEvent } from './simulation-event';
+
+export class VehiclesSentEvent implements SimulationEvent {
+ @IsValue('vehiclesSentEvent')
+ readonly type = 'vehiclesSentEvent';
+
+ @IsUUID(4, uuidValidationOptions)
+ public readonly id: UUID;
+
+ @Type(() => VehicleResource)
+ readonly vehiclesSent!: VehicleResource;
+
+ /**
+ * @deprecated Use {@link create} instead
+ */
+ constructor(id: UUID, vehiclesSent: VehicleResource) {
+ this.id = id;
+ this.vehiclesSent = vehiclesSent;
+ }
+
+ static readonly create = getCreate(this);
+}
From ee2bacaaaee44a2caa3c7335012e58d26e72bc6c Mon Sep 17 00:00:00 2001
From: Nils1729 <45318774+Nils1729@users.noreply.github.com>
Date: Wed, 29 Mar 2023 14:00:45 +0200
Subject: [PATCH 10/31] Add resource requirement event (#842)
* Add resource requirement event
* Add resources required event to index.ts
---
.../events/exercise-simulation-event.ts | 3 ++
shared/src/simulation/events/index.ts | 1 +
.../simulation/events/resources-required.ts | 45 +++++++++++++++++++
3 files changed, 49 insertions(+)
create mode 100644 shared/src/simulation/events/resources-required.ts
diff --git a/shared/src/simulation/events/exercise-simulation-event.ts b/shared/src/simulation/events/exercise-simulation-event.ts
index df741830c..ca81e6748 100644
--- a/shared/src/simulation/events/exercise-simulation-event.ts
+++ b/shared/src/simulation/events/exercise-simulation-event.ts
@@ -10,6 +10,7 @@ import { TreatmentsTimerEvent } from './treatments-timer-event';
import { TreatmentProgressChangedEvent } from './treatment-progress-changed';
import { CollectInformationEvent } from './collect';
import { StartCollectingInformationEvent } from './start-collecting';
+import { ResourceRequiredEvent } from './resources-required';
import { VehiclesSentEvent } from './vehicles-sent';
export const simulationEvents = {
@@ -22,6 +23,7 @@ export const simulationEvents = {
VehicleArrivedEvent,
CollectInformationEvent,
StartCollectingInformationEvent,
+ ResourceRequiredEvent,
VehiclesSentEvent,
};
@@ -44,6 +46,7 @@ export const simulationEventDictionary: ExerciseSimulationEventDictionary = {
vehicleArrivedEvent: VehicleArrivedEvent,
collectInformationEvent: CollectInformationEvent,
startCollectingInformationEvent: StartCollectingInformationEvent,
+ resourceRequiredEvent: ResourceRequiredEvent,
vehiclesSentEvent: VehiclesSentEvent,
};
diff --git a/shared/src/simulation/events/index.ts b/shared/src/simulation/events/index.ts
index 15026c1e9..6e35520b9 100644
--- a/shared/src/simulation/events/index.ts
+++ b/shared/src/simulation/events/index.ts
@@ -6,4 +6,5 @@ export * from './tick';
export * from './treatment-progress-changed';
export * from './treatments-timer-event';
export * from './vehicle-arrived';
+export * from './resources-required';
export * from './vehicles-sent';
diff --git a/shared/src/simulation/events/resources-required.ts b/shared/src/simulation/events/resources-required.ts
new file mode 100644
index 000000000..affd2d2a7
--- /dev/null
+++ b/shared/src/simulation/events/resources-required.ts
@@ -0,0 +1,45 @@
+import { Type } from 'class-transformer';
+import { IsString, IsUUID, ValidateNested } from 'class-validator';
+import { getCreate } from '../../models/utils';
+import { VehicleResource } from '../../models/utils/vehicle-resource';
+import { UUID, uuidValidationOptions } from '../../utils';
+import { IsValue } from '../../utils/validators';
+import type { SimulationEvent } from './simulation-event';
+
+export class ResourceRequiredEvent implements SimulationEvent {
+ @IsValue('resourceRequiredEvent')
+ readonly type = 'resourceRequiredEvent';
+
+ @IsUUID(4, uuidValidationOptions)
+ public readonly id!: UUID;
+
+ @IsUUID(4, uuidValidationOptions)
+ readonly requiringSimulatedRegionId: UUID;
+
+ @ValidateNested()
+ @Type(() => VehicleResource)
+ readonly requiredResource: VehicleResource;
+
+ /**
+ * Used for deduplication of needs between different events of this type
+ */
+ @IsString()
+ readonly key: string;
+
+ /**
+ * @deprecated Use {@link create} instead
+ */
+ constructor(
+ id: UUID,
+ requiringSimulatedRegionId: UUID,
+ requiredResource: VehicleResource,
+ key: string
+ ) {
+ this.id = id;
+ this.requiringSimulatedRegionId = requiringSimulatedRegionId;
+ this.requiredResource = requiredResource;
+ this.key = key;
+ }
+
+ static readonly create = getCreate(this);
+}
From 4c0f96262165fc9e722f14200306233829d20f6c Mon Sep 17 00:00:00 2001
From: Lukas Hagen <43916057+Greenscreen23@users.noreply.github.com>
Date: Wed, 29 Mar 2023 14:11:07 +0200
Subject: [PATCH 11/31] Feature/828 create missing transfer connection
radiogram component (#836)
* Refactor typing of the selector for radiograms
* Add radiogram component for missing transfer connections
* Fix linter
* Reword radiogram
---
...missing-transfer-connection.component.html | 7 +++
...missing-transfer-connection.component.scss | 0
...t-missing-transfer-connection.component.ts | 51 +++++++++++++++++++
...am-card-content-patient-count.component.ts | 5 +-
...-card-content-personnel-count.component.ts | 4 +-
...card-content-treatment-status.component.ts | 4 +-
...am-card-content-vehicle-count.component.ts | 5 +-
.../radiogram-card-content.component.html | 12 +++--
.../simulated-region-overview.module.ts | 2 +
.../selectors/exercise.selectors.ts | 9 +++-
10 files changed, 83 insertions(+), 16 deletions(-)
create mode 100644 frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radigoram-card-content-missing-transfer-connection/radigoram-card-content-missing-transfer-connection.component.html
create mode 100644 frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radigoram-card-content-missing-transfer-connection/radigoram-card-content-missing-transfer-connection.component.scss
create mode 100644 frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radigoram-card-content-missing-transfer-connection/radigoram-card-content-missing-transfer-connection.component.ts
diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radigoram-card-content-missing-transfer-connection/radigoram-card-content-missing-transfer-connection.component.html b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radigoram-card-content-missing-transfer-connection/radigoram-card-content-missing-transfer-connection.component.html
new file mode 100644
index 000000000..384a0ba20
--- /dev/null
+++ b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radigoram-card-content-missing-transfer-connection/radigoram-card-content-missing-transfer-connection.component.html
@@ -0,0 +1,7 @@
+
+ Fehlende Verbindung
+ Fahrzeuge sollten zum Transferpunkt "{{ transferPointName }}" gesendet
+ werden, aktuell existiert aber keine Verbindung dorthin
+
diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radigoram-card-content-missing-transfer-connection/radigoram-card-content-missing-transfer-connection.component.scss b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radigoram-card-content-missing-transfer-connection/radigoram-card-content-missing-transfer-connection.component.scss
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radigoram-card-content-missing-transfer-connection/radigoram-card-content-missing-transfer-connection.component.ts b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radigoram-card-content-missing-transfer-connection/radigoram-card-content-missing-transfer-connection.component.ts
new file mode 100644
index 000000000..d10e9fa82
--- /dev/null
+++ b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radigoram-card-content-missing-transfer-connection/radigoram-card-content-missing-transfer-connection.component.ts
@@ -0,0 +1,51 @@
+import type { OnInit } from '@angular/core';
+import { Component, Input } from '@angular/core';
+import { Store } from '@ngrx/store';
+import type { MissingTransferConnectionRadiogram } from 'digital-fuesim-manv-shared';
+import { UUID } from 'digital-fuesim-manv-shared';
+import type { Observable } from 'rxjs';
+import { combineLatest, map } from 'rxjs';
+import type { AppState } from 'src/app/state/app.state';
+import {
+ createSelectRadiogram,
+ selectTransferPoints,
+} from 'src/app/state/application/selectors/exercise.selectors';
+
+@Component({
+ selector: 'app-radigoram-card-content-missing-transfer-connection',
+ templateUrl:
+ './radigoram-card-content-missing-transfer-connection.component.html',
+ styleUrls: [
+ './radigoram-card-content-missing-transfer-connection.component.scss',
+ ],
+})
+export class RadigoramCardContentMissingTransferConnectionComponent
+ implements OnInit
+{
+ @Input() radiogramId!: UUID;
+
+ transferPointName$!: Observable;
+
+ constructor(private readonly store: Store) {}
+
+ ngOnInit(): void {
+ const radiogram$ = this.store.select(
+ createSelectRadiogram(
+ this.radiogramId
+ )
+ );
+
+ const transferPoints$ = this.store.select(selectTransferPoints);
+
+ this.transferPointName$ = combineLatest([
+ radiogram$,
+ transferPoints$,
+ ]).pipe(
+ map(
+ ([radiogram, transferPoints]) =>
+ transferPoints[radiogram.targetTransferPointId]
+ ?.externalName ?? 'Unbekannt'
+ )
+ );
+ }
+}
diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radiogram-card-content-patient-count/radiogram-card-content-patient-count.component.ts b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radiogram-card-content-patient-count/radiogram-card-content-patient-count.component.ts
index 471100ca3..7d443125d 100644
--- a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radiogram-card-content-patient-count/radiogram-card-content-patient-count.component.ts
+++ b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radiogram-card-content-patient-count/radiogram-card-content-patient-count.component.ts
@@ -1,6 +1,5 @@
import type { OnInit } from '@angular/core';
import { Component, Input } from '@angular/core';
-import type { MemoizedSelector } from '@ngrx/store';
import { createSelector, Store } from '@ngrx/store';
import type { PatientCountRadiogram } from 'digital-fuesim-manv-shared';
import { UUID } from 'digital-fuesim-manv-shared';
@@ -25,9 +24,9 @@ export class RadiogramCardContentPatientCountComponent implements OnInit {
constructor(private readonly store: Store) {}
ngOnInit(): void {
- const radiogramSelector = createSelectRadiogram(
+ const radiogramSelector = createSelectRadiogram(
this.radiogramId
- ) as MemoizedSelector>;
+ );
const totalPatientCountSelector = createSelector(
radiogramSelector,
diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radiogram-card-content-personnel-count/radiogram-card-content-personnel-count.component.ts b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radiogram-card-content-personnel-count/radiogram-card-content-personnel-count.component.ts
index 1be36737a..31d66234b 100644
--- a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radiogram-card-content-personnel-count/radiogram-card-content-personnel-count.component.ts
+++ b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radiogram-card-content-personnel-count/radiogram-card-content-personnel-count.component.ts
@@ -29,7 +29,7 @@ export class RadiogramCardContentPersonnelCountComponent implements OnInit {
ngOnInit(): void {
this.radiogram$ = this.store.select(
- createSelectRadiogram(this.radiogramId)
- ) as Observable;
+ createSelectRadiogram(this.radiogramId)
+ );
}
}
diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radiogram-card-content-treatment-status/radiogram-card-content-treatment-status.component.ts b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radiogram-card-content-treatment-status/radiogram-card-content-treatment-status.component.ts
index f0857f96c..c336f7003 100644
--- a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radiogram-card-content-treatment-status/radiogram-card-content-treatment-status.component.ts
+++ b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radiogram-card-content-treatment-status/radiogram-card-content-treatment-status.component.ts
@@ -21,7 +21,7 @@ export class RadiogramCardContentTreatmentStatusComponent implements OnInit {
ngOnInit(): void {
this.radiogram$ = this.store.select(
- createSelectRadiogram(this.radiogramId)
- ) as Observable;
+ createSelectRadiogram(this.radiogramId)
+ );
}
}
diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radiogram-card-content-vehicle-count/radiogram-card-content-vehicle-count.component.ts b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radiogram-card-content-vehicle-count/radiogram-card-content-vehicle-count.component.ts
index f0fadf152..ead34b46d 100644
--- a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radiogram-card-content-vehicle-count/radiogram-card-content-vehicle-count.component.ts
+++ b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radiogram-card-content-vehicle-count/radiogram-card-content-vehicle-count.component.ts
@@ -1,6 +1,5 @@
import type { OnInit } from '@angular/core';
import { Component, Input } from '@angular/core';
-import type { MemoizedSelector } from '@ngrx/store';
import { createSelector, Store } from '@ngrx/store';
import type { VehicleCountRadiogram } from 'digital-fuesim-manv-shared';
import { UUID } from 'digital-fuesim-manv-shared';
@@ -24,9 +23,9 @@ export class RadiogramCardContentVehicleCountComponent implements OnInit {
constructor(private readonly store: Store) {}
ngOnInit(): void {
- const radiogramSelector = createSelectRadiogram(
+ const radiogramSelector = createSelectRadiogram(
this.radiogramId
- ) as MemoizedSelector>;
+ );
const vehicleCountsSelector = createSelector(
radiogramSelector,
diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radiogram-card-content/radiogram-card-content.component.html b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radiogram-card-content/radiogram-card-content.component.html
index e70802556..99879c74f 100644
--- a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radiogram-card-content/radiogram-card-content.component.html
+++ b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radiogram-card-content/radiogram-card-content.component.html
@@ -19,14 +19,18 @@
*ngSwitchCase="'personnelCountRadiogram'"
[radiogramId]="radiogram.id"
>
-
+
+
(id: UUID) {
+ return createSelector(
+ selectRadiograms,
+ (radiograms) => radiograms[id] as R
+ );
+}
function createSelectElementFromArrayFactory(
elementsSelector: (state: AppState) => readonly Element[]
From 560623dffece3b7d1904d842afe2f6f6d9494090 Mon Sep 17 00:00:00 2001
From: Nils1729 <45318774+Nils1729@users.noreply.github.com>
Date: Wed, 29 Mar 2023 14:22:46 +0200
Subject: [PATCH 12/31] Fix validation for vehicle request event (#843)
* Add resource requirement event
* Add resources required event to index.ts
* Remove unnecessary @ValidateNested
* Remove unnecessary import
---
shared/src/models/utils/vehicle-resource.ts | 2 --
1 file changed, 2 deletions(-)
diff --git a/shared/src/models/utils/vehicle-resource.ts b/shared/src/models/utils/vehicle-resource.ts
index dfeff62b5..fd4d11ae7 100644
--- a/shared/src/models/utils/vehicle-resource.ts
+++ b/shared/src/models/utils/vehicle-resource.ts
@@ -1,4 +1,3 @@
-import { ValidateNested } from 'class-validator';
import { IsValue } from '../../utils/validators';
import { IsResourceDescription } from '../../utils/validators/is-resource-description';
import { getCreate } from './get-create';
@@ -7,7 +6,6 @@ export class VehicleResource {
@IsValue('vehicleResource' as const)
public readonly type = 'vehicleResource';
- @ValidateNested()
@IsResourceDescription()
public readonly vehicleCounts!: { [key: string]: number };
From 52b2be1b1a8c3dcfae7a45082653d82471523904 Mon Sep 17 00:00:00 2001
From: benn02 <82985280+benn02@users.noreply.github.com>
Date: Wed, 29 Mar 2023 14:31:01 +0200
Subject: [PATCH 13/31] Add Vehicle Request Radiogram (#840)
---
CHANGELOG.md | 2 +-
.../human-readable-radiogram-type.pipe.ts | 1 +
.../models/radiogram/exercise-radiogram.ts | 3 ++
shared/src/models/radiogram/index.ts | 1 +
.../radiogram/resource-request-radiogram.ts | 52 +++++++++++++++++++
shared/src/simulation/events/vehicles-sent.ts | 3 +-
6 files changed, 60 insertions(+), 2 deletions(-)
create mode 100644 shared/src/models/radiogram/resource-request-radiogram.ts
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c7215e258..8fb592f3b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,7 +12,7 @@ and this project does **not** adhere to [Semantic Versioning](https://semver.org
- There is now a display for how many different variations of a patient template exists.
- There is now a display for whether a patient is pregnant.
- The patient status display that visualizes the progression of a patient explains its icons via a tooltip
-- There is now a radiogram for missing transfer connections
+- There is now a radiogram for missing transfer connections and vehicle requests
### Changed
diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/human-readable-radiogram-type.pipe.ts b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/human-readable-radiogram-type.pipe.ts
index a4adb0941..4ef2d47aa 100644
--- a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/human-readable-radiogram-type.pipe.ts
+++ b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/human-readable-radiogram-type.pipe.ts
@@ -9,6 +9,7 @@ const map: { [Key in ExerciseRadiogram['type']]: string } = {
personnelCountRadiogram: 'Anzahl an Personal',
treatmentStatusRadiogram: 'Behandlungsstatus',
vehicleCountRadiogram: 'Anzahl an Fahrzeugen',
+ resourceRequestRadiogram: 'Anfrage nach Fahrzeugen',
};
@Pipe({
diff --git a/shared/src/models/radiogram/exercise-radiogram.ts b/shared/src/models/radiogram/exercise-radiogram.ts
index 76193f794..1ac3e1fc1 100644
--- a/shared/src/models/radiogram/exercise-radiogram.ts
+++ b/shared/src/models/radiogram/exercise-radiogram.ts
@@ -7,12 +7,14 @@ import { PersonnelCountRadiogram } from './personnel-count-radiogram';
import { Radiogram } from './radiogram';
import { TreatmentStatusRadiogram } from './treatment-status-radiogram';
import { VehicleCountRadiogram } from './vehicle-count-radiogram';
+import { ResourceRequestRadiogram } from './resource-request-radiogram';
export const radiograms = {
MaterialCountRadiogram,
MissingTransferConnectionRadiogram,
PatientCountRadiogram,
PersonnelCountRadiogram,
+ ResourceRequestRadiogram,
TreatmentStatusRadiogram,
VehicleCountRadiogram,
};
@@ -30,6 +32,7 @@ export const radiogramDictionary: ExerciseRadiogramDictionary = {
missingTransferConnectionRadiogram: MissingTransferConnectionRadiogram,
patientCountRadiogram: PatientCountRadiogram,
personnelCountRadiogram: PersonnelCountRadiogram,
+ resourceRequestRadiogram: ResourceRequestRadiogram,
treatmentStatusRadiogram: TreatmentStatusRadiogram,
vehicleCountRadiogram: VehicleCountRadiogram,
};
diff --git a/shared/src/models/radiogram/index.ts b/shared/src/models/radiogram/index.ts
index 3389da6c0..dfea8ddf1 100644
--- a/shared/src/models/radiogram/index.ts
+++ b/shared/src/models/radiogram/index.ts
@@ -6,4 +6,5 @@ export * from './personnel-count-radiogram';
export * from './radiogram-helpers';
export * from './treatment-status-radiogram';
export * from './vehicle-count-radiogram';
+export * from './resource-request-radiogram';
export * from './status';
diff --git a/shared/src/models/radiogram/resource-request-radiogram.ts b/shared/src/models/radiogram/resource-request-radiogram.ts
new file mode 100644
index 000000000..5f911db22
--- /dev/null
+++ b/shared/src/models/radiogram/resource-request-radiogram.ts
@@ -0,0 +1,52 @@
+import { Type } from 'class-transformer';
+import { IsBoolean, IsUUID, ValidateNested } from 'class-validator';
+import { UUID } from '../../utils';
+import { IsValue } from '../../utils/validators';
+import { IsRadiogramStatus } from '../../utils/validators/is-radiogram-status';
+import { getCreate } from '../utils';
+import { VehicleResource } from '../utils/vehicle-resource';
+import type { Radiogram } from './radiogram';
+import { ExerciseRadiogramStatus } from './status/exercise-radiogram-status';
+
+export class ResourceRequestRadiogram implements Radiogram {
+ @IsUUID()
+ readonly id: UUID;
+
+ @IsValue('resourceRequestRadiogram')
+ readonly type = 'resourceRequestRadiogram';
+
+ @IsUUID()
+ readonly simulatedRegionId: UUID;
+
+ /**
+ * @deprecated use the helpers from {@link radiogram-helpers.ts}
+ * or {@link radiogram-helpers-mutable.ts} instead
+ */
+ @IsRadiogramStatus()
+ @ValidateNested()
+ readonly status: ExerciseRadiogramStatus;
+
+ @IsBoolean()
+ readonly informationAvailable: boolean = true;
+
+ @Type(() => VehicleResource)
+ @ValidateNested()
+ readonly requiredResource: VehicleResource;
+
+ /**
+ * @deprecated Use {@link create} instead
+ */
+ constructor(
+ id: UUID,
+ simulatedRegionId: UUID,
+ status: ExerciseRadiogramStatus,
+ requiredResource: VehicleResource
+ ) {
+ this.id = id;
+ this.simulatedRegionId = simulatedRegionId;
+ this.status = status;
+ this.requiredResource = requiredResource;
+ }
+
+ static readonly create = getCreate(this);
+}
diff --git a/shared/src/simulation/events/vehicles-sent.ts b/shared/src/simulation/events/vehicles-sent.ts
index b0d427244..1d518d761 100644
--- a/shared/src/simulation/events/vehicles-sent.ts
+++ b/shared/src/simulation/events/vehicles-sent.ts
@@ -1,5 +1,5 @@
import { Type } from 'class-transformer';
-import { IsUUID } from 'class-validator';
+import { IsUUID, ValidateNested } from 'class-validator';
import { getCreate } from '../../models/utils';
import { VehicleResource } from '../../models/utils/vehicle-resource';
import { UUID, uuidValidationOptions } from '../../utils';
@@ -14,6 +14,7 @@ export class VehiclesSentEvent implements SimulationEvent {
public readonly id: UUID;
@Type(() => VehicleResource)
+ @ValidateNested()
readonly vehiclesSent!: VehicleResource;
/**
From f97fbb38cfa2a8ccfb1554c59027ac071f8e658a Mon Sep 17 00:00:00 2001
From: benn02 <82985280+benn02@users.noreply.github.com>
Date: Thu, 30 Mar 2023 11:23:11 +0200
Subject: [PATCH 14/31] Add Transfer Vehicles Activity (#844)
---
backend/tsconfig.json | 1 +
benchmark/tsconfig.json | 1 +
frontend/tsconfig.json | 1 +
.../exercise-simulation-activity.ts | 2 +
shared/src/simulation/activities/index.ts | 1 +
.../activities/transfer-vehicles.ts | 195 ++++++++++++++++++
shared/tsconfig.json | 1 +
7 files changed, 202 insertions(+)
create mode 100644 shared/src/simulation/activities/transfer-vehicles.ts
diff --git a/backend/tsconfig.json b/backend/tsconfig.json
index 48590ee10..4da78d3a5 100644
--- a/backend/tsconfig.json
+++ b/backend/tsconfig.json
@@ -1,6 +1,7 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
+ "moduleResolution": "node",
"outDir": "./dist",
"rootDir": ".",
"types": ["jest", "node"],
diff --git a/benchmark/tsconfig.json b/benchmark/tsconfig.json
index fd9826f82..f25961042 100644
--- a/benchmark/tsconfig.json
+++ b/benchmark/tsconfig.json
@@ -1,6 +1,7 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
+ "moduleResolution": "node",
"outDir": "./dist",
"rootDir": "./src",
"types": ["node"]
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
index 05e4db227..cf567b9be 100644
--- a/frontend/tsconfig.json
+++ b/frontend/tsconfig.json
@@ -3,6 +3,7 @@
"extends": "../tsconfig.base.json",
"compileOnSave": false,
"compilerOptions": {
+ "moduleResolution": "node",
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"lib": ["ES2022", "dom"],
diff --git a/shared/src/simulation/activities/exercise-simulation-activity.ts b/shared/src/simulation/activities/exercise-simulation-activity.ts
index 682eec1cb..801bab756 100644
--- a/shared/src/simulation/activities/exercise-simulation-activity.ts
+++ b/shared/src/simulation/activities/exercise-simulation-activity.ts
@@ -5,6 +5,7 @@ import { reassignTreatmentsActivity } from './reassign-treatments';
import { unloadVehicleActivity } from './unload-vehicle';
import { recurringEventActivity } from './recurring-event';
import { generateReportActivity } from './generate-report';
+import { transferVehiclesActivity } from './transfer-vehicles';
export const simulationActivities = {
reassignTreatmentsActivity,
@@ -12,6 +13,7 @@ export const simulationActivities = {
delayEventActivity,
recurringEventActivity,
generateReportActivity,
+ transferVehiclesActivity,
};
export type ExerciseSimulationActivity =
diff --git a/shared/src/simulation/activities/index.ts b/shared/src/simulation/activities/index.ts
index 889579a43..3767538f3 100644
--- a/shared/src/simulation/activities/index.ts
+++ b/shared/src/simulation/activities/index.ts
@@ -3,3 +3,4 @@ export * from './exercise-simulation-activity';
export * from './reassign-treatments';
export * from './unload-vehicle';
export * from './recurring-event';
+export * from './transfer-vehicles';
diff --git a/shared/src/simulation/activities/transfer-vehicles.ts b/shared/src/simulation/activities/transfer-vehicles.ts
new file mode 100644
index 000000000..a46d14f95
--- /dev/null
+++ b/shared/src/simulation/activities/transfer-vehicles.ts
@@ -0,0 +1,195 @@
+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 { VehicleResource } from '../../models/utils/vehicle-resource';
+import { TransferActionReducers } from '../../store/action-reducers/transfer';
+import {
+ getElement,
+ getElementByPredicate,
+} from '../../store/action-reducers/utils';
+import { cloneDeepMutable, UUID, uuidValidationOptions } from '../../utils';
+import { IsValue } from '../../utils/validators';
+import { ResourceRequiredEvent, VehiclesSentEvent } 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
+ ) {
+ 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)
+ );
+ const groupedVehicles = groupBy(
+ vehicles,
+ (vehicle) => vehicle.vehicleType
+ );
+
+ if (
+ Object.entries(
+ activityState.vehiclesToBeTransferred.vehicleCounts
+ ).some(
+ ([vehicleType, vehicleCount]) =>
+ (groupedVehicles[vehicleType]?.length ?? 0) <
+ vehicleCount
+ )
+ ) {
+ 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
+ );
+ for (let i = 0; i < sentVehicles[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 (
+ isInSimulatedRegion(targetTransferPoint) &&
+ Object.values(sentVehicles).some((value) => value !== 0)
+ ) {
+ sendSimulationEvent(
+ getElement(
+ draftState,
+ 'simulatedRegion',
+ currentSimulatedRegionIdOf(targetTransferPoint)
+ ),
+ VehiclesSentEvent.create(
+ nextUUID(draftState),
+ VehicleResource.create(sentVehicles)
+ )
+ );
+ }
+
+ terminate();
+ },
+ };
diff --git a/shared/tsconfig.json b/shared/tsconfig.json
index 7c82b6f90..ad777ae55 100644
--- a/shared/tsconfig.json
+++ b/shared/tsconfig.json
@@ -1,6 +1,7 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
+ "moduleResolution": "node",
"outDir": "./dist",
"rootDir": ".",
"types": ["jest"],
From c263575fb689c744c5b657d2996552ff8ef7f0a4 Mon Sep 17 00:00:00 2001
From: Lukas Hagen <43916057+Greenscreen23@users.noreply.github.com>
Date: Thu, 30 Mar 2023 13:50:36 +0200
Subject: [PATCH 15/31] Add vehicle request radiogram component (#845)
* Add vehicle request radiogram component
* Add test scenarios
* Add changelog entry
---
CHANGELOG.md | 1 +
...rd-content-resource-request.component.html | 48 ++++++++++
...rd-content-resource-request.component.scss | 0
...card-content-resource-request.component.ts | 60 +++++++++++++
.../radiogram-card-content.component.html | 4 +
.../simulated-region-overview.module.ts | 2 +
shared/src/models/index.ts | 1 +
shared/src/models/utils/index.ts | 1 +
shared/src/store/action-reducers/radiogram.ts | 88 ++++++++++++++++++-
.../action-reducers/utils/get-element.ts | 20 +++++
test-scenarios | 2 +-
11 files changed, 225 insertions(+), 2 deletions(-)
create mode 100644 frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radigoram-card-content-resource-request/radigoram-card-content-resource-request.component.html
create mode 100644 frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radigoram-card-content-resource-request/radigoram-card-content-resource-request.component.scss
create mode 100644 frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radigoram-card-content-resource-request/radigoram-card-content-resource-request.component.ts
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8fb592f3b..3a5b94aed 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ and this project does **not** adhere to [Semantic Versioning](https://semver.org
- There is now a display for whether a patient is pregnant.
- The patient status display that visualizes the progression of a patient explains its icons via a tooltip
- There is now a radiogram for missing transfer connections and vehicle requests
+ - Radiograms for vehicle requests can also be answered in the user interface, whether they have been accepted or not
### Changed
diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radigoram-card-content-resource-request/radigoram-card-content-resource-request.component.html b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radigoram-card-content-resource-request/radigoram-card-content-resource-request.component.html
new file mode 100644
index 000000000..6d0e9edf8
--- /dev/null
+++ b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radigoram-card-content-resource-request/radigoram-card-content-resource-request.component.html
@@ -0,0 +1,48 @@
+
+ Anfrage von Ressourcen
+
+ 0; else noResourcesRequired"
+ >
+
+
+
+ Kategorie
+ Anzahl
+
+
+
+
+ {{ vehicleType }}
+ {{ requiredResource.vehicleCounts[vehicleType] }}
+
+
+
+
+
+ Ablehnen
+
+
+ Akzeptieren
+
+
+
+
+
+ Es werden keine Ressourcen benötigt.
+
+
diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radigoram-card-content-resource-request/radigoram-card-content-resource-request.component.scss b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radigoram-card-content-resource-request/radigoram-card-content-resource-request.component.scss
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radigoram-card-content-resource-request/radigoram-card-content-resource-request.component.ts b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radigoram-card-content-resource-request/radigoram-card-content-resource-request.component.ts
new file mode 100644
index 000000000..b4a58e98c
--- /dev/null
+++ b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radigoram-card-content-resource-request/radigoram-card-content-resource-request.component.ts
@@ -0,0 +1,60 @@
+import type { OnInit } from '@angular/core';
+import { Component, Input } from '@angular/core';
+import { createSelector, Store } from '@ngrx/store';
+import type {
+ ResourceRequestRadiogram,
+ VehicleResource,
+} from 'digital-fuesim-manv-shared';
+import { UUID, isAccepted } from 'digital-fuesim-manv-shared';
+import type { Observable } from 'rxjs';
+import { ExerciseService } from 'src/app/core/exercise.service';
+import type { AppState } from 'src/app/state/app.state';
+import { createSelectRadiogram } from 'src/app/state/application/selectors/exercise.selectors';
+
+@Component({
+ selector: 'app-radigoram-card-content-resource-request',
+ templateUrl: './radigoram-card-content-resource-request.component.html',
+ styleUrls: ['./radigoram-card-content-resource-request.component.scss'],
+})
+export class RadigoramCardContentResourceRequestComponent implements OnInit {
+ @Input() radiogramId!: UUID;
+
+ requiredResource$!: Observable;
+ enableActionButtons$!: Observable;
+
+ constructor(
+ private readonly store: Store,
+ private readonly exerciseService: ExerciseService
+ ) {}
+
+ acceptRequest() {
+ this.exerciseService.proposeAction({
+ type: '[Radiogram] Accept resource request',
+ radiogramId: this.radiogramId,
+ });
+ }
+
+ denyRequest() {
+ this.exerciseService.proposeAction({
+ type: '[Radiogram] Deny resource request',
+ radiogramId: this.radiogramId,
+ });
+ }
+
+ ngOnInit(): void {
+ const selectRadiogram = createSelectRadiogram(
+ this.radiogramId
+ );
+ this.requiredResource$ = this.store.select(
+ createSelector(
+ selectRadiogram,
+ (radiogram) => radiogram.requiredResource
+ )
+ );
+ this.enableActionButtons$ = this.store.select(
+ createSelector(selectRadiogram, (radiogram) =>
+ isAccepted(radiogram)
+ )
+ );
+ }
+}
diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radiogram-card-content/radiogram-card-content.component.html b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radiogram-card-content/radiogram-card-content.component.html
index 99879c74f..a211acb09 100644
--- a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radiogram-card-content/radiogram-card-content.component.html
+++ b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/radiogram-list/radiogram-card/radiogram-card-content/radiogram-card-content.component.html
@@ -27,6 +27,10 @@
*ngSwitchCase="'missingTransferConnectionRadiogram'"
[radiogramId]="radiogram.id"
>
+
=
{
@@ -49,4 +71,68 @@ export namespace RadiogramActionReducers {
},
rights: 'participant',
};
+
+ export const acceptResourceRequestRadiogramReducer: ActionReducer =
+ {
+ action: AcceptResourceRequestRadiogramAction,
+ reducer: (draftState, { radiogramId }) => {
+ const radiogram = getRadiogramById(
+ draftState,
+ radiogramId,
+ 'resourceRequestRadiogram'
+ );
+ const simulatedRegion = getElement(
+ draftState,
+ 'simulatedRegion',
+ radiogram.simulatedRegionId
+ );
+
+ sendSimulationEvent(
+ simulatedRegion,
+ cloneDeepMutable(
+ VehiclesSentEvent.create(
+ nextUUID(draftState),
+ radiogram.requiredResource
+ )
+ )
+ );
+
+ markRadiogramDone(draftState, radiogramId);
+
+ return draftState;
+ },
+ rights: 'participant',
+ };
+
+ export const denyResourceRequestRadiogramReducer: ActionReducer =
+ {
+ action: DenyResourceRequestRadiogramAction,
+ reducer: (draftState, { radiogramId }) => {
+ const radiogram = getRadiogramById(
+ draftState,
+ radiogramId,
+ 'resourceRequestRadiogram'
+ );
+ const simulatedRegion = getElement(
+ draftState,
+ 'simulatedRegion',
+ radiogram.simulatedRegionId
+ );
+
+ sendSimulationEvent(
+ simulatedRegion,
+ cloneDeepMutable(
+ VehiclesSentEvent.create(
+ nextUUID(draftState),
+ VehicleResource.create({})
+ )
+ )
+ );
+
+ markRadiogramDone(draftState, radiogramId);
+
+ return draftState;
+ },
+ rights: 'participant',
+ };
}
diff --git a/shared/src/store/action-reducers/utils/get-element.ts b/shared/src/store/action-reducers/utils/get-element.ts
index e6c1c69a4..edff9a212 100644
--- a/shared/src/store/action-reducers/utils/get-element.ts
+++ b/shared/src/store/action-reducers/utils/get-element.ts
@@ -1,3 +1,4 @@
+import type { ExerciseRadiogram } from '../../../models/radiogram';
import type {
ExerciseSimulationActivityState,
ExerciseSimulationActivityType,
@@ -54,6 +55,25 @@ export function getElementByPredicate<
return element;
}
+export function getRadiogramById(
+ state: Mutable,
+ radiogramId: UUID,
+ radiogramType: R['type']
+) {
+ const radiogram = state.radiograms[radiogramId];
+ if (!radiogram) {
+ throw new ReducerError(
+ `Radiogram with id ${radiogramId} does not exist`
+ );
+ }
+ if (radiogram.type !== radiogramType) {
+ throw new ReducerError(
+ `Expected radiogram with id ${radiogramId} to be of type ${radiogramType}, but was ${radiogram.type}`
+ );
+ }
+ return radiogram as Mutable;
+}
+
export function getBehaviorById(
state: Mutable,
simulatedRegionId: UUID,
diff --git a/test-scenarios b/test-scenarios
index 4d9cf83fc..98b17187b 160000
--- a/test-scenarios
+++ b/test-scenarios
@@ -1 +1 @@
-Subproject commit 4d9cf83fcf7edba4116baab72dafd1d592835922
+Subproject commit 98b17187bec463d90fcec181bbee8d0308c03f7f
From bc8a00106b406c916329af58d0eb9487e247a2fe Mon Sep 17 00:00:00 2001
From: benn02 <82985280+benn02@users.noreply.github.com>
Date: Thu, 30 Mar 2023 14:45:54 +0200
Subject: [PATCH 16/31] Add Answer Requests Behavior (#846)
---
CHANGELOG.md | 1 +
.../simulated-region-overview.module.ts | 2 +
...ior-answer-vehicle-requests.component.html | 1 +
...ior-answer-vehicle-requests.component.scss | 0
...avior-answer-vehicle-requests.component.ts | 11 ++++
...egion-overview-behavior-tab.component.html | 3 +
.../utils/behavior-to-german-name.pipe.ts | 1 +
.../simulation/behaviors/answer-requests.ts | 64 +++++++++++++++++++
.../behaviors/exercise-simulation-behavior.ts | 2 +
test-scenarios | 2 +-
10 files changed, 86 insertions(+), 1 deletion(-)
create mode 100644 frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/answer-vehicle-requests/simulated-region-overview-behavior-answer-vehicle-requests.component.html
create mode 100644 frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/answer-vehicle-requests/simulated-region-overview-behavior-answer-vehicle-requests.component.scss
create mode 100644 frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/answer-vehicle-requests/simulated-region-overview-behavior-answer-vehicle-requests.component.ts
create mode 100644 shared/src/simulation/behaviors/answer-requests.ts
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3a5b94aed..cc243830c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@ and this project does **not** adhere to [Semantic Versioning](https://semver.org
- There is now a display for how many different variations of a patient template exists.
- There is now a display for whether a patient is pregnant.
- The patient status display that visualizes the progression of a patient explains its icons via a tooltip
+- There is now a behavior that answers vehicle requests from other regions
- There is now a radiogram for missing transfer connections and vehicle requests
- Radiograms for vehicle requests can also be answered in the user interface, whether they have been accepted or not
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 874503e5d..0fc223aa1 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
@@ -39,6 +39,7 @@ import { RadiogramCardContentInformationUnavailableComponent } from './radiogram
import { HumanReadableRadiogramTypePipe } from './radiogram-list/human-readable-radiogram-type.pipe';
import { TreatmentStatusBadgeComponent } from './treatment-status-badge/treatment-status-badge.component';
import { RadigoramCardContentMissingTransferConnectionComponent } from './radiogram-list/radiogram-card/radigoram-card-content-missing-transfer-connection/radigoram-card-content-missing-transfer-connection.component';
+import { SimulatedRegionOverviewBehaviorAnswerVehicleRequestsComponent } from './tabs/behavior-tab/behaviors/answer-vehicle-requests/simulated-region-overview-behavior-answer-vehicle-requests.component';
import { RadigoramCardContentResourceRequestComponent } from './radiogram-list/radiogram-card/radigoram-card-content-resource-request/radigoram-card-content-resource-request.component';
@NgModule({
@@ -71,6 +72,7 @@ import { RadigoramCardContentResourceRequestComponent } from './radiogram-list/r
HumanReadableRadiogramTypePipe,
TreatmentStatusBadgeComponent,
RadigoramCardContentMissingTransferConnectionComponent,
+ SimulatedRegionOverviewBehaviorAnswerVehicleRequestsComponent,
RadigoramCardContentResourceRequestComponent,
],
imports: [
diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/answer-vehicle-requests/simulated-region-overview-behavior-answer-vehicle-requests.component.html b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/answer-vehicle-requests/simulated-region-overview-behavior-answer-vehicle-requests.component.html
new file mode 100644
index 000000000..2a28a0bf6
--- /dev/null
+++ b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/answer-vehicle-requests/simulated-region-overview-behavior-answer-vehicle-requests.component.html
@@ -0,0 +1 @@
+Dieser Bereich beantwortet Anfragen anderer Bereiche nach Fahrzeugen
diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/answer-vehicle-requests/simulated-region-overview-behavior-answer-vehicle-requests.component.scss b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/answer-vehicle-requests/simulated-region-overview-behavior-answer-vehicle-requests.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/answer-vehicle-requests/simulated-region-overview-behavior-answer-vehicle-requests.component.ts b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/answer-vehicle-requests/simulated-region-overview-behavior-answer-vehicle-requests.component.ts
new file mode 100644
index 000000000..19cc08c69
--- /dev/null
+++ b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/answer-vehicle-requests/simulated-region-overview-behavior-answer-vehicle-requests.component.ts
@@ -0,0 +1,11 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'app-simulated-region-overview-behavior-answer-vehicle-requests',
+ templateUrl:
+ './simulated-region-overview-behavior-answer-vehicle-requests.component.html',
+ styleUrls: [
+ './simulated-region-overview-behavior-answer-vehicle-requests.component.scss',
+ ],
+})
+export class SimulatedRegionOverviewBehaviorAnswerVehicleRequestsComponent {}
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 652661ef3..91f88b395 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
@@ -44,6 +44,9 @@
[simulatedRegionId]="simulatedRegion.id"
[reportBehaviorId]="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 d3fb60565..5e6d2b849 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
@@ -9,6 +9,7 @@ const behaviorToGermanNameDictionary: {
treatPatientsBehavior: 'Patienten behandeln',
unloadArrivingVehiclesBehavior: 'Fahrzeuge entladen',
reportBehavior: 'Berichte erstellen',
+ answerRequestsBehavior: 'Fahrzeuganfragen beantworten',
};
@Pipe({
name: 'behaviorToGermanName',
diff --git a/shared/src/simulation/behaviors/answer-requests.ts b/shared/src/simulation/behaviors/answer-requests.ts
new file mode 100644
index 000000000..27f955b1d
--- /dev/null
+++ b/shared/src/simulation/behaviors/answer-requests.ts
@@ -0,0 +1,64 @@
+import { IsUUID } from 'class-validator';
+import { getCreate, isInSpecificSimulatedRegion } from '../../models/utils';
+import { getElementByPredicate } from '../../store/action-reducers/utils';
+import { UUID, uuid } from '../../utils';
+import { IsValue } from '../../utils/validators';
+import { TransferVehiclesActivityState } from '../activities';
+import { addActivity } from '../activities/utils';
+import type { ResourceRequiredEvent } from '../events';
+import { nextUUID } from '../utils/randomness';
+import type {
+ SimulationBehavior,
+ SimulationBehaviorState,
+} from './simulation-behavior';
+
+export class AnswerRequestsBehaviorState implements SimulationBehaviorState {
+ @IsValue('answerRequestsBehavior')
+ readonly type = 'answerRequestsBehavior';
+
+ @IsUUID()
+ public readonly id: UUID = uuid();
+
+ static readonly create = getCreate(this);
+}
+
+export const answerRequestsBehavior: SimulationBehavior =
+ {
+ behaviorState: AnswerRequestsBehaviorState,
+ handleEvent: (draftState, simulatedRegion, _behaviorState, event) => {
+ switch (event.type) {
+ case 'resourceRequiredEvent': {
+ const resourceRequiredEvent =
+ event as ResourceRequiredEvent;
+
+ if (
+ resourceRequiredEvent.requiringSimulatedRegionId !==
+ simulatedRegion.id
+ ) {
+ const requiringSimulatedRegionTransferPoint =
+ getElementByPredicate(
+ draftState,
+ 'transferPoint',
+ (transferPoint) =>
+ isInSpecificSimulatedRegion(
+ transferPoint,
+ resourceRequiredEvent.requiringSimulatedRegionId
+ )
+ );
+ addActivity(
+ simulatedRegion,
+ TransferVehiclesActivityState.create(
+ nextUUID(draftState),
+ requiringSimulatedRegionTransferPoint.id,
+ requiringSimulatedRegionTransferPoint.id,
+ resourceRequiredEvent.requiredResource
+ )
+ );
+ }
+ break;
+ }
+ default:
+ // Ignore event
+ }
+ },
+ };
diff --git a/shared/src/simulation/behaviors/exercise-simulation-behavior.ts b/shared/src/simulation/behaviors/exercise-simulation-behavior.ts
index 6cb916521..12029fc27 100644
--- a/shared/src/simulation/behaviors/exercise-simulation-behavior.ts
+++ b/shared/src/simulation/behaviors/exercise-simulation-behavior.ts
@@ -4,12 +4,14 @@ import { assignLeaderBehavior } from './assign-leader';
import { treatPatientsBehavior } from './treat-patients';
import { unloadArrivingVehiclesBehavior } from './unload-arrived-vehicles';
import { reportBehavior } from './report';
+import { answerRequestsBehavior } from './answer-requests';
export const simulationBehaviors = {
assignLeaderBehavior,
treatPatientsBehavior,
unloadArrivingVehiclesBehavior,
reportBehavior,
+ answerRequestsBehavior,
};
export type ExerciseSimulationBehavior =
diff --git a/test-scenarios b/test-scenarios
index 98b17187b..1c96376fd 160000
--- a/test-scenarios
+++ b/test-scenarios
@@ -1 +1 @@
-Subproject commit 98b17187bec463d90fcec181bbee8d0308c03f7f
+Subproject commit 1c96376fd8b4de55c133435e48a9bc17c6afe43c
From 89d33ebac6f617a2fd45687df37cbc6dbde4a025 Mon Sep 17 00:00:00 2001
From: Lukas Hagen <43916057+Greenscreen23@users.noreply.github.com>
Date: Fri, 31 Mar 2023 12:21:14 +0200
Subject: [PATCH 17/31] Prioritize and load vehicles before transferring them
(#848)
---
.../utils/amount-of-resources-in-vehicle.ts | 27 +++++++++++++++++++
.../activities/transfer-vehicles.ts | 22 ++++++++++++---
2 files changed, 46 insertions(+), 3 deletions(-)
create mode 100644 shared/src/models/utils/amount-of-resources-in-vehicle.ts
diff --git a/shared/src/models/utils/amount-of-resources-in-vehicle.ts b/shared/src/models/utils/amount-of-resources-in-vehicle.ts
new file mode 100644
index 000000000..38c425fc8
--- /dev/null
+++ b/shared/src/models/utils/amount-of-resources-in-vehicle.ts
@@ -0,0 +1,27 @@
+import type { ExerciseState } from '../../state';
+import { getElement } from '../../store/action-reducers/utils';
+import type { Mutable, UUID } from '../../utils';
+import { isInSpecificVehicle } from './position/position-helpers';
+
+export function amountOfResourcesInVehicle(
+ state: Mutable,
+ vehicleId: UUID
+) {
+ const vehicle = getElement(state, 'vehicle', vehicleId);
+ const amountOfPersonnel =
+ Object.keys(vehicle.personnelIds).filter((personnelId) =>
+ isInSpecificVehicle(
+ getElement(state, 'personnel', personnelId),
+ vehicleId
+ )
+ )?.length ?? 0;
+ const amountOfMaterial =
+ Object.keys(vehicle.materialIds).filter((materialId) =>
+ isInSpecificVehicle(
+ getElement(state, 'material', materialId),
+ vehicleId
+ )
+ )?.length ?? 0;
+
+ return amountOfPersonnel + amountOfMaterial;
+}
diff --git a/shared/src/simulation/activities/transfer-vehicles.ts b/shared/src/simulation/activities/transfer-vehicles.ts
index a46d14f95..a552460fa 100644
--- a/shared/src/simulation/activities/transfer-vehicles.ts
+++ b/shared/src/simulation/activities/transfer-vehicles.ts
@@ -13,12 +13,14 @@ import {
isInSpecificSimulatedRegion,
TransferStartPoint,
} from '../../models/utils';
+import { amountOfResourcesInVehicle } from '../../models/utils/amount-of-resources-in-vehicle';
import { VehicleResource } from '../../models/utils/vehicle-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, VehiclesSentEvent } from '../events';
@@ -104,10 +106,13 @@ export const transferVehiclesActivity: SimulationActivity
+ 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
@@ -154,7 +159,18 @@ export const transferVehiclesActivity: SimulationActivity
+ 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',
From f143145fe45f7922632ebaf5277271d387f1601d Mon Sep 17 00:00:00 2001
From: Lukas Hagen <43916057+Greenscreen23@users.noreply.github.com>
Date: Wed, 12 Apr 2023 15:29:06 +0200
Subject: [PATCH 18/31] Fix GHSA-776f-qx25-q3cc (#855)
---
backend/package-lock.json | 140 +++++++++++++++++++++-----------------
1 file changed, 76 insertions(+), 64 deletions(-)
diff --git a/backend/package-lock.json b/backend/package-lock.json
index c5d16e8d6..a0caa06c5 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -1929,7 +1929,8 @@
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
- "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true
},
"node_modules/array-flatten": {
"version": "1.1.1",
@@ -2182,6 +2183,7 @@
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -2584,7 +2586,8 @@
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true
},
"node_modules/content-disposition": {
"version": "0.5.4",
@@ -2661,18 +2664,6 @@
"node": ">= 8"
}
},
- "node_modules/date-fns": {
- "version": "2.29.3",
- "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
- "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==",
- "engines": {
- "node": ">=0.11"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/date-fns"
- }
- },
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@@ -3818,6 +3809,7 @@
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "dev": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@@ -5080,6 +5072,7 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
"dependencies": {
"argparse": "^2.0.1"
},
@@ -5360,6 +5353,7 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
@@ -5377,14 +5371,17 @@
}
},
"node_modules/mkdirp": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
- "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz",
+ "integrity": "sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==",
"bin": {
- "mkdirp": "bin/cmd.js"
+ "mkdirp": "dist/cjs/src/bin.js"
},
"engines": {
"node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/ms": {
@@ -5782,6 +5779,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -6498,11 +6496,6 @@
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
- "node_modules/sax": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
- "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
- },
"node_modules/semver": {
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
@@ -7257,27 +7250,24 @@
}
},
"node_modules/typeorm": {
- "version": "0.3.11",
- "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.11.tgz",
- "integrity": "sha512-pzdOyWbVuz/z8Ww6gqvBW4nylsM0KLdUCDExr2gR20/x1khGSVxQkjNV/3YqliG90jrWzrknYbYscpk8yxFJVg==",
+ "version": "0.3.14",
+ "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.14.tgz",
+ "integrity": "sha512-tEPEN8qmA2a2wmjkaDcWBZ6LsECHofJW2vaCQMklYs+4JRJMAJ5FfbPIWMbhJ3ANJGMtLAmU1GfC8rLFIpbWsg==",
"dependencies": {
- "@sqltools/formatter": "^1.2.2",
- "app-root-path": "^3.0.0",
+ "@sqltools/formatter": "^1.2.5",
+ "app-root-path": "^3.1.0",
"buffer": "^6.0.3",
- "chalk": "^4.1.0",
+ "chalk": "^4.1.2",
"cli-highlight": "^2.1.11",
- "date-fns": "^2.28.0",
- "debug": "^4.3.3",
- "dotenv": "^16.0.0",
- "glob": "^7.2.0",
- "js-yaml": "^4.1.0",
- "mkdirp": "^1.0.4",
+ "debug": "^4.3.4",
+ "dotenv": "^16.0.3",
+ "glob": "^8.1.0",
+ "mkdirp": "^2.1.3",
"reflect-metadata": "^0.1.13",
"sha.js": "^2.4.11",
- "tslib": "^2.3.1",
- "uuid": "^8.3.2",
- "xml2js": "^0.4.23",
- "yargs": "^17.3.1"
+ "tslib": "^2.5.0",
+ "uuid": "^9.0.0",
+ "yargs": "^17.6.2"
},
"bin": {
"typeorm": "cli.js",
@@ -7297,8 +7287,8 @@
"hdb-pool": "^0.1.6",
"ioredis": "^5.0.4",
"mongodb": "^3.6.0",
- "mssql": "^7.3.0",
- "mysql2": "^2.2.5",
+ "mssql": "^9.1.1",
+ "mysql2": "^2.2.5 || ^3.0.1",
"oracledb": "^5.1.0",
"pg": "^8.5.1",
"pg-native": "^3.0.0",
@@ -7363,6 +7353,48 @@
}
}
},
+ "node_modules/typeorm/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/typeorm/node_modules/glob": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
+ "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^5.0.1",
+ "once": "^1.3.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/typeorm/node_modules/minimatch": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
+ "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/typeorm/node_modules/tslib": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
+ "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
+ },
"node_modules/typescript": {
"version": "4.9.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz",
@@ -7449,9 +7481,9 @@
}
},
"node_modules/uuid": {
- "version": "8.3.2",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
- "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
+ "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
"bin": {
"uuid": "dist/bin/uuid"
}
@@ -7611,26 +7643,6 @@
}
}
},
- "node_modules/xml2js": {
- "version": "0.4.23",
- "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
- "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
- "dependencies": {
- "sax": ">=0.6.0",
- "xmlbuilder": "~11.0.0"
- },
- "engines": {
- "node": ">=4.0.0"
- }
- },
- "node_modules/xmlbuilder": {
- "version": "11.0.1",
- "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
- "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
- "engines": {
- "node": ">=4.0"
- }
- },
"node_modules/xmlhttprequest-ssl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
From dd15742c84b44bf487e43b4232726966220a456a Mon Sep 17 00:00:00 2001
From: Lukas Hagen <43916057+Greenscreen23@users.noreply.github.com>
Date: Wed, 12 Apr 2023 15:38:35 +0200
Subject: [PATCH 19/31] Refactoring/resolve circular dependencies (#853)
* Resolve circular dependencies
* Install madge
* Add madge to the pipeline
* Fix madge script
* Introduce circular dependency
* Revert "Introduce circular dependency"
This reverts commit f9fd9b8283d6b53a3deda8ff1d275551f2622c42.
* Add eslint rule for circular dependencies
* Revert "Install madge"
This reverts commit ca0612537205b6a913158bf51c8095ca717dda43.
* Introduce circular dependency
* Revert "Introduce circular dependency"
This reverts commit eb027a3529b294379cc2643363ad7e736991255c.
* Please the prettier
---
.eslint/typescript.eslintrc.cjs | 2 ++
.../application/selectors/shared.selectors.ts | 2 +-
shared/src/data/default-state/patient-templates.ts | 6 +++---
.../src/export-import/file-format/state-export.ts | 4 ++--
shared/src/models/exercise-configuration.ts | 2 +-
shared/src/models/simulated-region.ts | 14 +++++++-------
shared/src/models/utils/spatial-tree.ts | 5 +++--
.../simulation/activities/reassign-treatments.ts | 5 +++--
shared/src/simulation/behaviors/treat-patients.ts | 4 ++--
shared/src/store/action-reducers/transfer.ts | 7 ++++---
.../action-reducers/utils/calculate-treatments.ts | 5 +++--
11 files changed, 31 insertions(+), 25 deletions(-)
diff --git a/.eslint/typescript.eslintrc.cjs b/.eslint/typescript.eslintrc.cjs
index acabc7cd0..d67200fc6 100644
--- a/.eslint/typescript.eslintrc.cjs
+++ b/.eslint/typescript.eslintrc.cjs
@@ -7,6 +7,7 @@ module.exports = {
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
+ 'plugin:import/typescript',
// TODO: doesn't work for some reason from this config and has to be imported separately
// 'prettier',
],
@@ -249,6 +250,7 @@ module.exports = {
*/
'import/no-deprecated': 'warn',
'import/order': 'warn',
+ 'import/no-cycle': 'warn',
/**
* @typescript-eslint
diff --git a/frontend/src/app/state/application/selectors/shared.selectors.ts b/frontend/src/app/state/application/selectors/shared.selectors.ts
index 3fe525754..51ce63dea 100644
--- a/frontend/src/app/state/application/selectors/shared.selectors.ts
+++ b/frontend/src/app/state/application/selectors/shared.selectors.ts
@@ -45,7 +45,7 @@ export const selectOwnClient = createSelector(
);
/**
- * @deprecated Do not use this to distinguish between the exerciseStateModes
+ * Do not use this to distinguish between the exerciseStateModes
*/
export const selectCurrentRole = createSelector(
selectExerciseStateMode,
diff --git a/shared/src/data/default-state/patient-templates.ts b/shared/src/data/default-state/patient-templates.ts
index 231bfce0b..13fe9f033 100644
--- a/shared/src/data/default-state/patient-templates.ts
+++ b/shared/src/data/default-state/patient-templates.ts
@@ -1,9 +1,9 @@
+import { PatientCategory } from '../../models/patient-category';
import {
FunctionParameters,
- PatientCategory,
PatientHealthState,
- PatientTemplate,
-} from '../../models';
+} from '../../models/patient-health-state';
+import { PatientTemplate } from '../../models/patient-template';
import type { ImageProperties } from '../../models/utils';
import { healthPointsDefaults } from '../../models/utils';
diff --git a/shared/src/export-import/file-format/state-export.ts b/shared/src/export-import/file-format/state-export.ts
index eddc3f1a8..570b4bdf7 100644
--- a/shared/src/export-import/file-format/state-export.ts
+++ b/shared/src/export-import/file-format/state-export.ts
@@ -1,10 +1,10 @@
import { Type } from 'class-transformer';
import { IsArray, IsOptional, ValidateNested } from 'class-validator';
import { ExerciseState } from '../../state';
-import type { ExerciseAction } from '../../store';
-import { IsExerciseAction } from '../../store';
import { Mutable } from '../../utils';
import { IsValue } from '../../utils/validators';
+import type { ExerciseAction } from '../../store/action-reducers/action-reducers';
+import { IsExerciseAction } from '../../store/validate-exercise-action';
import { BaseExportImportFile } from './base-file';
export class StateHistoryCompound {
diff --git a/shared/src/models/exercise-configuration.ts b/shared/src/models/exercise-configuration.ts
index dce59e0a9..6d8510b75 100644
--- a/shared/src/models/exercise-configuration.ts
+++ b/shared/src/models/exercise-configuration.ts
@@ -1,7 +1,7 @@
import { Type } from 'class-transformer';
import { IsBoolean, ValidateNested } from 'class-validator';
-import { defaultTileMapProperties } from '../data';
import { IsValue } from '../utils/validators';
+import { defaultTileMapProperties } from '../data/default-state/tile-map-properties';
import { getCreate, TileMapProperties } from './utils';
export class ExerciseConfiguration {
diff --git a/shared/src/models/simulated-region.ts b/shared/src/models/simulated-region.ts
index 65635b67f..f0692bd23 100644
--- a/shared/src/models/simulated-region.ts
+++ b/shared/src/models/simulated-region.ts
@@ -3,14 +3,14 @@ import { IsString, IsUUID, ValidateNested } from 'class-validator';
import { UUID, uuid, uuidValidationOptions } from '../utils';
import { IsPosition } from '../utils/validators/is-position';
import { IsMultiTypedIdMap, IsValue } from '../utils/validators';
-import type { ExerciseSimulationEvent } from '../simulation';
-import { simulationEventTypeOptions } from '../simulation';
-import type { ExerciseSimulationActivityState } from '../simulation/activities';
-import { getSimulationActivityConstructor } from '../simulation/activities';
-import type { ExerciseSimulationBehaviorState } from '../simulation/behaviors';
-import { simulationBehaviorTypeOptions } from '../simulation/behaviors';
-import { getCreate, MapPosition, Position, Size } from './utils';
+import type { ExerciseSimulationEvent } from '../simulation/events/exercise-simulation-event';
+import { simulationEventTypeOptions } from '../simulation/events/exercise-simulation-event';
+import type { ExerciseSimulationActivityState } from '../simulation/activities/exercise-simulation-activity';
+import { getSimulationActivityConstructor } from '../simulation/activities/exercise-simulation-activity';
+import type { ExerciseSimulationBehaviorState } from '../simulation/behaviors/exercise-simulation-behavior';
+import { simulationBehaviorTypeOptions } from '../simulation/behaviors/exercise-simulation-behavior';
import type { ImageProperties, MapCoordinates } from './utils';
+import { getCreate, MapPosition, Position, Size } from './utils';
export class SimulatedRegion {
@IsUUID(4, uuidValidationOptions)
diff --git a/shared/src/models/utils/spatial-tree.ts b/shared/src/models/utils/spatial-tree.ts
index cdb2a30db..1d5041688 100644
--- a/shared/src/models/utils/spatial-tree.ts
+++ b/shared/src/models/utils/spatial-tree.ts
@@ -9,8 +9,9 @@ import RBush from 'rbush';
import knn from 'rbush-knn';
import type { Mutable, UUID, JsonObject } from '../../utils';
import { Immutable } from '../../utils';
-import type { MapCoordinates, Size } from '.';
-import { getCreate } from '.';
+import { getCreate } from './get-create';
+import type { MapCoordinates } from './position/map-coordinates';
+import type { Size } from './size';
/**
* A data structure that enables efficient search of elements (interpreted as points) in a circle or rectangle
diff --git a/shared/src/simulation/activities/reassign-treatments.ts b/shared/src/simulation/activities/reassign-treatments.ts
index 7a342ad61..1d061fff3 100644
--- a/shared/src/simulation/activities/reassign-treatments.ts
+++ b/shared/src/simulation/activities/reassign-treatments.ts
@@ -1,6 +1,4 @@
import { IsInt, IsOptional, IsUUID, Min } from 'class-validator';
-import type { Material, Personnel } from '../../models';
-import { Patient } from '../../models';
import type { PatientStatus, PersonnelType } from '../../models/utils';
import { getCreate, isInSpecificSimulatedRegion } from '../../models/utils';
import type { ExerciseState } from '../../state';
@@ -21,6 +19,9 @@ import {
import { TreatmentProgressChangedEvent } from '../events';
import { sendSimulationEvent } from '../events/utils';
import type { AssignLeaderBehaviorState } from '../behaviors/assign-leader';
+import type { Material } from '../../models/material';
+import type { Personnel } from '../../models/personnel';
+import { Patient } from '../../models/patient';
import type {
SimulationActivity,
SimulationActivityState,
diff --git a/shared/src/simulation/behaviors/treat-patients.ts b/shared/src/simulation/behaviors/treat-patients.ts
index dbe60ce1b..bc5207ed2 100644
--- a/shared/src/simulation/behaviors/treat-patients.ts
+++ b/shared/src/simulation/behaviors/treat-patients.ts
@@ -7,13 +7,11 @@ import {
ValidateNested,
} from 'class-validator';
import { groupBy } from 'lodash-es';
-import { Patient } from '../../models';
import type {
PatientCountRadiogram,
TreatmentStatusRadiogram,
} from '../../models/radiogram';
import { getCreate, isInSpecificSimulatedRegion } from '../../models/utils';
-import type { SimulatedRegion } from '../../models';
import type { ExerciseState } from '../../state';
import { getActivityById } from '../../store/action-reducers/utils';
import type { Mutable } from '../../utils';
@@ -28,6 +26,8 @@ import {
TreatmentProgress,
treatmentProgressAllowedValues,
} from '../utils/treatment';
+import { Patient } from '../../models/patient';
+import type { SimulatedRegion } from '../../models/simulated-region';
import type {
SimulationBehavior,
SimulationBehaviorState,
diff --git a/shared/src/store/action-reducers/transfer.ts b/shared/src/store/action-reducers/transfer.ts
index f7813d398..8e7967ce8 100644
--- a/shared/src/store/action-reducers/transfer.ts
+++ b/shared/src/store/action-reducers/transfer.ts
@@ -1,6 +1,5 @@
import { Type } from 'class-transformer';
import { IsInt, IsOptional, IsUUID, ValidateNested } from 'class-validator';
-import { TransferPoint } from '../../models';
import {
isPositionOnMap,
isInTransfer,
@@ -18,15 +17,17 @@ import {
offsetMapPositionBy,
} from '../../models/utils/position/position-helpers-mutable';
import type { ExerciseState } from '../../state';
-import { imageSizeToPosition } from '../../state-helpers';
import type { Mutable } from '../../utils';
import { cloneDeepMutable, UUID, uuidValidationOptions } from '../../utils';
import type { AllowedValues } from '../../utils/validators';
import { IsLiteralUnion, IsValue } from '../../utils/validators';
import type { Action, ActionReducer } from '../action-reducer';
import { ReducerError } from '../reducer-error';
-import { PersonnelAvailableEvent, VehicleArrivedEvent } from '../../simulation';
import { sendSimulationEvent } from '../../simulation/events/utils';
+import { TransferPoint } from '../../models/transfer-point';
+import { PersonnelAvailableEvent } from '../../simulation/events/personnel-available';
+import { VehicleArrivedEvent } from '../../simulation/events/vehicle-arrived';
+import { imageSizeToPosition } from '../../state-helpers/image-size-to-position';
import { getElement } from './utils';
export type TransferableElementType = 'personnel' | 'vehicle';
diff --git a/shared/src/store/action-reducers/utils/calculate-treatments.ts b/shared/src/store/action-reducers/utils/calculate-treatments.ts
index 4e49b95d9..b340b2561 100644
--- a/shared/src/store/action-reducers/utils/calculate-treatments.ts
+++ b/shared/src/store/action-reducers/utils/calculate-treatments.ts
@@ -1,6 +1,4 @@
import { groupBy } from 'lodash-es';
-import type { Material, Personnel } from '../../../models';
-import { Patient } from '../../../models';
import type { MapCoordinates, PatientStatus } from '../../../models/utils';
import {
currentCoordinatesOf,
@@ -12,6 +10,9 @@ import type { ExerciseState } from '../../../state';
import { maxTreatmentRange } from '../../../state-helpers/max-treatment-range';
import type { Mutable, UUID } from '../../../utils';
import { elementTypePluralMap } from '../../../utils/element-type-plural-map';
+import type { Personnel } from '../../../models/personnel';
+import type { Material } from '../../../models/material';
+import { Patient } from '../../../models/patient';
import { getElement } from './get-element';
// TODO: `caterFor` and `treat` are currently used as synonyms without a clear distinction.
From 91df21719852e3f9909fc93c1675b992cd9d7969 Mon Sep 17 00:00:00 2001
From: Lukas Radermacher <49586507+lukasrad02@users.noreply.github.com>
Date: Thu, 13 Apr 2023 15:18:30 +0200
Subject: [PATCH 20/31] Add UUID version and options to all validators (#859)
---
.../src/models/radiogram/material-count-radiogram.ts | 6 +++---
.../radiogram/missing-transfer-connection-radiogram.ts | 8 ++++----
shared/src/models/radiogram/patient-count-radiogram.ts | 6 +++---
.../src/models/radiogram/personnel-count-radiogram.ts | 6 +++---
.../src/models/radiogram/resource-request-radiogram.ts | 6 +++---
.../src/models/radiogram/treatment-status-radiogram.ts | 6 +++---
shared/src/models/radiogram/vehicle-count-radiogram.ts | 6 +++---
.../models/utils/position/simulated-region-position.ts | 4 ++--
shared/src/models/utils/position/vehicle-position.ts | 4 ++--
shared/src/simulation/behaviors/answer-requests.ts | 4 ++--
shared/src/simulation/behaviors/report.ts | 10 ++++++++--
11 files changed, 36 insertions(+), 30 deletions(-)
diff --git a/shared/src/models/radiogram/material-count-radiogram.ts b/shared/src/models/radiogram/material-count-radiogram.ts
index 801dfb731..420ad3d8f 100644
--- a/shared/src/models/radiogram/material-count-radiogram.ts
+++ b/shared/src/models/radiogram/material-count-radiogram.ts
@@ -1,6 +1,6 @@
import { Type } from 'class-transformer';
import { IsBoolean, IsUUID, ValidateNested } from 'class-validator';
-import { UUID } from '../../utils';
+import { UUID, uuidValidationOptions } from '../../utils';
import { IsValue } from '../../utils/validators';
import { IsRadiogramStatus } from '../../utils/validators/is-radiogram-status';
import { CanCaterFor, getCreate } from '../utils';
@@ -8,13 +8,13 @@ import type { Radiogram } from './radiogram';
import { ExerciseRadiogramStatus } from './status/exercise-radiogram-status';
export class MaterialCountRadiogram implements Radiogram {
- @IsUUID()
+ @IsUUID(4, uuidValidationOptions)
readonly id: UUID;
@IsValue('materialCountRadiogram')
readonly type = 'materialCountRadiogram';
- @IsUUID()
+ @IsUUID(4, uuidValidationOptions)
readonly simulatedRegionId: UUID;
/**
diff --git a/shared/src/models/radiogram/missing-transfer-connection-radiogram.ts b/shared/src/models/radiogram/missing-transfer-connection-radiogram.ts
index b846feeee..ef84c031a 100644
--- a/shared/src/models/radiogram/missing-transfer-connection-radiogram.ts
+++ b/shared/src/models/radiogram/missing-transfer-connection-radiogram.ts
@@ -1,5 +1,5 @@
import { IsBoolean, IsUUID, ValidateNested } from 'class-validator';
-import { UUID } from '../../utils';
+import { UUID, uuidValidationOptions } from '../../utils';
import { IsValue } from '../../utils/validators';
import { IsRadiogramStatus } from '../../utils/validators/is-radiogram-status';
import { getCreate } from '../utils';
@@ -7,13 +7,13 @@ import type { Radiogram } from './radiogram';
import { ExerciseRadiogramStatus } from './status/exercise-radiogram-status';
export class MissingTransferConnectionRadiogram implements Radiogram {
- @IsUUID()
+ @IsUUID(4, uuidValidationOptions)
readonly id: UUID;
@IsValue('missingTransferConnectionRadiogram')
readonly type = 'missingTransferConnectionRadiogram';
- @IsUUID()
+ @IsUUID(4, uuidValidationOptions)
readonly simulatedRegionId: UUID;
/**
@@ -27,7 +27,7 @@ export class MissingTransferConnectionRadiogram implements Radiogram {
@IsBoolean()
readonly informationAvailable: boolean = true;
- @IsUUID()
+ @IsUUID(4, uuidValidationOptions)
readonly targetTransferPointId: UUID;
/**
diff --git a/shared/src/models/radiogram/patient-count-radiogram.ts b/shared/src/models/radiogram/patient-count-radiogram.ts
index 9257a82c3..03a223b56 100644
--- a/shared/src/models/radiogram/patient-count-radiogram.ts
+++ b/shared/src/models/radiogram/patient-count-radiogram.ts
@@ -1,5 +1,5 @@
import { IsBoolean, IsUUID, ValidateNested } from 'class-validator';
-import { UUID } from '../../utils';
+import { UUID, uuidValidationOptions } from '../../utils';
import { IsValue } from '../../utils/validators';
import { IsPatientCount } from '../../utils/validators/is-patient-count';
import { IsRadiogramStatus } from '../../utils/validators/is-radiogram-status';
@@ -11,13 +11,13 @@ import { ExerciseRadiogramStatus } from './status/exercise-radiogram-status';
export type PatientCount = { [key in PatientStatus]: number };
export class PatientCountRadiogram implements Radiogram {
- @IsUUID()
+ @IsUUID(4, uuidValidationOptions)
readonly id: UUID;
@IsValue('patientCountRadiogram')
readonly type = 'patientCountRadiogram';
- @IsUUID()
+ @IsUUID(4, uuidValidationOptions)
readonly simulatedRegionId: UUID;
/**
diff --git a/shared/src/models/radiogram/personnel-count-radiogram.ts b/shared/src/models/radiogram/personnel-count-radiogram.ts
index 7f12a3582..1f9fe18f2 100644
--- a/shared/src/models/radiogram/personnel-count-radiogram.ts
+++ b/shared/src/models/radiogram/personnel-count-radiogram.ts
@@ -1,5 +1,5 @@
import { IsBoolean, IsUUID, ValidateNested } from 'class-validator';
-import { UUID } from '../../utils';
+import { UUID, uuidValidationOptions } from '../../utils';
import { IsValue } from '../../utils/validators';
import { IsPersonnelCount } from '../../utils/validators/is-personnel-count';
import { IsRadiogramStatus } from '../../utils/validators/is-radiogram-status';
@@ -11,13 +11,13 @@ import { ExerciseRadiogramStatus } from './status/exercise-radiogram-status';
export type PersonnelCount = { [key in PersonnelType]: number };
export class PersonnelCountRadiogram implements Radiogram {
- @IsUUID()
+ @IsUUID(4, uuidValidationOptions)
readonly id: UUID;
@IsValue('personnelCountRadiogram')
readonly type = 'personnelCountRadiogram';
- @IsUUID()
+ @IsUUID(4, uuidValidationOptions)
readonly simulatedRegionId: UUID;
/**
diff --git a/shared/src/models/radiogram/resource-request-radiogram.ts b/shared/src/models/radiogram/resource-request-radiogram.ts
index 5f911db22..eb5024614 100644
--- a/shared/src/models/radiogram/resource-request-radiogram.ts
+++ b/shared/src/models/radiogram/resource-request-radiogram.ts
@@ -1,6 +1,6 @@
import { Type } from 'class-transformer';
import { IsBoolean, IsUUID, ValidateNested } from 'class-validator';
-import { UUID } from '../../utils';
+import { UUID, uuidValidationOptions } from '../../utils';
import { IsValue } from '../../utils/validators';
import { IsRadiogramStatus } from '../../utils/validators/is-radiogram-status';
import { getCreate } from '../utils';
@@ -9,13 +9,13 @@ import type { Radiogram } from './radiogram';
import { ExerciseRadiogramStatus } from './status/exercise-radiogram-status';
export class ResourceRequestRadiogram implements Radiogram {
- @IsUUID()
+ @IsUUID(4, uuidValidationOptions)
readonly id: UUID;
@IsValue('resourceRequestRadiogram')
readonly type = 'resourceRequestRadiogram';
- @IsUUID()
+ @IsUUID(4, uuidValidationOptions)
readonly simulatedRegionId: UUID;
/**
diff --git a/shared/src/models/radiogram/treatment-status-radiogram.ts b/shared/src/models/radiogram/treatment-status-radiogram.ts
index 71216dbca..6755bb06e 100644
--- a/shared/src/models/radiogram/treatment-status-radiogram.ts
+++ b/shared/src/models/radiogram/treatment-status-radiogram.ts
@@ -3,7 +3,7 @@ import {
TreatmentProgress,
treatmentProgressAllowedValues,
} from '../../simulation/utils/treatment';
-import { UUID } from '../../utils';
+import { UUID, uuidValidationOptions } from '../../utils';
import { IsLiteralUnion, IsValue } from '../../utils/validators';
import { IsRadiogramStatus } from '../../utils/validators/is-radiogram-status';
import { getCreate } from '../utils';
@@ -11,13 +11,13 @@ import type { Radiogram } from './radiogram';
import { ExerciseRadiogramStatus } from './status/exercise-radiogram-status';
export class TreatmentStatusRadiogram implements Radiogram {
- @IsUUID()
+ @IsUUID(4, uuidValidationOptions)
readonly id: UUID;
@IsValue('treatmentStatusRadiogram')
readonly type = 'treatmentStatusRadiogram';
- @IsUUID()
+ @IsUUID(4, uuidValidationOptions)
readonly simulatedRegionId: UUID;
/**
diff --git a/shared/src/models/radiogram/vehicle-count-radiogram.ts b/shared/src/models/radiogram/vehicle-count-radiogram.ts
index 2847e8b1f..b0c44df1e 100644
--- a/shared/src/models/radiogram/vehicle-count-radiogram.ts
+++ b/shared/src/models/radiogram/vehicle-count-radiogram.ts
@@ -1,5 +1,5 @@
import { IsBoolean, IsUUID, ValidateNested } from 'class-validator';
-import { UUID } from '../../utils';
+import { UUID, uuidValidationOptions } from '../../utils';
import { IsValue } from '../../utils/validators';
import { IsRadiogramStatus } from '../../utils/validators/is-radiogram-status';
import { IsVehicleCount } from '../../utils/validators/is-vehicle-count';
@@ -11,13 +11,13 @@ import { ExerciseRadiogramStatus } from './status/exercise-radiogram-status';
export type VehicleCount = { [key: string]: number };
export class VehicleCountRadiogram implements Radiogram {
- @IsUUID()
+ @IsUUID(4, uuidValidationOptions)
readonly id: UUID;
@IsValue('vehicleCountRadiogram')
readonly type = 'vehicleCountRadiogram';
- @IsUUID()
+ @IsUUID(4, uuidValidationOptions)
readonly simulatedRegionId: UUID;
/**
diff --git a/shared/src/models/utils/position/simulated-region-position.ts b/shared/src/models/utils/position/simulated-region-position.ts
index fc1dc9fc6..a81847bd8 100644
--- a/shared/src/models/utils/position/simulated-region-position.ts
+++ b/shared/src/models/utils/position/simulated-region-position.ts
@@ -1,5 +1,5 @@
import { IsUUID } from 'class-validator';
-import { UUID } from '../../../utils';
+import { UUID, uuidValidationOptions } from '../../../utils';
import { IsValue } from '../../../utils/validators';
import { getCreate } from '../get-create';
import {
@@ -24,7 +24,7 @@ export class SimulatedRegionPosition {
/**
* @deprecated Use {@link currentSimulatedRegionIdOf } instead
*/
- @IsUUID()
+ @IsUUID(4, uuidValidationOptions)
public readonly simulatedRegionId: UUID;
/**
diff --git a/shared/src/models/utils/position/vehicle-position.ts b/shared/src/models/utils/position/vehicle-position.ts
index 4f91a3e0b..71b4c9a30 100644
--- a/shared/src/models/utils/position/vehicle-position.ts
+++ b/shared/src/models/utils/position/vehicle-position.ts
@@ -1,5 +1,5 @@
import { IsUUID } from 'class-validator';
-import { UUID } from '../../../utils';
+import { UUID, uuidValidationOptions } from '../../../utils';
import { IsValue } from '../../../utils/validators';
import { getCreate } from '../get-create';
import {
@@ -24,7 +24,7 @@ export class VehiclePosition {
/**
* @deprecated Use {@link currentVehicleIdOf } instead
*/
- @IsUUID()
+ @IsUUID(4, uuidValidationOptions)
public readonly vehicleId: UUID;
/**
diff --git a/shared/src/simulation/behaviors/answer-requests.ts b/shared/src/simulation/behaviors/answer-requests.ts
index 27f955b1d..af3131c87 100644
--- a/shared/src/simulation/behaviors/answer-requests.ts
+++ b/shared/src/simulation/behaviors/answer-requests.ts
@@ -1,7 +1,7 @@
import { IsUUID } from 'class-validator';
import { getCreate, isInSpecificSimulatedRegion } from '../../models/utils';
import { getElementByPredicate } from '../../store/action-reducers/utils';
-import { UUID, uuid } from '../../utils';
+import { UUID, uuid, uuidValidationOptions } from '../../utils';
import { IsValue } from '../../utils/validators';
import { TransferVehiclesActivityState } from '../activities';
import { addActivity } from '../activities/utils';
@@ -16,7 +16,7 @@ export class AnswerRequestsBehaviorState implements SimulationBehaviorState {
@IsValue('answerRequestsBehavior')
readonly type = 'answerRequestsBehavior';
- @IsUUID()
+ @IsUUID(4, uuidValidationOptions)
public readonly id: UUID = uuid();
static readonly create = getCreate(this);
diff --git a/shared/src/simulation/behaviors/report.ts b/shared/src/simulation/behaviors/report.ts
index 570be5918..3c23d5576 100644
--- a/shared/src/simulation/behaviors/report.ts
+++ b/shared/src/simulation/behaviors/report.ts
@@ -1,7 +1,13 @@
import { isUUID, IsUUID } from 'class-validator';
import { RadiogramUnpublishedStatus } from '../../models/radiogram/status/radiogram-unpublished-status';
import { getCreate } from '../../models/utils';
-import { cloneDeepMutable, StrictObject, UUID, uuid } from '../../utils';
+import {
+ cloneDeepMutable,
+ StrictObject,
+ UUID,
+ uuid,
+ uuidValidationOptions,
+} from '../../utils';
import { IsLiteralUnionMap, IsValue } from '../../utils/validators';
import { GenerateReportActivityState } from '../activities/generate-report';
import { CollectInformationEvent } from '../events/collect';
@@ -20,7 +26,7 @@ export class ReportBehaviorState implements SimulationBehaviorState {
@IsValue('reportBehavior')
readonly type = 'reportBehavior';
- @IsUUID()
+ @IsUUID(4, uuidValidationOptions)
public readonly id: UUID = uuid();
@IsLiteralUnionMap(reportableInformationAllowedValues, ((value) =>
From 310d40ff35ef176ea5c0058bcb3ac262a2879b3c Mon Sep 17 00:00:00 2001
From: Lukas Hagen <43916057+Greenscreen23@users.noreply.github.com>
Date: Fri, 14 Apr 2023 13:22:30 +0200
Subject: [PATCH 21/31] Feature/831 create request behavior (#854)
* Implement request behavior
* Fix change detection in html
* Add validation
* Set activityId when creating activity
* Wait for the answer of a request before sending a new one
* Add workaround to combat infinite waiting for responses
* Simplify state logic
* Make state changes explicit and fix some bugs
* Invalide promises when they did not arrive in time
* Fix bug found by tests
* Rename helper functions
* Fix error due to name collision
* Fix name collision
* Add tests for the behavior
* Appease linter
* Add migration
* Add changelog entry
* Add special resource promise type
* Remove debug statements
* Fix bug where promises would not correctly be invalidated after some time if there were no new requests
* Update datasets for migrations and fix frontend visual bug
* Rename settings to only include intervals
* Completely refactor the testing structure
* Fix cyclic dependencies
* Revert "Add migration"
This reverts commit b8841085acbeeb2f75e23b4e4e059cca51a1b634.
---
CHANGELOG.md | 1 +
.../simulated-region-overview.module.ts | 2 +
...w-behavior-request-vehicles.component.html | 99 ++
...w-behavior-request-vehicles.component.scss | 0
...iew-behavior-request-vehicles.component.ts | 164 ++++
...egion-overview-behavior-tab.component.html | 5 +
.../utils/behavior-to-german-name.pipe.ts | 1 +
.../radiogram/resource-request-radiogram.ts | 11 +-
.../status/exercise-radiogram-status.ts | 2 +-
.../radiogram/status/radiogram-done-status.ts | 2 +-
.../status/radiogram-unpublished-status.ts | 2 +-
.../status/radiogram-unread-status.ts | 2 +-
shared/src/models/utils/index.ts | 1 +
.../request-target/exercise-request-target.ts | 46 +
.../src/models/utils/request-target/index.ts | 3 +
.../utils/request-target/request-target.ts | 18 +
.../utils/request-target/simulated-region.ts | 57 ++
.../models/utils/request-target/trainees.ts | 45 +
shared/src/models/utils/vehicle-resource.ts | 30 +
.../simulation/activities/create-request.ts | 76 ++
.../exercise-simulation-activity.ts | 2 +
.../activities/transfer-vehicles.ts | 3 +-
.../behaviors/exercise-simulation-behavior.ts | 2 +
shared/src/simulation/behaviors/index.ts | 1 +
.../src/simulation/behaviors/request.spec.ts | 893 ++++++++++++++++++
shared/src/simulation/behaviors/request.ts | 255 +++++
.../events/exercise-simulation-event.ts | 3 +
.../simulation/events/resources-required.ts | 2 +-
shared/src/simulation/events/send-request.ts | 10 +
shared/src/simulation/events/vehicles-sent.ts | 8 +-
shared/src/simulation/utils/randomness.ts | 2 +-
.../src/simulation/utils/resource-promise.ts | 28 +
shared/src/simulation/utils/simulation.ts | 2 +-
shared/src/store/action-reducers/radiogram.ts | 26 +-
.../src/store/action-reducers/simulation.ts | 140 ++-
shared/src/utils/validators/is-id-map.ts | 34 +-
shared/src/utils/validators/is-string-map.ts | 54 ++
test-scenarios | 2 +-
38 files changed, 1990 insertions(+), 44 deletions(-)
create mode 100644 frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/request-vehicles/simulated-region-overview-behavior-request-vehicles.component.html
create mode 100644 frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/request-vehicles/simulated-region-overview-behavior-request-vehicles.component.scss
create mode 100644 frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/request-vehicles/simulated-region-overview-behavior-request-vehicles.component.ts
create mode 100644 shared/src/models/utils/request-target/exercise-request-target.ts
create mode 100644 shared/src/models/utils/request-target/index.ts
create mode 100644 shared/src/models/utils/request-target/request-target.ts
create mode 100644 shared/src/models/utils/request-target/simulated-region.ts
create mode 100644 shared/src/models/utils/request-target/trainees.ts
create mode 100644 shared/src/simulation/activities/create-request.ts
create mode 100644 shared/src/simulation/behaviors/request.spec.ts
create mode 100644 shared/src/simulation/behaviors/request.ts
create mode 100644 shared/src/simulation/events/send-request.ts
create mode 100644 shared/src/simulation/utils/resource-promise.ts
create mode 100644 shared/src/utils/validators/is-string-map.ts
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cc243830c..d990ea5a7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ and this project does **not** adhere to [Semantic Versioning](https://semver.org
- There is now a display for whether a patient is pregnant.
- The patient status display that visualizes the progression of a patient explains its icons via a tooltip
- There is now a behavior that answers vehicle requests from other regions
+- There is now a behavior to forward requests to other simulated regions or the trainees
- There is now a radiogram for missing transfer connections and vehicle requests
- Radiograms for vehicle requests can also be answered in the user interface, whether they have been accepted or not
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 0fc223aa1..1b0e28d9b 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
@@ -41,6 +41,7 @@ import { TreatmentStatusBadgeComponent } from './treatment-status-badge/treatmen
import { RadigoramCardContentMissingTransferConnectionComponent } from './radiogram-list/radiogram-card/radigoram-card-content-missing-transfer-connection/radigoram-card-content-missing-transfer-connection.component';
import { SimulatedRegionOverviewBehaviorAnswerVehicleRequestsComponent } from './tabs/behavior-tab/behaviors/answer-vehicle-requests/simulated-region-overview-behavior-answer-vehicle-requests.component';
import { RadigoramCardContentResourceRequestComponent } from './radiogram-list/radiogram-card/radigoram-card-content-resource-request/radigoram-card-content-resource-request.component';
+import { RequestVehiclesComponent } from './tabs/behavior-tab/behaviors/request-vehicles/simulated-region-overview-behavior-request-vehicles.component';
@NgModule({
declarations: [
@@ -74,6 +75,7 @@ import { RadigoramCardContentResourceRequestComponent } from './radiogram-list/r
RadigoramCardContentMissingTransferConnectionComponent,
SimulatedRegionOverviewBehaviorAnswerVehicleRequestsComponent,
RadigoramCardContentResourceRequestComponent,
+ RequestVehiclesComponent,
],
imports: [
CommonModule,
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.html b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/request-vehicles/simulated-region-overview-behavior-request-vehicles.component.html
new file mode 100644
index 000000000..a927c584d
--- /dev/null
+++ b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/request-vehicles/simulated-region-overview-behavior-request-vehicles.component.html
@@ -0,0 +1,99 @@
+
+ Anfragenziel
+
+
+ Wartet auf die Antwort einer Anfrage von
+ Nächste Anfrage potentiell in
+ {{ nextTimeoutIn.time | date : 'mm:ss' }} an
+
+
+
+
+
+
+ {{ requestTargetOptions[option] }}
+
+
+
+
+
+
+
Zeitintervalle
+
+
+ Minimaler Zeitabstand zwischen mehreren Anfragen
+
+
+
+
+
+ Dauer, nachdem eine nicht eingelöste Zusage von Fahrzeugen
+ invalidiert werden soll
+
+
+
+
+
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.scss b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/request-vehicles/simulated-region-overview-behavior-request-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/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
new file mode 100644
index 000000000..33b086fb4
--- /dev/null
+++ 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
@@ -0,0 +1,164 @@
+import type { OnChanges } from '@angular/core';
+import { Component, Input } from '@angular/core';
+import { Store } from '@ngrx/store';
+import type {
+ RecurringEventActivityState,
+ RequestBehaviorState,
+} from 'digital-fuesim-manv-shared';
+import {
+ isWaitingForAnswer,
+ UUID,
+ SimulatedRegionRequestTargetConfiguration,
+ TraineesRequestTargetConfiguration,
+} from 'digital-fuesim-manv-shared';
+import type { Observable } from 'rxjs';
+import { map, combineLatest } from 'rxjs';
+import { ExerciseService } from 'src/app/core/exercise.service';
+import type { AppState } from 'src/app/state/app.state';
+import {
+ createSelectActivityStates,
+ createSelectBehaviorState,
+ selectSimulatedRegions,
+ selectCurrentTime,
+} from 'src/app/state/application/selectors/exercise.selectors';
+
+type RequestTargetOption = UUID | 'trainees';
+
+@Component({
+ selector: 'app-simulated-region-overview-behavior-request-vehicles',
+ templateUrl:
+ './simulated-region-overview-behavior-request-vehicles.component.html',
+ styleUrls: [
+ './simulated-region-overview-behavior-request-vehicles.component.scss',
+ ],
+})
+export class RequestVehiclesComponent implements OnChanges {
+ @Input() simulatedRegionId!: UUID;
+ @Input() requestBehaviorId!: UUID;
+
+ requestBehaviorState$!: Observable;
+
+ requestTargetOptions$!: Observable<{
+ [key in RequestTargetOption]: string;
+ }>;
+
+ waitingForAnswer$!: Observable;
+
+ nextTimeoutIn$!: Observable;
+
+ selectedRequestTarget$!: Observable;
+
+ constructor(
+ private readonly store: Store,
+ private readonly exerciseService: ExerciseService
+ ) {}
+
+ ngOnChanges(): void {
+ this.requestTargetOptions$ = this.store
+ .select(selectSimulatedRegions)
+ .pipe(
+ map((simulatedRegions) => {
+ const options = Object.entries(simulatedRegions)
+ .map(([id, simulatedRegion]) => [
+ id,
+ simulatedRegion.name,
+ ])
+ .filter(([id, _name]) => id !== this.simulatedRegionId)
+ .sort(([id1, name1], [id2, name2]) =>
+ name1 === name2 ? 0 : name1! < name2! ? -1 : 1
+ );
+ options.unshift(['trainees', 'Die Trainierenden']);
+ return Object.fromEntries(options);
+ })
+ );
+
+ this.requestBehaviorState$ = this.store.select(
+ createSelectBehaviorState(
+ this.simulatedRegionId,
+ this.requestBehaviorId
+ )
+ );
+
+ this.selectedRequestTarget$ = this.requestBehaviorState$.pipe(
+ map((requestBehaviorState) => {
+ if (
+ requestBehaviorState.requestTarget.type ===
+ 'traineesRequestTarget'
+ ) {
+ return 'trainees';
+ }
+ return requestBehaviorState.requestTarget
+ .targetSimulatedRegionId;
+ })
+ );
+
+ this.waitingForAnswer$ = this.requestBehaviorState$.pipe(
+ map((requestBehaviorState) =>
+ isWaitingForAnswer(requestBehaviorState)
+ )
+ );
+
+ const activities$ = this.store.select(
+ createSelectActivityStates(this.simulatedRegionId)
+ );
+
+ const currentTime$ = this.store.select(selectCurrentTime);
+
+ this.nextTimeoutIn$ = combineLatest([
+ this.requestBehaviorState$,
+ activities$,
+ currentTime$,
+ ]).pipe(
+ map(([requestBehaviorState, activities, currentTime]) => {
+ if (!requestBehaviorState.recurringEventActivityId)
+ return undefined;
+ const recurringEventActivityState = activities[
+ requestBehaviorState.recurringEventActivityId
+ ] as RecurringEventActivityState;
+ if (!recurringEventActivityState) return undefined;
+
+ return Math.max(
+ recurringEventActivityState.lastOccurrenceTime +
+ recurringEventActivityState.recurrenceIntervalTime -
+ currentTime,
+ 0
+ );
+ })
+ );
+ }
+
+ updateRequestInterval(interval: number) {
+ this.exerciseService.proposeAction({
+ type: '[RequestBehavior] Update RequestInterval',
+ simulatedRegionId: this.simulatedRegionId,
+ behaviorId: this.requestBehaviorId,
+ requestInterval: interval,
+ });
+ }
+
+ updatePromiseInterval(interval: number) {
+ this.exerciseService.proposeAction({
+ type: '[RequestBehavior] Update Promise invalidation interval',
+ simulatedRegionId: this.simulatedRegionId,
+ behaviorId: this.requestBehaviorId,
+ promiseInvalidationInterval: interval,
+ });
+ }
+
+ updateRequestTarget(requestTarget: RequestTargetOption) {
+ let requestTargetConfiguration;
+ if (requestTarget === 'trainees') {
+ requestTargetConfiguration =
+ TraineesRequestTargetConfiguration.create();
+ } else {
+ requestTargetConfiguration =
+ SimulatedRegionRequestTargetConfiguration.create(requestTarget);
+ }
+ this.exerciseService.proposeAction({
+ type: '[RequestBehavior] Update RequestTarget',
+ simulatedRegionId: this.simulatedRegionId,
+ behaviorId: this.requestBehaviorId,
+ requestTarget: requestTargetConfiguration,
+ });
+ }
+}
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 91f88b395..6422d00c0 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
@@ -47,6 +47,11 @@
+
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 5e6d2b849..a90d41c5d 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
@@ -10,6 +10,7 @@ const behaviorToGermanNameDictionary: {
unloadArrivingVehiclesBehavior: 'Fahrzeuge entladen',
reportBehavior: 'Berichte erstellen',
answerRequestsBehavior: 'Fahrzeuganfragen beantworten',
+ requestBehavior: 'Fahrzeuge anfordern',
};
@Pipe({
name: 'behaviorToGermanName',
diff --git a/shared/src/models/radiogram/resource-request-radiogram.ts b/shared/src/models/radiogram/resource-request-radiogram.ts
index eb5024614..6844783e4 100644
--- a/shared/src/models/radiogram/resource-request-radiogram.ts
+++ b/shared/src/models/radiogram/resource-request-radiogram.ts
@@ -1,9 +1,9 @@
import { Type } from 'class-transformer';
-import { IsBoolean, IsUUID, ValidateNested } from 'class-validator';
+import { IsBoolean, IsString, IsUUID, ValidateNested } from 'class-validator';
import { UUID, uuidValidationOptions } from '../../utils';
import { IsValue } from '../../utils/validators';
import { IsRadiogramStatus } from '../../utils/validators/is-radiogram-status';
-import { getCreate } from '../utils';
+import { getCreate } from '../utils/get-create';
import { VehicleResource } from '../utils/vehicle-resource';
import type { Radiogram } from './radiogram';
import { ExerciseRadiogramStatus } from './status/exercise-radiogram-status';
@@ -33,6 +33,9 @@ export class ResourceRequestRadiogram implements Radiogram {
@ValidateNested()
readonly requiredResource: VehicleResource;
+ @IsString()
+ readonly key: string;
+
/**
* @deprecated Use {@link create} instead
*/
@@ -40,12 +43,14 @@ export class ResourceRequestRadiogram implements Radiogram {
id: UUID,
simulatedRegionId: UUID,
status: ExerciseRadiogramStatus,
- requiredResource: VehicleResource
+ requiredResource: VehicleResource,
+ key: string
) {
this.id = id;
this.simulatedRegionId = simulatedRegionId;
this.status = status;
this.requiredResource = requiredResource;
+ this.key = key;
}
static readonly create = getCreate(this);
diff --git a/shared/src/models/radiogram/status/exercise-radiogram-status.ts b/shared/src/models/radiogram/status/exercise-radiogram-status.ts
index e9f792bc7..08b014cd3 100644
--- a/shared/src/models/radiogram/status/exercise-radiogram-status.ts
+++ b/shared/src/models/radiogram/status/exercise-radiogram-status.ts
@@ -1,5 +1,5 @@
import type { Type } from 'class-transformer';
-import type { Constructor } from '../../../utils';
+import type { Constructor } from '../../../utils/constructor';
import { RadiogramAcceptedStatus } from './radiogram-accepted-status';
import { RadiogramDoneStatus } from './radiogram-done-status';
import { RadiogramStatus } from './radiogram-status';
diff --git a/shared/src/models/radiogram/status/radiogram-done-status.ts b/shared/src/models/radiogram/status/radiogram-done-status.ts
index 8d15d9dd4..b2cb5b180 100644
--- a/shared/src/models/radiogram/status/radiogram-done-status.ts
+++ b/shared/src/models/radiogram/status/radiogram-done-status.ts
@@ -1,6 +1,6 @@
import { IsInt, Min } from 'class-validator';
import { IsValue } from '../../../utils/validators';
-import { getCreate } from '../../utils';
+import { getCreate } from '../../utils/get-create';
import type { RadiogramStatus } from './radiogram-status';
export class RadiogramDoneStatus implements RadiogramStatus {
diff --git a/shared/src/models/radiogram/status/radiogram-unpublished-status.ts b/shared/src/models/radiogram/status/radiogram-unpublished-status.ts
index 260cd0d18..72ddcc875 100644
--- a/shared/src/models/radiogram/status/radiogram-unpublished-status.ts
+++ b/shared/src/models/radiogram/status/radiogram-unpublished-status.ts
@@ -1,5 +1,5 @@
import { IsValue } from '../../../utils/validators';
-import { getCreate } from '../../utils';
+import { getCreate } from '../../utils/get-create';
import type { RadiogramStatus } from './radiogram-status';
export class RadiogramUnpublishedStatus implements RadiogramStatus {
diff --git a/shared/src/models/radiogram/status/radiogram-unread-status.ts b/shared/src/models/radiogram/status/radiogram-unread-status.ts
index a8cd06c9f..cacfec90a 100644
--- a/shared/src/models/radiogram/status/radiogram-unread-status.ts
+++ b/shared/src/models/radiogram/status/radiogram-unread-status.ts
@@ -1,6 +1,6 @@
import { IsInt, Min } from 'class-validator';
import { IsValue } from '../../../utils/validators';
-import { getCreate } from '../../utils';
+import { getCreate } from '../../utils/get-create';
import type { RadiogramStatus } from './radiogram-status';
export class RadiogramUnreadStatus implements RadiogramStatus {
diff --git a/shared/src/models/utils/index.ts b/shared/src/models/utils/index.ts
index c036676e9..728dcb9b0 100644
--- a/shared/src/models/utils/index.ts
+++ b/shared/src/models/utils/index.ts
@@ -24,3 +24,4 @@ export * from './patient-status-code';
export * from './start-points';
export * from './spatial-tree';
export * from './vehicle-resource';
+export * from './request-target';
diff --git a/shared/src/models/utils/request-target/exercise-request-target.ts b/shared/src/models/utils/request-target/exercise-request-target.ts
new file mode 100644
index 000000000..743835cb9
--- /dev/null
+++ b/shared/src/models/utils/request-target/exercise-request-target.ts
@@ -0,0 +1,46 @@
+import type { Type } from 'class-transformer';
+import { simulatedRegionRequestTarget } from './simulated-region';
+import { traineesRequestTarget } from './trainees';
+import { RequestTargetConfiguration } from './request-target';
+
+export const requestTargets = {
+ simulatedRegionRequestTarget,
+ traineesRequestTarget,
+};
+
+export type ExerciseRequestTarget =
+ (typeof requestTargets)[keyof typeof requestTargets];
+
+export type ExerciseRequestTargetType = InstanceType<
+ ExerciseRequestTarget['configuration']
+>['type'];
+
+type ExerciseRequestTargetDictionary = {
+ [Target in ExerciseRequestTarget as InstanceType<
+ Target['configuration']
+ >['type']]: Target;
+};
+
+export type ExerciseRequestTargetConfiguration<
+ T extends ExerciseRequestTargetType = ExerciseRequestTargetType
+> = InstanceType;
+
+export const requestTargetDictionary = Object.fromEntries(
+ Object.values(requestTargets).map((target) => [
+ new target.configuration().type,
+ target,
+ ])
+) as ExerciseRequestTargetDictionary;
+
+export const requestTargetTypeOptions: Parameters = [
+ () => RequestTargetConfiguration,
+ {
+ keepDiscriminatorProperty: true,
+ discriminator: {
+ property: 'type',
+ subTypes: Object.entries(requestTargetDictionary).map(
+ ([name, value]) => ({ name, value: value.configuration })
+ ),
+ },
+ },
+];
diff --git a/shared/src/models/utils/request-target/index.ts b/shared/src/models/utils/request-target/index.ts
new file mode 100644
index 000000000..3a72761f7
--- /dev/null
+++ b/shared/src/models/utils/request-target/index.ts
@@ -0,0 +1,3 @@
+export * from './exercise-request-target';
+export * from './trainees';
+export * from './simulated-region';
diff --git a/shared/src/models/utils/request-target/request-target.ts b/shared/src/models/utils/request-target/request-target.ts
new file mode 100644
index 000000000..daadb7884
--- /dev/null
+++ b/shared/src/models/utils/request-target/request-target.ts
@@ -0,0 +1,18 @@
+import type { VehicleResource } from '../../../models';
+import type { ExerciseState } from '../../../state';
+import type { Constructor, Mutable, UUID } from '../../../utils';
+
+export class RequestTargetConfiguration {
+ public readonly type!: `${string}RequestTarget`;
+}
+
+export interface RequestTarget {
+ readonly configuration: Constructor;
+ readonly createRequest: (
+ draftState: Mutable,
+ requestingSimulatedRegionId: UUID,
+ configuration: Mutable,
+ requestedResource: Mutable,
+ key: string
+ ) => void;
+}
diff --git a/shared/src/models/utils/request-target/simulated-region.ts b/shared/src/models/utils/request-target/simulated-region.ts
new file mode 100644
index 000000000..0124263aa
--- /dev/null
+++ b/shared/src/models/utils/request-target/simulated-region.ts
@@ -0,0 +1,57 @@
+import { IsUUID } from 'class-validator';
+import { UUID } from '../../../utils/uuid';
+import { IsValue } from '../../../utils/validators/is-value';
+import { getCreate } from '../../../models/utils/get-create';
+import { getElement } from '../../../store/action-reducers/utils/get-element';
+import { sendSimulationEvent } from '../../../simulation/events/utils';
+import { ResourceRequiredEvent } from '../../../simulation/events/resources-required';
+import { nextUUID } from '../../../simulation/utils/randomness';
+import type {
+ RequestTarget,
+ RequestTargetConfiguration,
+} from './request-target';
+
+export class SimulatedRegionRequestTargetConfiguration
+ implements RequestTargetConfiguration
+{
+ @IsValue('simulatedRegionRequestTarget')
+ public readonly type = 'simulatedRegionRequestTarget';
+
+ @IsUUID()
+ public readonly targetSimulatedRegionId: UUID;
+
+ /**
+ * @deprecated Use {@link create} instead
+ */
+ constructor(targetSimulatedRegionId: UUID) {
+ this.targetSimulatedRegionId = targetSimulatedRegionId;
+ }
+
+ static readonly create = getCreate(this);
+}
+
+export const simulatedRegionRequestTarget: RequestTarget =
+ {
+ configuration: SimulatedRegionRequestTargetConfiguration,
+ createRequest: (
+ draftState,
+ requestingSimulatedRegionId,
+ configuration,
+ requestedResource,
+ key
+ ) => {
+ sendSimulationEvent(
+ getElement(
+ draftState,
+ 'simulatedRegion',
+ configuration.targetSimulatedRegionId
+ ),
+ ResourceRequiredEvent.create(
+ nextUUID(draftState),
+ requestingSimulatedRegionId,
+ requestedResource,
+ key
+ )
+ );
+ },
+ };
diff --git a/shared/src/models/utils/request-target/trainees.ts b/shared/src/models/utils/request-target/trainees.ts
new file mode 100644
index 000000000..afe25717d
--- /dev/null
+++ b/shared/src/models/utils/request-target/trainees.ts
@@ -0,0 +1,45 @@
+import { IsValue } from '../../../utils/validators/is-value';
+import { cloneDeepMutable } from '../../../utils/clone-deep';
+import { getCreate } from '../../../models/utils/get-create';
+import { RadiogramUnpublishedStatus } from '../../../models/radiogram/status/radiogram-unpublished-status';
+import { publishRadiogram } from '../../../models/radiogram/radiogram-helpers-mutable';
+import { nextUUID } from '../../../simulation/utils/randomness';
+import { ResourceRequestRadiogram } from '../../radiogram/resource-request-radiogram';
+import type {
+ RequestTarget,
+ RequestTargetConfiguration,
+} from './request-target';
+
+export class TraineesRequestTargetConfiguration
+ implements RequestTargetConfiguration
+{
+ @IsValue('traineesRequestTarget')
+ public readonly type = 'traineesRequestTarget';
+
+ static readonly create = getCreate(this);
+}
+
+export const traineesRequestTarget: RequestTarget =
+ {
+ configuration: TraineesRequestTargetConfiguration,
+ createRequest: (
+ draftState,
+ requestingSimulatedRegionId,
+ _configuration,
+ requestedResource,
+ key
+ ) => {
+ publishRadiogram(
+ draftState,
+ cloneDeepMutable(
+ ResourceRequestRadiogram.create(
+ nextUUID(draftState),
+ requestingSimulatedRegionId,
+ RadiogramUnpublishedStatus.create(),
+ requestedResource,
+ key
+ )
+ )
+ );
+ },
+ };
diff --git a/shared/src/models/utils/vehicle-resource.ts b/shared/src/models/utils/vehicle-resource.ts
index fd4d11ae7..29ecbe8ab 100644
--- a/shared/src/models/utils/vehicle-resource.ts
+++ b/shared/src/models/utils/vehicle-resource.ts
@@ -1,3 +1,5 @@
+import type { Mutable } from '../../utils';
+import { cloneDeepMutable } from '../../utils';
import { IsValue } from '../../utils/validators';
import { IsResourceDescription } from '../../utils/validators/is-resource-description';
import { getCreate } from './get-create';
@@ -18,3 +20,31 @@ export class VehicleResource {
static readonly create = getCreate(this);
}
+
+export function aggregateResources(
+ resources: Mutable[]
+): Mutable {
+ return resources.reduce((total, current) => {
+ Object.entries(current.vehicleCounts).forEach(([type, count]) => {
+ if (!total.vehicleCounts[type]) total.vehicleCounts[type] = 0;
+ total.vehicleCounts[type] += count;
+ });
+ return total;
+ }, cloneDeepMutable(VehicleResource.create({})));
+}
+
+export function subtractResources(
+ minuend: VehicleResource,
+ subtrahend: VehicleResource
+): Mutable {
+ const result = cloneDeepMutable(minuend);
+ Object.entries(subtrahend.vehicleCounts).forEach(([type, count]) => {
+ if (!(type in result.vehicleCounts)) return;
+ if (result.vehicleCounts[type]! <= count) {
+ delete result.vehicleCounts[type];
+ } else {
+ result.vehicleCounts[type] -= count;
+ }
+ });
+ return result;
+}
diff --git a/shared/src/simulation/activities/create-request.ts b/shared/src/simulation/activities/create-request.ts
new file mode 100644
index 000000000..a39e33562
--- /dev/null
+++ b/shared/src/simulation/activities/create-request.ts
@@ -0,0 +1,76 @@
+import { IsString, IsUUID, ValidateNested } from 'class-validator';
+import { Type } from 'class-transformer';
+import { UUID, uuidValidationOptions } from '../../utils';
+import { IsValue } from '../../utils/validators';
+import type { RequestTarget } from '../../models/utils/request-target/request-target';
+import {
+ ExerciseRequestTargetConfiguration,
+ requestTargetDictionary,
+ requestTargetTypeOptions,
+} from '../../models/utils/request-target/exercise-request-target';
+import { VehicleResource } from '../../models/utils/vehicle-resource';
+import { getCreate } from '../../models/utils/get-create';
+import type {
+ SimulationActivity,
+ SimulationActivityState,
+} from './simulation-activity';
+
+export class CreateRequestActivityState implements SimulationActivityState {
+ @IsUUID(4, uuidValidationOptions)
+ public readonly id: UUID;
+
+ @IsValue('createRequestActivity')
+ public readonly type = 'createRequestActivity';
+
+ @Type(...requestTargetTypeOptions)
+ @ValidateNested()
+ public readonly targetConfiguration: ExerciseRequestTargetConfiguration;
+
+ @Type(() => VehicleResource)
+ @ValidateNested()
+ public readonly requestedResource: VehicleResource;
+
+ @IsString()
+ public readonly key: string;
+
+ /**
+ * @deprecated Use {@link create} instead
+ */
+ constructor(
+ id: UUID,
+ target: ExerciseRequestTargetConfiguration,
+ requestedResource: VehicleResource,
+ key: string
+ ) {
+ this.id = id;
+ this.targetConfiguration = target;
+ this.requestedResource = requestedResource;
+ this.key = key;
+ }
+
+ static readonly create = getCreate(this);
+}
+
+export const createRequestActivity: SimulationActivity =
+ {
+ activityState: CreateRequestActivityState,
+ tick: (
+ draftState,
+ simulatedRegion,
+ activityState,
+ _tickInterval,
+ terminate
+ ) => {
+ const requestTarget = requestTargetDictionary[
+ activityState.targetConfiguration.type
+ ] as RequestTarget;
+ requestTarget.createRequest(
+ draftState,
+ simulatedRegion.id,
+ activityState.targetConfiguration,
+ activityState.requestedResource,
+ activityState.key
+ );
+ terminate();
+ },
+ };
diff --git a/shared/src/simulation/activities/exercise-simulation-activity.ts b/shared/src/simulation/activities/exercise-simulation-activity.ts
index 801bab756..1bbb86d95 100644
--- a/shared/src/simulation/activities/exercise-simulation-activity.ts
+++ b/shared/src/simulation/activities/exercise-simulation-activity.ts
@@ -6,6 +6,7 @@ import { unloadVehicleActivity } from './unload-vehicle';
import { recurringEventActivity } from './recurring-event';
import { generateReportActivity } from './generate-report';
import { transferVehiclesActivity } from './transfer-vehicles';
+import { createRequestActivity } from './create-request';
export const simulationActivities = {
reassignTreatmentsActivity,
@@ -14,6 +15,7 @@ export const simulationActivities = {
recurringEventActivity,
generateReportActivity,
transferVehiclesActivity,
+ createRequestActivity,
};
export type ExerciseSimulationActivity =
diff --git a/shared/src/simulation/activities/transfer-vehicles.ts b/shared/src/simulation/activities/transfer-vehicles.ts
index a552460fa..be87f8b64 100644
--- a/shared/src/simulation/activities/transfer-vehicles.ts
+++ b/shared/src/simulation/activities/transfer-vehicles.ts
@@ -201,7 +201,8 @@ export const transferVehiclesActivity: SimulationActivity,
+ simulatedRegion: Mutable,
+ behaviorState: Mutable
+ ) => void,
+ initializeRequestsAndPromises: (
+ state: Mutable,
+ simulatedRegion: Mutable,
+ behaviorState: Mutable
+ ) => void,
+ interaction: (
+ state: Mutable,
+ simulatedRegion: Mutable,
+ behaviorState: Mutable
+ ) => void
+) {
+ const simulatedRegion = SimulatedRegion.create(
+ MapCoordinates.create(0, 0),
+ Size.create(10, 10),
+ 'test region'
+ );
+ const transferPoint = TransferPoint.create(
+ SimulatedRegionPosition.create(simulatedRegion.id),
+ {},
+ {},
+ '',
+ `[Simuliert] test region`
+ );
+
+ const beforeState = produce(emptyState, (draftState) => {
+ draftState.simulatedRegions[simulatedRegion.id] =
+ cloneDeepMutable(simulatedRegion);
+ draftState.simulatedRegions[simulatedRegion.id]?.behaviors.push(
+ cloneDeepMutable(RequestBehaviorState.create())
+ );
+ draftState.transferPoints[transferPoint.id] =
+ cloneDeepMutable(transferPoint);
+
+ draftState.currentTime = currentTime;
+
+ const mutableSimulatedRegion =
+ draftState.simulatedRegions[simulatedRegion.id]!;
+ const behaviorState = mutableSimulatedRegion
+ .behaviors[0] as Mutable;
+ initializeBehaviorState(
+ draftState,
+ mutableSimulatedRegion,
+ behaviorState
+ );
+ initializeRequestsAndPromises(
+ draftState,
+ mutableSimulatedRegion,
+ behaviorState
+ );
+ });
+
+ const afterState = produce(beforeState, (draftState) => {
+ const mutableSimulatedRegion =
+ draftState.simulatedRegions[simulatedRegion.id]!;
+ interaction(
+ draftState,
+ mutableSimulatedRegion,
+ mutableSimulatedRegion.behaviors[0] as Mutable
+ );
+ handleSimulationEvents(draftState, mutableSimulatedRegion);
+ });
+
+ const beforeSimulatedRegion =
+ beforeState.simulatedRegions[simulatedRegion.id]!;
+ const afterSimulatedRegion =
+ afterState.simulatedRegions[simulatedRegion.id]!;
+ const beforeBehaviorState = beforeSimulatedRegion
+ .behaviors[0] as RequestBehaviorState;
+ const afterBehaviorState = afterSimulatedRegion
+ .behaviors[0] as RequestBehaviorState;
+ return {
+ beforeState,
+ afterState,
+ beforeSimulatedRegion,
+ afterSimulatedRegion,
+ beforeBehaviorState,
+ afterBehaviorState,
+ };
+}
+
+const updateRequestInterval = (
+ draftState: Mutable,
+ simulatedRegion: Mutable,
+ behaviorState: Mutable
+) => {
+ updateBehaviorsRequestInterval(
+ draftState,
+ simulatedRegion,
+ behaviorState,
+ newRequestInterval
+ );
+};
+
+const updateRequestTarget = (
+ draftState: Mutable,
+ simulatedRegion: Mutable,
+ behaviorState: Mutable
+) => {
+ const otherSimulatedRegion = cloneDeepMutable(
+ SimulatedRegion.create(
+ MapCoordinates.create(0, 0),
+ Size.create(10, 10),
+ 'requestable region'
+ )
+ );
+ const transferPoint = TransferPoint.create(
+ SimulatedRegionPosition.create(otherSimulatedRegion.id),
+ {},
+ {},
+ '',
+ `[Simuliert] requestable region`
+ );
+ draftState.transferPoints[transferPoint.id] =
+ cloneDeepMutable(transferPoint);
+ draftState.simulatedRegions[otherSimulatedRegion.id] =
+ cloneDeepMutable(otherSimulatedRegion);
+ updateBehaviorsRequestTarget(
+ draftState,
+ simulatedRegion,
+ behaviorState,
+ SimulatedRegionRequestTargetConfiguration.create(
+ otherSimulatedRegion.id
+ )
+ );
+};
+
+const updateInvalidationInterval = (
+ draftState: Mutable,
+ simulatedRegion: Mutable,
+ behaviorState: Mutable
+) => {
+ behaviorState.invalidatePromiseInterval = newInvalidationInterval;
+ // update its promised resources
+ getResourcesToRequest(draftState, simulatedRegion, behaviorState);
+};
+
+// factories
+const setBehaviorState = {
+ onTimer: (
+ draftState: Mutable,
+ simulatedRegion: Mutable,
+ behaviorState: Mutable
+ ) => {
+ behaviorState.recurringEventActivityId = uuid();
+ addActivity(
+ simulatedRegion,
+ RecurringEventActivityState.create(
+ behaviorState.recurringEventActivityId,
+ SendRequestEvent.create(),
+ draftState.currentTime,
+ behaviorState.requestInterval
+ )
+ );
+ },
+ waiting: (
+ draftState: Mutable,
+ simulatedRegion: Mutable,
+ behaviorState: Mutable
+ ) => {
+ behaviorState.recurringEventActivityId = uuid();
+ addActivity(
+ simulatedRegion,
+ RecurringEventActivityState.create(
+ behaviorState.recurringEventActivityId,
+ SendRequestEvent.create(),
+ draftState.currentTime,
+ behaviorState.requestInterval
+ )
+ );
+ behaviorState.answerKey = `${simulatedRegion.id}-request-${behaviorState.requestTargetVersion}`;
+ },
+};
+
+const addRequestsAndPromises = {
+ withoutRequestsAndPromises: (
+ draftState: Mutable,
+ simulatedRegion: Mutable,
+ behaviorState: Mutable
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
+ ) => {},
+ withRequests: (
+ draftState: Mutable,
+ simulatedRegion: Mutable,
+ behaviorState: Mutable
+ ) => {
+ behaviorState.requestedResources[requestKey] = cloneDeepMutable(
+ VehicleResource.create({
+ KTW: 1,
+ })
+ );
+ },
+ withPromises: (
+ draftState: Mutable,
+ simulatedRegion: Mutable,
+ behaviorState: Mutable
+ ) => {
+ behaviorState.promisedResources = cloneDeepMutable([
+ ResourcePromise.create(
+ draftState.currentTime,
+ VehicleResource.create({ KTW: 1 })
+ ),
+ ]);
+ },
+ withOldPromises: (
+ draftState: Mutable,
+ simulatedRegion: Mutable,
+ behaviorState: Mutable
+ ) => {
+ behaviorState.promisedResources = cloneDeepMutable([
+ ResourcePromise.create(oldTime, VehicleResource.create({ KTW: 1 })),
+ ]);
+ },
+ withOldAndNewPromises: (
+ draftState: Mutable,
+ simulatedRegion: Mutable,
+ behaviorState: Mutable
+ ) => {
+ behaviorState.promisedResources = cloneDeepMutable([
+ ResourcePromise.create(oldTime, VehicleResource.create({ KTW: 1 })),
+ ResourcePromise.create(
+ draftState.currentTime,
+ VehicleResource.create({ KTW: 1 })
+ ),
+ ]);
+ },
+ withRequestsAndEnoughPromises: (
+ draftState: Mutable,
+ simulatedRegion: Mutable,
+ behaviorState: Mutable
+ ) => {
+ behaviorState.requestedResources[requestKey] = cloneDeepMutable(
+ VehicleResource.create({
+ KTW: 1,
+ })
+ );
+ behaviorState.promisedResources = cloneDeepMutable([
+ ResourcePromise.create(
+ draftState.currentTime,
+ VehicleResource.create({ KTW: 1 })
+ ),
+ ]);
+ },
+ withRequestsAndNotEnoughPromises: (
+ draftState: Mutable,
+ simulatedRegion: Mutable,
+ behaviorState: Mutable
+ ) => {
+ behaviorState.requestedResources[requestKey] = cloneDeepMutable(
+ VehicleResource.create({
+ KTW: 2,
+ })
+ );
+ behaviorState.promisedResources = cloneDeepMutable([
+ ResourcePromise.create(
+ draftState.currentTime,
+ VehicleResource.create({ KTW: 1 })
+ ),
+ ]);
+ },
+ withPromiseOfOtherType: (
+ draftState: Mutable,
+ simulatedRegion: Mutable,
+ behaviorState: Mutable
+ ) => {
+ behaviorState.promisedResources = cloneDeepMutable([
+ ResourcePromise.create(
+ draftState.currentTime,
+ VehicleResource.create({ RTW: 1 })
+ ),
+ ]);
+ },
+ withPromisesOfMultipleTypes: (
+ draftState: Mutable,
+ simulatedRegion: Mutable,
+ behaviorState: Mutable
+ ) => {
+ behaviorState.promisedResources = cloneDeepMutable([
+ ResourcePromise.create(
+ draftState.currentTime,
+ VehicleResource.create({ KTW: 1 })
+ ),
+ ResourcePromise.create(
+ draftState.currentTime,
+ VehicleResource.create({ RTW: 1 })
+ ),
+ ]);
+ },
+};
+
+const sendEvent = {
+ resourceRequiredEvent: (
+ draftState: Mutable,
+ simulatedRegion: Mutable,
+ behaviorState: Mutable
+ ) => {
+ sendSimulationEvent(
+ simulatedRegion,
+ ResourceRequiredEvent.create(
+ uuid(),
+ simulatedRegion.id,
+ VehicleResource.create({ KTW: 1 }),
+ 'new-request-key'
+ )
+ );
+ },
+ resourceRequiredEventWithKnownKey: (
+ draftState: Mutable,
+ simulatedRegion: Mutable,
+ behaviorState: Mutable
+ ) => {
+ sendSimulationEvent(
+ simulatedRegion,
+ ResourceRequiredEvent.create(
+ uuid(),
+ simulatedRegion.id,
+ VehicleResource.create({ KTW: 1 }),
+ requestKey
+ )
+ );
+ },
+ vehiclesSendEventForAnswerKey: (
+ draftState: Mutable,
+ simulatedRegion: Mutable,
+ behaviorState: Mutable
+ ) => {
+ sendSimulationEvent(
+ simulatedRegion,
+ VehiclesSentEvent.create(
+ uuid(),
+ VehicleResource.create({ KTW: 1 }),
+ `${simulatedRegion.id}-request-${behaviorState.requestTargetVersion}`
+ )
+ );
+ },
+ vehiclesSendEventForOtherKey: (
+ draftState: Mutable,
+ simulatedRegion: Mutable,
+ behaviorState: Mutable
+ ) => {
+ sendSimulationEvent(
+ simulatedRegion,
+ VehiclesSentEvent.create(
+ uuid(),
+ VehicleResource.create({ KTW: 1 }),
+ 'other-key'
+ )
+ );
+ },
+ ktwVehicleArrivedEvent: (
+ draftState: Mutable,
+ simulatedRegion: Mutable,
+ behaviorState: Mutable
+ ) => {
+ const vehicle = Vehicle.create(
+ 'KTW',
+ 'KTW 1',
+ {},
+ 0,
+ ImageProperties.create('', 0, 0),
+ SimulatedRegionPosition.create(simulatedRegion.id)
+ );
+ draftState.vehicles[vehicle.id] = cloneDeepMutable(vehicle);
+
+ sendSimulationEvent(
+ simulatedRegion,
+ VehicleArrivedEvent.create(vehicle.id, draftState.currentTime)
+ );
+ },
+ sendRequestEvent: (
+ draftState: Mutable,
+ simulatedRegion: Mutable,
+ behaviorState: Mutable
+ ) => {
+ sendSimulationEvent(simulatedRegion, SendRequestEvent.create());
+ },
+};
+
+// assertion helpers
+function assertSameState(
+ beforeBehaviorState: RequestBehaviorState,
+ afterBehaviorState: RequestBehaviorState
+) {
+ expect(afterBehaviorState.answerKey).toEqual(beforeBehaviorState.answerKey);
+}
+
+function assertWaitingState(behaviorState: RequestBehaviorState) {
+ expect(isWaitingForAnswer(behaviorState)).toBe(true);
+}
+function assertNotWaitingState(behaviorState: RequestBehaviorState) {
+ expect(isWaitingForAnswer(behaviorState)).toBe(false);
+}
+
+// tests
+describe('request behavior', () => {
+ describe('on a resource required event', () => {
+ describe.each(StrictObject.keys(addRequestsAndPromises))(
+ '%s',
+ (requestsAndPromises) => {
+ describe.each(StrictObject.keys(setBehaviorState))(
+ 'in %s state',
+ (state) => {
+ it('should note the request', () => {
+ const { afterBehaviorState } =
+ setupStateAndInteract(
+ setBehaviorState[state],
+ addRequestsAndPromises[requestsAndPromises],
+ sendEvent.resourceRequiredEvent
+ );
+
+ expect(
+ afterBehaviorState.requestedResources[
+ 'new-request-key'
+ ]
+ ).toEqual(VehicleResource.create({ KTW: 1 }));
+ });
+
+ it('should not change its state', () => {
+ const { beforeBehaviorState, afterBehaviorState } =
+ setupStateAndInteract(
+ setBehaviorState[state],
+ addRequestsAndPromises[requestsAndPromises],
+ sendEvent.resourceRequiredEvent
+ );
+
+ assertSameState(
+ beforeBehaviorState,
+ afterBehaviorState
+ );
+ });
+ }
+ );
+ }
+ );
+ });
+
+ describe('on a resource required event with a known key', () => {
+ describe.each(StrictObject.keys(addRequestsAndPromises))(
+ '%s',
+ (requestsAndPromises) => {
+ describe.each(StrictObject.keys(setBehaviorState))(
+ 'in %s state',
+ (state) => {
+ it('should overwrite any existing requests', () => {
+ const { afterBehaviorState } =
+ setupStateAndInteract(
+ setBehaviorState[state],
+ addRequestsAndPromises[requestsAndPromises],
+ sendEvent.resourceRequiredEventWithKnownKey
+ );
+
+ expect(
+ Object.keys(
+ afterBehaviorState.requestedResources
+ ).length
+ ).toBe(1);
+ });
+ }
+ );
+ }
+ );
+ });
+
+ describe.each(vehicleSendEvents)('on a %s', (event) => {
+ describe.each(StrictObject.keys(addRequestsAndPromises))(
+ '%s',
+ (requestsAndPromises) => {
+ describe.each(StrictObject.keys(setBehaviorState))(
+ 'in %s state',
+ (state) => {
+ it('should note the promise', () => {
+ const { afterBehaviorState } =
+ setupStateAndInteract(
+ setBehaviorState[state],
+ addRequestsAndPromises[requestsAndPromises],
+ sendEvent[event]
+ );
+
+ const promisedResources =
+ afterBehaviorState.promisedResources;
+ expect(
+ promisedResources.length
+ ).toBeGreaterThanOrEqual(1);
+ const promise = promisedResources.at(-1)!;
+ expect(promise.resource).toEqual(
+ VehicleResource.create({ KTW: 1 })
+ );
+ });
+ }
+ );
+
+ it('should not start waiting for an answer', () => {
+ const { afterBehaviorState } = setupStateAndInteract(
+ setBehaviorState.onTimer,
+ addRequestsAndPromises[requestsAndPromises],
+ sendEvent[event]
+ );
+
+ assertNotWaitingState(afterBehaviorState);
+ });
+ }
+ );
+ });
+
+ describe('on a vehiclesSendEventForAnswerKey', () => {
+ describe.each(StrictObject.keys(addRequestsAndPromises))(
+ '%s',
+ (requestsAndPromises) => {
+ describe('in waiting state', () => {
+ it('should not continue waiting for an answer', () => {
+ const { afterBehaviorState } = setupStateAndInteract(
+ setBehaviorState.waiting,
+ addRequestsAndPromises[requestsAndPromises],
+ sendEvent.vehiclesSendEventForAnswerKey
+ );
+
+ assertNotWaitingState(afterBehaviorState);
+ });
+ });
+ }
+ );
+ });
+
+ describe('on a vehiclesSendEventForOtherKey', () => {
+ describe.each(StrictObject.keys(addRequestsAndPromises))(
+ '%s',
+ (requestsAndPromises) => {
+ describe('in waiting state', () => {
+ it('should continue waiting for an answer', () => {
+ const { afterBehaviorState } = setupStateAndInteract(
+ setBehaviorState.waiting,
+ addRequestsAndPromises[requestsAndPromises],
+ sendEvent.vehiclesSendEventForOtherKey
+ );
+
+ assertWaitingState(afterBehaviorState);
+ });
+ });
+ }
+ );
+ });
+
+ describe('on a ktw vehicle arrived event', () => {
+ describe.each(withoutKTWPromise)('%s', (requestsAndPromises) => {
+ describe.each(StrictObject.keys(setBehaviorState))(
+ 'in %s state',
+ (state) => {
+ it('should not change its noted promises', () => {
+ const { beforeBehaviorState, afterBehaviorState } =
+ setupStateAndInteract(
+ setBehaviorState[state],
+ addRequestsAndPromises[requestsAndPromises],
+ sendEvent.ktwVehicleArrivedEvent
+ );
+
+ expect(afterBehaviorState.promisedResources).toEqual(
+ beforeBehaviorState.promisedResources
+ );
+ });
+ }
+ );
+ });
+
+ describe.each(withOneKTWPromised)('%s', (requestsAndPromises) => {
+ describe.each(StrictObject.keys(setBehaviorState))(
+ 'in %s state',
+ (state) => {
+ it('should remove the promise', () => {
+ const { afterBehaviorState } = setupStateAndInteract(
+ setBehaviorState[state],
+ addRequestsAndPromises[requestsAndPromises],
+ sendEvent.ktwVehicleArrivedEvent
+ );
+
+ expect(
+ afterBehaviorState.promisedResources.find(
+ (promise) =>
+ 'KTW' in promise.resource.vehicleCounts
+ )
+ ).toBeUndefined();
+ });
+ }
+ );
+ });
+ });
+
+ describe('on a send request event', () => {
+ describe.each(withOneKTWRequired)('%s', (requestsAndPromises) => {
+ describe('in onTimer State', () => {
+ it('should move to the waiting state', () => {
+ const { afterBehaviorState } = setupStateAndInteract(
+ setBehaviorState.onTimer,
+ addRequestsAndPromises[requestsAndPromises],
+ sendEvent.sendRequestEvent
+ );
+
+ assertWaitingState(afterBehaviorState);
+ });
+
+ it('should create a request via an activity', () => {
+ const { afterSimulatedRegion, afterBehaviorState } =
+ setupStateAndInteract(
+ setBehaviorState.onTimer,
+ addRequestsAndPromises[requestsAndPromises],
+ sendEvent.sendRequestEvent
+ );
+
+ const activities = afterSimulatedRegion.activities;
+ expect(
+ StrictObject.keys(activities).length
+ ).toBeGreaterThanOrEqual(1);
+
+ const activity = StrictObject.values(activities).find(
+ (a) => a.type === 'createRequestActivity'
+ );
+ expect(activity).toBeDefined();
+
+ const typedActivity =
+ activity as CreateRequestActivityState;
+ expect(typedActivity.targetConfiguration).toEqual(
+ afterBehaviorState.requestTarget
+ );
+ expect(typedActivity.requestedResource).toEqual(
+ VehicleResource.create({ KTW: 1 })
+ );
+ expect(typedActivity.key).toEqual(
+ afterBehaviorState.answerKey
+ );
+ });
+ });
+ });
+
+ describe.each(withoutVehiclesRequired)('%s', (requestsAndPromises) => {
+ describe('in onTimer state', () => {
+ it('should stay in the onTimer state', () => {
+ const { afterBehaviorState } = setupStateAndInteract(
+ setBehaviorState.onTimer,
+ addRequestsAndPromises[requestsAndPromises],
+ sendEvent.sendRequestEvent
+ );
+
+ assertNotWaitingState(afterBehaviorState);
+ });
+ });
+ });
+ });
+
+ describe('when the request interval is updated', () => {
+ describe.each(StrictObject.keys(addRequestsAndPromises))(
+ '%s',
+ (requestsAndPromises) => {
+ describe.each(StrictObject.keys(setBehaviorState))(
+ 'in %s state',
+ (state) => {
+ it('should update the request interval', () => {
+ const { afterBehaviorState } =
+ setupStateAndInteract(
+ setBehaviorState[state],
+ addRequestsAndPromises[requestsAndPromises],
+ updateRequestInterval
+ );
+
+ expect(afterBehaviorState.requestInterval).toBe(
+ newRequestInterval
+ );
+ });
+
+ it('should update the timer', () => {
+ const { afterSimulatedRegion } =
+ setupStateAndInteract(
+ setBehaviorState[state],
+ addRequestsAndPromises[requestsAndPromises],
+ updateRequestInterval
+ );
+
+ const afterRecurringEventActivity =
+ StrictObject.values(
+ afterSimulatedRegion.activities
+ ).find(
+ (a) => a.type === 'recurringEventActivity'
+ ) as RecurringEventActivityState;
+
+ expect(
+ afterRecurringEventActivity.recurrenceIntervalTime
+ ).toBe(newRequestInterval);
+ });
+ }
+ );
+ }
+ );
+ });
+
+ describe('when the request target is updated', () => {
+ describe.each(StrictObject.keys(addRequestsAndPromises))(
+ '%s',
+ (requestsAndPromises) => {
+ describe.each(StrictObject.keys(setBehaviorState))(
+ 'in %s state',
+ (state) => {
+ it('should update the request target', () => {
+ const { afterBehaviorState } =
+ setupStateAndInteract(
+ setBehaviorState[state],
+ addRequestsAndPromises[requestsAndPromises],
+ updateRequestTarget
+ );
+
+ expect(
+ afterBehaviorState.requestTarget.type
+ ).toEqual('simulatedRegionRequestTarget');
+ });
+ }
+ );
+
+ describe('in waiting state', () => {
+ it('should not continue waiting for an answer', () => {
+ const { afterBehaviorState } = setupStateAndInteract(
+ setBehaviorState.waiting,
+ addRequestsAndPromises[requestsAndPromises],
+ updateRequestTarget
+ );
+
+ assertNotWaitingState(afterBehaviorState);
+ });
+ });
+ }
+ );
+ });
+
+ describe('when the invalidation interval for promises is updated', () => {
+ describe.each(withoutOldTime)('%s', (requestsAndPromises) => {
+ describe.each(StrictObject.keys(setBehaviorState))(
+ 'in %s state',
+ (state) => {
+ it('should not invalidate any promises', () => {
+ const { beforeBehaviorState, afterBehaviorState } =
+ setupStateAndInteract(
+ setBehaviorState[state],
+ addRequestsAndPromises[requestsAndPromises],
+ updateInvalidationInterval
+ );
+
+ expect(afterBehaviorState.promisedResources).toEqual(
+ beforeBehaviorState.promisedResources
+ );
+ });
+ }
+ );
+ });
+
+ describe.each(withOldTime)('%s', (requestsAndPromises) => {
+ describe.each(StrictObject.keys(setBehaviorState))(
+ 'in %s state',
+ (state) => {
+ it('should invalidate old promises', () => {
+ const { afterState, afterBehaviorState } =
+ setupStateAndInteract(
+ setBehaviorState[state],
+ addRequestsAndPromises[requestsAndPromises],
+ updateInvalidationInterval
+ );
+
+ expect(
+ Object.keys(
+ afterBehaviorState.promisedResources.filter(
+ (promise) =>
+ promise.promisedTime +
+ newInvalidationInterval <
+ afterState.currentTime
+ )
+ ).length
+ ).toBe(0);
+ });
+
+ it('should keep current promises', () => {
+ const { beforeBehaviorState, afterBehaviorState } =
+ setupStateAndInteract(
+ setBehaviorState[state],
+ addRequestsAndPromises[requestsAndPromises],
+ updateInvalidationInterval
+ );
+
+ beforeBehaviorState.promisedResources.forEach(
+ (beforePromise) => {
+ if (
+ beforePromise.promisedTime +
+ newInvalidationInterval >=
+ currentTime
+ ) {
+ expect(
+ afterBehaviorState.promisedResources
+ ).toContainEqual(beforePromise);
+ }
+ }
+ );
+ });
+ }
+ );
+ });
+ });
+});
diff --git a/shared/src/simulation/behaviors/request.ts b/shared/src/simulation/behaviors/request.ts
new file mode 100644
index 000000000..daab9c671
--- /dev/null
+++ b/shared/src/simulation/behaviors/request.ts
@@ -0,0 +1,255 @@
+import {
+ IsArray,
+ IsInt,
+ IsOptional,
+ IsString,
+ IsUUID,
+ Min,
+ ValidateNested,
+} from 'class-validator';
+import { Type } from 'class-transformer';
+import { IsStringMap } from '../../utils/validators/is-string-map';
+import { cloneDeepMutable, uuid, UUID } from '../../utils';
+import type { Mutable } from '../../utils';
+import { IsValue } from '../../utils/validators';
+import { getActivityById, getElement } from '../../store/action-reducers/utils';
+import type { ExerciseState } from '../../state';
+import { addActivity } from '../activities/utils';
+import { nextUUID } from '../utils/randomness';
+import { RecurringEventActivityState } from '../activities';
+import { SendRequestEvent } from '../events/send-request';
+import { CreateRequestActivityState } from '../activities/create-request';
+import {
+ VehicleResource,
+ aggregateResources,
+ subtractResources,
+} from '../../models/utils/vehicle-resource';
+import {
+ ExerciseRequestTargetConfiguration,
+ requestTargetTypeOptions,
+} from '../../models/utils/request-target/exercise-request-target';
+import { TraineesRequestTargetConfiguration } from '../../models/utils/request-target/trainees';
+import { getCreate } from '../../models/utils/get-create';
+import type { SimulatedRegion } from '../../models';
+import { ResourcePromise } from '../utils/resource-promise';
+import type {
+ SimulationBehavior,
+ SimulationBehaviorState,
+} from './simulation-behavior';
+
+export class RequestBehaviorState implements SimulationBehaviorState {
+ @IsValue('requestBehavior')
+ readonly type = 'requestBehavior';
+
+ @IsUUID()
+ public readonly id: UUID = uuid();
+
+ /**
+ * @deprecated Use {@link isWaitingForAnswer} instead
+ */
+ @IsString()
+ @IsOptional()
+ public readonly answerKey?: string;
+
+ @IsUUID()
+ @IsOptional()
+ public readonly recurringEventActivityId?: UUID;
+
+ @IsStringMap(VehicleResource)
+ public readonly requestedResources: { [key: string]: VehicleResource } = {};
+
+ @IsArray()
+ @Type(() => ResourcePromise)
+ @ValidateNested({ each: true })
+ public readonly promisedResources: readonly ResourcePromise[] = [];
+
+ /**
+ * @deprecated Use {@link updateBehaviorsRequestInterval} instead
+ */
+ @IsInt()
+ @Min(0)
+ public readonly requestInterval: number = 1000 * 60 * 5;
+
+ @IsInt()
+ @Min(0)
+ public readonly invalidatePromiseInterval: number = 1000 * 60 * 30;
+ /**
+ * @deprecated Use {@link updateBehaviorsRequestTarget} instead
+ */
+ @Type(...requestTargetTypeOptions)
+ @ValidateNested()
+ public readonly requestTarget: ExerciseRequestTargetConfiguration =
+ TraineesRequestTargetConfiguration.create();
+
+ @IsInt()
+ @Min(0)
+ public readonly requestTargetVersion: number = 0;
+
+ static readonly create = getCreate(this);
+}
+
+export const requestBehavior: SimulationBehavior = {
+ behaviorState: RequestBehaviorState,
+ handleEvent(draftState, simulatedRegion, behaviorState, event) {
+ switch (event.type) {
+ case 'tickEvent': {
+ if (!behaviorState.recurringEventActivityId) {
+ behaviorState.recurringEventActivityId =
+ nextUUID(draftState);
+ addActivity(
+ simulatedRegion,
+ RecurringEventActivityState.create(
+ behaviorState.recurringEventActivityId,
+ SendRequestEvent.create(),
+ draftState.currentTime,
+ behaviorState.requestInterval
+ )
+ );
+ }
+ break;
+ }
+ case 'resourceRequiredEvent': {
+ if (event.requiringSimulatedRegionId === simulatedRegion.id) {
+ behaviorState.requestedResources[event.key] =
+ event.requiredResource;
+ }
+ break;
+ }
+ case 'vehiclesSentEvent': {
+ behaviorState.promisedResources.push(
+ cloneDeepMutable(
+ ResourcePromise.create(
+ draftState.currentTime,
+ event.vehiclesSent
+ )
+ )
+ );
+
+ if (event.key === behaviorState.answerKey) {
+ behaviorState.answerKey = undefined;
+ }
+ break;
+ }
+ case 'vehicleArrivedEvent': {
+ const vehicle = getElement(
+ draftState,
+ 'vehicle',
+ event.vehicleId
+ );
+ let arrivatedResource = cloneDeepMutable(
+ VehicleResource.create({ [vehicle.vehicleType]: 1 })
+ );
+ behaviorState.promisedResources.forEach((promise) => {
+ const remainingResources = subtractResources(
+ arrivatedResource,
+ promise.resource
+ );
+
+ promise.resource = subtractResources(
+ promise.resource,
+ arrivatedResource
+ );
+
+ arrivatedResource = remainingResources;
+ });
+ behaviorState.promisedResources =
+ behaviorState.promisedResources.filter(
+ (promise) =>
+ Object.keys(promise.resource.vehicleCounts).length >
+ 0
+ );
+ break;
+ }
+ case 'sendRequestEvent': {
+ if (!isWaitingForAnswer(behaviorState)) {
+ const resourcesToRequest = getResourcesToRequest(
+ draftState,
+ simulatedRegion,
+ behaviorState
+ );
+ if (
+ Object.keys(resourcesToRequest.vehicleCounts).length > 0
+ ) {
+ // create a request to wait for an answer
+ behaviorState.answerKey = `${simulatedRegion.id}-request-${behaviorState.requestTargetVersion}`;
+ const activityId = nextUUID(draftState);
+ addActivity(
+ simulatedRegion,
+ CreateRequestActivityState.create(
+ activityId,
+ behaviorState.requestTarget,
+ resourcesToRequest,
+ behaviorState.answerKey
+ )
+ );
+ }
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ },
+};
+
+export function isWaitingForAnswer(behaviorState: RequestBehaviorState) {
+ return behaviorState.answerKey !== undefined;
+}
+
+export function updateBehaviorsRequestInterval(
+ draftState: Mutable,
+ simulatedRegion: Mutable,
+ behaviorState: Mutable,
+ requestInterval: number
+) {
+ if (behaviorState.recurringEventActivityId) {
+ const activity = getActivityById(
+ draftState,
+ simulatedRegion.id,
+ behaviorState.recurringEventActivityId,
+ 'recurringEventActivity'
+ );
+ activity.recurrenceIntervalTime = requestInterval;
+ }
+ behaviorState.requestInterval = requestInterval;
+}
+
+export function updateBehaviorsRequestTarget(
+ draftState: Mutable,
+ simulatedRegion: Mutable,
+ behaviorState: Mutable,
+ requestTarget: ExerciseRequestTargetConfiguration
+) {
+ behaviorState.requestTarget = cloneDeepMutable(requestTarget);
+ behaviorState.requestTargetVersion++;
+
+ if (isWaitingForAnswer(behaviorState)) {
+ behaviorState.answerKey = undefined;
+ }
+}
+
+export function getResourcesToRequest(
+ draftState: Mutable,
+ simulatedRegion: Mutable,
+ behaviorState: Mutable
+) {
+ const requestedResources = aggregateResources(
+ Object.values(behaviorState.requestedResources)
+ );
+
+ // remove invalidated resources
+ let firstValidIndex: number | undefined =
+ behaviorState.promisedResources.findIndex(
+ (promise) =>
+ promise.promisedTime + behaviorState.invalidatePromiseInterval >
+ draftState.currentTime
+ );
+ if (firstValidIndex === -1)
+ firstValidIndex = behaviorState.promisedResources.length;
+ behaviorState.promisedResources.splice(0, firstValidIndex);
+
+ const promisedResources = aggregateResources(
+ behaviorState.promisedResources.map((promise) => promise.resource)
+ );
+ return subtractResources(requestedResources, promisedResources);
+}
diff --git a/shared/src/simulation/events/exercise-simulation-event.ts b/shared/src/simulation/events/exercise-simulation-event.ts
index ca81e6748..e6b638cd3 100644
--- a/shared/src/simulation/events/exercise-simulation-event.ts
+++ b/shared/src/simulation/events/exercise-simulation-event.ts
@@ -12,6 +12,7 @@ import { CollectInformationEvent } from './collect';
import { StartCollectingInformationEvent } from './start-collecting';
import { ResourceRequiredEvent } from './resources-required';
import { VehiclesSentEvent } from './vehicles-sent';
+import { SendRequestEvent } from './send-request';
export const simulationEvents = {
MaterialAvailableEvent,
@@ -25,6 +26,7 @@ export const simulationEvents = {
StartCollectingInformationEvent,
ResourceRequiredEvent,
VehiclesSentEvent,
+ SendRequestEvent,
};
export type ExerciseSimulationEvent = InstanceType<
@@ -48,6 +50,7 @@ export const simulationEventDictionary: ExerciseSimulationEventDictionary = {
startCollectingInformationEvent: StartCollectingInformationEvent,
resourceRequiredEvent: ResourceRequiredEvent,
vehiclesSentEvent: VehiclesSentEvent,
+ sendRequestEvent: SendRequestEvent,
};
export const simulationEventTypeOptions: Parameters = [
diff --git a/shared/src/simulation/events/resources-required.ts b/shared/src/simulation/events/resources-required.ts
index affd2d2a7..36a51d9a9 100644
--- a/shared/src/simulation/events/resources-required.ts
+++ b/shared/src/simulation/events/resources-required.ts
@@ -1,6 +1,6 @@
import { Type } from 'class-transformer';
import { IsString, IsUUID, ValidateNested } from 'class-validator';
-import { getCreate } from '../../models/utils';
+import { getCreate } from '../../models/utils/get-create';
import { VehicleResource } from '../../models/utils/vehicle-resource';
import { UUID, uuidValidationOptions } from '../../utils';
import { IsValue } from '../../utils/validators';
diff --git a/shared/src/simulation/events/send-request.ts b/shared/src/simulation/events/send-request.ts
new file mode 100644
index 000000000..5744c8a3e
--- /dev/null
+++ b/shared/src/simulation/events/send-request.ts
@@ -0,0 +1,10 @@
+import { getCreate } from '../../models/utils/get-create';
+import { IsValue } from '../../utils/validators';
+import type { SimulationEvent } from './simulation-event';
+
+export class SendRequestEvent implements SimulationEvent {
+ @IsValue('sendRequestEvent')
+ readonly type = 'sendRequestEvent';
+
+ static readonly create = getCreate(this);
+}
diff --git a/shared/src/simulation/events/vehicles-sent.ts b/shared/src/simulation/events/vehicles-sent.ts
index 1d518d761..7e97f0fc5 100644
--- a/shared/src/simulation/events/vehicles-sent.ts
+++ b/shared/src/simulation/events/vehicles-sent.ts
@@ -1,5 +1,5 @@
import { Type } from 'class-transformer';
-import { IsUUID, ValidateNested } from 'class-validator';
+import { IsString, IsUUID, ValidateNested } from 'class-validator';
import { getCreate } from '../../models/utils';
import { VehicleResource } from '../../models/utils/vehicle-resource';
import { UUID, uuidValidationOptions } from '../../utils';
@@ -17,12 +17,16 @@ export class VehiclesSentEvent implements SimulationEvent {
@ValidateNested()
readonly vehiclesSent!: VehicleResource;
+ @IsString()
+ readonly key: string;
+
/**
* @deprecated Use {@link create} instead
*/
- constructor(id: UUID, vehiclesSent: VehicleResource) {
+ constructor(id: UUID, vehiclesSent: VehicleResource, key: string) {
this.id = id;
this.vehiclesSent = vehiclesSent;
+ this.key = key;
}
static readonly create = getCreate(this);
diff --git a/shared/src/simulation/utils/randomness.ts b/shared/src/simulation/utils/randomness.ts
index 3fd7a431d..91b301cad 100644
--- a/shared/src/simulation/utils/randomness.ts
+++ b/shared/src/simulation/utils/randomness.ts
@@ -2,7 +2,7 @@
import { IsInt, Min, ValidateIf } from 'class-validator';
import { sha256 } from '@noble/hashes/sha256';
import { v4 } from 'uuid';
-import { getCreate } from '../../models/utils';
+import { getCreate } from '../../models/utils/get-create';
import type { ExerciseState } from '../../state';
import type { Mutable, UUID } from '../../utils';
import { IsLiteralUnion, IsValue } from '../../utils/validators';
diff --git a/shared/src/simulation/utils/resource-promise.ts b/shared/src/simulation/utils/resource-promise.ts
new file mode 100644
index 000000000..321c7a91a
--- /dev/null
+++ b/shared/src/simulation/utils/resource-promise.ts
@@ -0,0 +1,28 @@
+import { Type } from 'class-transformer';
+import { IsInt, Min, ValidateNested } from 'class-validator';
+import { VehicleResource } from '../../models/utils/vehicle-resource';
+import { getCreate } from '../../models/utils/get-create';
+import { IsValue } from '../../utils/validators/is-value';
+
+export class ResourcePromise {
+ @IsValue('resourcePromise')
+ readonly type = 'resourcePromise';
+
+ @IsInt()
+ @Min(0)
+ readonly promisedTime: number;
+
+ @Type(() => VehicleResource)
+ @ValidateNested()
+ readonly resource: VehicleResource;
+
+ /**
+ * @deprecated Use {@link create} instead
+ */
+ constructor(promisedTime: number, resource: VehicleResource) {
+ this.promisedTime = promisedTime;
+ this.resource = resource;
+ }
+
+ static readonly create = getCreate(this);
+}
diff --git a/shared/src/simulation/utils/simulation.ts b/shared/src/simulation/utils/simulation.ts
index 54715b83f..6eac9eff3 100644
--- a/shared/src/simulation/utils/simulation.ts
+++ b/shared/src/simulation/utils/simulation.ts
@@ -48,7 +48,7 @@ function tickActivities(
});
}
-function handleSimulationEvents(
+export function handleSimulationEvents(
draftState: Mutable,
simulatedRegion: Mutable
) {
diff --git a/shared/src/store/action-reducers/radiogram.ts b/shared/src/store/action-reducers/radiogram.ts
index 63f1044d4..4cba34d0c 100644
--- a/shared/src/store/action-reducers/radiogram.ts
+++ b/shared/src/store/action-reducers/radiogram.ts
@@ -66,7 +66,27 @@ export namespace RadiogramActionReducers {
export const markDoneReducer: ActionReducer = {
action: MarkDoneRadiogramAction,
reducer: (draftState, { radiogramId }) => {
+ const radiogram = draftState.radiograms[radiogramId];
+ if (radiogram?.type === 'resourceRequestRadiogram') {
+ const simulatedRegion = getElement(
+ draftState,
+ 'simulatedRegion',
+ radiogram.simulatedRegionId
+ );
+ sendSimulationEvent(
+ simulatedRegion,
+ cloneDeepMutable(
+ VehiclesSentEvent.create(
+ nextUUID(draftState),
+ VehicleResource.create({}),
+ radiogram.key
+ )
+ )
+ );
+ }
+
markRadiogramDone(draftState, radiogramId);
+
return draftState;
},
rights: 'participant',
@@ -92,7 +112,8 @@ export namespace RadiogramActionReducers {
cloneDeepMutable(
VehiclesSentEvent.create(
nextUUID(draftState),
- radiogram.requiredResource
+ radiogram.requiredResource,
+ radiogram.key
)
)
);
@@ -124,7 +145,8 @@ export namespace RadiogramActionReducers {
cloneDeepMutable(
VehiclesSentEvent.create(
nextUUID(draftState),
- VehicleResource.create({})
+ VehicleResource.create({}),
+ radiogram.key
)
)
);
diff --git a/shared/src/store/action-reducers/simulation.ts b/shared/src/store/action-reducers/simulation.ts
index 5a9cc0565..5ca9c67de 100644
--- a/shared/src/store/action-reducers/simulation.ts
+++ b/shared/src/store/action-reducers/simulation.ts
@@ -1,9 +1,19 @@
-import { IsNumber, IsOptional, IsUUID, Min } from 'class-validator';
+import {
+ IsInt,
+ IsNumber,
+ IsOptional,
+ IsUUID,
+ Min,
+ ValidateNested,
+} from 'class-validator';
+import { Type } from 'class-transformer';
import type {
TreatPatientsBehaviorState,
UnloadArrivingVehiclesBehaviorState,
} from '../../simulation';
import {
+ updateBehaviorsRequestTarget,
+ updateBehaviorsRequestInterval,
ReportableInformation,
reportableInformationAllowedValues,
RecurringEventActivityState,
@@ -16,6 +26,10 @@ import { UUID, uuidValidationOptions, cloneDeepMutable } from '../../utils';
import { IsLiteralUnion, IsValue } from '../../utils/validators';
import type { Action, ActionReducer } from '../action-reducer';
import { ExpectedReducerError, ReducerError } from '../reducer-error';
+import {
+ requestTargetTypeOptions,
+ ExerciseRequestTargetConfiguration,
+} from '../../models';
import { getActivityById, getBehaviorById, getElement } from './utils';
export class UpdateTreatPatientsIntervalsAction implements Action {
@@ -134,6 +148,52 @@ export class RemoveRecurringReportsAction implements Action {
public readonly informationType!: ReportableInformation;
}
+export class UpdateRequestIntervalAction implements Action {
+ @IsValue('[RequestBehavior] Update RequestInterval')
+ public readonly type = '[RequestBehavior] Update RequestInterval';
+
+ @IsUUID(4, uuidValidationOptions)
+ public readonly simulatedRegionId!: UUID;
+
+ @IsUUID(4, uuidValidationOptions)
+ public readonly behaviorId!: UUID;
+
+ @IsInt()
+ @Min(0)
+ public readonly requestInterval!: number;
+}
+
+export class UpdateRequestTargetAction implements Action {
+ @IsValue('[RequestBehavior] Update RequestTarget')
+ public readonly type = '[RequestBehavior] Update RequestTarget';
+
+ @IsUUID(4, uuidValidationOptions)
+ public readonly simulatedRegionId!: UUID;
+
+ @IsUUID(4, uuidValidationOptions)
+ public readonly behaviorId!: UUID;
+
+ @Type(...requestTargetTypeOptions)
+ @ValidateNested()
+ public readonly requestTarget!: ExerciseRequestTargetConfiguration;
+}
+
+export class UpdatePromiseInvalidationIntervalAction implements Action {
+ @IsValue('[RequestBehavior] Update Promise invalidation interval')
+ public readonly type =
+ '[RequestBehavior] Update Promise invalidation interval';
+
+ @IsUUID(4, uuidValidationOptions)
+ public readonly simulatedRegionId!: UUID;
+
+ @IsUUID(4, uuidValidationOptions)
+ public readonly behaviorId!: UUID;
+
+ @IsInt()
+ @Min(0)
+ public readonly promiseInvalidationInterval!: number;
+}
+
export namespace SimulationActionReducers {
export const updateTreatPatientsIntervals: ActionReducer =
{
@@ -346,4 +406,82 @@ export namespace SimulationActionReducers {
},
rights: 'trainer',
};
+
+ export const updateRequestInterval: ActionReducer =
+ {
+ action: UpdateRequestIntervalAction,
+ reducer(
+ draftState,
+ { simulatedRegionId, behaviorId, requestInterval }
+ ) {
+ const behaviorState = getBehaviorById(
+ draftState,
+ simulatedRegionId,
+ behaviorId,
+ 'requestBehavior'
+ );
+ const simulatedRegion = getElement(
+ draftState,
+ 'simulatedRegion',
+ simulatedRegionId
+ );
+ updateBehaviorsRequestInterval(
+ draftState,
+ simulatedRegion,
+ behaviorState,
+ requestInterval
+ );
+ return draftState;
+ },
+ rights: 'trainer',
+ };
+
+ export const updateRequestTarget: ActionReducer =
+ {
+ action: UpdateRequestTargetAction,
+ reducer(
+ draftState,
+ { simulatedRegionId, behaviorId, requestTarget }
+ ) {
+ const behaviorState = getBehaviorById(
+ draftState,
+ simulatedRegionId,
+ behaviorId,
+ 'requestBehavior'
+ );
+ const simulatedRegion = getElement(
+ draftState,
+ 'simulatedRegion',
+ simulatedRegionId
+ );
+ updateBehaviorsRequestTarget(
+ draftState,
+ simulatedRegion,
+ behaviorState,
+ requestTarget
+ );
+ return draftState;
+ },
+ rights: 'trainer',
+ };
+
+ export const updatePromiseInvalidationInterval: ActionReducer =
+ {
+ action: UpdatePromiseInvalidationIntervalAction,
+ reducer(
+ draftState,
+ { simulatedRegionId, behaviorId, promiseInvalidationInterval }
+ ) {
+ const behaviorState = getBehaviorById(
+ draftState,
+ simulatedRegionId,
+ behaviorId,
+ 'requestBehavior'
+ );
+ behaviorState.invalidatePromiseInterval =
+ promiseInvalidationInterval;
+ return draftState;
+ },
+ rights: 'trainer',
+ };
}
diff --git a/shared/src/utils/validators/is-id-map.ts b/shared/src/utils/validators/is-id-map.ts
index e4fbd5e2c..76eb97476 100644
--- a/shared/src/utils/validators/is-id-map.ts
+++ b/shared/src/utils/validators/is-id-map.ts
@@ -1,10 +1,9 @@
-import { plainToInstance, Transform } from 'class-transformer';
import type { ValidationOptions } from 'class-validator';
-import { isUUID, ValidateNested } from 'class-validator';
+import { isUUID } from 'class-validator';
import type { Constructor } from '../constructor';
import type { UUID } from '../uuid';
-import { combineDecorators } from './combine-decorators';
import type { GenericPropertyDecorator } from './generic-property-decorator';
+import { IsMultiTypedStringMap } from './is-string-map';
// An `isIdMap` function is omitted.
// It's currently not used and it's not trivial to migrate the decorator approach below
@@ -42,29 +41,10 @@ export function IsMultiTypedIdMap<
(value as { id: UUID }).id,
validationOptions?: ValidationOptions & { each?: Each }
): GenericPropertyDecorator<{ readonly [key: UUID]: InstanceType }, Each> {
- const transform = Transform(
- ({ value }) => {
- const plainMap = value as { [key: UUID]: InstanceType };
- if (
- Object.entries(plainMap).some(
- ([key, plain]) => !isUUID(key, 4) || key !== getId(plain)
- )
- ) {
- return 'invalid';
- }
- const plainWithConstructor = Object.values(plainMap).map(
- (entry) => [entry, getConstructor(entry)] as const
- );
- if (plainWithConstructor.some(([_, constr]) => !constr)) {
- return 'invalid';
- }
- const instances = plainWithConstructor.map(([entry, constr]) =>
- plainToInstance(constr!, entry)
- );
- return instances;
- },
- { toClassOnly: true }
+ return IsMultiTypedStringMap(
+ getConstructor,
+ (key: string, plain: InstanceType) =>
+ isUUID(key, 4) && key === getId(plain),
+ validationOptions
);
- const validateNested = ValidateNested({ ...validationOptions, each: true });
- return combineDecorators(transform, validateNested);
}
diff --git a/shared/src/utils/validators/is-string-map.ts b/shared/src/utils/validators/is-string-map.ts
new file mode 100644
index 000000000..1084404e9
--- /dev/null
+++ b/shared/src/utils/validators/is-string-map.ts
@@ -0,0 +1,54 @@
+import { Transform, plainToInstance } from 'class-transformer';
+import type { ValidationOptions } from 'class-validator';
+import { ValidateNested } from 'class-validator';
+import type { Constructor } from '../constructor';
+import { combineDecorators } from './combine-decorators';
+import type { GenericPropertyDecorator } from './generic-property-decorator';
+
+// eslint-disable-next-line @typescript-eslint/naming-convention
+export function IsStringMap(
+ type: Constructor,
+ validationOptions?: ValidationOptions & { each?: Each }
+): GenericPropertyDecorator<{ readonly [key: string]: T }, Each> {
+ return IsMultiTypedStringMap(
+ () => type,
+ () => true,
+ validationOptions
+ );
+}
+
+// eslint-disable-next-line @typescript-eslint/naming-convention
+export function IsMultiTypedStringMap<
+ T extends Constructor,
+ Each extends boolean = false
+>(
+ getConstructor: (value: InstanceType) => T | undefined,
+ keyValidator: (key: string, plain: InstanceType) => boolean,
+ validationOptions?: ValidationOptions & { each?: Each }
+): GenericPropertyDecorator<{ readonly [key: string]: InstanceType }, Each> {
+ const transform = Transform(
+ ({ value }) => {
+ const plainMap = value as { [key: string]: InstanceType };
+ if (
+ Object.entries(plainMap).some(
+ ([key, plain]) => !keyValidator(key, plain)
+ )
+ ) {
+ return 'invalid';
+ }
+ const plainWithConstructor = Object.values(plainMap).map(
+ (entry) => [entry, getConstructor(entry)] as const
+ );
+ if (plainWithConstructor.some(([_, constr]) => !constr)) {
+ return 'invalid';
+ }
+ const instances = plainWithConstructor.map(([entry, constr]) =>
+ plainToInstance(constr!, entry)
+ );
+ return instances;
+ },
+ { toClassOnly: true }
+ );
+ const validateNested = ValidateNested({ ...validationOptions, each: true });
+ return combineDecorators(transform, validateNested);
+}
diff --git a/test-scenarios b/test-scenarios
index 1c96376fd..07106f3df 160000
--- a/test-scenarios
+++ b/test-scenarios
@@ -1 +1 @@
-Subproject commit 1c96376fd8b4de55c133435e48a9bc17c6afe43c
+Subproject commit 07106f3df3a23c0fd4f55b58eac4e91af91204c9
From 0575938e43e5c974e8c3b9c670d265bf8e556d47 Mon Sep 17 00:00:00 2001
From: Nils1729 <45318774+Nils1729@users.noreply.github.com>
Date: Fri, 14 Apr 2023 14:56:16 +0200
Subject: [PATCH 22/31] Request new vehices when treatment lacks personnel
(#850)
* WIP: Add prioritizing UI for patient treatment behavior
* WIP: Migrate functionality to new behavior
* WIP: Add missing files
* Reassing Treatments Activity emits resource required events for personnel
* Add new activity to translate personnel needs into vehicle needs
* Prefer NEF over GW-San by default
* Fix errors from merge
* WIP
* WIP: Overhaul treatment missing personnel calculation (with debug printing)
* Remove debug logging
* Update changelog
* Move resource description utils
* Fix imports
* Update shared/src/models/utils/rescue-resource.ts
Co-authored-by: Lukas Radermacher <49586507+lukasrad02@users.noreply.github.com>
* Update shared/src/models/utils/resource-description.ts
Co-authored-by: Lukas Radermacher <49586507+lukasrad02@users.noreply.github.com>
* Update shared/src/models/utils/resource-description.ts
Co-authored-by: Lukas Radermacher <49586507+lukasrad02@users.noreply.github.com>
* Update shared/src/models/utils/resource-description.ts
Co-authored-by: Lukas Radermacher <49586507+lukasrad02@users.noreply.github.com>
* Rename behavior type to name pipe
* Improve rescue resources code style
* Remove false comment
* Bump test scenario commit
* Use own branch for migration tests
* Update JSDoc
* Fix semantic merge conflicts
---------
Co-authored-by: Lukas Radermacher <49586507+lukasrad02@users.noreply.github.com>
---
CHANGELOG.md | 13 +-
.../simulated-region-overview.module.ts | 8 +-
...-behavior-provide-personnel.component.html | 82 +++
...-behavior-provide-personnel.component.scss | 7 +
...ew-behavior-provide-personnel.component.ts | 115 ++++
...iew-behavior-treat-patients.component.html | 32 +-
...egion-overview-behavior-tab.component.html | 24 +-
...-region-overview-behavior-tab.component.ts | 79 ++-
.../utils/behavior-to-german-name.pipe.ts | 15 +-
.../data/default-state/vehicle-templates.ts | 2 +-
.../radiogram/resource-request-radiogram.ts | 2 +-
shared/src/models/utils/index.ts | 2 +-
shared/src/models/utils/rescue-resource.ts | 82 +++
.../src/models/utils/resource-description.ts | 74 +++
shared/src/models/utils/vehicle-resource.ts | 50 --
.../simulation/activities/create-request.ts | 2 +-
.../exercise-simulation-activity.ts | 2 +
shared/src/simulation/activities/index.ts | 1 +
.../provide-personnel-from-vehicles.ts | 209 ++++++
.../activities/reassign-treatments.spec.ts | 9 +-
.../activities/reassign-treatments.ts | 616 +++++++++++++-----
.../activities/transfer-vehicles.ts | 2 +-
.../simulation/behaviors/answer-requests.ts | 45 +-
.../behaviors/exercise-simulation-behavior.ts | 4 +-
shared/src/simulation/behaviors/index.ts | 1 +
.../simulation/behaviors/provide-personnel.ts | 57 ++
shared/src/simulation/behaviors/request.ts | 69 +-
.../behaviors/simulation-behavior.ts | 9 +-
.../simulation/events/resources-required.ts | 11 +-
shared/src/simulation/events/vehicles-sent.ts | 2 +-
.../src/simulation/utils/resource-promise.ts | 2 +-
shared/src/store/action-reducers/radiogram.ts | 2 +-
.../src/store/action-reducers/simulation.ts | 43 +-
33 files changed, 1335 insertions(+), 338 deletions(-)
create mode 100644 frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/provide-personnel/simulated-region-overview-behavior-provide-personnel.component.html
create mode 100644 frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/provide-personnel/simulated-region-overview-behavior-provide-personnel.component.scss
create mode 100644 frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/provide-personnel/simulated-region-overview-behavior-provide-personnel.component.ts
create mode 100644 shared/src/models/utils/rescue-resource.ts
create mode 100644 shared/src/models/utils/resource-description.ts
delete mode 100644 shared/src/models/utils/vehicle-resource.ts
create mode 100644 shared/src/simulation/activities/provide-personnel-from-vehicles.ts
create mode 100644 shared/src/simulation/behaviors/provide-personnel.ts
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d990ea5a7..9a3646010 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,11 +11,14 @@ and this project does **not** adhere to [Semantic Versioning](https://semver.org
- There is now a display for how many different variations of a patient template exists.
- There is now a display for whether a patient is pregnant.
-- The patient status display that visualizes the progression of a patient explains its icons via a tooltip
-- There is now a behavior that answers vehicle requests from other regions
-- There is now a behavior to forward requests to other simulated regions or the trainees
-- There is now a radiogram for missing transfer connections and vehicle requests
- - Radiograms for vehicle requests can also be answered in the user interface, whether they have been accepted or not
+- The patient status display that visualizes the progression of a patient explains its icons via a tooltip.
+- There is now a behavior that answers vehicle requests from other regions.
+- There is now a behavior to forward requests to other simulated regions or the trainees.
+- There is now a radiogram for missing transfer connections and vehicle requests.
+ - Radiograms for vehicle requests can also be answered in the user interface, whether they have been accepted or not.
+- When personnel is missing during patient treatment in a simulated region, the reassign treatment activity now sends an event to notify the region about the shortage.
+- A new behavior has been added to respond to personnel shortages by instructing the region to request new vehicles.
+ - The priorities of vehicles to request can be configured in a new behavior tab.
### Changed
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 1b0e28d9b..c31290396 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
@@ -2,6 +2,7 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { SharedModule } from 'src/app/shared/shared.module';
+import { DragDropModule } from '@angular/cdk/drag-drop';
import {
NgbCollapseModule,
NgbDropdownModule,
@@ -15,7 +16,7 @@ import { SimulatedRegionOverviewBehaviorTabComponent } from './tabs/behavior-tab
import { SimulatedRegionOverviewGeneralTabComponent } from './tabs/general-tab/simulated-region-overview-general-tab.component';
import { SimulatedRegionOverviewBehaviorTreatPatientsComponent } from './tabs/behavior-tab/behaviors/treat-patients/simulated-region-overview-behavior-treat-patients.component';
import { SimulatedRegionOverviewBehaviorAssignLeaderComponent } from './tabs/behavior-tab/behaviors/assign-leader/simulated-region-overview-behavior-assign-leader.component';
-import { BehaviorToGermanNamePipe } from './tabs/behavior-tab/utils/behavior-to-german-name.pipe';
+import { BehaviorTypeToGermanNamePipe } from './tabs/behavior-tab/utils/behavior-to-german-name.pipe';
import { SimulatedRegionOverviewBehaviorUnloadArrivingVehiclesComponent } from './tabs/behavior-tab/behaviors/unload-arriving-vehicles/simulated-region-overview-behavior-unload-arriving-vehicles.component';
import { TreatmentProgressToGermanNamePipe } from './tabs/behavior-tab/utils/treatment-progress-to-german-name.pipe';
import { SimulatedRegionOverviewBehaviorTreatPatientsPatientDetailsComponent } from './tabs/behavior-tab/behaviors/treat-patients/patient-details/simulated-region-overview-behavior-treat-patients-patient-details.component';
@@ -39,6 +40,7 @@ import { RadiogramCardContentInformationUnavailableComponent } from './radiogram
import { HumanReadableRadiogramTypePipe } from './radiogram-list/human-readable-radiogram-type.pipe';
import { TreatmentStatusBadgeComponent } from './treatment-status-badge/treatment-status-badge.component';
import { RadigoramCardContentMissingTransferConnectionComponent } from './radiogram-list/radiogram-card/radigoram-card-content-missing-transfer-connection/radigoram-card-content-missing-transfer-connection.component';
+import { SimulatedRegionOverviewBehaviorProvidePersonnelComponent } from './tabs/behavior-tab/behaviors/provide-personnel/simulated-region-overview-behavior-provide-personnel.component';
import { SimulatedRegionOverviewBehaviorAnswerVehicleRequestsComponent } from './tabs/behavior-tab/behaviors/answer-vehicle-requests/simulated-region-overview-behavior-answer-vehicle-requests.component';
import { RadigoramCardContentResourceRequestComponent } from './radiogram-list/radiogram-card/radigoram-card-content-resource-request/radigoram-card-content-resource-request.component';
import { RequestVehiclesComponent } from './tabs/behavior-tab/behaviors/request-vehicles/simulated-region-overview-behavior-request-vehicles.component';
@@ -52,7 +54,7 @@ import { RequestVehiclesComponent } from './tabs/behavior-tab/behaviors/request-
SimulatedRegionOverviewBehaviorTreatPatientsComponent,
SimulatedRegionOverviewBehaviorAssignLeaderComponent,
SimulatedRegionOverviewBehaviorUnloadArrivingVehiclesComponent,
- BehaviorToGermanNamePipe,
+ BehaviorTypeToGermanNamePipe,
TreatmentProgressToGermanNamePipe,
SimulatedRegionOverviewBehaviorTreatPatientsPatientDetailsComponent,
WithDollarPipe,
@@ -73,6 +75,7 @@ import { RequestVehiclesComponent } from './tabs/behavior-tab/behaviors/request-
HumanReadableRadiogramTypePipe,
TreatmentStatusBadgeComponent,
RadigoramCardContentMissingTransferConnectionComponent,
+ SimulatedRegionOverviewBehaviorProvidePersonnelComponent,
SimulatedRegionOverviewBehaviorAnswerVehicleRequestsComponent,
RadigoramCardContentResourceRequestComponent,
RequestVehiclesComponent,
@@ -86,6 +89,7 @@ import { RequestVehiclesComponent } from './tabs/behavior-tab/behaviors/request-
NgbDropdownModule,
NgbProgressbarModule,
NgbTooltipModule,
+ DragDropModule,
TransferPointOverviewModule,
],
exports: [SimulatedRegionOverviewGeneralComponent],
diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/provide-personnel/simulated-region-overview-behavior-provide-personnel.component.html b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/provide-personnel/simulated-region-overview-behavior-provide-personnel.component.html
new file mode 100644
index 000000000..5674f17da
--- /dev/null
+++ b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/provide-personnel/simulated-region-overview-behavior-provide-personnel.component.html
@@ -0,0 +1,82 @@
+Fahrzeugpräferenzen
+
+ Bei Personalmangel werden weitere Fahrzeuge angefordert. Es wird das oberste
+ Fahrzeug aus dieser Liste genommen, welches das nötige Personal enthält.
+
+Sie können die Elemente in die gewünschte Reihenfolge ziehen.
+
+
+
+ Anderes Fahrzeug anfordern
+
+
+
+
+ {{ template.vehicleType }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ template.vehicleType }}
+
+ Nicht anfordern
+
+
+
+
+
+
+
+
+ Keine Priorität eingerichtet. Bei Personalmangel werden keine Fahrzeuge
+ angefordert.
+
+
diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/provide-personnel/simulated-region-overview-behavior-provide-personnel.component.scss b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/provide-personnel/simulated-region-overview-behavior-provide-personnel.component.scss
new file mode 100644
index 000000000..aeb82a457
--- /dev/null
+++ b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/provide-personnel/simulated-region-overview-behavior-provide-personnel.component.scss
@@ -0,0 +1,7 @@
+.cdk-drag-preview {
+ opacity: 0;
+}
+
+.drag-list.cdk-drop-list-dragging .drag-list-item {
+ transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
+}
diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/provide-personnel/simulated-region-overview-behavior-provide-personnel.component.ts b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/provide-personnel/simulated-region-overview-behavior-provide-personnel.component.ts
new file mode 100644
index 000000000..e5e62ca47
--- /dev/null
+++ b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/provide-personnel/simulated-region-overview-behavior-provide-personnel.component.ts
@@ -0,0 +1,115 @@
+import type { OnDestroy, OnInit } from '@angular/core';
+import { Component, Input } from '@angular/core';
+import { Store } from '@ngrx/store';
+import type {
+ ProvidePersonnelBehaviorState,
+ VehicleTemplate,
+} from 'digital-fuesim-manv-shared';
+import { UUID } from 'digital-fuesim-manv-shared';
+import type { Observable } from 'rxjs';
+import { combineLatest, map, Subject, takeUntil } from 'rxjs';
+import { ExerciseService } from 'src/app/core/exercise.service';
+import type { AppState } from 'src/app/state/app.state';
+import {
+ createSelectBehaviorState,
+ selectVehicleTemplates,
+} from 'src/app/state/application/selectors/exercise.selectors';
+import type { CdkDragDrop } from '@angular/cdk/drag-drop';
+
+@Component({
+ selector: 'app-simulated-region-overview-behavior-provide-personnel',
+ templateUrl:
+ './simulated-region-overview-behavior-provide-personnel.component.html',
+ styleUrls: [
+ './simulated-region-overview-behavior-provide-personnel.component.scss',
+ ],
+})
+export class SimulatedRegionOverviewBehaviorProvidePersonnelComponent
+ implements OnInit, OnDestroy
+{
+ @Input() simulatedRegionId!: UUID;
+ @Input() behaviorId!: UUID;
+
+ public vehicleTemplatesToAdd$!: Observable;
+ public vehicleTemplatesCurrent$!: Observable;
+
+ private ownPriorities!: readonly UUID[];
+
+ private readonly destroy$ = new Subject();
+
+ constructor(
+ private readonly exerciseService: ExerciseService,
+ public readonly store: Store
+ ) {}
+
+ ngOnInit(): void {
+ const behaviorState$ = this.store.select(
+ createSelectBehaviorState(
+ this.simulatedRegionId,
+ this.behaviorId
+ )
+ );
+
+ const ownVehicleTemplateIds$ = behaviorState$.pipe(
+ map((behaviorState) => behaviorState.vehicleTemplatePriorities)
+ );
+ const availableVehicleTemplates$ = this.store.select(
+ selectVehicleTemplates
+ );
+ this.vehicleTemplatesCurrent$ = combineLatest(
+ [availableVehicleTemplates$, ownVehicleTemplateIds$],
+ (templates, ownIds) => {
+ const templateMap = Object.fromEntries(
+ templates.map((template) => [template.id, template])
+ );
+ return ownIds.map((id) => templateMap[id]!);
+ }
+ );
+ this.vehicleTemplatesToAdd$ = combineLatest(
+ [availableVehicleTemplates$, ownVehicleTemplateIds$],
+ (templates, ownIds) =>
+ templates.filter((template) => !ownIds.includes(template.id))
+ );
+
+ ownVehicleTemplateIds$
+ .pipe(takeUntil(this.destroy$))
+ .subscribe((ids) => {
+ this.ownPriorities = ids;
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.destroy$.next();
+ }
+
+ public vehiclePriorityReorder({
+ item: { data: id },
+ currentIndex,
+ }: CdkDragDrop) {
+ const newPriorities = this.ownPriorities.filter((item) => item !== id);
+ newPriorities.splice(currentIndex, 0, id);
+ this.proposeVehiclePriorities(newPriorities);
+ }
+
+ public vehiclePriorityRemove(id: UUID) {
+ this.proposeVehiclePriorities(
+ this.ownPriorities.filter((item) => item !== id)
+ );
+ }
+
+ public vehiclePriorityAdd(id: UUID) {
+ this.proposeVehiclePriorities([id, ...this.ownPriorities]);
+ }
+
+ private proposeVehiclePriorities(newPriorities: readonly UUID[]) {
+ this.exerciseService.proposeAction(
+ {
+ type: '[ProvidePersonnelBehavior] Update VehiclePriorities',
+ simulatedRegionId: this.simulatedRegionId,
+ behaviorId: this.behaviorId,
+ priorities: newPriorities,
+ },
+ true
+ );
+ }
+}
diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/treat-patients/simulated-region-overview-behavior-treat-patients.component.html b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/treat-patients/simulated-region-overview-behavior-treat-patients.component.html
index 037adca0a..553796e6a 100644
--- a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/treat-patients/simulated-region-overview-behavior-treat-patients.component.html
+++ b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/treat-patients/simulated-region-overview-behavior-treat-patients.component.html
@@ -1,11 +1,11 @@
-
+
Allgemein
@@ -46,7 +46,7 @@
Allgemein
0"
#collapse="ngbCollapse"
[(ngbCollapse)]="informationCollapsed"
@@ -58,7 +58,7 @@
Derzeitige Zuordnung
0"
#collapse="ngbCollapse"
[(ngbCollapse)]="informationCollapsed"
@@ -78,7 +78,7 @@
Zugeteiltes Personal
@@ -96,7 +96,7 @@
Zugeteiltes Personal
@@ -113,7 +113,7 @@
@@ -123,7 +123,7 @@
@@ -161,7 +161,7 @@
@@ -199,7 +199,7 @@
@@ -237,7 +237,7 @@
@@ -274,7 +274,7 @@
-
+
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 6422d00c0..87fea3223 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
@@ -18,7 +18,7 @@
(click)="onBehaviorSelect(behavior)"
class="list-group-item list-group-item-action d-flex align-items-center flex-nowrap"
>
- {{ behavior | behaviorToGermanName }}
+ {{ behavior.type | behaviorTypeToGermanName }}
@@ -44,6 +44,11 @@
[simulatedRegionId]="simulatedRegion.id"
[reportBehaviorId]="selectedBehavior!.id"
>
+
@@ -70,19 +75,26 @@
ngbDropdownToggle
type="button"
class="btn btn-outline-primary"
- [disabled]="behaviorsToBeAdded.length === 0"
+ [disabled]="
+ ((behaviorTypesToBeAdded$ | async)?.length ?? 0) === 0
+ "
>
Hinzufügen
-
+
- {{ behavior | behaviorToGermanName }}
+ {{ $any(behaviorType) | behaviorTypeToGermanName }}
diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/simulated-region-overview-behavior-tab.component.ts b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/simulated-region-overview-behavior-tab.component.ts
index 7ffe49328..24e40af88 100644
--- a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/simulated-region-overview-behavior-tab.component.ts
+++ b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/simulated-region-overview-behavior-tab.component.ts
@@ -1,13 +1,26 @@
import type { OnChanges, OnInit } from '@angular/core';
import { Component, Input } from '@angular/core';
-import type { ExerciseSimulationBehaviorState } from 'digital-fuesim-manv-shared';
+import { Store } from '@ngrx/store';
+import type {
+ ExerciseSimulationBehaviorState,
+ ExerciseSimulationBehaviorType,
+} from 'digital-fuesim-manv-shared';
import {
- simulationBehaviors,
+ simulationBehaviorDictionary,
+ StrictObject,
SimulatedRegion,
} from 'digital-fuesim-manv-shared';
+import type { Observable } from 'rxjs';
+import { map } from 'rxjs';
import { ExerciseService } from 'src/app/core/exercise.service';
+import type { AppState } from 'src/app/state/app.state';
+import {
+ createSelectBehaviorStates,
+ selectVehicleTemplates,
+} from 'src/app/state/application/selectors/exercise.selectors';
+import { selectStateSnapshot } from 'src/app/state/get-state-snapshot';
-let globalLastBehavior: ExerciseSimulationBehaviorState | undefined;
+let globalLastBehaviorType: ExerciseSimulationBehaviorType | undefined;
@Component({
selector: 'app-simulated-region-overview-behavior-tab',
@@ -18,19 +31,38 @@ export class SimulatedRegionOverviewBehaviorTabComponent
implements OnChanges, OnInit
{
@Input() simulatedRegion!: SimulatedRegion;
- public behaviorsToBeAdded: ExerciseSimulationBehaviorState[] = [];
+ public behaviorTypesToBeAdded$!: Observable<
+ ExerciseSimulationBehaviorType[]
+ >;
public selectedBehavior?: ExerciseSimulationBehaviorState;
- constructor(private readonly exerciseService: ExerciseService) {}
+
+ constructor(
+ private readonly exerciseService: ExerciseService,
+ private readonly store: Store
+ ) {}
+
ngOnInit(): void {
- if (globalLastBehavior?.type !== undefined) {
+ if (globalLastBehaviorType !== undefined) {
this.selectedBehavior = this.simulatedRegion.behaviors.find(
- (behavior) => behavior.type === globalLastBehavior?.type
+ (behavior) => behavior.type === globalLastBehaviorType
);
}
+
+ this.behaviorTypesToBeAdded$ = this.store
+ .select(createSelectBehaviorStates(this.simulatedRegion.id))
+ .pipe(
+ map((states) => {
+ const currentTypes = new Set(
+ states.map((state) => state.type)
+ );
+ return StrictObject.keys(
+ simulationBehaviorDictionary
+ ).filter((type) => !currentTypes.has(type));
+ })
+ );
}
public ngOnChanges() {
- this.updateBehaviorsToBeAdded();
if (
// if the selected behavior has been removed by a different client
this.selectedBehavior !== undefined &&
@@ -42,7 +74,22 @@ export class SimulatedRegionOverviewBehaviorTabComponent
}
}
- public addBehavior(behaviorState: ExerciseSimulationBehaviorState) {
+ public addBehavior(behaviorType: ExerciseSimulationBehaviorType) {
+ const args: any[] = [];
+ switch (behaviorType) {
+ case 'providePersonnelBehavior':
+ args.push(
+ selectStateSnapshot(selectVehicleTemplates, this.store).map(
+ (template) => template.id
+ )
+ );
+ break;
+ default:
+ break;
+ }
+ const behaviorState = simulationBehaviorDictionary[
+ behaviorType
+ ].behaviorState.create(...args);
this.exerciseService.proposeAction({
type: '[SimulatedRegion] Add Behavior',
simulatedRegionId: this.simulatedRegion.id,
@@ -61,18 +108,6 @@ export class SimulatedRegionOverviewBehaviorTabComponent
public onBehaviorSelect(behavior: ExerciseSimulationBehaviorState): void {
this.selectedBehavior = behavior;
- globalLastBehavior = behavior;
- }
-
- private updateBehaviorsToBeAdded() {
- this.behaviorsToBeAdded = Object.values(simulationBehaviors)
- .map((behavior) => new behavior.behaviorState())
- .filter(
- (behaviorState) =>
- !this.simulatedRegion.behaviors.some(
- (regionBehaviorState) =>
- regionBehaviorState.type === behaviorState.type
- )
- );
+ globalLastBehaviorType = behavior.type;
}
}
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 a90d41c5d..d2d96d198 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
@@ -1,22 +1,23 @@
import type { PipeTransform } from '@angular/core';
import { Pipe } from '@angular/core';
-import type { ExerciseSimulationBehaviorState } from 'digital-fuesim-manv-shared';
+import type { ExerciseSimulationBehaviorType } from 'digital-fuesim-manv-shared';
-const behaviorToGermanNameDictionary: {
- [Key in ExerciseSimulationBehaviorState['type']]: string;
+const behaviorTypeToGermanNameDictionary: {
+ [Key in ExerciseSimulationBehaviorType]: string;
} = {
assignLeaderBehavior: 'Führung zuweisen',
treatPatientsBehavior: 'Patienten behandeln',
unloadArrivingVehiclesBehavior: 'Fahrzeuge entladen',
reportBehavior: 'Berichte erstellen',
+ providePersonnelBehavior: 'Personal nachfordern',
answerRequestsBehavior: 'Fahrzeuganfragen beantworten',
requestBehavior: 'Fahrzeuge anfordern',
};
@Pipe({
- name: 'behaviorToGermanName',
+ name: 'behaviorTypeToGermanName',
})
-export class BehaviorToGermanNamePipe implements PipeTransform {
- transform(value: ExerciseSimulationBehaviorState): string {
- return behaviorToGermanNameDictionary[value.type];
+export class BehaviorTypeToGermanNamePipe implements PipeTransform {
+ transform(value: ExerciseSimulationBehaviorType): string {
+ return behaviorTypeToGermanNameDictionary[value];
}
}
diff --git a/shared/src/data/default-state/vehicle-templates.ts b/shared/src/data/default-state/vehicle-templates.ts
index ddce93157..787ba6c9c 100644
--- a/shared/src/data/default-state/vehicle-templates.ts
+++ b/shared/src/data/default-state/vehicle-templates.ts
@@ -104,8 +104,8 @@ export const defaultVehicleTemplates: readonly VehicleTemplate[] = [
rtwVehicleTemplate,
ktwVehicleTemplate,
ktwKatSchutzVehicleTemplate,
- gwSanVehicleTemplate,
nefVehicleTemplate,
+ gwSanVehicleTemplate,
carryingUnitVehicleTemplate,
rthVehicleTemplate,
];
diff --git a/shared/src/models/radiogram/resource-request-radiogram.ts b/shared/src/models/radiogram/resource-request-radiogram.ts
index 6844783e4..e7615ed52 100644
--- a/shared/src/models/radiogram/resource-request-radiogram.ts
+++ b/shared/src/models/radiogram/resource-request-radiogram.ts
@@ -4,7 +4,7 @@ import { UUID, uuidValidationOptions } from '../../utils';
import { IsValue } from '../../utils/validators';
import { IsRadiogramStatus } from '../../utils/validators/is-radiogram-status';
import { getCreate } from '../utils/get-create';
-import { VehicleResource } from '../utils/vehicle-resource';
+import { VehicleResource } from '../utils/rescue-resource';
import type { Radiogram } from './radiogram';
import { ExerciseRadiogramStatus } from './status/exercise-radiogram-status';
diff --git a/shared/src/models/utils/index.ts b/shared/src/models/utils/index.ts
index 728dcb9b0..64ff7c5cf 100644
--- a/shared/src/models/utils/index.ts
+++ b/shared/src/models/utils/index.ts
@@ -23,5 +23,5 @@ export { AlarmGroupVehicle } from './alarm-group-vehicle';
export * from './patient-status-code';
export * from './start-points';
export * from './spatial-tree';
-export * from './vehicle-resource';
+export * from './rescue-resource';
export * from './request-target';
diff --git a/shared/src/models/utils/rescue-resource.ts b/shared/src/models/utils/rescue-resource.ts
new file mode 100644
index 000000000..c83ee184d
--- /dev/null
+++ b/shared/src/models/utils/rescue-resource.ts
@@ -0,0 +1,82 @@
+import type { Type } from 'class-transformer';
+import { StrictObject } from '../../utils';
+import { IsValue } from '../../utils/validators';
+import { IsResourceDescription } from '../../utils/validators/is-resource-description';
+import { getCreate } from './get-create';
+import type { PersonnelType } from './personnel-type';
+import { ResourceDescription } from './resource-description';
+
+class RescueResource {
+ public readonly type!: `${string}Resource`;
+}
+
+export class VehicleResource {
+ @IsValue('vehicleResource' as const)
+ public readonly type = 'vehicleResource';
+
+ @IsResourceDescription()
+ public readonly vehicleCounts!: ResourceDescription;
+
+ /**
+ * @deprecated Use {@link create} instead
+ */
+ constructor(vehicleCounts: ResourceDescription) {
+ this.vehicleCounts = vehicleCounts;
+ }
+
+ static readonly create = getCreate(this);
+}
+
+export class PersonnelResource {
+ @IsValue('personnelResource' as const)
+ public readonly type = 'personnelResource';
+
+ @IsResourceDescription()
+ public readonly personnelCounts!: ResourceDescription;
+
+ /**
+ * @deprecated Use {@link create} instead
+ */
+ constructor(personnelCounts?: ResourceDescription) {
+ this.personnelCounts = personnelCounts ?? {
+ gf: 0,
+ notarzt: 0,
+ notSan: 0,
+ rettSan: 0,
+ san: 0,
+ };
+ }
+
+ static readonly create = getCreate(this);
+}
+
+export type ExerciseRescueResource = PersonnelResource | VehicleResource;
+
+export const rescueResourceTypeOptions: Parameters = [
+ () => RescueResource,
+ {
+ keepDiscriminatorProperty: true,
+ discriminator: {
+ property: 'type',
+ subTypes: [
+ { name: 'vehicleResource', value: VehicleResource },
+ { name: 'personnelResource', value: PersonnelResource },
+ ],
+ },
+ },
+];
+
+export function isEmptyResource(resource: ExerciseRescueResource) {
+ let resourceDescription: ResourceDescription;
+ switch (resource.type) {
+ case 'personnelResource':
+ resourceDescription = resource.personnelCounts;
+ break;
+ case 'vehicleResource':
+ resourceDescription = resource.vehicleCounts;
+ break;
+ }
+ return StrictObject.values(resourceDescription).every(
+ (count) => count === 0
+ );
+}
diff --git a/shared/src/models/utils/resource-description.ts b/shared/src/models/utils/resource-description.ts
new file mode 100644
index 000000000..78b8cf612
--- /dev/null
+++ b/shared/src/models/utils/resource-description.ts
@@ -0,0 +1,74 @@
+import { StrictObject } from '../../utils';
+
+export const addResourceDescription = createCombine((a, b) => a + b);
+export const subtractResourceDescription = createCombine((a, b) => a - b);
+export const greaterEqualResourceDescription = createCompare((a, b) => a >= b);
+export const scaleResourceDescription = createMap((a, s) => a * s);
+export const ceilResourceDescription = createMap(Math.ceil);
+export const maxResourceDescription = createMap(Math.max);
+
+export type ResourceDescription = {
+ [key in K]: number;
+};
+
+type ReadonlyResourceDescription = {
+ readonly [key in K]: number;
+};
+
+export function createCombine(transform: (a: number, b: number) => number) {
+ return (
+ a: ResourceDescription,
+ b: ResourceDescription
+ ) =>
+ Object.fromEntries(
+ StrictObject.keys(a).map((key) => [key, transform(a[key], b[key])])
+ ) as ResourceDescription;
+}
+
+export function createCompare(comparator: (a: number, b: number) => boolean) {
+ return (
+ a: ReadonlyResourceDescription,
+ b: ReadonlyResourceDescription
+ ) => StrictObject.keys(a).every((key) => comparator(a[key], b[key]));
+}
+
+export function createMap(fn: (a: number, ...args: any) => number) {
+ return (
+ a: ReadonlyResourceDescription,
+ ...args: any
+ ) =>
+ Object.fromEntries(
+ StrictObject.entries(a).map(([key, value]) => [
+ key,
+ fn(value, ...args),
+ ])
+ ) as ResourceDescription;
+}
+
+export function addPartialResourceDescriptions(
+ resourceDescriptions: Partial>[]
+): Partial> {
+ return resourceDescriptions.reduce>>(
+ (total, current) => {
+ StrictObject.entries(current).forEach(([key, value]) => {
+ total[key] = (total[key] ?? 0) + (value ?? 0);
+ });
+ return total;
+ },
+ {}
+ );
+}
+
+export function subtractPartialResourceDescriptions(
+ minuend: Partial>,
+ subtrahend: Partial>
+): Partial> {
+ const result = addPartialResourceDescriptions([
+ minuend,
+ scaleResourceDescription(subtrahend as ResourceDescription, -1),
+ ]);
+ StrictObject.entries(result)
+ .filter(([_, value]) => (value ?? 0) <= 0)
+ .forEach(([key]) => delete result[key]);
+ return result;
+}
diff --git a/shared/src/models/utils/vehicle-resource.ts b/shared/src/models/utils/vehicle-resource.ts
deleted file mode 100644
index 29ecbe8ab..000000000
--- a/shared/src/models/utils/vehicle-resource.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import type { Mutable } from '../../utils';
-import { cloneDeepMutable } from '../../utils';
-import { IsValue } from '../../utils/validators';
-import { IsResourceDescription } from '../../utils/validators/is-resource-description';
-import { getCreate } from './get-create';
-
-export class VehicleResource {
- @IsValue('vehicleResource' as const)
- public readonly type = 'vehicleResource';
-
- @IsResourceDescription()
- public readonly vehicleCounts!: { [key: string]: number };
-
- /**
- * @deprecated Use {@link create} instead
- */
- constructor(vehicleCounts: { [key: string]: number }) {
- this.vehicleCounts = vehicleCounts;
- }
-
- static readonly create = getCreate(this);
-}
-
-export function aggregateResources(
- resources: Mutable[]
-): Mutable {
- return resources.reduce((total, current) => {
- Object.entries(current.vehicleCounts).forEach(([type, count]) => {
- if (!total.vehicleCounts[type]) total.vehicleCounts[type] = 0;
- total.vehicleCounts[type] += count;
- });
- return total;
- }, cloneDeepMutable(VehicleResource.create({})));
-}
-
-export function subtractResources(
- minuend: VehicleResource,
- subtrahend: VehicleResource
-): Mutable {
- const result = cloneDeepMutable(minuend);
- Object.entries(subtrahend.vehicleCounts).forEach(([type, count]) => {
- if (!(type in result.vehicleCounts)) return;
- if (result.vehicleCounts[type]! <= count) {
- delete result.vehicleCounts[type];
- } else {
- result.vehicleCounts[type] -= count;
- }
- });
- return result;
-}
diff --git a/shared/src/simulation/activities/create-request.ts b/shared/src/simulation/activities/create-request.ts
index a39e33562..eac44cde6 100644
--- a/shared/src/simulation/activities/create-request.ts
+++ b/shared/src/simulation/activities/create-request.ts
@@ -8,7 +8,7 @@ import {
requestTargetDictionary,
requestTargetTypeOptions,
} from '../../models/utils/request-target/exercise-request-target';
-import { VehicleResource } from '../../models/utils/vehicle-resource';
+import { VehicleResource } from '../../models/utils/rescue-resource';
import { getCreate } from '../../models/utils/get-create';
import type {
SimulationActivity,
diff --git a/shared/src/simulation/activities/exercise-simulation-activity.ts b/shared/src/simulation/activities/exercise-simulation-activity.ts
index 1bbb86d95..aa4c1141b 100644
--- a/shared/src/simulation/activities/exercise-simulation-activity.ts
+++ b/shared/src/simulation/activities/exercise-simulation-activity.ts
@@ -5,6 +5,7 @@ import { reassignTreatmentsActivity } from './reassign-treatments';
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';
@@ -14,6 +15,7 @@ export const simulationActivities = {
delayEventActivity,
recurringEventActivity,
generateReportActivity,
+ providePersonnelFromVehiclesActivity,
transferVehiclesActivity,
createRequestActivity,
};
diff --git a/shared/src/simulation/activities/index.ts b/shared/src/simulation/activities/index.ts
index 3767538f3..0a173f713 100644
--- a/shared/src/simulation/activities/index.ts
+++ b/shared/src/simulation/activities/index.ts
@@ -3,4 +3,5 @@ export * from './exercise-simulation-activity';
export * from './reassign-treatments';
export * from './unload-vehicle';
export * from './recurring-event';
+export * from './provide-personnel-from-vehicles';
export * from './transfer-vehicles';
diff --git a/shared/src/simulation/activities/provide-personnel-from-vehicles.ts b/shared/src/simulation/activities/provide-personnel-from-vehicles.ts
new file mode 100644
index 000000000..bfc05fb21
--- /dev/null
+++ b/shared/src/simulation/activities/provide-personnel-from-vehicles.ts
@@ -0,0 +1,209 @@
+import { IsString, IsUUID } from 'class-validator';
+import type { SimulatedRegion } from '../../models';
+import type { PersonnelType } from '../../models/utils';
+import {
+ PersonnelResource,
+ VehicleResource,
+ getCreate,
+} from '../../models/utils';
+import {
+ addResourceDescription,
+ greaterEqualResourceDescription,
+ ResourceDescription,
+} from '../../models/utils/resource-description';
+import type { ExerciseState } from '../../state';
+import {
+ cloneDeepMutable,
+ StrictObject,
+ UUID,
+ uuidArrayValidationOptions,
+ uuidValidationOptions,
+} from '../../utils';
+import { IsValue } from '../../utils/validators';
+import { IsResourceDescription } from '../../utils/validators/is-resource-description';
+import { ResourceRequiredEvent } from '../events';
+import { sendSimulationEvent } from '../events/utils';
+import { nextUUID } from '../utils/randomness';
+import type {
+ SimulationActivity,
+ SimulationActivityState,
+} from './simulation-activity';
+import type { UnloadVehicleActivityState } from './unload-vehicle';
+
+export class ProvidePersonnelFromVehiclesActivityState
+ implements SimulationActivityState
+{
+ @IsValue('providePersonnelFromVehicleActivity' as const)
+ public readonly type = 'providePersonnelFromVehicleActivity';
+
+ @IsUUID(4, uuidValidationOptions)
+ public readonly id: UUID;
+
+ @IsResourceDescription()
+ public readonly requiredPersonnelCounts: ResourceDescription;
+
+ @IsUUID(4, uuidArrayValidationOptions)
+ public readonly vehiclePriorities: readonly UUID[];
+
+ @IsString()
+ public readonly key: string;
+
+ /**
+ * @deprecated Use {@link create} instead
+ */
+ constructor(
+ id: UUID,
+ requiredPersonnelCounts: ResourceDescription,
+ vehiclePriorities: UUID[],
+ key: string
+ ) {
+ this.vehiclePriorities = vehiclePriorities;
+ this.requiredPersonnelCounts = requiredPersonnelCounts;
+ this.id = id;
+ this.key = key;
+ }
+
+ static readonly create = getCreate(this);
+}
+
+export const providePersonnelFromVehiclesActivity: SimulationActivity =
+ {
+ activityState: ProvidePersonnelFromVehiclesActivityState,
+ tick(
+ draftState,
+ simulatedRegion,
+ activityState,
+ _tickInterval,
+ terminate
+ ) {
+ // We count personnel of unloading vehicles as provided because it should be available shortly.
+ let availablePersonnel = personnelInUnloadingVehicles(
+ draftState,
+ simulatedRegion
+ );
+ const missingPersonnel = activityState.requiredPersonnelCounts;
+
+ if (
+ greaterEqualResourceDescription(
+ availablePersonnel,
+ missingPersonnel
+ )
+ ) {
+ terminate();
+ return;
+ }
+
+ const vehiclePriorities = activityState.vehiclePriorities
+ .map((id) => personnelInVehicleTemplate(draftState, id))
+ .filter(
+ (
+ priority
+ ): priority is {
+ vehicleType: string;
+ vehiclePersonnel: ResourceDescription;
+ } => priority.vehicleType !== undefined
+ );
+ const missingVehicleCounts: ResourceDescription = {};
+
+ const personnelStillMissing = ([personnelType, personnelCount]: [
+ PersonnelType,
+ number
+ ]) => personnelCount > availablePersonnel[personnelType];
+
+ while (
+ !greaterEqualResourceDescription(
+ availablePersonnel,
+ missingPersonnel
+ )
+ ) {
+ const minRequiredVehiclePriorities = StrictObject.entries(
+ missingPersonnel
+ )
+ .filter(personnelStillMissing)
+ .map(([personnelType]) =>
+ vehiclePriorities.findIndex(
+ // Match requested personnel type exactly, no better personnel is accepted as substitute
+ ({ vehiclePersonnel: personnel }) =>
+ personnel[personnelType] > 0
+ )
+ );
+ // We use max here because we need the vehicle with the highest value in this list anyways and might
+ // save 'smaller' vehicles of higher priority that would be extra if we used min
+ const selectedTemplateIndex = Math.max(
+ ...minRequiredVehiclePriorities
+ );
+ if (selectedTemplateIndex === -1) {
+ // The rest of the personnel needs cannot be satisfied with the allowed vehicle templates. They are ignored for now.
+ break;
+ }
+ const { vehicleType, vehiclePersonnel } =
+ vehiclePriorities[selectedTemplateIndex]!;
+ missingVehicleCounts[vehicleType] =
+ (missingVehicleCounts[vehicleType] ?? 0) + 1;
+ availablePersonnel = addResourceDescription(
+ availablePersonnel,
+ vehiclePersonnel
+ );
+ }
+
+ if (
+ Object.values(missingVehicleCounts).some(
+ (vehicleCount) => vehicleCount > 0
+ )
+ ) {
+ const event = ResourceRequiredEvent.create(
+ nextUUID(draftState),
+ simulatedRegion.id,
+ VehicleResource.create(missingVehicleCounts),
+ activityState.key
+ );
+ sendSimulationEvent(simulatedRegion, event);
+ }
+
+ terminate();
+ },
+ };
+
+function personnelInVehicleTemplate(
+ draftState: ExerciseState,
+ templateId: UUID
+): {
+ vehicleType: string | undefined;
+ vehiclePersonnel: ResourceDescription;
+} {
+ const resource = cloneDeepMutable(
+ PersonnelResource.create()
+ ).personnelCounts;
+ const template = draftState.vehicleTemplates.filter(
+ (tp) => tp.id === templateId
+ )[0];
+ if (template) {
+ template.personnel.forEach((pt) => {
+ resource[pt]++;
+ });
+ }
+ return { vehicleType: template?.vehicleType, vehiclePersonnel: resource };
+}
+
+function personnelInUnloadingVehicles(
+ draftState: ExerciseState,
+ simulatedRegion: SimulatedRegion
+): ResourceDescription {
+ const resource = cloneDeepMutable(
+ PersonnelResource.create()
+ ).personnelCounts;
+ StrictObject.values(simulatedRegion.activities)
+ .filter(
+ (a): a is UnloadVehicleActivityState =>
+ a.type === 'unloadVehicleActivity'
+ )
+ .flatMap((activity) =>
+ StrictObject.keys(
+ draftState.vehicles[activity.vehicleId]?.personnelIds ?? {}
+ )
+ )
+ .map((personnelId) => draftState.personnel[personnelId]?.personnelType)
+ .filter((pt): pt is PersonnelType => pt !== undefined)
+ .forEach((pt) => resource[pt]++);
+ return resource;
+}
diff --git a/shared/src/simulation/activities/reassign-treatments.spec.ts b/shared/src/simulation/activities/reassign-treatments.spec.ts
index 581dcb2ef..1e176c1d6 100644
--- a/shared/src/simulation/activities/reassign-treatments.spec.ts
+++ b/shared/src/simulation/activities/reassign-treatments.spec.ts
@@ -1401,7 +1401,7 @@ describe('reassign treatment', () => {
],
},
] as const)(
- 'does not send an event if not all patients are secured (case %#)',
+ 'does not send a treatment progress changed event if not all patients are secured (case %#)',
({ patients, personnel }) => {
const leaderId = uuid();
@@ -1436,7 +1436,12 @@ describe('reassign treatment', () => {
);
expect(terminate).toBeCalled();
- expect(simulatedRegion?.inEvents).toBeEmpty();
+ expect(
+ simulatedRegion?.inEvents.filter(
+ (element) =>
+ element.type === 'treatmentProgressChangedEvent'
+ )
+ ).toBeEmpty();
}
);
diff --git a/shared/src/simulation/activities/reassign-treatments.ts b/shared/src/simulation/activities/reassign-treatments.ts
index 1d061fff3..da467327d 100644
--- a/shared/src/simulation/activities/reassign-treatments.ts
+++ b/shared/src/simulation/activities/reassign-treatments.ts
@@ -1,6 +1,11 @@
import { IsInt, IsOptional, IsUUID, Min } from 'class-validator';
import type { PatientStatus, PersonnelType } from '../../models/utils';
-import { getCreate, isInSpecificSimulatedRegion } from '../../models/utils';
+import {
+ isEmptyResource,
+ PersonnelResource,
+ getCreate,
+ isInSpecificSimulatedRegion,
+} from '../../models/utils';
import type { ExerciseState } from '../../state';
import type { CatersFor } from '../../store/action-reducers/utils/calculate-treatments';
import {
@@ -8,20 +13,35 @@ import {
removeTreatmentsOfElement,
tryToCaterFor,
} from '../../store/action-reducers/utils/calculate-treatments';
-import type { Mutable } from '../../utils';
-import { UUID, uuidValidationOptions } from '../../utils';
+import type { Immutable, Mutable } from '../../utils/immutability';
+import { UUID, uuidValidationOptions } from '../../utils/uuid';
import { IsLiteralUnion, IsValue } from '../../utils/validators';
// Do not import from "../utils" since it would cause circular dependencies
import {
TreatmentProgress,
treatmentProgressAllowedValues,
} from '../utils/treatment';
-import { TreatmentProgressChangedEvent } from '../events';
+import {
+ ResourceRequiredEvent,
+ TreatmentProgressChangedEvent,
+} from '../events';
import { sendSimulationEvent } from '../events/utils';
import type { AssignLeaderBehaviorState } from '../behaviors/assign-leader';
+import { nextUUID } from '../utils/randomness';
+import { defaultPersonnelTemplates } from '../../data/default-state/personnel-templates';
+import { StrictObject } from '../../utils/strict-object';
+import { cloneDeepMutable } from '../../utils/clone-deep';
import type { Material } from '../../models/material';
import type { Personnel } from '../../models/personnel';
import { Patient } from '../../models/patient';
+import type { ResourceDescription } from '../../models/utils/resource-description';
+import {
+ addResourceDescription,
+ ceilResourceDescription,
+ maxResourceDescription,
+ scaleResourceDescription,
+ subtractResourceDescription,
+} from '../../models/utils/resource-description';
import type {
SimulationActivity,
SimulationActivityState,
@@ -112,6 +132,8 @@ export const reassignTreatmentsActivity: SimulationActivity;
+
interface CateringMaterial {
material: Mutable;
catersFor: CatersFor;
@@ -187,6 +226,18 @@ interface CateringPersonnel {
catersFor: CatersFor;
}
+type PatientToPersonnelDict = {
+ [patientId in UUID]: { [personnelType in PersonnelType]?: UUID[] };
+};
+
+type PersonnelToPatientCategoryDict = {
+ [personnelId in UUID]: {
+ [personnelType in PersonnelType]?: {
+ [sk in TreatablePatientStatus]?: number;
+ };
+ };
+};
+
function createCateringMaterials(
materials: Mutable[]
): CateringMaterial[] {
@@ -204,6 +255,72 @@ const personnelPriorities: { [Key in PersonnelType]: number } = {
notarzt: 4,
};
+// Estimation for required personnel based on the 7/2/1 rule.
+const estimatedSkDistribution = { green: 7 / 10, yellow: 2 / 10, red: 1 / 10 };
+
+// We only ever allow two patients to be treated by one personnel, except for notarzt
+const capacities = Object.fromEntries(
+ StrictObject.entries(defaultPersonnelTemplates).map(
+ ([personnelType, { canCaterFor }]) => [
+ personnelType,
+ {
+ red: Math.min(2, canCaterFor.red),
+ yellow: Math.min(2, canCaterFor.yellow),
+ green: Math.min(2, canCaterFor.green),
+ },
+ ]
+ )
+) as { [key in PersonnelType]: ResourceDescription };
+capacities.notarzt = {
+ green: defaultPersonnelTemplates.notarzt.canCaterFor.green,
+ red: defaultPersonnelTemplates.notarzt.canCaterFor.red,
+ yellow: defaultPersonnelTemplates.notarzt.canCaterFor.yellow,
+};
+
+const minimumRequiredPersonnel: {
+ [SK in TreatablePatientStatus]: Immutable<
+ ResourceDescription
+ >;
+} = {
+ red: {
+ gf: 0,
+ san: 0,
+ rettSan: 1,
+ notSan: 1,
+ notarzt: 1 / capacities.notarzt.red,
+ },
+ yellow: {
+ gf: 0,
+ san: 0,
+ rettSan: 1,
+ notSan: 0,
+ notarzt: 1 / capacities.notarzt.yellow,
+ },
+ green: {
+ gf: 0,
+ san: 1 / capacities.san.green,
+ rettSan: 0,
+ notSan: 0,
+ notarzt: 0,
+ },
+};
+
+const requiresExclusivity: {
+ [SK in TreatablePatientStatus]?: { [Per in PersonnelType]?: boolean };
+} = {
+ red: { rettSan: true, notSan: true },
+ yellow: { rettSan: true },
+};
+
+// Ascending priority
+const personnelTypePriorityList = StrictObject.entries(personnelPriorities)
+ .sort(([typeA, priorityA], [typeB, priorityB]) => priorityA - priorityB)
+ .map(([personnelType]) => personnelType);
+// Descending priority
+const reversedPersonnelTypePriorityList = [
+ ...personnelTypePriorityList,
+].reverse();
+
function createCateringPersonnel(
personnel: Mutable[]
): CateringPersonnel[] {
@@ -249,14 +366,15 @@ function count(
* @param patients A list of the patients to operate on
* @param personnel A list of the personnel to operate on
* @param materials A list of the patients to operate on
- * @returns Whether all patients are triaged
+ * @returns Whether all patients are triaged and estimated numbers of personnel that is missing to secure treatment
*/
function triage(
draftState: Mutable,
patients: Mutable[],
personnel: Mutable[],
materials: Mutable[]
-): boolean {
+): [boolean, PersonnelResource] {
+ const missingPersonnel = estimateRequiredPersonnel(patients, personnel);
const cateringPersonnel = createCateringPersonnel(personnel).sort(
(a, b) => a.priority - b.priority
);
@@ -288,8 +406,8 @@ function triage(
});
assignTreatments(draftState, patientsToTreat, cateringPersonnel, materials);
-
- return patientsToTreat.length === patients.length;
+ const triageDone = patientsToTreat.length === patients.length;
+ return [triageDone, missingPersonnel];
}
/**
@@ -299,14 +417,14 @@ function triage(
* @param patients A list of the patients to operate on
* @param personnel A list of the personnel to operate on
* @param materials A list of the patients to operate on
- * @returns Whether the treatment for all patients is secured.
+ * @returns Whether the treatment for all patients is secured and numbers of personnel that is missing to secure treatment.
*/
function treat(
draftState: Mutable,
patients: Mutable[],
personnel: Mutable[],
materials: Mutable[]
-): boolean {
+): [boolean, PersonnelResource] {
return assignTreatments(
draftState,
patients,
@@ -357,7 +475,7 @@ function hasNoTreatments(cateringPersonnel: CateringPersonnel): boolean {
*/
function hasNoHigherTreatments(
cateringPersonnel: CateringPersonnel,
- status: Exclude
+ status: TreatablePatientStatus
): boolean {
if (status === 'green') {
return (
@@ -399,7 +517,7 @@ function findAssignablePersonnel(
[Key in PersonnelType]?: CateringPersonnel[];
},
minType: PersonnelType,
- patientStatus: Exclude,
+ patientStatus: TreatablePatientStatus,
maxPatients: number,
mixWithHigherStatus = true
): { personnel: CateringPersonnel; isExclusive: boolean } | undefined {
@@ -427,42 +545,20 @@ function findAssignablePersonnel(
return { personnel: availablePersonnel, isExclusive: false };
}
- switch (minType) {
- case 'gf':
- return findAssignablePersonnel(
- groupedPersonnel,
- 'san',
- patientStatus,
- maxPatients,
- mixWithHigherStatus
- );
- case 'san':
- return findAssignablePersonnel(
- groupedPersonnel,
- 'rettSan',
- patientStatus,
- maxPatients,
- mixWithHigherStatus
- );
- case 'rettSan':
- return findAssignablePersonnel(
- groupedPersonnel,
- 'notSan',
- patientStatus,
- maxPatients,
- mixWithHigherStatus
- );
- case 'notSan':
- return findAssignablePersonnel(
- groupedPersonnel,
- 'notarzt',
- patientStatus,
- maxPatients,
- mixWithHigherStatus
- );
- case 'notarzt':
- return undefined;
+ const nextType =
+ personnelTypePriorityList[
+ personnelTypePriorityList.indexOf(minType) + 1
+ ];
+ if (nextType === undefined) {
+ return undefined;
}
+ return findAssignablePersonnel(
+ groupedPersonnel,
+ nextType,
+ patientStatus,
+ maxPatients,
+ mixWithHigherStatus
+ );
}
/**
@@ -474,14 +570,14 @@ function findAssignablePersonnel(
* @param cateringPersonnel A list of the personnel to operate on.
* Personnel may be treating some other patients already, expressed by the catersFor property.
* @param materials A list of the materials to operate on.
- * @returns Whether the treatment for all patients is secured.
+ * @returns Whether the treatment for all patients is secured and numbers of personnel that is missing to secure treatment.
*/
function assignTreatments(
draftState: Mutable,
patients: Mutable[],
cateringPersonnel: CateringPersonnel[],
materials: Mutable[]
-): boolean {
+): [boolean, PersonnelResource] {
const groupedPatients = groupBy(patients, (patient) =>
Patient.getVisibleStatus(
patient,
@@ -495,92 +591,27 @@ function assignTreatments(
(personnel) => personnel.personnel.personnelType
);
- let securedPatients = 0;
-
- groupedPatients.red?.forEach((patient) => {
- const assignableNotSan = findAssignablePersonnel(
- groupedPersonnel,
- 'notSan',
- 'red',
- 2
- );
-
- if (assignableNotSan) {
- tryToCaterFor(
- assignableNotSan.personnel.personnel,
- assignableNotSan.personnel.catersFor,
- patient,
- draftState.configuration.pretriageEnabled,
- draftState.configuration.bluePatientsEnabled
- );
- }
-
- const assignableRettSan = findAssignablePersonnel(
- groupedPersonnel,
- 'rettSan',
- 'red',
- 2
- );
+ const patientTreatments: PatientToPersonnelDict = {};
+ const personnelTreatments: PersonnelToPatientCategoryDict = {};
- if (assignableRettSan) {
- tryToCaterFor(
- assignableRettSan.personnel.personnel,
- assignableRettSan.personnel.catersFor,
- patient,
- draftState.configuration.pretriageEnabled,
- draftState.configuration.bluePatientsEnabled
- );
- }
+ const treatmentContext = {
+ groupedPersonnel,
+ draftState,
+ personnelTreatments,
+ patientTreatments,
+ };
- if (assignableNotSan?.isExclusive && assignableRettSan?.isExclusive) {
- securedPatients++;
- }
+ groupedPatients.red?.forEach((patient) => {
+ tryAssignPersonnel(patient, 'red', 'notSan', treatmentContext);
+ tryAssignPersonnel(patient, 'red', 'rettSan', treatmentContext);
});
groupedPatients.yellow?.forEach((patient) => {
- const assignableRettSan = findAssignablePersonnel(
- groupedPersonnel,
- 'rettSan',
- 'yellow',
- 2
- );
-
- if (assignableRettSan) {
- tryToCaterFor(
- assignableRettSan.personnel.personnel,
- assignableRettSan.personnel.catersFor,
- patient,
- draftState.configuration.pretriageEnabled,
- draftState.configuration.bluePatientsEnabled
- );
-
- if (assignableRettSan.isExclusive) {
- securedPatients++;
- }
- }
+ tryAssignPersonnel(patient, 'yellow', 'rettSan', treatmentContext);
});
groupedPatients.green?.forEach((patient) => {
- const assignableSan = findAssignablePersonnel(
- groupedPersonnel,
- 'san',
- 'green',
- 2,
- false
- );
-
- if (assignableSan) {
- tryToCaterFor(
- assignableSan.personnel.personnel,
- assignableSan.personnel.catersFor,
- patient,
- draftState.configuration.pretriageEnabled,
- draftState.configuration.bluePatientsEnabled
- );
-
- // Green patients do not need individual treatment to be secured
- securedPatients++;
- }
+ tryAssignPersonnel(patient, 'green', 'san', treatmentContext, false);
});
const cateringMaterials = createCateringMaterials(materials);
@@ -592,7 +623,37 @@ function assignTreatments(
hasNoTreatments(notarzt)
) ?? [];
- let patientsWithNotarzt = 0;
+ [
+ ...(groupedPatients.red?.map((patient) => ['red', patient] as const) ??
+ []),
+ ...(groupedPatients.yellow?.map(
+ (patient) => ['yellow', patient] as const
+ ) ?? []),
+ ].forEach(([visibleStatus, patient]) => {
+ // Usually, notarzts are needed for some specific tasks, but they do not have to treat a patient continuously and exclusively.
+ // Therefore, we can just use this simple approach based on their normal treatment capacity
+ remainingNotarzts.some((notarzt) => {
+ if (
+ tryToCaterFor(
+ notarzt.personnel,
+ notarzt.catersFor,
+ patient,
+ draftState.configuration.pretriageEnabled,
+ draftState.configuration.bluePatientsEnabled
+ )
+ ) {
+ recordPatientTreatedByPersonnel(
+ patient.id,
+ visibleStatus,
+ notarzt.personnel.id,
+ 'notarzt',
+ treatmentContext
+ );
+ return true;
+ }
+ return false;
+ });
+ });
[
...(groupedPatients.red ?? []),
@@ -610,38 +671,269 @@ function assignTreatments(
draftState.configuration.bluePatientsEnabled
)
);
-
- if (
- Patient.getVisibleStatus(
- patient,
- draftState.configuration.pretriageEnabled,
- draftState.configuration.bluePatientsEnabled
- ) !== 'green'
- ) {
- // Usually, notarzts are needed for some specific tasks, but they do not have to treat a patient continuously and exclusively.
- // Therefore, we can just use this simple approach based on their normal treatment capacity
- if (
- remainingNotarzts.some((notarzt) =>
- tryToCaterFor(
- notarzt.personnel,
- notarzt.catersFor,
- patient,
- draftState.configuration.pretriageEnabled,
- draftState.configuration.bluePatientsEnabled
- )
- )
- ) {
- patientsWithNotarzt++;
- }
- }
});
- const redAndYellowPatientsCount =
- (groupedPatients.red?.length ?? 0) +
- (groupedPatients.yellow?.length ?? 0);
+ const [secured, missingPersonnel] = calculateMissingPersonnel(
+ groupedPatients,
+ cateringPersonnel,
+ personnelTreatments,
+ patientTreatments
+ );
+ return [secured, PersonnelResource.create(missingPersonnel)];
+}
- return (
- securedPatients >= patients.length &&
- patientsWithNotarzt >= redAndYellowPatientsCount
+function tryAssignPersonnel(
+ patient: Mutable,
+ patientStatus: TreatablePatientStatus,
+ minType: PersonnelType,
+ context: {
+ groupedPersonnel: { [K in PersonnelType]?: CateringPersonnel[] };
+ personnelTreatments: PersonnelToPatientCategoryDict;
+ patientTreatments: PatientToPersonnelDict;
+ draftState: Mutable;
+ },
+ mixWithHigherStatus = true,
+ maxPatients: number = 2
+) {
+ const { groupedPersonnel, draftState } = context;
+ const assignablePersonnel = findAssignablePersonnel(
+ groupedPersonnel,
+ minType,
+ patientStatus,
+ maxPatients,
+ mixWithHigherStatus
);
+
+ if (assignablePersonnel) {
+ tryToCaterFor(
+ assignablePersonnel.personnel.personnel,
+ assignablePersonnel.personnel.catersFor,
+ patient,
+ draftState.configuration.pretriageEnabled,
+ draftState.configuration.bluePatientsEnabled
+ );
+ recordPatientTreatedByPersonnel(
+ patient.id,
+ patientStatus,
+ assignablePersonnel.personnel.personnel.id,
+ minType,
+ context
+ );
+ }
+}
+
+function recordPatientTreatedByPersonnel(
+ patientId: UUID,
+ patientStatus: TreatablePatientStatus,
+ personnelId: UUID,
+ minType: PersonnelType,
+ context: {
+ personnelTreatments: PersonnelToPatientCategoryDict;
+ patientTreatments: PatientToPersonnelDict;
+ }
+) {
+ const { personnelTreatments, patientTreatments } = context;
+ if (personnelTreatments[personnelId] === undefined)
+ personnelTreatments[personnelId] = {};
+ if (personnelTreatments[personnelId]![minType] === undefined)
+ personnelTreatments[personnelId]![minType] = {};
+ personnelTreatments[personnelId]![minType]![patientStatus] =
+ (personnelTreatments[personnelId]![minType]![patientStatus] ?? 0) + 1;
+
+ if (patientTreatments[patientId] === undefined)
+ patientTreatments[patientId] = {};
+ if (patientTreatments[patientId]![minType] === undefined)
+ patientTreatments[patientId]![minType] = [];
+ patientTreatments[patientId]![minType]!.push(personnelId);
+}
+
+/**
+ * Computes an estimation of required personnel based on the number of patients and typical MCI rules of thumb.
+ * @param patients patients to treat, ignoring their triage status
+ * @param personnel available personnel
+ * @returns the types and number of personnel estimated to missing to secure treatment
+ */
+function estimateRequiredPersonnel(
+ patients: Patient[],
+ personnel: Personnel[]
+): PersonnelResource {
+ const patientCount = patients.length;
+ const groupedPersonnel = groupBy(personnel, (p) => p.personnelType);
+ const havePersonnel = Object.fromEntries(
+ StrictObject.keys(personnelPriorities).map((pt) => [
+ pt,
+ groupedPersonnel[pt]?.length ?? 0,
+ ])
+ ) as ResourceDescription;
+ const wantPersonnel = requiredPersonnelForPatients(
+ scaleResourceDescription(estimatedSkDistribution, patientCount)
+ );
+
+ // We want the missing personnel exactly, no substitution of personnel tiers takes place.
+ return PersonnelResource.create(
+ maxResourceDescription(
+ subtractResourceDescription(wantPersonnel, havePersonnel),
+ 0
+ )
+ );
+}
+
+function requiredPersonnelForPatients(
+ patientCounts: ResourceDescription
+): ResourceDescription {
+ const requiredByType = Object.fromEntries(
+ StrictObject.entries(patientCounts).map(
+ ([patientType, patientCount]) => [
+ patientType,
+ scaleResourceDescription(
+ minimumRequiredPersonnel[patientType],
+ patientCount
+ ),
+ ]
+ )
+ );
+ return addResourceDescription(
+ ceilResourceDescription(
+ addResourceDescription(
+ requiredByType['red']!,
+ requiredByType['yellow']!
+ )
+ ),
+ // Green should not be mixed. Therefore we ceil them before merging.
+ ceilResourceDescription(requiredByType['green']!)
+ );
+}
+
+function calculateMissingPersonnel(
+ groupedPatients: { [Key in TreatablePatientStatus]?: Patient[] },
+ personnel: CateringPersonnel[],
+ personnelTreatments: PersonnelToPatientCategoryDict,
+ patientTreatments: PatientToPersonnelDict
+) {
+ const cateringPersonnelDict = Object.fromEntries(
+ personnel.map((cateringPersonnel) => [
+ cateringPersonnel.personnel.id,
+ cateringPersonnel,
+ ])
+ );
+
+ const patientStatusMissingPersonnel = Object.fromEntries(
+ (['red', 'yellow', 'green'] as const).map((patientStatus) => {
+ const patients = groupedPatients[patientStatus] ?? [];
+
+ let statusMissingPersonnel =
+ PersonnelResource.create().personnelCounts;
+ patients.forEach((patient) => {
+ const patientMissingPersonnel = cloneDeepMutable(
+ minimumRequiredPersonnel[patientStatus]
+ );
+
+ personnelTypePriorityList.forEach((personnelType) => {
+ (patientTreatments[patient.id]?.[personnelType] ?? [])
+ .map(
+ (personnelId) => cateringPersonnelDict[personnelId]
+ )
+ .forEach((p) => {
+ if (p) {
+ patientMissingPersonnel[personnelType] -=
+ 1 /
+ (requiresExclusivity[patientStatus]?.[
+ personnelType
+ ]
+ ? sumOfTreatments(p)
+ : p.catersFor[patientStatus]);
+ }
+ });
+ });
+ statusMissingPersonnel = addResourceDescription(
+ statusMissingPersonnel,
+ maxResourceDescription(patientMissingPersonnel, 0)
+ );
+ });
+ return [patientStatus, statusMissingPersonnel] as const;
+ })
+ ) as { [K in TreatablePatientStatus]: ResourceDescription };
+
+ const totalMissingPersonnel = addResourceDescription(
+ ceilResourceDescription(
+ addResourceDescription(
+ patientStatusMissingPersonnel.red,
+ patientStatusMissingPersonnel.yellow
+ )
+ ),
+ ceilResourceDescription(patientStatusMissingPersonnel.green)
+ );
+
+ totalMissingPersonnel.notarzt = Math.max(
+ patientStatusMissingPersonnel.red.notarzt,
+ patientStatusMissingPersonnel.yellow.notarzt
+ );
+
+ const secured = StrictObject.values(totalMissingPersonnel).every(
+ (cnt) => cnt <= 0
+ );
+
+ if (secured) {
+ return [secured, totalMissingPersonnel] as const;
+ }
+
+ // Try to substitute missing personnel to request low priorities if that helps.
+ // For example, if we need notarzt and one is treating a green patient, we request a san instead.
+ const finalMissingPersonnel = calculateSubstitutedMissingPersonnel(
+ totalMissingPersonnel,
+ cateringPersonnelDict,
+ personnelTreatments
+ );
+ return [secured, finalMissingPersonnel] as const;
+}
+
+function calculateSubstitutedMissingPersonnel(
+ missingPersonnel: ResourceDescription,
+ cateringDict: { [k: string]: CateringPersonnel },
+ personnelTreatments: PersonnelToPatientCategoryDict
+): ResourceDescription {
+ let missing = { ...missingPersonnel };
+ const substitutions = StrictObject.entries(personnelTreatments)
+ .map(
+ ([persId, roles]) =>
+ [cateringDict[persId]!.personnel.personnelType, roles] as const
+ )
+ .filter(([realPersonnelType, roles]) =>
+ StrictObject.keys(roles).some(
+ (personnelType) => personnelType !== realPersonnelType
+ )
+ )
+ .map(([realPersonnelType, roles]) => {
+ const from = Object.fromEntries(
+ personnelTypePriorityList.map((personnelType) => {
+ const role = roles[personnelType];
+ const requiredPersonnelCount = role
+ ? StrictObject.keys(role)
+ .map(
+ (patientType) =>
+ (role[patientType] ?? 0) /
+ capacities[personnelType][patientType]
+ )
+ .reduce((a, b) => Math.max(a, b), 0)
+ : 0;
+
+ return [personnelType, requiredPersonnelCount];
+ })
+ ) as ResourceDescription;
+ return { from, to: realPersonnelType };
+ });
+
+ reversedPersonnelTypePriorityList.forEach((personnelType) => {
+ while (missing[personnelType] > 0) {
+ const substitutionIndex = substitutions.findIndex(
+ (s) => s.to === personnelType
+ );
+ if (substitutionIndex === -1) break;
+ const substitution = substitutions.splice(substitutionIndex, 1)[0]!;
+ missing[substitution!.to] -= 1;
+
+ missing = addResourceDescription(missing, substitution.from);
+ }
+ });
+ return maxResourceDescription(ceilResourceDescription(missing), 0);
}
diff --git a/shared/src/simulation/activities/transfer-vehicles.ts b/shared/src/simulation/activities/transfer-vehicles.ts
index be87f8b64..a5326a910 100644
--- a/shared/src/simulation/activities/transfer-vehicles.ts
+++ b/shared/src/simulation/activities/transfer-vehicles.ts
@@ -14,7 +14,7 @@ import {
TransferStartPoint,
} from '../../models/utils';
import { amountOfResourcesInVehicle } from '../../models/utils/amount-of-resources-in-vehicle';
-import { VehicleResource } from '../../models/utils/vehicle-resource';
+import { VehicleResource } from '../../models/utils/rescue-resource';
import { TransferActionReducers } from '../../store/action-reducers/transfer';
import {
getElement,
diff --git a/shared/src/simulation/behaviors/answer-requests.ts b/shared/src/simulation/behaviors/answer-requests.ts
index af3131c87..d863c3707 100644
--- a/shared/src/simulation/behaviors/answer-requests.ts
+++ b/shared/src/simulation/behaviors/answer-requests.ts
@@ -5,7 +5,6 @@ import { UUID, uuid, uuidValidationOptions } from '../../utils';
import { IsValue } from '../../utils/validators';
import { TransferVehiclesActivityState } from '../activities';
import { addActivity } from '../activities/utils';
-import type { ResourceRequiredEvent } from '../events';
import { nextUUID } from '../utils/randomness';
import type {
SimulationBehavior,
@@ -28,32 +27,30 @@ export const answerRequestsBehavior: SimulationBehavior {
switch (event.type) {
case 'resourceRequiredEvent': {
- const resourceRequiredEvent =
- event as ResourceRequiredEvent;
-
if (
- resourceRequiredEvent.requiringSimulatedRegionId !==
- simulatedRegion.id
+ event.requiringSimulatedRegionId !== simulatedRegion.id
) {
- const requiringSimulatedRegionTransferPoint =
- getElementByPredicate(
- draftState,
- 'transferPoint',
- (transferPoint) =>
- isInSpecificSimulatedRegion(
- transferPoint,
- resourceRequiredEvent.requiringSimulatedRegionId
- )
+ if (event.requiredResource.type === 'vehicleResource') {
+ const requiringSimulatedRegionTransferPoint =
+ getElementByPredicate(
+ draftState,
+ 'transferPoint',
+ (transferPoint) =>
+ isInSpecificSimulatedRegion(
+ transferPoint,
+ event.requiringSimulatedRegionId
+ )
+ );
+ addActivity(
+ simulatedRegion,
+ TransferVehiclesActivityState.create(
+ nextUUID(draftState),
+ requiringSimulatedRegionTransferPoint.id,
+ requiringSimulatedRegionTransferPoint.id,
+ event.requiredResource
+ )
);
- addActivity(
- simulatedRegion,
- TransferVehiclesActivityState.create(
- nextUUID(draftState),
- requiringSimulatedRegionTransferPoint.id,
- requiringSimulatedRegionTransferPoint.id,
- resourceRequiredEvent.requiredResource
- )
- );
+ }
}
break;
}
diff --git a/shared/src/simulation/behaviors/exercise-simulation-behavior.ts b/shared/src/simulation/behaviors/exercise-simulation-behavior.ts
index 6c1eec02a..16c16c12c 100644
--- a/shared/src/simulation/behaviors/exercise-simulation-behavior.ts
+++ b/shared/src/simulation/behaviors/exercise-simulation-behavior.ts
@@ -4,6 +4,7 @@ import { assignLeaderBehavior } from './assign-leader';
import { treatPatientsBehavior } from './treat-patients';
import { unloadArrivingVehiclesBehavior } from './unload-arrived-vehicles';
import { reportBehavior } from './report';
+import { providePersonnelBehavior } from './provide-personnel';
import { answerRequestsBehavior } from './answer-requests';
import { requestBehavior } from './request';
@@ -12,6 +13,7 @@ export const simulationBehaviors = {
treatPatientsBehavior,
unloadArrivingVehiclesBehavior,
reportBehavior,
+ providePersonnelBehavior,
answerRequestsBehavior,
requestBehavior,
};
@@ -23,7 +25,7 @@ export type ExerciseSimulationBehaviorType = InstanceType<
ExerciseSimulationBehavior['behaviorState']
>['type'];
-type ExerciseSimulationBehaviorDictionary = {
+export type ExerciseSimulationBehaviorDictionary = {
[Behavior in ExerciseSimulationBehavior as InstanceType<
Behavior['behaviorState']
>['type']]: Behavior;
diff --git a/shared/src/simulation/behaviors/index.ts b/shared/src/simulation/behaviors/index.ts
index d715a6005..5de87e447 100644
--- a/shared/src/simulation/behaviors/index.ts
+++ b/shared/src/simulation/behaviors/index.ts
@@ -3,5 +3,6 @@ export * from './exercise-simulation-behavior';
export * from './treat-patients';
export * from './unload-arrived-vehicles';
export * from './report';
+export * from './provide-personnel';
export * from './utils';
export * from './request';
diff --git a/shared/src/simulation/behaviors/provide-personnel.ts b/shared/src/simulation/behaviors/provide-personnel.ts
new file mode 100644
index 000000000..6d6a6103a
--- /dev/null
+++ b/shared/src/simulation/behaviors/provide-personnel.ts
@@ -0,0 +1,57 @@
+import { IsUUID } from 'class-validator';
+import { getCreate } from '../../models/utils';
+import {
+ UUID,
+ uuid,
+ uuidArrayValidationOptions,
+ uuidValidationOptions,
+} from '../../utils';
+import { IsValue } from '../../utils/validators';
+import { ProvidePersonnelFromVehiclesActivityState } from '../activities/provide-personnel-from-vehicles';
+import { addActivity } from '../activities/utils';
+import { nextUUID } from '../utils/randomness';
+import type {
+ SimulationBehavior,
+ SimulationBehaviorState,
+} from './simulation-behavior';
+
+export class ProvidePersonnelBehaviorState implements SimulationBehaviorState {
+ @IsValue('providePersonnelBehavior' as const)
+ readonly type = 'providePersonnelBehavior';
+
+ @IsUUID(4, uuidValidationOptions)
+ public readonly id: UUID = uuid();
+
+ @IsUUID(4, uuidArrayValidationOptions)
+ public readonly vehicleTemplatePriorities: readonly UUID[];
+
+ /**
+ * @deprecated Use {@link create} instead.
+ */
+ constructor(vehicleTemplatePriorities?: UUID[]) {
+ this.vehicleTemplatePriorities = vehicleTemplatePriorities ?? [];
+ }
+
+ static readonly create = getCreate(this);
+}
+
+export const providePersonnelBehavior: SimulationBehavior =
+ {
+ behaviorState: ProvidePersonnelBehaviorState,
+ handleEvent(draftState, simulatedRegion, behaviorState, event) {
+ if (
+ event.type === 'resourceRequiredEvent' &&
+ event.requiredResource.type === 'personnelResource'
+ ) {
+ addActivity(
+ simulatedRegion,
+ ProvidePersonnelFromVehiclesActivityState.create(
+ nextUUID(draftState),
+ event.requiredResource.personnelCounts,
+ behaviorState.vehicleTemplatePriorities,
+ event.key
+ )
+ );
+ }
+ },
+ };
diff --git a/shared/src/simulation/behaviors/request.ts b/shared/src/simulation/behaviors/request.ts
index daab9c671..f075f9ccd 100644
--- a/shared/src/simulation/behaviors/request.ts
+++ b/shared/src/simulation/behaviors/request.ts
@@ -9,7 +9,7 @@ import {
} from 'class-validator';
import { Type } from 'class-transformer';
import { IsStringMap } from '../../utils/validators/is-string-map';
-import { cloneDeepMutable, uuid, UUID } from '../../utils';
+import { cloneDeepMutable, StrictObject, uuid, UUID } from '../../utils';
import type { Mutable } from '../../utils';
import { IsValue } from '../../utils/validators';
import { getActivityById, getElement } from '../../store/action-reducers/utils';
@@ -20,10 +20,9 @@ import { RecurringEventActivityState } from '../activities';
import { SendRequestEvent } from '../events/send-request';
import { CreateRequestActivityState } from '../activities/create-request';
import {
+ isEmptyResource,
VehicleResource,
- aggregateResources,
- subtractResources,
-} from '../../models/utils/vehicle-resource';
+} from '../../models/utils/rescue-resource';
import {
ExerciseRequestTargetConfiguration,
requestTargetTypeOptions,
@@ -32,6 +31,11 @@ import { TraineesRequestTargetConfiguration } from '../../models/utils/request-t
import { getCreate } from '../../models/utils/get-create';
import type { SimulatedRegion } from '../../models';
import { ResourcePromise } from '../utils/resource-promise';
+import type { ResourceDescription } from '../../models/utils/resource-description';
+import {
+ addPartialResourceDescriptions,
+ subtractPartialResourceDescriptions,
+} from '../../models/utils/resource-description';
import type {
SimulationBehavior,
SimulationBehaviorState,
@@ -109,7 +113,10 @@ export const requestBehavior: SimulationBehavior = {
break;
}
case 'resourceRequiredEvent': {
- if (event.requiringSimulatedRegionId === simulatedRegion.id) {
+ if (
+ event.requiringSimulatedRegionId === simulatedRegion.id &&
+ event.requiredResource.type === 'vehicleResource'
+ ) {
behaviorState.requestedResources[event.key] =
event.requiredResource;
}
@@ -136,21 +143,23 @@ export const requestBehavior: SimulationBehavior = {
'vehicle',
event.vehicleId
);
- let arrivatedResource = cloneDeepMutable(
- VehicleResource.create({ [vehicle.vehicleType]: 1 })
- );
+ let arrivedResourceDescripton: Partial = {
+ [vehicle.vehicleType]: 1,
+ };
behaviorState.promisedResources.forEach((promise) => {
- const remainingResources = subtractResources(
- arrivatedResource,
- promise.resource
- );
+ const remainingResources =
+ subtractPartialResourceDescriptions(
+ arrivedResourceDescripton,
+ promise.resource.vehicleCounts
+ );
- promise.resource = subtractResources(
- promise.resource,
- arrivatedResource
- );
+ promise.resource.vehicleCounts =
+ subtractPartialResourceDescriptions(
+ promise.resource.vehicleCounts,
+ arrivedResourceDescripton
+ ) as ResourceDescription;
- arrivatedResource = remainingResources;
+ arrivedResourceDescripton = remainingResources;
});
behaviorState.promisedResources =
behaviorState.promisedResources.filter(
@@ -167,9 +176,10 @@ export const requestBehavior: SimulationBehavior = {
simulatedRegion,
behaviorState
);
- if (
- Object.keys(resourcesToRequest.vehicleCounts).length > 0
- ) {
+ const resource = VehicleResource.create(
+ resourcesToRequest as ResourceDescription
+ );
+ if (!isEmptyResource(resource)) {
// create a request to wait for an answer
behaviorState.answerKey = `${simulatedRegion.id}-request-${behaviorState.requestTargetVersion}`;
const activityId = nextUUID(draftState);
@@ -178,7 +188,7 @@ export const requestBehavior: SimulationBehavior = {
CreateRequestActivityState.create(
activityId,
behaviorState.requestTarget,
- resourcesToRequest,
+ resource,
behaviorState.answerKey
)
);
@@ -233,8 +243,10 @@ export function getResourcesToRequest(
simulatedRegion: Mutable,
behaviorState: Mutable
) {
- const requestedResources = aggregateResources(
- Object.values(behaviorState.requestedResources)
+ const requestedResources = addPartialResourceDescriptions(
+ StrictObject.values(behaviorState.requestedResources).map(
+ (resource) => resource.vehicleCounts
+ )
);
// remove invalidated resources
@@ -248,8 +260,13 @@ export function getResourcesToRequest(
firstValidIndex = behaviorState.promisedResources.length;
behaviorState.promisedResources.splice(0, firstValidIndex);
- const promisedResources = aggregateResources(
- behaviorState.promisedResources.map((promise) => promise.resource)
+ const promisedResources = addPartialResourceDescriptions(
+ behaviorState.promisedResources.map(
+ (promise) => promise.resource.vehicleCounts
+ )
+ );
+ return subtractPartialResourceDescriptions(
+ requestedResources,
+ promisedResources
);
- return subtractResources(requestedResources, promisedResources);
}
diff --git a/shared/src/simulation/behaviors/simulation-behavior.ts b/shared/src/simulation/behaviors/simulation-behavior.ts
index 5532b2868..330f45df5 100644
--- a/shared/src/simulation/behaviors/simulation-behavior.ts
+++ b/shared/src/simulation/behaviors/simulation-behavior.ts
@@ -8,8 +8,13 @@ export class SimulationBehaviorState {
readonly id!: UUID;
}
-export interface SimulationBehavior {
- readonly behaviorState: Constructor;
+export interface SimulationBehavior<
+ S extends SimulationBehaviorState,
+ C extends Constructor = Constructor
+> {
+ readonly behaviorState: C & {
+ readonly create: (...args: ConstructorParameters) => S;
+ };
readonly handleEvent: (
draftState: Mutable,
simulatedRegion: Mutable,
diff --git a/shared/src/simulation/events/resources-required.ts b/shared/src/simulation/events/resources-required.ts
index 36a51d9a9..a71fa2567 100644
--- a/shared/src/simulation/events/resources-required.ts
+++ b/shared/src/simulation/events/resources-required.ts
@@ -1,7 +1,10 @@
import { Type } from 'class-transformer';
import { IsString, IsUUID, ValidateNested } from 'class-validator';
import { getCreate } from '../../models/utils/get-create';
-import { VehicleResource } from '../../models/utils/vehicle-resource';
+import {
+ ExerciseRescueResource,
+ rescueResourceTypeOptions,
+} from '../../models/utils/rescue-resource';
import { UUID, uuidValidationOptions } from '../../utils';
import { IsValue } from '../../utils/validators';
import type { SimulationEvent } from './simulation-event';
@@ -17,8 +20,8 @@ export class ResourceRequiredEvent implements SimulationEvent {
readonly requiringSimulatedRegionId: UUID;
@ValidateNested()
- @Type(() => VehicleResource)
- readonly requiredResource: VehicleResource;
+ @Type(...rescueResourceTypeOptions)
+ readonly requiredResource: ExerciseRescueResource;
/**
* Used for deduplication of needs between different events of this type
@@ -32,7 +35,7 @@ export class ResourceRequiredEvent implements SimulationEvent {
constructor(
id: UUID,
requiringSimulatedRegionId: UUID,
- requiredResource: VehicleResource,
+ requiredResource: ExerciseRescueResource,
key: string
) {
this.id = id;
diff --git a/shared/src/simulation/events/vehicles-sent.ts b/shared/src/simulation/events/vehicles-sent.ts
index 7e97f0fc5..58903ba05 100644
--- a/shared/src/simulation/events/vehicles-sent.ts
+++ b/shared/src/simulation/events/vehicles-sent.ts
@@ -1,7 +1,7 @@
import { Type } from 'class-transformer';
import { IsString, IsUUID, ValidateNested } from 'class-validator';
import { getCreate } from '../../models/utils';
-import { VehicleResource } from '../../models/utils/vehicle-resource';
+import { VehicleResource } from '../../models/utils/rescue-resource';
import { UUID, uuidValidationOptions } from '../../utils';
import { IsValue } from '../../utils/validators';
import type { SimulationEvent } from './simulation-event';
diff --git a/shared/src/simulation/utils/resource-promise.ts b/shared/src/simulation/utils/resource-promise.ts
index 321c7a91a..9884f0e3c 100644
--- a/shared/src/simulation/utils/resource-promise.ts
+++ b/shared/src/simulation/utils/resource-promise.ts
@@ -1,6 +1,6 @@
import { Type } from 'class-transformer';
import { IsInt, Min, ValidateNested } from 'class-validator';
-import { VehicleResource } from '../../models/utils/vehicle-resource';
+import { VehicleResource } from '../../models/utils/rescue-resource';
import { getCreate } from '../../models/utils/get-create';
import { IsValue } from '../../utils/validators/is-value';
diff --git a/shared/src/store/action-reducers/radiogram.ts b/shared/src/store/action-reducers/radiogram.ts
index 4cba34d0c..68f27dee5 100644
--- a/shared/src/store/action-reducers/radiogram.ts
+++ b/shared/src/store/action-reducers/radiogram.ts
@@ -4,7 +4,7 @@ import {
acceptRadiogram,
markRadiogramDone,
} from '../../models/radiogram/radiogram-helpers-mutable';
-import { VehicleResource } from '../../models/utils/vehicle-resource';
+import { VehicleResource } from '../../models/utils/rescue-resource';
import { VehiclesSentEvent } from '../../simulation';
import { sendSimulationEvent } from '../../simulation/events/utils';
import { nextUUID } from '../../simulation/utils/randomness';
diff --git a/shared/src/store/action-reducers/simulation.ts b/shared/src/store/action-reducers/simulation.ts
index 5ca9c67de..54749b36c 100644
--- a/shared/src/store/action-reducers/simulation.ts
+++ b/shared/src/store/action-reducers/simulation.ts
@@ -22,7 +22,12 @@ import { StartCollectingInformationEvent } from '../../simulation/events/start-c
import { sendSimulationEvent } from '../../simulation/events/utils';
import { nextUUID } from '../../simulation/utils/randomness';
import type { Mutable } from '../../utils';
-import { UUID, uuidValidationOptions, cloneDeepMutable } from '../../utils';
+import {
+ UUID,
+ uuidValidationOptions,
+ cloneDeepMutable,
+ uuidArrayValidationOptions,
+} from '../../utils';
import { IsLiteralUnion, IsValue } from '../../utils/validators';
import type { Action, ActionReducer } from '../action-reducer';
import { ExpectedReducerError, ReducerError } from '../reducer-error';
@@ -69,6 +74,23 @@ export class UpdateTreatPatientsIntervalsAction implements Action {
public readonly countingTimePerPatient?: number;
}
+export class ProvidePersonnelBehaviorUpdateVehiclePrioritiesAction
+ implements Action
+{
+ @IsValue('[ProvidePersonnelBehavior] Update VehiclePriorities' as const)
+ public readonly type =
+ '[ProvidePersonnelBehavior] Update VehiclePriorities';
+
+ @IsUUID(4, uuidValidationOptions)
+ public readonly simulatedRegionId!: UUID;
+
+ @IsUUID(4, uuidValidationOptions)
+ public readonly behaviorId!: UUID;
+
+ @IsUUID(4, uuidArrayValidationOptions)
+ public readonly priorities!: readonly UUID[];
+}
+
export class UnloadArrivingVehiclesBehaviorUpdateUnloadDelayAction
implements Action
{
@@ -484,4 +506,23 @@ export namespace SimulationActionReducers {
},
rights: 'trainer',
};
+
+ export const updateTreatmentVehiclePriorities: ActionReducer =
+ {
+ action: ProvidePersonnelBehaviorUpdateVehiclePrioritiesAction,
+ reducer(draftState, { simulatedRegionId, behaviorId, priorities }) {
+ const behaviorState = getBehaviorById(
+ draftState,
+ simulatedRegionId,
+ behaviorId,
+ 'providePersonnelBehavior'
+ );
+
+ behaviorState.vehicleTemplatePriorities =
+ cloneDeepMutable(priorities);
+
+ return draftState;
+ },
+ rights: 'trainer',
+ };
}
From a036978894c29c7a363b809015445e83a7e9bcfb Mon Sep 17 00:00:00 2001
From: Lukas Radermacher <49586507+lukasrad02@users.noreply.github.com>
Date: Mon, 17 Apr 2023 12:38:31 +0200
Subject: [PATCH 23/31] Add current commit hash to version of dev container
(#864)
* Add current commit hash to version of dev container
* Add changes to changelog
* Test new action
* Move duplicated version extraction to own action
* Remove test job
* Increase time for lint job
---
.../add-commit-hash-to-version/action.yml | 14 ++++++++++++++
.github/actions/extract-version/action.yml | 10 ++++++++++
.github/workflows/pipeline.yml | 16 ++++++----------
CHANGELOG.md | 1 +
4 files changed, 31 insertions(+), 10 deletions(-)
create mode 100644 .github/actions/add-commit-hash-to-version/action.yml
create mode 100644 .github/actions/extract-version/action.yml
diff --git a/.github/actions/add-commit-hash-to-version/action.yml b/.github/actions/add-commit-hash-to-version/action.yml
new file mode 100644
index 000000000..9ccc9d7d3
--- /dev/null
+++ b/.github/actions/add-commit-hash-to-version/action.yml
@@ -0,0 +1,14 @@
+name: Add commit hash to version
+description: Adds the current branch name and commit hash as suffix to the current version number
+runs:
+ using: composite
+ steps:
+ - uses: ./.github/actions/extract-version
+ - name: Get suffix
+ id: get-suffix
+ run: echo "SUFFIX=$(git branch --show-current).$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
+ shell: bash
+ - name: Add suffix to version number
+ uses: ./.github/actions/update-version
+ with:
+ version: '${{ env.VERSION_NAME }}+${{ steps.get-suffix.outputs.SUFFIX }}'
diff --git a/.github/actions/extract-version/action.yml b/.github/actions/extract-version/action.yml
new file mode 100644
index 000000000..db0bdfa83
--- /dev/null
+++ b/.github/actions/extract-version/action.yml
@@ -0,0 +1,10 @@
+name: Extract version
+description: Reads the current version number from the package.json and writes it to the VERSION_NAME GitHub environment variable
+runs:
+ using: composite
+ steps:
+ - name: Read version from package.json
+ run: |
+ versionName=`jq -jM .version package.json`
+ echo "VERSION_NAME=$versionName" >> "$GITHUB_ENV"
+ shell: bash
diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml
index 97be9f7f8..d7197a3db 100644
--- a/.github/workflows/pipeline.yml
+++ b/.github/workflows/pipeline.yml
@@ -30,7 +30,7 @@ jobs:
run: cd shared && npm run build && cd ..
lint:
- timeout-minutes: 3
+ timeout-minutes: 5
runs-on: ubuntu-latest
needs: build
@@ -277,6 +277,8 @@ jobs:
steps:
- uses: actions/checkout@v3
# Source: https://docs.docker.com/ci-cd/github-actions/
+ - name: Add commit hash to version
+ uses: ./.github/actions/add-commit-hash-to-version
- name: Login to docker
uses: docker/login-action@v2
with:
@@ -300,10 +302,7 @@ jobs:
steps:
- uses: actions/checkout@v3
- - name: Extract version
- run: |
- versionName=`jq -jM .version package.json`
- echo "version_name=$versionName" >> "$GITHUB_ENV"
+ - uses: ./.github/actions/extract-version
# Source: https://docs.docker.com/ci-cd/github-actions/
- name: Login to docker
uses: docker/login-action@v2
@@ -318,7 +317,7 @@ jobs:
push: true
tags: >
${{ secrets.DOCKER_HUB_USERNAME }}/dfm:latest ,
- ${{ secrets.DOCKER_HUB_USERNAME }}/dfm:${{ env.version_name }}
+ ${{ secrets.DOCKER_HUB_USERNAME }}/dfm:${{ env.VERSION_NAME }}
release-main:
timeout-minutes: 2
@@ -333,10 +332,7 @@ jobs:
run: |
git config user.name "GitHub Actions"
git config user.email noreply@github.com
- - name: Extract version
- run: |
- versionName=`jq -jM .version package.json`
- echo "VERSION_NAME=$versionName" >> "$GITHUB_ENV"
+ - uses: ./.github/actions/extract-version
- name: Extract release notes
id: extract_release_notes
uses: ffurrer2/extract-release-notes@v1
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9a3646010..0537aff5e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,7 @@ and this project does **not** adhere to [Semantic Versioning](https://semver.org
- When personnel is missing during patient treatment in a simulated region, the reassign treatment activity now sends an event to notify the region about the shortage.
- A new behavior has been added to respond to personnel shortages by instructing the region to request new vehicles.
- The priorities of vehicles to request can be configured in a new behavior tab.
+- Development builds (the docker container with the `dev` tag) now show the commit hash they have been built from in the version number.
### Changed
From a212b4dc140c9940789d33c1b74de83fd1311b67 Mon Sep 17 00:00:00 2001
From: Lukas Radermacher <49586507+lukasrad02@users.noreply.github.com>
Date: Mon, 17 Apr 2023 15:18:59 +0200
Subject: [PATCH 24/31] Show time until next treatment recalculation (#862)
* Show time until next treatment recalculation
* Mention changes in changelog
* Switch `null` and `0` for `timeUntilNextRecalculation` in some cases
Co-authored-by: Nils1729 <45318774+Nils1729@users.noreply.github.com>
* Add margin for new row
---------
Co-authored-by: Nils1729 <45318774+Nils1729@users.noreply.github.com>
---
CHANGELOG.md | 1 +
...iew-behavior-treat-patients.component.html | 20 ++++-
...rview-behavior-treat-patients.component.ts | 75 +++++++++++++++++--
...egion-overview-behavior-tab.component.html | 4 +-
4 files changed, 88 insertions(+), 12 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0537aff5e..93b850b85 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,6 +20,7 @@ and this project does **not** adhere to [Semantic Versioning](https://semver.org
- A new behavior has been added to respond to personnel shortages by instructing the region to request new vehicles.
- The priorities of vehicles to request can be configured in a new behavior tab.
- Development builds (the docker container with the `dev` tag) now show the commit hash they have been built from in the version number.
+- The time until the next treatment recalculation for the automatic patient treatment is shown.
### Changed
diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/treat-patients/simulated-region-overview-behavior-treat-patients.component.html b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/treat-patients/simulated-region-overview-behavior-treat-patients.component.html
index 553796e6a..1fb12ded2 100644
--- a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/treat-patients/simulated-region-overview-behavior-treat-patients.component.html
+++ b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/treat-patients/simulated-region-overview-behavior-treat-patients.component.html
@@ -1,4 +1,7 @@
-