working Aidbox/Health Samurai integration. We can now convert CCDA records to FHIR.

This commit is contained in:
Jason Kulatunga 2024-01-24 13:16:07 -08:00
parent 27297f8eb6
commit 0b98758d2c
No known key found for this signature in database
7 changed files with 128 additions and 38 deletions

View File

@ -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>

View File

@ -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">-->
<!--&lt;!&ndash; <button (click)="sourceSyncHandler(modalSelectedSourceListItem.source)" type="button" class="btn btn-indigo">Sync</button>&ndash;&gt;-->
<!-- &lt;!&ndash; <button (click)="connectHandler($event, modalSelectedSourceListItem.source['source_type'])" type="button" class="btn btn-outline-light">Reconnect</button>&ndash;&gt;-->
<!-- <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>

View File

@ -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
})
}
} }

View File

@ -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();
});
});

View File

@ -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