Skip to content

Commit db0ddfa

Browse files
committed
Unify clinical CDS event dispatch and legacy hook fallback
1 parent 98a7aee commit db0ddfa

2 files changed

Lines changed: 131 additions & 90 deletions

File tree

src/app/benefits-demo/clinical-record/clinical-record.component.ts

Lines changed: 56 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { MatDialog } from '@angular/material/dialog';
44
import { ActivatedRoute, Router } from '@angular/router';
55
import { PatientService } from '../../services/patient.service';
66
import { AiAssistedEntryTransactionResult } from '../../services/patient-storage.types';
7-
import { CdsService, STANDARD_CDS_HOOK_LABELS, StandardCdsHook } from '../../services/cds.service';
7+
import { CdsService } from '../../services/cds.service';
88
import { TerminologyService } from '../../services/terminology.service';
99
import { ClinicalEntryComponent, ClinicalEntryType } from '../clinical-entry/clinical-entry.component';
1010
import { CdsState } from '../cds-panel/cds-panel.component';
@@ -459,7 +459,7 @@ export class ClinicalRecordComponent implements OnInit, OnDestroy, AfterViewInit
459459
this.populateClinicalDataFromService(patientId);
460460
this.touchDataVersion();
461461
this.refreshSummaryVisualizations();
462-
this.triggerPatientViewHook();
462+
this.dispatchCdsEvent({ type: 'patient-view' });
463463
this.showClinicalDataLoadedSnackBar(summary);
464464
} catch (error) {
465465
console.error('Error loading clinical data from FHIR server:', error);
@@ -477,7 +477,7 @@ export class ClinicalRecordComponent implements OnInit, OnDestroy, AfterViewInit
477477
this.populateClinicalDataFromService(patientId);
478478
this.touchDataVersion();
479479
this.refreshSummaryVisualizations();
480-
this.triggerPatientViewHook();
480+
this.dispatchCdsEvent({ type: 'patient-view' });
481481
}
482482

483483
private populateClinicalDataFromService(patientId: string): void {
@@ -913,7 +913,7 @@ export class ClinicalRecordComponent implements OnInit, OnDestroy, AfterViewInit
913913
// and use the updated snapshot as the source of truth for hook execution.
914914
const createdCondition = event as Condition;
915915
this.conditions = [...this.conditions, createdCondition];
916-
this.triggerProblemListItemCreateHook(createdCondition, this.conditions);
916+
this.dispatchCdsEvent({ type: 'condition-added', newCondition: createdCondition, conditionsSnapshot: this.conditions });
917917
this.conditionEntry?.resetAndCloseForm();
918918
this.touchDataVersion();
919919

@@ -1041,7 +1041,7 @@ export class ClinicalRecordComponent implements OnInit, OnDestroy, AfterViewInit
10411041

10421042
// Add the new medication to the local array only if it was successfully added
10431043
// Create new array reference to trigger Angular change detection
1044-
this.triggerOrderSignHook(event);
1044+
this.dispatchCdsEvent({ type: 'medication-signed', draftMedication: event });
10451045
this.medications = [...this.medications, event];
10461046
this.medicationEntry?.resetAndCloseForm();
10471047
this.touchDataVersion();
@@ -1076,7 +1076,7 @@ export class ClinicalRecordComponent implements OnInit, OnDestroy, AfterViewInit
10761076
}
10771077

10781078
this.medicationOrderSelectPreviewTimeout = setTimeout(() => {
1079-
this.triggerOrderSelectHook(draftMedication);
1079+
this.dispatchCdsEvent({ type: 'medication-draft-changed', draftMedication });
10801080
this.medicationOrderSelectPreviewTimeout = null;
10811081
}, 350);
10821082
}
@@ -1104,7 +1104,7 @@ export class ClinicalRecordComponent implements OnInit, OnDestroy, AfterViewInit
11041104

11051105
this.immunizations = [...this.immunizations, event];
11061106
this.immunizationEntry?.resetAndCloseForm();
1107-
this.triggerPatientViewHook();
1107+
this.dispatchCdsEvent({ type: 'immunization-added' });
11081108
this.touchDataVersion();
11091109

11101110
if (this.clinicalTimeline && this.clinicalTimeline.refreshTimeline) {
@@ -1227,7 +1227,7 @@ export class ClinicalRecordComponent implements OnInit, OnDestroy, AfterViewInit
12271227
// Reload conditions after mapping to get the updated computedLocation
12281228
this.conditions = this.patientService.getPatientConditions(this.patient.id);
12291229
if (newestAllergy) {
1230-
this.triggerAllergyIntoleranceCreateHook(newestAllergy);
1230+
this.dispatchCdsEvent({ type: 'allergy-added', newAllergy: newestAllergy });
12311231
}
12321232

12331233
// Show success notification (already shown in clinical-forms component, but update here if needed)
@@ -1984,7 +1984,7 @@ export class ClinicalRecordComponent implements OnInit, OnDestroy, AfterViewInit
19841984
this.patientService.deletePatientCondition(this.patient.id, conditionId);
19851985
// Reload conditions directly from service to ensure fresh data
19861986
this.conditions = this.patientService.getPatientConditions(this.patient.id);
1987-
this.triggerPatientViewHook();
1987+
this.dispatchCdsEvent({ type: 'condition-deleted' });
19881988
this.touchDataVersion();
19891989
}
19901990
}
@@ -2009,7 +2009,7 @@ export class ClinicalRecordComponent implements OnInit, OnDestroy, AfterViewInit
20092009
this.patientService.deletePatientMedication(this.patient.id, medicationId);
20102010
// Reload medications directly from service to ensure fresh data
20112011
this.medications = this.patientService.getPatientMedications(this.patient.id);
2012-
this.triggerPatientViewHook();
2012+
this.dispatchCdsEvent({ type: 'medication-deleted' });
20132013
this.touchDataVersion();
20142014
}
20152015
}
@@ -2021,7 +2021,7 @@ export class ClinicalRecordComponent implements OnInit, OnDestroy, AfterViewInit
20212021
if (immunization && confirm(`Are you sure you want to delete the immunization "${immunization.vaccineCode?.text || immunization.vaccineCode?.coding?.[0]?.display}"?`)) {
20222022
this.patientService.deletePatientImmunization(this.patient.id, immunizationId);
20232023
this.immunizations = this.patientService.getPatientImmunizations(this.patient.id);
2024-
this.triggerPatientViewHook();
2024+
this.dispatchCdsEvent({ type: 'immunization-deleted' });
20252025
this.touchDataVersion();
20262026
}
20272027
}
@@ -2036,7 +2036,7 @@ export class ClinicalRecordComponent implements OnInit, OnDestroy, AfterViewInit
20362036
this.patientService.deletePatientAllergy(this.patient.id, allergyId);
20372037
// Reload allergies directly from service to ensure fresh data
20382038
this.allergies = this.patientService.getPatientAllergies(this.patient.id);
2039-
this.triggerPatientViewHook();
2039+
this.dispatchCdsEvent({ type: 'allergy-deleted' });
20402040
this.touchDataVersion();
20412041
}
20422042
}
@@ -2614,72 +2614,59 @@ export class ClinicalRecordComponent implements OnInit, OnDestroy, AfterViewInit
26142614
this.dataVersion += 1;
26152615
}
26162616

2617-
private triggerPatientViewHook(): void {
2617+
private dispatchCdsEvent(
2618+
event:
2619+
| { type: 'patient-view' | 'condition-deleted' | 'allergy-deleted' | 'medication-deleted' | 'immunization-added' | 'immunization-deleted' }
2620+
| { type: 'condition-added'; newCondition: Condition; conditionsSnapshot?: Condition[] }
2621+
| { type: 'allergy-added'; newAllergy: AllergyIntolerance }
2622+
| { type: 'medication-draft-changed' | 'medication-signed'; draftMedication: MedicationStatement }
2623+
): void {
26182624
if (!this.patient) {
26192625
return;
26202626
}
26212627

2622-
void firstValueFrom(this.cdsService.invokePatientView({
2628+
const baseContext = {
26232629
patient: this.patient,
2624-
conditions: this.conditions,
2630+
conditions: 'conditionsSnapshot' in event && event.conditionsSnapshot ? event.conditionsSnapshot : this.conditions,
26252631
medications: this.medications,
26262632
allergies: this.allergies
2627-
}));
2628-
}
2629-
2630-
private triggerProblemListItemCreateHook(newCondition: Condition, conditionsSnapshot?: Condition[]): void {
2631-
if (!this.patient) {
2632-
return;
2633-
}
2634-
2635-
void firstValueFrom(this.cdsService.invokeProblemListItemCreate({
2636-
patient: this.patient,
2637-
conditions: conditionsSnapshot || this.conditions,
2638-
medications: this.medications,
2639-
allergies: this.allergies,
2640-
newConditions: [newCondition]
2641-
}));
2642-
}
2643-
2644-
private triggerAllergyIntoleranceCreateHook(newAllergy: AllergyIntolerance): void {
2645-
if (!this.patient) {
2646-
return;
2647-
}
2648-
2649-
void firstValueFrom(this.cdsService.invokeAllergyIntoleranceCreate({
2650-
patient: this.patient,
2651-
conditions: this.conditions,
2652-
medications: this.medications,
2653-
allergies: [...this.allergies.filter((allergy) => allergy.id !== newAllergy.id), newAllergy]
2654-
}));
2655-
}
2656-
2657-
private triggerOrderSelectHook(draftMedication: MedicationStatement): void {
2658-
if (!this.patient) {
2659-
return;
2660-
}
2661-
2662-
void firstValueFrom(this.cdsService.invokeOrderSelect({
2663-
patient: this.patient,
2664-
conditions: this.conditions,
2665-
medications: this.medications,
2666-
allergies: this.allergies,
2667-
selectedMedications: [draftMedication]
2668-
}));
2669-
}
2633+
};
26702634

2671-
private triggerOrderSignHook(draftMedication: MedicationStatement): void {
2672-
if (!this.patient) {
2673-
return;
2635+
switch (event.type) {
2636+
case 'patient-view':
2637+
case 'condition-deleted':
2638+
case 'allergy-deleted':
2639+
case 'medication-deleted':
2640+
case 'immunization-added':
2641+
case 'immunization-deleted':
2642+
void firstValueFrom(this.cdsService.handleClinicalEvent({
2643+
type: event.type,
2644+
context: baseContext
2645+
}));
2646+
break;
2647+
case 'condition-added':
2648+
void firstValueFrom(this.cdsService.handleClinicalEvent({
2649+
type: 'condition-added',
2650+
context: baseContext,
2651+
newCondition: event.newCondition
2652+
}));
2653+
break;
2654+
case 'allergy-added':
2655+
void firstValueFrom(this.cdsService.handleClinicalEvent({
2656+
type: 'allergy-added',
2657+
context: baseContext,
2658+
newAllergy: event.newAllergy
2659+
}));
2660+
break;
2661+
case 'medication-draft-changed':
2662+
case 'medication-signed':
2663+
void firstValueFrom(this.cdsService.handleClinicalEvent({
2664+
type: event.type,
2665+
context: baseContext,
2666+
draftMedication: event.draftMedication
2667+
}));
2668+
break;
26742669
}
2675-
2676-
void firstValueFrom(this.cdsService.invokeOrderSign({
2677-
patient: this.patient,
2678-
conditions: this.conditions,
2679-
medications: this.medications,
2680-
allergies: this.allergies,
2681-
draftMedications: [draftMedication]
2682-
}));
26832670
}
26842671

26852672
private refreshSummaryVisualizations(): void {

src/app/services/cds.service.ts

Lines changed: 75 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,18 @@ export interface HookExecutionSnapshot {
354354
context: HookExecutionContextSnapshot | null;
355355
}
356356

357+
export type ClinicalCdsEvent =
358+
| { type: 'patient-view'; context: HookExecutionContextSnapshot }
359+
| { type: 'condition-added'; context: HookExecutionContextSnapshot; newCondition: Condition }
360+
| { type: 'condition-deleted'; context: HookExecutionContextSnapshot }
361+
| { type: 'allergy-added'; context: HookExecutionContextSnapshot; newAllergy: AllergyIntolerance }
362+
| { type: 'allergy-deleted'; context: HookExecutionContextSnapshot }
363+
| { type: 'medication-draft-changed'; context: HookExecutionContextSnapshot; draftMedication: MedicationStatement }
364+
| { type: 'medication-signed'; context: HookExecutionContextSnapshot; draftMedication: MedicationStatement }
365+
| { type: 'medication-deleted'; context: HookExecutionContextSnapshot }
366+
| { type: 'immunization-added'; context: HookExecutionContextSnapshot }
367+
| { type: 'immunization-deleted'; context: HookExecutionContextSnapshot };
368+
357369
type PatientHookStore = Record<StandardCdsHook, HookExecutionSnapshot>;
358370

359371
const STANDARD_HOOKS: StandardCdsHook[] = ['patient-view', 'order-select', 'order-sign', 'problem-list-item-create', 'allergyintolerance-create'];
@@ -427,6 +439,38 @@ export class CdsService {
427439
return this.invokeHook('allergyintolerance-create', context);
428440
}
429441

442+
handleClinicalEvent(event: ClinicalCdsEvent): Observable<HookExecutionSnapshot> {
443+
switch (event.type) {
444+
case 'patient-view':
445+
case 'condition-deleted':
446+
case 'allergy-deleted':
447+
case 'medication-deleted':
448+
case 'immunization-added':
449+
case 'immunization-deleted':
450+
return this.invokePatientView(event.context);
451+
case 'condition-added':
452+
return this.invokeProblemListItemCreate({
453+
...event.context,
454+
newConditions: [event.newCondition]
455+
});
456+
case 'allergy-added':
457+
return this.invokeAllergyIntoleranceCreate({
458+
...event.context,
459+
allergies: [...event.context.allergies.filter((allergy) => allergy.id !== event.newAllergy.id), event.newAllergy]
460+
});
461+
case 'medication-draft-changed':
462+
return this.invokeOrderSelect({
463+
...event.context,
464+
selectedMedications: [event.draftMedication]
465+
});
466+
case 'medication-signed':
467+
return this.invokeOrderSign({
468+
...event.context,
469+
draftMedications: [event.draftMedication]
470+
});
471+
}
472+
}
473+
430474
rerunHook(patientId: string, hook: StandardCdsHook): Observable<HookExecutionSnapshot> {
431475
const snapshot = this.getPatientHookSnapshot(patientId)[hook];
432476
if (!snapshot?.context) {
@@ -570,7 +614,7 @@ export class CdsService {
570614
return this.callHookOnServer(server, hook, standardRequest, legacyRequest, true);
571615
}
572616

573-
if (hook === 'order-select' && legacyRequest) {
617+
if (legacyRequest) {
574618
const legacyService = discovery.services.find((service) => service.id === 'medication-order-select' && (!service.hook || service.hook.trim().length === 0));
575619
if (legacyService || discovery.failed) {
576620
return this.http.post<CDSResponse>(this.buildServiceUrl(server.baseUrl, 'medication-order-select'), legacyRequest, {
@@ -647,6 +691,22 @@ export class CdsService {
647691
const conditionsBundle = this.createConditionsBundle(context.patient.id, conditions);
648692
const medicationsBundle = this.createMedicationBundle(context.patient.id, medications);
649693
const allergiesBundle = this.createAllergyBundle(context.patient.id, allergies);
694+
const createLegacyCompatibilityRequest = (draftMedicationRequests: CDSBundle<CDSMedicationRequest>): LegacyOrderSelectRequest => ({
695+
hook: 'order-page',
696+
hookInstance,
697+
fhirServer: this.fhirBaseUrl,
698+
context: {
699+
patientId: context.patient.id,
700+
encounterId,
701+
userId
702+
},
703+
prefetch: {
704+
patient,
705+
conditions: conditionsBundle,
706+
draftMedicationRequests,
707+
allergies: allergiesBundle
708+
}
709+
});
650710

651711
if (hook === 'patient-view') {
652712
return {
@@ -665,7 +725,8 @@ export class CdsService {
665725
medications: medicationsBundle,
666726
allergies: allergiesBundle
667727
}
668-
}
728+
},
729+
legacyRequest: createLegacyCompatibilityRequest(medicationsBundle)
669730
};
670731
}
671732

@@ -696,7 +757,8 @@ export class CdsService {
696757
patient,
697758
medications: medicationsBundle
698759
}
699-
}
760+
},
761+
legacyRequest: createLegacyCompatibilityRequest(medicationsBundle)
700762
};
701763
}
702764

@@ -723,7 +785,8 @@ export class CdsService {
723785
patient,
724786
medications: medicationsBundle
725787
}
726-
}
788+
},
789+
legacyRequest: createLegacyCompatibilityRequest(medicationsBundle)
727790
};
728791
}
729792

@@ -758,22 +821,7 @@ export class CdsService {
758821
allergies: allergiesBundle
759822
}
760823
},
761-
legacyRequest: {
762-
hook: 'order-page',
763-
hookInstance,
764-
fhirServer: this.fhirBaseUrl,
765-
context: {
766-
patientId: context.patient.id,
767-
encounterId,
768-
userId
769-
},
770-
prefetch: {
771-
patient,
772-
conditions: conditionsBundle,
773-
draftMedicationRequests: draftOrders,
774-
allergies: allergiesBundle
775-
}
776-
}
824+
legacyRequest: createLegacyCompatibilityRequest(draftOrders)
777825
};
778826
}
779827

@@ -804,7 +852,13 @@ export class CdsService {
804852
medications: medicationsBundle,
805853
allergies: allergiesBundle
806854
}
807-
}
855+
},
856+
legacyRequest: createLegacyCompatibilityRequest(
857+
this.createMedicationBundle(
858+
context.patient.id,
859+
this.convertMedicationsToCdsFormat(draftMedications, encounterId)
860+
)
861+
)
808862
};
809863
}
810864

0 commit comments

Comments
 (0)