diff --git a/backend/src/fuesim-server.spec.ts b/backend/src/fuesim-server.spec.ts index 0d4cd3528..ae28339a5 100644 --- a/backend/src/fuesim-server.spec.ts +++ b/backend/src/fuesim-server.spec.ts @@ -38,6 +38,7 @@ describe('Exercise saving', () => { alarmGroup: { alarmGroupVehicles: {}, id: uuid(), + type: 'alarmGroup', name: 'Alarm Group', }, }, diff --git a/frontend/src/app/pages/exercises/exercise/shared/editor-panel/create-image-template-modal/create-image-template-modal.component.ts b/frontend/src/app/pages/exercises/exercise/shared/editor-panel/create-image-template-modal/create-image-template-modal.component.ts index 11a6aead7..134360443 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/editor-panel/create-image-template-modal/create-image-template-modal.component.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/editor-panel/create-image-template-modal/create-image-template-modal.component.ts @@ -35,6 +35,7 @@ export class CreateImageTemplateModalComponent { type: '[MapImageTemplate] Add mapImageTemplate', mapImageTemplate: { id: uuid(), + type: 'mapImageTemplate', image: { url, height, diff --git a/frontend/src/app/pages/exercises/exercise/shared/emergency-operations-center/send-alarm-group-interface/send-alarm-group-interface.component.ts b/frontend/src/app/pages/exercises/exercise/shared/emergency-operations-center/send-alarm-group-interface/send-alarm-group-interface.component.ts index 15b9189ab..06755aa93 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/emergency-operations-center/send-alarm-group-interface/send-alarm-group-interface.component.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/emergency-operations-center/send-alarm-group-interface/send-alarm-group-interface.component.ts @@ -112,7 +112,7 @@ export class SendAlarmGroupInterfaceComponent implements OnDestroy { }), this.exerciseService.proposeAction({ type: '[Transfer] Add to transfer', - elementType: 'vehicles', + elementType: vehicleParameters.vehicle.type, elementId: vehicleParameters.vehicle.id, startPoint: AlarmGroupStartPoint.create( alarmGroup.name, diff --git a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/catering-lines-feature-manager.ts b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/catering-lines-feature-manager.ts index d3088c938..8272b8807 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/catering-lines-feature-manager.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/catering-lines-feature-manager.ts @@ -18,7 +18,6 @@ export class CateringLinesFeatureManager > implements FeatureManager { - readonly type = 'cateringLines'; readonly unsupportedChangeProperties = new Set(['id'] as const); private readonly lineStyleHelper = new LineStyleHelper( diff --git a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/element-manager.ts b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/element-manager.ts index 4ff6e3b48..d341b294e 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/element-manager.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/element-manager.ts @@ -18,19 +18,12 @@ export abstract class ElementManager< UnsupportedChangeProperties > = Exclude, UnsupportedChangeProperties> > { - /** - * When an element gets (dragged &) dropped, this identifies the type of the dropped element. - * @example `patients` - */ - abstract readonly type: string; - /** * This should be called if a new element is added. */ public onElementCreated(element: Element) { const feature = this.createFeature(element); - feature.set(featureKeys.type, this.type); - feature.set(featureKeys.value, element); + feature.set(featureElementKey, element); } /** @@ -68,7 +61,7 @@ export abstract class ElementManager< this.onElementCreated(newElement); return; } - elementFeature.set(featureKeys.value, newElement); + elementFeature.set(featureElementKey, newElement); this.changeFeature( oldElement, newElement, @@ -111,10 +104,7 @@ export abstract class ElementManager< ): Feature | undefined; public getElementFromFeature(feature: Feature) { - return { - type: feature.get(featureKeys.type), - value: feature.get(featureKeys.value), - }; + return feature.get(featureElementKey); } private areAllPropertiesSupported( @@ -132,8 +122,4 @@ export abstract class ElementManager< /** * The keys of the feature, where the type and most recent value of the respective element are saved to */ -const featureKeys = { - value: 'elementValue', - // TODO: In the future the type should be saved in the element itself - type: 'elementType', -}; +const featureElementKey = 'element'; diff --git a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/map-images-feature-manager.ts b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/map-images-feature-manager.ts index cee02e94a..1d34bd8ae 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/map-images-feature-manager.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/map-images-feature-manager.ts @@ -16,9 +16,8 @@ import { ImageStyleHelper } from '../utility/style-helper/image-style-helper'; import { MoveableFeatureManager } from './moveable-feature-manager'; export class MapImageFeatureManager extends MoveableFeatureManager { - readonly type = 'mapImages'; private readonly imageStyleHelper = new ImageStyleHelper( - (feature) => this.getElementFromFeature(feature)!.value.image + (feature) => (this.getElementFromFeature(feature) as MapImage).image ); private readonly popupHelper = new ImagePopupHelper(this.olMap, this.layer); @@ -46,7 +45,8 @@ export class MapImageFeatureManager extends MoveableFeatureManager { resolution ); style.setZIndex( - this.getElementFromFeature(feature as Feature)!.value.zIndex + (this.getElementFromFeature(feature as Feature) as MapImage) + .zIndex ); return style; }); @@ -69,7 +69,7 @@ export class MapImageFeatureManager extends MoveableFeatureManager { } override isFeatureTranslatable(feature: Feature): boolean { - const mapImage = this.getElementFromFeature(feature).value as MapImage; + const mapImage = this.getElementFromFeature(feature) as MapImage; return ( selectStateSnapshot(selectCurrentRole, this.store) === 'trainer' && !mapImage.isLocked diff --git a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/material-feature-manager.ts b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/material-feature-manager.ts index 44888b48e..42bd0446e 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/material-feature-manager.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/material-feature-manager.ts @@ -17,13 +17,12 @@ import { MoveableFeatureManager } from './moveable-feature-manager'; export class MaterialFeatureManager extends MoveableFeatureManager< WithPosition > { - readonly type = 'materials'; private readonly imageStyleHelper = new ImageStyleHelper( - (feature) => this.getElementFromFeature(feature)!.value.image + (feature) => (this.getElementFromFeature(feature) as Material).image ); private readonly nameStyleHelper = new NameStyleHelper( (feature) => { - const material = this.getElementFromFeature(feature)!.value; + const material = this.getElementFromFeature(feature) as Material; return { name: material.vehicleName, offsetY: material.image.height / 2 / normalZoom, diff --git a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/moveable-feature-manager.ts b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/moveable-feature-manager.ts index 4e8f56ea5..94034d561 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/moveable-feature-manager.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/moveable-feature-manager.ts @@ -1,4 +1,3 @@ -import type { ExerciseState } from 'digital-fuesim-manv-shared'; import type { MapBrowserEvent, Feature } from 'ol'; import type Point from 'ol/geom/Point'; import type { TranslateEvent } from 'ol/interaction/Translate'; @@ -30,7 +29,6 @@ export abstract class MoveableFeatureManager< extends ElementManager> implements FeatureManager { - abstract override readonly type: keyof ExerciseState; public readonly togglePopup$ = new Subject>(); protected readonly movementAnimator: MovementAnimator; constructor( diff --git a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/patient-feature-manager.ts b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/patient-feature-manager.ts index 69acb7ba9..c2a3e70be 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/patient-feature-manager.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/patient-feature-manager.ts @@ -22,11 +22,10 @@ import { MoveableFeatureManager } from './moveable-feature-manager'; export class PatientFeatureManager extends MoveableFeatureManager< WithPosition > { - readonly type = 'patients'; private readonly popupHelper = new ImagePopupHelper(this.olMap, this.layer); private readonly imageStyleHelper = new ImageStyleHelper((feature) => { - const patient = this.getElementFromFeature(feature)!.value; + const patient = this.getElementFromFeature(feature) as Patient; return { ...patient.image, rotation: patient.pretriageInformation.isWalkable @@ -37,7 +36,7 @@ export class PatientFeatureManager extends MoveableFeatureManager< private readonly circleStyleHelper = new CircleStyleHelper( (feature) => { - const patient = this.getElementFromFeature(feature)!.value; + const patient = this.getElementFromFeature(feature) as Patient; const configuration = selectStateSnapshot( selectConfiguration, this.store @@ -60,8 +59,8 @@ export class PatientFeatureManager extends MoveableFeatureManager< }, 0.025, (feature) => - this.getElementFromFeature(feature)!.value.pretriageInformation - .isWalkable + (this.getElementFromFeature(feature) as Patient) + .pretriageInformation.isWalkable ? [0, 0.25] : [-0.25, 0] ); diff --git a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/personnel-feature-manager.ts b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/personnel-feature-manager.ts index 183443f19..98f6a144a 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/personnel-feature-manager.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/personnel-feature-manager.ts @@ -17,13 +17,12 @@ import { MoveableFeatureManager } from './moveable-feature-manager'; export class PersonnelFeatureManager extends MoveableFeatureManager< WithPosition > { - readonly type = 'personnel'; private readonly imageStyleHelper = new ImageStyleHelper( - (feature) => this.getElementFromFeature(feature)!.value.image + (feature) => (this.getElementFromFeature(feature) as Personnel).image ); private readonly nameStyleHelper = new NameStyleHelper( (feature) => { - const personnel = this.getElementFromFeature(feature)!.value; + const personnel = this.getElementFromFeature(feature) as Personnel; return { name: personnel.vehicleName, offsetY: personnel.image.height / 2 / normalZoom, diff --git a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/simulated-region-feature-manager.ts b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/simulated-region-feature-manager.ts index f4cd2894c..58c820119 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/simulated-region-feature-manager.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/simulated-region-feature-manager.ts @@ -24,8 +24,6 @@ export class SimulatedRegionFeatureManager extends MoveableFeatureManager implements FeatureManager { - readonly type = 'simulatedRegions'; - override unsupportedChangeProperties = new Set(['id'] as const); constructor( @@ -64,8 +62,9 @@ export class SimulatedRegionFeatureManager ResizeRectangleInteraction.onResize( feature, ({ topLeftCoordinate, scale }) => { - const currentElement = this.getElementFromFeature(feature)! - .value as SimulatedRegion; + const currentElement = this.getElementFromFeature( + feature + ) as SimulatedRegion; this.exerciseService.proposeAction( { type: '[SimulatedRegion] Resize simulated region', diff --git a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/transfer-lines-feature-manager.ts b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/transfer-lines-feature-manager.ts index 97aa951f2..be10f0b39 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/transfer-lines-feature-manager.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/transfer-lines-feature-manager.ts @@ -18,7 +18,6 @@ export class TransferLinesFeatureManager > implements FeatureManager { - readonly type = 'transferLines'; readonly unsupportedChangeProperties = new Set(['id'] as const); constructor(public readonly layer: VectorLayer>) { diff --git a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/transfer-point-feature-manager.ts b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/transfer-point-feature-manager.ts index 1dfcda572..8012a443c 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/transfer-point-feature-manager.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/transfer-point-feature-manager.ts @@ -20,7 +20,6 @@ import { NameStyleHelper } from '../utility/style-helper/name-style-helper'; import { MoveableFeatureManager } from './moveable-feature-manager'; export class TransferPointFeatureManager extends MoveableFeatureManager { - readonly type = 'transferPoints'; private readonly popupHelper = new ImagePopupHelper(this.olMap, this.layer); constructor( @@ -62,7 +61,8 @@ export class TransferPointFeatureManager extends MoveableFeatureManager ({ - name: this.getElementFromFeature(feature)!.value.internalName, + name: (this.getElementFromFeature(feature) as TransferPoint) + .internalName, offsetY: 0, }), 0.2, @@ -76,14 +76,15 @@ export class TransferPointFeatureManager extends MoveableFeatureManager fix getElementFromFeature typings const droppedElement = this.getElementFromFeature(droppedFeature); - const droppedOnTransferPoint: TransferPoint = - this.getElementFromFeature(droppedOnFeature)!.value!; + const droppedOnTransferPoint = this.getElementFromFeature( + droppedOnFeature + ) as TransferPoint; if (!droppedElement || !droppedOnTransferPoint) { console.error('Could not find element for the features'); return false; } if ( - droppedElement.type !== 'vehicles' && + droppedElement.type !== 'vehicle' && droppedElement.type !== 'personnel' ) { return false; @@ -107,7 +108,7 @@ export class TransferPointFeatureManager extends MoveableFeatureManager > { - readonly type = 'vehicles'; - private readonly imageStyleHelper = new ImageStyleHelper( - (feature) => this.getElementFromFeature(feature)!.value.image + (feature) => (this.getElementFromFeature(feature) as Vehicle).image ); private readonly nameStyleHelper = new NameStyleHelper( (feature) => { - const vehicle = this.getElementFromFeature(feature)!.value; + const vehicle = this.getElementFromFeature(feature) as Vehicle; return { name: vehicle.name, offsetY: vehicle.image.height / 2 / normalZoom, @@ -67,29 +65,26 @@ export class VehicleFeatureManager extends MoveableFeatureManager< const droppedElement = this.getElementFromFeature(droppedFeature); const droppedOnVehicle = this.getElementFromFeature( droppedOnFeature - ) as { - type: 'vehicles'; - value: Vehicle; - }; + ) as Vehicle; if (!droppedElement || !droppedOnVehicle) { console.error('Could not find element for the features'); return false; } if ( (droppedElement.type === 'personnel' && - droppedOnVehicle.value.personnelIds[droppedElement.value.id]) || - (droppedElement.type === 'materials' && - droppedOnVehicle.value.materialIds[droppedElement.value.id]) || - (droppedElement.type === 'patients' && - Object.keys(droppedOnVehicle.value.patientIds).length < - droppedOnVehicle.value.patientCapacity) + droppedOnVehicle.personnelIds[droppedElement.id]) || + (droppedElement.type === 'material' && + droppedOnVehicle.materialIds[droppedElement.id]) || + (droppedElement.type === 'patient' && + Object.keys(droppedOnVehicle.patientIds).length < + droppedOnVehicle.patientCapacity) ) { // TODO: user feedback (e.g. toast) this.exerciseService.proposeAction( { type: '[Vehicle] Load vehicle', - vehicleId: droppedOnVehicle.value.id, - elementToBeLoadedId: droppedElement.value.id, + vehicleId: droppedOnVehicle.id, + elementToBeLoadedId: droppedElement.id, elementToBeLoadedType: droppedElement.type, }, true diff --git a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/viewport-feature-manager.ts b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/viewport-feature-manager.ts index 5cc97963b..5f2ceef63 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/viewport-feature-manager.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/viewport-feature-manager.ts @@ -34,8 +34,6 @@ export class ViewportFeatureManager extends MoveableFeatureManager implements FeatureManager { - readonly type = 'viewports'; - override unsupportedChangeProperties = new Set(['id'] as const); constructor( @@ -72,8 +70,9 @@ export class ViewportFeatureManager ResizeRectangleInteraction.onResize( feature, ({ topLeftCoordinate, scale }) => { - const currentElement = this.getElementFromFeature(feature)! - .value as Viewport; + const currentElement = this.getElementFromFeature( + feature + ) as Viewport; this.exerciseService.proposeAction( { type: '[Viewport] Resize viewport', diff --git a/frontend/src/app/pages/exercises/exercise/shared/transfer-overview/transfer-overview-table/transfer-overview-table.component.html b/frontend/src/app/pages/exercises/exercise/shared/transfer-overview/transfer-overview-table/transfer-overview-table.component.html index b17308b0d..4d592c856 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/transfer-overview/transfer-overview-table/transfer-overview-table.component.html +++ b/frontend/src/app/pages/exercises/exercise/shared/transfer-overview/transfer-overview-table/transfer-overview-table.component.html @@ -42,14 +42,14 @@ diff --git a/frontend/src/app/pages/exercises/exercise/shared/transfer-overview/transfer-target-input/transfer-target-input.component.ts b/frontend/src/app/pages/exercises/exercise/shared/transfer-overview/transfer-target-input/transfer-target-input.component.ts index 2bd0ab98f..dd84fa34e 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/transfer-overview/transfer-target-input/transfer-target-input.component.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/transfer-overview/transfer-target-input/transfer-target-input.component.ts @@ -11,7 +11,7 @@ import { selectTransferPoints } from 'src/app/state/application/selectors/exerci styleUrls: ['./transfer-target-input.component.scss'], }) export class TransferTargetInputComponent { - @Input() elementType!: 'personnel' | 'vehicles'; + @Input() elementType!: 'personnel' | 'vehicle'; @Input() elementId!: UUID; @Input() transfer!: Transfer; diff --git a/frontend/src/app/pages/exercises/exercise/shared/transfer-overview/transfer-time-input/transfer-time-input.component.ts b/frontend/src/app/pages/exercises/exercise/shared/transfer-overview/transfer-time-input/transfer-time-input.component.ts index 011a25963..f01016989 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/transfer-overview/transfer-time-input/transfer-time-input.component.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/transfer-overview/transfer-time-input/transfer-time-input.component.ts @@ -11,7 +11,7 @@ import { selectCurrentTime } from 'src/app/state/application/selectors/exercise. styleUrls: ['./transfer-time-input.component.scss'], }) export class TransferTimeInputComponent { - @Input() elementType!: 'personnel' | 'vehicles'; + @Input() elementType!: 'personnel' | 'vehicle'; @Input() elementId!: UUID; diff --git a/shared/src/models/alarm-group.ts b/shared/src/models/alarm-group.ts index 3d4d58d72..2d6ed8dc1 100644 --- a/shared/src/models/alarm-group.ts +++ b/shared/src/models/alarm-group.ts @@ -1,5 +1,6 @@ import { IsString, IsUUID } from 'class-validator'; import { UUID, uuid, uuidValidationOptions } from '../utils'; +import { IsValue } from '../utils/validators'; import { IsIdMap } from '../utils/validators/is-id-map'; import { getCreate } from './utils'; import { AlarmGroupVehicle } from './utils/alarm-group-vehicle'; @@ -8,6 +9,9 @@ export class AlarmGroup { @IsUUID(4, uuidValidationOptions) public readonly id: UUID = uuid(); + @IsValue('alarmGroup' as const) + public readonly type = 'alarmGroup'; + @IsString() public readonly name: string; diff --git a/shared/src/models/client.ts b/shared/src/models/client.ts index aac8ec2eb..ae42a3546 100644 --- a/shared/src/models/client.ts +++ b/shared/src/models/client.ts @@ -6,7 +6,7 @@ import { MaxLength, } from 'class-validator'; import { UUID, uuid, uuidValidationOptions } from '../utils'; -import { IsLiteralUnion } from '../utils/validators'; +import { IsLiteralUnion, IsValue } from '../utils/validators'; import { getCreate, Role } from './utils'; import { roleAllowedValues } from './utils/role'; @@ -14,6 +14,9 @@ export class Client { @IsUUID(4, uuidValidationOptions) public readonly id: UUID = uuid(); + @IsValue('client' as const) + public readonly type = 'client'; + @IsString() // Required by database @MaxLength(255) diff --git a/shared/src/models/element.ts b/shared/src/models/element.ts new file mode 100644 index 000000000..9aa445366 --- /dev/null +++ b/shared/src/models/element.ts @@ -0,0 +1,26 @@ +import type { + AlarmGroup, + Client, + Hospital, + MapImage, + Material, + Patient, + Personnel, + SimulatedRegion, + TransferPoint, + Vehicle, + Viewport, +} from '.'; + +export type Element = + | AlarmGroup + | Client + | Hospital + | MapImage + | Material + | Patient + | Personnel + | SimulatedRegion + | TransferPoint + | Vehicle + | Viewport; diff --git a/shared/src/models/eoc-log-entry.ts b/shared/src/models/eoc-log-entry.ts index f6b7c0113..ed83508e9 100644 --- a/shared/src/models/eoc-log-entry.ts +++ b/shared/src/models/eoc-log-entry.ts @@ -1,11 +1,15 @@ import { IsInt, IsString, IsUUID, MaxLength } from 'class-validator'; import { UUID, uuid, uuidValidationOptions } from '../utils'; +import { IsValue } from '../utils/validators'; import { getCreate } from './utils'; export class EocLogEntry { @IsUUID(4, uuidValidationOptions) public readonly id: UUID = uuid(); + @IsValue('eocLogEntry' as const) + public readonly type = 'eocLogEntry'; + /** * The time in the exercise */ diff --git a/shared/src/models/exercise-configuration.ts b/shared/src/models/exercise-configuration.ts index 9899cdc3d..dce59e0a9 100644 --- a/shared/src/models/exercise-configuration.ts +++ b/shared/src/models/exercise-configuration.ts @@ -1,9 +1,13 @@ import { Type } from 'class-transformer'; import { IsBoolean, ValidateNested } from 'class-validator'; import { defaultTileMapProperties } from '../data'; +import { IsValue } from '../utils/validators'; import { getCreate, TileMapProperties } from './utils'; export class ExerciseConfiguration { + @IsValue('exerciseConfiguration' as const) + public readonly type = 'exerciseConfiguration'; + @IsBoolean() public readonly pretriageEnabled: boolean = true; @IsBoolean() diff --git a/shared/src/models/hospital-patient.ts b/shared/src/models/hospital-patient.ts index b9aa4655f..17a2588ac 100644 --- a/shared/src/models/hospital-patient.ts +++ b/shared/src/models/hospital-patient.ts @@ -8,7 +8,7 @@ import { } from 'class-validator'; import type { Mutable } from '../utils'; import { cloneDeepMutable, UUID, uuidValidationOptions } from '../utils'; -import { IsIdMap, IsLiteralUnion } from '../utils/validators'; +import { IsIdMap, IsLiteralUnion, IsValue } from '../utils/validators'; import { getCreate, HealthPoints, @@ -29,6 +29,9 @@ export class HospitalPatient { @IsUUID(4, uuidValidationOptions) public readonly patientId: UUID; + @IsValue('hospitalPatient' as const) + public readonly type = 'hospitalPatient'; + /** * the vehicle that a patient was transported with */ diff --git a/shared/src/models/hospital.ts b/shared/src/models/hospital.ts index 80797693a..528e35c83 100644 --- a/shared/src/models/hospital.ts +++ b/shared/src/models/hospital.ts @@ -1,12 +1,15 @@ import { IsNumber, IsString, IsUUID, Min } from 'class-validator'; import { uuid, uuidValidationOptions, UUID, UUIDSet } from '../utils'; -import { IsUUIDSet } from '../utils/validators'; +import { IsUUIDSet, IsValue } from '../utils/validators'; import { getCreate } from './utils'; export class Hospital { @IsUUID(4, uuidValidationOptions) public readonly id: UUID = uuid(); + @IsValue('hospital' as const) + public readonly type = 'hospital'; + @IsString() public readonly name: string; diff --git a/shared/src/models/map-image-template.ts b/shared/src/models/map-image-template.ts index c701df501..9a2f4ab90 100644 --- a/shared/src/models/map-image-template.ts +++ b/shared/src/models/map-image-template.ts @@ -1,12 +1,16 @@ import { Type } from 'class-transformer'; import { IsString, IsUUID, ValidateNested } from 'class-validator'; import { UUID, uuid, uuidValidationOptions } from '../utils'; +import { IsValue } from '../utils/validators'; import { getCreate, ImageProperties } from './utils'; export class MapImageTemplate { @IsUUID(4, uuidValidationOptions) public readonly id: UUID = uuid(); + @IsValue('mapImageTemplate' as const) + public readonly type = 'mapImageTemplate'; + @IsString() public readonly name: string; diff --git a/shared/src/models/map-image.ts b/shared/src/models/map-image.ts index 6dc0c3e46..ad15022a7 100644 --- a/shared/src/models/map-image.ts +++ b/shared/src/models/map-image.ts @@ -1,12 +1,16 @@ import { Type } from 'class-transformer'; import { IsBoolean, IsInt, IsUUID, ValidateNested } from 'class-validator'; import { uuid, UUID, uuidValidationOptions } from '../utils'; +import { IsValue } from '../utils/validators'; import { Position, getCreate, ImageProperties } from './utils'; export class MapImage { @IsUUID(4, uuidValidationOptions) public readonly id: UUID = uuid(); + @IsValue('mapImage' as const) + public readonly type = 'mapImage'; + @ValidateNested() @Type(() => Position) public readonly position: Position; diff --git a/shared/src/models/material-template.ts b/shared/src/models/material-template.ts index 95f6ba6ec..39b45da3e 100644 --- a/shared/src/models/material-template.ts +++ b/shared/src/models/material-template.ts @@ -1,11 +1,14 @@ import { Type } from 'class-transformer'; import { IsNumber, Max, Min, ValidateNested } from 'class-validator'; import { maxTreatmentRange } from '../state-helpers/max-treatment-range'; -import { IsLiteralUnion } from '../utils/validators'; +import { IsLiteralUnion, IsValue } from '../utils/validators'; import { CanCaterFor, getCreate, ImageProperties } from './utils'; import { MaterialType, materialTypeAllowedValues } from './utils/material-type'; export class MaterialTemplate { + @IsValue('materialTemplate' as const) + public readonly type = 'materialTemplate'; + @IsLiteralUnion(materialTypeAllowedValues) public readonly materialType: MaterialType; diff --git a/shared/src/models/material.ts b/shared/src/models/material.ts index 10dc9e2fa..4fea13d4d 100644 --- a/shared/src/models/material.ts +++ b/shared/src/models/material.ts @@ -10,7 +10,7 @@ import { } from 'class-validator'; import { maxTreatmentRange } from '../state-helpers/max-treatment-range'; import { uuidValidationOptions, UUID, uuid, UUIDSet } from '../utils'; -import { IsUUIDSet } from '../utils/validators'; +import { IsUUIDSet, IsValue } from '../utils/validators'; import { IsMetaPosition } from '../utils/validators/is-metaposition'; import type { MaterialTemplate } from './material-template'; import { CanCaterFor, Position, ImageProperties, getCreate } from './utils'; @@ -20,6 +20,9 @@ export class Material { @IsUUID(4, uuidValidationOptions) public readonly id: UUID = uuid(); + @IsValue('material' as const) + public readonly type = 'material'; + @IsUUID(4, uuidValidationOptions) public readonly vehicleId: UUID; diff --git a/shared/src/models/patient-category.ts b/shared/src/models/patient-category.ts index 6b9d8b683..d6161453c 100644 --- a/shared/src/models/patient-category.ts +++ b/shared/src/models/patient-category.ts @@ -1,10 +1,14 @@ import { Type } from 'class-transformer'; import { ArrayNotEmpty, IsArray, ValidateNested } from 'class-validator'; +import { IsValue } from '../utils/validators'; import { PatientTemplate } from './patient-template'; import { getCreate, ImageProperties } from './utils'; import { PatientStatusCode } from './utils/patient-status-code'; export class PatientCategory { + @IsValue('patientCategory' as const) + public readonly type = 'patientCategory'; + @ValidateNested() @Type(() => PatientStatusCode) public readonly name: PatientStatusCode; diff --git a/shared/src/models/patient-health-state.ts b/shared/src/models/patient-health-state.ts index 60810920e..9e04c85ea 100644 --- a/shared/src/models/patient-health-state.ts +++ b/shared/src/models/patient-health-state.ts @@ -7,6 +7,7 @@ import { ValidateNested, } from 'class-validator'; import { uuid, UUID, uuidValidationOptions } from '../utils'; +import { IsValue } from '../utils/validators'; import { getCreate, HealthPoints, IsValidHealthPoint } from './utils'; /** @@ -109,6 +110,9 @@ export class PatientHealthState { @IsUUID(4, uuidValidationOptions) public readonly id: UUID = uuid(); + @IsValue('patientHealthState' as const) + public readonly type = 'patientHealthState'; + @Type(() => FunctionParameters) @ValidateNested() public readonly functionParameters: FunctionParameters; diff --git a/shared/src/models/patient-template.ts b/shared/src/models/patient-template.ts index ef8841390..82a4905c4 100644 --- a/shared/src/models/patient-template.ts +++ b/shared/src/models/patient-template.ts @@ -1,7 +1,7 @@ import { Type } from 'class-transformer'; import { IsUUID, ValidateNested } from 'class-validator'; import { cloneDeepMutable, UUID, uuid, uuidValidationOptions } from '../utils'; -import { IsIdMap } from '../utils/validators'; +import { IsIdMap, IsValue } from '../utils/validators'; import type { PatientStatusCode } from './utils'; import { BiometricInformation, @@ -22,6 +22,9 @@ export class PatientTemplate { @IsUUID(4, uuidValidationOptions) public readonly id: UUID = uuid(); + @IsValue('patientTemplate' as const) + public readonly type = 'patientTemplate'; + @ValidateNested() @Type(() => BiometricInformation) public readonly biometricInformation: BiometricInformation; diff --git a/shared/src/models/patient.ts b/shared/src/models/patient.ts index 4942509ed..6c5cecee4 100644 --- a/shared/src/models/patient.ts +++ b/shared/src/models/patient.ts @@ -12,7 +12,12 @@ import { } from 'class-validator'; import { isEmpty } from 'lodash-es'; import { uuidValidationOptions, UUID, uuid, UUIDSet } from '../utils'; -import { IsLiteralUnion, IsIdMap, IsUUIDSet } from '../utils/validators'; +import { + IsLiteralUnion, + IsIdMap, + IsUUIDSet, + IsValue, +} from '../utils/validators'; import { IsMetaPosition } from '../utils/validators/is-metaposition'; import { PatientHealthState } from './patient-health-state'; import { @@ -34,6 +39,9 @@ export class Patient { @IsUUID(4, uuidValidationOptions) public readonly id: UUID = uuid(); + @IsValue('patient') + public readonly type = 'patient'; + @ValidateNested() @Type(() => PersonalInformation) public readonly personalInformation: PersonalInformation; diff --git a/shared/src/models/personnel-template.ts b/shared/src/models/personnel-template.ts index ccc06782a..fc3e3c101 100644 --- a/shared/src/models/personnel-template.ts +++ b/shared/src/models/personnel-template.ts @@ -1,7 +1,7 @@ import { Type } from 'class-transformer'; import { IsNumber, Max, Min, ValidateNested } from 'class-validator'; import { maxTreatmentRange } from '../state-helpers/max-treatment-range'; -import { IsLiteralUnion } from '../utils/validators'; +import { IsLiteralUnion, IsValue } from '../utils/validators'; import { PersonnelType, CanCaterFor, @@ -12,6 +12,9 @@ import { personnelTypeAllowedValues } from './utils/personnel-type'; // TODO: These are not (yet) saved in the state -> Decide whether they should and if not move this file from the models folder away export class PersonnelTemplate { + @IsValue('personnelTemplate' as const) + public readonly type = 'personnelTemplate'; + @IsLiteralUnion(personnelTypeAllowedValues) public readonly personnelType: PersonnelType; diff --git a/shared/src/models/personnel.ts b/shared/src/models/personnel.ts index 9ed6b4f79..16ebc287c 100644 --- a/shared/src/models/personnel.ts +++ b/shared/src/models/personnel.ts @@ -10,7 +10,7 @@ import { } from 'class-validator'; import { maxTreatmentRange } from '../state-helpers/max-treatment-range'; import { uuidValidationOptions, UUID, uuid, UUIDSet } from '../utils'; -import { IsLiteralUnion, IsUUIDSet } from '../utils/validators'; +import { IsLiteralUnion, IsUUIDSet, IsValue } from '../utils/validators'; import { IsMetaPosition } from '../utils/validators/is-metaposition'; import type { PersonnelTemplate } from './personnel-template'; import { @@ -28,6 +28,9 @@ export class Personnel { @IsUUID(4, uuidValidationOptions) public readonly id: UUID = uuid(); + @IsValue('personnel' as const) + public readonly type = 'personnel'; + @IsUUID(4, uuidValidationOptions) public readonly vehicleId: UUID; diff --git a/shared/src/models/simulated-region.ts b/shared/src/models/simulated-region.ts index 6e3db79a4..8d4f47cc4 100644 --- a/shared/src/models/simulated-region.ts +++ b/shared/src/models/simulated-region.ts @@ -1,6 +1,7 @@ import { Type } from 'class-transformer'; import { IsString, IsUUID, ValidateNested } from 'class-validator'; import { UUID, uuid, uuidValidationOptions } from '../utils'; +import { IsValue } from '../utils/validators'; import { getCreate, Position, Size } from './utils'; import type { ImageProperties } from './utils'; @@ -8,6 +9,9 @@ export class SimulatedRegion { @IsUUID(4, uuidValidationOptions) public readonly id: UUID = uuid(); + @IsValue('simulatedRegion' as const) + public readonly type = 'simulatedRegion'; + /** * top-left position */ diff --git a/shared/src/models/transfer-point.ts b/shared/src/models/transfer-point.ts index b825b36be..5aa895ca5 100644 --- a/shared/src/models/transfer-point.ts +++ b/shared/src/models/transfer-point.ts @@ -1,7 +1,11 @@ import { Type } from 'class-transformer'; import { IsString, IsUUID, ValidateNested } from 'class-validator'; import { UUID, uuid, UUIDSet, uuidValidationOptions } from '../utils'; -import { IsReachableTransferPoints, IsUUIDSet } from '../utils/validators'; +import { + IsReachableTransferPoints, + IsUUIDSet, + IsValue, +} from '../utils/validators'; import type { ImageProperties } from './utils'; import { getCreate, Position } from './utils'; @@ -9,6 +13,9 @@ export class TransferPoint { @IsUUID(4, uuidValidationOptions) public readonly id: UUID = uuid(); + @IsValue('transferPoint' as const) + public readonly type = 'transferPoint'; + @ValidateNested() @Type(() => Position) public readonly position: Position; diff --git a/shared/src/models/vehicle-template.ts b/shared/src/models/vehicle-template.ts index f0dd3157b..c2956dd40 100644 --- a/shared/src/models/vehicle-template.ts +++ b/shared/src/models/vehicle-template.ts @@ -7,7 +7,7 @@ import { IsArray, } from 'class-validator'; import { uuidValidationOptions, UUID, uuid } from '../utils'; -import { IsLiteralUnion } from '../utils/validators'; +import { IsLiteralUnion, IsValue } from '../utils/validators'; import type { PersonnelType } from './utils'; import { ImageProperties, @@ -21,6 +21,9 @@ export class VehicleTemplate { @IsUUID(4, uuidValidationOptions) public readonly id: UUID = uuid(); + @IsValue('vehicleTemplate' as const) + public readonly type = 'vehicleTemplate'; + @IsString() public readonly vehicleType: string; diff --git a/shared/src/models/vehicle.ts b/shared/src/models/vehicle.ts index 8c18b39da..e053b021a 100644 --- a/shared/src/models/vehicle.ts +++ b/shared/src/models/vehicle.ts @@ -7,7 +7,7 @@ import { ValidateNested, } from 'class-validator'; import { uuid, uuidValidationOptions, UUID, UUIDSet } from '../utils'; -import { IsUUIDSet } from '../utils/validators'; +import { IsUUIDSet, IsValue } from '../utils/validators'; import { IsMetaPosition } from '../utils/validators/is-metaposition'; import { getCreate, Position, Transfer } from './utils'; import { ImageProperties } from './utils/image-properties'; @@ -17,6 +17,9 @@ export class Vehicle { @IsUUID(4, uuidValidationOptions) public readonly id: UUID = uuid(); + @IsValue('vehicle' as const) + public readonly type = 'vehicle'; + @IsString() public readonly vehicleType: string; diff --git a/shared/src/models/viewport.ts b/shared/src/models/viewport.ts index fc31482b7..4a147e472 100644 --- a/shared/src/models/viewport.ts +++ b/shared/src/models/viewport.ts @@ -1,6 +1,7 @@ import { Type } from 'class-transformer'; import { IsString, IsUUID, ValidateNested } from 'class-validator'; import { UUID, uuid, uuidValidationOptions } from '../utils'; +import { IsValue } from '../utils/validators'; import { getCreate, Position, Size } from './utils'; import type { ImageProperties } from './utils'; @@ -8,6 +9,9 @@ export class Viewport { @IsUUID(4, uuidValidationOptions) public readonly id: UUID = uuid(); + @IsValue('viewport' as const) + public readonly type = 'viewport'; + /** * top-left position */ diff --git a/shared/src/state-helpers/create-vehicle-parameters.ts b/shared/src/state-helpers/create-vehicle-parameters.ts index e6f0021a3..bacbb4ef4 100644 --- a/shared/src/state-helpers/create-vehicle-parameters.ts +++ b/shared/src/state-helpers/create-vehicle-parameters.ts @@ -48,6 +48,7 @@ export function createVehicleParameters( const vehicle: Vehicle = { id: vehicleId, + type: 'vehicle', materialIds: arrayToUUIDSet(materials.map((m) => m.id)), vehicleType: vehicleTemplate.vehicleType, name: vehicleTemplate.name, diff --git a/shared/src/state-migrations/17-add-type-property.ts b/shared/src/state-migrations/17-add-type-property.ts new file mode 100644 index 000000000..afd36843f --- /dev/null +++ b/shared/src/state-migrations/17-add-type-property.ts @@ -0,0 +1,329 @@ +import type { UUID } from '../utils'; +import type { Migration } from './migration-functions'; + +export const addTypeProperty17: Migration = { + actions: (_initialState, actions) => { + actions.forEach((action) => { + const actionType = (action as { type: string } | null)?.type; + + if (actionType === '[AlarmGroup] Add AlarmGroup') { + const typedAction = action as { + alarmGroup: { + type: 'alarmGroup'; + }; + }; + + typedAction.alarmGroup.type = 'alarmGroup'; + } + + if (actionType === '[Client] Add client') { + const typedAction = action as { + client: { + type: 'client'; + }; + }; + + typedAction.client.type = 'client'; + } + + if (actionType === '[Hospital] Add hospital') { + const typedAction = action as { + hospital: { + type: 'hospital'; + }; + }; + + typedAction.hospital.type = 'hospital'; + } + + if (actionType === '[MapImageTemplate] Add mapImageTemplate') { + const typedAction = action as { + mapImageTemplate: { + type: 'mapImageTemplate'; + }; + }; + + typedAction.mapImageTemplate.type = 'mapImageTemplate'; + } + + if (actionType === '[MapImage] Add MapImage') { + const typedAction = action as { + mapImage: { + type: 'mapImage'; + }; + }; + + typedAction.mapImage.type = 'mapImage'; + } + + if (actionType === '[Patient] Add patient') { + const typedAction = action as { + patient: { + type: 'patient'; + healthStates: { + [key: UUID]: { type: 'patientHealthState' }; + }; + }; + }; + + typedAction.patient.type = 'patient'; + Object.values(typedAction.patient.healthStates).forEach( + (healthState) => { + healthState.type = 'patientHealthState'; + } + ); + } + + if (actionType === '[SimulatedRegion] Add simulated region') { + const typedAction = action as { + simulatedRegion: { + type: 'simulatedRegion'; + }; + }; + + typedAction.simulatedRegion.type = 'simulatedRegion'; + } + + if ( + actionType === '[Transfer] Add to transfer' || + actionType === '[Transfer] Edit transfer' || + actionType === '[Transfer] Finish transfer' || + actionType === '[Transfer] Toggle pause transfer' + ) { + const typedAction = action as { + elementType: 'personnel' | 'vehicle' | 'vehicles'; + }; + + if (typedAction.elementType === 'vehicles') { + typedAction.elementType = 'vehicle'; + } + } + + if (actionType === '[TransferPoint] Add TransferPoint') { + const typedAction = action as { + transferPoint: { + type: 'transferPoint'; + }; + }; + + typedAction.transferPoint.type = 'transferPoint'; + } + + if (actionType === '[Vehicle] Add vehicle') { + const typedAction = action as { + vehicle: { + type: 'vehicle'; + }; + materials: { type: 'material' }[]; + personnel: { type: 'personnel' }[]; + }; + + typedAction.vehicle.type = 'vehicle'; + typedAction.materials.forEach((material) => { + material.type = 'material'; + }); + typedAction.personnel.forEach((personnel) => { + personnel.type = 'personnel'; + }); + } + + if (actionType === '[Vehicle] Load vehicle') { + const typedAction = action as { + elementToBeLoadedType: + | 'material' + | 'materials' + | 'patient' + | 'patients' + | 'personnel'; + }; + + if (typedAction.elementToBeLoadedType === 'materials') { + typedAction.elementToBeLoadedType = 'material'; + } else if (typedAction.elementToBeLoadedType === 'patients') { + typedAction.elementToBeLoadedType = 'patient'; + } + } + + if (actionType === '[Viewport] Add viewport') { + const typedAction = action as { + viewport: { + type: 'viewport'; + }; + }; + + typedAction.viewport.type = 'viewport'; + } + }); + }, + state: (state) => { + const typedState = state as { + alarmGroups: { + [key: UUID]: { + type: 'alarmGroup'; + }; + }; + clients: { + [key: UUID]: { + type: 'client'; + }; + }; + eocLog: { type: 'eocLogEntry' }[]; + configuration: { type: 'exerciseConfiguration' }; + hospitalPatients: { + [key: UUID]: { + type: 'hospitalPatient'; + }; + }; + hospitals: { + [key: UUID]: { + type: 'hospital'; + }; + }; + mapImageTemplates: { type: 'mapImageTemplate' }[]; + mapImages: { + [key: UUID]: { + type: 'mapImage'; + }; + }; + materialTemplates: { type: 'materialTemplate' }[]; + materials: { + [key: UUID]: { + type: 'material'; + }; + }; + patientCategories: { + type: 'patientCategory'; + patientTemplates: { type: 'patientTemplate' }[]; + }[]; + patients: { + [key: UUID]: { + type: 'patient'; + healthStates: { + [key: UUID]: { type: 'patientHealthState' }; + }; + }; + }; + personnelTemplates: { type: 'personnelTemplate' }[]; + personnel: { + [key: UUID]: { + type: 'personnel'; + }; + }; + simulatedRegions: { + [key: UUID]: { + type: 'simulatedRegion'; + }; + }; + transferPoints: { + [key: UUID]: { + type: 'transferPoint'; + }; + }; + vehicleTemplates: { type: 'vehicleTemplate' }[]; + vehicles: { + [key: UUID]: { + type: 'vehicle'; + }; + }; + viewports: { + [key: UUID]: { + type: 'viewport'; + }; + }; + }; + + Object.values(typedState.alarmGroups).forEach((alarmGroup) => { + alarmGroup.type = 'alarmGroup'; + }); + + Object.values(typedState.clients).forEach((client) => { + client.type = 'client'; + }); + + Object.values(typedState.eocLog).forEach((logEntry) => { + logEntry.type = 'eocLogEntry'; + }); + + typedState.configuration.type = 'exerciseConfiguration'; + + Object.values(typedState.hospitalPatients).forEach( + (hospitalPatient) => { + hospitalPatient.type = 'hospitalPatient'; + } + ); + + Object.values(typedState.hospitals).forEach((hospital) => { + hospital.type = 'hospital'; + }); + + Object.values(typedState.mapImageTemplates).forEach( + (mapImageTemplate) => { + mapImageTemplate.type = 'mapImageTemplate'; + } + ); + + Object.values(typedState.mapImages).forEach((mapImage) => { + mapImage.type = 'mapImage'; + }); + + Object.values(typedState.materialTemplates).forEach( + (materialTemplate) => { + materialTemplate.type = 'materialTemplate'; + } + ); + + Object.values(typedState.materials).forEach((material) => { + material.type = 'material'; + }); + + Object.values(typedState.patientCategories).forEach( + (patientCategory) => { + patientCategory.type = 'patientCategory'; + patientCategory.patientTemplates.forEach((patientTemplate) => { + patientTemplate.type = 'patientTemplate'; + }); + } + ); + + Object.values(typedState.patients).forEach((patient) => { + patient.type = 'patient'; + Object.values(patient.healthStates).forEach((healthState) => { + healthState.type = 'patientHealthState'; + }); + }); + + Object.values(typedState.personnelTemplates).forEach( + (personnelTemplates) => { + personnelTemplates.type = 'personnelTemplate'; + } + ); + + Object.values(typedState.personnel).forEach((personnel) => { + personnel.type = 'personnel'; + }); + + Object.values(typedState.simulatedRegions).forEach( + (simulatedRegion) => { + simulatedRegion.type = 'simulatedRegion'; + } + ); + + Object.values(typedState.transferPoints).forEach((transferPoint) => { + transferPoint.type = 'transferPoint'; + }); + + Object.values(typedState.vehicleTemplates).forEach( + (vehicleTemplate) => { + vehicleTemplate.type = 'vehicleTemplate'; + } + ); + + Object.values(typedState.vehicles).forEach((vehicle) => { + vehicle.type = 'vehicle'; + }); + + Object.values(typedState.viewports).forEach((viewport) => { + viewport.type = 'viewport'; + }); + }, +}; diff --git a/shared/src/state-migrations/migration-functions.ts b/shared/src/state-migrations/migration-functions.ts index 8bbed2f7b..965436bb5 100644 --- a/shared/src/state-migrations/migration-functions.ts +++ b/shared/src/state-migrations/migration-functions.ts @@ -5,6 +5,7 @@ import { addMapImageZIndex13 } from './13-add-map-image-zindex'; import { addPersonnelAndMaterialToState14 } from './14-add-personnel-and-material-templates-to-state'; import { addSimulatedRegions15 } from './15-add-simulated-regions'; import { addMetaPosition16 } from './16-add-meta-position'; +import { addTypeProperty17 } from './17-add-type-property'; import { updateEocLog3 } from './3-update-eoc-log'; import { removeSetParticipantIdAction4 } from './4-remove-set-participant-id-action'; import { removeStatistics5 } from './5-remove-statistics'; @@ -55,4 +56,5 @@ export const migrations: { 14: addPersonnelAndMaterialToState14, 15: addSimulatedRegions15, 16: addMetaPosition16, + 17: addTypeProperty17, }; diff --git a/shared/src/state.ts b/shared/src/state.ts index 0f05987f2..de96e2b71 100644 --- a/shared/src/state.ts +++ b/shared/src/state.ts @@ -44,7 +44,7 @@ import { SpatialTree, } from './models/utils'; import type { MaterialType } from './models/utils/material-type'; -import type { SpatialElementType } from './store/action-reducers/utils/spatial-elements'; +import type { SpatialElementPlural } from './store/action-reducers/utils/spatial-elements'; import type { UUID } from './utils'; import { uuid, uuidValidationOptions } from './utils'; import { IsIdMap, IsLiteralUnion } from './utils/validators'; @@ -123,7 +123,7 @@ export class ExerciseState { // Mutable` could still have immutable objects in spatialTree @IsObject() public readonly spatialTrees: { - [type in SpatialElementType]: SpatialTree; + [type in SpatialElementPlural]: SpatialTree; } = { materials: SpatialTree.create(), patients: SpatialTree.create(), @@ -148,5 +148,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 = 16; + static readonly currentStateVersion = 17; } diff --git a/shared/src/store/action-reducers/alarm-group.ts b/shared/src/store/action-reducers/alarm-group.ts index 598c30c63..cf4d3cead 100644 --- a/shared/src/store/action-reducers/alarm-group.ts +++ b/shared/src/store/action-reducers/alarm-group.ts @@ -96,7 +96,7 @@ export namespace AlarmGroupActionReducers { reducer: (draftState, { alarmGroupId, name }) => { const alarmGroup = getElement( draftState, - 'alarmGroups', + 'alarmGroup', alarmGroupId ); alarmGroup.name = name; @@ -108,7 +108,7 @@ export namespace AlarmGroupActionReducers { export const removeAlarmGroup: ActionReducer = { action: RemoveAlarmGroupAction, reducer: (draftState, { alarmGroupId }) => { - getElement(draftState, 'alarmGroups', alarmGroupId); + getElement(draftState, 'alarmGroup', alarmGroupId); delete draftState.alarmGroups[alarmGroupId]; return draftState; }, @@ -121,7 +121,7 @@ export namespace AlarmGroupActionReducers { reducer: (draftState, { alarmGroupId, alarmGroupVehicle }) => { const alarmGroup = getElement( draftState, - 'alarmGroups', + 'alarmGroup', alarmGroupId ); alarmGroup.alarmGroupVehicles[alarmGroupVehicle.id] = @@ -140,7 +140,7 @@ export namespace AlarmGroupActionReducers { ) => { const alarmGroup = getElement( draftState, - 'alarmGroups', + 'alarmGroup', alarmGroupId ); const alarmGroupVehicle = getAlarmGroupVehicle( @@ -160,7 +160,7 @@ export namespace AlarmGroupActionReducers { reducer: (draftState, { alarmGroupId, alarmGroupVehicleId }) => { const alarmGroup = getElement( draftState, - 'alarmGroups', + 'alarmGroup', alarmGroupId ); getAlarmGroupVehicle(alarmGroup, alarmGroupVehicleId); diff --git a/shared/src/store/action-reducers/client.ts b/shared/src/store/action-reducers/client.ts index 5c1438f22..55208470e 100644 --- a/shared/src/store/action-reducers/client.ts +++ b/shared/src/store/action-reducers/client.ts @@ -53,7 +53,7 @@ export namespace ClientActionReducers { export const removeClient: ActionReducer = { action: RemoveClientAction, reducer: (draftState, { clientId }) => { - getElement(draftState, 'clients', clientId); + getElement(draftState, 'client', clientId); delete draftState.clients[clientId]; return draftState; }, @@ -64,12 +64,12 @@ export namespace ClientActionReducers { { action: RestrictViewToViewportAction, reducer: (draftState, { clientId, viewportId }) => { - const client = getElement(draftState, 'clients', clientId); + const client = getElement(draftState, 'client', clientId); if (viewportId === undefined) { client.viewRestrictedToViewportId = viewportId; return draftState; } - getElement(draftState, 'viewports', viewportId); + getElement(draftState, 'viewport', viewportId); client.viewRestrictedToViewportId = viewportId; return draftState; }, @@ -79,7 +79,7 @@ export namespace ClientActionReducers { export const setWaitingRoom: ActionReducer = { action: SetWaitingRoomAction, reducer: (draftState, { clientId, shouldBeInWaitingRoom }) => { - const client = getElement(draftState, 'clients', clientId); + const client = getElement(draftState, 'client', clientId); client.isInWaitingRoom = shouldBeInWaitingRoom; return draftState; }, diff --git a/shared/src/store/action-reducers/exercise.ts b/shared/src/store/action-reducers/exercise.ts index 55a1fbba3..1fd29a514 100644 --- a/shared/src/store/action-reducers/exercise.ts +++ b/shared/src/store/action-reducers/exercise.ts @@ -11,9 +11,12 @@ import { Patient } from '../../models'; import { getStatus } from '../../models/utils'; import type { ExerciseState } from '../../state'; import type { Mutable } from '../../utils'; +import type { ElementTypePluralMap } from '../../utils/element-type-plural-map'; +import { elementTypePluralMap } from '../../utils/element-type-plural-map'; import { IsValue } from '../../utils/validators'; import type { Action, ActionReducer } from '../action-reducer'; import { ReducerError } from '../reducer-error'; +import type { TransferableElementType } from './transfer'; import { letElementArrive } from './transfer'; import { updateTreatments } from './utils/calculate-treatments'; import { PatientUpdate } from './utils/patient-updates'; @@ -122,7 +125,7 @@ export namespace ExerciseActionReducers { }); // Refresh transfers - refreshTransfer(draftState, 'vehicles', tickInterval); + refreshTransfer(draftState, 'vehicle', tickInterval); refreshTransfer(draftState, 'personnel', tickInterval); return draftState; }, @@ -130,12 +133,17 @@ export namespace ExerciseActionReducers { }; } +type TransferTypePluralMap = Pick< + ElementTypePluralMap, + TransferableElementType +>; + function refreshTransfer( draftState: Mutable, - key: 'personnel' | 'vehicles', + type: keyof TransferTypePluralMap, tickInterval: number ): void { - const elements = draftState[key]; + const elements = draftState[elementTypePluralMap[type]]; Object.values(elements).forEach((element: Mutable) => { if (!element.transfer) { return; @@ -148,6 +156,6 @@ function refreshTransfer( if (element.transfer.endTimeStamp > draftState.currentTime) { return; } - letElementArrive(draftState, key, element.id); + letElementArrive(draftState, type, element.id); }); } diff --git a/shared/src/store/action-reducers/hospital.ts b/shared/src/store/action-reducers/hospital.ts index 0f2214691..b5bb8e559 100644 --- a/shared/src/store/action-reducers/hospital.ts +++ b/shared/src/store/action-reducers/hospital.ts @@ -74,11 +74,7 @@ export namespace HospitalActionReducers { { action: EditTransportDurationToHospitalAction, reducer: (draftState, { hospitalId, transportDuration }) => { - const hospital = getElement( - draftState, - 'hospitals', - hospitalId - ); + const hospital = getElement(draftState, 'hospital', hospitalId); hospital.transportDuration = transportDuration; return draftState; }, @@ -88,7 +84,7 @@ export namespace HospitalActionReducers { export const renameHospital: ActionReducer = { action: RenameHospitalAction, reducer: (draftState, { hospitalId, name }) => { - const hospital = getElement(draftState, 'hospitals', hospitalId); + const hospital = getElement(draftState, 'hospital', hospitalId); hospital.name = name; return draftState; }, @@ -98,7 +94,7 @@ export namespace HospitalActionReducers { export const removeHospital: ActionReducer = { action: RemoveHospitalAction, reducer: (draftState, { hospitalId }) => { - const hospital = getElement(draftState, 'hospitals', hospitalId); + const hospital = getElement(draftState, 'hospital', hospitalId); // TODO: maybe make a hospital undeletable (if at least one patient is in it) for (const patientId of Object.keys(hospital.patientIds)) { delete draftState.hospitalPatients[patientId]; @@ -118,17 +114,13 @@ export namespace HospitalActionReducers { { action: TransportPatientToHospitalAction, reducer: (draftState, { hospitalId, vehicleId }) => { - const hospital = getElement( - draftState, - 'hospitals', - hospitalId - ); - const vehicle = getElement(draftState, 'vehicles', vehicleId); + const hospital = getElement(draftState, 'hospital', hospitalId); + const vehicle = getElement(draftState, 'vehicle', vehicleId); // TODO: Block vehicles whose material and personnel are unloaded for (const patientId of Object.keys(vehicle.patientIds)) { const patient = getElement( draftState, - 'patients', + 'patient', patientId ); draftState.hospitalPatients[patientId] = diff --git a/shared/src/store/action-reducers/map-images.ts b/shared/src/store/action-reducers/map-images.ts index 26bce6329..7384033ff 100644 --- a/shared/src/store/action-reducers/map-images.ts +++ b/shared/src/store/action-reducers/map-images.ts @@ -133,7 +133,7 @@ export namespace MapImagesActionReducers { export const moveMapImage: ActionReducer = { action: MoveMapImageAction, reducer: (draftState, { mapImageId, targetPosition }) => { - const mapImage = getElement(draftState, 'mapImages', mapImageId); + const mapImage = getElement(draftState, 'mapImage', mapImageId); mapImage.position = cloneDeepMutable(targetPosition); return draftState; }, @@ -143,7 +143,7 @@ export namespace MapImagesActionReducers { export const scaleMapImage: ActionReducer = { action: ScaleMapImageAction, reducer: (draftState, { mapImageId, newHeight, newAspectRatio }) => { - const mapImage = getElement(draftState, 'mapImages', mapImageId); + const mapImage = getElement(draftState, 'mapImage', mapImageId); if (newHeight) { mapImage.image.height = newHeight; } @@ -158,7 +158,7 @@ export namespace MapImagesActionReducers { export const removeMapImage: ActionReducer = { action: RemoveMapImageAction, reducer: (draftState, { mapImageId }) => { - getElement(draftState, 'mapImages', mapImageId); + getElement(draftState, 'mapImage', mapImageId); delete draftState.mapImages[mapImageId]; return draftState; }, @@ -169,11 +169,7 @@ export namespace MapImagesActionReducers { { action: ReconfigureMapImageUrlAction, reducer: (draftState, { mapImageId, newUrl }) => { - const mapImage = getElement( - draftState, - 'mapImages', - mapImageId - ); + const mapImage = getElement(draftState, 'mapImage', mapImageId); mapImage.image.url = newUrl; return draftState; }, @@ -183,7 +179,7 @@ export namespace MapImagesActionReducers { export const setLockedMapImage: ActionReducer = { action: SetIsLockedMapImageAction, reducer: (draftState, { mapImageId, newLocked }) => { - const mapImage = getElement(draftState, 'mapImages', mapImageId); + const mapImage = getElement(draftState, 'mapImage', mapImageId); mapImage.isLocked = newLocked; return draftState; }, @@ -193,7 +189,7 @@ export namespace MapImagesActionReducers { export const changeZIndex: ActionReducer = { action: ChangeZIndexMapImageAction, reducer: (draftState, { mapImageId, mode }) => { - const mapImage = getElement(draftState, 'mapImages', mapImageId); + const mapImage = getElement(draftState, 'mapImage', mapImageId); switch (mode) { case 'bringToFront': case 'bringToBack': { diff --git a/shared/src/store/action-reducers/material.ts b/shared/src/store/action-reducers/material.ts index a38b201ab..4d1847dc4 100644 --- a/shared/src/store/action-reducers/material.ts +++ b/shared/src/store/action-reducers/material.ts @@ -24,7 +24,7 @@ export namespace MaterialActionReducers { reducer: (draftState, { materialId, targetPosition }) => { updateElementPosition( draftState, - 'materials', + 'material', materialId, targetPosition ); diff --git a/shared/src/store/action-reducers/patient.ts b/shared/src/store/action-reducers/patient.ts index d25fca861..b3953393a 100644 --- a/shared/src/store/action-reducers/patient.ts +++ b/shared/src/store/action-reducers/patient.ts @@ -29,7 +29,7 @@ export function deletePatient( draftState: Mutable, patientId: UUID ) { - removeElementPosition(draftState, 'patients', patientId); + removeElementPosition(draftState, 'patient', patientId); delete draftState.patients[patientId]; } @@ -120,7 +120,7 @@ export namespace PatientActionReducers { } const mutablePatient = cloneDeepMutable(patient); draftState.patients[mutablePatient.id] = mutablePatient; - addElementPosition(draftState, 'patients', mutablePatient.id); + addElementPosition(draftState, 'patient', mutablePatient.id); return draftState; }, rights: 'trainer', @@ -131,7 +131,7 @@ export namespace PatientActionReducers { reducer: (draftState, { patientId, targetPosition }) => { updateElementPosition( draftState, - 'patients', + 'patient', patientId, targetPosition ); @@ -152,7 +152,7 @@ export namespace PatientActionReducers { export const setVisibleStatus: ActionReducer = { action: SetVisibleStatusAction, reducer: (draftState, { patientId, patientStatus }) => { - const patient = getElement(draftState, 'patients', patientId); + const patient = getElement(draftState, 'patient', patientId); patient.pretriageStatus = patientStatus; if (patient.metaPosition.type === 'coordinates') { @@ -167,7 +167,7 @@ export namespace PatientActionReducers { export const setUserTextAction: ActionReducer = { action: SetUserTextAction, reducer: (draftState, { patientId, remarks }) => { - const patient = getElement(draftState, 'patients', patientId); + const patient = getElement(draftState, 'patient', patientId); patient.remarks = remarks; return draftState; }, diff --git a/shared/src/store/action-reducers/simulated-region.ts b/shared/src/store/action-reducers/simulated-region.ts index 390e0456e..4de91f2ec 100644 --- a/shared/src/store/action-reducers/simulated-region.ts +++ b/shared/src/store/action-reducers/simulated-region.ts @@ -71,7 +71,7 @@ export namespace SimulatedRegionActionReducers { { action: RemoveSimulatedRegionAction, reducer: (draftState, { simulatedRegionId }) => { - getElement(draftState, 'simulatedRegions', simulatedRegionId); + getElement(draftState, 'simulatedRegion', simulatedRegionId); delete draftState.simulatedRegions[simulatedRegionId]; return draftState; }, @@ -84,7 +84,7 @@ export namespace SimulatedRegionActionReducers { reducer: (draftState, { simulatedRegionId, targetPosition }) => { const simulatedRegion = getElement( draftState, - 'simulatedRegions', + 'simulatedRegion', simulatedRegionId ); simulatedRegion.position = cloneDeepMutable(targetPosition); @@ -102,7 +102,7 @@ export namespace SimulatedRegionActionReducers { ) => { const simulatedRegion = getElement( draftState, - 'simulatedRegions', + 'simulatedRegion', simulatedRegionId ); simulatedRegion.position = cloneDeepMutable(targetPosition); @@ -118,7 +118,7 @@ export namespace SimulatedRegionActionReducers { reducer: (draftState, { simulatedRegionId, newName }) => { const simulatedRegion = getElement( draftState, - 'simulatedRegions', + 'simulatedRegion', simulatedRegionId ); simulatedRegion.name = newName; diff --git a/shared/src/store/action-reducers/transfer-point.ts b/shared/src/store/action-reducers/transfer-point.ts index dd55cbae0..f72d916c9 100644 --- a/shared/src/store/action-reducers/transfer-point.ts +++ b/shared/src/store/action-reducers/transfer-point.ts @@ -124,7 +124,7 @@ export namespace TransferPointActionReducers { reducer: (draftState, { transferPointId, targetPosition }) => { const transferPoint = getElement( draftState, - 'transferPoints', + 'transferPoint', transferPointId ); transferPoint.position = cloneDeepMutable(targetPosition); @@ -142,7 +142,7 @@ export namespace TransferPointActionReducers { ) => { const transferPoint = getElement( draftState, - 'transferPoints', + 'transferPoint', transferPointId ); // Empty strings are ignored @@ -173,12 +173,12 @@ export namespace TransferPointActionReducers { } const transferPoint1 = getElement( draftState, - 'transferPoints', + 'transferPoint', transferPointId1 ); const transferPoint2 = getElement( draftState, - 'transferPoints', + 'transferPoint', transferPointId2 ); const _duration = @@ -210,12 +210,12 @@ export namespace TransferPointActionReducers { } const transferPoint1 = getElement( draftState, - 'transferPoints', + 'transferPoint', transferPointId1 ); const transferPoint2 = getElement( draftState, - 'transferPoints', + 'transferPoint', transferPointId2 ); delete transferPoint1.reachableTransferPoints[transferPointId2]; @@ -230,20 +230,20 @@ export namespace TransferPointActionReducers { action: RemoveTransferPointAction, reducer: (draftState, { transferPointId }) => { // check if transferPoint exists - getElement(draftState, 'transferPoints', transferPointId); + getElement(draftState, 'transferPoint', transferPointId); // TODO: make it dynamic (if at any time something else is able to transfer this part needs to be changed accordingly) // Let all vehicles and personnel arrive that are on transfer to this transferPoint before deleting it for (const vehicleId of Object.keys(draftState.vehicles)) { const vehicle = getElement( draftState, - 'vehicles', + 'vehicle', vehicleId ); if ( vehicle.transfer?.targetTransferPointId === transferPointId ) { - letElementArrive(draftState, 'vehicles', vehicleId); + letElementArrive(draftState, vehicle.type, vehicleId); } } for (const personnelId of Object.keys(draftState.personnel)) { @@ -256,7 +256,11 @@ export namespace TransferPointActionReducers { personnel.transfer?.targetTransferPointId === transferPointId ) { - letElementArrive(draftState, 'personnel', personnelId); + letElementArrive( + draftState, + personnel.type, + personnelId + ); } } // TODO: If we can assume that the transfer points are always connected to each other, @@ -286,10 +290,10 @@ export namespace TransferPointActionReducers { action: ConnectHospitalAction, reducer: (draftState, { transferPointId, hospitalId }) => { // Check if hospital with this Id exists - getElement(draftState, 'hospitals', hospitalId); + getElement(draftState, 'hospital', hospitalId); const transferPoint = getElement( draftState, - 'transferPoints', + 'transferPoint', transferPointId ); transferPoint.reachableHospitals[hospitalId] = true; @@ -302,10 +306,10 @@ export namespace TransferPointActionReducers { action: DisconnectHospitalAction, reducer: (draftState, { hospitalId, transferPointId }) => { // Check if hospital with this Id exists - getElement(draftState, 'hospitals', hospitalId); + getElement(draftState, 'hospital', hospitalId); const transferPoint = getElement( draftState, - 'transferPoints', + 'transferPoint', transferPointId ); delete transferPoint.reachableHospitals[hospitalId]; diff --git a/shared/src/store/action-reducers/transfer.ts b/shared/src/store/action-reducers/transfer.ts index 9dc2eee4d..eb0b2f5f1 100644 --- a/shared/src/store/action-reducers/transfer.ts +++ b/shared/src/store/action-reducers/transfer.ts @@ -17,9 +17,9 @@ import { updateElementPosition, } from './utils/spatial-elements'; -type TransferableElementType = 'personnel' | 'vehicles'; +export type TransferableElementType = 'personnel' | 'vehicle'; const transferableElementTypeAllowedValues: AllowedValues = - { personnel: true, vehicles: true }; + { personnel: true, vehicle: true }; /** * Personnel/Vehicle in transfer will arrive immediately at new targetTransferPoint @@ -38,7 +38,7 @@ export function letElementArrive( } const targetTransferPoint = getElement( draftState, - 'transferPoints', + 'transferPoint', element.transfer.targetTransferPointId ); const newPosition: Mutable = { @@ -135,7 +135,7 @@ export namespace TransferActionReducers { { elementType, elementId, startPoint, targetTransferPointId } ) => { // check if transferPoint exists - getElement(draftState, 'transferPoints', targetTransferPointId); + getElement(draftState, 'transferPoint', targetTransferPointId); const element = getElement(draftState, elementType, elementId); if (element.transfer) { throw new ReducerError( @@ -148,7 +148,7 @@ export namespace TransferActionReducers { if (startPoint.type === 'transferPoint') { const transferStartPoint = getElement( draftState, - 'transferPoints', + 'transferPoint', startPoint.transferPointId ); const connection = @@ -206,7 +206,7 @@ export namespace TransferActionReducers { } if (targetTransferPointId) { // check if transferPoint exists - getElement(draftState, 'transferPoints', targetTransferPointId); + getElement(draftState, 'transferPoint', targetTransferPointId); element.transfer.targetTransferPointId = targetTransferPointId; } if (timeToAdd) { @@ -228,7 +228,7 @@ export namespace TransferActionReducers { { elementType, elementId, targetTransferPointId } ) => { // check if transferPoint exists - getElement(draftState, 'transferPoints', targetTransferPointId); + getElement(draftState, 'transferPoint', targetTransferPointId); const element = getElement(draftState, elementType, elementId); if (!element.transfer) { throw getNotInTransferError(element.id); diff --git a/shared/src/store/action-reducers/utils/calculate-treatments.spec.ts b/shared/src/store/action-reducers/utils/calculate-treatments.spec.ts index e811becfc..ca0fde55a 100644 --- a/shared/src/store/action-reducers/utils/calculate-treatments.spec.ts +++ b/shared/src/store/action-reducers/utils/calculate-treatments.spec.ts @@ -22,7 +22,7 @@ interface Catering { * The id of the material or personnel catering */ catererId: UUID; - catererType: 'materials' | 'personnel'; + catererType: 'material' | 'personnel'; /** * All patients treated by {@link catererId} */ @@ -43,11 +43,11 @@ function assertCatering( for (const catering of caterings) { // Update all the patients const patients = catering.patientIds.map((patientId) => - getElement(draftState, 'patients', patientId) + getElement(draftState, 'patient', patientId) ); for (const patient of patients) { patient[ - catering.catererType === 'materials' + catering.catererType === 'material' ? 'assignedMaterialIds' : 'assignedPersonnelIds' ][catering.catererId] = true; @@ -288,7 +288,7 @@ describe('calculate treatment', () => { assertCatering(beforeState, newState, [ { catererId: ids.material, - catererType: 'materials', + catererType: 'material', patientIds: [ids.greenPatient], }, ]); @@ -324,7 +324,7 @@ describe('calculate treatment', () => { assertCatering(beforeState, newState, [ { catererId: ids.material, - catererType: 'materials', + catererType: 'material', patientIds: [ids.redPatient], }, ]); @@ -392,7 +392,7 @@ describe('calculate treatment', () => { assertCatering(beforeState, newState, [ { catererId: ids.material, - catererType: 'materials', + catererType: 'material', patientIds: [ids.redPatient, ids.greenPatient], }, ]); diff --git a/shared/src/store/action-reducers/utils/calculate-treatments.ts b/shared/src/store/action-reducers/utils/calculate-treatments.ts index fbc9fd9d7..090729406 100644 --- a/shared/src/store/action-reducers/utils/calculate-treatments.ts +++ b/shared/src/store/action-reducers/utils/calculate-treatments.ts @@ -6,6 +6,7 @@ import { SpatialTree } from '../../../models/utils/spatial-tree'; 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 { getElement } from './get-element'; // TODO: `caterFor` and `treat` are currently used as synonyms without a clear distinction. @@ -82,7 +83,7 @@ function tryToCaterFor( cateringElement.assignedPatientIds[patient.id] = true; - if (isPersonnel(cateringElement)) { + if (cateringElement.type === 'personnel') { patient.assignedPersonnelIds[cateringElement.id] = true; } else { patient.assignedMaterialIds[cateringElement.id] = true; @@ -92,26 +93,6 @@ function tryToCaterFor( return true; } -// TODO: Instead, give each Element a "type" property -> discriminated union -function isPatient( - element: Mutable -): element is Mutable { - return (element as Patient).personalInformation !== undefined; -} - -function isPersonnel( - element: Mutable -): element is Mutable { - return (element as Personnel).personnelType !== undefined; -} - -function isMaterial( - element: Mutable -): element is Mutable { - // as Material does not include any distinguishable properties, we will check if it is not of type Personnel or Patient - return !isPersonnel(element) && !isPatient(element); -} - /** * @param position of the patient where all elements of {@link elementType} should be recalculated * @param elementIdsToBeSkipped the elements whose treatment should not be updated @@ -119,11 +100,11 @@ function isMaterial( function updateCateringAroundPatient( state: Mutable, position: Position, - elementType: 'materials' | 'personnel', + elementType: 'material' | 'personnel', elementIdsToBeSkipped: Set ) { const elementsInTreatmentRange = SpatialTree.findAllElementsInCircle( - state.spatialTrees[elementType], + state.spatialTrees[elementTypePluralMap[elementType]], position, maxTreatmentRange ).filter((elementId) => !elementIdsToBeSkipped.has(elementId)); @@ -140,7 +121,7 @@ function removeTreatmentsOfElement( // TODO: when elements have their own type saved don't use const patient = getElement(state, 'patients', element.id); // instead use const patient = element; // same for personnel and material in the other if statements - if (isPatient(element)) { + if (element.type === 'patient') { const patient = element; // Make all personnel stop treating this patient for (const personnelId of Object.keys(patient.assignedPersonnelIds)) { @@ -150,23 +131,23 @@ function removeTreatmentsOfElement( patient.assignedPersonnelIds = {}; // Make all material stop treating this patient for (const materialId of Object.keys(patient.assignedMaterialIds)) { - const material = getElement(state, 'materials', materialId); + const material = getElement(state, 'material', materialId); delete material.assignedPatientIds[patient.id]; } patient.assignedMaterialIds = {}; - } else if (isPersonnel(element)) { + } else if (element.type === 'personnel') { const personnel = element; // This personnel doesn't treat any patients anymore for (const patientId of Object.keys(personnel.assignedPatientIds)) { - const patient = getElement(state, 'patients', patientId); + const patient = getElement(state, 'patient', patientId); delete patient.assignedPersonnelIds[personnel.id]; } personnel.assignedPatientIds = {}; - } else if (isMaterial(element)) { + } else if (element.type === 'material') { const material = element; // This material doesn't treat any patients anymore for (const patientId of Object.keys(material.assignedPatientIds)) { - const patient = getElement(state, 'patients', patientId); + const patient = getElement(state, 'patient', patientId); delete patient.assignedMaterialIds[material.id]; } material.assignedPatientIds = {}; @@ -194,7 +175,7 @@ export function updateTreatments( return; } - if (isPersonnel(element) || isMaterial(element)) { + if (element.type === 'personnel' || element.type === 'material') { updateCatering(state, element); return; } @@ -208,7 +189,7 @@ export function updateTreatments( alreadyUpdatedElementIds.add(personnelId); } for (const materialId of Object.keys(element.assignedMaterialIds)) { - updateCatering(state, getElement(state, 'materials', materialId)); + updateCatering(state, getElement(state, 'material', materialId)); // Saving materialIds of material that already got calculated - makes small movements of patients more efficient alreadyUpdatedElementIds.add(materialId); } @@ -222,7 +203,7 @@ export function updateTreatments( updateCateringAroundPatient( state, element.position, - 'materials', + 'material', alreadyUpdatedElementIds ); // The treatment of the patient has just been updated -> hence the visible status hasn't been changed since the last update @@ -270,7 +251,7 @@ function updateCatering( tryToCaterFor( cateringElement, catersFor, - getElement(state, 'patients', patientId), + getElement(state, 'patient', patientId), state.configuration.pretriageEnabled, state.configuration.bluePatientsEnabled ); @@ -296,7 +277,7 @@ function updateCatering( ) // Filter out every patient in the overrideTreatmentRange .filter((patientId) => !cateredForPatients.has(patientId)) - .map((patientId) => getElement(state, 'patients', patientId)); + .map((patientId) => getElement(state, 'patient', patientId)); const patientsPerStatus = groupBy(patientsInTreatmentRange, (patient) => getCateringStatus( diff --git a/shared/src/store/action-reducers/utils/get-element.ts b/shared/src/store/action-reducers/utils/get-element.ts index 721faa5dd..653f6087c 100644 --- a/shared/src/store/action-reducers/utils/get-element.ts +++ b/shared/src/store/action-reducers/utils/get-element.ts @@ -1,5 +1,7 @@ import type { ExerciseState } from '../../../state'; import type { Mutable, UUID } from '../../../utils'; +import type { ElementTypePluralMap } from '../../../utils/element-type-plural-map'; +import { elementTypePluralMap } from '../../../utils/element-type-plural-map'; import { ReducerError } from '../../reducer-error'; /** @@ -7,25 +9,16 @@ import { ReducerError } from '../../reducer-error'; * @throws ReducerError if the element does not exist */ export function getElement< - ElementType extends - | 'alarmGroups' - | 'clients' - | 'hospitals' - | 'mapImages' - | 'materials' - | 'patients' - | 'personnel' - | 'simulatedRegions' - | 'transferPoints' - | 'vehicles' - | 'viewports', + ElementType extends keyof ElementTypePluralMap, State extends ExerciseState | Mutable >( state: State, elementType: ElementType, elementId: UUID -): State[ElementType][UUID] { - const element = state[elementType][elementId] as State[ElementType][UUID]; +): State[ElementTypePluralMap[ElementType]][UUID] { + const element = state[elementTypePluralMap[elementType]][ + elementId + ] as State[ElementTypePluralMap[ElementType]][UUID]; if (!element) { throw new ReducerError( `Element of type ${elementType} with id ${elementId} does not exist` diff --git a/shared/src/store/action-reducers/utils/spatial-elements.ts b/shared/src/store/action-reducers/utils/spatial-elements.ts index 6a88929cf..43fa7a604 100644 --- a/shared/src/store/action-reducers/utils/spatial-elements.ts +++ b/shared/src/store/action-reducers/utils/spatial-elements.ts @@ -3,6 +3,8 @@ import { SpatialTree } from '../../../models/utils/spatial-tree'; import type { ExerciseState } from '../../../state'; import type { Mutable, UUID } from '../../../utils'; import { cloneDeepMutable } from '../../../utils'; +import type { ElementTypePluralMap } from '../../../utils/element-type-plural-map'; +import { elementTypePluralMap } from '../../../utils/element-type-plural-map'; import { updateTreatments } from './calculate-treatments'; import { getElement } from './get-element'; @@ -11,7 +13,9 @@ import { getElement } from './get-element'; * The position of the element must be changed via one of the function in this file. * In addition, the respective functions must be called when an element gets added or removed. */ -export type SpatialElementType = 'materials' | 'patients' | 'personnel'; +type SpatialElementType = 'material' | 'patient' | 'personnel'; +type SpatialTypePluralMap = Pick; +export type SpatialElementPlural = SpatialTypePluralMap[SpatialElementType]; /** * Adds an element with a position and executes side effects to guarantee the consistency of the state. @@ -27,7 +31,7 @@ export function addElementPosition( return; } SpatialTree.addElement( - state.spatialTrees[elementType], + state.spatialTrees[elementTypePluralMap[elementType]], element.id, element.position ); @@ -47,14 +51,14 @@ export function updateElementPosition( const startPosition = element.position; if (startPosition !== undefined) { SpatialTree.moveElement( - state.spatialTrees[elementType], + state.spatialTrees[elementTypePluralMap[elementType]], element.id, startPosition, targetPosition ); } else { SpatialTree.addElement( - state.spatialTrees[elementType], + state.spatialTrees[elementTypePluralMap[elementType]], element.id, targetPosition ); @@ -81,7 +85,7 @@ export function removeElementPosition( return; } SpatialTree.removeElement( - state.spatialTrees[elementType], + state.spatialTrees[elementTypePluralMap[elementType]], element.id, element.position ); diff --git a/shared/src/store/action-reducers/vehicle.ts b/shared/src/store/action-reducers/vehicle.ts index c0704b6b3..227586a35 100644 --- a/shared/src/store/action-reducers/vehicle.ts +++ b/shared/src/store/action-reducers/vehicle.ts @@ -26,10 +26,10 @@ export function deleteVehicle( draftState: Mutable, vehicleId: UUID ) { - const vehicle = getElement(draftState, 'vehicles', vehicleId); + const vehicle = getElement(draftState, 'vehicle', vehicleId); // Delete related material and personnel Object.keys(vehicle.materialIds).forEach((materialId) => { - removeElementPosition(draftState, 'materials', materialId); + removeElementPosition(draftState, 'material', materialId); delete draftState.materials[materialId]; }); Object.keys(vehicle.personnelIds).forEach((personnelId) => { @@ -105,13 +105,13 @@ export class LoadVehicleAction implements Action { public readonly vehicleId!: UUID; @IsLiteralUnion({ - materials: true, - patients: true, + material: true, + patient: true, personnel: true, }) public readonly elementToBeLoadedType!: - | 'materials' - | 'patients' + | 'material' + | 'patient' | 'personnel'; @IsUUID(4, uuidValidationOptions) @@ -155,7 +155,7 @@ export namespace VehicleActionReducers { vehicleId: vehicle.id, }; draftState.materials[material.id] = material; - addElementPosition(draftState, 'materials', material.id); + addElementPosition(draftState, 'material', material.id); } for (const person of cloneDeepMutable(personnel)) { person.metaPosition = { @@ -173,7 +173,7 @@ export namespace VehicleActionReducers { export const moveVehicle: ActionReducer = { action: MoveVehicleAction, reducer: (draftState, { vehicleId, targetPosition }) => { - const vehicle = getElement(draftState, 'vehicles', vehicleId); + const vehicle = getElement(draftState, 'vehicle', vehicleId); vehicle.position = cloneDeepMutable(targetPosition); vehicle.metaPosition = { type: 'coordinates', @@ -187,7 +187,7 @@ export namespace VehicleActionReducers { export const renameVehicle: ActionReducer = { action: RenameVehicleAction, reducer: (draftState, { vehicleId, name }) => { - const vehicle = getElement(draftState, 'vehicles', vehicleId); + const vehicle = getElement(draftState, 'vehicle', vehicleId); vehicle.name = name; for (const personnelId of Object.keys(vehicle.personnelIds)) { draftState.personnel[personnelId]!.vehicleName = name; @@ -212,7 +212,7 @@ export namespace VehicleActionReducers { export const unloadVehicle: ActionReducer = { action: UnloadVehicleAction, reducer: (draftState, { vehicleId }) => { - const vehicle = getElement(draftState, 'vehicles', vehicleId); + const vehicle = getElement(draftState, 'vehicle', vehicleId); const unloadMetaPosition = vehicle.metaPosition; if (unloadMetaPosition.type !== 'coordinates') { throw new ReducerError( @@ -239,7 +239,7 @@ export namespace VehicleActionReducers { for (const patientId of patientIds) { x += space; - updateElementPosition(draftState, 'patients', patientId, { + updateElementPosition(draftState, 'patient', patientId, { x, y: unloadPosition.y, }); @@ -268,13 +268,9 @@ export namespace VehicleActionReducers { for (const materialId of materialIds) { x += space; - const material = getElement( - draftState, - 'materials', - materialId - ); + const material = getElement(draftState, 'material', materialId); if (Material.isInVehicle(material)) { - updateElementPosition(draftState, 'materials', materialId, { + updateElementPosition(draftState, 'material', materialId, { x, y: unloadPosition.y, }); @@ -292,12 +288,12 @@ export namespace VehicleActionReducers { draftState, { vehicleId, elementToBeLoadedId, elementToBeLoadedType } ) => { - const vehicle = getElement(draftState, 'vehicles', vehicleId); + const vehicle = getElement(draftState, 'vehicle', vehicleId); switch (elementToBeLoadedType) { - case 'materials': { + case 'material': { const material = getElement( draftState, - 'materials', + 'material', elementToBeLoadedId ); if (!vehicle.materialIds[elementToBeLoadedId]) { @@ -309,7 +305,7 @@ export namespace VehicleActionReducers { type: 'vehicle', vehicleId, }; - removeElementPosition(draftState, 'materials', material.id); + removeElementPosition(draftState, 'material', material.id); break; } case 'personnel': { @@ -339,10 +335,10 @@ export namespace VehicleActionReducers { ); break; } - case 'patients': { + case 'patient': { const patient = getElement( draftState, - 'patients', + 'patient', elementToBeLoadedId ); if ( @@ -359,13 +355,13 @@ export namespace VehicleActionReducers { type: 'vehicle', vehicleId, }; - removeElementPosition(draftState, 'patients', patient.id); + removeElementPosition(draftState, patient.type, patient.id); // Load in all materials Object.keys(vehicle.materialIds).forEach((materialId) => { getElement( draftState, - 'materials', + 'material', materialId ).metaPosition = { type: 'vehicle', @@ -373,7 +369,7 @@ export namespace VehicleActionReducers { }; removeElementPosition( draftState, - 'materials', + 'material', materialId ); }); diff --git a/shared/src/store/action-reducers/viewport.ts b/shared/src/store/action-reducers/viewport.ts index 13c3b1919..f81ee56a3 100644 --- a/shared/src/store/action-reducers/viewport.ts +++ b/shared/src/store/action-reducers/viewport.ts @@ -69,7 +69,7 @@ export namespace ViewportActionReducers { export const removeViewport: ActionReducer = { action: RemoveViewportAction, reducer: (draftState, { viewportId }) => { - getElement(draftState, 'viewports', viewportId); + getElement(draftState, 'viewport', viewportId); delete draftState.viewports[viewportId]; return draftState; }, @@ -79,7 +79,7 @@ export namespace ViewportActionReducers { export const moveViewport: ActionReducer = { action: MoveViewportAction, reducer: (draftState, { viewportId, targetPosition }) => { - const viewport = getElement(draftState, 'viewports', viewportId); + const viewport = getElement(draftState, 'viewport', viewportId); viewport.position = cloneDeepMutable(targetPosition); return draftState; }, @@ -89,7 +89,7 @@ export namespace ViewportActionReducers { export const resizeViewport: ActionReducer = { action: ResizeViewportAction, reducer: (draftState, { viewportId, targetPosition, newSize }) => { - const viewport = getElement(draftState, 'viewports', viewportId); + const viewport = getElement(draftState, 'viewport', viewportId); viewport.position = cloneDeepMutable(targetPosition); viewport.size = cloneDeepMutable(newSize); return draftState; @@ -100,7 +100,7 @@ export namespace ViewportActionReducers { export const renameViewport: ActionReducer = { action: RenameViewportAction, reducer: (draftState, { viewportId, newName }) => { - const viewport = getElement(draftState, 'viewports', viewportId); + const viewport = getElement(draftState, 'viewport', viewportId); viewport.name = newName; return draftState; }, diff --git a/shared/src/store/reduce-exercise-state.spec.ts b/shared/src/store/reduce-exercise-state.spec.ts index 49afadc2d..ac72e6cdd 100644 --- a/shared/src/store/reduce-exercise-state.spec.ts +++ b/shared/src/store/reduce-exercise-state.spec.ts @@ -12,6 +12,7 @@ describe('exerciseReducer', () => { function generateViewport(): Viewport { return { id: uuid(), + type: 'viewport', name: 'Test', size: { width: 100, height: 100 }, position: { x: 0, y: 0 }, diff --git a/shared/src/store/validate-exercise-action.spec.ts b/shared/src/store/validate-exercise-action.spec.ts index 9bc74a97d..da9aab590 100644 --- a/shared/src/store/validate-exercise-action.spec.ts +++ b/shared/src/store/validate-exercise-action.spec.ts @@ -71,6 +71,7 @@ describe('validateExerciseAction', () => { type: '[Viewport] Add viewport', viewport: { id: 'b02c7756-ea52-427f-9fc3-0e163799544d', + type: 'viewport', name: '', size: { height: 1, @@ -112,6 +113,7 @@ describe('validateExerciseAction', () => { type: '[Viewport] Add viewport', viewport: { id: 'b02c7756-ea52-427f-9fc3-0e163799544d', + type: 'viewport', name: '', size: { height: 1, diff --git a/shared/src/utils/element-type-plural-map.ts b/shared/src/utils/element-type-plural-map.ts new file mode 100644 index 000000000..630dcd7ef --- /dev/null +++ b/shared/src/utils/element-type-plural-map.ts @@ -0,0 +1,21 @@ +// eslint-disable-next-line @typescript-eslint/no-shadow +import type { Element } from '../models/element'; +import type { ExerciseState } from '../state'; + +type ElementType = Element['type']; + +export const elementTypePluralMap = { + alarmGroup: 'alarmGroups', + client: 'clients', + hospital: 'hospitals', + mapImage: 'mapImages', + material: 'materials', + patient: 'patients', + personnel: 'personnel', + simulatedRegion: 'simulatedRegions', + transferPoint: 'transferPoints', + vehicle: 'vehicles', + viewport: 'viewports', +} as const satisfies { [Key in ElementType]: keyof ExerciseState }; + +export type ElementTypePluralMap = typeof elementTypePluralMap;