From 868eb9cbdfa4e842349e1bb1034620a696c3089a Mon Sep 17 00:00:00 2001 From: Hector Dearman Date: Sat, 31 Jan 2026 15:05:26 +0000 Subject: [PATCH] Add functionality to remove equipment --- src/commands/equipment/index.ts | 6 +++ src/commands/equipment/remove-form.ts | 43 ++++++++++++++++++++ src/commands/equipment/remove.ts | 40 ++++++++++++++++++ src/queries/equipment/render.ts | 19 +++++++++ src/read-models/shared-state/update-state.ts | 9 ++++ src/routes.ts | 1 + src/types/domain-event.ts | 6 +++ tests/read-models/test-framework.ts | 1 + 8 files changed, 125 insertions(+) create mode 100644 src/commands/equipment/remove-form.ts create mode 100644 src/commands/equipment/remove.ts diff --git a/src/commands/equipment/index.ts b/src/commands/equipment/index.ts index 24f9bc7d..8cc6d77e 100644 --- a/src/commands/equipment/index.ts +++ b/src/commands/equipment/index.ts @@ -4,6 +4,8 @@ import {registerTrainingSheet} from './register-training-sheet'; import {registerTrainingSheetForm} from './register-training-sheet-form'; import {removeTrainingSheet} from './remove-training-sheet'; import {removeTrainingSheetForm} from './remove-training-sheet-form'; +import {removeEquipment} from './remove'; +import {removeEquipmentForm} from './remove-form'; export const equipment = { add: { @@ -18,4 +20,8 @@ export const equipment = { ...removeTrainingSheet, ...removeTrainingSheetForm, }, + remove: { + ...removeEquipment, + ...removeEquipmentForm, + }, }; diff --git a/src/commands/equipment/remove-form.ts b/src/commands/equipment/remove-form.ts new file mode 100644 index 00000000..bd66b6b1 --- /dev/null +++ b/src/commands/equipment/remove-form.ts @@ -0,0 +1,43 @@ +import * as E from 'fp-ts/Either'; +import {Form} from '../../types/form'; +import {pipe} from 'fp-ts/lib/function'; +import {html, safe, sanitizeString, toLoggedInContent} from '../../types/html'; +import {getEquipmentIdFromForm} from './get-equipment-id-from-form'; +import {getEquipmentName} from './get-equipment-name'; + +type ViewModel = { + equipmentId: string; + equipmentName: string; +}; + +const renderForm = (viewModel: ViewModel) => + pipe( + html` +
+

Remove '${sanitizeString(viewModel.equipmentName)}'?

+
+ + +
+
+ `, + toLoggedInContent(safe('Remove Equipment')) + ); + +export const removeEquipmentForm: Form = { + renderForm, + constructForm: + input => + ({readModel}) => + pipe( + E.Do, + E.bind('equipmentId', () => getEquipmentIdFromForm(input)), + E.bind('equipmentName', ({equipmentId}) => + getEquipmentName(readModel, equipmentId) + ) + ), +}; diff --git a/src/commands/equipment/remove.ts b/src/commands/equipment/remove.ts new file mode 100644 index 00000000..4cb98347 --- /dev/null +++ b/src/commands/equipment/remove.ts @@ -0,0 +1,40 @@ +import {constructEvent, isEventOfType} from '../../types'; +import * as RA from 'fp-ts/ReadonlyArray'; +import * as t from 'io-ts'; +import * as tt from 'io-ts-types'; +import * as O from 'fp-ts/Option'; +import {pipe} from 'fp-ts/lib/function'; +import {Command} from '../command'; +import {isAdminOrSuperUser} from '../is-admin-or-super-user'; + +const codec = t.strict({ + id: tt.UUID, +}); + +type RemoveEquipment = t.TypeOf; + +const process: Command['process'] = input => { + if (input.events.length === 0) { + return O.none; + } + return pipe( + input.events, + RA.filter(isEventOfType('EquipmentRemoved')), + RA.match( + () => O.some(constructEvent('EquipmentRemoved')(input.command)), + () => O.none + ) + ); +}; + +const resource: Command['resource'] = command => ({ + type: 'Equipment', + id: command.id, +}); + +export const removeEquipment: Command = { + process, + resource, + decode: codec.decode, + isAuthorized: isAdminOrSuperUser, +}; diff --git a/src/queries/equipment/render.ts b/src/queries/equipment/render.ts index 7fea0065..366d091e 100644 --- a/src/queries/equipment/render.ts +++ b/src/queries/equipment/render.ts @@ -73,6 +73,24 @@ const adminMarkTrainedBy = (viewModel: ViewModel) => O.getOrElse(() => html``) ); +const adminRemoveEquipment = (viewModel: ViewModel) => + pipe( + viewModel, + O.of, + O.filter(viewModel => viewModel.isSuperUser), + O.map(viewModel => viewModel.equipment.id), + O.map( + id => + html`
  • + [Admin] Remove equipment + ${tooltip(safe('Only admins can remove equipment'))} +
  • ` + ), + O.getOrElse(() => html``) + ); + const addTrainer = (viewModel: ViewModel) => pipe( viewModel, @@ -141,6 +159,7 @@ const equipmentActions = (viewModel: ViewModel) => html` ${trainMember(viewModel)} ${adminMarkTrainedBy(viewModel)} ${addTrainer(viewModel)} ${registerSheet(viewModel)} ${currentSheet(viewModel)} ${removeTrainingSheet(viewModel)} + ${adminRemoveEquipment(viewModel)} `; diff --git a/src/read-models/shared-state/update-state.ts b/src/read-models/shared-state/update-state.ts index fe4e937c..8d62d079 100644 --- a/src/read-models/shared-state/update-state.ts +++ b/src/read-models/shared-state/update-state.ts @@ -92,6 +92,15 @@ export const updateState = .values({id: event.id, name: event.name, areaId: event.areaId}) .run(); break; + case 'EquipmentRemoved': + db.delete(trainersTable) + .where(eq(trainersTable.equipmentId, event.id)) + .run(); + db.delete(trainedMemberstable) + .where(eq(trainedMemberstable.equipmentId, event.id)) + .run(); + db.delete(equipmentTable).where(eq(equipmentTable.id, event.id)).run(); + break; case 'TrainerAdded': { if ( isOwnerOfAreaContainingEquipment(db, linking)( diff --git a/src/routes.ts b/src/routes.ts index 2917b545..dfe1f4c1 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -42,6 +42,7 @@ export const initRoutes = ( 'remove-training-sheet', commands.equipment.removeTrainingSheet ), + ...command('equipment', 'remove', commands.equipment.remove), ...command( 'equipment', 'mark-member-trained', diff --git a/src/types/domain-event.ts b/src/types/domain-event.ts index 3ab9f0f4..6f426cda 100644 --- a/src/types/domain-event.ts +++ b/src/types/domain-event.ts @@ -54,6 +54,10 @@ const EquipmentAdded = defineEvent('EquipmentAdded', { areaId: tt.UUID, }); +const EquipmentRemoved = defineEvent('EquipmentRemoved', { + id: tt.UUID, +}); + const OwnerAdded = defineEvent('OwnerAdded', { areaId: tt.UUID, memberNumber: t.number, @@ -202,6 +206,7 @@ export const events = [ AreaRemoved, AreaEmailUpdated, EquipmentAdded, + EquipmentRemoved, OwnerAdded, OwnerRemoved, SuperUserDeclared, @@ -233,6 +238,7 @@ export const DomainEvent = t.union([ AreaRemoved.codec, AreaEmailUpdated.codec, EquipmentAdded.codec, + EquipmentRemoved.codec, OwnerAdded.codec, OwnerRemoved.codec, SuperUserDeclared.codec, diff --git a/tests/read-models/test-framework.ts b/tests/read-models/test-framework.ts index e5dce21b..3161f08d 100644 --- a/tests/read-models/test-framework.ts +++ b/tests/read-models/test-framework.ts @@ -136,6 +136,7 @@ export const initTestFramework = async (): Promise => { removeTrainingSheet: frameworkify( commands.equipment.removeTrainingSheet ), + remove: frameworkify(commands.equipment.remove), }, trainers: { add: frameworkify(commands.trainers.add),