From 4642223657dc8fc100d18a2415748de07a08e080 Mon Sep 17 00:00:00 2001 From: David Radcliffe Date: Tue, 13 Aug 2024 00:42:59 -0400 Subject: [PATCH] patient resource list and card (#501) --- .../components/fhir-card/fhir-card.module.ts | 3 + .../fhir-card/fhir-card.component.ts | 4 + .../immunization.component.spec.ts | 4 +- .../immunization/immunization.component.ts | 18 +- .../resources/patient/patient.component.html | 73 ++++++++ .../resources/patient/patient.component.scss | 28 ++++ .../patient/patient.component.spec.ts | 28 ++++ .../resources/patient/patient.component.ts | 31 ++++ .../datatable-generic-resource.component.html | 2 +- .../datatable-patient.component.ts | 15 ++ .../datatable-generic-resource/utils.ts | 25 ++- .../fhir-datatable/fhir-datatable.module.ts | 6 +- .../fhir-datatable.component.ts | 4 + ...rm-request-health-system.component.spec.ts | 15 +- .../form-request-health-system.component.ts | 9 +- frontend/src/custom.scss | 3 + .../models/resources/patient-model.spec.ts | 141 ++++++++++++++++ .../src/lib/models/resources/patient-model.ts | 157 ++++++++++++++---- 18 files changed, 507 insertions(+), 59 deletions(-) create mode 100644 frontend/src/app/components/fhir-card/resources/patient/patient.component.html create mode 100644 frontend/src/app/components/fhir-card/resources/patient/patient.component.scss create mode 100644 frontend/src/app/components/fhir-card/resources/patient/patient.component.spec.ts create mode 100644 frontend/src/app/components/fhir-card/resources/patient/patient.component.ts create mode 100644 frontend/src/app/components/fhir-datatable/datatable-generic-resource/datatable-patient.component.ts diff --git a/frontend/src/app/components/fhir-card/fhir-card.module.ts b/frontend/src/app/components/fhir-card/fhir-card.module.ts index 257a2c64..aeb45bde 100644 --- a/frontend/src/app/components/fhir-card/fhir-card.module.ts +++ b/frontend/src/app/components/fhir-card/fhir-card.module.ts @@ -22,6 +22,7 @@ import {MedicationComponent} from './resources/medication/medication.component'; import {MedicationRequestComponent} from './resources/medication-request/medication-request.component'; import {ObservationComponent} from './resources/observation/observation.component'; import {OrganizationComponent} from './resources/organization/organization.component'; +import {PatientComponent} from './resources/patient/patient.component'; import {PractitionerComponent} from './resources/practitioner/practitioner.component'; import {ProcedureComponent} from './resources/procedure/procedure.component'; import {FhirCardComponent} from './fhir-card/fhir-card.component'; @@ -67,6 +68,7 @@ import { ObservationVisualizationComponent } from './common/observation-visualiz MedicationRequestComponent, ObservationComponent, OrganizationComponent, + PatientComponent, PractitionerComponent, ProcedureComponent, @@ -108,6 +110,7 @@ import { ObservationVisualizationComponent } from './common/observation-visualiz MedicationRequestComponent, ObservationComponent, OrganizationComponent, + PatientComponent, PractitionerComponent, ProcedureComponent, diff --git a/frontend/src/app/components/fhir-card/fhir-card/fhir-card.component.ts b/frontend/src/app/components/fhir-card/fhir-card/fhir-card.component.ts index a753d602..bda48df1 100644 --- a/frontend/src/app/components/fhir-card/fhir-card/fhir-card.component.ts +++ b/frontend/src/app/components/fhir-card/fhir-card/fhir-card.component.ts @@ -29,6 +29,7 @@ import {LocationComponent} from '../resources/location/location.component'; import {OrganizationComponent} from '../resources/organization/organization.component'; import {ObservationComponent} from '../resources/observation/observation.component'; import {EncounterComponent} from '../resources/encounter/encounter.component'; +import {PatientComponent} from '../resources/patient/patient.component'; @Component({ @@ -152,6 +153,9 @@ export class FhirCardComponent implements OnInit, OnChanges { case "Organization": { return OrganizationComponent; } + case "Patient": { + return PatientComponent; + } case "Procedure": { return ProcedureComponent; } diff --git a/frontend/src/app/components/fhir-card/resources/immunization/immunization.component.spec.ts b/frontend/src/app/components/fhir-card/resources/immunization/immunization.component.spec.ts index b4a02742..a059f7a2 100644 --- a/frontend/src/app/components/fhir-card/resources/immunization/immunization.component.spec.ts +++ b/frontend/src/app/components/fhir-card/resources/immunization/immunization.component.spec.ts @@ -1,8 +1,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap'; +import { RouterTestingModule } from '@angular/router/testing'; import { ImmunizationComponent } from './immunization.component'; -import {NgbCollapseModule} from '@ng-bootstrap/ng-bootstrap'; -import {RouterTestingModule} from '@angular/router/testing'; describe('ImmunizationComponent', () => { let component: ImmunizationComponent; diff --git a/frontend/src/app/components/fhir-card/resources/immunization/immunization.component.ts b/frontend/src/app/components/fhir-card/resources/immunization/immunization.component.ts index 916147a1..f372c154 100644 --- a/frontend/src/app/components/fhir-card/resources/immunization/immunization.component.ts +++ b/frontend/src/app/components/fhir-card/resources/immunization/immunization.component.ts @@ -1,13 +1,13 @@ -import {ChangeDetectorRef, Component, Input, OnInit} from '@angular/core'; -import {FhirCardComponentInterface} from '../../fhir-card/fhir-card-component-interface'; -import {Router, RouterModule} from '@angular/router'; -import {ImmunizationModel} from '../../../../../lib/models/resources/immunization-model'; -import {TableRowItem, TableRowItemDataType} from '../../common/table/table-row-item'; +import { CommonModule } from "@angular/common"; +import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; +import { Router, RouterModule } from '@angular/router'; +import { NgbCollapseModule } from "@ng-bootstrap/ng-bootstrap"; import * as _ from "lodash"; -import {NgbCollapseModule} from "@ng-bootstrap/ng-bootstrap"; -import {CommonModule} from "@angular/common"; -import {BadgeComponent} from "../../common/badge/badge.component"; -import {TableComponent} from "../../common/table/table.component"; +import { ImmunizationModel } from '../../../../../lib/models/resources/immunization-model'; +import { BadgeComponent } from "../../common/badge/badge.component"; +import { TableRowItem, TableRowItemDataType } from '../../common/table/table-row-item'; +import { TableComponent } from "../../common/table/table.component"; +import { FhirCardComponentInterface } from '../../fhir-card/fhir-card-component-interface'; @Component({ standalone: true, diff --git a/frontend/src/app/components/fhir-card/resources/patient/patient.component.html b/frontend/src/app/components/fhir-card/resources/patient/patient.component.html new file mode 100644 index 00000000..6528e597 --- /dev/null +++ b/frontend/src/app/components/fhir-card/resources/patient/patient.component.html @@ -0,0 +1,73 @@ +
+
+

{{ displayModel.patient_name }}

+
+
+
+ +
+

Basic Information

+
    +
  • Date of Birth: {{ displayModel.patient_birthdate | date:'mediumDate' }}
  • +
  • Age: {{ displayModel.patient_age }}
  • +
  • Gender: {{ displayModel.patient_gender | titlecase }}
  • +
  • Birth Sex: {{ displayModel.birth_sex }}
  • +
  • Marital Status: {{ displayModel.marital_status }}
  • +
  • Race: {{ displayModel.race }}
  • +
  • Ethnicity: {{ displayModel.ethnicity }}
  • +
+
+ + +
+

Contact Information

+
    +
  • Address: +
    +
    {{line}}
    +
    +
  • +
  • Phone{{ displayModel.patient_phones.length > 0 ? '' : 's' }}: +
      +
    • {{ phone.value }} ({{ phone.use }})
    • +
    +
  • +
  • Language{{ displayModel.communication_languages.length > 0 ? '' : 's' }}: + + {{ language.display }}{{last ? '' : ', '}} + +
  • +
+
+ + +
+

Additional Information

+
    +
  • Mother's Maiden Name: {{ displayModel.mothers_maiden_name }}
  • +
  • Birth Place: {{ displayModel.birth_place }}
  • +
  • Multiple Birth: {{ displayModel.multiple_birth ? 'Yes' : 'No' }}
  • +
+
+ + +
+

Identifiers

+
    +
  • Medical Record Number: {{ displayModel.mrn }}
  • +
  • SSN: {{ displayModel.ssn }}
  • +
+ +

Other Identifiers:

+
    +
  • + {{ identifier.type }}: {{ identifier.value }} +
  • +
+
+
+ + +
+
+
diff --git a/frontend/src/app/components/fhir-card/resources/patient/patient.component.scss b/frontend/src/app/components/fhir-card/resources/patient/patient.component.scss new file mode 100644 index 00000000..f2b42c42 --- /dev/null +++ b/frontend/src/app/components/fhir-card/resources/patient/patient.component.scss @@ -0,0 +1,28 @@ +.bg-fasten-purple { + background-color: #5b47fb; +} + +.patient-card { + border: 1px solid #ccc; + border-radius: 4px; + padding: 20px; + margin-bottom: 20px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + + h2 { + margin-top: 0; + margin-bottom: 15px; + font-size: 1.5rem; + } + + .patient-info { + p { + margin: 5px 0; + } + + strong { + font-weight: bold; + margin-right: 5px; + } + } +} diff --git a/frontend/src/app/components/fhir-card/resources/patient/patient.component.spec.ts b/frontend/src/app/components/fhir-card/resources/patient/patient.component.spec.ts new file mode 100644 index 00000000..2aa9c306 --- /dev/null +++ b/frontend/src/app/components/fhir-card/resources/patient/patient.component.spec.ts @@ -0,0 +1,28 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap'; + +import { PatientModel } from 'src/lib/models/resources/patient-model'; +import { PatientComponent } from './patient.component'; + +describe('PatientComponent', () => { + let component: PatientComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ PatientComponent, NgbCollapseModule, RouterTestingModule ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(PatientComponent); + component = fixture.componentInstance; + component.displayModel = new PatientModel({}); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + +}); diff --git a/frontend/src/app/components/fhir-card/resources/patient/patient.component.ts b/frontend/src/app/components/fhir-card/resources/patient/patient.component.ts new file mode 100644 index 00000000..6f1eb3ff --- /dev/null +++ b/frontend/src/app/components/fhir-card/resources/patient/patient.component.ts @@ -0,0 +1,31 @@ +import { CommonModule } from '@angular/common'; +import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; +import { Router, RouterModule } from '@angular/router'; +import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap'; +import { PatientModel } from '../../../../../lib/models/resources/patient-model'; +import { BadgeComponent } from '../../common/badge/badge.component'; +import { TableComponent } from '../../common/table/table.component'; +import { FhirCardComponentInterface } from '../../fhir-card/fhir-card-component-interface'; + +@Component({ + standalone: true, + imports: [NgbCollapseModule, CommonModule, BadgeComponent, TableComponent, RouterModule], + selector: 'fhir-patient', + templateUrl: './patient.component.html', + styleUrls: ['./patient.component.scss'] +}) +export class PatientComponent implements OnInit, FhirCardComponentInterface { + @Input() displayModel: PatientModel; + @Input() showDetails: boolean = true; + @Input() isCollapsed: boolean = false; + + constructor(public changeRef: ChangeDetectorRef, public router: Router) { } + + ngOnInit(): void { + } + + markForCheck(){ + this.changeRef.markForCheck() + } + +} diff --git a/frontend/src/app/components/fhir-datatable/datatable-generic-resource/datatable-generic-resource.component.html b/frontend/src/app/components/fhir-datatable/datatable-generic-resource/datatable-generic-resource.component.html index b1b50d1e..628d8110 100644 --- a/frontend/src/app/components/fhir-datatable/datatable-generic-resource/datatable-generic-resource.component.html +++ b/frontend/src/app/components/fhir-datatable/datatable-generic-resource/datatable-generic-resource.component.html @@ -1,7 +1,7 @@
p.name?.[0] }, + { title: 'DOB', versions: '*', getter: p => p.birthDate, format: 'date' }, + ] +} diff --git a/frontend/src/app/components/fhir-datatable/datatable-generic-resource/utils.ts b/frontend/src/app/components/fhir-datatable/datatable-generic-resource/utils.ts index 23b486d0..26a5421e 100644 --- a/frontend/src/app/components/fhir-datatable/datatable-generic-resource/utils.ts +++ b/frontend/src/app/components/fhir-datatable/datatable-generic-resource/utils.ts @@ -14,6 +14,19 @@ export function getPath(obj, path = ""): string { } export const FORMATTERS = { + age: (patientDOB: number|string): number => { + if (patientDOB == null) { return NaN; } + const dob = typeof patientDOB === 'string' ? new Date(patientDOB) : new Date(patientDOB); + const today = new Date(); + let age = today.getFullYear() - dob.getFullYear(); + const monthDiff = today.getMonth() - dob.getMonth(); + + if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < dob.getDate())) { + age--; + } + + return age; + }, date: (str) => str ? moment(str).format('YYYY-MM-DD') : '', time: (str) => str ? moment(str).format('HH:mm') : '', dateTime: (str) => str ? moment(str).format('YYYY-MM-DD - h:mm a') : '', @@ -24,15 +37,13 @@ export const FORMATTERS = { if(codeableConcept.text) return codeableConcept.text return codeableConcept.coding && codeableConcept.coding[0] ? `${codeableConcept.coding[0].code}: ${codeableConcept.coding[0].display ? codeableConcept.coding[0].display : ''}` : '' }, - address: (address) => { - if(!address) return '' + address: (address): Array => { + if(!address) return [] var addressParts = [] - if(address.line) addressParts.push(address.line.join(', ')) - if(address.city) addressParts.push(address.city) - if(address.state) addressParts.push(address.state) - if(address.postalCode) addressParts.push(address.postalCode) + if(address.line) addressParts.push(...address.line) + addressParts.push(`${address.city}, ${address.state} ${address.postalCode}`) if(address.country) addressParts.push(address.country) - return addressParts.join(', ') + return addressParts }, humanName: (humanName) => { if(!humanName) return '' diff --git a/frontend/src/app/components/fhir-datatable/fhir-datatable.module.ts b/frontend/src/app/components/fhir-datatable/fhir-datatable.module.ts index 7953a01a..42cdd144 100644 --- a/frontend/src/app/components/fhir-datatable/fhir-datatable.module.ts +++ b/frontend/src/app/components/fhir-datatable/fhir-datatable.module.ts @@ -25,7 +25,7 @@ import {DatatableMedicationRequestComponent} from './datatable-generic-resource/ import {DatatableNutritionOrderComponent} from './datatable-generic-resource/datatable-nutrition-order.component'; import {DatatableObservationComponent} from './datatable-generic-resource/datatable-observation.component'; import {DatatableOrganizationComponent} from './datatable-generic-resource/datatable-organization.component'; -import {ListPatientComponent} from './list-patient/list-patient.component'; +import {DatatablePatientComponent} from './datatable-generic-resource/datatable-patient.component'; import {DatatablePractitionerComponent} from './datatable-generic-resource/datatable-practitioner.component'; import {DatatableProcedureComponent} from './datatable-generic-resource/datatable-procedure.component'; import {DatatableServiceRequestComponent} from './datatable-generic-resource/datatable-service-request.component'; @@ -65,7 +65,7 @@ import {NgxDatatableModule} from '@swimlane/ngx-datatable'; DatatableNutritionOrderComponent, DatatableObservationComponent, DatatableOrganizationComponent, - ListPatientComponent, + DatatablePatientComponent, DatatablePractitionerComponent, DatatableProcedureComponent, DatatableServiceRequestComponent, @@ -99,7 +99,7 @@ import {NgxDatatableModule} from '@swimlane/ngx-datatable'; DatatableNutritionOrderComponent, DatatableObservationComponent, DatatableOrganizationComponent, - ListPatientComponent, + DatatablePatientComponent, DatatablePractitionerComponent, DatatableProcedureComponent, DatatableServiceRequestComponent, diff --git a/frontend/src/app/components/fhir-datatable/fhir-datatable/fhir-datatable.component.ts b/frontend/src/app/components/fhir-datatable/fhir-datatable/fhir-datatable.component.ts index de931a5d..70ec5f49 100644 --- a/frontend/src/app/components/fhir-datatable/fhir-datatable/fhir-datatable.component.ts +++ b/frontend/src/app/components/fhir-datatable/fhir-datatable/fhir-datatable.component.ts @@ -27,6 +27,7 @@ import {DatatableMedicationRequestComponent} from '../datatable-generic-resource import {DatatableNutritionOrderComponent} from '../datatable-generic-resource/datatable-nutrition-order.component'; import {DatatableObservationComponent} from '../datatable-generic-resource/datatable-observation.component'; import {DatatableOrganizationComponent} from '../datatable-generic-resource/datatable-organization.component'; +import {DatatablePatientComponent} from '../datatable-generic-resource/datatable-patient.component'; import {DatatablePractitionerComponent} from '../datatable-generic-resource/datatable-practitioner.component'; import {DatatableProcedureComponent} from '../datatable-generic-resource/datatable-procedure.component'; import {DatatableServiceRequestComponent} from '../datatable-generic-resource/datatable-service-request.component'; @@ -162,6 +163,9 @@ export class FhirDatatableComponent implements OnInit, OnChanges { case "Organization": { return DatatableOrganizationComponent; } + case "Patient": { + return DatatablePatientComponent; + } case "Practitioner": { return DatatablePractitionerComponent; } diff --git a/frontend/src/app/components/form-request-health-system/form-request-health-system.component.spec.ts b/frontend/src/app/components/form-request-health-system/form-request-health-system.component.spec.ts index 596942e5..ede05e7f 100644 --- a/frontend/src/app/components/form-request-health-system/form-request-health-system.component.spec.ts +++ b/frontend/src/app/components/form-request-health-system/form-request-health-system.component.spec.ts @@ -1,4 +1,9 @@ +import { HttpClient } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormsModule } from '@angular/forms'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { HTTP_CLIENT_TOKEN } from '../../dependency-injection'; import { FormRequestHealthSystemComponent } from './form-request-health-system.component'; @@ -8,7 +13,15 @@ describe('FormRequestHealthSystemComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ FormRequestHealthSystemComponent ] + declarations: [ FormRequestHealthSystemComponent ], + imports: [HttpClientTestingModule, FormsModule], + providers: [ + NgbActiveModal, + { + provide: HTTP_CLIENT_TOKEN, + useClass: HttpClient, + }, + ] }) .compileComponents(); diff --git a/frontend/src/app/components/form-request-health-system/form-request-health-system.component.ts b/frontend/src/app/components/form-request-health-system/form-request-health-system.component.ts index 6dc49ee3..805d943f 100644 --- a/frontend/src/app/components/form-request-health-system/form-request-health-system.component.ts +++ b/frontend/src/app/components/form-request-health-system/form-request-health-system.component.ts @@ -1,10 +1,7 @@ import { Component, OnInit } from '@angular/core'; -import {NgbActiveModal, NgbModal} from '@ng-bootstrap/ng-bootstrap'; -import {SupportRequest} from '../../models/fasten/support-request'; -import {FormRequestHealthSystem} from '../../models/fasten/form-request-health-system'; -import {environment} from '../../../environments/environment'; -import {versionInfo} from '../../../environments/versions'; -import {FastenApiService} from '../../services/fasten-api.service'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { FormRequestHealthSystem } from '../../models/fasten/form-request-health-system'; +import { FastenApiService } from '../../services/fasten-api.service'; @Component({ selector: 'app-form-request-health-system', diff --git a/frontend/src/custom.scss b/frontend/src/custom.scss index f5f2e508..9f237790 100644 --- a/frontend/src/custom.scss +++ b/frontend/src/custom.scss @@ -264,6 +264,9 @@ app-medical-sources-filter > .az-content-left-components:hover{ background-color: $indigo !important; color: #fff; } +.fhir-resource-datatable .datatable-body-row { + cursor: pointer; +} // Callouts diff --git a/frontend/src/lib/models/resources/patient-model.spec.ts b/frontend/src/lib/models/resources/patient-model.spec.ts index 033655c4..0a1baada 100644 --- a/frontend/src/lib/models/resources/patient-model.spec.ts +++ b/frontend/src/lib/models/resources/patient-model.spec.ts @@ -1,7 +1,148 @@ import { PatientModel } from './patient-model'; describe('PatientModel', () => { + let patientModel: PatientModel; + let mockFhirResource: any; + + beforeEach(() => { + mockFhirResource = { + id: '123', + name: [{ given: ['John'], family: 'Doe' }], + birthDate: '1990-01-01', + gender: 'male', + telecom: [ + { system: 'phone', value: '123-456-7890', use: 'home' }, + { system: 'email', value: 'john@example.com', use: 'work' } + ], + communication: [ + { language: { coding: [{ system: 'urn:ietf:bcp:47', code: 'en', display: 'English' }] } } + ], + maritalStatus: { text: 'Married' }, + extension: [ + { + url: 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex', + valueCode: 'M' + }, + { + url: 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-race', + extension: [{ url: 'text', valueString: 'White' }] + }, + { + url: 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity', + extension: [{ url: 'text', valueString: 'Not Hispanic or Latino' }] + }, + { + url: 'http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName', + valueString: 'Smith' + }, + { + url: 'http://hl7.org/fhir/StructureDefinition/patient-birthPlace', + valueAddress: { city: 'New York', state: 'NY', country: 'USA' } + } + ], + identifier: [ + { system: 'http://hl7.org/fhir/sid/us-ssn', value: '123-45-6789' }, + { type: { coding: [{ code: 'MR' }] }, value: 'MRN12345' }, + { type: { text: 'Driver\'s License', coding: [{ display: 'DL' }] }, system: 'urn:oid:2.16.840.1.113883.4.3.25', value: 'S99969890' } + ] + }; + patientModel = new PatientModel(mockFhirResource); + }); + it('should create an instance', () => { expect(new PatientModel({})).toBeTruthy(); }); + + it('should return the correct id', () => { + expect(patientModel.id).toBe('123'); + }); + + it('should return the formatted name', () => { + expect(patientModel.patient_name).toBe('John Doe'); + }); + + it('should return the correct birth date', () => { + expect(patientModel.patient_birthdate).toBe('1990-01-01'); + }); + + // it('should return the correct age', () => { + + + // const currentDate = new Date('2022-02-01'); + // jasmine.clock().install(); + // jasmine.clock().mockDate(currentDate); + + // expect(FORMATTERS.age('1990-01-01')).toBe(32); + // expect(patientModel.patient_birthdate).toBe('1990-01-01'); + // expect(patientModel.patient_age).toBe(32); + // jasmine.clock().uninstall(); + // }); + + it('should return the correct gender', () => { + expect(patientModel.patient_gender).toBe('male'); + }); + + it('should return the correct telecom information', () => { + expect(patientModel.patient_phones).toEqual([ + { system: 'phone', value: '123-456-7890', use: 'home' }, + { system: 'email', value: 'john@example.com', use: 'work' } + ]); + }); + + it('should return the correct language information', () => { + expect(patientModel.communication_languages).toEqual([ + { system: 'urn:ietf:bcp:47', code: 'en', display: 'English' } + ]); + }); + + it('should return the correct birth sex', () => { + expect(patientModel.birth_sex).toBe('M'); + }); + + it('should return the correct marital status', () => { + expect(patientModel.marital_status).toBe('Married'); + }); + + it('should return the correct race', () => { + expect(patientModel.race).toBe('White'); + }); + + it('should return the correct ethnicity', () => { + expect(patientModel.ethnicity).toBe('Not Hispanic or Latino'); + }); + + it('should return the correct mother\'s maiden name', () => { + expect(patientModel.mothers_maiden_name).toBe('Smith'); + }); + + it('should return the correct birth place', () => { + expect(patientModel.birth_place).toBe('New York, NY, USA'); + }); + + it('should correctly parse other identifiers', () => { + expect(patientModel.identifiers).toEqual([ + { type: 'Driver\'s License', system: 'urn:oid:2.16.840.1.113883.4.3.25', value: 'S99969890' } + ]); + }); + + // it('should return the correct extensions', () => { + // const extensions = patientModel.getExtensions(mockFhirResource); + // expect(extensions).toContainEqual({ url: 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex', value: 'M' }); + // expect(extensions).toContainEqual({ url: 'http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName', value: 'Smith' }); + // }); + + it('should return the correct SSN', () => { + expect(patientModel.ssn).toBe('123-45-6789'); + }); + + it('should return the correct MRN', () => { + const mockResourceWithMRN = { + ...mockFhirResource, + extension: [ + ...mockFhirResource.extension, + { url: 'http://hl7.org/fhir/StructureDefinition/patient-mrn', valueString: 'MRN54321' } + ] + }; + expect(patientModel.getMRN(mockResourceWithMRN)).toBe('MRN54321'); + }); }); diff --git a/frontend/src/lib/models/resources/patient-model.ts b/frontend/src/lib/models/resources/patient-model.ts index dc0b934e..43b92e8e 100644 --- a/frontend/src/lib/models/resources/patient-model.ts +++ b/frontend/src/lib/models/resources/patient-model.ts @@ -1,64 +1,161 @@ -import {fhirVersions, ResourceType} from '../constants'; import * as _ from "lodash"; -import {CodableConceptModel, hasValue} from '../datatypes/codable-concept-model'; -import {ReferenceModel} from '../datatypes/reference-model'; -import {CodingModel} from '../datatypes/coding-model'; -import {FastenDisplayModel} from '../fasten/fasten-display-model'; -import {FastenOptions} from '../fasten/fasten-options'; +import { FORMATTERS } from 'src/app/components/fhir-datatable/datatable-generic-resource/utils'; +import { fhirVersions, ResourceType } from '../constants'; +import { FastenDisplayModel } from '../fasten/fasten-display-model'; +import { FastenOptions } from '../fasten/fasten-options'; export class PatientModel extends FastenDisplayModel { id: string|undefined patient_name: string|undefined patient_birthdate: string|undefined + patient_age: number|undefined patient_gender: string|undefined patient_contact: string|undefined - patient_address: string|undefined - patient_phones: string|undefined - communication_language: string|undefined + patient_address: Array|undefined + patient_phones: Array|undefined + communication_languages: Array|undefined has_communication_language: boolean|undefined has_patient_phones: boolean|undefined active: string|undefined active_status: string|undefined is_deceased: boolean|undefined deceased_date: string|undefined + birth_sex: string|undefined + marital_status: string|undefined + race: string|undefined + ethnicity: string|undefined + mothers_maiden_name: string|undefined + birth_place: string|undefined + multiple_birth: boolean|undefined + identifiers: Array|undefined + extensions: Array|undefined + ssn: string|undefined + mrn: string|undefined constructor(fhirResource: any, fhirVersion?: fhirVersions, fastenOptions?: FastenOptions) { super(fastenOptions) this.source_resource_type = ResourceType.Patient - this.id = this.getId(fhirResource); - this.patient_name = this.getNames(fhirResource); - this.patient_birthdate = this.getBirthDate(fhirResource); - this.patient_gender = this.getGender(fhirResource); + this.id = _.get(fhirResource, 'id'); + this.patient_name = FORMATTERS.humanName(_.get(fhirResource, 'name.0', null)); + this.patient_birthdate = _.get(fhirResource, 'birthDate'); + this.patient_age = FORMATTERS.age(this.patient_birthdate) + this.patient_gender = _.get(fhirResource, 'gender'); this.patient_contact = _.get(fhirResource, 'contact[0]'); - this.patient_address = _.get(fhirResource, 'address[0]'); - this.patient_phones = _.get(fhirResource, 'telecom', []).filter( - (telecom: any) => telecom.system === 'phone', - ); - let communicationLanguage = _.get(fhirResource, 'communication', []) - .filter((item: any) => Boolean(_.get(item, 'language.coding', null))) - .map((item: any) => item.language.coding); - this.communication_language = _.get(communicationLanguage, '0', []); - this.has_communication_language = !_.isEmpty(communicationLanguage); + this.patient_address = FORMATTERS.address(_.get(fhirResource, 'address[0]')); + this.patient_phones = this.getTelecom(fhirResource); + this.communication_languages = this.getCommunicationLanguages(fhirResource); + this.has_communication_language = !_.isEmpty(this.communication_languages); this.has_patient_phones = !_.isEmpty(this.patient_phones); this.active = _.get(fhirResource, 'active', false); this.active_status = this.active ? 'active' : 'inactive'; let deceasedBoolean = _.get(fhirResource, 'deceasedBoolean', false); this.deceased_date = _.get(fhirResource, 'deceasedDateTime'); this.is_deceased = deceasedBoolean || this.deceased_date; + this.birth_sex = this.getBirthSex(fhirResource); + this.marital_status = _.get(fhirResource, 'maritalStatus.text'); + this.race = this.getRace(fhirResource); + this.ethnicity = this.getEthnicity(fhirResource); + this.mothers_maiden_name = this.getMothersMaidenName(fhirResource); + this.birth_place = this.getBirthPlace(fhirResource); + this.multiple_birth = _.get(fhirResource, 'multipleBirthBoolean', false); + this.identifiers = []; + this.parseIdentifiers(fhirResource); + this.extensions = this.getExtensions(fhirResource); + this.ssn = this.ssn || this.getSSN(fhirResource); + this.mrn = this.mrn || this.getMRN(fhirResource); } - getId(fhirResource: any) { - return _.get(fhirResource, 'id'); + + getTelecom(fhirResource: any) { + return _.get(fhirResource, 'telecom', []).map((telecom: any) => ({ + system: telecom.system, + value: telecom.value, + use: telecom.use + })); } - getNames(fhirResource: any) { - return _.get(fhirResource, 'name.0', null); + + getCommunicationLanguages(fhirResource: any) { + return _.get(fhirResource, 'communication', []) + .filter((item: any) => Boolean(_.get(item, 'language.coding', null))) + .map((item: any) => item.language.coding[0]); } - getBirthDate(fhirResource: any) { - return _.get(fhirResource, 'birthDate'); + + getBirthSex(fhirResource: any) { + const extension = fhirResource.extension?.find((ext: any) => + ext.url === "http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex" + ); + return extension?.valueCode; } - getGender(fhirResource: any) { - return _.get(fhirResource, 'gender'); + + getRace(fhirResource: any) { + const extension = fhirResource.extension?.find((ext: any) => + ext.url === "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race" + ); + return extension?.extension?.find((ext: any) => ext.url === "text")?.valueString; + } + + getEthnicity(fhirResource: any) { + const extension = fhirResource.extension?.find((ext: any) => + ext.url === "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity" + ); + return extension?.extension?.find((ext: any) => ext.url === "text")?.valueString; + } + + getMothersMaidenName(fhirResource: any) { + const extension = fhirResource.extension?.find((ext: any) => + ext.url === "http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName" + ); + return extension?.valueString; + } + + getBirthPlace(fhirResource: any) { + const extension = fhirResource.extension?.find((ext: any) => + ext.url === "http://hl7.org/fhir/StructureDefinition/patient-birthPlace" + ); + if (extension?.valueAddress) { + const address = extension.valueAddress; + return `${address.city}, ${address.state}, ${address.country}`; + } + return undefined; + } + + parseIdentifiers(fhirResource: any) { + const identifiers = _.get(fhirResource, 'identifier', []); + identifiers.forEach((identifier: any) => { + if (identifier.system === "http://hl7.org/fhir/sid/us-ssn") { + this.ssn = identifier.value; + } else if (identifier.type?.coding?.some((coding: any) => coding.code === "MR")) { + this.mrn = identifier.value; + } else if (identifier.type) { + this.identifiers.push({ + type: identifier.type.text || identifier.type.coding[0].display, + system: identifier.system, + value: identifier.value + }); + } + }); + } + + getExtensions(fhirResource: any) { + return fhirResource.extension?.map((ext: any) => ({ + url: ext.url, + value: ext.valueDecimal || ext.valueString || ext.valueCode || JSON.stringify(ext.extension) + })); + } + + getSSN(fhirResource: any) { + const extension = fhirResource.extension?.find((ext: any) => + ext.url === "http://hl7.org/fhir/StructureDefinition/us-core-ssn" + ); + return extension?.valueString; + } + + getMRN(fhirResource: any) { + const extension = fhirResource.extension?.find((ext: any) => + ext.url === "http://hl7.org/fhir/StructureDefinition/patient-mrn" + ); + return extension?.valueString; } }