From 1af81d0f0e3f7cd75d0feeccaae75c70128e70cf Mon Sep 17 00:00:00 2001 From: Lukas Radermacher <49586507+lukasrad02@users.noreply.github.com> Date: Tue, 28 Mar 2023 09:18:55 +0200 Subject: [PATCH 01/31] Update test scenarios submodule to include radiograms (#817) --- test-scenarios | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-scenarios b/test-scenarios index b35df82c5..dac9453dd 160000 --- a/test-scenarios +++ b/test-scenarios @@ -1 +1 @@ -Subproject commit b35df82c55cf6fd896653aad6d39ce8accea3531 +Subproject commit dac9453dd1ad1c25ad9e10e7406dc50bc867c269 From 27827658398f8e18fbf1d5bc5c2c2601c947df82 Mon Sep 17 00:00:00 2001 From: benn02 <82985280+benn02@users.noreply.github.com> Date: Tue, 28 Mar 2023 14:41:11 +0200 Subject: [PATCH 02/31] Add Template Count to frontend (#819) --- .../trainer-map-editor.component.html | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/frontend/src/app/pages/exercises/exercise/shared/trainer-map-editor/trainer-map-editor.component.html b/frontend/src/app/pages/exercises/exercise/shared/trainer-map-editor/trainer-map-editor.component.html index 094c73654..ddbc50759 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/trainer-map-editor/trainer-map-editor.component.html +++ b/frontend/src/app/pages/exercises/exercise/shared/trainer-map-editor/trainer-map-editor.component.html @@ -211,6 +211,26 @@
> +

+ Eine Variante +

+ +

+ {{ + patientCategory.patientTemplates.length + }} + Varianten +

From 680d63ed1d5bd87aa38e00ab4f4e112054583113 Mon Sep 17 00:00:00 2001 From: Lukas Radermacher <49586507+lukasrad02@users.noreply.github.com> Date: Tue, 28 Mar 2023 16:57:58 +0200 Subject: [PATCH 03/31] Change priority icon and display tooltips in patient status display (#820) * Change priority icon and display tooltips * Add changed to changelog --- CHANGELOG.md | 8 ++++ .../trainer-map-editor.component.html | 6 ++- .../trainer-map-editor.component.ts | 12 +++--- .../patient-status-data-field.component.html | 7 +++- .../patient-status-data-field.component.ts | 14 +------ .../patient-behavior-description.pipe.ts | 20 ++++++++++ .../pipes/patient-behavior-icon.pipe.ts | 20 ++++++++++ .../shared/pipes/patient-status-color.pipe.ts | 20 ++++++++++ frontend/src/app/shared/shared.module.ts | 14 ++++++- .../src/models/utils/patient-status-code.ts | 37 ++++++++++--------- 10 files changed, 117 insertions(+), 41 deletions(-) create mode 100644 frontend/src/app/shared/pipes/patient-behavior-description.pipe.ts create mode 100644 frontend/src/app/shared/pipes/patient-behavior-icon.pipe.ts create mode 100644 frontend/src/app/shared/pipes/patient-status-color.pipe.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a67104d81..3abc96ab7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project does **not** adhere to [Semantic Versioning](https://semver.org ## [Unreleased] +### Added + +- The patient status display that visualizes the progression of a patient explains its icons via a tooltip + +### Changed + +- The icon for `C` (transport priority) in a patient status code has been changed to a road sign to be distinguishable from the icon for `D` (complication) + ## [0.3.0] - 2023-03-27 ### Added diff --git a/frontend/src/app/pages/exercises/exercise/shared/trainer-map-editor/trainer-map-editor.component.html b/frontend/src/app/pages/exercises/exercise/shared/trainer-map-editor/trainer-map-editor.component.html index ddbc50759..4ad4bc892 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/trainer-map-editor/trainer-map-editor.component.html +++ b/frontend/src/app/pages/exercises/exercise/shared/trainer-map-editor/trainer-map-editor.component.html @@ -138,7 +138,9 @@
ngbDropdownToggle > @@ -154,7 +156,7 @@
class="btn-outline-secondary" > diff --git a/frontend/src/app/pages/exercises/exercise/shared/trainer-map-editor/trainer-map-editor.component.ts b/frontend/src/app/pages/exercises/exercise/shared/trainer-map-editor/trainer-map-editor.component.ts index 6a74a0a67..6e7d9a298 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/trainer-map-editor/trainer-map-editor.component.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/trainer-map-editor/trainer-map-editor.component.ts @@ -1,9 +1,12 @@ import { Component } from '@angular/core'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { Store } from '@ngrx/store'; -import type { UUID, VehicleTemplate } from 'digital-fuesim-manv-shared'; +import type { + ColorCode, + UUID, + VehicleTemplate, +} from 'digital-fuesim-manv-shared'; import { - colorCodeMap, TransferPoint, Viewport, SimulatedRegion, @@ -28,8 +31,7 @@ import { openEditImageTemplateModal } from '../editor-panel/edit-image-template- * A wrapper around the map that provides trainers with more options and tools. */ export class TrainerMapEditorComponent { - public currentCategory: keyof typeof colorCodeMap = 'X'; - public readonly colorCodeMap = colorCodeMap; + public currentCategory: ColorCode = 'X'; public readonly categories = ['X', 'Y', 'Z'] as const; public readonly vehicleTemplates$ = this.store.select( @@ -75,7 +77,7 @@ export class TrainerMapEditorComponent { openEditImageTemplateModal(this.ngbModalService, mapImageTemplateId); } - public setCurrentCategory(category: keyof typeof colorCodeMap) { + public setCurrentCategory(category: ColorCode) { this.currentCategory = category; } public async vehicleOnMouseDown( diff --git a/frontend/src/app/shared/components/patient-status-displayl/patient-status-data-field/patient-status-data-field.component.html b/frontend/src/app/shared/components/patient-status-displayl/patient-status-data-field/patient-status-data-field.component.html index 1b4f25619..20a753b38 100644 --- a/frontend/src/app/shared/components/patient-status-displayl/patient-status-data-field/patient-status-data-field.component.html +++ b/frontend/src/app/shared/components/patient-status-displayl/patient-status-data-field/patient-status-data-field.component.html @@ -1,10 +1,13 @@ diff --git a/frontend/src/app/shared/components/patient-status-displayl/patient-status-data-field/patient-status-data-field.component.ts b/frontend/src/app/shared/components/patient-status-displayl/patient-status-data-field/patient-status-data-field.component.ts index 9c7218f90..125eebfc8 100644 --- a/frontend/src/app/shared/components/patient-status-displayl/patient-status-data-field/patient-status-data-field.component.ts +++ b/frontend/src/app/shared/components/patient-status-displayl/patient-status-data-field/patient-status-data-field.component.ts @@ -1,9 +1,5 @@ import { Component, Input } from '@angular/core'; -import { - PatientStatusDataField, - colorCodeMap, - behaviourCodeMap, -} from 'digital-fuesim-manv-shared'; +import { PatientStatusDataField } from 'digital-fuesim-manv-shared'; import { rgbColorPalette } from 'src/app/shared/functions/colors'; @Component({ @@ -14,14 +10,6 @@ import { rgbColorPalette } from 'src/app/shared/functions/colors'; export class PatientStatusDataFieldComponent { @Input() patientStatusDataField!: PatientStatusDataField; - public get colorCodeMap() { - return colorCodeMap; - } - - public get behaviourCodeMap() { - return behaviourCodeMap; - } - public get rgbColorPalette() { return rgbColorPalette; } diff --git a/frontend/src/app/shared/pipes/patient-behavior-description.pipe.ts b/frontend/src/app/shared/pipes/patient-behavior-description.pipe.ts new file mode 100644 index 000000000..1a5eca429 --- /dev/null +++ b/frontend/src/app/shared/pipes/patient-behavior-description.pipe.ts @@ -0,0 +1,20 @@ +import type { PipeTransform } from '@angular/core'; +import { Pipe } from '@angular/core'; +import type { BehaviourCode } from 'digital-fuesim-manv-shared'; + +const behaviorDescriptionMap: { [Key in BehaviourCode]: string } = { + A: 'Stabil', + B: 'Lebensrettende Maßnahme erforderlich', + C: 'Transportpriorität', + D: 'Komplikation', + E: 'Verstorben', +}; + +@Pipe({ + name: 'patientBehaviorDescription', +}) +export class PatientBehaviorDescriptionPipe implements PipeTransform { + transform(value: BehaviourCode): string { + return behaviorDescriptionMap[value]; + } +} diff --git a/frontend/src/app/shared/pipes/patient-behavior-icon.pipe.ts b/frontend/src/app/shared/pipes/patient-behavior-icon.pipe.ts new file mode 100644 index 000000000..481879d90 --- /dev/null +++ b/frontend/src/app/shared/pipes/patient-behavior-icon.pipe.ts @@ -0,0 +1,20 @@ +import type { PipeTransform } from '@angular/core'; +import { Pipe } from '@angular/core'; +import type { BehaviourCode } from 'digital-fuesim-manv-shared'; + +const behaviorIconMap: { [Key in BehaviourCode]: string } = { + A: 'bi-arrow-right-square-fill', + B: 'bi-heartbreak-fill', + C: 'bi-signpost-fill', + D: 'bi-exclamation-triangle-fill', + E: 'bi-x-circle-fill', +}; + +@Pipe({ + name: 'patientBehaviorIcon', +}) +export class PatientBehaviorIconPipe implements PipeTransform { + transform(value: BehaviourCode): string { + return behaviorIconMap[value]; + } +} diff --git a/frontend/src/app/shared/pipes/patient-status-color.pipe.ts b/frontend/src/app/shared/pipes/patient-status-color.pipe.ts new file mode 100644 index 000000000..a9fb5863a --- /dev/null +++ b/frontend/src/app/shared/pipes/patient-status-color.pipe.ts @@ -0,0 +1,20 @@ +import type { PipeTransform } from '@angular/core'; +import { Pipe } from '@angular/core'; +import type { ColorCode } from 'digital-fuesim-manv-shared'; + +const colorCodeMap = { + V: 'black', + W: 'blue', + X: 'green', + Y: 'yellow', + Z: 'red', +} as const satisfies { readonly [Key in ColorCode]: string }; + +@Pipe({ + name: 'patientStatusColor', +}) +export class PatientStatusColorPipe implements PipeTransform { + transform(value: ColorCode): (typeof colorCodeMap)[ColorCode] { + return colorCodeMap[value]; + } +} diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index e76ec0e2d..b67b8c6f8 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -1,7 +1,11 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; -import { NgbDropdownModule, NgbNavModule } from '@ng-bootstrap/ng-bootstrap'; +import { + NgbDropdownModule, + NgbNavModule, + NgbTooltip, +} from '@ng-bootstrap/ng-bootstrap'; import { FormsModule } from '@angular/forms'; import { HospitalNameComponent } from './components/hospital-name/hospital-name.component'; import { PatientStatusDataFieldComponent } from './components/patient-status-displayl/patient-status-data-field/patient-status-data-field.component'; @@ -30,6 +34,9 @@ import { CaterCapacityCountPipe } from './pipes/cater-capacity-count.pipe'; 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 { PatientBehaviorIconPipe } from './pipes/patient-behavior-icon.pipe'; +import { PatientBehaviorDescriptionPipe } from './pipes/patient-behavior-description.pipe'; @NgModule({ declarations: [ @@ -60,6 +67,9 @@ import { PatientsDetailsComponent } from './components/patients-details/patients FooterComponent, PatientHealthPointDisplayComponent, PatientsDetailsComponent, + PatientStatusColorPipe, + PatientBehaviorIconPipe, + PatientBehaviorDescriptionPipe, ], imports: [ CommonModule, @@ -67,6 +77,7 @@ import { PatientsDetailsComponent } from './components/patients-details/patients RouterModule, NgbDropdownModule, NgbNavModule, + NgbTooltip, ], exports: [ AutofocusDirective, @@ -95,6 +106,7 @@ import { PatientsDetailsComponent } from './components/patients-details/patients FooterComponent, PatientHealthPointDisplayComponent, PatientsDetailsComponent, + PatientStatusColorPipe, ], }) export class SharedModule {} diff --git a/shared/src/models/utils/patient-status-code.ts b/shared/src/models/utils/patient-status-code.ts index 618d609e0..545102abc 100644 --- a/shared/src/models/utils/patient-status-code.ts +++ b/shared/src/models/utils/patient-status-code.ts @@ -4,7 +4,15 @@ import type { AllowedValues } from '../../utils/validators'; import { IsLiteralUnion } from '../../utils/validators'; import { getCreate } from './get-create'; -type ColorCode = 'V' | 'W' | 'X' | 'Y' | 'Z'; +/** + * A letter that defines the color of a patient in a patient status. + * * `V`: ex (black) + * * `W`: SK IV (blue) + * * `X`: SK III (green) + * * `Y`: SK II (yellow) + * * `Z`: SK I (red) + */ +export type ColorCode = 'V' | 'W' | 'X' | 'Y' | 'Z'; const colorCodeAllowedValues: AllowedValues = { V: true, W: true, @@ -12,7 +20,16 @@ const colorCodeAllowedValues: AllowedValues = { Y: true, Z: true, }; -type BehaviourCode = 'A' | 'B' | 'C' | 'D' | 'E'; + +/** + * A letter that defines how a patients changes + * * `A`: stable + * * `B`: treatment required + * * `C`: transport priority + * * `D`: complication + * * `E`: dead + */ +export type BehaviourCode = 'A' | 'B' | 'C' | 'D' | 'E'; const behaviourCodeAllowedValues: AllowedValues = { A: true, B: true, @@ -21,22 +38,6 @@ const behaviourCodeAllowedValues: AllowedValues = { E: true, }; -export const colorCodeMap = { - V: 'black', - W: 'blue', - X: 'green', - Y: 'yellow', - Z: 'red', -} as const satisfies { readonly [Key in ColorCode]: string }; - -export const behaviourCodeMap: { [Key in BehaviourCode]: string } = { - A: 'bi-arrow-right-square-fill', - B: 'bi-heartbreak-fill', - C: 'bi-exclamation-circle-fill', - D: 'bi-exclamation-triangle-fill', - E: 'bi-x-circle-fill', -}; - export class PatientStatusDataField { @IsLiteralUnion(colorCodeAllowedValues) public readonly colorCode: ColorCode; From e04c156c5d811c8a01c3bf94550c3ec6a28e2d88 Mon Sep 17 00:00:00 2001 From: Lukas Radermacher <49586507+lukasrad02@users.noreply.github.com> Date: Tue, 28 Mar 2023 17:04:13 +0200 Subject: [PATCH 04/31] Add PR template (#821) --- .github/pull_request_template.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..b1e24b07f --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,6 @@ +### PR Checklist + +Please make sure to fulfil the following conditions before marking this PR ready for review: + +- [ ] If this PR adds or changes features or fixes bugs, this has been added to the changelog +- [ ] If this PR adds new actions or other ways to alter the state, [test scenarios](https://github.com/hpi-sam/digital-fuesim-manv-public-test-scenarios) have been added From ea0d22573458cb720ed5088042dfdd0e777dbb94 Mon Sep 17 00:00:00 2001 From: benn02 <82985280+benn02@users.noreply.github.com> Date: Wed, 29 Mar 2023 10:52:12 +0200 Subject: [PATCH 05/31] Tag pregnant patients (#834) --- CHANGELOG.md | 2 ++ .../trainer-map-editor.component.html | 4 +-- .../patient-status-display.component.html | 32 ++++++++++------- .../patient-status-tags-field.component.html | 3 ++ .../patient-status-tags-field.component.scss | 0 .../patient-status-tags-field.component.ts | 16 +++++++++ .../patients-details.component.html | 4 ++- frontend/src/app/shared/shared.module.ts | 2 ++ .../data/default-state/patient-templates.ts | 10 ++++-- .../src/models/utils/patient-status-code.ts | 18 +++++++++- .../25-add-patient-status-tags.ts | 36 +++++++++++++++++++ .../state-migrations/migration-functions.ts | 2 ++ shared/src/state.ts | 2 +- test-scenarios | 2 +- 14 files changed, 112 insertions(+), 21 deletions(-) create mode 100644 frontend/src/app/shared/components/patient-status-displayl/patient-status-tags-field/patient-status-tags-field.component.html create mode 100644 frontend/src/app/shared/components/patient-status-displayl/patient-status-tags-field/patient-status-tags-field.component.scss create mode 100644 frontend/src/app/shared/components/patient-status-displayl/patient-status-tags-field/patient-status-tags-field.component.ts create mode 100644 shared/src/state-migrations/25-add-patient-status-tags.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 3abc96ab7..e31d0e39e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project does **not** adhere to [Semantic Versioning](https://semver.org ### Added +- 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 ### Changed diff --git a/frontend/src/app/pages/exercises/exercise/shared/trainer-map-editor/trainer-map-editor.component.html b/frontend/src/app/pages/exercises/exercise/shared/trainer-map-editor/trainer-map-editor.component.html index 4ad4bc892..41a5c3d9c 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/trainer-map-editor/trainer-map-editor.component.html +++ b/frontend/src/app/pages/exercises/exercise/shared/trainer-map-editor/trainer-map-editor.component.html @@ -171,7 +171,7 @@
white-space: nowrap; flex-direction: column; align-content: flex-start; - height: 240px; + height: 280px; " >
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 @@ +
+ Schwanger +
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
+ + + + + + + + + + + + + + + +
KategorieAnzahl
{{ vehicleType }}{{ requiredResource.vehicleCounts[vehicleType] }}
+
+ + +
+
+ + +

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 + + + + + + + + +
+
Zeitintervalle
+
+
+ Minimaler Zeitabstand zwischen mehreren Anfragen +
+
+
+ + Min +
+
+
+
+
+ Dauer, nachdem eine nicht eingelöste Zusage von Fahrzeugen + invalidiert werden soll +
+
+
+ + Min +
+
+
+
+
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.

+
+ +
+ +
+
+ +
+
+
+
+
+
+ +
+
+
+

{{ template.vehicleType }}

+ +
+
+
+
+
+ +

+ 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
Derzeitige Zuordnung
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 -
+
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 @@ -
+
Allgemein >
+
+
Nächste Zuteilungs-Berechnung in
+
+ {{ timeUntilNextRecalculation | formatDuration }} +
+
[(ngbCollapse)]="settingsCollapsed" >
- Häufigkeit der Zuteilungsberechnung durch die Simulation in den + Häufigkeit der Zuteilungs-Berechnung durch die Simulation in den jeweiligen Zuständen
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.ts b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/treat-patients/simulated-region-overview-behavior-treat-patients.component.ts index f0684b876..d045b1003 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.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/treat-patients/simulated-region-overview-behavior-treat-patients.component.ts @@ -1,17 +1,22 @@ import type { OnInit } from '@angular/core'; import { Component, Input } from '@angular/core'; import { createSelector, Store } from '@ngrx/store'; -import type { UUID } from 'digital-fuesim-manv-shared'; -import { +import { UUID } from 'digital-fuesim-manv-shared'; +import type { TreatPatientsBehaviorState, - SimulatedRegion, + DelayEventActivityState, + ReassignTreatmentsActivityState, } from 'digital-fuesim-manv-shared'; import type { Observable } from 'rxjs'; +import { combineLatest, map } from 'rxjs'; import { ExerciseService } from 'src/app/core/exercise.service'; import type { AppState } from 'src/app/state/app.state'; import { + createSelectBehaviorState, createSelectElementsInSimulatedRegion, + createSelectSimulatedRegion, selectConfiguration, + selectCurrentTime, selectPatients, } from 'src/app/state/application/selectors/exercise.selectors'; import { SelectPatientService } from '../../../../select-patient.service'; @@ -30,9 +35,12 @@ let globalLastInformationCollapsed = true; export class SimulatedRegionOverviewBehaviorTreatPatientsComponent implements OnInit { - @Input() simulatedRegion!: SimulatedRegion; - @Input() treatPatientsBehaviorState!: TreatPatientsBehaviorState; + @Input() simulatedRegionId!: UUID; + @Input() treatPatientsBehaviorId!: UUID; + + public treatPatientsBehaviorState$!: Observable; public patientIds$!: Observable; + public timeUntilNextRecalculation$!: Observable; private _settingsCollapsed!: boolean; private _informationCollapsed!: boolean; @@ -60,11 +68,19 @@ export class SimulatedRegionOverviewBehaviorTreatPatientsComponent ngOnInit(): void { this.settingsCollapsed = globalLastSettingsCollapsed; this.informationCollapsed = globalLastInformationCollapsed; + + this.treatPatientsBehaviorState$ = this.store.select( + createSelectBehaviorState( + this.simulatedRegionId, + this.treatPatientsBehaviorId + ) + ); + this.patientIds$ = this.store.select( createSelector( createSelectElementsInSimulatedRegion( selectPatients, - this.simulatedRegion.id + this.simulatedRegionId ), selectConfiguration, (patients, configuration) => @@ -79,6 +95,49 @@ export class SimulatedRegionOverviewBehaviorTreatPatientsComponent .map((patient) => patient.id) ) ); + + const simulatedRegion$ = this.store.select( + createSelectSimulatedRegion(this.simulatedRegionId) + ); + + const currentTime$ = this.store.select(selectCurrentTime); + + this.timeUntilNextRecalculation$ = combineLatest([ + this.treatPatientsBehaviorState$, + simulatedRegion$, + currentTime$, + this.patientIds$, + ]).pipe( + map(([behaviorState, simulatedRegion, currentTime, patientIds]) => { + if (behaviorState.treatmentProgress === 'noTreatment') + return null; + + if (behaviorState.treatmentProgress === 'unknown') { + if (!behaviorState.treatmentActivityId) return null; + + const reassignActivity = simulatedRegion.activities[ + behaviorState.treatmentActivityId + ] as ReassignTreatmentsActivityState | undefined; + if (!reassignActivity) return null; + + return ( + (reassignActivity.countingStartedAt ?? currentTime) + + reassignActivity.countingTimePerPatient * + patientIds.length - + currentTime + ); + } + + if (behaviorState.delayActivityId === null) return null; + + const delayActivity = simulatedRegion.activities[ + behaviorState.delayActivityId + ] as DelayEventActivityState | undefined; + if (!delayActivity) return 0; + + return delayActivity.endTime - currentTime; + }) + ); } public updateTreatPatientsBehaviorState( @@ -90,8 +149,8 @@ export class SimulatedRegionOverviewBehaviorTreatPatientsComponent ) { this.exerciseService.proposeAction({ type: '[TreatPatientsBehavior] Update TreatPatientsIntervals', - simulatedRegionId: this.simulatedRegion.id, - behaviorStateId: this.treatPatientsBehaviorState.id, + simulatedRegionId: this.simulatedRegionId, + behaviorStateId: this.treatPatientsBehaviorId, unknown, counted, triaged, 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 87fea3223..0c807b57f 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 @@ -31,8 +31,8 @@ > Date: Mon, 17 Apr 2023 16:07:17 +0200 Subject: [PATCH 25/31] Allow triage of black patients (#866) * Allow triage of black patients * Ensure black patients are not treated after triage * Add changes to changelog * Document new semantics of ConditionParameters --- CHANGELOG.md | 7 ++- backend/src/exercise/patient-ticking.ts | 59 ++++++++++---------- shared/src/models/patient.ts | 6 +- shared/src/store/action-reducers/exercise.ts | 6 +- 4 files changed, 36 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93b850b85..88160e25a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,12 @@ and this project does **not** adhere to [Semantic Versioning](https://semver.org ### Changed -- The icon for `C` (transport priority) in a patient status code has been changed to a road sign to be distinguishable from the icon for `D` (complication) +- The icon for `C` (transport priority) in a patient status code has been changed to a road sign to be distinguishable from the icon for `D` (complication). +- `ConditionParameters.minimumHealth` and `ConditionParameters.maximumHealth` are now inclusive. + +### Fixed + +- Dead/Black patients can now be treated (for the automatic triage to work), but they won't be treated after triage. ## [0.3.0] - 2023-03-27 diff --git a/backend/src/exercise/patient-ticking.ts b/backend/src/exercise/patient-ticking.ts index de3f323ab..d8d423569 100644 --- a/backend/src/exercise/patient-ticking.ts +++ b/backend/src/exercise/patient-ticking.ts @@ -25,35 +25,32 @@ export function patientTick( state: ExerciseState, patientTickInterval: number ): PatientUpdate[] { - return ( - Object.values(state.patients) - // Only look at patients that are alive and have a position, i.e. are not in a vehicle - .filter((patient) => Patient.canBeTreated(patient)) - .map((patient) => { - // update the time a patient is being treated, to check for pretriage later - const treatmentTime = Patient.isTreatedByPersonnel(patient) - ? patient.treatmentTime + patientTickInterval - : patient.treatmentTime; - const nextHealthPoints = getNextPatientHealthPoints( - patient, - getDedicatedResources(state, patient), - patientTickInterval - ); - const nextStateId = getNextStateId(patient); - const nextStateTime = - nextStateId === patient.currentHealthStateId - ? patient.stateTime + - patientTickInterval * patient.timeSpeed - : 0; - return { - id: patient.id, - nextHealthPoints, - nextStateId, - nextStateTime, - treatmentTime, - }; - }) - ); + return Object.values(state.patients) + .filter((patient) => Patient.canBeTreated(patient)) + .map((patient) => { + // update the time a patient is being treated, to check for pretriage later + const treatmentTime = Patient.isTreatedByPersonnel(patient) + ? patient.treatmentTime + patientTickInterval + : patient.treatmentTime; + const nextHealthPoints = getNextPatientHealthPoints( + patient, + getDedicatedResources(state, patient), + patientTickInterval + ); + const nextStateId = getNextStateId(patient); + const nextStateTime = + nextStateId === patient.currentHealthStateId + ? patient.stateTime + + patientTickInterval * patient.timeSpeed + : 0; + return { + id: patient.id, + nextHealthPoints, + nextStateId, + nextStateTime, + treatmentTime, + }; + }); } /** @@ -155,9 +152,9 @@ function getNextStateId(patient: Patient) { (nextConditions.latestTime === undefined || patient.stateTime < nextConditions.latestTime) && (nextConditions.minimumHealth === undefined || - patient.health > nextConditions.minimumHealth) && + patient.health >= nextConditions.minimumHealth) && (nextConditions.maximumHealth === undefined || - patient.health < nextConditions.maximumHealth) && + patient.health <= nextConditions.maximumHealth) && (nextConditions.isBeingTreated === undefined || Patient.isTreatedByPersonnel(patient) === nextConditions.isBeingTreated) diff --git a/shared/src/models/patient.ts b/shared/src/models/patient.ts index 1719f296a..d1b3e04e3 100644 --- a/shared/src/models/patient.ts +++ b/shared/src/models/patient.ts @@ -28,7 +28,6 @@ import { healthPointsDefaults, HealthPoints, getCreate, - isAlive, isOnMap, isInSimulatedRegion, } from './utils'; @@ -199,9 +198,6 @@ export class Patient { } static canBeTreated(patient: Patient) { - return ( - isAlive(patient.health) && - (isOnMap(patient) || isInSimulatedRegion(patient)) - ); + return isOnMap(patient) || isInSimulatedRegion(patient); } } diff --git a/shared/src/store/action-reducers/exercise.ts b/shared/src/store/action-reducers/exercise.ts index 1e0a0842f..5fdc0750f 100644 --- a/shared/src/store/action-reducers/exercise.ts +++ b/shared/src/store/action-reducers/exercise.ts @@ -92,10 +92,7 @@ export namespace ExerciseActionReducers { export const exerciseTick: ActionReducer = { action: ExerciseTickAction, - reducer: ( - draftState, - { patientUpdates, refreshTreatments, tickInterval } - ) => { + reducer: (draftState, { patientUpdates, tickInterval }) => { // Refresh the current time draftState.currentTime += tickInterval; @@ -124,7 +121,6 @@ export namespace ExerciseActionReducers { currentPatient.visibleStatusChanged = visibleStatusBefore !== visibleStatusAfter; if ( - refreshTreatments && // We only want to do this expensive calculation, when it is really necessary currentPatient.visibleStatusChanged ) { From 9e44a437a8a91b585c9cef4801f63ca329d973c4 Mon Sep 17 00:00:00 2001 From: Nils1729 <45318774+Nils1729@users.noreply.github.com> Date: Mon, 17 Apr 2023 16:53:45 +0200 Subject: [PATCH 26/31] Fix/revert treatment phases (#863) * Fix update bug in UI * Allow backwards treatment progress transitions * Reassign personnel after progress change * Patch personnel estimation logic again for notarzt * Update changelog and triage calculations * Update changelog again --- CHANGELOG.md | 6 + ...reat-patients-patient-details.component.ts | 2 +- shared/src/models/patient.ts | 3 +- .../activities/reassign-treatments.ts | 201 ++++++++++++------ .../simulation/behaviors/treat-patients.ts | 7 +- 5 files changed, 144 insertions(+), 75 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88160e25a..777aea988 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,12 @@ and this project does **not** adhere to [Semantic Versioning](https://semver.org - 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. +### Fixed + +- New patients added to simulated regions during treatment are now also triaged and treated. +- When treatment is no longer secured, the displayed status is reverted back to lack of personnel. +- When the treatment status changes, personnel is reassigned immediately instead of after the next interval. + ### Changed - The icon for `C` (transport priority) in a patient status code has been changed to a road sign to be distinguishable from the icon for `D` (complication). diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/treat-patients/patient-details/simulated-region-overview-behavior-treat-patients-patient-details.component.ts b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/treat-patients/patient-details/simulated-region-overview-behavior-treat-patients-patient-details.component.ts index 92894b911..ffd3e92eb 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/treat-patients/patient-details/simulated-region-overview-behavior-treat-patients-patient-details.component.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/treat-patients/patient-details/simulated-region-overview-behavior-treat-patients-patient-details.component.ts @@ -49,7 +49,7 @@ export class SimulatedRegionOverviewBehaviorTreatPatientsPatientDetailsComponent selectPersonnel, patientSelector, (personnel, patient) => - Object.keys(patient.assignedPersonnelIds) + Object.keys(patient?.assignedPersonnelIds ?? {}) .map((personnelId) => personnel[personnelId]) .filter((person) => person !== undefined) .map((person) => ({ diff --git a/shared/src/models/patient.ts b/shared/src/models/patient.ts index d1b3e04e3..e6e1b8f70 100644 --- a/shared/src/models/patient.ts +++ b/shared/src/models/patient.ts @@ -182,8 +182,7 @@ export class Patient { bluePatientsEnabled: boolean ) { const status = - !pretriageEnabled || - patient.treatmentTime >= this.pretriageTimeThreshold + !pretriageEnabled || Patient.pretriageStatusIsLocked(patient) ? patient.realStatus : patient.pretriageStatus; return status === 'blue' && !bluePatientsEnabled ? 'red' : status; diff --git a/shared/src/simulation/activities/reassign-treatments.ts b/shared/src/simulation/activities/reassign-treatments.ts index da467327d..29841e7f2 100644 --- a/shared/src/simulation/activities/reassign-treatments.ts +++ b/shared/src/simulation/activities/reassign-treatments.ts @@ -16,7 +16,6 @@ import { 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, @@ -131,10 +130,12 @@ export const reassignTreatmentsActivity: SimulationActivity + | undefined; - let missingPersonnel: PersonnelResource | null = null; - - switch (activityState.treatmentProgress) { + const progress = activityState.treatmentProgress; + switch (progress) { case 'noTreatment': { // Since we've reached this line, there is a leader and other personnel so treatment can start sendSimulationEvent( @@ -154,54 +155,68 @@ export const reassignTreatmentsActivity: SimulationActivity; + to: Mutable; +} + function createCateringMaterials( materials: Mutable[] ): CateringMaterial[] { @@ -373,13 +393,13 @@ function triage( patients: Mutable[], personnel: Mutable[], materials: Mutable[] -): [boolean, PersonnelResource] { - const missingPersonnel = estimateRequiredPersonnel(patients, personnel); +): [boolean, ResourceDescription] { + const patientsToTreat: Mutable[] = []; const cateringPersonnel = createCateringPersonnel(personnel).sort( (a, b) => a.priority - b.priority ); - - const patientsToTreat: Mutable[] = []; + const emptyPersonnelCount = PersonnelResource.create().personnelCounts; + const triagePersonnelSubstitution: PersonnelSubstitution[] = []; patients.forEach((patient) => { if (Patient.pretriageStatusIsLocked(patient)) { @@ -401,13 +421,39 @@ function triage( if (triagePersonnelIndex !== -1) { // Personnel that is used for triage shall not be used for treatments - cateringPersonnel.splice(triagePersonnelIndex, 1); + const triagePersonnel = cateringPersonnel.splice( + triagePersonnelIndex, + 1 + ); + // But we can get it back + triagePersonnelSubstitution.push({ + to: triagePersonnel[0]!.personnel.personnelType, + from: emptyPersonnelCount, + }); } }); - assignTreatments(draftState, patientsToTreat, cateringPersonnel, materials); + const missingPersonnel = estimateRequiredPersonnel( + patients.length - patientsToTreat.length, + personnel + ); + let [, treatmentMissingPersonnel] = assignTreatments( + draftState, + patientsToTreat, + cateringPersonnel, + materials + ); + + treatmentMissingPersonnel = applySubstitutions( + treatmentMissingPersonnel, + triagePersonnelSubstitution + ); + const triageDone = patientsToTreat.length === patients.length; - return [triageDone, missingPersonnel]; + return [ + triageDone, + addResourceDescription(missingPersonnel, treatmentMissingPersonnel), + ]; } /** @@ -424,7 +470,7 @@ function treat( patients: Mutable[], personnel: Mutable[], materials: Mutable[] -): [boolean, PersonnelResource] { +): [boolean, ResourceDescription] { return assignTreatments( draftState, patients, @@ -500,6 +546,15 @@ function sumOfTreatments(cateringPersonnel: CateringPersonnel): number { ); } +/** + * Determines if {@link patients} contains no patient that is not triaged. + */ +function allPatientsTriaged(patients: Patient[]): boolean { + return patients.every((patient) => + Patient.pretriageStatusIsLocked(patient) + ); +} + /** * Selects the best personnel to treat a specific patient. * This function tries to find a personnel of the requested type and prefers personnel that is not treating any other patient. @@ -577,7 +632,7 @@ function assignTreatments( patients: Mutable[], cateringPersonnel: CateringPersonnel[], materials: Mutable[] -): [boolean, PersonnelResource] { +): [boolean, ResourceDescription] { const groupedPatients = groupBy(patients, (patient) => Patient.getVisibleStatus( patient, @@ -679,7 +734,7 @@ function assignTreatments( personnelTreatments, patientTreatments ); - return [secured, PersonnelResource.create(missingPersonnel)]; + return [secured, missingPersonnel]; } function tryAssignPersonnel( @@ -754,10 +809,9 @@ function recordPatientTreatedByPersonnel( * @returns the types and number of personnel estimated to missing to secure treatment */ function estimateRequiredPersonnel( - patients: Patient[], + patientCount: number, personnel: Personnel[] -): PersonnelResource { - const patientCount = patients.length; +): ResourceDescription { const groupedPersonnel = groupBy(personnel, (p) => p.personnelType); const havePersonnel = Object.fromEntries( StrictObject.keys(personnelPriorities).map((pt) => [ @@ -769,12 +823,10 @@ function estimateRequiredPersonnel( scaleResourceDescription(estimatedSkDistribution, patientCount) ); - // We want the missing personnel exactly, no substitution of personnel tiers takes place. - return PersonnelResource.create( - maxResourceDescription( - subtractResourceDescription(wantPersonnel, havePersonnel), - 0 - ) + // We want the missing personnel exactly, no substitution of personnel qualification takes place. + return maxResourceDescription( + subtractResourceDescription(wantPersonnel, havePersonnel), + 0 ); } @@ -792,16 +844,19 @@ function requiredPersonnelForPatients( ] ) ); - return addResourceDescription( - ceilResourceDescription( - addResourceDescription( - requiredByType['red']!, - requiredByType['yellow']! - ) + const result = addResourceDescription( + addResourceDescription( + requiredByType['red']!, + requiredByType['yellow']! ), // Green should not be mixed. Therefore we ceil them before merging. ceilResourceDescription(requiredByType['green']!) ); + result.notarzt = Math.max( + requiredByType['red']!.notarzt, + requiredByType['yellow']!.notarzt + ); + return result; } function calculateMissingPersonnel( @@ -855,11 +910,9 @@ function calculateMissingPersonnel( ) as { [K in TreatablePatientStatus]: ResourceDescription }; const totalMissingPersonnel = addResourceDescription( - ceilResourceDescription( - addResourceDescription( - patientStatusMissingPersonnel.red, - patientStatusMissingPersonnel.yellow - ) + addResourceDescription( + patientStatusMissingPersonnel.red, + patientStatusMissingPersonnel.yellow ), ceilResourceDescription(patientStatusMissingPersonnel.green) ); @@ -892,7 +945,6 @@ function calculateSubstitutedMissingPersonnel( cateringDict: { [k: string]: CateringPersonnel }, personnelTreatments: PersonnelToPatientCategoryDict ): ResourceDescription { - let missing = { ...missingPersonnel }; const substitutions = StrictObject.entries(personnelTreatments) .map( ([persId, roles]) => @@ -923,17 +975,28 @@ function calculateSubstitutedMissingPersonnel( return { from, to: realPersonnelType }; }); + return applySubstitutions(missingPersonnel, substitutions); +} + +function applySubstitutions( + missing: ResourceDescription, + substitutions: PersonnelSubstitution[] +) { + let stillMissing = { ...missing }; reversedPersonnelTypePriorityList.forEach((personnelType) => { - while (missing[personnelType] > 0) { + while (stillMissing[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; + stillMissing[substitution!.to] -= 1; - missing = addResourceDescription(missing, substitution.from); + stillMissing = addResourceDescription( + stillMissing, + substitution.from + ); } }); - return maxResourceDescription(ceilResourceDescription(missing), 0); + return maxResourceDescription(stillMissing, 0); } diff --git a/shared/src/simulation/behaviors/treat-patients.ts b/shared/src/simulation/behaviors/treat-patients.ts index bc5207ed2..cf0d71d4c 100644 --- a/shared/src/simulation/behaviors/treat-patients.ts +++ b/shared/src/simulation/behaviors/treat-patients.ts @@ -156,10 +156,14 @@ export const treatPatientsBehavior: SimulationBehavior Date: Tue, 18 Apr 2023 10:38:46 +0200 Subject: [PATCH 27/31] Sort connected transfer points and hospitals by name (#869) * Order connected transfer points by name * Order connected hospitals by name * Add changes to changelog --- CHANGELOG.md | 1 + .../other-transfer-point-tab.component.html | 29 +++------ .../other-transfer-point-tab.component.ts | 54 ++++++++++++----- .../transfer-hospitals-tab.component.html | 20 +++---- .../transfer-hospitals-tab.component.ts | 60 ++++++++++++------- 5 files changed, 96 insertions(+), 68 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 777aea988..f34cc88d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ and this project does **not** adhere to [Semantic Versioning](https://semver.org - The icon for `C` (transport priority) in a patient status code has been changed to a road sign to be distinguishable from the icon for `D` (complication). - `ConditionParameters.minimumHealth` and `ConditionParameters.maximumHealth` are now inclusive. +- Connected transfer points and hospitals are now listed in alphabetical order in the transfer popups. ### Fixed diff --git a/frontend/src/app/pages/exercises/exercise/shared/transfer-point-overview/other-transfer-point-tab/other-transfer-point-tab.component.html b/frontend/src/app/pages/exercises/exercise/shared/transfer-point-overview/other-transfer-point-tab/other-transfer-point-tab.component.html index ed454d65b..244303708 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/transfer-point-overview/other-transfer-point-tab/other-transfer-point-tab.component.html +++ b/frontend/src/app/pages/exercises/exercise/shared/transfer-point-overview/other-transfer-point-tab/other-transfer-point-tab.component.html @@ -9,17 +9,14 @@ Es sind noch keine Transferpunkte verbunden.

-
    +
    • @@ -28,21 +25,17 @@ style="max-width: 300px" >
diff --git a/frontend/src/app/pages/exercises/exercise/shared/transfer-point-overview/other-transfer-point-tab/other-transfer-point-tab.component.ts b/frontend/src/app/pages/exercises/exercise/shared/transfer-point-overview/other-transfer-point-tab/other-transfer-point-tab.component.ts index 248652158..219db809d 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/transfer-point-overview/other-transfer-point-tab/other-transfer-point-tab.component.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/transfer-point-overview/other-transfer-point-tab/other-transfer-point-tab.component.ts @@ -1,8 +1,9 @@ import type { OnInit } from '@angular/core'; import { Component, Input } from '@angular/core'; -import { createSelector, Store } from '@ngrx/store'; +import { Store } from '@ngrx/store'; import { UUID, TransferPoint } from 'digital-fuesim-manv-shared'; import type { Observable } from 'rxjs'; +import { combineLatest, map } from 'rxjs'; import { ExerciseService } from 'src/app/core/exercise.service'; import type { AppState } from 'src/app/state/app.state'; import { @@ -20,24 +21,16 @@ export class OtherTransferPointTabComponent implements OnInit { public transferPoint$!: Observable; - public transferPoints$: Observable<{ [key: UUID]: TransferPoint }> = - this.store.select(selectTransferPoints); + public reachableTransferPoints$!: Observable< + { id: UUID; name: string; duration: number }[] + >; /** * All transferPoints that are neither connected to this one nor this one itself */ - public readonly transferPointsToBeAdded$ = this.store.select( - createSelector(selectTransferPoints, (transferPoints) => { - const currentTransferPoint = transferPoints[this.transferPointId]!; - return Object.fromEntries( - Object.entries(transferPoints).filter( - ([key]) => - key !== this.transferPointId && - !currentTransferPoint.reachableTransferPoints[key] - ) - ); - }) - ); + public transferPointsToBeAdded$!: Observable<{ + [key: UUID]: TransferPoint; + }>; constructor( private readonly store: Store, @@ -48,6 +41,37 @@ export class OtherTransferPointTabComponent implements OnInit { this.transferPoint$ = this.store.select( createSelectTransferPoint(this.transferPointId) ); + + const transferPoints$ = this.store.select(selectTransferPoints); + + this.transferPointsToBeAdded$ = transferPoints$.pipe( + map((transferPoints) => { + const currentTransferPoint = + transferPoints[this.transferPointId]!; + return Object.fromEntries( + Object.entries(transferPoints).filter( + ([key]) => + key !== this.transferPointId && + !currentTransferPoint.reachableTransferPoints[key] + ) + ); + }) + ); + + this.reachableTransferPoints$ = combineLatest([ + this.transferPoint$, + transferPoints$, + ]).pipe( + map(([transferPoint, transferPoints]) => + Object.entries(transferPoint.reachableTransferPoints) + .map(([key, value]) => ({ + id: key, + name: TransferPoint.getFullName(transferPoints[key]!), + duration: value.duration, + })) + .sort((a, b) => a.name.localeCompare(b.name)) + ) + ); } public connectTransferPoint(transferPointId: UUID, duration?: number) { diff --git a/frontend/src/app/pages/exercises/exercise/shared/transfer-point-overview/transfer-hospitals-tab/transfer-hospitals-tab.component.html b/frontend/src/app/pages/exercises/exercise/shared/transfer-point-overview/transfer-hospitals-tab/transfer-hospitals-tab.component.html index 1c5b8c6c5..ff0f7ff97 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/transfer-point-overview/transfer-hospitals-tab/transfer-hospitals-tab.component.html +++ b/frontend/src/app/pages/exercises/exercise/shared/transfer-point-overview/transfer-hospitals-tab/transfer-hospitals-tab.component.html @@ -9,36 +9,32 @@ Es sind noch keine Krankenhäuser verbunden.

-
    +
    • {{ - hospitals[reachableHospitalId]!.transportDuration / - 1000 / - 60 + reachableHospital.transportDuration / 1000 / 60 }} min +
      + +
      +
+ + + + + + + + + + + + + + + + + + + + + +
FahrzeugLimitVerteilt
{{ vehicleLimit.vehicleType }} +
+ +
+
+
+ +
+
+ {{ + automaticallyDistributeVehiclesBehaviorState + .distributedRounds[vehicleLimit.vehicleType] ?? + 0 + }} + + +
+
+ +
Fahrzeuge Erhaltende Bereiche
+ +
+ +
+ +
+
+ + + + + + + + + + + + + + + +
Ziel
{{ destination.name }} + +
+
+ diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/automatically-distribute-vehicles/simulated-region-overview-behavior-automatically-distribute-vehicles.component.scss b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/automatically-distribute-vehicles/simulated-region-overview-behavior-automatically-distribute-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/automatically-distribute-vehicles/simulated-region-overview-behavior-automatically-distribute-vehicles.component.ts b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/automatically-distribute-vehicles/simulated-region-overview-behavior-automatically-distribute-vehicles.component.ts new file mode 100644 index 000000000..93a582076 --- /dev/null +++ b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/automatically-distribute-vehicles/simulated-region-overview-behavior-automatically-distribute-vehicles.component.ts @@ -0,0 +1,234 @@ +import type { OnInit } from '@angular/core'; +import { Component, Input } from '@angular/core'; +import { createSelector, Store } from '@ngrx/store'; +import { + isInSpecificSimulatedRegion, + TransferPoint, + UUID, +} from 'digital-fuesim-manv-shared'; +import type { AutomaticallyDistributeVehiclesBehaviorState } 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 { + createSelectBehaviorState, + selectTransferPoints, + selectVehicleTemplates, +} from 'src/app/state/application/selectors/exercise.selectors'; + +@Component({ + selector: + 'app-simulated-region-overview-behavior-automatically-distribute-vehicles', + templateUrl: + './simulated-region-overview-behavior-automatically-distribute-vehicles.component.html', + styleUrls: [ + './simulated-region-overview-behavior-automatically-distribute-vehicles.component.scss', + ], +}) +export class SimulatedRegionOverviewBehaviorAutomaticallyDistributeVehiclesComponent + implements OnInit +{ + @Input() simulatedRegionId!: UUID; + @Input() automaticallyDistributeVehiclesBehaviorId!: UUID; + + public automaticallyDistributeVehiclesBehaviorState$!: Observable; + public distributionLimits$!: Observable< + { vehicleType: string; vehicleAmount: number }[] + >; + public distributionDestinations$!: Observable< + { name: string; id: string }[] + >; + + public addableVehicleTypes$!: Observable; + public addableTransferPoints$!: Observable<{ + [k: string]: TransferPoint; + }>; + public getTransferPointOrderByValue = (transferPoint: TransferPoint) => + TransferPoint.getFullName(transferPoint); + + public readonly infinity = Number.MAX_VALUE; + + constructor( + private readonly exerciseService: ExerciseService, + private readonly store: Store + ) {} + + ngOnInit(): void { + const automaticallyDistributeVehiclesBehaviorStateSelector = + createSelectBehaviorState( + this.simulatedRegionId, + this.automaticallyDistributeVehiclesBehaviorId + ); + + const distributionLimitsSelector = createSelector( + automaticallyDistributeVehiclesBehaviorStateSelector, + (automaticallyDistributeVehiclesBehaviorState) => + Object.entries( + automaticallyDistributeVehiclesBehaviorState.distributionLimits + ) + .filter( + ([_vehicleType, vehicleAmount]) => vehicleAmount > 0 + ) + .map(([vehicleType, vehicleAmount]) => ({ + vehicleType, + vehicleAmount, + })) + ); + + const presentDistributionDestinationsSelector = createSelector( + automaticallyDistributeVehiclesBehaviorStateSelector, + (automaticallyDistributeVehiclesBehaviorState) => + automaticallyDistributeVehiclesBehaviorState.distributionDestinations + ); + + const presentVehicleTypesSelector = createSelector( + distributionLimitsSelector, + (distributionLimits) => + distributionLimits.map( + (distributionLimit) => distributionLimit.vehicleType + ) + ); + + const addableVehicleTypesSelector = createSelector( + selectVehicleTemplates, + presentVehicleTypesSelector, + (vehicleTemplates, presentVehicleTypes) => + vehicleTemplates + .map((vehicleTemplate) => vehicleTemplate.vehicleType) + .filter( + (vehicleType) => + !presentVehicleTypes.includes(vehicleType) + ) + ); + + const distributionDestinationsSelector = createSelector( + presentDistributionDestinationsSelector, + selectTransferPoints, + (presentDestinations, transferPoints) => + Object.keys(presentDestinations).map((destinationId) => ({ + name: transferPoints[destinationId]!.externalName, + id: destinationId, + })) + ); + + const ownTransferPointSelector = createSelector( + selectTransferPoints, + (transferPoints) => + Object.values(transferPoints).find((transferPoint) => + isInSpecificSimulatedRegion( + transferPoint, + this.simulatedRegionId + ) + )! + ); + + const addableTransferPointsSelector = createSelector( + selectTransferPoints, + ownTransferPointSelector, + presentDistributionDestinationsSelector, + ( + transferPoints, + ownTransferPoint, + presentDistributionDestinations + ) => + Object.fromEntries( + Object.entries(transferPoints).filter( + ([key]) => + key !== ownTransferPoint.id && + !presentDistributionDestinations[key] + ) + ) + ); + + this.addableVehicleTypes$ = this.store.select( + addableVehicleTypesSelector + ); + + this.automaticallyDistributeVehiclesBehaviorState$ = this.store.select( + automaticallyDistributeVehiclesBehaviorStateSelector + ); + + this.addableTransferPoints$ = this.store.select( + addableTransferPointsSelector + ); + + this.distributionLimits$ = this.store.select( + distributionLimitsSelector + ); + + this.distributionDestinations$ = this.store.select( + distributionDestinationsSelector + ); + } + + public addVehicle(vehicleType: string) { + this.exerciseService.proposeAction({ + type: '[AutomaticDistributionBehavior] Change Limit', + simulatedRegionId: this.simulatedRegionId, + behaviorId: this.automaticallyDistributeVehiclesBehaviorId, + vehicleType, + newLimit: 1, + }); + } + + public removeVehicle(vehicleType: string) { + this.exerciseService.proposeAction({ + type: '[AutomaticDistributionBehavior] Change Limit', + simulatedRegionId: this.simulatedRegionId, + behaviorId: this.automaticallyDistributeVehiclesBehaviorId, + vehicleType, + newLimit: 0, + }); + } + + public changeLimitOfVehicle(vehicleType: string, newLimit: number) { + this.exerciseService.proposeAction({ + type: '[AutomaticDistributionBehavior] Change Limit', + simulatedRegionId: this.simulatedRegionId, + behaviorId: this.automaticallyDistributeVehiclesBehaviorId, + vehicleType, + newLimit, + }); + } + + public unlimitedLimitOfVehicle(vehicleType: string) { + this.exerciseService.proposeAction({ + type: '[AutomaticDistributionBehavior] Change Limit', + simulatedRegionId: this.simulatedRegionId, + behaviorId: this.automaticallyDistributeVehiclesBehaviorId, + vehicleType, + newLimit: this.infinity, + }); + } + + public limitedLimitOfVehicle( + vehicleType: string, + currentlyDistributed: number + ) { + this.exerciseService.proposeAction({ + type: '[AutomaticDistributionBehavior] Change Limit', + simulatedRegionId: this.simulatedRegionId, + behaviorId: this.automaticallyDistributeVehiclesBehaviorId, + vehicleType, + newLimit: currentlyDistributed, + }); + } + + public addDistributionDestination(destinationId: UUID) { + this.exerciseService.proposeAction({ + type: '[AutomaticDistributionBehavior] Add Destination', + simulatedRegionId: this.simulatedRegionId, + behaviorId: this.automaticallyDistributeVehiclesBehaviorId, + destinationId, + }); + } + + public removeDistributionDestination(destinationId: UUID) { + this.exerciseService.proposeAction({ + type: '[AutomaticDistributionBehavior] Remove Destination', + simulatedRegionId: this.simulatedRegionId, + behaviorId: this.automaticallyDistributeVehiclesBehaviorId, + destinationId, + }); + } +} 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 0c807b57f..27e86e4ba 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 @@ -23,7 +23,10 @@
-
+
+ value !== 0) - ) { + if (Object.values(sentVehicles).some((value) => value !== 0)) { sendSimulationEvent( - getElement( - draftState, - 'simulatedRegion', - currentSimulatedRegionIdOf(targetTransferPoint) - ), - VehiclesSentEvent.create( + simulatedRegion, + VehicleTransferSuccessfulEvent.create( nextUUID(draftState), - VehicleResource.create(sentVehicles), - activityState.key + targetTransferPoint.id, + activityState.key, + VehicleResource.create(sentVehicles) ) ); + + if (isInSimulatedRegion(targetTransferPoint)) { + sendSimulationEvent( + getElement( + draftState, + 'simulatedRegion', + currentSimulatedRegionIdOf(targetTransferPoint) + ), + VehiclesSentEvent.create( + nextUUID(draftState), + VehicleResource.create(sentVehicles), + activityState.key + ) + ); + } } terminate(); diff --git a/shared/src/simulation/behaviors/automatically-distribute-vehicles.ts b/shared/src/simulation/behaviors/automatically-distribute-vehicles.ts new file mode 100644 index 000000000..f7f6311de --- /dev/null +++ b/shared/src/simulation/behaviors/automatically-distribute-vehicles.ts @@ -0,0 +1,275 @@ +import { IsInt, IsOptional, IsUUID, Min } from 'class-validator'; +import { getCreate, VehicleResource } from '../../models/utils'; +import type { Mutable } from '../../utils'; +import { cloneDeepMutable, UUID, uuid, UUIDSet } from '../../utils'; +import { IsUUIDSet, IsUUIDSetMap, IsValue } from '../../utils/validators'; +import { IsResourceDescription } from '../../utils/validators/is-resource-description'; +import { + RecurringEventActivityState, + TransferVehiclesActivityState, +} from '../activities'; +import { addActivity } from '../activities/utils'; +import { TryToDistributeEvent } from '../events/try-to-distribute'; +import { nextUUID } from '../utils/randomness'; +import type { + SimulationBehavior, + SimulationBehaviorState, +} from './simulation-behavior'; + +export class AutomaticallyDistributeVehiclesBehaviorState + implements SimulationBehaviorState +{ + @IsValue('automaticallyDistributeVehiclesBehavior') + readonly type = 'automaticallyDistributeVehiclesBehavior'; + + @IsUUID() + public readonly id: UUID = uuid(); + + @IsUUIDSet() + public readonly distributionDestinations: UUIDSet = {}; + + @IsResourceDescription() + public readonly distributionLimits: { [vehicleType: string]: number } = {}; + + @IsResourceDescription() + public readonly distributedRounds: { [vehicleType: string]: number } = {}; + + @IsResourceDescription() + public readonly distributedLastRound: { [vehicleType: string]: number } = + {}; + + @IsUUIDSetMap() + public readonly remainingInNeed: { [vehicleType: string]: UUIDSet } = {}; + + @IsInt() + @Min(1) + /* + * This *MUST* be greater that the tick Duration to Ensure that we can wait for a response in the next tick + */ + public readonly distributionDelay: number = 60_000; // 1 minute + + @IsUUID() + @IsOptional() + public readonly recurringActivityId!: UUID; + + static readonly create = getCreate(this); +} + +export const automaticallyDistributeVehiclesBehavior: SimulationBehavior = + { + behaviorState: AutomaticallyDistributeVehiclesBehaviorState, + handleEvent: (draftState, simulatedRegion, behaviorState, event) => { + switch (event.type) { + case 'tickEvent': { + if (!behaviorState.recurringActivityId) { + // initialize recurring activity + behaviorState.recurringActivityId = + nextUUID(draftState); + addActivity( + simulatedRegion, + RecurringEventActivityState.create( + behaviorState.recurringActivityId, + TryToDistributeEvent.create(behaviorState.id), + draftState.currentTime, + behaviorState.distributionDelay + ) + ); + } + break; + } + case 'tryToDistributeEvent': + { + // Ignore the event if it is not meant for this behavior + + if (event.behaviorId !== behaviorState.id) { + return; + } + + // Don't do anything until there is a region to distribute to + + if ( + Object.keys(behaviorState.distributionDestinations) + .length === 0 + ) { + return; + } + + // Check for completed rounds + + Object.entries(behaviorState.remainingInNeed).forEach( + ([vehicleType, regionsInNeed]) => { + if (Object.keys(regionsInNeed).length === 0) { + if ( + !behaviorState.distributedRounds[ + vehicleType + ] + ) { + behaviorState.distributedRounds[ + vehicleType + ] = 0; + } + + // Check if a vehicle was distributed during the last distribution try + // to not increase the distributed rounds if all transfer connections were missing + + if ( + (behaviorState.distributedLastRound[ + vehicleType + ] ?? 0) > 0 + ) { + behaviorState.distributedRounds[ + vehicleType + ]++; + } + + behaviorState.remainingInNeed[vehicleType] = + behaviorState.distributionDestinations; + } + behaviorState.distributedLastRound[ + vehicleType + ] = 0; + } + ); + + // distribute + + const regionsOrderedByNeed = Object.keys( + cloneDeepMutable( + behaviorState.distributionDestinations + ) + ); + regionsOrderedByNeed.sort( + (regionIdA, regionIdB) => + numberOfDifferentVehiclesNeeded( + behaviorState, + regionIdB + ) - + numberOfDifferentVehiclesNeeded( + behaviorState, + regionIdA + ) + ); + + const vehiclesToBeSent: { + [region: UUID]: { [vehicletype: string]: 1 }; + } = {}; + + Object.entries(behaviorState.remainingInNeed).forEach( + ([vehicleType, regionsInNeed]) => { + if ( + distributionLimitOfVehicleTypeReached( + behaviorState, + vehicleType + ) + ) { + return; + } + + Object.keys(regionsInNeed).forEach((region) => { + if (!vehiclesToBeSent[region]) { + vehiclesToBeSent[region] = {}; + } + vehiclesToBeSent[region]![vehicleType] = 1; + }); + } + ); + + regionsOrderedByNeed.forEach((region) => { + if (!vehiclesToBeSent[region]) { + return; + } + addActivity( + simulatedRegion, + TransferVehiclesActivityState.create( + nextUUID(draftState), + region, + 'automatic-distribution', + VehicleResource.create( + cloneDeepMutable( + vehiclesToBeSent[region]! + ) + ) + ) + ); + }); + } + break; + case 'transferConnectionMissingEvent': + { + // If a connection is missing the vehicle counts as sent + + Object.entries(behaviorState.remainingInNeed).forEach( + ([vehicleType, regionsInNeed]) => { + if ( + distributionLimitOfVehicleTypeReached( + behaviorState, + vehicleType + ) + ) { + return; + } + delete regionsInNeed[event.transferPointId]; + } + ); + } + break; + case 'vehicleTransferSuccessfulEvent': + { + if (event.key !== 'automatic-distribution') { + return; + } + + Object.entries( + event.vehiclesSent.vehicleCounts + ).forEach(([vehicleType, vehicleAmount]) => { + if (vehicleAmount === 0) { + return; + } + if ( + !behaviorState.distributedLastRound[vehicleType] + ) { + behaviorState.distributedLastRound[ + vehicleType + ] = 0; + } + behaviorState.distributedLastRound[vehicleType]++; + + if (behaviorState.remainingInNeed[vehicleType]) { + delete behaviorState.remainingInNeed[ + vehicleType + ]![event.targetId]; + } + }); + } + break; + + default: + // Ignore event + } + }, + }; + +function distributionLimitOfVehicleTypeReached( + behaviorState: Mutable, + vehicleType: string +) { + return ( + (behaviorState.distributedRounds[vehicleType] ?? 0) >= + (behaviorState.distributionLimits[vehicleType] ?? 0) + ); +} + +function numberOfDifferentVehiclesNeeded( + behaviorState: Mutable, + regionId: string +) { + return Object.values(behaviorState.remainingInNeed).reduce( + (numberOfVehiclesNeededByRegion, regionsInNeed) => { + if (regionsInNeed[regionId]) { + return numberOfVehiclesNeededByRegion + 1; + } + return numberOfVehiclesNeededByRegion; + }, + 0 + ); +} diff --git a/shared/src/simulation/behaviors/exercise-simulation-behavior.ts b/shared/src/simulation/behaviors/exercise-simulation-behavior.ts index 16c16c12c..3398a5996 100644 --- a/shared/src/simulation/behaviors/exercise-simulation-behavior.ts +++ b/shared/src/simulation/behaviors/exercise-simulation-behavior.ts @@ -4,11 +4,13 @@ import { assignLeaderBehavior } from './assign-leader'; import { treatPatientsBehavior } from './treat-patients'; import { unloadArrivingVehiclesBehavior } from './unload-arrived-vehicles'; import { reportBehavior } from './report'; +import { automaticallyDistributeVehiclesBehavior } from './automatically-distribute-vehicles'; import { providePersonnelBehavior } from './provide-personnel'; import { answerRequestsBehavior } from './answer-requests'; import { requestBehavior } from './request'; export const simulationBehaviors = { + automaticallyDistributeVehiclesBehavior, assignLeaderBehavior, treatPatientsBehavior, unloadArrivingVehiclesBehavior, diff --git a/shared/src/simulation/behaviors/index.ts b/shared/src/simulation/behaviors/index.ts index 5de87e447..b23d5b2d9 100644 --- a/shared/src/simulation/behaviors/index.ts +++ b/shared/src/simulation/behaviors/index.ts @@ -2,6 +2,8 @@ export * from './assign-leader'; export * from './exercise-simulation-behavior'; export * from './treat-patients'; export * from './unload-arrived-vehicles'; +export * from './answer-requests'; +export * from './automatically-distribute-vehicles'; export * from './report'; export * from './provide-personnel'; export * from './utils'; diff --git a/shared/src/simulation/events/exercise-simulation-event.ts b/shared/src/simulation/events/exercise-simulation-event.ts index e6b638cd3..d0f391c96 100644 --- a/shared/src/simulation/events/exercise-simulation-event.ts +++ b/shared/src/simulation/events/exercise-simulation-event.ts @@ -12,6 +12,9 @@ import { CollectInformationEvent } from './collect'; import { StartCollectingInformationEvent } from './start-collecting'; import { ResourceRequiredEvent } from './resources-required'; import { VehiclesSentEvent } from './vehicles-sent'; +import { TryToDistributeEvent } from './try-to-distribute'; +import { VehicleTransferSuccessfulEvent } from './vehicle-transfer-successful'; +import { TransferConnectionMissingEvent } from './transfer-connection-missing'; import { SendRequestEvent } from './send-request'; export const simulationEvents = { @@ -26,6 +29,9 @@ export const simulationEvents = { StartCollectingInformationEvent, ResourceRequiredEvent, VehiclesSentEvent, + TryToDistributeEvent, + VehicleTransferSuccessfulEvent, + TransferConnectionMissingEvent, SendRequestEvent, }; @@ -50,6 +56,9 @@ export const simulationEventDictionary: ExerciseSimulationEventDictionary = { startCollectingInformationEvent: StartCollectingInformationEvent, resourceRequiredEvent: ResourceRequiredEvent, vehiclesSentEvent: VehiclesSentEvent, + tryToDistributeEvent: TryToDistributeEvent, + vehicleTransferSuccessfulEvent: VehicleTransferSuccessfulEvent, + transferConnectionMissingEvent: TransferConnectionMissingEvent, sendRequestEvent: SendRequestEvent, }; diff --git a/shared/src/simulation/events/index.ts b/shared/src/simulation/events/index.ts index 6e35520b9..99cf2aad7 100644 --- a/shared/src/simulation/events/index.ts +++ b/shared/src/simulation/events/index.ts @@ -8,3 +8,6 @@ export * from './treatments-timer-event'; export * from './vehicle-arrived'; export * from './resources-required'; export * from './vehicles-sent'; +export * from './try-to-distribute'; +export * from './vehicle-transfer-successful'; +export * from './transfer-connection-missing'; diff --git a/shared/src/simulation/events/transfer-connection-missing.ts b/shared/src/simulation/events/transfer-connection-missing.ts new file mode 100644 index 000000000..d923bb14f --- /dev/null +++ b/shared/src/simulation/events/transfer-connection-missing.ts @@ -0,0 +1,26 @@ +import { IsUUID } from 'class-validator'; +import { getCreate } from '../../models/utils'; +import { UUID, uuidValidationOptions } from '../../utils'; +import { IsValue } from '../../utils/validators'; +import type { SimulationEvent } from './simulation-event'; + +export class TransferConnectionMissingEvent implements SimulationEvent { + @IsValue('transferConnectionMissingEvent') + readonly type = 'transferConnectionMissingEvent'; + + @IsUUID(4, uuidValidationOptions) + public readonly id: UUID; + + @IsUUID(4, uuidValidationOptions) + public readonly transferPointId: UUID; + + /** + * @deprecated Use {@link create} instead + */ + constructor(id: UUID, transferPointId: UUID) { + this.id = id; + this.transferPointId = transferPointId; + } + + static readonly create = getCreate(this); +} diff --git a/shared/src/simulation/events/try-to-distribute.ts b/shared/src/simulation/events/try-to-distribute.ts new file mode 100644 index 000000000..d3c591109 --- /dev/null +++ b/shared/src/simulation/events/try-to-distribute.ts @@ -0,0 +1,22 @@ +import { IsUUID } from 'class-validator'; +import { getCreate } from '../../models/utils'; +import { UUID } from '../../utils'; +import { IsValue } from '../../utils/validators'; +import type { SimulationEvent } from './simulation-event'; + +export class TryToDistributeEvent implements SimulationEvent { + @IsValue('tryToDistributeEvent') + readonly type = 'tryToDistributeEvent'; + + @IsUUID() + public readonly behaviorId: UUID; + + /** + * @deprecated Use {@link create} instead + */ + constructor(behaviorId: UUID) { + this.behaviorId = behaviorId; + } + + static readonly create = getCreate(this); +} diff --git a/shared/src/simulation/events/vehicle-transfer-successful.ts b/shared/src/simulation/events/vehicle-transfer-successful.ts new file mode 100644 index 000000000..b7532ff3e --- /dev/null +++ b/shared/src/simulation/events/vehicle-transfer-successful.ts @@ -0,0 +1,41 @@ +import { Type } from 'class-transformer'; +import { IsString, IsUUID, ValidateNested } from 'class-validator'; +import { VehicleResource, getCreate } from '../../models/utils'; +import { UUID, uuidValidationOptions } from '../../utils'; +import { IsValue } from '../../utils/validators'; +import type { SimulationEvent } from './simulation-event'; + +export class VehicleTransferSuccessfulEvent implements SimulationEvent { + @IsValue('vehicleTransferSuccessfulEvent') + readonly type = 'vehicleTransferSuccessfulEvent'; + + @IsUUID(4, uuidValidationOptions) + public readonly id: UUID; + + @IsUUID(4, uuidValidationOptions) + public readonly targetId: UUID; + + @IsString() + public readonly key: string; + + @Type(() => VehicleResource) + @ValidateNested() + readonly vehiclesSent: VehicleResource; + + /** + * @deprecated Use {@link create} instead + */ + constructor( + id: UUID, + targetId: UUID, + key: string, + vehiclesSent: VehicleResource + ) { + this.id = id; + this.targetId = targetId; + this.key = key; + this.vehiclesSent = vehiclesSent; + } + + static readonly create = getCreate(this); +} diff --git a/shared/src/simulation/events/vehicles-sent.ts b/shared/src/simulation/events/vehicles-sent.ts index 58903ba05..934746ae9 100644 --- a/shared/src/simulation/events/vehicles-sent.ts +++ b/shared/src/simulation/events/vehicles-sent.ts @@ -15,7 +15,7 @@ export class VehiclesSentEvent implements SimulationEvent { @Type(() => VehicleResource) @ValidateNested() - readonly vehiclesSent!: VehicleResource; + readonly vehiclesSent: VehicleResource; @IsString() readonly key: string; diff --git a/shared/src/store/action-reducers/simulation.ts b/shared/src/store/action-reducers/simulation.ts index 54749b36c..9ba17fcf4 100644 --- a/shared/src/store/action-reducers/simulation.ts +++ b/shared/src/store/action-reducers/simulation.ts @@ -2,6 +2,7 @@ import { IsInt, IsNumber, IsOptional, + IsString, IsUUID, Min, ValidateNested, @@ -170,6 +171,23 @@ export class RemoveRecurringReportsAction implements Action { public readonly informationType!: ReportableInformation; } +export class ChangeAutomaticDistributionLimitAction implements Action { + @IsValue('[AutomaticDistributionBehavior] Change Limit') + public readonly type = '[AutomaticDistributionBehavior] Change Limit'; + + @IsUUID(4, uuidValidationOptions) + public readonly simulatedRegionId!: UUID; + + @IsUUID(4, uuidValidationOptions) + public readonly behaviorId!: UUID; + + @IsString() + public readonly vehicleType!: string; + + @IsInt() + @Min(0) + public readonly newLimit!: number; +} export class UpdateRequestIntervalAction implements Action { @IsValue('[RequestBehavior] Update RequestInterval') public readonly type = '[RequestBehavior] Update RequestInterval'; @@ -185,6 +203,20 @@ export class UpdateRequestIntervalAction implements Action { public readonly requestInterval!: number; } +export class AddAutomaticDistributionDestinationAction implements Action { + @IsValue('[AutomaticDistributionBehavior] Add Destination') + public readonly type = '[AutomaticDistributionBehavior] Add Destination'; + + @IsUUID(4, uuidValidationOptions) + public readonly simulatedRegionId!: UUID; + + @IsUUID(4, uuidValidationOptions) + public readonly behaviorId!: UUID; + + @IsUUID(4, uuidValidationOptions) + public readonly destinationId!: string; +} + export class UpdateRequestTargetAction implements Action { @IsValue('[RequestBehavior] Update RequestTarget') public readonly type = '[RequestBehavior] Update RequestTarget'; @@ -200,6 +232,20 @@ export class UpdateRequestTargetAction implements Action { public readonly requestTarget!: ExerciseRequestTargetConfiguration; } +export class RemoveAutomaticDistributionDestinationAction implements Action { + @IsValue('[AutomaticDistributionBehavior] Remove Destination') + public readonly type = '[AutomaticDistributionBehavior] Remove Destination'; + + @IsUUID(4, uuidValidationOptions) + public readonly simulatedRegionId!: UUID; + + @IsUUID(4, uuidValidationOptions) + public readonly behaviorId!: UUID; + + @IsUUID(4, uuidValidationOptions) + public readonly destinationId!: string; +} + export class UpdatePromiseInvalidationIntervalAction implements Action { @IsValue('[RequestBehavior] Update Promise invalidation interval') public readonly type = @@ -429,6 +475,46 @@ export namespace SimulationActionReducers { rights: 'trainer', }; + export const changeAutomaticDistributionLimit: ActionReducer = + { + action: ChangeAutomaticDistributionLimitAction, + reducer( + draftState, + { simulatedRegionId, behaviorId, vehicleType, newLimit } + ) { + const automaticDistributionBehaviorState = getBehaviorById( + draftState, + simulatedRegionId, + behaviorId, + 'automaticallyDistributeVehiclesBehavior' + ); + + automaticDistributionBehaviorState.distributionLimits[ + vehicleType + ] = newLimit; + + if (newLimit === 0) { + delete automaticDistributionBehaviorState.remainingInNeed[ + vehicleType + ]; + } else { + if ( + !automaticDistributionBehaviorState.remainingInNeed[ + vehicleType + ] + ) { + automaticDistributionBehaviorState.remainingInNeed[ + vehicleType + ] = cloneDeepMutable( + automaticDistributionBehaviorState.distributionDestinations + ); + } + } + return draftState; + }, + rights: 'trainer', + }; + export const updateRequestInterval: ActionReducer = { action: UpdateRequestIntervalAction, @@ -458,6 +544,46 @@ export namespace SimulationActionReducers { rights: 'trainer', }; + export const addAutomaticDistributionLimit: ActionReducer = + { + action: AddAutomaticDistributionDestinationAction, + reducer( + draftState, + { simulatedRegionId, behaviorId, destinationId } + ) { + const automaticDistributionBehaviorState = getBehaviorById( + draftState, + simulatedRegionId, + behaviorId, + 'automaticallyDistributeVehiclesBehavior' + ); + + // Do not re-add the destination if it was already added previously + + if ( + automaticDistributionBehaviorState.distributionDestinations[ + destinationId + ] + ) { + throw new ReducerError( + `The destination with id: ${destinationId} was already added to the behavior with id: ${behaviorId} in simulated region with id:${simulatedRegionId}` + ); + } + + automaticDistributionBehaviorState.distributionDestinations[ + destinationId + ] = true; + + Object.values( + automaticDistributionBehaviorState.remainingInNeed + ).forEach((regionsInNeed) => { + regionsInNeed[destinationId] = true; + }); + return draftState; + }, + rights: 'trainer', + }; + export const updateRequestTarget: ActionReducer = { action: UpdateRequestTargetAction, @@ -487,6 +613,32 @@ export namespace SimulationActionReducers { rights: 'trainer', }; + export const removeAutomaticDistributionLimit: ActionReducer = + { + action: RemoveAutomaticDistributionDestinationAction, + reducer( + draftState, + { simulatedRegionId, behaviorId, destinationId } + ) { + const automaticDistributionBehaviorState = getBehaviorById( + draftState, + simulatedRegionId, + behaviorId, + 'automaticallyDistributeVehiclesBehavior' + ); + + delete automaticDistributionBehaviorState + .distributionDestinations[destinationId]; + + Object.values( + automaticDistributionBehaviorState.remainingInNeed + ).forEach((regionsInNeed) => { + delete regionsInNeed[destinationId]; + }); + return draftState; + }, + rights: 'trainer', + }; export const updatePromiseInvalidationInterval: ActionReducer = { action: UpdatePromiseInvalidationIntervalAction, diff --git a/shared/src/utils/validators/is-uuid-set.ts b/shared/src/utils/validators/is-uuid-set.ts index 8e586a497..ed8ccc521 100644 --- a/shared/src/utils/validators/is-uuid-set.ts +++ b/shared/src/utils/validators/is-uuid-set.ts @@ -1,5 +1,5 @@ import type { ValidationOptions, ValidationArguments } from 'class-validator'; -import { isUUID } from 'class-validator'; +import { isUUID, isString } from 'class-validator'; import type { UUID } from '../uuid'; import type { UUIDSet } from '../uuid-set'; import { createMapValidator } from './create-map-validator'; @@ -25,3 +25,19 @@ export function IsUUIDSet( validationOptions ); } + +export const isUUIDSetMap = createMapValidator({ + keyValidator: isString, + valueValidator: isUUIDSet, +}); + +// eslint-disable-next-line @typescript-eslint/naming-convention +export function IsUUIDSetMap( + validationOptions?: ValidationOptions & { each?: Each } +): GenericPropertyDecorator<{ [key: string]: UUIDSet }, Each> { + return makeValidator<{ [key: string]: UUIDSet }, Each>( + 'isUUIDSetMap', + (value: unknown, args?: ValidationArguments) => isUUIDSetMap(value), + validationOptions + ); +} diff --git a/test-scenarios b/test-scenarios index 07106f3df..d6aae089a 160000 --- a/test-scenarios +++ b/test-scenarios @@ -1 +1 @@ -Subproject commit 07106f3df3a23c0fd4f55b58eac4e91af91204c9 +Subproject commit d6aae089ad92d5b02f8ae7fc56714f34515239c8 From a89914cd461b6e4b7da789b1f2e38f95dc584911 Mon Sep 17 00:00:00 2001 From: Lukas Hagen <43916057+Greenscreen23@users.noreply.github.com> Date: Wed, 19 Apr 2023 12:57:53 +0200 Subject: [PATCH 29/31] Fix/867 request behavior blocks when it does not recieve an answer (#868) * Remove the state system from the request behavior and deduplicate request radiograms * Update test scenarios accordingly * Fix test scenarios * Change wording in frontend * Also pass events that notify when a request has been fulfilled * Fix ids in export according to #874 * Always tell the request target that we are not requesting resources anymore. * Invalidate requestedResources after receiving vehicles * Fix linter * apply suggestions --- ...w-behavior-request-vehicles.component.html | 10 +- ...iew-behavior-request-vehicles.component.ts | 9 - .../models/utils/request-target/trainees.ts | 61 +- .../provide-personnel-from-vehicles.ts | 30 +- .../activities/reassign-treatments.ts | 21 +- .../activities/transfer-vehicles.ts | 54 +- .../src/simulation/behaviors/request.spec.ts | 583 +++++------------- shared/src/simulation/behaviors/request.ts | 134 ++-- shared/src/simulation/events/vehicles-sent.ts | 8 +- shared/src/store/action-reducers/radiogram.ts | 9 +- test-scenarios | 2 +- 11 files changed, 319 insertions(+), 602 deletions(-) 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 index a927c584d..ad13e028b 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/request-vehicles/simulated-region-overview-behavior-request-vehicles.component.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 @@ -2,17 +2,13 @@
Anfragenziel
- Wartet auf die Antwort einer Anfrage von Nächste Anfrage potentiell in - {{ nextTimeoutIn.time | date : 'mm:ss' }} anNächste potentielle Anfrage in + {{ nextTimeoutIn.time | formatDuration }} diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/request-vehicles/simulated-region-overview-behavior-request-vehicles.component.ts b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/request-vehicles/simulated-region-overview-behavior-request-vehicles.component.ts index 33b086fb4..9862718e0 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/request-vehicles/simulated-region-overview-behavior-request-vehicles.component.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/request-vehicles/simulated-region-overview-behavior-request-vehicles.component.ts @@ -6,7 +6,6 @@ import type { RequestBehaviorState, } from 'digital-fuesim-manv-shared'; import { - isWaitingForAnswer, UUID, SimulatedRegionRequestTargetConfiguration, TraineesRequestTargetConfiguration, @@ -42,8 +41,6 @@ export class RequestVehiclesComponent implements OnChanges { [key in RequestTargetOption]: string; }>; - waitingForAnswer$!: Observable; - nextTimeoutIn$!: Observable; selectedRequestTarget$!: Observable; @@ -92,12 +89,6 @@ export class RequestVehiclesComponent implements OnChanges { }) ); - this.waitingForAnswer$ = this.requestBehaviorState$.pipe( - map((requestBehaviorState) => - isWaitingForAnswer(requestBehaviorState) - ) - ); - const activities$ = this.store.select( createSelectActivityStates(this.simulatedRegionId) ); diff --git a/shared/src/models/utils/request-target/trainees.ts b/shared/src/models/utils/request-target/trainees.ts index afe25717d..b281b2684 100644 --- a/shared/src/models/utils/request-target/trainees.ts +++ b/shared/src/models/utils/request-target/trainees.ts @@ -5,6 +5,10 @@ import { RadiogramUnpublishedStatus } from '../../../models/radiogram/status/rad import { publishRadiogram } from '../../../models/radiogram/radiogram-helpers-mutable'; import { nextUUID } from '../../../simulation/utils/randomness'; import { ResourceRequestRadiogram } from '../../radiogram/resource-request-radiogram'; +import type { Mutable } from '../../../utils/immutability'; +import { isDone, isUnread } from '../../radiogram/radiogram-helpers'; +import { StrictObject } from '../../../utils/strict-object'; +import { isEmptyResource } from '../rescue-resource'; import type { RequestTarget, RequestTargetConfiguration, @@ -29,17 +33,52 @@ export const traineesRequestTarget: RequestTarget { - publishRadiogram( - draftState, - cloneDeepMutable( - ResourceRequestRadiogram.create( - nextUUID(draftState), - requestingSimulatedRegionId, - RadiogramUnpublishedStatus.create(), - requestedResource, - key + const unreadRadiogram = StrictObject.values( + draftState.radiograms + ).find( + (radiogram) => + radiogram.type === 'resourceRequestRadiogram' && + isUnread(radiogram) && + radiogram.key === key + ) as Mutable | undefined; + if (unreadRadiogram) { + if (isEmptyResource(requestedResource)) { + delete draftState.radiograms[unreadRadiogram.id]; + } else { + unreadRadiogram.requiredResource = requestedResource; + } + return; + } + + if ( + StrictObject.values(draftState.radiograms) + .filter( + (radiogram) => + radiogram.type === 'resourceRequestRadiogram' && + radiogram.key === key + ) + .every(isDone) && + !isEmptyResource(requestedResource) + ) { + publishRadiogram( + draftState, + cloneDeepMutable( + ResourceRequestRadiogram.create( + nextUUID(draftState), + requestingSimulatedRegionId, + RadiogramUnpublishedStatus.create(), + requestedResource, + key + ) ) - ) - ); + ); + // eslint-disable-next-line no-useless-return + return; + } + + /** + * There is a radiogram that is currently accepted, + * we therefore wait for an answer and don't send another request + */ }, }; diff --git a/shared/src/simulation/activities/provide-personnel-from-vehicles.ts b/shared/src/simulation/activities/provide-personnel-from-vehicles.ts index bfc05fb21..480ba2602 100644 --- a/shared/src/simulation/activities/provide-personnel-from-vehicles.ts +++ b/shared/src/simulation/activities/provide-personnel-from-vehicles.ts @@ -83,16 +83,6 @@ export const providePersonnelFromVehiclesActivity: SimulationActivity personnelInVehicleTemplate(draftState, id)) .filter( @@ -146,19 +136,13 @@ export const providePersonnelFromVehiclesActivity: SimulationActivity vehicleCount > 0 - ) - ) { - const event = ResourceRequiredEvent.create( - nextUUID(draftState), - simulatedRegion.id, - VehicleResource.create(missingVehicleCounts), - activityState.key - ); - sendSimulationEvent(simulatedRegion, event); - } + const event = ResourceRequiredEvent.create( + nextUUID(draftState), + simulatedRegion.id, + VehicleResource.create(missingVehicleCounts), + activityState.key + ); + sendSimulationEvent(simulatedRegion, event); terminate(); }, diff --git a/shared/src/simulation/activities/reassign-treatments.ts b/shared/src/simulation/activities/reassign-treatments.ts index 29841e7f2..7acd7625d 100644 --- a/shared/src/simulation/activities/reassign-treatments.ts +++ b/shared/src/simulation/activities/reassign-treatments.ts @@ -1,7 +1,6 @@ import { IsInt, IsOptional, IsUUID, Min } from 'class-validator'; import type { PatientStatus, PersonnelType } from '../../models/utils'; import { - isEmptyResource, PersonnelResource, getCreate, isInSpecificSimulatedRegion, @@ -206,17 +205,15 @@ export const reassignTreatmentsActivity: SimulationActivity 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 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( @@ -221,8 +210,7 @@ export const transferVehiclesActivity: SimulationActivity, - simulatedRegion: Mutable, - behaviorState: Mutable - ) => void, initializeRequestsAndPromises: ( state: Mutable, simulatedRegion: Mutable, @@ -130,10 +112,15 @@ function setupStateAndInteract( draftState.simulatedRegions[simulatedRegion.id]!; const behaviorState = mutableSimulatedRegion .behaviors[0] as Mutable; - initializeBehaviorState( - draftState, + behaviorState.recurringEventActivityId = uuid(); + addActivity( mutableSimulatedRegion, - behaviorState + RecurringEventActivityState.create( + behaviorState.recurringEventActivityId, + SendRequestEvent.create(), + draftState.currentTime, + behaviorState.requestInterval + ) ); initializeRequestsAndPromises( draftState, @@ -224,46 +211,10 @@ const updateInvalidationInterval = ( ) => { behaviorState.invalidatePromiseInterval = newInvalidationInterval; // update its promised resources - getResourcesToRequest(draftState, simulatedRegion, behaviorState); + getResourcesToRequest(draftState, 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, @@ -411,32 +362,14 @@ const sendEvent = { ) ); }, - vehiclesSendEventForAnswerKey: ( + vehiclesSendEvent: ( 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' - ) + VehiclesSentEvent.create(uuid(), VehicleResource.create({ KTW: 1 })) ); }, ktwVehicleArrivedEvent: ( @@ -468,60 +401,22 @@ const sendEvent = { }, }; -// 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 - ); + it('should note the request', () => { + const { afterBehaviorState } = setupStateAndInteract( + 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 - ); - }); - } - ); + expect( + afterBehaviorState.requestedResources['new-request-key'] + ).toEqual(VehicleResource.create({ KTW: 1 })); + }); } ); }); @@ -530,103 +425,38 @@ describe('request behavior', () => { 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', () => { + it('should overwrite any existing requests', () => { const { afterBehaviorState } = setupStateAndInteract( - setBehaviorState.onTimer, addRequestsAndPromises[requestsAndPromises], - sendEvent[event] + sendEvent.resourceRequiredEventWithKnownKey ); - assertNotWaitingState(afterBehaviorState); + expect( + Object.keys(afterBehaviorState.requestedResources) + .length + ).toBe(1); }); } ); }); - describe('on a vehiclesSendEventForAnswerKey', () => { + describe('on a vehicle send event', () => { 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); - }); - }); - } - ); - }); + it('should note the promise', () => { + const { afterBehaviorState } = setupStateAndInteract( + addRequestsAndPromises[requestsAndPromises], + sendEvent.vehiclesSendEvent + ); - 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); - }); + const promisedResources = + afterBehaviorState.promisedResources; + expect(promisedResources.length).toBeGreaterThanOrEqual(1); + const promise = promisedResources.at(-1)!; + expect(promise.resource).toEqual( + VehicleResource.create({ KTW: 1 }) + ); }); } ); @@ -634,150 +464,96 @@ describe('request behavior', () => { 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 - ); - }); - } - ); + it('should not change its noted promises', () => { + const { beforeBehaviorState, afterBehaviorState } = + setupStateAndInteract( + 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(); - }); - } - ); + it('should remove the promise', () => { + const { afterBehaviorState } = setupStateAndInteract( + 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, + it('should create a request via an activity', () => { + const { afterSimulatedRegion, afterBehaviorState } = + setupStateAndInteract( 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 activities = afterSimulatedRegion.activities; - expect( - StrictObject.keys(activities).length - ).toBeGreaterThanOrEqual(1); - - const activity = StrictObject.values(activities).find( - (a) => a.type === 'createRequestActivity' - ); - expect(activity).toBeDefined(); + 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 - ); - }); + const typedActivity = activity as CreateRequestActivityState; + expect(typedActivity.targetConfiguration).toEqual( + afterBehaviorState.requestTarget + ); + expect(typedActivity.requestedResource).toEqual( + VehicleResource.create({ KTW: 1 }) + ); }); }); + }); - describe.each(withoutVehiclesRequired)('%s', (requestsAndPromises) => { - describe('in onTimer state', () => { - it('should stay in the onTimer state', () => { + describe('when the request interval is updated', () => { + describe.each(StrictObject.keys(addRequestsAndPromises))( + '%s', + (requestsAndPromises) => { + it('should update the request interval', () => { const { afterBehaviorState } = setupStateAndInteract( - setBehaviorState.onTimer, addRequestsAndPromises[requestsAndPromises], - sendEvent.sendRequestEvent + updateRequestInterval ); - assertNotWaitingState(afterBehaviorState); + expect(afterBehaviorState.requestInterval).toBe( + newRequestInterval + ); }); - }); - }); - }); - 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; + it('should update the timer', () => { + const { afterSimulatedRegion } = setupStateAndInteract( + addRequestsAndPromises[requestsAndPromises], + updateRequestInterval + ); - expect( - afterRecurringEventActivity.recurrenceIntervalTime - ).toBe(newRequestInterval); - }); - } - ); + const afterRecurringEventActivity = StrictObject.values( + afterSimulatedRegion.activities + ).find( + (a) => a.type === 'recurringEventActivity' + ) as RecurringEventActivityState; + + expect( + afterRecurringEventActivity.recurrenceIntervalTime + ).toBe(newRequestInterval); + }); } ); }); @@ -786,34 +562,15 @@ describe('request behavior', () => { 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 - ); + it('should update the request target', () => { + const { afterBehaviorState } = setupStateAndInteract( + addRequestsAndPromises[requestsAndPromises], + updateRequestTarget + ); - assertNotWaitingState(afterBehaviorState); - }); + expect(afterBehaviorState.requestTarget.type).toEqual( + 'simulatedRegionRequestTarget' + ); }); } ); @@ -821,73 +578,59 @@ describe('request behavior', () => { 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 - ); - }); - } - ); + it('should not invalidate any promises', () => { + const { beforeBehaviorState, afterBehaviorState } = + setupStateAndInteract( + 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); - } - } - ); - }); - } - ); + it('should invalidate old promises', () => { + const { afterState, afterBehaviorState } = + setupStateAndInteract( + 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( + 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 index f075f9ccd..1591bb471 100644 --- a/shared/src/simulation/behaviors/request.ts +++ b/shared/src/simulation/behaviors/request.ts @@ -2,7 +2,6 @@ import { IsArray, IsInt, IsOptional, - IsString, IsUUID, Min, ValidateNested, @@ -19,10 +18,7 @@ import { nextUUID } from '../utils/randomness'; import { RecurringEventActivityState } from '../activities'; import { SendRequestEvent } from '../events/send-request'; import { CreateRequestActivityState } from '../activities/create-request'; -import { - isEmptyResource, - VehicleResource, -} from '../../models/utils/rescue-resource'; +import { VehicleResource } from '../../models/utils/rescue-resource'; import { ExerciseRequestTargetConfiguration, requestTargetTypeOptions, @@ -48,13 +44,6 @@ export class RequestBehaviorState implements SimulationBehaviorState { @IsUUID() public readonly id: UUID = uuid(); - /** - * @deprecated Use {@link isWaitingForAnswer} instead - */ - @IsString() - @IsOptional() - public readonly answerKey?: string; - @IsUUID() @IsOptional() public readonly recurringEventActivityId?: UUID; @@ -77,18 +66,12 @@ export class RequestBehaviorState implements SimulationBehaviorState { @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); } @@ -131,10 +114,6 @@ export const requestBehavior: SimulationBehavior = { ) ) ); - - if (event.key === behaviorState.answerKey) { - behaviorState.answerKey = undefined; - } break; } case 'vehicleArrivedEvent': { @@ -143,23 +122,23 @@ export const requestBehavior: SimulationBehavior = { 'vehicle', event.vehicleId ); - let arrivedResourceDescripton: Partial = { + let arrivedResourceDescription: Partial = { [vehicle.vehicleType]: 1, }; behaviorState.promisedResources.forEach((promise) => { const remainingResources = subtractPartialResourceDescriptions( - arrivedResourceDescripton, + arrivedResourceDescription, promise.resource.vehicleCounts ); promise.resource.vehicleCounts = subtractPartialResourceDescriptions( promise.resource.vehicleCounts, - arrivedResourceDescripton + arrivedResourceDescription ) as ResourceDescription; - arrivedResourceDescripton = remainingResources; + arrivedResourceDescription = remainingResources; }); behaviorState.promisedResources = behaviorState.promisedResources.filter( @@ -167,43 +146,66 @@ export const requestBehavior: SimulationBehavior = { Object.keys(promise.resource.vehicleCounts).length > 0 ); + + behaviorState.requestedResources = {}; break; } case 'sendRequestEvent': { - if (!isWaitingForAnswer(behaviorState)) { - const resourcesToRequest = getResourcesToRequest( - draftState, - simulatedRegion, - behaviorState - ); - 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); - addActivity( - simulatedRegion, - CreateRequestActivityState.create( - activityId, - behaviorState.requestTarget, - resource, - behaviorState.answerKey - ) - ); - } - } + const resourcesToRequest = getResourcesToRequest( + draftState, + behaviorState + ); + const resource = VehicleResource.create( + resourcesToRequest as ResourceDescription + ); + addActivity( + simulatedRegion, + CreateRequestActivityState.create( + nextUUID(draftState), + behaviorState.requestTarget, + resource, + requestBehaviorKey(simulatedRegion) + ) + ); break; } default: break; } }, + onRemove(draftState, simulatedRegion, behaviorState) { + addActivity( + simulatedRegion, + CreateRequestActivityState.create( + nextUUID(draftState), + behaviorState.requestTarget, + VehicleResource.create({}), + requestBehaviorKey(simulatedRegion) + ) + ); + }, }; -export function isWaitingForAnswer(behaviorState: RequestBehaviorState) { - return behaviorState.answerKey !== undefined; +function requestBehaviorKey(simulatedRegion: Mutable) { + return `${simulatedRegion.id}-request`; +} + +export function updateBehaviorsRequestTarget( + draftState: Mutable, + simulatedRegion: Mutable, + behaviorState: Mutable, + requestTarget: ExerciseRequestTargetConfiguration +) { + addActivity( + simulatedRegion, + CreateRequestActivityState.create( + nextUUID(draftState), + behaviorState.requestTarget, + VehicleResource.create({}), + requestBehaviorKey(simulatedRegion) + ) + ); + behaviorState.requestTarget = cloneDeepMutable(requestTarget); } export function updateBehaviorsRequestInterval( @@ -224,23 +226,8 @@ export function updateBehaviorsRequestInterval( 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 = addPartialResourceDescriptions( @@ -250,12 +237,11 @@ export function getResourcesToRequest( ); // remove invalidated resources - let firstValidIndex: number | undefined = - behaviorState.promisedResources.findIndex( - (promise) => - promise.promisedTime + behaviorState.invalidatePromiseInterval > - draftState.currentTime - ); + let firstValidIndex = behaviorState.promisedResources.findIndex( + (promise) => + promise.promisedTime + behaviorState.invalidatePromiseInterval > + draftState.currentTime + ); if (firstValidIndex === -1) firstValidIndex = behaviorState.promisedResources.length; behaviorState.promisedResources.splice(0, firstValidIndex); diff --git a/shared/src/simulation/events/vehicles-sent.ts b/shared/src/simulation/events/vehicles-sent.ts index 934746ae9..9c28f028c 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 { IsString, IsUUID, ValidateNested } from 'class-validator'; +import { IsUUID, ValidateNested } from 'class-validator'; import { getCreate } from '../../models/utils'; import { VehicleResource } from '../../models/utils/rescue-resource'; import { UUID, uuidValidationOptions } from '../../utils'; @@ -17,16 +17,12 @@ export class VehiclesSentEvent implements SimulationEvent { @ValidateNested() readonly vehiclesSent: VehicleResource; - @IsString() - readonly key: string; - /** * @deprecated Use {@link create} instead */ - constructor(id: UUID, vehiclesSent: VehicleResource, key: string) { + constructor(id: UUID, vehiclesSent: VehicleResource) { this.id = id; this.vehiclesSent = vehiclesSent; - this.key = key; } static readonly create = getCreate(this); diff --git a/shared/src/store/action-reducers/radiogram.ts b/shared/src/store/action-reducers/radiogram.ts index 68f27dee5..978552e42 100644 --- a/shared/src/store/action-reducers/radiogram.ts +++ b/shared/src/store/action-reducers/radiogram.ts @@ -78,8 +78,7 @@ export namespace RadiogramActionReducers { cloneDeepMutable( VehiclesSentEvent.create( nextUUID(draftState), - VehicleResource.create({}), - radiogram.key + VehicleResource.create({}) ) ) ); @@ -112,8 +111,7 @@ export namespace RadiogramActionReducers { cloneDeepMutable( VehiclesSentEvent.create( nextUUID(draftState), - radiogram.requiredResource, - radiogram.key + radiogram.requiredResource ) ) ); @@ -145,8 +143,7 @@ export namespace RadiogramActionReducers { cloneDeepMutable( VehiclesSentEvent.create( nextUUID(draftState), - VehicleResource.create({}), - radiogram.key + VehicleResource.create({}) ) ) ); diff --git a/test-scenarios b/test-scenarios index d6aae089a..3b63693a9 160000 --- a/test-scenarios +++ b/test-scenarios @@ -1 +1 @@ -Subproject commit d6aae089ad92d5b02f8ae7fc56714f34515239c8 +Subproject commit 3b63693a96cb5f9c4228c50ce577adc36fbbf92e From 8da8529e4dd356e87a146ee6f627b60ee0b13c6f Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Wed, 19 Apr 2023 12:33:07 +0000 Subject: [PATCH 30/31] Prepare release v0.4.0 --- CHANGELOG.md | 5 ++++- backend/package-lock.json | 6 +++--- backend/package.json | 2 +- benchmark/package-lock.json | 6 +++--- benchmark/package.json | 2 +- docs/swagger.yml | 2 +- frontend/package-lock.json | 6 +++--- frontend/package.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- shared/package-lock.json | 4 ++-- shared/package.json | 2 +- 12 files changed, 23 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a213679ff..621ade92b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project does **not** adhere to [Semantic Versioning](https://semver.org ## [Unreleased] +## [0.4.0] - 2023-04-19 + ### Added - There is now a display for how many different variations of a patient template exists. @@ -142,7 +144,8 @@ and this project does **not** adhere to [Semantic Versioning](https://semver.org ### Initial unstable release of Digitale FüSim MANV -[Unreleased]: https://github.com/hpi-sam/digital-fuesim-manv/compare/v0.3.0...HEAD +[Unreleased]: https://github.com/hpi-sam/digital-fuesim-manv/compare/0.4.0...HEAD +[0.4.0]: https://github.com/hpi-sam/digital-fuesim-manv/compare/v0.3.0...0.4.0 [0.3.0]: https://github.com/hpi-sam/digital-fuesim-manv/compare/v0.2.1...v0.3.0 [0.2.1]: https://github.com/hpi-sam/digital-fuesim-manv/compare/v0.2.0...v0.2.1 [0.2.0]: https://github.com/hpi-sam/digital-fuesim-manv/compare/v0.1.0...v0.2.0 diff --git a/backend/package-lock.json b/backend/package-lock.json index a0caa06c5..47efd7756 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,12 +1,12 @@ { "name": "digital-fuesim-manv-backend", - "version": "0.3.0", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "digital-fuesim-manv-backend", - "version": "0.3.0", + "version": "0.4.0", "dependencies": { "class-transformer": "^0.5.1", "class-validator": "^0.14.0", @@ -48,7 +48,7 @@ }, "../shared": { "name": "digital-fuesim-manv-shared", - "version": "0.3.0", + "version": "0.4.0", "dependencies": { "@noble/hashes": "^1.2.0", "class-transformer": "^0.5.1", diff --git a/backend/package.json b/backend/package.json index 67ff3d1cd..592f90bc7 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "digital-fuesim-manv-backend", - "version": "0.3.0", + "version": "0.4.0", "type": "module", "scripts": { "start:once:linux-macos": "NODE_ENV=production node --experimental-specifier-resolution=node dist/src/index.js", diff --git a/benchmark/package-lock.json b/benchmark/package-lock.json index a5916216a..132346dd2 100644 --- a/benchmark/package-lock.json +++ b/benchmark/package-lock.json @@ -1,12 +1,12 @@ { "name": "digital-fuesim-manv-benchmark", - "version": "0.3.0", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "digital-fuesim-manv-benchmark", - "version": "0.3.0", + "version": "0.4.0", "dependencies": { "digital-fuesim-manv-shared": "file:../shared", "immer": "^9.0.17", @@ -32,7 +32,7 @@ }, "../shared": { "name": "digital-fuesim-manv-shared", - "version": "0.3.0", + "version": "0.4.0", "dependencies": { "@noble/hashes": "^1.2.0", "class-transformer": "^0.5.1", diff --git a/benchmark/package.json b/benchmark/package.json index 473efb463..328c6e2e2 100644 --- a/benchmark/package.json +++ b/benchmark/package.json @@ -1,6 +1,6 @@ { "name": "digital-fuesim-manv-benchmark", - "version": "0.3.0", + "version": "0.4.0", "type": "module", "scripts": { "lint": "eslint --max-warnings 0 --ignore-path .gitignore \"./**/*.{ts,js,yml,html}\"", diff --git a/docs/swagger.yml b/docs/swagger.yml index 474934009..22ac2fbd9 100644 --- a/docs/swagger.yml +++ b/docs/swagger.yml @@ -2,7 +2,7 @@ openapi: 3.0.3 info: title: Digital Fuesim MANV HTTP API description: HTTP API of the digital-fuesim-manv project - version: 0.3.0 + version: 0.4.0 paths: /api/health: get: diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7ad31ebfb..cf37fe20c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "digital-fuesim-manv-frontend", - "version": "0.3.0", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "digital-fuesim-manv-frontend", - "version": "0.3.0", + "version": "0.4.0", "dependencies": { "@angular/animations": "~15.1.0", "@angular/common": "~15.1.0", @@ -66,7 +66,7 @@ }, "../shared": { "name": "digital-fuesim-manv-shared", - "version": "0.3.0", + "version": "0.4.0", "dependencies": { "@noble/hashes": "^1.2.0", "class-transformer": "^0.5.1", diff --git a/frontend/package.json b/frontend/package.json index 6d7b33928..6266eea29 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "digital-fuesim-manv-frontend", - "version": "0.3.0", + "version": "0.4.0", "type": "module", "scripts": { "cy:open": "cypress open", diff --git a/package-lock.json b/package-lock.json index 0f1c29186..0dab5b55a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "digital-fuesim-manv", - "version": "0.3.0", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "digital-fuesim-manv", - "version": "0.3.0", + "version": "0.4.0", "devDependencies": { "concurrently": "^7.6.0", "nyc": "^15.1.0", diff --git a/package.json b/package.json index cec9b9e4b..2159361ee 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "digital-fuesim-manv", - "version": "0.3.0", + "version": "0.4.0", "type": "module", "scripts": { "build": "cd shared && npm run build && cd .. && concurrently \"cd frontend && npm run build\" \"cd backend && npm run build\"", diff --git a/shared/package-lock.json b/shared/package-lock.json index b2298f857..7dcd14d97 100644 --- a/shared/package-lock.json +++ b/shared/package-lock.json @@ -1,12 +1,12 @@ { "name": "digital-fuesim-manv-shared", - "version": "0.3.0", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "digital-fuesim-manv-shared", - "version": "0.3.0", + "version": "0.4.0", "dependencies": { "@noble/hashes": "^1.2.0", "class-transformer": "^0.5.1", diff --git a/shared/package.json b/shared/package.json index d9b391d9e..c884ac7d4 100644 --- a/shared/package.json +++ b/shared/package.json @@ -1,6 +1,6 @@ { "name": "digital-fuesim-manv-shared", - "version": "0.3.0", + "version": "0.4.0", "type": "module", "main": "./dist/index.js", "esnext": "./dist/index.js", From 36723f4cd1512ae3bcf2fdb96e9bb1020832e23f Mon Sep 17 00:00:00 2001 From: Nils1729 <45318774+Nils1729@users.noreply.github.com> Date: Wed, 19 Apr 2023 15:20:42 +0200 Subject: [PATCH 31/31] Fix CHANGELOG --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 621ade92b..13cb178e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ and this project does **not** adhere to [Semantic Versioning](https://semver.org - 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 that automatically distributes vehicles to regions. - - The types and optional limits of the distribution can be specified + - The types and optional limits of the distribution can be specified. - The behavior distributes the vehicles in rounds of one vehicle per category for every region every 60 seconds - 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. @@ -144,7 +144,7 @@ and this project does **not** adhere to [Semantic Versioning](https://semver.org ### Initial unstable release of Digitale FüSim MANV -[Unreleased]: https://github.com/hpi-sam/digital-fuesim-manv/compare/0.4.0...HEAD +[Unreleased]: https://github.com/hpi-sam/digital-fuesim-manv/compare/v0.4.0...HEAD [0.4.0]: https://github.com/hpi-sam/digital-fuesim-manv/compare/v0.3.0...0.4.0 [0.3.0]: https://github.com/hpi-sam/digital-fuesim-manv/compare/v0.2.1...v0.3.0 [0.2.1]: https://github.com/hpi-sam/digital-fuesim-manv/compare/v0.2.0...v0.2.1