Adding ability to attach documents (PDFs, text, notes, DICOM) to manually created conditions. (#108)

This commit is contained in:
Jason Kulatunga 2023-03-17 20:25:55 -07:00 committed by GitHub
parent 003e713ccf
commit 41cea8601f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 766 additions and 77 deletions

View File

@ -46,7 +46,7 @@
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"sourceMap": true,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,

View File

@ -29,6 +29,7 @@
"@fortawesome/free-regular-svg-icons": "^6.2.0",
"@fortawesome/free-solid-svg-icons": "^6.2.0",
"@ng-bootstrap/ng-bootstrap": "10.0.0",
"@ng-select/ng-select": "9.1.0",
"@panva/oauth4webapi": "1.2.0",
"@swimlane/ngx-datatable": "^20.0.0",
"@types/fhir": "^0.0.35",

View File

@ -31,6 +31,7 @@ import { ReportLabsComponent } from './pages/report-labs/report-labs.component';
import {PipesModule} from './pipes/pipes.module';
import { ResourceCreatorComponent } from './pages/resource-creator/resource-creator.component';
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { NgSelectModule } from '@ng-select/ng-select';
@NgModule({
declarations: [
@ -62,7 +63,8 @@ import { InfiniteScrollModule } from 'ngx-infinite-scroll';
HighlightModule,
MomentModule,
PipesModule,
InfiniteScrollModule
InfiniteScrollModule,
NgSelectModule
],
providers: [
{

View File

@ -23,6 +23,7 @@ import {FastenDisplayModel} from '../../../../lib/models/fasten/fasten-display-m
import {ProcedureComponent} from '../resources/procedure/procedure.component';
import {DiagnosticReportComponent} from '../resources/diagnostic-report/diagnostic-report.component';
import {PractitionerComponent} from '../resources/practitioner/practitioner.component';
import {DocumentReferenceComponent} from '../resources/document-reference/document-reference.component';
@Component({
selector: 'fhir-resource',
@ -105,9 +106,9 @@ export class FhirResourceComponent implements OnInit, OnChanges {
case "DiagnosticReport": {
return DiagnosticReportComponent;
}
// case "DocumentReference": {
// return ListDocumentReferenceComponent;
// }
case "DocumentReference": {
return DocumentReferenceComponent;
}
// case "Encounter": {
// return ListEncounterComponent;
// }

View File

@ -1,8 +1,10 @@
<ng-container [ngTemplateOutlet]="
displayModel?.content_type == 'application/pdf' ? showPdf :
displayModel?.content_type == 'image/jpeg' ? showImg :
displayModel?.content_type == 'text/markdown' ? showMarkdown :
displayModel?.content_type == 'text/plain' ? showText :
(displayModel?.content_type == 'text/html' || displayModel?.content_type == 'application/html') ? showHtml :
(displayModel?.content_type == 'application/xml' || displayModel?.content_type == 'application/json') ? showText :
(displayModel?.content_type == 'application/xml' || displayModel?.content_type == 'application/json') ? showHighlight :
(displayModel?.content_type == 'image/jpeg' || displayModel?.content_type == 'image/png') ? showImg :
showEmpty
"></ng-container>
@ -15,6 +17,12 @@
<ng-template #showHtml>
<fhir-html [displayModel]="displayModel"></fhir-html>
</ng-template>
<ng-template #showMarkdown>
<fhir-markdown [displayModel]="displayModel"></fhir-markdown>
</ng-template>
<ng-template #showHighlight>
<pre><code [languages]="['json', 'xml']" [highlight]="displayModel | json"></code></pre>
</ng-template>
<ng-template #showText>
<fhir-binary-text [displayModel]="displayModel"></fhir-binary-text>
</ng-template>

View File

@ -0,0 +1,25 @@
<div class="card card-fhir-resource" >
<div class="card-header" (click)="isCollapsed = ! isCollapsed">
<div>
<h6 class="card-title">{{displayModel?.category?.text}}</h6>
<p class="card-text tx-gray-400" *ngIf="displayModel?.created_at"><strong>Created at</strong> {{displayModel?.created_at | date}}</p>
</div>
<fhir-ui-badge class="float-right" [status]="displayModel?.status">{{displayModel?.status}}</fhir-ui-badge>
<!-- <div class="btn-group">-->
<!-- <button class="btn active">Day</button>-->
<!-- <button class="btn">Week</button>-->
<!-- <button class="btn">Month</button>-->
<!-- </div>-->
</div>
<div #collapse="ngbCollapse" [(ngbCollapse)]="isCollapsed" class="card-body">
<p class="az-content-text mg-b-20">An action that is or was performed on or for a patient, practitioner, device, organization, or location. For example, this can be a physical intervention on a patient like an operation, or less invasive like long term services, counseling, or hypnotherapy.</p>
<fhir-ui-table [displayModel]="displayModel" [tableData]="tableData"></fhir-ui-table>
<div *ngIf="!showDetails">
<fhir-binary *ngFor="let binaryModel of displayModel.content" [displayModel]="binaryModel"></fhir-binary>
</div>
</div>
<div *ngIf="showDetails" class="card-footer">
<a class="float-right" routerLink="/source/{{displayModel?.source_id}}/resource/{{displayModel?.source_resource_id}}">details</a>
</div>
</div>

View File

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

View File

@ -0,0 +1,51 @@
import {ChangeDetectorRef, Component, Input, OnInit} from '@angular/core';
import {DiagnosticReportModel} from '../../../../../lib/models/resources/diagnostic-report-model';
import {TableRowItem, TableRowItemDataType} from '../../common/table/table-row-item';
import {Router} from '@angular/router';
import {DocumentReferenceModel} from '../../../../../lib/models/resources/document-reference-model';
import {FhirResourceComponentInterface} from '../../fhir-resource/fhir-resource-component-interface';
@Component({
selector: 'app-document-reference',
templateUrl: './document-reference.component.html',
styleUrls: ['./document-reference.component.scss']
})
export class DocumentReferenceComponent implements OnInit, FhirResourceComponentInterface {
@Input() displayModel: DocumentReferenceModel
@Input() showDetails: boolean = true
isCollapsed: boolean = false
tableData: TableRowItem[] = []
constructor(public changeRef: ChangeDetectorRef, public router: Router) {}
ngOnInit(): void {
this.tableData = [
{
label: 'Description',
data: this.displayModel?.description,
enabled: !!this.displayModel?.description,
},
{
label: 'Category',
data: this.displayModel?.category?.coding,
data_type: TableRowItemDataType.CodingList,
enabled: !!this.displayModel?.category,
},
// {
// label: 'Performer',
// data: this.displayModel?.performer,
// data_type: TableRowItemDataType.Reference,
// enabled: this.displayModel?.has_performer,
// },
// {
// label: 'Conclusion',
// data: this.displayModel?.conclusion,
// enabled: !!this.displayModel?.conclusion,
// },
];
}
markForCheck(){
this.changeRef.markForCheck()
}
}

View File

@ -1,7 +1,7 @@
<div class="card card-fhir-resource" >
<div class="card-header" (click)="isCollapsed = ! isCollapsed">
<div>
<h6 class="card-title">{{displayModel?.medication_reference.display}}</h6>
<h6 class="card-title">{{displayModel?.medication_reference?.display}}</h6>
<p class="card-text tx-gray-400" *ngIf="displayModel?.sort_date"><strong>Start date</strong> {{displayModel?.sort_date | date}}</p>
</div>
<fhir-ui-badge class="float-right" [status]="displayModel?.status">{{displayModel?.status}}</fhir-ui-badge>

View File

@ -22,6 +22,8 @@ export enum NlmSearchType {
Vaccine = 'Vaccine',
Countries = 'Countries',
AttachmentFileType = 'AttachmentFileType',
AttachmentCategory = 'AttachmentCategory',
PrePopulated = 'PrePopulated'
}
@ -79,6 +81,17 @@ export class NlmTypeaheadComponent implements ControlValueAccessor {
searchOpFn = this.nlmClinicalTableSearchService.searchAllergyReaction
this.openOnFocus = true
break
case NlmSearchType.AttachmentFileType:
searchOpFn = this.nlmClinicalTableSearchService.searchAttachmentFileType
this.openOnFocus = true
this.idResult = true
this.editable = false
break
case NlmSearchType.AttachmentCategory:
searchOpFn = this.nlmClinicalTableSearchService.searchAttachmentCategory
this.openOnFocus = true
this.editable = false
break
case NlmSearchType.Condition:
searchOpFn = this.nlmClinicalTableSearchService.searchCondition
break

View File

@ -64,6 +64,7 @@ import { DiagnosticReportComponent } from './fhir/resources/diagnostic-report/di
import { PractitionerComponent } from './fhir/resources/practitioner/practitioner.component';
import {PipesModule} from '../pipes/pipes.module';
import { NlmTypeaheadComponent } from './nlm-typeahead/nlm-typeahead.component';
import { DocumentReferenceComponent } from './fhir/resources/document-reference/document-reference.component';
@NgModule({
imports: [
@ -136,6 +137,7 @@ import { NlmTypeaheadComponent } from './nlm-typeahead/nlm-typeahead.component';
DiagnosticReportComponent,
PractitionerComponent,
NlmTypeaheadComponent,
DocumentReferenceComponent,
],
exports: [
ComponentsSidebarComponent,
@ -186,7 +188,8 @@ import { NlmTypeaheadComponent } from './nlm-typeahead/nlm-typeahead.component';
ProcedureComponent,
DiagnosticReportComponent,
PractitionerComponent,
NlmTypeaheadComponent
NlmTypeaheadComponent,
DocumentReferenceComponent,
]
})

View File

@ -129,7 +129,8 @@ export interface ResourceCreate {
"medications": ResourceCreateMedication[],
"procedures": ResourceCreateProcedure[],
"practitioners": ResourceCreatePractitioner[],
"organizations": ResourceCreateOrganization[]
"organizations": ResourceCreateOrganization[],
"attachments": ResourceCreateAttachment[],
}
export interface ResourceCreateCondition {
@ -155,6 +156,7 @@ export interface ResourceCreateMedication {
"whystopped": NlmSearchResults
"requester": string,
"instructions": string
"attachments": ResourceCreateAttachment[],
}
export interface ResourceCreateProcedure {
@ -162,7 +164,8 @@ export interface ResourceCreateProcedure {
"whendone": ResourceCreateDate,
"comment": string,
"performer": string,
"location": string
"location": string,
"attachments": ResourceCreateAttachment[],
}
export interface ResourceCreatePractitioner {
@ -187,6 +190,18 @@ export interface ResourceCreateOrganization {
"address": Address,
}
export interface ResourceCreateAttachment {
"id"?: string,
"identifier": CodingModel[]
"name": string,
"category": NlmSearchResults,
"file_type": string,
"file_name": string,
"file_content": string,
"file_size": number,
}
export interface Address {
line1?: string
line2?: string

View File

@ -134,6 +134,21 @@
<textarea formControlName="instructions" rows="3" class="form-control" placeholder="Textarea"></textarea>
</div><!-- col -->
</div><!-- row -->
<div class="row row-sm mg-t-20">
<div class="col-12 mg-t-10 mg-lg-t-0">
<p class="mg-b-10">Attachments</p>
</div><!-- col -->
<div class="col-1">
<button type="button" (click)="openAttachmentModal(attachmentCreateModal, medicationGroup, 'attachments')" class="btn btn-indigo btn-with-icon">
<i class="fas fa-paperclip"></i> Add
</button>
</div>
<div class="col-11">
<ng-select class="ng-select-form-control" [readonly]="!attachments.controls.length" appendTo="body" formControlName="attachments" placeholder="Select Attachment" [multiple]="true" [hideSelected]="true">
<ng-option *ngFor="let attachment of attachments.controls; let i = index" [value]="attachment.value.id">{{attachment.value.name}} ({{attachment.value.file_name}})</ng-option>
</ng-select>
</div>
</div>
</div>
</ng-container>
<div class="row pt-2">
@ -206,6 +221,22 @@
<textarea formControlName="comment" class="form-control" placeholder="Input box" rows="3"></textarea>
</div><!-- col -->
</div><!-- row -->
<div class="row row-sm mg-t-20">
<div class="col-12 mg-t-10 mg-lg-t-0">
<p class="mg-b-10">Attachments</p>
</div><!-- col -->
<div class="col-1">
<button type="button" (click)="openAttachmentModal(attachmentCreateModal, procedureGroup, 'attachments')" class="btn btn-indigo btn-with-icon">
<i class="fas fa-paperclip"></i> Add
</button>
</div>
<div class="col-11">
<ng-select class="ng-select-form-control" [readonly]="!attachments.controls.length" formControlName="attachments" placeholder="Select Attachment" [multiple]="true" [hideSelected]="true">
<ng-option *ngFor="let attachment of attachments.controls; let i = index" [value]="attachment.value.id">{{attachment.value.name}} ({{attachment.value.file_name}})</ng-option>
</ng-select>
</div>
</div>
</div>
</ng-container>
@ -238,6 +269,9 @@
<ng-container formArrayName="practitioners">
<div class="card mg-t-10 pd-20" [formGroup]="practitionerGroup" *ngFor="let practitionerGroup of practitioners.controls; let i = index">
<div *ngIf="debugMode" class="alert alert-warning">
<strong>Practitioner Status: {{practitionerGroup.status}}</strong>
</div>
<div class="tx-right">
<span class="cursor-pointer" (click)="deletePractitioner(i)" aria-hidden="true"><i class="fas fa-trash"></i></span>
</div>
@ -305,6 +339,9 @@
<ng-container formArrayName="locations">
<div class="card mg-t-10 pd-20" [formGroup]="organizationGroup" *ngFor="let organizationGroup of organizations.controls; let i = index">
<div *ngIf="debugMode" class="alert alert-warning">
<strong>Organization Status: {{organizationGroup.status}}</strong>
</div>
<div class="tx-right">
<span class="cursor-pointer" (click)="deleteOrganization(i)" aria-hidden="true"><i class="fas fa-trash"></i></span>
</div>
@ -364,20 +401,42 @@
</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 -->
<ng-container formArrayName="locations">
<div class="card mg-t-10 pd-20" [formGroup]="attachmentGroup" *ngFor="let attachmentGroup of attachments.controls; let i = index">
<div *ngIf="debugMode" class="alert alert-warning">
<strong>Attachment Status: {{attachmentGroup.status}}</strong>
</div>
<div class="tx-right">
<span class="cursor-pointer" (click)="deleteAttachment(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">Category</p>
<app-nlm-typeahead formControlName="category" searchType="AttachmentCategory" [debugMode]="debugMode"></app-nlm-typeahead>
</div><!-- col -->
<div class="col-lg mg-t-10 mg-lg-t-0">
<p class="mg-b-10">File Type</p>
<input readonly class="form-control" formControlName="file_type"/>
</div><!-- col -->
</div><!-- row -->
</div>
</ng-container>
<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>
<button type="button" (click)="openAttachmentModal(attachmentCreateModal)" class="btn btn-outline-indigo btn-block">Create Attachment/Document</button>
</div>
</div>
</div>
@ -541,3 +600,48 @@
<button type="button" class="btn btn-az-primary" (click)="modal.dismiss()">Add Location</button>
</div>
</ng-template>
<ng-template #attachmentCreateModal let-modal>
<div class="modal-header">
<h4 class="modal-title" id="modal-attachment">New Attachment</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]="newAttachmentForm.getRawValue() | json"></code></pre>
<strong>New Attachment Form Status: {{ newAttachmentForm.status }}</strong>
</div>
<div class="row row-sm">
<ng-container [formGroup]="newAttachmentForm">
<div class="col-12">
<p class="mg-b-10">Name<span ngbTooltip="required" class="text-danger">*</span></p>
<input formControlName="name" class="form-control" type="text">
</div><!-- col -->
<div class="col-6 mg-t-10 mg-lg-t-0">
<p class="mg-b-10">Category<span ngbTooltip="required" class="text-danger">*</span></p>
<app-nlm-typeahead formControlName="category" searchType="AttachmentCategory" [debugMode]="debugMode"></app-nlm-typeahead>
</div><!-- col -->
<div class="col-6 mg-t-10 mg-lg-t-0">
<p class="mg-b-10">File Type<span ngbTooltip="required" class="text-danger">*</span></p>
<app-nlm-typeahead formControlName="file_type" searchType="AttachmentFileType" [debugMode]="debugMode"></app-nlm-typeahead>
</div><!-- col -->
<div class="col-12 mg-t-10 mg-lg-t-0">
<p class="mg-b-10">File<span ngbTooltip="required" class="text-danger">*</span></p>
</div><!-- col -->
<div class="col-6 mg-t-10 mg-lg-t-0 mg-l-10">
<label for="customFile" class="custom-file-label">{{newAttachmentForm.get('file_name').value || 'Choose file'}}</label>
<input id="customFile" (change)="onAttachmentFileChange($event)" class="custom-file-input" formControlName="file_name" type="file">
</div><!-- col -->
</ng-container>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-az-primary" (click)="modal.dismiss()">Create Attachment</button>
</div>
</ng-template>

View File

@ -2,6 +2,7 @@ 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 {
ResourceCreateAttachment,
ResourceCreateOrganization,
ResourceCreatePractitioner,
} from '../../models/fasten/resource_create';
@ -74,6 +75,7 @@ export class ResourceCreatorComponent implements OnInit {
procedures: new FormArray([]),
practitioners: new FormArray([]),
organizations: new FormArray([]),
attachments: new FormArray([]),
});
this.resetOrganizationForm()
@ -95,6 +97,7 @@ export class ResourceCreatorComponent implements OnInit {
whystopped: new FormControl(null),
requester: new FormControl(null, Validators.required),
instructions: new FormControl(null),
attachments: new FormControl([]),
});
medicationGroup.get("data").valueChanges.subscribe(val => {
@ -118,7 +121,8 @@ export class ResourceCreatorComponent implements OnInit {
whendone: new FormControl(null, Validators.required),
performer: new FormControl(null),
location: new FormControl(null),
comment: new FormControl('')
comment: new FormControl(''),
attachments: new FormControl([]),
});
this.procedures.push(procedureGroup);
@ -181,7 +185,26 @@ export class ResourceCreatorComponent implements OnInit {
this.organizations.removeAt(index);
}
get attachments(): FormArray {
return this.form.controls["attachments"] as FormArray;
}
addAttachment(attachment: ResourceCreateAttachment){
const attachmentGroup = new FormGroup({
id: new FormControl(attachment.id, Validators.required),
name: new FormControl(attachment.name, Validators.required),
category: new FormControl(attachment.category, Validators.required),
file_type: new FormControl(attachment.file_type, Validators.required),
file_name: new FormControl(attachment.file_name, Validators.required),
file_content: new FormControl(attachment.file_content, Validators.required),
file_size: new FormControl(attachment.file_size),
});
this.attachments.push(attachmentGroup);
}
deleteAttachment(index: number) {
this.attachments.removeAt(index);
}
onSubmit() {
@ -212,6 +235,8 @@ export class ResourceCreatorComponent implements OnInit {
newOrganizationTypeaheadForm: FormGroup
newOrganizationForm: FormGroup //ResourceCreateOrganization
newAttachmentForm: FormGroup
openPractitionerModal(content, formGroup?: AbstractControl, controlName?: string) {
this.resetPractitionerForm()
this.modalService.open(content, {
@ -269,6 +294,37 @@ export class ResourceCreatorComponent implements OnInit {
);
}
openAttachmentModal(content, formGroup?: AbstractControl, controlName?: string) {
this.resetAttachmentForm()
this.modalService.open(content, {
ariaLabelledBy: 'modal-attachment',
beforeDismiss: () => {
console.log("validate Attachment form")
this.newAttachmentForm.markAllAsTouched()
return this.newAttachmentForm.valid
},
}).result.then(
() => {
console.log('Closed without saving');
},
() => {
console.log('Closing, saving form');
//add this to the list of organization
let result = this.newAttachmentForm.getRawValue()
result.id = uuidV4();
this.addAttachment(result);
if(formGroup && controlName){
//add this attachment id to the current FormArray
let controlArrayVal = formGroup.get(controlName).getRawValue();
controlArrayVal.push(result.id)
formGroup.get(controlName).setValue(controlArrayVal);
}
},
);
}
private resetPractitionerForm(){
this.newPractitionerTypeaheadForm = new FormGroup({
@ -376,4 +432,33 @@ export class ResourceCreatorComponent implements OnInit {
})
}
private resetAttachmentForm(){
this.newAttachmentForm = new FormGroup({
name: new FormControl(null, Validators.required),
category: new FormControl(null, Validators.required),
file_type: new FormControl(null, Validators.required),
file_name: new FormControl(null, Validators.required),
file_content: new FormControl(null, Validators.required),
file_size: new FormControl(null),
})
}
onAttachmentFileChange($event){
console.log("onAttachmentFileChange")
let fileInput = $event.target as HTMLInputElement;
if (fileInput.files && fileInput.files[0]) {
let reader = new FileReader();
reader.onloadend = () => {
// use a regex to remove data url part
const base64String = (reader.result as string).replace('data:', '').replace(/^.+,/, '');
this.newAttachmentForm.get('file_content').setValue(base64String)
};
reader.readAsDataURL(fileInput.files[0]);
this.newAttachmentForm.get('file_name').setValue(fileInput.files[0].name)
this.newAttachmentForm.get('file_size').setValue(fileInput.files[0].size)
}
}
}

View File

@ -1,6 +1,6 @@
import {
ResourceCreate,
ResourceCreateCondition, ResourceCreateMedication,
ResourceCreateCondition, ResourceCreateAttachment, ResourceCreateMedication,
ResourceCreateOrganization, ResourceCreatePractitioner,
ResourceCreateProcedure
} from '../../models/fasten/resource_create';
@ -12,13 +12,13 @@ import {
BundleEntry,
Bundle,
Organization,
Practitioner, MedicationRequest, Patient, Encounter
Practitioner, MedicationRequest, Patient, Encounter, DocumentReference, Media, DiagnosticReport, Reference
} from 'fhir/r4';
import {uuidV4} from '../../../lib/utils/uuid';
interface ResourceStorage {
[resourceType: string]: {
[resourceId: string]: Condition | Patient | MedicationRequest | Organization | FhirLocation | Practitioner | Procedure | Encounter
[resourceId: string]: Condition | Patient | MedicationRequest | Organization | FhirLocation | Practitioner | Procedure | Encounter | DocumentReference | Media | DiagnosticReport
}
}
@ -27,6 +27,21 @@ export function GenerateR4Bundle(resourceCreate: ResourceCreate): Bundle {
let resourceStorage: ResourceStorage = {} //{"resourceType": {"resourceId": resourceData}}
resourceStorage = placeholderR4Patient(resourceStorage)
resourceStorage = resourceCreateConditionToR4Condition(resourceStorage, resourceCreate.condition)
for(let attachment of resourceCreate.attachments) {
if(attachment.file_type == 'application/dicom' ||
attachment.category.id == '18726-0' || //Radiology studies (set)
attachment.category.id == '27897-8' || // Neuromuscular electrophysiology studies (set)
attachment.category.id == '18748-4' // Diagnostic imaging study
) {
//Diagnostic imaging study (DiagnosticReport -> Media)
resourceStorage = resourceAttachmentToR4DiagnosticReport(resourceStorage, attachment)
}
else {
resourceStorage = resourceAttachmentToR4DocumentReference(resourceStorage, attachment)
}
}
for(let organization of resourceCreate.organizations) {
resourceStorage = resourceCreateOrganizationToR4Organization(resourceStorage, organization)
}
@ -40,6 +55,12 @@ export function GenerateR4Bundle(resourceCreate: ResourceCreate): Bundle {
resourceStorage = resourceCreateProcedureToR4Procedure(resourceStorage, procedure)
}
//DocumentReference -> (Optional) Binary
//DiagnosticReport -> Media
//ImagingStudy
//ImagingSelection
console.log("POPULATED RESOURCE STORAGE", resourceStorage)
let bundle = {
@ -182,6 +203,11 @@ function resourceCreateProcedureToR4Procedure(resourceStorage: ResourceStorage,
reference: `urn:uuid:${findCondition(resourceStorage).id}` //Condition
}
],
report: (resourceCreateProcedure.attachments || []).map(attachmentId => {
return {
reference: `urn:uuid:${attachmentId}` //DocumentReference or DiagnosticReport
}
}),
performer: [
{
actor: {
@ -311,7 +337,7 @@ function resourceCreatePractitionerToR4Practitioner(resourceStorage: ResourceSto
return resourceStorage
}
// this model is based on FHIR401 Resource Medication - http://hl7.org/fhir/R4/medication.html
// this model is based on FHIR401 Resource Medication - https://www.hl7.org/fhir/R4/MedicationRequest.html
function resourceCreateMedicationToR4MedicationRequest(resourceStorage: ResourceStorage, resourceCreateMedication: ResourceCreateMedication): ResourceStorage {
resourceStorage['MedicationRequest'] = resourceStorage['MedicationRequest'] || {}
@ -362,6 +388,11 @@ function resourceCreateMedicationToR4MedicationRequest(resourceStorage: Resource
requester: {
reference: `urn:uuid:${resourceCreateMedication.requester}` // Practitioner
},
supportingInformation: (resourceCreateMedication.attachments || []).map((attachmentId) => {
return {
reference: `urn:uuid:${attachmentId}` //DocumentReference or DiagnosticReport
}
}),
reasonReference: [
{
reference: `urn:uuid:${findCondition(resourceStorage).id}` //Condition
@ -384,6 +415,98 @@ function resourceCreateMedicationToR4MedicationRequest(resourceStorage: Resource
return resourceStorage
}
function resourceAttachmentToR4DocumentReference(resourceStorage: ResourceStorage, resourceAttachment: ResourceCreateAttachment): ResourceStorage {
resourceStorage['DocumentReference'] = resourceStorage['DocumentReference'] || {}
let documentReferenceResource = {
id: resourceAttachment.id,
resourceType: 'DocumentReference',
status: 'current',
category: [
{
coding: resourceAttachment.category.identifier || [],
text: resourceAttachment.category.text,
}
],
// description: resourceAttachment.description,
subject: {
reference: `urn:uuid:${findPatient(resourceStorage).id}` //Patient
},
content: [
{
attachment: {
contentType: resourceAttachment.file_type,
data: resourceAttachment.file_content,
title: resourceAttachment.name,
}
}
],
context: [
{
related: [
{
reference: `urn:uuid:${findCondition(resourceStorage).id}` //Condition
}
]
}
]
// date: `${new Date(resourceDocumentReference.date.year,resourceDocumentReference.date.month-1,resourceDocumentReference.date.day).toISOString()}`,
} as DocumentReference
resourceStorage['DocumentReference'][documentReferenceResource.id] = documentReferenceResource
//TODO create Binary object?
return resourceStorage
}
function resourceAttachmentToR4DiagnosticReport(resourceStorage: ResourceStorage, resourceAttachment: ResourceCreateAttachment): ResourceStorage {
resourceStorage['Media'] = resourceStorage['Media'] || {}
let mediaResource = {
id: uuidV4(),
resourceType: 'Media',
status: 'completed',
type: {
coding: resourceAttachment.category.identifier || [],
display: resourceAttachment.category.text,
},
subject: {
reference: `urn:uuid:${findPatient(resourceStorage).id}` //Patient
},
content: {
contentType: resourceAttachment.file_type,
data: resourceAttachment.file_content,
title: resourceAttachment.name,
},
} as Media
resourceStorage['Media'][mediaResource.id] = mediaResource
resourceStorage['DiagnosticReport'] = resourceStorage['DiagnosticReport'] || {}
let diagnosticReportResource = {
id: resourceAttachment.id,
resourceType: 'DiagnosticReport',
status: 'final',
code: {
coding: resourceAttachment.category.identifier || [],
},
subject: {
reference: `urn:uuid:${findPatient(resourceStorage).id}` //Patient
},
media: [
{
link: {
reference: `urn:uuid:${mediaResource.id}` //Media
}
},
],
} as DiagnosticReport
resourceStorage['DiagnosticReport'][diagnosticReportResource.id] = diagnosticReportResource
return resourceStorage
}
function findCondition(resourceStorage: ResourceStorage): Condition {
let [conditionId] = Object.keys(resourceStorage['Condition'])
return resourceStorage['Condition'][conditionId] as Condition

View File

@ -1242,4 +1242,247 @@ export class NlmClinicalTableSearchService {
);
}
searchAttachmentFileType(searchTerm: string): Observable<NlmSearchResults[]> {
let searchOptions: NlmSearchResults[] = [
{
id: "application/json",
text: "Document - JSON"
},
{
id: "text/markdown",
text: "Document - Markdown"
},
{
id: "application/pdf",
text: "Document - PDF"
},
{
id: "application/dicom",
text: "Image - DICOM"
},
{
id: "text/csv",
text: "Document - CSV"
},
{
id: "image/png",
text: "Image - PNG"
},
{
id: "image/jpeg",
text: "Image - JPEG"
},
{
id: "text/plain",
text: "Document - Plain Text"
},
]
let result = searchTerm.length == 0 ? searchOptions : searchOptions.filter((v) => v['text'].toLowerCase().indexOf(searchTerm.toLowerCase()) > -1).slice(0, 10)
return of(result)
}
//https://build.fhir.org/valueset-referenced-item-category.html
searchAttachmentCategory(searchTerm: string): Observable<NlmSearchResults[]> {
//https://tx.fhir.org/r4/ValueSet/$expand?_format=json&filter=Referral&url=http://hl7.org/fhir/ValueSet/document-classcodes
let queryParams = {
'_format': 'json',
'filter':searchTerm,
'url': 'http://hl7.org/fhir/ValueSet/document-classcodes'
}
return this._httpClient.get<any>(`https://tx.fhir.org/r4/ValueSet/$expand`, {params: queryParams})
.pipe(
map((response) => {
return (response.expansion.contains || []).map((valueSetItem):NlmSearchResults => {
return {
id: valueSetItem.code,
identifier: [valueSetItem],
text: valueSetItem.display,
}
})
})
)
// let searchOptions: NlmSearchResults[] = [
// {
// id: "image",
// identifier: [{
// code: "image",
// display: "Image",
// system: "http://terminology.hl7.org/CodeSystem/media-category"
// }],
// text: "Image"
// },
// {
// id: "11485-0",
// identifier: [{
// code: "11485-0",
// display: "Anesthesia records",
// system: "http://loinc.org"
// }],
// text: "Anesthesia records"
// },
// {
// id: "11488-4",
// identifier: [{
// code: "11488-4",
// display: "Consult note",
// system: "http://loinc.org"
// }],
// text: "Consult note"
// },
// {
// id: "11490-0",
// identifier: [{
// code: "11490-0",
// display: "Physician Discharge summary",
// system: "http://loinc.org"
// }],
// text: "Physician Discharge summary"
// },
// {
// id: "11502-2",
// identifier: [{
// code: "11502-2",
// display: "Laboratory report",
// system: "http://loinc.org"
// }],
// text: "Laboratory report"
// },
// {
// id: "11504-8",
// identifier: [{
// code: "11504-8",
// display: "Surgical operation note",
// system: "http://loinc.org"
// }],
// text: "Surgical operation note"
// },
// {
// id: "11506-3",
// identifier: [{
// code: "11506-3",
// display: "Progress note",
// system: "http://loinc.org"
// }],
// text: "Progress note"
// },
// {
// id: "11505-5",
// identifier: [{
// code: "11505-5",
// display: "Physician procedure note",
// system: "http://loinc.org"
// }],
// text: "Physician procedure note"
// },
// {
// id: "11524-6",
// identifier: [{
// code: "11524-6",
// display: "EKG study",
// system: "http://loinc.org"
// }],
// text: "EKG study"
// },
// {
// id: "11526-1",
// identifier: [{
// code: "11526-1",
// display: "Pathology study",
// system: "http://loinc.org"
// }],
// text: "Pathology study"
// },
// {
// id: "11527-9",
// identifier: [{
// code: "11527-9",
// display: "Psychiatry study",
// system: "http://loinc.org"
// }],
// text: "Psychiatry study"
// },
// {
// id: "11543-6",
// identifier: [{
// code: "11543-6",
// display: "Nursery records",
// system: "http://loinc.org"
// }],
// text: "Nursery records"
// },
// {
// id: "11543-6",
// identifier: [{
// code: "11543-6",
// display: "Nursery records",
// system: "http://loinc.org"
// }],
// text: "Nursery records"
// },
// {
// id: "15508-5",
// identifier: [{
// code: "15508-5",
// display: "Labor and delivery records",
// system: "http://loinc.org"
// }],
// text: "Labor and delivery records"
// },
// {
// id: "18682-5",
// identifier: [{
// code: "18682-5",
// display: "Ambulance records",
// system: "http://loinc.org"
// }],
// text: "Ambulance records"
// },
// {
// id: "18748-4",
// identifier: [{
// code: "18748-4",
// display: "Diagnostic imaging study",
// system: "http://loinc.org"
// }],
// text: "Diagnostic imaging study"
// },
// {
// id: "18761-7",
// identifier: [{
// code: "18761-7",
// display: "Transfer summary note",
// system: "http://loinc.org"
// }],
// text: "Transfer summary note"
// },
// {
// id: "18776-5",
// identifier: [{
// code: "18776-5",
// display: "Plan of care note",
// system: "http://loinc.org"
// }],
// text: "Plan of care note"
// },
// {
// id: "18776-5",
// identifier: [{
// code: "18776-5",
// display: "Plan of care note",
// system: "http://loinc.org"
// }],
// text: "Plan of care note"
// }
// ]
// let result = searchTerm.length == 0 ? searchOptions : searchOptions.filter((v) => v['text'].toLowerCase().indexOf(searchTerm.toLowerCase()) > -1).slice(0, 10)
// return of(result)
}
}

View File

@ -188,3 +188,19 @@ select > optgroup > .divider {
}
// ng-select/select2 styles
ng-select.ng-select-form-control {
.ng-select-container {
border-radius: 0px;
height: 38px;
}
}
//ngTypeAhead
app-nlm-typeahead {
.dropdown-menu.show {
max-height: 350px;
overflow-y: scroll;
}
}

View File

@ -5,22 +5,20 @@ 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 {Attachment} from 'fhir/r4';
import {BinaryModel} from './binary-model';
export class DocumentReferenceModel extends FastenDisplayModel {
description: string | undefined
status: string | undefined
category: CodableConceptModel | undefined
doc_status: string | undefined
type_coding: CodingModel | undefined
class_coding: CodingModel | undefined
created_at: string | undefined
security_label_coding: CodingModel | undefined
content: {
url: string
isUrlBinaryResourceReference: boolean
size: string
formatCoding: CodingModel
} | undefined
content: BinaryModel[] | undefined
context: {
eventCoding: CodingModel
facilityTypeCoding: CodingModel
@ -79,47 +77,12 @@ export class DocumentReferenceModel extends FastenDisplayModel {
};
contentDTO(fhirResource: any, fhirVersion: fhirVersions){
this.content = _.get(fhirResource, 'content', []).map((item:any) => {
const attachmentUrl = _.get(item, 'attachment.url');
let url = attachmentUrl;
let isUrlBinaryResourceReference = false;
// Check if URL ends with "/Binary/someId". If so, swap the url for this reference, and change the flag to render different component.
// For now raw link to the resource won't open properly, so it's better to show more valuable info for the user.
const regex = /\/(Binary\/[\w-]+$)/gm;
// @ts-ignore
const matches = Array.from(attachmentUrl.matchAll(regex), m => m[1]);
if (matches.length > 0) {
url = matches[0];
isUrlBinaryResourceReference = true;
}
const size = _.get(item, 'attachment.size');
let formatCoding = null;
switch (fhirVersion) {
case fhirVersions.DSTU2: {
formatCoding = _.get(item, 'format[0]');
break;
}
case fhirVersions.STU3: {
formatCoding = _.get(item, 'format');
break;
}
case fhirVersions.R4: {
formatCoding = _.get(item, 'format');
break;
}
default:
throw Error('Unrecognized the fhir version property type.');
}
return {
url,
isUrlBinaryResourceReference,
size,
formatCoding,
};
console.log('INSIDE CONTENTDTO', fhirResource)
this.category = new CodableConceptModel(_.get(fhirResource, 'category[0]') || {});
this.content = _.get(fhirResource, 'content', []).map((content: any) => {
const attachment: Attachment = _.get(content, 'attachment');
const binaryModel = new BinaryModel(attachment, fhirVersion);
return binaryModel;
})
};
@ -127,19 +90,19 @@ export class DocumentReferenceModel extends FastenDisplayModel {
switch (fhirVersion) {
case fhirVersions.DSTU2: {
this.commonDTO(fhirResource)
this.contentDTO(fhirVersion, fhirResource)
this.contentDTO(fhirResource,fhirVersion)
this.dstu2DTO(fhirResource)
return
}
case fhirVersions.STU3: {
this.commonDTO(fhirResource)
this.contentDTO(fhirVersion, fhirResource)
this.contentDTO(fhirResource,fhirVersion)
this.stu3DTO(fhirResource)
return
}
case fhirVersions.R4: {
this.commonDTO(fhirResource)
this.contentDTO(fhirVersion, fhirResource)
this.contentDTO(fhirResource,fhirVersion)
this.r4DTO(fhirResource)
return
}

View File

@ -215,5 +215,6 @@
@import '~@swimlane/ngx-datatable/assets/icons.css';
@import '~@circlon/angular-tree-component/css/angular-tree-component.css';
@import '~highlight.js/styles/github.css';
@import "~@ng-select/ng-select/themes/default.theme.css";
@import 'custom';

View File

@ -1500,6 +1500,13 @@
dependencies:
tslib "^2.1.0"
"@ng-select/ng-select@9.1.0":
version "9.1.0"
resolved "https://registry.npmjs.org/@ng-select/ng-select/-/ng-select-9.1.0.tgz#c400ea3e90c4dd2e7966e830fcf7d025ea0343fa"
integrity sha512-vxSRD2d84H39eqtTJaethlpQ+xkJUU8epQNUr3yPiah23z8MBCqSDE1t0chxi+rXJz7+xoC9qFa1aYnUVFan4w==
dependencies:
tslib "^2.3.1"
"@ngtools/webpack@14.2.10":
version "14.2.10"
resolved "https://registry.npmjs.org/@ngtools/webpack/-/webpack-14.2.10.tgz#d33ff1147d01bd1f5d936a3d1744c81a28e2ca6a"
@ -7216,6 +7223,11 @@ tslib@^2.0.0, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0:
resolved "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e"
integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==
tslib@^2.3.1:
version "2.5.0"
resolved "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf"
integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==
tslint@~6.1.0:
version "6.1.3"
resolved "https://registry.npmjs.org/tslint/-/tslint-6.1.3.tgz#5c23b2eccc32487d5523bd3a470e9aa31789d904"