working Aidbox/Health Samurai integration. We can now convert CCDA records to FHIR.
This commit is contained in:
parent
27297f8eb6
commit
0b98758d2c
|
@ -166,3 +166,4 @@ I'd also like to thank the following Corporate Sponsors:
|
||||||
|
|
||||||
<a href="https://depot.dev/"><img src="https://raw.githubusercontent.com/fastenhealth/docs/main/img/sponsors/depot.png" height="100px" /></a>
|
<a href="https://depot.dev/"><img src="https://raw.githubusercontent.com/fastenhealth/docs/main/img/sponsors/depot.png" height="100px" /></a>
|
||||||
<a style="padding-left:5px" href="https://www.macminivault.com/"><img src="https://raw.githubusercontent.com/fastenhealth/docs/main/img/sponsors/macminivault.png" height="100px" /></a>
|
<a style="padding-left:5px" href="https://www.macminivault.com/"><img src="https://raw.githubusercontent.com/fastenhealth/docs/main/img/sponsors/macminivault.png" height="100px" /></a>
|
||||||
|
<a style="padding-left:5px" href="https://www.health-samurai.io/"><img src="https://raw.githubusercontent.com/fastenhealth/docs/main/img/sponsors/health-samurai-logo.png" height="100px" /></a>
|
||||||
|
|
|
@ -33,8 +33,8 @@
|
||||||
|
|
||||||
<div class="row row-sm">
|
<div class="row row-sm">
|
||||||
<div class="col-lg">
|
<div class="col-lg">
|
||||||
<ngx-dropzone [multiple]="false" (change)="uploadSourceBundleHandler($event)" accept=".json,.phr,.ndjson,.jsonl">
|
<ngx-dropzone [multiple]="false" (change)="uploadSourceBundleHandler($event)" accept=".json,.phr,.ndjson,.jsonl,.xml,.ccda,.cda">
|
||||||
<ngx-dropzone-label>Select your EMR/EHR bundle. Must be in JSON format</ngx-dropzone-label>
|
<ngx-dropzone-label>Select your EMR/EHR bundle. Must be in JSON or XML format</ngx-dropzone-label>
|
||||||
<ngx-dropzone-preview *ngFor="let f of uploadedFile" [removable]="false">
|
<ngx-dropzone-preview *ngFor="let f of uploadedFile" [removable]="false">
|
||||||
<ngx-dropzone-label>{{ f.name }} ({{ f.type }})</ngx-dropzone-label>
|
<ngx-dropzone-label>{{ f.name }} ({{ f.type }})</ngx-dropzone-label>
|
||||||
</ngx-dropzone-preview>
|
</ngx-dropzone-preview>
|
||||||
|
@ -130,42 +130,41 @@
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
|
||||||
<!-- <ng-container *ngIf="modalSelectedSourceListItem?.metadata?.category?.length > 0 || modalSelectedSourceListItem?.metadata?.patient_access_description">-->
|
<ng-template #ccdaWarningModalRef let-modal>
|
||||||
<!-- <hr/>-->
|
|
||||||
<!-- <ng-container *ngIf="modalSelectedSourceListItem?.metadata?.patient_access_description">-->
|
<div class="modal-header">
|
||||||
<!-- <h6>About this Source</h6>-->
|
<h6 class="modal-title">Convert your records?</h6>
|
||||||
<!-- <p >{{modalSelectedSourceListItem?.metadata?.patient_access_description}}</p>-->
|
<button type="button" class="btn close" aria-label="Close" (click)="modal.dismiss('cancel')">
|
||||||
<!-- </ng-container>-->
|
<span aria-hidden="true">×</span>
|
||||||
<!-- <ng-container *ngIf="modalSelectedSourceListItem?.metadata.aliases?.length > 0">-->
|
</button>
|
||||||
<!-- <h6>Aliases</h6>-->
|
</div>
|
||||||
<!-- <ul>-->
|
|
||||||
<!-- <li *ngFor="let alias of modalSelectedSourceListItem?.metadata?.aliases">{{alias}}</li>-->
|
<div class="modal-body">
|
||||||
<!-- </ul>-->
|
<h6>Fasten Does not natively support <a href="https://en.wikipedia.org/wiki/Consolidated_Clinical_Document_Architecture" externalLink >CCDA</a> Health Records</h6>
|
||||||
<!-- </ng-container>-->
|
|
||||||
<!-- <ng-container *ngIf="modalSelectedSourceListItem?.metadata.platform_type">-->
|
<p>However we can convert it automatically using software generously donated by the team at <a href="https://www.health-samurai.io/" externalLink>Health Samurai</a><br/><br/>
|
||||||
<!-- <h6>Platform Type</h6>-->
|
This converter is hosted by <a href="https://www.fastenhealth.com/" externalLink>Fasten Health, Inc.</a> and is subject to our <a href="https://policy.fastenhealth.com/privacy_policy.html" externalLink>Privacy Policy</a>.
|
||||||
<!-- <p>{{modalSelectedSourceListItem?.metadata.platform_type}}</p>-->
|
Your data will <strong>NEVER</strong> be shared with any third parties.
|
||||||
<!-- </ng-container>-->
|
</p>
|
||||||
<!-- <ng-container *ngIf="modalSelectedSourceListItem?.metadata?.category?.length > 0">-->
|
|
||||||
<!-- <h6>Categories</h6>-->
|
<div class="d-flex h-100">
|
||||||
<!-- <ul>-->
|
<div class="mx-auto my-auto">
|
||||||
<!-- <li *ngFor="let cat of modalSelectedSourceListItem?.metadata?.category">{{cat | medicalSourcesCategoryLookup}}</li>-->
|
<a href="https://www.health-samurai.io/" externalLink>
|
||||||
<!-- </ul>-->
|
<img style="height:50px" src="assets/images/health-samurai-logo.png">
|
||||||
<!-- </ng-container>-->
|
</a>
|
||||||
<!-- </ng-container>-->
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button (click)="modal.close('convert')" type="button" class="btn btn-indigo">Convert</button>
|
||||||
|
<button (click)="modal.dismiss('cancel')" type="button" class="btn btn-outline-light">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <div class="modal-footer">-->
|
|
||||||
<!--<!– <button (click)="sourceSyncHandler(modalSelectedSourceListItem.source)" type="button" class="btn btn-indigo">Sync</button>–>-->
|
|
||||||
<!-- <!– <button (click)="connectHandler($event, modalSelectedSourceListItem.source['source_type'])" type="button" class="btn btn-outline-light">Reconnect</button>–>-->
|
|
||||||
<!-- <button type="button" (click)="connectHandler($event, modalSelectedSourceListItem)" class="btn btn-indigo">-->
|
|
||||||
<!-- <span *ngIf="status[modalSelectedSourceListItem?.metadata?.source_type] == 'authorize'" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>-->
|
|
||||||
<!-- Connect-->
|
|
||||||
<!-- </button>-->
|
|
||||||
<!-- <button (click)="modal.dismiss('Close click')" type="button" class="btn btn-outline-light">Close</button>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import {Component, EventEmitter, OnInit, Optional, Output} from '@angular/core';
|
import {Component, EventEmitter, OnInit, Optional, Output, ViewChild} from '@angular/core';
|
||||||
import {LighthouseService} from '../../services/lighthouse.service';
|
import {LighthouseService} from '../../services/lighthouse.service';
|
||||||
import {FastenApiService} from '../../services/fasten-api.service';
|
import {FastenApiService} from '../../services/fasten-api.service';
|
||||||
import {LighthouseSourceMetadata} from '../../models/lighthouse/lighthouse-source-metadata';
|
import {LighthouseSourceMetadata} from '../../models/lighthouse/lighthouse-source-metadata';
|
||||||
|
@ -17,6 +17,7 @@ import {MedicalSourcesFilter, MedicalSourcesFilterService} from '../../services/
|
||||||
import {FormControl, FormGroup} from '@angular/forms';
|
import {FormControl, FormGroup} from '@angular/forms';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import {PatientAccessBrand} from '../../models/patient-access-brands';
|
import {PatientAccessBrand} from '../../models/patient-access-brands';
|
||||||
|
import {PlatformService} from '../../services/platform.service';
|
||||||
|
|
||||||
export const sourceConnectWindowTimeout = 24*5000 //wait 2 minutes (5 * 24 = 120)
|
export const sourceConnectWindowTimeout = 24*5000 //wait 2 minutes (5 * 24 = 120)
|
||||||
|
|
||||||
|
@ -72,9 +73,14 @@ export class MedicalSourcesComponent implements OnInit {
|
||||||
modalSelectedBrandListItem: LighthouseBrandListDisplayItem | PatientAccessBrand = null;
|
modalSelectedBrandListItem: LighthouseBrandListDisplayItem | PatientAccessBrand = null;
|
||||||
modalCloseResult = '';
|
modalCloseResult = '';
|
||||||
|
|
||||||
|
|
||||||
|
// CCDA-FHIR modal
|
||||||
|
@ViewChild('ccdaWarningModalRef') ccdaWarningModalRef : any;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private lighthouseApi: LighthouseService,
|
private lighthouseApi: LighthouseService,
|
||||||
private fastenApi: FastenApiService,
|
private fastenApi: FastenApiService,
|
||||||
|
private platformApi: PlatformService,
|
||||||
private activatedRoute: ActivatedRoute,
|
private activatedRoute: ActivatedRoute,
|
||||||
private filterService: MedicalSourcesFilterService,
|
private filterService: MedicalSourcesFilterService,
|
||||||
private modalService: NgbModal,
|
private modalService: NgbModal,
|
||||||
|
@ -315,10 +321,28 @@ export class MedicalSourcesComponent implements OnInit {
|
||||||
* this function is used to process manually "uploaded" FHIR bundle files, adding them to the database.
|
* this function is used to process manually "uploaded" FHIR bundle files, adding them to the database.
|
||||||
* @param event
|
* @param event
|
||||||
*/
|
*/
|
||||||
public uploadSourceBundleHandler(event) {
|
public async uploadSourceBundleHandler(event) {
|
||||||
this.uploadedFile = [event.addedFiles[0]]
|
|
||||||
|
let processingFile = event.addedFiles[0] as File
|
||||||
|
this.uploadedFile = [processingFile]
|
||||||
|
|
||||||
|
if(processingFile.type == "text/xml"){
|
||||||
|
|
||||||
|
let shouldConvert = await this.showCcdaWarningModal()
|
||||||
|
if(shouldConvert){
|
||||||
|
let convertedFile = await this.platformApi.convertCcdaToFhir(processingFile).toPromise()
|
||||||
|
console.log("converted file: ", convertedFile.name)
|
||||||
|
processingFile = convertedFile
|
||||||
|
} else {
|
||||||
|
console.log("removing file from list")
|
||||||
|
this.uploadedFile = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
//TODO: handle manual bundles.
|
//TODO: handle manual bundles.
|
||||||
this.fastenApi.createManualSource(event.addedFiles[0]).subscribe(
|
this.fastenApi.createManualSource(processingFile).subscribe(
|
||||||
(respData) => {
|
(respData) => {
|
||||||
console.log("source manual source create response:", respData)
|
console.log("source manual source create response:", respData)
|
||||||
},
|
},
|
||||||
|
@ -329,6 +353,21 @@ export class MedicalSourcesComponent implements OnInit {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showCcdaWarningModal(): Promise<boolean> {
|
||||||
|
|
||||||
|
console.log("SHOWING CCDA Warning MODAL")
|
||||||
|
|
||||||
|
return this.modalService.open(this.ccdaWarningModalRef).result.then<boolean>(
|
||||||
|
(result) => {
|
||||||
|
//convert button clicked, .close()
|
||||||
|
return true //convert from CCDA -> FHIR.
|
||||||
|
}
|
||||||
|
).catch((reason) => {
|
||||||
|
// x or cancel button clicked, .dismiss()
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { PlatformService } from './platform.service';
|
||||||
|
|
||||||
|
describe('PlatformService', () => {
|
||||||
|
let service: PlatformService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(PlatformService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,35 @@
|
||||||
|
import {Inject, Injectable} from '@angular/core';
|
||||||
|
import {HTTP_CLIENT_TOKEN} from '../dependency-injection';
|
||||||
|
import {HttpClient} from '@angular/common/http';
|
||||||
|
import {Router} from '@angular/router';
|
||||||
|
import {AuthService} from './auth.service';
|
||||||
|
import {MedicalSourcesFilter} from './medical-sources-filter.service';
|
||||||
|
import {Observable} from 'rxjs';
|
||||||
|
import {LighthouseSourceSearch} from '../models/lighthouse/lighthouse-source-search';
|
||||||
|
import {environment} from '../../environments/environment';
|
||||||
|
import {ResponseWrapper} from '../models/response-wrapper';
|
||||||
|
import {map} from 'rxjs/operators';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class PlatformService {
|
||||||
|
|
||||||
|
constructor(private _httpClient: HttpClient) {}
|
||||||
|
|
||||||
|
public convertCcdaToFhir(ccdaFile: File): Observable<File> {
|
||||||
|
if(!ccdaFile || ccdaFile.size === 0){
|
||||||
|
throw new Error("Invalid CCDA file")
|
||||||
|
}
|
||||||
|
|
||||||
|
const endpointUrl = new URL(`https://api.platform.fastenhealth.com/v1/app/ccda-to-fhir`);
|
||||||
|
return this._httpClient.post<string>(endpointUrl.toString(), ccdaFile, { headers: {'Content-Type': 'application/cda+xml'} })
|
||||||
|
.pipe(
|
||||||
|
map((responseJson: string) => {
|
||||||
|
console.log("Converter RESPONSE", responseJson)
|
||||||
|
return new File([JSON.stringify(responseJson)], ccdaFile.name + ".converted.json", {type: "application/json", lastModified: ccdaFile.lastModified || Date.now()})
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 49 KiB |
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
Loading…
Reference in New Issue