Skip to content

Commit e334bdd

Browse files
committed
Add IPS import provenance and centralize patient resource handling
1 parent f7761b8 commit e334bdd

11 files changed

Lines changed: 795 additions & 286 deletions

src/app/benefits-demo/fhir-data/fhir-data.component.ts

Lines changed: 100 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -5,41 +5,39 @@ import { PatientService } from '../../services/patient.service';
55
import { saveAs } from 'file-saver';
66
import { FhirService } from '../../services/fhir.service';
77
import { IpsService } from '../../services/ips.service';
8+
import { FLAT_PATIENT_RESOURCE_CATALOG, FlatPatientResourceType } from '../../services/patient-resource-catalog';
89
import { Subscription, firstValueFrom } from 'rxjs';
910
import type {
1011
AllergyIntolerance,
12+
BodyStructure,
1113
Condition,
1214
DeathRecord,
1315
Encounter,
1416
FhirObservation,
1517
LaboratoryOrderGroup,
1618
MedicationStatement,
1719
Patient,
20+
Provenance,
1821
Procedure,
1922
QuestionnaireResponse,
2023
ServiceRequest
2124
} from '../../model';
2225

2326
type SupportedResourceType =
2427
| 'Patient'
25-
| 'Condition'
26-
| 'Procedure'
27-
| 'MedicationStatement'
28-
| 'AllergyIntolerance'
29-
| 'Observation'
30-
| 'Encounter'
31-
| 'QuestionnaireResponse'
32-
| 'ServiceRequest'
28+
| FlatPatientResourceType
3329
| 'Bundle';
3430

3531
type FhirBundleResource = LaboratoryOrderGroup['fhirBundle'] | DeathRecord;
3632

3733
type SupportedResource =
3834
| Patient
35+
| BodyStructure
3936
| Condition
4037
| Procedure
4138
| MedicationStatement
4239
| AllergyIntolerance
40+
| Provenance
4341
| FhirObservation
4442
| Encounter
4543
| QuestionnaireResponse
@@ -337,71 +335,27 @@ export class FhirDataComponent implements OnChanges, OnDestroy {
337335
const patientId = this.patient.id;
338336
const freshPatient = this.patientService.getPatientById(patientId) || this.patient;
339337
const deathRecordBundle = this.patientService.getPatientDeathRecord(patientId);
340-
const laboratoryBundles = this.patientService.getPatientLabOrders(patientId);
341-
342338
const groups: ResourceGroup[] = [
343339
{
344340
resourceType: 'Patient',
345341
title: 'Patient',
346342
icon: 'person',
347343
items: [this.toPatientItem(freshPatient)]
348344
},
349-
{
350-
resourceType: 'Condition',
351-
title: 'Condition',
352-
icon: 'stethoscope',
353-
iconFontSet: 'material-symbols-outlined',
354-
items: this.patientService.getPatientConditions(patientId).map(resource => this.toConditionItem(resource))
355-
},
356-
{
357-
resourceType: 'Procedure',
358-
title: 'Procedure',
359-
icon: 'healing',
360-
items: this.patientService.getPatientProcedures(patientId).map(resource => this.toProcedureItem(resource))
361-
},
362-
{
363-
resourceType: 'MedicationStatement',
364-
title: 'MedicationStatement',
365-
icon: 'medication',
366-
items: this.patientService.getPatientMedications(patientId).map(resource => this.toMedicationItem(resource))
367-
},
368-
{
369-
resourceType: 'AllergyIntolerance',
370-
title: 'AllergyIntolerance',
371-
icon: 'warning',
372-
items: this.patientService.getPatientAllergies(patientId).map(resource => this.toAllergyItem(resource))
373-
},
374-
{
375-
resourceType: 'Observation',
376-
title: 'Observation',
377-
icon: 'monitor_heart',
378-
items: this.patientService.getPatientObservations(patientId).map(resource => this.toObservationItem(resource))
379-
},
380-
{
381-
resourceType: 'Encounter',
382-
title: 'Encounter',
383-
icon: 'event_note',
384-
items: this.patientService.getPatientEncounters(patientId).map(resource => this.toEncounterItem(resource))
385-
},
386-
{
387-
resourceType: 'QuestionnaireResponse',
388-
title: 'QuestionnaireResponse',
389-
icon: 'assignment',
390-
items: this.patientService.getPatientQuestionnaireResponses(patientId).map(resource => this.toQuestionnaireItem(resource))
391-
},
392-
{
393-
resourceType: 'ServiceRequest',
394-
title: 'ServiceRequest',
395-
icon: 'biotech',
396-
items: laboratoryBundles
397-
.flatMap(labOrder => labOrder.serviceRequests.map(resource => this.toServiceRequestItem(resource, labOrder)))
398-
},
345+
...FLAT_PATIENT_RESOURCE_CATALOG.map((entry) => ({
346+
resourceType: entry.resourceType,
347+
title: entry.title,
348+
icon: entry.icon,
349+
iconFontSet: entry.iconFontSet,
350+
items: this.getFlatResources(patientId, entry.patientServiceGetter)
351+
.map((resource) => this.toResourceListItem(entry.resourceType, resource))
352+
})),
399353
{
400354
resourceType: 'Bundle',
401355
title: 'Bundle',
402356
icon: 'folder_zip',
403357
items: [
404-
...laboratoryBundles.map(resource => this.toBundleItem(resource)),
358+
...this.patientService.getPatientLabOrders(patientId).map(resource => this.toBundleItem(resource)),
405359
...(deathRecordBundle ? [this.toDeathCertificateBundleItem(deathRecordBundle)] : []),
406360
...(this.localIpsItem && this.localIpsItem.id === `${patientId}-local-ips` ? [this.localIpsItem] : []),
407361
...(this.serverSummaryItem && this.serverSummaryItem.id === `${patientId}-$summary` ? [this.serverSummaryItem] : [])
@@ -485,6 +439,50 @@ export class FhirDataComponent implements OnChanges, OnDestroy {
485439
};
486440
}
487441

442+
private getFlatResources(patientId: string, getterName: string): any[] {
443+
const getter = (this.patientService as any)[getterName];
444+
return typeof getter === 'function' ? getter.call(this.patientService, patientId) : [];
445+
}
446+
447+
private toResourceListItem(resourceType: FlatPatientResourceType, resource: any): ResourceListItem {
448+
switch (resourceType) {
449+
case 'BodyStructure':
450+
return this.toBodyStructureItem(resource as BodyStructure);
451+
case 'Condition':
452+
return this.toConditionItem(resource as Condition);
453+
case 'Procedure':
454+
return this.toProcedureItem(resource as Procedure);
455+
case 'MedicationStatement':
456+
return this.toMedicationItem(resource as MedicationStatement);
457+
case 'AllergyIntolerance':
458+
return this.toAllergyItem(resource as AllergyIntolerance);
459+
case 'Provenance':
460+
return this.toProvenanceItem(resource as Provenance);
461+
case 'Observation':
462+
return this.toObservationItem(resource as FhirObservation);
463+
case 'Encounter':
464+
return this.toEncounterItem(resource as Encounter);
465+
case 'QuestionnaireResponse':
466+
return this.toQuestionnaireItem(resource as QuestionnaireResponse);
467+
case 'ServiceRequest':
468+
return this.toServiceRequestItem(resource as ServiceRequest);
469+
}
470+
}
471+
472+
private toBodyStructureItem(resource: BodyStructure): ResourceListItem {
473+
const label = resource.includedStructure?.[0]?.structure?.text
474+
|| resource.includedStructure?.[0]?.structure?.coding?.[0]?.display
475+
|| resource.note?.[0]?.text
476+
|| resource.id;
477+
return {
478+
resourceType: 'BodyStructure',
479+
id: resource.id,
480+
label,
481+
subtitle: 'Body structure resource',
482+
resource
483+
};
484+
}
485+
488486
private toConditionItem(resource: Condition): ResourceListItem {
489487
const label = resource.code?.text || resource.code?.coding?.[0]?.display || resource.id;
490488
return {
@@ -544,6 +542,23 @@ export class FhirDataComponent implements OnChanges, OnDestroy {
544542
};
545543
}
546544

545+
private toProvenanceItem(resource: Provenance): ResourceListItem {
546+
const targetLabel = resource.target?.find(target => !target.reference.startsWith('Patient/'))?.display
547+
|| resource.target?.find(target => !target.reference.startsWith('Patient/'))?.reference
548+
|| resource.id;
549+
const sourceLabel = resource.entity?.[0]?.what?.display
550+
|| resource.entity?.[0]?.what?.identifier?.value
551+
|| 'IPS import';
552+
553+
return {
554+
resourceType: 'Provenance',
555+
id: resource.id,
556+
label: `Import provenance for ${targetLabel}`,
557+
subtitle: `${sourceLabel}${this.buildDateSubtitle(resource.recorded)}`,
558+
resource
559+
};
560+
}
561+
547562
private toEncounterItem(resource: Encounter): ResourceListItem {
548563
const label = resource.type?.[0]?.text || resource.type?.[0]?.coding?.[0]?.display || resource.id;
549564
const dateValue = resource.period?.start;
@@ -597,17 +612,13 @@ export class FhirDataComponent implements OnChanges, OnDestroy {
597612
};
598613
}
599614

600-
private toServiceRequestItem(resource: ServiceRequest, labOrder: LaboratoryOrderGroup): ResourceListItem {
615+
private toServiceRequestItem(resource: ServiceRequest): ResourceListItem {
601616
const label = resource.code?.text || resource.code?.coding?.[0]?.display || resource.id;
602-
const bundleLabel = labOrder.serviceRequests.length === 1
603-
? 'From laboratory order bundle'
604-
: `From laboratory order bundle (${labOrder.serviceRequests.length} determinations)`;
605-
606617
return {
607618
resourceType: 'ServiceRequest',
608619
id: resource.id,
609620
label,
610-
subtitle: `${bundleLabel}${this.buildDateSubtitle(resource.authoredOn || labOrder.createdAt)}`,
621+
subtitle: this.buildDateSubtitle(resource.authoredOn || resource.occurrenceDateTime),
611622
resource
612623
};
613624
}
@@ -643,15 +654,10 @@ export class FhirDataComponent implements OnChanges, OnDestroy {
643654
throw new Error('No patient selected');
644655
}
645656

646-
const conditions = this.patientService.getPatientConditions(patientId);
647-
const procedures = this.patientService.getPatientProcedures(patientId);
648-
const medications = this.patientService.getPatientMedications(patientId);
649-
const allergies = this.patientService.getPatientAllergies(patientId);
650-
const observations = this.patientService.getPatientObservations(patientId);
651-
const encounters = this.patientService.getPatientEncounters(patientId);
652-
const questionnaireResponses = this.patientService.getPatientQuestionnaireResponses(patientId);
653-
const serviceRequests = this.patientService.getPatientLabOrders(patientId)
654-
.flatMap((labOrder) => labOrder.serviceRequests);
657+
const flatResourceCollections = FLAT_PATIENT_RESOURCE_CATALOG.map((entry) => ({
658+
entry,
659+
resources: this.getFlatResources(patientId, entry.patientServiceGetter)
660+
}));
655661
const bundles = [
656662
...this.patientService.getPatientLabOrders(patientId).map((labOrder) => labOrder.fhirBundle),
657663
...(this.patientService.getPatientDeathRecord(patientId) ? [this.patientService.getPatientDeathRecord(patientId)] : [])
@@ -662,25 +668,27 @@ export class FhirDataComponent implements OnChanges, OnDestroy {
662668
[`Patient/${patient.id}`, patientFullUrl]
663669
]);
664670

665-
conditions.forEach((resource) => referenceMap.set(`Condition/${resource.id}`, this.createTransactionFullUrl(`condition-${resource.id}`)));
666-
encounters.forEach((resource) => referenceMap.set(`Encounter/${resource.id}`, this.createTransactionFullUrl(`encounter-${resource.id}`)));
667-
procedures.forEach((resource) => referenceMap.set(`Procedure/${resource.id}`, this.createTransactionFullUrl(`procedure-${resource.id}`)));
668-
medications.forEach((resource) => referenceMap.set(`MedicationStatement/${resource.id}`, this.createTransactionFullUrl(`medication-${resource.id}`)));
669-
allergies.forEach((resource) => referenceMap.set(`AllergyIntolerance/${resource.id}`, this.createTransactionFullUrl(`allergy-${resource.id}`)));
670-
observations.forEach((resource) => referenceMap.set(`Observation/${resource.id}`, this.createTransactionFullUrl(`observation-${resource.id}`)));
671-
questionnaireResponses.forEach((resource) => referenceMap.set(`QuestionnaireResponse/${resource.id}`, this.createTransactionFullUrl(`questionnaire-${resource.id}`)));
672-
serviceRequests.forEach((resource) => referenceMap.set(`ServiceRequest/${resource.id}`, this.createTransactionFullUrl(`service-request-${resource.id}`)));
671+
flatResourceCollections.forEach(({ entry, resources }) => {
672+
resources.forEach((resource: any) => {
673+
referenceMap.set(
674+
`${entry.resourceType}/${resource.id}`,
675+
this.createTransactionFullUrl(`${entry.exportFullUrlPrefix}-${resource.id}`)
676+
);
677+
});
678+
});
673679

674680
const entries = [
675681
this.createExportTransactionEntry(patient, 'Patient', patientFullUrl, referenceMap),
676-
...conditions.map((resource) => this.createExportTransactionEntry(resource, 'Condition', referenceMap.get(`Condition/${resource.id}`)!, referenceMap)),
677-
...procedures.map((resource) => this.createExportTransactionEntry(resource, 'Procedure', referenceMap.get(`Procedure/${resource.id}`)!, referenceMap)),
678-
...medications.map((resource) => this.createExportTransactionEntry(resource, 'MedicationStatement', referenceMap.get(`MedicationStatement/${resource.id}`)!, referenceMap)),
679-
...allergies.map((resource) => this.createExportTransactionEntry(resource, 'AllergyIntolerance', referenceMap.get(`AllergyIntolerance/${resource.id}`)!, referenceMap)),
680-
...observations.map((resource) => this.createExportTransactionEntry(resource, 'Observation', referenceMap.get(`Observation/${resource.id}`)!, referenceMap)),
681-
...encounters.map((resource) => this.createExportTransactionEntry(resource, 'Encounter', referenceMap.get(`Encounter/${resource.id}`)!, referenceMap)),
682-
...questionnaireResponses.map((resource) => this.createExportTransactionEntry(resource, 'QuestionnaireResponse', referenceMap.get(`QuestionnaireResponse/${resource.id}`)!, referenceMap)),
683-
...serviceRequests.map((resource) => this.createExportTransactionEntry(resource, 'ServiceRequest', referenceMap.get(`ServiceRequest/${resource.id}`)!, referenceMap)),
682+
...flatResourceCollections.flatMap(({ entry, resources }) =>
683+
resources.map((resource: any) =>
684+
this.createExportTransactionEntry(
685+
resource,
686+
entry.resourceType,
687+
referenceMap.get(`${entry.resourceType}/${resource.id}`)!,
688+
referenceMap
689+
)
690+
)
691+
),
684692
...bundles.map((resource: any, index: number) => this.createExportTransactionEntry(resource, 'Bundle', this.createTransactionFullUrl(`bundle-${index + 1}`), referenceMap))
685693
];
686694

0 commit comments

Comments
 (0)