Adding UI for manually creating conditions (and associated resources) (#92)

This commit is contained in:
Jason Kulatunga 2023-03-05 21:46:55 -08:00 committed by GitHub
parent 3692fd462f
commit 5afb6ef659
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 3151 additions and 63 deletions

View File

@ -31,6 +31,7 @@
"@ng-bootstrap/ng-bootstrap": "10.0.0", "@ng-bootstrap/ng-bootstrap": "10.0.0",
"@panva/oauth4webapi": "1.2.0", "@panva/oauth4webapi": "1.2.0",
"@swimlane/ngx-datatable": "^20.0.0", "@swimlane/ngx-datatable": "^20.0.0",
"@types/fhir": "^0.0.35",
"asmcrypto.js": "^2.3.2", "asmcrypto.js": "^2.3.2",
"bootstrap": "^4.4.1", "bootstrap": "^4.4.1",
"chart.js": "2.9.4", "chart.js": "2.9.4",

View File

@ -12,6 +12,7 @@ import {SourceDetailComponent} from './pages/source-detail/source-detail.compone
import {PatientProfileComponent} from './pages/patient-profile/patient-profile.component'; import {PatientProfileComponent} from './pages/patient-profile/patient-profile.component';
import {MedicalHistoryComponent} from './pages/medical-history/medical-history.component'; import {MedicalHistoryComponent} from './pages/medical-history/medical-history.component';
import {ReportLabsComponent} from './pages/report-labs/report-labs.component'; import {ReportLabsComponent} from './pages/report-labs/report-labs.component';
import {ResourceCreatorComponent} from './pages/resource-creator/resource-creator.component';
const routes: Routes = [ const routes: Routes = [
@ -27,6 +28,7 @@ const routes: Routes = [
{ path: 'source/:source_id/resource/:resource_type/:resource_id', component: ResourceDetailComponent, canActivate: [ IsAuthenticatedAuthGuard] }, { path: 'source/:source_id/resource/:resource_type/:resource_id', component: ResourceDetailComponent, canActivate: [ IsAuthenticatedAuthGuard] },
{ path: 'sources', component: MedicalSourcesComponent, canActivate: [ IsAuthenticatedAuthGuard] }, { path: 'sources', component: MedicalSourcesComponent, canActivate: [ IsAuthenticatedAuthGuard] },
{ path: 'sources/callback/:source_type', component: MedicalSourcesComponent, canActivate: [ IsAuthenticatedAuthGuard] }, { path: 'sources/callback/:source_type', component: MedicalSourcesComponent, canActivate: [ IsAuthenticatedAuthGuard] },
{ path: 'resource/create', component: ResourceCreatorComponent, canActivate: [ IsAuthenticatedAuthGuard] },
{ path: 'patient-profile', component: PatientProfileComponent, canActivate: [ IsAuthenticatedAuthGuard] }, { path: 'patient-profile', component: PatientProfileComponent, canActivate: [ IsAuthenticatedAuthGuard] },

View File

@ -16,10 +16,9 @@ import { far } from '@fortawesome/free-regular-svg-icons';
import { ResourceDetailComponent } from './pages/resource-detail/resource-detail.component'; import { ResourceDetailComponent } from './pages/resource-detail/resource-detail.component';
import { AuthSignupComponent } from './pages/auth-signup/auth-signup.component'; import { AuthSignupComponent } from './pages/auth-signup/auth-signup.component';
import { AuthSigninComponent } from './pages/auth-signin/auth-signin.component'; import { AuthSigninComponent } from './pages/auth-signin/auth-signin.component';
import { FormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { NgxDropzoneModule } from 'ngx-dropzone'; import { NgxDropzoneModule } from 'ngx-dropzone';
import { IsAuthenticatedAuthGuard } from './auth-guards/is-authenticated-auth-guard'; import { IsAuthenticatedAuthGuard } from './auth-guards/is-authenticated-auth-guard';
import {FastenApiService} from './services/fasten-api.service';
import {Router} from '@angular/router'; import {Router} from '@angular/router';
import { SourceDetailComponent } from './pages/source-detail/source-detail.component'; import { SourceDetailComponent } from './pages/source-detail/source-detail.component';
import { HighlightModule, HIGHLIGHT_OPTIONS } from 'ngx-highlightjs'; import { HighlightModule, HIGHLIGHT_OPTIONS } from 'ngx-highlightjs';
@ -30,6 +29,7 @@ import { PatientProfileComponent } from './pages/patient-profile/patient-profile
import { MedicalHistoryComponent } from './pages/medical-history/medical-history.component'; import { MedicalHistoryComponent } from './pages/medical-history/medical-history.component';
import { ReportLabsComponent } from './pages/report-labs/report-labs.component'; import { ReportLabsComponent } from './pages/report-labs/report-labs.component';
import {PipesModule} from './pipes/pipes.module'; import {PipesModule} from './pipes/pipes.module';
import { ResourceCreatorComponent } from './pages/resource-creator/resource-creator.component';
@NgModule({ @NgModule({
@ -46,9 +46,11 @@ import {PipesModule} from './pipes/pipes.module';
PatientProfileComponent, PatientProfileComponent,
MedicalHistoryComponent, MedicalHistoryComponent,
ReportLabsComponent, ReportLabsComponent,
ResourceCreatorComponent,
], ],
imports: [ imports: [
FormsModule, FormsModule,
ReactiveFormsModule,
BrowserModule, BrowserModule,
FontAwesomeModule, FontAwesomeModule,
SharedModule, SharedModule,
@ -59,7 +61,7 @@ import {PipesModule} from './pipes/pipes.module';
NgxDropzoneModule, NgxDropzoneModule,
HighlightModule, HighlightModule,
MomentModule, MomentModule,
PipesModule PipesModule,
], ],
providers: [ providers: [
{ {

View File

@ -84,6 +84,9 @@ export class FhirResourceComponent implements OnInit, OnChanges {
// case "CarePlan": { // case "CarePlan": {
// return ListCarePlanComponent; // return ListCarePlanComponent;
// } // }
// case "CareTeam": {
// return CareTeamComponent;
// }
// case "Communication": { // case "Communication": {
// return ListCommunicationComponent; // return ListCommunicationComponent;
// } // }
@ -135,8 +138,11 @@ export class FhirResourceComponent implements OnInit, OnChanges {
case "Procedure": { case "Procedure": {
return ProcedureComponent; return ProcedureComponent;
} }
// case "Practitioner": { case "Practitioner": {
// return PractitionerComponent; return PractitionerComponent;
}
// case "PractitionerRole": {
// return PractitionerRoleComponent;
// } // }
// case "ServiceRequest": { // case "ServiceRequest": {
// return ListServiceRequestComponent; // return ListServiceRequestComponent;

View File

@ -12,7 +12,7 @@
<!-- </div>--> <!-- </div>-->
</div> </div>
<div #collapse="ngbCollapse" [(ngbCollapse)]="isCollapsed" class="card-body"> <div #collapse="ngbCollapse" [(ngbCollapse)]="isCollapsed" class="card-body">
<p class="az-content-text mg-b-20">Describes the event of a patient being administered a vaccine or a record of an immunization as reported by a patient, a clinician or another party.</p> <p class="az-content-text mg-b-20">A person who is directly or indirectly involved in the provisioning of healthcare.</p>
<fhir-ui-table [displayModel]="displayModel" [tableData]="tableData"></fhir-ui-table> <fhir-ui-table [displayModel]="displayModel" [tableData]="tableData"></fhir-ui-table>
</div> </div>
<div *ngIf="showDetails" class="card-footer"> <div *ngIf="showDetails" class="card-footer">

View File

@ -21,14 +21,7 @@ export class PractitionerComponent implements OnInit, FhirResourceComponentInter
constructor(public changeRef: ChangeDetectorRef, public router: Router) {} constructor(public changeRef: ChangeDetectorRef, public router: Router) {}
ngOnInit(): void { ngOnInit(): void {
this.tableData = [ this.tableData = [
// {
// label: 'Identifiers',
// testId: 'identifier',
// data: identifier && <Identifier fhirData={identifier} />,
// status: identifier,
// },
{ {
label: 'Gender', label: 'Gender',
data: this.displayModel?.gender, data: this.displayModel?.gender,
@ -51,18 +44,29 @@ export class PractitionerComponent implements OnInit, FhirResourceComponentInter
// }, // },
// { // {
// label: 'Address', // label: 'Address',
// testId: 'address', // data: this.displayModel?.address.,
// data: address && <Address fhirData={address} />, // status: !!this.displayModel?.address,
// status: address,
// }, // },
// { // {
// label: 'Telephone', // label: 'Telephone',
// testId: 'telecom', // data: this.displayModel.telecom,
// data: telecom && <Telecom fhirData={telecom} />, // enabled: !!this.displayModel.telecom,
// status: telecom,
// }, // },
]; ];
for(let idCoding of this.displayModel.identifier){
this.tableData.push({
label: `Identifier (${idCoding.system})`,
data: idCoding.display || idCoding.value,
enabled: true,
})
}
for(let telecom of this.displayModel.telecom){
this.tableData.push({
label: telecom.system,
data: telecom.value,
enabled: !!telecom.value,
})
}
} }
markForCheck(){ markForCheck(){
this.changeRef.markForCheck() this.changeRef.markForCheck()

View File

@ -0,0 +1,24 @@
<ng-template #rt let-r="result" let-t="term">
<span>{{r.text}}</span> <span *ngIf="r.subtext">&nbsp;({{r.subtext}})</span>
</ng-template>
<input
type="text"
class="form-control"
[class.is-invalid]="showError"
[(ngModel)]="searchResult"
[ngbTypeahead]="search"
[inputFormatter]="formatter"
[resultTemplate]="rt"
(ngModelChange)="typeAheadChangeEvent($event)"
(click)="typeAheadClickEvent($event)"
(focus)="typeAheadFocusEvent($event)"
placeholder="Search"
#instance="ngbTypeahead"
/>
<small *ngIf="searching" class="form-text text-muted">searching...</small>
<div class="invalid-feedback" *ngIf="searchFailed">Sorry, suggestions could not be loaded.</div>
<div *ngIf="debugMode" class="alert alert-warning">
<pre><code [highlight]="searchResult | json"></code></pre>
</div>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NlmTypeaheadComponent } from './nlm-typeahead.component';
describe('NlmTypeaheadComponent', () => {
let component: NlmTypeaheadComponent;
let fixture: ComponentFixture<NlmTypeaheadComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ NlmTypeaheadComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(NlmTypeaheadComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,218 @@
import {Component, EventEmitter, Input, OnInit, Optional, Output, Self, ViewChild} from '@angular/core';
import {merge, Observable, ObservableInput, of, Subject} from 'rxjs';
import {catchError, debounceTime, distinctUntilChanged, filter, switchMap, tap} from 'rxjs/operators';
import {NlmClinicalTableSearchService, NlmSearchResults} from '../../services/nlm-clinical-table-search.service';
import {
ControlValueAccessor,
NgControl,
} from '@angular/forms';
import {NgbTypeahead} from '@ng-bootstrap/ng-bootstrap';
export enum NlmSearchType {
Allergy = 'Allergy',
AllergyReaction = 'AllergyReaction',
Condition = 'Condition',
MedicalContactIndividualProfession = 'MedicalContactIndividualProfession',
MedicalContactIndividual = 'MedicalContactIndividual',
MedicalContactOrganization = 'MedicalContactOrganization',
MedicalContactOrganizationType = 'MedicalContactOrganizationType',
Medication = 'Medication',
MedicationWhyStopped = 'MedicationWhyStopped',
Procedure = 'Procedure',
Vaccine = 'Vaccine',
PrePopulated = 'PrePopulated'
}
@Component({
selector: 'app-nlm-typeahead',
templateUrl: './nlm-typeahead.component.html',
styleUrls: ['./nlm-typeahead.component.scss'],
providers: [
// {
// provide: NG_VALUE_ACCESSOR,
// multi:true,
// useExisting: NlmTypeaheadComponent
// },
// {
// provide: NG_VALIDATORS,
// multi:true,
// useExisting: NlmTypeaheadComponent
// }
]
})
export class NlmTypeaheadComponent implements ControlValueAccessor {
@Input() searchType: NlmSearchType = NlmSearchType.Condition;
@Input() debugMode: Boolean = false;
@Input() openOnFocus: Boolean = false;
@Input() prePopulatedOptions: NlmSearchResults[] = []
@ViewChild('instance', { static: true }) instance: NgbTypeahead;
focus$ = new Subject<string>();
click$ = new Subject<string>();
searching = false;
searchFailed = false;
searchResult: any = {};
onChange = (searchResult) => {};
onTouched = () => {};
touched = false;
disabled = false;
constructor(@Self() @Optional() public control: NgControl, private nlmClinicalTableSearchService: NlmClinicalTableSearchService) {
this.control && (this.control.valueAccessor = this);
}
formatter = (x: { text: string }) => x.text;
search = (text$: Observable<string>) => {
let searchOpFn
switch (this.searchType) {
case NlmSearchType.Allergy:
searchOpFn = this.nlmClinicalTableSearchService.searchAllergy
this.openOnFocus = true
break
case NlmSearchType.AllergyReaction:
searchOpFn = this.nlmClinicalTableSearchService.searchAllergyReaction
this.openOnFocus = true
break
case NlmSearchType.Condition:
searchOpFn = this.nlmClinicalTableSearchService.searchCondition
break
case NlmSearchType.MedicalContactIndividualProfession:
searchOpFn = this.nlmClinicalTableSearchService.searchMedicalContactIndividualProfession
this.openOnFocus = true
break
case NlmSearchType.MedicalContactIndividual:
searchOpFn = this.nlmClinicalTableSearchService.searchMedicalContactIndividual
break
case NlmSearchType.MedicalContactOrganization:
searchOpFn = this.nlmClinicalTableSearchService.searchMedicalContactOrganization
break
case NlmSearchType.MedicalContactOrganizationType:
searchOpFn = this.nlmClinicalTableSearchService.searchMedicalContactOrganizationType
this.openOnFocus = true
break
case NlmSearchType.Medication:
searchOpFn = this.nlmClinicalTableSearchService.searchMedication
break
case NlmSearchType.MedicationWhyStopped:
searchOpFn = this.nlmClinicalTableSearchService.searchMedicationWhyStopped
this.openOnFocus = true
break
case NlmSearchType.Procedure:
searchOpFn = this.nlmClinicalTableSearchService.searchProcedure
break
case NlmSearchType.Vaccine:
searchOpFn = this.nlmClinicalTableSearchService.searchVaccine
this.openOnFocus = true
break
case NlmSearchType.PrePopulated:
// searchOpFn = this.nlmClinicalTableSearchService.searchVaccine
console.log("PREPOPUlATED", this.prePopulatedOptions)
break
default:
console.error(`unknown search type: ${this.searchType}`)
return of([]);
}
// https://github.com/ng-bootstrap/ng-bootstrap/issues/917
// Note that the this argument is undefined so you need to explicitly bind it to a desired "this" target.
// https://ng-bootstrap.github.io/#/components/typeahead/api
searchOpFn = searchOpFn.bind(this.nlmClinicalTableSearchService)
const debouncedText$ = text$.pipe(debounceTime(200), distinctUntilChanged());
const clicksWithClosedPopup$ = this.click$.pipe(filter(() => !this.instance.isPopupOpen()));
const inputFocus$ = this.focus$;
return merge(debouncedText$, inputFocus$, clicksWithClosedPopup$).pipe(
tap(() => { this.searching = true }),
switchMap((term): ObservableInput<any> => {
console.log("searching for", term)
//must use bind
return searchOpFn(term).pipe(
tap(() => {this.searchFailed = false}),
catchError(() => {
this.searchFailed = true;
return of([]);
}),
)
}),
tap(() => {this.searching = false}),
);
}
typeAheadChangeEvent(event){
this.markAsTouched()
console.log("bubbling modelChange event", event)
if(typeof event === 'string'){
if (event.length === 0) {
this.onChange(null);
} else {
this.onChange({text: event});
}
} else{
this.onChange(event);
}
}
// If `openOnFocus` is true, we want to show dropdown/typeahead when the field is clicked or in focus, even if there is no text entered.
//See:https://ng-bootstrap.github.io/#/components/typeahead/examples#focus
typeAheadClickEvent($event){
if(this.openOnFocus){
this.click$.next($event.target.value)
}
}
typeAheadFocusEvent($event){
if(this.openOnFocus){
this.focus$.next($event.target.value)
}
}
/*
Methods related to ControlValueAccessor
See: https://blog.angular-university.io/angular-custom-form-controls/
See: http://prideparrot.com/blog/archive/2019/2/applying_validation_custom_form_component
This is what allows ngModel and formControlName to be used with this component
*/
writeValue(searchResult: any) {
this.searchResult = searchResult;
}
registerOnChange(onChange: any) {
this.onChange = onChange;
}
registerOnTouched(onTouched: any) {
this.onTouched = onTouched;
}
markAsTouched() {
if (!this.touched) {
this.onTouched();
this.touched = true;
}
}
setDisabledState(disabled: boolean) {
this.disabled = disabled;
}
public get invalid(): boolean {
return this.control ? this.control.invalid : false;
}
public get showError(): boolean {
if (!this.control) {
return false;
}
const { dirty, touched } = this.control;
return this.invalid ? (dirty || touched) : false;
}
}

View File

@ -6,7 +6,7 @@
{{conditionDisplayModel?.sort_title ? conditionDisplayModel?.sort_title : (conditionGroup | fhirPath: "Condition.code.text.first()":"Condition.code.coding.display.first()")}} {{conditionDisplayModel?.sort_title ? conditionDisplayModel?.sort_title : (conditionGroup | fhirPath: "Condition.code.text.first()":"Condition.code.coding.display.first()")}}
</div> </div>
<div class="col-6"> <div class="col-6">
{{conditionDisplayModel?.sort_date ? (conditionDisplayModel?.sort_date | date) : (conditionGroup | fhirPath: "Condition.onsetPeriod.start":"Condition.onsetDateTime" | date) }} - {{conditionGroup | fhirPath: "Condition.onsetPeriod.end" | date}} {{conditionDisplayModel?.onset_datetime | date }} <span *ngIf="conditionDisplayModel.abatement_datetime">- {{conditionDisplayModel.abatement_datetime | date}}</span>
</div> </div>
</div> </div>
</div><!-- card-header --> </div><!-- card-header -->
@ -32,12 +32,13 @@
</ng-container> </ng-container>
<!-- <div class="col-12 mt-3 mb-2 tx-indigo">--> <div *ngIf="conditionDisplayModel" class="col-12 mt-3 mb-2">
<!-- <h5>Initial Presentation</h5>--> <p class="tx-indigo">Initial Presentation</p>
<!-- </div>--> <p>
<!-- <div class="col-12">--> Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
<!-- Acute right knee pain and tenderness around the joint line - this was likely caused by acute renal failure.--> </p>
<!-- </div>--> </div>
</div> </div>
<div class="row pt-2" *ngIf="conditionGroup?.related_resources?.length > 0"> <div class="row pt-2" *ngIf="conditionGroup?.related_resources?.length > 0">
@ -58,7 +59,6 @@
<div class="row"> <div class="row">
<ng-container *ngFor="let encounter of encounters"> <ng-container *ngFor="let encounter of encounters">
<div routerLink="/source/{{encounter?.source_id}}/resource/{{encounter?.source_resource_id}}" class="col-6 mt-3 mb-2 tx-indigo"> <div routerLink="/source/{{encounter?.source_id}}/resource/{{encounter?.source_resource_id}}" class="col-6 mt-3 mb-2 tx-indigo">
<strong>{{encounter.period_start | date}}</strong> <strong>{{encounter.period_start | date}}</strong>
</div> </div>

View File

@ -11,6 +11,7 @@ import {DeviceModel} from '../../../lib/models/resources/device-model';
import {DiagnosticReportModel} from '../../../lib/models/resources/diagnostic-report-model'; import {DiagnosticReportModel} from '../../../lib/models/resources/diagnostic-report-model';
import {FastenDisplayModel} from '../../../lib/models/fasten/fasten-display-model'; import {FastenDisplayModel} from '../../../lib/models/fasten/fasten-display-model';
import * as _ from "lodash"; import * as _ from "lodash";
import {ConditionModel} from '../../../lib/models/resources/condition-model';
@Component({ @Component({
selector: 'app-report-medical-history-condition', selector: 'app-report-medical-history-condition',
@ -50,7 +51,7 @@ export class ReportMedicalHistoryConditionComponent implements OnInit {
* */ * */
@Input() conditionGroup: ResourceFhir @Input() conditionGroup: ResourceFhir
conditionDisplayModel: FastenDisplayModel conditionDisplayModel: Partial<ConditionModel>
//lookup table for all resources //lookup table for all resources
resourcesLookup: {[name:string]: FastenDisplayModel} = {} resourcesLookup: {[name:string]: FastenDisplayModel} = {}
@ -94,12 +95,14 @@ export class ReportMedicalHistoryConditionComponent implements OnInit {
let telecomEmails =_.find(practitionerModel.telecom, {"system": "email"}) let telecomEmails =_.find(practitionerModel.telecom, {"system": "email"})
let email = _.get(telecomEmails, '[0].value') let email = _.get(telecomEmails, '[0].value')
let qualification = _.find(practitionerModel.qualification, {"system": "http://nucc.org/provider-taxonomy"})
involvedInCareMap[id] = _.mergeWith( involvedInCareMap[id] = _.mergeWith(
{}, {},
involvedInCareMap[id], involvedInCareMap[id],
{ {
displayName: practitionerModel.name?.family && practitionerModel.name?.given ? `${practitionerModel.name?.family }, ${practitionerModel.name?.given}` : practitionerModel.name?.text, displayName: practitionerModel.name?.family && practitionerModel.name?.given ? `${practitionerModel.name?.family }, ${practitionerModel.name?.given}` : practitionerModel.name?.text,
role: practitionerModel.name?.prefix || practitionerModel.name?.suffix, role: qualification?.display || practitionerModel.name?.prefix || practitionerModel.name?.suffix,
email: email, email: email,
}, },
) )

View File

@ -41,7 +41,7 @@ import { ReportMedicalHistoryConditionComponent } from './report-medical-history
import { ReportLabsObservationComponent } from './report-labs-observation/report-labs-observation.component'; import { ReportLabsObservationComponent } from './report-labs-observation/report-labs-observation.component';
import { ChartsModule } from 'ng2-charts'; import { ChartsModule } from 'ng2-charts';
import { LoadingSpinnerComponent } from './loading-spinner/loading-spinner.component'; import { LoadingSpinnerComponent } from './loading-spinner/loading-spinner.component';
import {FormsModule} from '@angular/forms'; import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import { BinaryComponent } from './fhir/resources/binary/binary.component'; import { BinaryComponent } from './fhir/resources/binary/binary.component';
import { PdfComponent } from './fhir/datatypes/pdf/pdf.component'; import { PdfComponent } from './fhir/datatypes/pdf/pdf.component';
import { ImgComponent } from './fhir/datatypes/img/img.component'; import { ImgComponent } from './fhir/datatypes/img/img.component';
@ -62,9 +62,8 @@ import { MedicationRequestComponent } from './fhir/resources/medication-request/
import { ProcedureComponent } from './fhir/resources/procedure/procedure.component'; import { ProcedureComponent } from './fhir/resources/procedure/procedure.component';
import { DiagnosticReportComponent } from './fhir/resources/diagnostic-report/diagnostic-report.component'; import { DiagnosticReportComponent } from './fhir/resources/diagnostic-report/diagnostic-report.component';
import { PractitionerComponent } from './fhir/resources/practitioner/practitioner.component'; import { PractitionerComponent } from './fhir/resources/practitioner/practitioner.component';
import {FhirPathPipe} from '../pipes/fhir-path.pipe';
import {FilterPipe} from '../pipes/filter.pipe';
import {PipesModule} from '../pipes/pipes.module'; import {PipesModule} from '../pipes/pipes.module';
import { NlmTypeaheadComponent } from './nlm-typeahead/nlm-typeahead.component';
@NgModule({ @NgModule({
imports: [ imports: [
@ -74,11 +73,12 @@ import {PipesModule} from '../pipes/pipes.module';
NgbModule, NgbModule,
NgbCollapseModule, NgbCollapseModule,
FormsModule, FormsModule,
ReactiveFormsModule,
MomentModule, MomentModule,
TreeModule, TreeModule,
ChartsModule, ChartsModule,
HighlightModule, HighlightModule,
PipesModule PipesModule,
], ],
declarations: [ declarations: [
ComponentsSidebarComponent, ComponentsSidebarComponent,
@ -135,6 +135,7 @@ import {PipesModule} from '../pipes/pipes.module';
ProcedureComponent, ProcedureComponent,
DiagnosticReportComponent, DiagnosticReportComponent,
PractitionerComponent, PractitionerComponent,
NlmTypeaheadComponent,
], ],
exports: [ exports: [
ComponentsSidebarComponent, ComponentsSidebarComponent,
@ -184,7 +185,8 @@ import {PipesModule} from '../pipes/pipes.module';
MedicationRequestComponent, MedicationRequestComponent,
ProcedureComponent, ProcedureComponent,
DiagnosticReportComponent, DiagnosticReportComponent,
PractitionerComponent PractitionerComponent,
NlmTypeaheadComponent
] ]
}) })

View File

@ -0,0 +1,197 @@
import {IResourceRaw} from './resource_fhir';
import {CodingModel} from '../../../lib/models/datatypes/coding-model';
import {NlmSearchResults} from '../../services/nlm-clinical-table-search.service';
//
// {
// "condition": {
// "data": {
// "id": "14673",
// "text": "Hepatitis C",
// "link": "http://www.nlm.nih.gov/medlineplus/hepatitisc.html",
// "identifier": {
// "icd10": "R19.7"
// }
// },
// "status": "active",
// "started": {
// "year": 2023,
// "month": 2,
// "day": 23
// },
// "stopped": {
// "year": 2023,
// "month": 2,
// "day": 24
// },
// "description": "hello world"
// },
// "medications": [
// {
// "data": {
// "id": "1171721",
// "text": "DIOVAN (Oral Pill)"
// },
// "status": "active",
// "dosage": {},
// "started": {
// "year": 2023,
// "month": 2,
// "day": 15
// },
// "stopped": {
// "year": 2023,
// "month": 2,
// "day": 16
// },
// "whystopped": {
// "id": "STP-4",
// "text": "Replaced by better drug"
// },
// "resupply": {
// "year": 2023,
// "month": 2,
// "day": 9
// },
// "instructions": "dfdsf"
// }
// ],
// "procedures": [
// {
// "data": {
// "id": "5592",
// "text": "Abscess drainage",
// "link": "http://www.nlm.nih.gov/medlineplus/abscesses.html",
// "identifier": { "icd9": "" }
// },
// "whendone": {
// "year": 2023,
// "month": 2,
// "day": 16
// },
// "comment": "dfsdf"
// }
// ],
// "practitioners": [
// {
// "contactType": "search",
// "name": "",
// "data": {
// "id": "1689675621",
// "text": "HAZEN, F.",
// "provider_type": "Pharmacist",
// "provider_address": "2562 MONROE BLVD, OGDEN, UT 84740",
// "provider_fax": "(801) 399-1154",
// "provider_phone": "(801) 399-1151"
// },
// "profession": {
// "id": "CLIN",
// "text": "Clinical psychologist"
// },
// "phone": "",
// "fax": "",
// "email": "",
// "comment": "df"
// }
// ],
// "locations": [
// {
// "name": "",
// "contactType": "search",
// "data": {
// "id": "1689935025",
// "text": "D & D SPECIAL CARE",
// "provider_type": "Assisted Living Facility",
// "provider_address": "5760 NW 40TH TER, COCONUT CREEK, FL 33073",
// "provider_fax": "",
// "provider_phone": "(954) 675-3395"
// },
// "phone": "",
// "fax": "",
// "email": "",
// "comment": "sfds"
// },
// {
// "name": "sdfsdf",
// "contactType": "manual",
// "data": {},
// "phone": "sdfsdf",
// "fax": "sdfsdf",
// "email": "sdfsdf",
// "comment": "sdfsdf"
// }
// ]
// }
export interface ResourceCreate {
condition: ResourceCreateCondition,
"medications": ResourceCreateMedication[],
"procedures": ResourceCreateProcedure[],
"practitioners": ResourceCreatePractitioner[],
"organizations": ResourceCreateOrganization[]
}
export interface ResourceCreateCondition {
"data": NlmSearchResults,
"status": "active" | "inactive",
"started": ResourceCreateDate,
"stopped": ResourceCreateDate,
"description": string
}
export interface ResourceCreateDate {
year: number
month: number
day: number
}
export interface ResourceCreateMedication {
"data": NlmSearchResults,
"status": "active" | "inactive",
"dosage": {},
"started": ResourceCreateDate,
"stopped": ResourceCreateDate,
"whystopped": NlmSearchResults
"requester": string,
"instructions": string
}
export interface ResourceCreateProcedure {
"data": NlmSearchResults,
"whendone": ResourceCreateDate,
"comment": string,
"performer": string,
"location": string
}
export interface ResourceCreatePractitioner {
"id"?: string,
"identifier": CodingModel[]
"name": string,
"profession": NlmSearchResults,
"phone": string,
"fax": string,
"email": string,
"address": Address,
}
export interface ResourceCreateOrganization {
"id"?: string,
"identifier": CodingModel[]
"type": NlmSearchResults,
"name": string,
"phone": string,
"fax": string,
"email": string,
"address": Address,
}
export interface Address {
line1?: string
line2?: string
city?: string
state?: string
zip?: string
country?: string
}

View File

@ -26,6 +26,9 @@
<div class="col-6"> <div class="col-6">
<h1 class="az-dashboard-title">Condition</h1> <h1 class="az-dashboard-title">Condition</h1>
</div> </div>
<div class="col-6">
<a class="float-right btn btn-outline-indigo" routerLink="/resource/create">Add Condition</a>
</div>
</div> </div>
<!-- Condition List --> <!-- Condition List -->

View File

@ -0,0 +1,543 @@
<div class="az-content">
<div class="container">
<div class="az-content-body pd-lg-l-40 d-flex flex-column">
<div class="az-content-breadcrumb">
<span>Components</span>
<span>Forms</span>
<span>Form Elements</span>
</div>
<h2 class="az-content-title">Create a Record</h2>
<!-- Editor Button -->
<div class="row mt-5 mb-3">
<div class="col-12">
<div class="alert alert-warning" role="alert">
<strong>Warning!</strong> This form is in early-alpha and is not ready for general use. You will encounter bugs and missing features.
Please open a <a href="https://github.com/fastenhealth/fasten-onprem/issues/new?title=Resource+Create+-+Your+Feature+Or+Bug+Here">Github Issue</a> if you find any bugs or have any suggestions.
<br/>
<br/>
Enable Debug mode: <input type="checkbox" [(ngModel)]="debugMode"/>
</div>
<div *ngIf="debugMode" class="alert alert-warning">
<pre><code [highlight]="form.getRawValue() | json"></code></pre>
<strong>Form Status: {{ form.status }}</strong>
</div>
</div>
</div>
<div class="az-content-label mg-b-5">Condition</div>
<p class="mg-b-20">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna </p>
<form (ngSubmit)="onSubmit()" [formGroup]="form">
<div *ngIf="debugMode" class="alert alert-warning">
<strong>Condition Status: {{form.get('condition').status}}</strong>
</div>
<ng-container formGroupName="condition">
<div class="row row-sm">
<div class="col-lg-4">
<p class="mg-b-10">Medical condition<span ngbTooltip="required" class="text-danger">*</span></p>
<app-nlm-typeahead formControlName="data" [debugMode]="debugMode"></app-nlm-typeahead>
</div><!-- col -->
<div class="col-lg mg-t-10 mg-lg-t-0">
<p class="mg-b-10">Status<span ngbTooltip="required" class="text-danger">*</span></p>
<select formControlName="status" class="form-control">
<option value="" hidden selected>Select Status</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
</div><!-- col -->
<div class="col-lg mg-t-10 mg-lg-t-0">
<p class="mg-b-10">Started<span ngbTooltip="required" class="text-danger">*</span></p>
<input formControlName="started" class="form-control" placeholder="yyyy-mm-dd" name="dp" ngbDatepicker #cds="ngbDatepicker" (click)="cds.toggle()">
</div><!-- col -->
<div class="col-lg mg-t-10 mg-lg-t-0">
<p class="mg-b-10">Stopped</p>
<input formControlName="stopped" [minDate]="form.get('condition').get('started').value" class="form-control" placeholder="yyyy-mm-dd" name="dp" ngbDatepicker #cde="ngbDatepicker" (click)="cde.toggle()">
</div><!-- col -->
</div><!-- row -->
<div class="row row-sm mg-t-20">
<div class="col-lg">
<p class="mg-b-10">Description/Comment</p>
<textarea formControlName="description" rows="3" class="form-control" placeholder="Textarea"></textarea>
</div><!-- col -->
</div><!-- row -->
</ng-container>
<hr class="mg-y-30">
<div class="row">
<div class="col-lg">
<div class="card card-fhir-resource" >
<div class="card-header" (click)="collapsePanel['medication'] = !collapsePanel['medication']">
<div>
<h6 class="card-title">Medications</h6>
<p class="card-text">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna</p>
</div>
</div>
<div #collapse="ngbCollapse" [(ngbCollapse)]="collapsePanel['medication']" class="card-body">
<ng-container formArrayName="medications">
<div class="card mg-t-10 pd-20" [formGroup]="medicationGroup" *ngFor="let medicationGroup of medications.controls; let i = index">
<div *ngIf="debugMode" class="alert alert-warning">
<strong>Medication Status: {{medicationGroup.status}}</strong>
</div>
<div class="tx-right">
<span class="cursor-pointer" (click)="deleteMedication(i)" aria-hidden="true"><i class="fas fa-trash"></i></span>
</div>
<div class="row row-sm">
<div class="col-lg-6">
<p class="mg-b-10">Medication name<span ngbTooltip="required" class="text-danger">*</span></p>
<app-nlm-typeahead formControlName="data" searchType="Medication" [debugMode]="debugMode"></app-nlm-typeahead>
</div><!-- col -->
<div class="col-lg-3 mg-t-10 mg-lg-t-0">
<p class="mg-b-10">Status<span ngbTooltip="required" class="text-danger">*</span></p>
<select formControlName="status" class="form-control">
<option value="" hidden selected>Select Status</option>
<option value="active">Active</option>
<option value="stopped">Stopped</option>
</select> </div><!-- col -->
<div class="col-lg-3 mg-t-10 mg-lg-t-0">
<p class="mg-b-10">Dosage</p>
<input formControlName="dosage" class="form-control" placeholder="Input box" type="text">
</div><!-- col -->
<div class="col-lg-3 mg-t-10 mg-lg-t-0">
<p class="mg-b-10">Started<span ngbTooltip="required" class="text-danger">*</span></p>
<input formControlName="started" class="form-control" placeholder="yyyy-mm-dd" ngbDatepicker #ds="ngbDatepicker" (click)="ds.toggle()">
</div><!-- col -->
<div class="col-lg-3 mg-t-10 mg-lg-t-0">
<p class="mg-b-10">Stopped</p>
<input [minDate]="medicationGroup.get('started').value" formControlName="stopped" class="form-control" placeholder="yyyy-mm-dd" ngbDatepicker #dstop="ngbDatepicker" (click)="dstop.toggle()">
</div><!-- col -->
<div class="col-lg-3 mg-t-10 mg-lg-t-0">
<p class="mg-b-10">Why Stopped</p>
<app-nlm-typeahead formControlName="whystopped" searchType="MedicationWhyStopped" [debugMode]="debugMode"></app-nlm-typeahead>
</div><!-- col -->
<div class="col-lg-3 mg-t-10 mg-lg-t-0">
<p class="mg-b-10">Prescribing Practitioner<span ngbTooltip="required" class="text-danger">*</span></p>
<select class="form-control" formControlName="requester">
<option value="" hidden selected>Select Practitioner</option>
<option value="" (click)="openPractitionerModal(practitionerCreateModal, medicationGroup, 'requester')">New Practitioner</option>
<optgroup *ngIf="practitioners.controls.length" class="divider"></optgroup>
<option *ngFor="let practitioner of practitioners.controls; let i = index" [value]="practitioner.value.id">
{{practitioner.value.name}} ({{practitioner.value.profession.text}})
</option>
</select>
</div><!-- col -->
</div><!-- row -->
<div class="row row-sm mg-t-20">
<div class="col-lg">
<p class="mg-b-10">Instructions</p>
<textarea formControlName="instructions" rows="3" class="form-control" placeholder="Textarea"></textarea>
</div><!-- col -->
</div><!-- row -->
</div>
</ng-container>
<div class="row pt-2">
<div class="col-lg-4 col-md-3">
<button type="button" (click)="addMedication()" class="btn btn-outline-indigo btn-block">Add Medication</button>
</div>
</div>
</div>
<div class="card-footer">
<a class="float-right" (click)="collapsePanel['medication'] = !collapsePanel['medication']">{{collapsePanel['medication'] ? 'expand' : 'collapse'}}</a>
</div>
</div>
</div>
</div>
<hr class="mg-y-30">
<div class="row">
<div class="col-lg">
<div class="card card-fhir-resource" >
<div class="card-header" (click)="collapsePanel['procedure'] = !collapsePanel['procedure']">
<div>
<h6 class="card-title">Major Surgeries and Implants</h6>
<p class="card-text">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna</p>
</div>
</div>
<div #collapse="ngbCollapse" [(ngbCollapse)]="collapsePanel['procedure']" class="card-body">
<ng-container formArrayName="procedures">
<div class="card mg-t-10 pd-20" [formGroup]="procedureGroup" *ngFor="let procedureGroup of procedures.controls; let i = index">
<div *ngIf="debugMode" class="alert alert-warning">
<strong>Procedure Status: {{procedureGroup.status}}</strong>
</div>
<div class="tx-right">
<span class="cursor-pointer" (click)="deleteProcedure(i)" aria-hidden="true"><i class="fas fa-trash"></i></span>
</div>
<div class="row row-sm">
<div class="col-lg-8">
<p class="mg-b-10">Surgery or Implant<span ngbTooltip="required" class="text-danger">*</span></p>
<app-nlm-typeahead formControlName="data" searchType="Procedure" [debugMode]="debugMode"></app-nlm-typeahead>
</div><!-- col -->
<div class="col-lg-4 mg-t-10 mg-lg-t-0">
<p class="mg-b-10">When done<span ngbTooltip="required" class="text-danger">*</span></p>
<input formControlName="whendone" class="form-control" placeholder="yyyy-mm-dd" ngbDatepicker #dwd="ngbDatepicker" (click)="dwd.toggle()">
</div><!-- col -->
<div class="col-lg-6 mg-t-10 mg-lg-t-0">
<p class="mg-b-10">Performed By</p>
<select class="form-control" formControlName="performer">
<option value="" hidden selected>Select Practitioner</option>
<option value="" (click)="openPractitionerModal(practitionerCreateModal, procedureGroup, 'performer')">New Practitioner</option>
<optgroup *ngIf="practitioners.controls.length" class="divider"></optgroup>
<option *ngFor="let practitioner of practitioners.controls; let i = index" [value]="practitioner.value.id">
{{practitioner.value.name}} ({{practitioner.value.profession.text}})
</option>
</select>
</div><!-- col -->
<div class="col-lg-6 mg-t-10 mg-lg-t-0">
<p class="mg-b-10">Location</p>
<select class="form-control" formControlName="location">
<option value="" hidden selected>Select Location</option>
<option value="" (click)="openOrganizationModal(organizationCreateModal, procedureGroup, 'location')">New Organization</option>
<optgroup *ngIf="organizations.controls.length" class="divider"></optgroup>
<option *ngFor="let organization of organizations.controls; let i = index" [value]="organization.value.id">
{{organization.value.name}} ({{organization.value.address}})
</option>
</select>
</div><!-- col -->
<div class="col-lg-12 mg-t-10 mg-lg-t-0">
<p class="mg-b-10">Comments</p>
<textarea formControlName="comment" class="form-control" placeholder="Input box" rows="3"></textarea>
</div><!-- col -->
</div><!-- row -->
</div>
</ng-container>
<div class="row pt-2">
<div class="col-lg-4 col-md-3">
<button type="button" (click)="addProcedure()" class="btn btn-outline-indigo btn-block">Add Surgery or Implant</button>
</div>
</div>
</div>
<div class="card-footer">
<a class="float-right" (click)="collapsePanel['procedure'] = !collapsePanel['procedure']">{{collapsePanel['procedure'] ? 'expand' : 'collapse'}}</a>
</div>
</div>
</div>
</div>
<hr class="mg-y-30">
<div class="row">
<div class="col-lg">
<div class="card card-fhir-resource" >
<div class="card-header" (click)="collapsePanel['practitioner'] = !collapsePanel['practitioner']">
<div>
<h6 class="card-title">Medical Practitioners</h6>
<p class="card-text">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna</p>
</div>
</div>
<div #collapse="ngbCollapse" [(ngbCollapse)]="collapsePanel['practitioner']" class="card-body">
<ng-container formArrayName="practitioners">
<div class="card mg-t-10 pd-20" [formGroup]="practitionerGroup" *ngFor="let practitionerGroup of practitioners.controls; let i = index">
<div class="tx-right">
<span class="cursor-pointer" (click)="deletePractitioner(i)" aria-hidden="true"><i class="fas fa-trash"></i></span>
</div>
<div class="row row-sm">
<input formControlName="id" class="form-control" type="hidden">
<div class="col-lg-6 mg-t-10 mg-lg-t-0">
<p class="mg-b-10">Name<span ngbTooltip="required" class="text-danger">*</span></p>
<input formControlName="name" class="form-control" readonly placeholder="Input box" type="text">
</div><!-- col -->
<div class="col-lg-6 mg-t-10 mg-lg-t-0">
<p class="mg-b-10">Type<span ngbTooltip="required" class="text-danger">*</span></p>
<app-nlm-typeahead formControlName="profession" searchType="MedicalContactIndividualProfession" [debugMode]="debugMode"></app-nlm-typeahead>
</div><!-- col -->
<div class="col-lg mg-t-10 mg-lg-t-0">
<p class="mg-b-10">Telephone</p>
<input formControlName="phone" class="form-control" placeholder="(123) 456-7890" type="text">
</div><!-- col -->
<div class="col-lg mg-t-10 mg-lg-t-0">
<p class="mg-b-10">Fax</p>
<input formControlName="fax" class="form-control" placeholder="(123) 456-7890" type="text">
</div><!-- col -->
<div class="col-lg mg-t-10 mg-lg-t-0">
<p class="mg-b-10">Email</p>
<input formControlName="email" class="form-control" placeholder="email@example.com" type="text">
</div><!-- col -->
<div class="col-lg mg-t-10 mg-lg-t-0">
<p class="mg-b-10">Address</p>
<input formControlName="address" class="form-control" placeholder="Input box" type="text">
</div><!-- col -->
</div><!-- row -->
</div>
</ng-container>
<div class="row pt-2">
<div class="col-lg-4 col-md-4">
<button type="button" (click)="openPractitionerModal(practitionerCreateModal)" class="btn btn-outline-indigo btn-block">Add Practitioner</button>
</div>
</div>
</div>
<div class="card-footer">
<a class="float-right" (click)="collapsePanel['practitioner'] = !collapsePanel['practitioner']">{{collapsePanel['practitioner'] ? 'expand' : 'collapse'}}</a>
</div>
</div>
</div>
</div>
<hr class="mg-y-30">
<div class="row">
<div class="col-lg">
<div class="card card-fhir-resource" >
<div class="card-header" (click)="collapsePanel['organization'] = !collapsePanel['organization']">
<div>
<h6 class="card-title">Medical Location/Organizations</h6>
<p class="card-text">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna</p>
</div>
</div>
<div #collapse="ngbCollapse" [(ngbCollapse)]="collapsePanel['organization']" class="card-body">
<ng-container formArrayName="locations">
<div class="card mg-t-10 pd-20" [formGroup]="organizationGroup" *ngFor="let organizationGroup of organizations.controls; let i = index">
<div class="tx-right">
<span class="cursor-pointer" (click)="deleteOrganization(i)" aria-hidden="true"><i class="fas fa-trash"></i></span>
</div>
<div class="row row-sm">
<input formControlName="id" class="form-control" type="hidden">
<div class="col-lg mg-t-10 mg-lg-t-0">
<p class="mg-b-10">Name<span ngbTooltip="required" class="text-danger">*</span></p>
<input formControlName="name" readonly class="form-control" placeholder="Input box" type="text">
</div><!-- col -->
<div class="col-lg mg-t-10 mg-lg-t-0">
<p class="mg-b-10">Type</p>
<app-nlm-typeahead formControlName="type" searchType="MedicalContactOrganizationType" [debugMode]="debugMode"></app-nlm-typeahead>
</div><!-- col -->
<div class="col-lg mg-t-10 mg-lg-t-0">
<p class="mg-b-10">Telephone</p>
<input formControlName="phone" class="form-control" placeholder="(123) 456-7890" type="text">
</div><!-- col -->
<div class="col-lg mg-t-10 mg-lg-t-0">
<p class="mg-b-10">Fax</p>
<input formControlName="fax" class="form-control" placeholder="(123) 456-7890" type="text">
</div><!-- col -->
<div class="col-lg mg-t-10 mg-lg-t-0">
<p class="mg-b-10">Email</p>
<input formControlName="email" class="form-control" placeholder="email@example.com" type="text">
</div><!-- col -->
</div><!-- row -->
</div>
</ng-container>
<div class="row pt-2">
<div class="col-lg-4 col-md-4">
<button type="button" (click)="openOrganizationModal(organizationCreateModal)" class="btn btn-outline-indigo btn-block">Add Organization</button>
</div>
</div>
</div>
<div class="card-footer">
<a class="float-right" (click)="collapsePanel['organization'] = !collapsePanel['organization']">{{collapsePanel['organization'] ? 'expand' : 'collapse'}}</a>
</div>
</div>
</div>
</div>
<hr class="mg-y-30">
<div class="row">
<div class="col-lg">
<div class="card card-fhir-resource" >
<div class="card-header" (click)="collapsePanel['attachments'] = !collapsePanel['attachments']">
<div>
<h6 class="card-title">Notes & Attachments</h6>
<p class="card-text">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna</p>
</div>
</div>
<div #collapse="ngbCollapse" [(ngbCollapse)]="collapsePanel['attachments']" class="card-body">
<div class="row row-sm">
<div class="col-lg">
<p class="mg-b-10">Name</p>
<input disabled class="form-control" placeholder="Input box" type="text">
</div><!-- col -->
<div class="col-lg mg-t-10 mg-lg-t-0">
<p class="mg-b-10">Type</p>
<input disabled class="form-control" placeholder="Input box" type="text">
</div><!-- col -->
</div><!-- row -->
<div class="row pt-2">
<div class="col-lg-4 col-md-3">
<button disabled type="button" class="btn btn-outline-indigo btn-block">Add Note or Attachment</button>
</div>
</div>
</div>
<div class="card-footer">
<a class="float-right" (click)="collapsePanel['attachments'] = !collapsePanel['attachments']">{{collapsePanel['attachments'] ? 'expand' : 'collapse'}}</a>
</div>
</div>
</div>
</div>
<button class="mg-t-20 mg-b-20 btn btn-az-primary btn-rounded btn-block " type="submit">Submit</button>
</form>
</div><!-- az-content-body -->
</div><!-- container -->
</div><!-- az-content -->
<ng-template #practitionerCreateModal let-modal>
<div class="modal-header">
<h4 class="modal-title" id="modal-practitioner">New Practitioner</h4>
<button type="button" class="close" aria-label="Close" (click)="modal.close()"><span aria-hidden="true">×</span></button>
</div>
<div class="modal-body">
<div *ngIf="debugMode" class="alert alert-warning">
<pre><code [highlight]="newPractitionerForm.getRawValue() | json"></code></pre>
<strong>New Practitioner Form Status: {{ newPractitionerForm.status }}</strong>
</div>
<div class="row row-sm">
<div class="col-12">
<p class="mg-b-10">Name<span ngbTooltip="required" class="text-danger">*</span></p>
<form [formGroup]="newPractitionerTypeaheadForm">
<app-nlm-typeahead formControlName="data" searchType="MedicalContactIndividual" [debugMode]="debugMode"></app-nlm-typeahead>
</form>
<span *ngFor="let extId of newPractitionerForm.get('identifier').getRawValue()" class="badge badge-pill badge-primary">{{extId.type?.coding[0].code}}: {{extId.value}}</span>
</div><!-- col -->
<ng-container [formGroup]="newPractitionerForm">
<div class="col-6 mg-t-10 mg-lg-t-0">
<p class="mg-b-10">Type<span ngbTooltip="required" class="text-danger">*</span></p>
<app-nlm-typeahead formControlName="profession" searchType="MedicalContactIndividualProfession" [debugMode]="debugMode"></app-nlm-typeahead>
</div><!-- col -->
<div class="col-6 mg-t-10 mg-lg-t-0">
<p class="mg-b-10">Telephone</p>
<input formControlName="phone" class="form-control" placeholder="(123) 456-7890" type="text">
</div><!-- col -->
<div class="col-6 mg-t-10 mg-lg-t-0">
<p class="mg-b-10">Fax</p>
<input formControlName="fax" class="form-control" placeholder="(123) 456-7890" type="text">
</div><!-- col -->
<div class="col-6 mg-t-10 mg-lg-t-0">
<p class="mg-b-10">Email</p>
<input formControlName="email" class="form-control" placeholder="email@example.com" type="text" email>
</div><!-- col -->
<ng-container formGroupName="address">
<div class="col-12 mg-t-10 mg-lg-t-0">
<p class="mg-b-10">Address</p>
<input formControlName="line1" class="form-control" placeholder="Line 1" type="text">
</div><!-- col -->
<div class="col-12 pd-t-10 mg-t-10 mg-lg-t-0">
<input formControlName="line2" class="form-control" placeholder="Line 2" type="text">
</div><!-- col -->
<div class="col-6 mg-t-10 mg-lg-t-0">
<p class="mg-b-10">City</p>
<input formControlName="city" class="form-control" placeholder="City" type="text">
</div><!-- col -->
<div class="col-6 mg-t-10 mg-lg-t-0">
<p class="mg-b-10">State</p>
<input formControlName="state" class="form-control" placeholder="State" type="text">
</div><!-- col -->
<div class="col-6 mg-t-10 mg-lg-t-0">
<p class="mg-b-10">Zip/Postal</p>
<input formControlName="zip" class="form-control" placeholder="Zip" type="text">
</div><!-- col -->
<div class="col-6 mg-t-10 mg-lg-t-0">
<p class="mg-b-10">Country</p>
<input formControlName="country" class="form-control" placeholder="Country" type="text">
</div><!-- col -->
</ng-container>
</ng-container>
</div><!-- row -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-az-primary" (click)="modal.dismiss()">Add Practitioner</button>
</div>
</ng-template>
<ng-template #organizationCreateModal let-modal>
<div class="modal-header">
<h4 class="modal-title" id="modal-location">New Location</h4>
<button type="button" class="close" aria-label="Close" (click)="modal.close()"><span aria-hidden="true">×</span></button>
</div>
<div class="modal-body">
<div *ngIf="debugMode" class="alert alert-warning">
<pre><code [highlight]="newOrganizationForm.getRawValue() | json"></code></pre>
<strong>New Organization Form Status: {{ newOrganizationForm.status }}</strong>
</div>
<div class="row row-sm">
<div class="col-12">
<p class="mg-b-10">Name<span ngbTooltip="required" class="text-danger">*</span></p>
<form [formGroup]="newOrganizationTypeaheadForm">
<app-nlm-typeahead formControlName="data" searchType="MedicalContactOrganization" [debugMode]="debugMode"></app-nlm-typeahead>
</form>
<span *ngFor="let extId of newOrganizationForm.get('identifier').getRawValue()" class="badge badge-pill badge-primary">{{extId.type?.coding[0].code}}: {{extId.value}}</span>
</div><!-- col -->
<ng-container [formGroup]="newOrganizationForm">
<div class="col-6 mg-t-10 mg-lg-t-0">
<p class="mg-b-10">Type<span ngbTooltip="required" class="text-danger">*</span></p>
<app-nlm-typeahead formControlName="type" searchType="MedicalContactOrganizationType" [debugMode]="debugMode"></app-nlm-typeahead>
</div><!-- col -->
<div class="col-6 mg-t-10 mg-lg-t-0">
<p class="mg-b-10">Telephone</p>
<input formControlName="phone" class="form-control" placeholder="(123) 456-7890" type="text">
</div><!-- col -->
<div class="col-6 mg-t-10 mg-lg-t-0">
<p class="mg-b-10">Fax</p>
<input formControlName="fax" class="form-control" placeholder="(123) 456-7890" type="text">
</div><!-- col -->
<div class="col-6 mg-t-10 mg-lg-t-0">
<p class="mg-b-10">Email</p>
<input formControlName="email" class="form-control" placeholder="email@example.com" type="text" email>
</div><!-- col -->
<ng-container formGroupName="address">
<div class="col-12 mg-t-10 mg-lg-t-0">
<p class="mg-b-10">Address</p>
<input formControlName="line1" class="form-control" placeholder="Line 1" type="text">
</div><!-- col -->
<div class="col-12 pd-t-10 mg-t-10 mg-lg-t-0">
<input formControlName="line2" class="form-control" placeholder="Line 2" type="text">
</div><!-- col -->
<div class="col-6 mg-t-10 mg-lg-t-0">
<p class="mg-b-10">City</p>
<input formControlName="city" class="form-control" placeholder="City" type="text">
</div><!-- col -->
<div class="col-6 mg-t-10 mg-lg-t-0">
<p class="mg-b-10">State</p>
<input formControlName="state" class="form-control" placeholder="State" type="text">
</div><!-- col -->
<div class="col-6 mg-t-10 mg-lg-t-0">
<p class="mg-b-10">Zip/Postal</p>
<input formControlName="zip" class="form-control" placeholder="Zip" type="text">
</div><!-- col -->
<div class="col-6 mg-t-10 mg-lg-t-0">
<p class="mg-b-10">Country</p>
<input formControlName="country" class="form-control" placeholder="Country" type="text">
</div><!-- col -->
</ng-container>
</ng-container>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-az-primary" (click)="modal.dismiss()">Add Location</button>
</div>
</ng-template>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ResourceCreatorComponent } from './resource-creator.component';
describe('ResourceCreatorComponent', () => {
let component: ResourceCreatorComponent;
let fixture: ComponentFixture<ResourceCreatorComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ResourceCreatorComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(ResourceCreatorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,379 @@
import {Component, Input, OnInit} from '@angular/core';
import {AbstractControl, FormArray, FormControl, FormGroup, Validators} from '@angular/forms';
import { ModalDismissReasons, NgbModal } from '@ng-bootstrap/ng-bootstrap';
import {
ResourceCreateOrganization,
ResourceCreatePractitioner,
} from '../../models/fasten/resource_create';
import {uuidV4} from '../../../lib/utils/uuid';
import {NlmSearchResults} from '../../services/nlm-clinical-table-search.service';
import {GenerateR4Bundle} from './resource-creator.utilities';
import {FastenApiService} from '../../services/fasten-api.service';
import {Router} from '@angular/router';
export interface MedicationModel {
data: {},
status: string,
dosage: string,
started: null,
stopped: null,
whystopped: string,
resupply: string
}
export enum ContactType {
ContactTypeSearch = 'search',
ContactTypeManual = 'manual',
}
@Component({
selector: 'app-resource-creator',
templateUrl: './resource-creator.component.html',
styleUrls: ['./resource-creator.component.scss']
})
export class ResourceCreatorComponent implements OnInit {
debugMode = false;
collapsePanel: {[name: string]: boolean} = {}
@Input() form!: FormGroup;
get isValid() { return true; }
// model: any = {
// condition: {
// data: {},
// status: null,
// started: null,
// stopped: null,
// description: null,
// },
// medication: []
// }
constructor(private router: Router, private modalService: NgbModal, private fastenApi: FastenApiService) { }
ngOnInit(): void {
//https://stackoverflow.com/questions/52038071/creating-nested-form-groups-using-angular-reactive-forms
//https://www.danywalls.com/creating-dynamic-forms-in-angular-a-step-by-step-guide
//https://www.telerik.com/blogs/angular-basics-creating-dynamic-forms-using-formarray-angular
//https://angular.io/guide/reactive-forms#creating-dynamic-forms
//https://angular.io/guide/dynamic-form
this.form = new FormGroup({
condition: new FormGroup({
data: new FormControl<NlmSearchResults>(null, Validators.required),
status: new FormControl(null, Validators.required),
started: new FormControl(null, Validators.required),
stopped: new FormControl(null),
description: new FormControl(null),
}),
medications: new FormArray([]),
procedures: new FormArray([]),
practitioners: new FormArray([]),
organizations: new FormArray([]),
});
this.resetOrganizationForm()
// this.resetPractitionerForm()
}
get medications(): FormArray {
return this.form.controls["medications"] as FormArray;
}
addMedication(){
const medicationGroup = new FormGroup({
data: new FormControl<NlmSearchResults>(null, Validators.required),
status: new FormControl(null, Validators.required),
dosage: new FormControl({
value: '', disabled: true
}),
started: new FormControl(null, Validators.required),
stopped: new FormControl(null),
whystopped: new FormControl(null),
requester: new FormControl(null, Validators.required),
instructions: new FormControl(null),
});
medicationGroup.get("data").valueChanges.subscribe(val => {
medicationGroup.get("dosage").enable();
//TODO: find a way to create dependant dosage information based on medication data.
});
this.medications.push(medicationGroup);
}
deleteMedication(index: number) {
this.medications.removeAt(index);
}
get procedures(): FormArray {
return this.form.controls["procedures"] as FormArray;
}
addProcedure(){
const procedureGroup = new FormGroup({
data: new FormControl<NlmSearchResults>(null, Validators.required),
whendone: new FormControl(null, Validators.required),
performer: new FormControl(null),
location: new FormControl(null),
comment: new FormControl('')
});
this.procedures.push(procedureGroup);
}
deleteProcedure(index: number) {
this.procedures.removeAt(index);
}
get practitioners(): FormArray {
return this.form.controls["practitioners"] as FormArray;
}
addPractitioner(practitioner: ResourceCreatePractitioner){
const practitionerGroup = new FormGroup({
id: new FormControl(practitioner.id, Validators.required),
identifier: new FormControl(practitioner.identifier),
profession: new FormControl(practitioner.profession, Validators.required),
name: new FormControl(practitioner.name, Validators.required),
phone: new FormControl(practitioner.phone, Validators.pattern('[- +()0-9]+')),
fax: new FormControl(practitioner.fax, Validators.pattern('[- +()0-9]+')),
email: new FormControl(practitioner.email, Validators.email),
address: new FormGroup({
line1: new FormControl(practitioner.address.line1),
line2: new FormControl(practitioner.address.line2),
city: new FormControl(practitioner.address.city),
state: new FormControl(practitioner.address.state),
zip: new FormControl(practitioner.address.zip),
country: new FormControl(practitioner.address.country),
}),
});
this.practitioners.push(practitionerGroup);
}
deletePractitioner(index: number) {
this.practitioners.removeAt(index);
}
get organizations(): FormArray {
return this.form.controls["organizations"] as FormArray;
}
addOrganization(organization: ResourceCreateOrganization){
const organizationGroup = new FormGroup({
id: new FormControl(organization.id, Validators.required),
identifier: new FormControl(organization.identifier),
name: new FormControl(organization.name, Validators.required),
type: new FormControl(organization.type),
phone: new FormControl(organization.phone, Validators.pattern('[- +()0-9]+')),
fax: new FormControl(organization.fax, Validators.pattern('[- +()0-9]+')),
email: new FormControl(organization.email, Validators.email),
address: new FormControl(organization.address),
});
this.organizations.push(organizationGroup);
}
deleteOrganization(index: number) {
this.organizations.removeAt(index);
}
onSubmit() {
console.log(this.form.getRawValue())
this.form.markAllAsTouched()
if (this.form.valid) {
console.log('form submitted');
let bundle = GenerateR4Bundle(this.form.getRawValue());
let bundleJsonStr = JSON.stringify(bundle);
let bundleBlob = new Blob([bundleJsonStr], { type: 'application/json' });
let bundleFile = new File([ bundleBlob ], 'bundle.json');
this.fastenApi.createManualSource(bundleFile).subscribe((resp) => {
console.log(resp)
this.router.navigate(['/medical-history'])
})
}
}
//Modal Helpers
newPractitionerTypeaheadForm: FormGroup
newPractitionerForm: FormGroup //ResourceCreatePractitioner
newOrganizationTypeaheadForm: FormGroup
newOrganizationForm: FormGroup //ResourceCreateOrganization
openPractitionerModal(content, formGroup?: AbstractControl, controlName?: string) {
this.resetPractitionerForm()
this.modalService.open(content, {
ariaLabelledBy: 'modal-practitioner',
beforeDismiss: () => {
console.log("validate Practitioner form")
this.newPractitionerForm.markAllAsTouched()
this.newPractitionerTypeaheadForm.markAllAsTouched()
return this.newPractitionerForm.valid
},
}).result.then(
() => {
console.log('Closed without saving');
},
() => {
console.log('Closing, saving form');
//add this to the list of organization
let result = this.newPractitionerForm.getRawValue()
result.id = uuidV4();
this.addPractitioner(result);
if(formGroup && controlName){
//set this practitioner to the current select box
formGroup.get(controlName).setValue(result.id);
}
},
);
}
openOrganizationModal(content, formGroup?: AbstractControl, controlName?: string) {
this.resetOrganizationForm()
this.modalService.open(content, {
ariaLabelledBy: 'modal-organization',
beforeDismiss: () => {
console.log("validate Organization form")
this.newOrganizationForm.markAllAsTouched()
this.newOrganizationTypeaheadForm.markAllAsTouched()
return this.newOrganizationForm.valid
},
}).result.then(
() => {
console.log('Closed without saving');
},
() => {
console.log('Closing, saving form');
//add this to the list of organization
let result = this.newOrganizationForm.getRawValue()
result.id = uuidV4();
this.addOrganization(result);
if(formGroup && controlName){
//set this practitioner to the current select box
formGroup.get(controlName).setValue(result.id);
}
},
);
}
private resetPractitionerForm(){
this.newPractitionerTypeaheadForm = new FormGroup({
data: new FormControl(null, Validators.required),
})
this.newPractitionerTypeaheadForm.valueChanges.subscribe(form => {
console.log("CHANGE INDIVIDUAL IN MODAL", form)
let val = form.data
if(val.provider_type){
this.newPractitionerForm.get('profession').setValue(val.provider_type)
}
if(val.identifier){
this.newPractitionerForm.get('identifier').setValue( val.identifier);
}
if(form.data.provider_phone){
this.newPractitionerForm.get('phone').setValue( val.provider_phone);
}
if(val.provider_fax){
this.newPractitionerForm.get('fax').setValue(val.provider_fax);
}
if(val.provider_address){
let addressGroup = this.newPractitionerForm.get('address')
addressGroup.get('line1').setValue(val.provider_address.line1)
addressGroup.get('line2').setValue(val.provider_address.line2)
addressGroup.get('city').setValue(val.provider_address.city)
addressGroup.get('state').setValue(val.provider_address.state)
addressGroup.get('zip').setValue(val.provider_address.zip)
addressGroup.get('country').setValue(val.provider_address.country)
}
if(val.text) {
this.newPractitionerForm.get('name').setValue( val.text);
}
});
this.newPractitionerForm = new FormGroup({
identifier: new FormControl([]),
name: new FormControl(null, Validators.required),
profession: new FormControl(null, Validators.required),
phone: new FormControl(null, Validators.pattern('[- +()0-9]+')),
fax: new FormControl(null, Validators.pattern('[- +()0-9]+')),
email: new FormControl(null, Validators.email),
address: new FormGroup({
line1: new FormControl(null),
line2: new FormControl(null),
city: new FormControl(null),
state: new FormControl(null),
zip: new FormControl(null),
country: new FormControl(null),
})
})
}
private resetOrganizationForm(){
this.newOrganizationTypeaheadForm = new FormGroup({
data: new FormControl(null, Validators.required),
})
this.newOrganizationTypeaheadForm.valueChanges.subscribe(form => {
console.log("CHANGE Organization IN MODAL", form)
let val = form.data
if(val.provider_type) {
this.newOrganizationForm.get('type').setValue(val.provider_type)
}
if(val.identifier){
this.newOrganizationForm.get('identifier').setValue(val.identifier)
}
if(val.provider_phone){
this.newOrganizationForm.get('phone').setValue(val.provider_phone)
}
if(val.provider_fax){
this.newOrganizationForm.get('fax').setValue(val.provider_fax)
}
if(val.provider_address){
let addressGroup = this.newOrganizationForm.get('address')
addressGroup.get('line1').setValue(val.provider_address.line1)
addressGroup.get('line2').setValue(val.provider_address.line2)
addressGroup.get('city').setValue(val.provider_address.city)
addressGroup.get('state').setValue(val.provider_address.state)
addressGroup.get('zip').setValue(val.provider_address.zip)
addressGroup.get('country').setValue(val.provider_address.country)
}
if(val.text) {
this.newOrganizationForm.get('name').setValue(val.text)
}
});
this.newOrganizationForm = new FormGroup({
identifier: new FormControl([]),
name: new FormControl(null, Validators.required),
type: new FormControl(null, Validators.required),
phone: new FormControl(null, Validators.pattern('[- +()0-9]+')),
fax: new FormControl(null, Validators.pattern('[- +()0-9]+')),
email: new FormControl(null, Validators.email),
address: new FormGroup({
line1: new FormControl(null),
line2: new FormControl(null),
city: new FormControl(null),
state: new FormControl(null),
zip: new FormControl(null),
country: new FormControl(null),
})
})
}
}

View File

@ -0,0 +1,395 @@
import {
ResourceCreate,
ResourceCreateCondition, ResourceCreateMedication,
ResourceCreateOrganization, ResourceCreatePractitioner,
ResourceCreateProcedure
} from '../../models/fasten/resource_create';
import {
Condition,
Medication,
Procedure,
Location as FhirLocation,
BundleEntry,
Bundle,
Organization,
Practitioner, MedicationRequest, Patient, Encounter
} from 'fhir/r4';
import {uuidV4} from '../../../lib/utils/uuid';
interface ResourceStorage {
[resourceType: string]: {
[resourceId: string]: Condition | Patient | MedicationRequest | Organization | FhirLocation | Practitioner | Procedure | Encounter
}
}
export function GenerateR4Bundle(resourceCreate: ResourceCreate): Bundle {
let resourceStorage: ResourceStorage = {} //{"resourceType": {"resourceId": resourceData}}
resourceStorage = placeholderR4Patient(resourceStorage)
resourceStorage = resourceCreateConditionToR4Condition(resourceStorage, resourceCreate.condition)
for(let organization of resourceCreate.organizations) {
resourceStorage = resourceCreateOrganizationToR4Organization(resourceStorage, organization)
}
for(let practitioner of resourceCreate.practitioners) {
resourceStorage = resourceCreatePractitionerToR4Practitioner(resourceStorage, practitioner)
}
for(let medication of resourceCreate.medications) {
resourceStorage = resourceCreateMedicationToR4MedicationRequest(resourceStorage, medication)
}
for(let procedure of resourceCreate.procedures) {
resourceStorage = resourceCreateProcedureToR4Procedure(resourceStorage, procedure)
}
console.log("POPULATED RESOURCE STORAGE", resourceStorage)
let bundle = {
resourceType: 'Bundle',
type: 'transaction',
entry: [],
} as Bundle
for(let resourceType in resourceStorage) {
for(let resourceId in resourceStorage[resourceType]) {
let resource = resourceStorage[resourceType][resourceId]
bundle.entry.push({
fullUrl: `urn:uuid:${resource.id}`,
resource: resource,
})
}
}
return bundle
}
//Private methods
function placeholderR4Patient(resourceStorage: ResourceStorage): ResourceStorage {
resourceStorage['Patient'] = resourceStorage['Patient'] || {}
let patientResource = {
resourceType: 'Patient',
id: uuidV4(),
name: [
{
family: 'Placeholder',
given: ['Patient'],
}
],
} as Patient
resourceStorage['Patient'][patientResource.id] = patientResource
return resourceStorage
}
// this model is based on FHIR401 Resource Condition - http://hl7.org/fhir/R4/condition.html
function resourceCreateConditionToR4Condition(resourceStorage: ResourceStorage, resourceCreateCondition: ResourceCreateCondition): ResourceStorage {
resourceStorage['Condition'] = resourceStorage['Condition'] || {}
resourceStorage['Encounter'] = resourceStorage['Encounter'] || {}
let note = []
if (resourceCreateCondition.description) {
note.push({
text: resourceCreateCondition.description,
})
}
let conditionResource = {
subject: {
reference: `urn:uuid:${findPatient(resourceStorage).id}` //Patient
},
resourceType: 'Condition',
id: uuidV4(),
code: {
coding: resourceCreateCondition.data.identifier || [],
text: resourceCreateCondition.data.identifier[0].display,
},
clinicalStatus: {
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/condition-clinical",
"code": resourceCreateCondition.status,
}
]
},
onsetDateTime: `${new Date(resourceCreateCondition.started.year, resourceCreateCondition.started.month-1,resourceCreateCondition.started.day).toISOString()}`,
abatementDateTime: resourceCreateCondition.stopped ? `${new Date(resourceCreateCondition.stopped.year,resourceCreateCondition.stopped.month-1, resourceCreateCondition.stopped.day).toISOString()}` : null,
recordedDate: new Date().toISOString(),
note: note
} as Condition
resourceStorage['Condition'][conditionResource.id] = conditionResource
return resourceStorage
}
// this model is based on FHIR401 Resource Procedure - http://hl7.org/fhir/R4/procedure.html
function resourceCreateProcedureToR4Procedure(resourceStorage: ResourceStorage, resourceCreateProcedure: ResourceCreateProcedure): ResourceStorage {
resourceStorage['Procedure'] = resourceStorage['Procedure'] || {}
let note = []
if (resourceCreateProcedure.comment) {
note.push({
text: resourceCreateProcedure.comment,
})
}
let encounterResource = {
resourceType: 'Encounter',
id: uuidV4(),
status: "finished",
subject: {
reference: `urn:uuid:${findPatient(resourceStorage).id}` //Patient
},
participant: [
{
individual: {
reference: `urn:uuid:${resourceCreateProcedure.performer}` //Practitioner
}
}
],
period: {
start: `${new Date(resourceCreateProcedure.whendone.year, resourceCreateProcedure.whendone.month-1,resourceCreateProcedure.whendone.day).toISOString()}`,
end: `${new Date(resourceCreateProcedure.whendone.year, resourceCreateProcedure.whendone.month-1,resourceCreateProcedure.whendone.day).toISOString()}`,
},
reasonReference: [
{
reference: `urn:uuid:${findCondition(resourceStorage).id}` //Condition
}
],
serviceProvider: {
reference: `urn:uuid:${resourceCreateProcedure.location}` //Organization
}
} as Encounter
resourceStorage['Encounter'][encounterResource.id] = encounterResource
let procedureResource = {
subject: {
reference: `urn:uuid:${findPatient(resourceStorage).id}` //Patient
},
status: "completed",
resourceType: 'Procedure',
id: uuidV4(),
code: {
coding: resourceCreateProcedure.data.identifier || [],
text: resourceCreateProcedure.data.identifier[0].display,
},
performedDateTime: `${new Date(resourceCreateProcedure.whendone.year, resourceCreateProcedure.whendone.month-1,resourceCreateProcedure.whendone.day).toISOString()}`,
encounter: {
reference: `urn:uuid:${encounterResource.id}` //Encounter
},
reasonReference: [
{
reference: `urn:uuid:${findCondition(resourceStorage).id}` //Condition
}
],
performer: [
{
actor: {
reference: `urn:uuid:${resourceCreateProcedure.performer}` //Practitioner
},
onBehalfOf: {
reference: `urn:uuid:${resourceCreateProcedure.location}` //Organization
}
}
],
note: note,
} as Procedure
resourceStorage['Procedure'][procedureResource.id] = procedureResource
return resourceStorage
}
// this model is based on FHIR401 Resource Organization - http://hl7.org/fhir/R4/organization.html
function resourceCreateOrganizationToR4Organization(resourceStorage: ResourceStorage, resourceCreateOrganization: ResourceCreateOrganization): ResourceStorage {
resourceStorage['Organization'] = resourceStorage['Organization'] || {}
let telecom = []
if (resourceCreateOrganization.phone) {
telecom.push({
system: 'phone',
value: resourceCreateOrganization.phone,
})
}
if (resourceCreateOrganization.fax) {
telecom.push({
system: 'fax',
value: resourceCreateOrganization.fax,
})
}
if (resourceCreateOrganization.email) {
telecom.push({
system: 'email',
value: resourceCreateOrganization.email,
})
}
let organizationResource = {
resourceType: 'Organization',
id: resourceCreateOrganization.id,
name: resourceCreateOrganization.name,
identifier: resourceCreateOrganization.identifier || [],
type: [
{
coding: resourceCreateOrganization.type.identifier || [],
}
],
address: [
{
line: [resourceCreateOrganization.address.line1, resourceCreateOrganization.address.line2],
city: resourceCreateOrganization.address.city,
state: resourceCreateOrganization.address.state,
postalCode: resourceCreateOrganization.address.zip,
country: resourceCreateOrganization.address.country,
}
],
telecom: telecom,
active: true,
} as Organization
resourceStorage['Organization'][organizationResource.id] = organizationResource
return resourceStorage
}
// this model is based on FHIR401 Resource Practitioner - http://hl7.org/fhir/R4/practitioner.html
function resourceCreatePractitionerToR4Practitioner(resourceStorage: ResourceStorage, resourceCreatePractitioner: ResourceCreatePractitioner): ResourceStorage {
resourceStorage['Practitioner'] = resourceStorage['Practitioner'] || {}
let telecom = []
if (resourceCreatePractitioner.phone) {
telecom.push({
system: 'phone',
value: resourceCreatePractitioner.phone,
})
}
if (resourceCreatePractitioner.fax) {
telecom.push({
system: 'fax',
value: resourceCreatePractitioner.fax,
})
}
if (resourceCreatePractitioner.email) {
telecom.push({
system: 'email',
value: resourceCreatePractitioner.email,
})
}
let qualification = []
if(resourceCreatePractitioner.profession){
qualification.push({
code: {
coding: resourceCreatePractitioner.profession.identifier || [],
}
})
}
resourceCreatePractitioner.name.split(" ")
let practitionerResource = {
resourceType: 'Practitioner',
id: resourceCreatePractitioner.id,
name: [
{
text: resourceCreatePractitioner.name,
},
],
identifier: resourceCreatePractitioner.identifier || [],
address: [
{
line: [resourceCreatePractitioner.address.line1, resourceCreatePractitioner.address.line2],
city: resourceCreatePractitioner.address.city,
state: resourceCreatePractitioner.address.state,
postalCode: resourceCreatePractitioner.address.zip,
country: resourceCreatePractitioner.address.country,
}
],
telecom: telecom,
active: true,
qualification: qualification
} as Practitioner
resourceStorage['Practitioner'][practitionerResource.id] = practitionerResource
return resourceStorage
}
// this model is based on FHIR401 Resource Medication - http://hl7.org/fhir/R4/medication.html
function resourceCreateMedicationToR4MedicationRequest(resourceStorage: ResourceStorage, resourceCreateMedication: ResourceCreateMedication): ResourceStorage {
resourceStorage['MedicationRequest'] = resourceStorage['MedicationRequest'] || {}
let encounterResource = {
resourceType: 'Encounter',
id: uuidV4(),
status: "finished",
subject: {
reference: `urn:uuid:${findPatient(resourceStorage).id}` //Patient
},
participant: [
{
individual: {
reference: `urn:uuid:${resourceCreateMedication.requester}` //Practitioner
}
}
],
period: {
start: `${new Date(resourceCreateMedication.started.year, resourceCreateMedication.started.month-1,resourceCreateMedication.started.day).toISOString()}`,
end: resourceCreateMedication.stopped ? `${new Date(resourceCreateMedication.stopped.year, resourceCreateMedication.stopped.month-1,resourceCreateMedication.stopped.day).toISOString()}` : null,
},
reasonReference: [
{
reference: `urn:uuid:${findCondition(resourceStorage).id}` //Condition
}
],
} as Encounter
resourceStorage['Encounter'][encounterResource.id] = encounterResource
let medicationRequestResource = {
id: uuidV4(),
resourceType: 'MedicationRequest',
status: resourceCreateMedication.status,
statusReason: {
coding: resourceCreateMedication.whystopped.identifier || [],
},
intent: 'order',
medicationCodeableConcept: {
coding: resourceCreateMedication.data.identifier || [],
},
subject: {
reference: `urn:uuid:${findPatient(resourceStorage).id}` //Patient
},
encounter: {
reference: `urn:uuid:${encounterResource.id}` //Encounter
},
authoredOn: `${new Date(resourceCreateMedication.started.year,resourceCreateMedication.started.month-1,resourceCreateMedication.started.day).toISOString()}`,
requester: {
reference: `urn:uuid:${resourceCreateMedication.requester}` // Practitioner
},
reasonReference: [
{
reference: `urn:uuid:${findCondition(resourceStorage).id}` //Condition
},
],
note: [
{
text: resourceCreateMedication.instructions,
}
],
dispenseRequest: {
validityPeriod: {
start: `${new Date(resourceCreateMedication.started.year,resourceCreateMedication.started.month-1,resourceCreateMedication.started.day).toISOString()}`,
end: resourceCreateMedication.stopped ? `${new Date(resourceCreateMedication.stopped.year,resourceCreateMedication.stopped.month-1,resourceCreateMedication.stopped.day).toISOString()}` : null,
},
},
} as MedicationRequest
resourceStorage['MedicationRequest'][medicationRequestResource.id] = medicationRequestResource
return resourceStorage
}
function findCondition(resourceStorage: ResourceStorage): Condition {
let [conditionId] = Object.keys(resourceStorage['Condition'])
return resourceStorage['Condition'][conditionId] as Condition
}
function findPatient(resourceStorage: ResourceStorage): Patient {
let [patientId] = Object.keys(resourceStorage['Patient'])
return resourceStorage['Patient'][patientId] as Patient
}

View File

@ -8,6 +8,7 @@ import * as Oauth from '@panva/oauth4webapi';
import {SourceState} from '../models/fasten/source-state'; import {SourceState} from '../models/fasten/source-state';
import * as jose from 'jose'; import * as jose from 'jose';
import {UserRegisteredClaims} from '../models/fasten/user-registered-claims'; import {UserRegisteredClaims} from '../models/fasten/user-registered-claims';
import {uuidV4} from '../../lib/utils/uuid';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -22,7 +23,7 @@ export class AuthService {
//Third-party JWT auth, used by Fasten Cloud //Third-party JWT auth, used by Fasten Cloud
public async IdpConnect(idp_type: string) { public async IdpConnect(idp_type: string) {
const state = this.uuidV4() const state = uuidV4()
let sourceStateInfo = new SourceState() let sourceStateInfo = new SourceState()
sourceStateInfo.state = state sourceStateInfo.state = state
sourceStateInfo.source_type = idp_type sourceStateInfo.source_type = idp_type
@ -192,11 +193,4 @@ export class AuthService {
private setAuthToken(token: string) { private setAuthToken(token: string) {
localStorage.setItem(this.FASTEN_JWT_LOCALSTORAGE_KEY, token) localStorage.setItem(this.FASTEN_JWT_LOCALSTORAGE_KEY, token)
} }
private uuidV4(){
// @ts-ignore
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
);
}
} }

View File

@ -8,6 +8,7 @@ import {LighthouseSourceMetadata} from '../models/lighthouse/lighthouse-source-m
import * as Oauth from '@panva/oauth4webapi'; import * as Oauth from '@panva/oauth4webapi';
import {SourceState} from '../models/fasten/source-state'; import {SourceState} from '../models/fasten/source-state';
import {MetadataSource} from '../models/fasten/metadata-source'; import {MetadataSource} from '../models/fasten/metadata-source';
import {uuidV4} from '../../lib/utils/uuid';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -43,7 +44,7 @@ export class LighthouseService {
async generateSourceAuthorizeUrl(sourceType: string, lighthouseSource: LighthouseSourceMetadata): Promise<URL> { async generateSourceAuthorizeUrl(sourceType: string, lighthouseSource: LighthouseSourceMetadata): Promise<URL> {
const state = this.uuidV4() const state = uuidV4()
let sourceStateInfo = new SourceState() let sourceStateInfo = new SourceState()
sourceStateInfo.state = state sourceStateInfo.state = state
sourceStateInfo.source_type = sourceType sourceStateInfo.source_type = sourceType
@ -182,10 +183,4 @@ export class LighthouseService {
return parts.join(separator); return parts.join(separator);
} }
private uuidV4(){
// @ts-ignore
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
);
}
} }

View File

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { NlmClinicalTableSearchService } from './nlm-clinical-table-search.service';
describe('NlmClinicalTableSearchService', () => {
let service: NlmClinicalTableSearchService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(NlmClinicalTableSearchService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,11 @@
cursor: pointer; cursor: pointer;
} }
// adds a divider between select options
select > optgroup > .divider {
font-size: 1px;
background: rgba(0, 0, 0, 0.5);
}
//disable card //disable card
@ -31,6 +36,18 @@
z-index: 1000; z-index: 1000;
} }
// Form Validation
.form-control.ng-invalid.ng-touched {
border-color: #dc3545;
padding-right: calc(1.5em + .75rem) !important;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right calc(.375em + .1875rem) center;
background-size: calc(.75em + .375rem) calc(.75em + .375rem);
}
// Fhir Resource Cards // Fhir Resource Cards
.card-fhir-resource-popover { .card-fhir-resource-popover {

View File

@ -4,4 +4,5 @@ export interface CodingModel {
system?: string system?: string
value?: any value?: any
unit?: string unit?: string
type?: any
} }

View File

@ -16,6 +16,8 @@ export class ConditionModel extends FastenDisplayModel {
clinical_status: string | undefined clinical_status: string | undefined
date_recorded: string | undefined date_recorded: string | undefined
onset_datetime: string | undefined onset_datetime: string | undefined
abatement_datetime: string | undefined
note: string | undefined
constructor(fhirResource: any, fhirVersion?: fhirVersions, fastenOptions?: FastenOptions) { constructor(fhirResource: any, fhirVersion?: fhirVersions, fastenOptions?: FastenOptions) {
super(fastenOptions) super(fastenOptions)
@ -32,7 +34,11 @@ export class ConditionModel extends FastenDisplayModel {
this.severity_text = this.severity_text =
_.get(fhirResource, 'severity.coding.0.display') || _.get(fhirResource, 'severity.coding.0.display') ||
_.get(fhirResource, 'severity.text'); _.get(fhirResource, 'severity.text');
this.onset_datetime = _.get(fhirResource, 'onsetDateTime'); this.onset_datetime = _.get(fhirResource, 'onsetDateTime') ||
_.get(fhirResource, 'onsetPeriod.start') ||
_.get(fhirResource, 'assertedDate');
this.abatement_datetime = _.get(fhirResource, 'abatementDateTime') ||
_.get(fhirResource, 'abatementPeriod.end');
this.has_asserter = _.has(fhirResource, 'asserter'); this.has_asserter = _.has(fhirResource, 'asserter');
this.asserter = _.get(fhirResource, 'asserter'); this.asserter = _.get(fhirResource, 'asserter');
this.has_body_site = !!_.get(fhirResource, 'bodySite.0.coding.0.display'); this.has_body_site = !!_.get(fhirResource, 'bodySite.0.coding.0.display');
@ -54,6 +60,7 @@ export class ConditionModel extends FastenDisplayModel {
r4DTO(fhirResource:any){ r4DTO(fhirResource:any){
this.clinical_status = _.get(fhirResource, 'clinicalStatus.coding.0.code'); this.clinical_status = _.get(fhirResource, 'clinicalStatus.coding.0.code');
this.date_recorded = _.get(fhirResource, 'recordedDate'); this.date_recorded = _.get(fhirResource, 'recordedDate');
this.note = _.get(fhirResource, 'note.0.text');
}; };
resourceDTO(fhirResource:any, fhirVersion:fhirVersions){ resourceDTO(fhirResource:any, fhirVersion:fhirVersions){

View File

@ -8,7 +8,7 @@ import {FastenOptions} from '../fasten/fasten-options';
export class PractitionerModel extends FastenDisplayModel { export class PractitionerModel extends FastenDisplayModel {
identifier: string|undefined identifier: CodingModel[]|undefined
name: any|undefined name: any|undefined
gender: string|undefined gender: string|undefined
status: string|undefined status: string|undefined
@ -20,6 +20,7 @@ export class PractitionerModel extends FastenDisplayModel {
telecom: { system?: string, value?: string, use?: string }[]|undefined telecom: { system?: string, value?: string, use?: string }[]|undefined
address: string|undefined address: string|undefined
birthdate: string|undefined birthdate: string|undefined
qualification: { code: string, system: string }[]|undefined
constructor(fhirResource: any, fhirVersion?: fhirVersions, fastenOptions?: FastenOptions) { constructor(fhirResource: any, fhirVersion?: fhirVersions, fastenOptions?: FastenOptions) {
super(fastenOptions) super(fastenOptions)
@ -30,7 +31,7 @@ export class PractitionerModel extends FastenDisplayModel {
commonDTO(fhirResource:any){ commonDTO(fhirResource:any){
const id = _.get(fhirResource, 'id', ''); const id = _.get(fhirResource, 'id', '');
this.identifier = _.get(fhirResource, 'identifier', ''); this.identifier = _.get(fhirResource, 'identifier');
this.gender = _.get(fhirResource, 'gender', ''); this.gender = _.get(fhirResource, 'gender', '');
this.status = _.get(fhirResource, 'active') === true ? 'active' : ''; this.status = _.get(fhirResource, 'active') === true ? 'active' : '';
this.is_contact_data = _.has(fhirResource, 'contact[0]'); this.is_contact_data = _.has(fhirResource, 'contact[0]');
@ -39,6 +40,7 @@ export class PractitionerModel extends FastenDisplayModel {
name: _.get(fhirResource, 'contact[0].name'), name: _.get(fhirResource, 'contact[0].name'),
relationship: _.get(fhirResource, 'contact[0].relationship[0].text'), relationship: _.get(fhirResource, 'contact[0].relationship[0].text'),
}; };
this.qualification = _.get(fhirResource, 'qualification[0].code.coding');
}; };
dstu2DTO(fhirResource:any){ dstu2DTO(fhirResource:any){

View File

@ -0,0 +1,7 @@
export function uuidV4(){
// @ts-ignore
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
);
}

View File

@ -1760,6 +1760,11 @@
"@types/qs" "*" "@types/qs" "*"
"@types/serve-static" "*" "@types/serve-static" "*"
"@types/fhir@^0.0.35":
version "0.0.35"
resolved "https://registry.npmjs.org/@types/fhir/-/fhir-0.0.35.tgz#e598b99e6468fee556f0e78d398c13ad2571b1eb"
integrity sha512-y+VI3Y48xzZBM8AjXPq67EWbiY9VN4Cx5KzN8EplS0Zju8D2KahLXoK6P/PWlKyNTKvJBwemkHzJIocczLRkhQ==
"@types/http-proxy@^1.17.8": "@types/http-proxy@^1.17.8":
version "1.17.9" version "1.17.9"
resolved "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.9.tgz#7f0e7931343761efde1e2bf48c40f02f3f75705a" resolved "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.9.tgz#7f0e7931343761efde1e2bf48c40f02f3f75705a"