added athena health client.

fixed header.
added a logout/signout function.
This commit is contained in:
Jason Kulatunga 2022-09-23 22:42:01 -07:00
parent a2b77e6182
commit 564fee9e90
18 changed files with 672 additions and 58 deletions

View File

@ -7,6 +7,7 @@ const (
SourceTypeManual SourceType = "manual" SourceTypeManual SourceType = "manual"
SourceTypeAetna SourceType = "aetna" SourceTypeAetna SourceType = "aetna"
SourceTypeAthena SourceType = "athena"
SourceTypeAnthem SourceType = "anthem" SourceTypeAnthem SourceType = "anthem"
SourceTypeCedarSinai SourceType = "cedarssinai" SourceTypeCedarSinai SourceType = "cedarssinai"
SourceTypeCerner SourceType = "cerner" SourceTypeCerner SourceType = "cerner"

View File

@ -7,6 +7,7 @@ import (
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg" "github.com/fastenhealth/fastenhealth-onprem/backend/pkg"
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/config" "github.com/fastenhealth/fastenhealth-onprem/backend/pkg/config"
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/hub/internal/fhir/aetna" "github.com/fastenhealth/fastenhealth-onprem/backend/pkg/hub/internal/fhir/aetna"
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/hub/internal/fhir/athena"
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/hub/internal/fhir/base" "github.com/fastenhealth/fastenhealth-onprem/backend/pkg/hub/internal/fhir/base"
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/hub/internal/fhir/cerner" "github.com/fastenhealth/fastenhealth-onprem/backend/pkg/hub/internal/fhir/cerner"
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/hub/internal/fhir/cigna" "github.com/fastenhealth/fastenhealth-onprem/backend/pkg/hub/internal/fhir/cigna"
@ -25,6 +26,8 @@ func NewClient(sourceType pkg.SourceType, ctx context.Context, appConfig config.
switch sourceType { switch sourceType {
case pkg.SourceTypeAetna: case pkg.SourceTypeAetna:
sourceClient, updatedSource, err = aetna.NewClient(ctx, appConfig, globalLogger, credentials, testHttpClient...) sourceClient, updatedSource, err = aetna.NewClient(ctx, appConfig, globalLogger, credentials, testHttpClient...)
case pkg.SourceTypeAthena:
sourceClient, updatedSource, err = athena.NewClient(ctx, appConfig, globalLogger, credentials, testHttpClient...)
case pkg.SourceTypeAnthem: case pkg.SourceTypeAnthem:
sourceClient, updatedSource, err = cigna.NewClient(ctx, appConfig, globalLogger, credentials, testHttpClient...) sourceClient, updatedSource, err = cigna.NewClient(ctx, appConfig, globalLogger, credentials, testHttpClient...)
case pkg.SourceTypeCerner: case pkg.SourceTypeCerner:

View File

@ -0,0 +1,69 @@
package athena
import (
"context"
"fmt"
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/config"
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/database"
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/hub/internal/fhir/base"
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/models"
"github.com/sirupsen/logrus"
"net/http"
)
type AthenaClient struct {
*base.FHIR401Client
}
func NewClient(ctx context.Context, appConfig config.Interface, globalLogger logrus.FieldLogger, source models.Source, testHttpClient ...*http.Client) (base.Client, *models.Source, error) {
baseClient, updatedSource, err := base.NewFHIR401Client(ctx, appConfig, globalLogger, source, testHttpClient...)
return AthenaClient{
baseClient,
}, updatedSource, err
}
func (c AthenaClient) SyncAll(db database.DatabaseRepository) error {
supportedResources := []string{
"AllergyIntolerance",
"CarePlan",
"CareTeam",
"Condition",
"Device",
"DiagnosticReport",
"DocumentReference",
"Encounter",
"Goal",
"Immunization",
"Location",
"Medication",
"MedicationRequest",
"Observation",
"Organization",
"Patient",
"Practitioner",
"Procedure",
"Provenance",
}
for _, resourceType := range supportedResources {
bundle, err := c.GetResourceBundle(fmt.Sprintf("%s?patient=%s", resourceType, c.Source.PatientId))
if err != nil {
return err
}
wrappedResourceModels, err := c.ProcessBundle(bundle)
if err != nil {
c.Logger.Infof("An error occurred while processing %s bundle %s", resourceType, c.Source.PatientId)
return err
}
//todo, create the resources in dependency order
for _, apiModel := range wrappedResourceModels {
err = db.UpsertResource(context.Background(), apiModel)
if err != nil {
return err
}
}
}
return nil
}

View File

@ -13,6 +13,7 @@ func GetMetadataSource(c *gin.Context) {
string(pkg.SourceTypeLogica): {Display: "Logica (Sandbox)", SourceType: pkg.SourceTypeLogica, Category: []string{"Sandbox"}, Supported: true}, string(pkg.SourceTypeLogica): {Display: "Logica (Sandbox)", SourceType: pkg.SourceTypeLogica, Category: []string{"Sandbox"}, Supported: true},
string(pkg.SourceTypeEpic): {Display: "Epic (Sandbox)", SourceType: pkg.SourceTypeEpic, Category: []string{"Sandbox"}, Supported: true}, string(pkg.SourceTypeEpic): {Display: "Epic (Sandbox)", SourceType: pkg.SourceTypeEpic, Category: []string{"Sandbox"}, Supported: true},
string(pkg.SourceTypeCerner): {Display: "Cerner (Sandbox)", SourceType: pkg.SourceTypeCerner, Category: []string{"Sandbox"}, Supported: true}, string(pkg.SourceTypeCerner): {Display: "Cerner (Sandbox)", SourceType: pkg.SourceTypeCerner, Category: []string{"Sandbox"}, Supported: true},
string(pkg.SourceTypeAthena): {Display: "Athena (Sandbox)", SourceType: pkg.SourceTypeAthena, Category: []string{"Sandbox"}, Supported: true},
// enabled // enabled
string(pkg.SourceTypeAetna): {Display: "Aetna", SourceType: pkg.SourceTypeAetna, Category: []string{"Insurance"}, Supported: true}, string(pkg.SourceTypeAetna): {Display: "Aetna", SourceType: pkg.SourceTypeAetna, Category: []string{"Insurance"}, Supported: true},

View File

@ -35,24 +35,8 @@ func CreateSource(c *gin.Context) {
} }
// after creating the source, we should do a bulk import // after creating the source, we should do a bulk import
sourceClient, updatedSource, err := hub.NewClient(sourceCred.SourceType, c, nil, logger, sourceCred) err = syncSourceResources(c, logger, databaseRepo, sourceCred)
if err != nil { if err != nil {
logger.Errorln("An error occurred while initializing hub client using source credential", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
return
}
if updatedSource != nil {
err := databaseRepo.CreateSource(c, updatedSource)
if err != nil {
logger.Errorln("An error occurred while updating source credential", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
return
}
}
err = sourceClient.SyncAll(databaseRepo)
if err != nil {
logger.Errorln("An error occurred while bulk import of resources from source", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false}) c.JSON(http.StatusInternalServerError, gin.H{"success": false})
return return
} }
@ -60,6 +44,28 @@ func CreateSource(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": true, "data": sourceCred}) c.JSON(http.StatusOK, gin.H{"success": true, "data": sourceCred})
} }
func SourceSync(c *gin.Context) {
logger := c.MustGet("LOGGER").(*logrus.Entry)
databaseRepo := c.MustGet("REPOSITORY").(database.DatabaseRepository)
logger.Infof("Get Source Credentials: %v", c.Param("sourceId"))
sourceCred, err := databaseRepo.GetSource(c, c.Param("sourceId"))
if err != nil {
logger.Errorln("An error occurred while retrieving source credential", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
return
}
// after creating the source, we should do a bulk import
err = syncSourceResources(c, logger, databaseRepo, *sourceCred)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
return
}
c.JSON(http.StatusOK, gin.H{"success": true, "data": sourceCred})
}
func CreateManualSource(c *gin.Context) { func CreateManualSource(c *gin.Context) {
logger := c.MustGet("LOGGER").(*logrus.Entry) logger := c.MustGet("LOGGER").(*logrus.Entry)
databaseRepo := c.MustGet("REPOSITORY").(database.DatabaseRepository) databaseRepo := c.MustGet("REPOSITORY").(database.DatabaseRepository)
@ -200,3 +206,27 @@ func RawRequestSource(c *gin.Context) {
} }
c.JSON(http.StatusOK, gin.H{"success": true, "data": resp}) c.JSON(http.StatusOK, gin.H{"success": true, "data": resp})
} }
////// private functions
func syncSourceResources(c *gin.Context, logger *logrus.Entry, databaseRepo database.DatabaseRepository, sourceCred models.Source) error {
// after creating the source, we should do a bulk import
sourceClient, updatedSource, err := hub.NewClient(sourceCred.SourceType, c, nil, logger, sourceCred)
if err != nil {
logger.Errorln("An error occurred while initializing hub client using source credential", err)
return err
}
if updatedSource != nil {
err := databaseRepo.CreateSource(c, updatedSource)
if err != nil {
logger.Errorln("An error occurred while updating source credential", err)
return err
}
}
err = sourceClient.SyncAll(databaseRepo)
if err != nil {
logger.Errorln("An error occurred while bulk import of resources from source", err)
return err
}
return nil
}

View File

@ -51,6 +51,7 @@ func (ae *AppEngine) Setup(logger *logrus.Entry) *gin.Engine {
secure.POST("/source/manual", handler.CreateManualSource) secure.POST("/source/manual", handler.CreateManualSource)
secure.GET("/source", handler.ListSource) secure.GET("/source", handler.ListSource)
secure.GET("/source/:sourceId", handler.GetSource) secure.GET("/source/:sourceId", handler.GetSource)
secure.POST("/source/:sourceId/sync", handler.SourceSync)
secure.GET("/source/:sourceId/summary", handler.GetSourceSummary) secure.GET("/source/:sourceId/summary", handler.GetSourceSummary)
secure.GET("/resource/fhir", handler.ListResourceFhir) // secure.GET("/resource/fhir", handler.ListResourceFhir) //
secure.GET("/resource/fhir/:resourceId", handler.GetResourceFhir) secure.GET("/resource/fhir/:resourceId", handler.GetResourceFhir)

View File

@ -65,24 +65,21 @@
</div><!-- dropdown-menu --> </div><!-- dropdown-menu -->
</div><!-- az-header-notification --> </div><!-- az-header-notification -->
<div class="dropdown az-profile-menu" ngbDropdown> <div class="dropdown az-profile-menu" ngbDropdown>
<a class="az-img-user" id="profileDropdown" ngbDropdownToggle><img src="assets/images/img1.jpg" alt="user"></a> <a class="az-img-user" id="profileDropdown" ngbDropdownToggle><img src="assets/logo/logo-text.png" alt="user"></a>
<div class="dropdown-menu" ngbDropdownMenu aria-labelledby="profileDropdown"> <div class="dropdown-menu" ngbDropdownMenu aria-labelledby="profileDropdown">
<div class="az-dropdown-header d-sm-none"> <div class="az-dropdown-header d-sm-none">
<a class="az-header-arrow" (click)="closeMenu($event)"><i class="icon ion-md-arrow-back"></i></a> <a class="az-header-arrow" (click)="closeMenu($event)"><i class="icon ion-md-arrow-back"></i></a>
</div> </div>
<div class="az-header-profile"> <div class="az-header-profile">
<div class="az-img-user"> <div class="az-img-user">
<img src="assets/images/img1.jpg" alt=""> <img src="assets/logo/logo-text.png" alt="">
</div><!-- az-img-user --> </div><!-- az-img-user -->
<h6>Aziana Pechon</h6> <h6>John Doe</h6>
<span>Premium Member</span> <span>Adminstrator</span>
</div><!-- az-header-profile --> </div><!-- az-header-profile -->
<a routerLink="/general-pages/profile" class="dropdown-item"><i class="typcn typcn-user-outline"></i> My Profile</a>
<a routerLink="/general-pages/profile" class="dropdown-item"><i class="typcn typcn-edit"></i> Edit Profile</a>
<a routerLink="/general-pages/profile" class="dropdown-item"><i class="typcn typcn-time"></i> Activity Logs</a> <a routerLink="/general-pages/profile" class="dropdown-item"><i class="typcn typcn-time"></i> Activity Logs</a>
<a routerLink="/general-pages/profile" class="dropdown-item"><i class="typcn typcn-cog-outline"></i> Account Settings</a> <a (click)="signOut($event)" class="dropdown-item"><i class="typcn typcn-power-outline"></i> Sign Out</a>
<a routerLink="/general-pages/signin" class="dropdown-item"><i class="typcn typcn-power-outline"></i> Sign Out</a>
</div><!-- dropdown-menu --> </div><!-- dropdown-menu -->
</div> </div>
</div><!-- az-header-right --> </div><!-- az-header-right -->

View File

@ -1,5 +1,6 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import {FastenApiService} from '../../services/fasten-api.service';
import { Router } from '@angular/router';
@Component({ @Component({
selector: 'app-header', selector: 'app-header',
templateUrl: './header.component.html', templateUrl: './header.component.html',
@ -7,7 +8,7 @@ import { Component, OnInit } from '@angular/core';
}) })
export class HeaderComponent implements OnInit { export class HeaderComponent implements OnInit {
constructor() { } constructor(private fastenApi: FastenApiService, private router: Router) { }
ngOnInit() { ngOnInit() {
} }
@ -22,4 +23,8 @@ export class HeaderComponent implements OnInit {
document.querySelector('body').classList.toggle('az-header-menu-show'); document.querySelector('body').classList.toggle('az-header-menu-show');
} }
signOut(e) {
this.fastenApi.logout()
this.router.navigate(['auth/signin']);
}
} }

View File

@ -76,9 +76,8 @@ export class ResourceListComponent implements OnInit, OnChanges {
getResources(): Observable<ResourceFhir[]>{ getResources(): Observable<ResourceFhir[]>{
if(!this.resourceListCache[this.resourceListType]){ if(this.resourceListType && !this.resourceListCache[this.resourceListType]){
// this resource type list has not been downloaded yet, do so now // this resource type list has not been downloaded yet, do so now
return this.fastenApi.getResources(this.resourceListType, this.source.id) return this.fastenApi.getResources(this.resourceListType, this.source.id)
.pipe(map((resourceList: ResourceFhir[]) => { .pipe(map((resourceList: ResourceFhir[]) => {
//cache this response so we can skip the request next time //cache this response so we can skip the request next time
@ -86,7 +85,7 @@ export class ResourceListComponent implements OnInit, OnChanges {
return resourceList return resourceList
})) }))
} else { } else {
return of(this.resourceListCache[this.resourceListType]) return of(this.resourceListCache[this.resourceListType] || [])
} }
} }

View File

@ -9,10 +9,10 @@
<h2 class="az-content-title">Connected Sources</h2> <h2 class="az-content-title">Connected Sources</h2>
<div class="row"> <div class="row">
<div *ngFor="let sourceData of connectedSourceList" (click)="connect($event, sourceData['source_type'])" class="col-sm-3 mg-b-20 px-3"> <div *ngFor="let sourceInfo of connectedSourceList" class="col-sm-3 mg-b-20 px-3">
<div class="card h-100 d-flex align-items-center justify-content-center p-3 rounded-0 cursor-pointer"> <div class="card h-100 d-flex align-items-center justify-content-center p-3 rounded-0 cursor-pointer">
<div class="card-body"> <div (click)="openModal(contentModalRef, sourceInfo)" class="card-body">
<img [src]="'assets/sources/'+sourceData['source_type']+'.png'" alt="client" class="img-fluid"> <img [src]="'assets/sources/'+sourceInfo.metadata['source_type']+'.png'" alt="client" class="img-fluid">
</div> </div>
</div> </div>
</div> </div>
@ -62,3 +62,29 @@
</div><!-- az-content-body --> </div><!-- az-content-body -->
</div><!-- container --> </div><!-- container -->
</div><!-- az-content --> </div><!-- az-content -->
<ng-template #contentModalRef let-modal>
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{modalSourceInfo.metadata["display"]}}</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="modal.dismiss('Cross click')">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<h6>Manage Source</h6>
<p>Existing connections can be "Synced", "Reconnected" or "Deleted"</p>
<ul>
<li><p><strong>Sync</strong> - Download all resources from this healthcare provider, storing them securely in Fasten</p></li>
<li><p><strong>Reconnect</strong> - If your healthcare connection has expired, you can use this button to reconnect</p></li>
<li><p><strong>Delete</strong> - Delete all resources for this healthcare provider. This will ONLY effect data stored in Fasten</p></li>
</ul>
</div>
<div class="modal-footer">
<button (click)="syncSource(modalSourceInfo.source)" type="button" class="btn btn-indigo">Sync</button>
<button type="button" class="btn btn-outline-light">Reconnect</button>
<button type="button" class="btn btn-outline-danger">Delete</button>
<button type="button" class="btn btn-outline-light">Close</button>
</div>
</ng-template>

View File

@ -11,6 +11,7 @@ import {Observable, of, throwError} from 'rxjs';
import {concatMap, delay, retryWhen} from 'rxjs/operators'; import {concatMap, delay, retryWhen} from 'rxjs/operators';
import * as FHIR from "fhirclient" import * as FHIR from "fhirclient"
import {MetadataSource} from '../../models/fasten/metadata-source'; import {MetadataSource} from '../../models/fasten/metadata-source';
import {NgbModal, ModalDismissReasons} from '@ng-bootstrap/ng-bootstrap';
export const retryCount = 24; //wait 2 minutes (5 * 24 = 120) export const retryCount = 24; //wait 2 minutes (5 * 24 = 120)
export const retryWaitMilliSeconds = 5000; //wait 5 seconds export const retryWaitMilliSeconds = 5000; //wait 5 seconds
@ -25,16 +26,20 @@ export class MedicalSourcesComponent implements OnInit {
constructor( constructor(
private lighthouseApi: LighthouseService, private lighthouseApi: LighthouseService,
private fastenApi: FastenApiService, private fastenApi: FastenApiService,
private modalService: NgbModal,
) { } ) { }
status: { [name: string]: string } = {} status: { [name: string]: string } = {}
metadataSources: {[name:string]: MetadataSource} = {} metadataSources: {[name:string]: MetadataSource} = {}
connectedSourceList = [] connectedSourceList:any[] = []
availableSourceList = [] availableSourceList:MetadataSource[] = []
uploadedFile: File[] = [] uploadedFile: File[] = []
closeResult = '';
modalSourceInfo:any = null;
ngOnInit(): void { ngOnInit(): void {
this.fastenApi.getMetadataSources().subscribe((metadataSources: {[name:string]: MetadataSource}) => { this.fastenApi.getMetadataSources().subscribe((metadataSources: {[name:string]: MetadataSource}) => {
this.metadataSources = metadataSources this.metadataSources = metadataSources
@ -46,7 +51,7 @@ export class MedicalSourcesComponent implements OnInit {
let isConnected = false let isConnected = false
for(const connectedSource of sourceList){ for(const connectedSource of sourceList){
if(connectedSource.source_type == sourceType){ if(connectedSource.source_type == sourceType){
this.connectedSourceList.push({"source_type": sourceType, "display": this.metadataSources[sourceType]["display"], "enabled": this.metadataSources[sourceType]["enabled"]}) this.connectedSourceList.push({source: connectedSource, metadata: this.metadataSources[sourceType]})
isConnected = true isConnected = true
break break
} }
@ -54,7 +59,7 @@ export class MedicalSourcesComponent implements OnInit {
if(!isConnected){ if(!isConnected){
//this source has not been found in the connected list, lets add it to the available list. //this source has not been found in the connected list, lets add it to the available list.
this.availableSourceList.push({"source_type": sourceType, "display": this.metadataSources[sourceType]["display"], "enabled": this.metadataSources[sourceType]["enabled"]}) this.availableSourceList.push(this.metadataSources[sourceType])
} }
} }
@ -141,6 +146,7 @@ export class MedicalSourcesComponent implements OnInit {
} }
//Create FHIR Client //Create FHIR Client
const sourceCredential: Source = { const sourceCredential: Source = {
source_type: sourceType, source_type: sourceType,
oauth_authorization_endpoint: connectData.oauth_authorization_endpoint, oauth_authorization_endpoint: connectData.oauth_authorization_endpoint,
@ -156,9 +162,12 @@ export class MedicalSourcesComponent implements OnInit {
access_token: payload.access_token, access_token: payload.access_token,
refresh_token: payload.refresh_token, refresh_token: payload.refresh_token,
id_token: payload.id_token, id_token: payload.id_token,
expires_at: getAccessTokenExpiration(payload, new BrowserAdapter()),
code_challenge: codeChallenge, code_challenge: codeChallenge,
code_verifier: codeVerifier, code_verifier: codeVerifier,
// @ts-ignore - in some cases the getAccessTokenExpiration is a string, which cases failures to store Source in db.
expires_at: parseInt(getAccessTokenExpiration(payload, new BrowserAdapter())),
} }
await this.fastenApi.createSource(sourceCredential).subscribe( await this.fastenApi.createSource(sourceCredential).subscribe(
@ -189,13 +198,54 @@ export class MedicalSourcesComponent implements OnInit {
// @ts-ignore // @ts-ignore
event.origin); event.origin);
}, 5000); }, 5000);
} }
uploadSourceBundle(event) {
this.uploadedFile = [event.addedFiles[0]]
this.fastenApi.createManualSource(event.addedFiles[0]).subscribe(
(respData) => {
console.log("source manual source create response:", respData)
},
(err) => {console.log(err)},
() => {
this.uploadedFile = []
}
)
}
openModal(contentModalRef, sourceInfo: any) {
this.modalSourceInfo = sourceInfo
let modalSourceInfo = this.modalSourceInfo
this.modalService.open(contentModalRef, {ariaLabelledBy: 'modal-basic-title'}).result.then((result) => {
modalSourceInfo = null
this.closeResult = `Closed with: ${result}`;
}, (reason) => {
modalSourceInfo = null
this.closeResult = `Dismissed ${this.getDismissReason(reason)}`;
});
}
waitForClaimOrTimeout(sourceType: string, state: string): Observable<AuthorizeClaim> { syncSource(source: Source){
this.modalService.dismissAll()
this.fastenApi.syncSource(source.id).subscribe(
(respData) => {
console.log("source sync response:", respData)
},
(err) => {console.log(err)},
)
}
private getDismissReason(reason: any): string {
if (reason === ModalDismissReasons.ESC) {
return 'by pressing ESC';
} else if (reason === ModalDismissReasons.BACKDROP_CLICK) {
return 'by clicking on a backdrop';
} else {
return `with: ${reason}`;
}
}
private waitForClaimOrTimeout(sourceType: string, state: string): Observable<AuthorizeClaim> {
return this.lighthouseApi.getSourceAuthorizeClaim(sourceType, state).pipe( return this.lighthouseApi.getSourceAuthorizeClaim(sourceType, state).pipe(
retryWhen(error => retryWhen(error =>
error.pipe( error.pipe(
@ -211,21 +261,7 @@ export class MedicalSourcesComponent implements OnInit {
) )
} }
private uuidV4(){
uploadSourceBundle(event) {
this.uploadedFile = [event.addedFiles[0]]
this.fastenApi.createManualSource(event.addedFiles[0]).subscribe(
(respData) => {
console.log("source manual source create response:", respData)
},
(err) => {console.log(err)},
() => {
this.uploadedFile = []
}
)
}
uuidV4(){
// @ts-ignore // @ts-ignore
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)

View File

@ -132,6 +132,16 @@ export class FastenApiService {
); );
} }
syncSource(sourceId: string): Observable<any> {
return this._httpClient.post<any>(`${this.getBasePath()}/api/secure/source/${sourceId}/sync`, {})
.pipe(
map((response: ResponseWrapper) => {
console.log("SOURCE RESPONSE", response)
return response.data
})
);
}
getResources(sourceResourceType?: string, sourceID?: string): Observable<ResourceFhir[]> { getResources(sourceResourceType?: string, sourceID?: string): Observable<ResourceFhir[]> {
let queryParams = {} let queryParams = {}
if(sourceResourceType){ if(sourceResourceType){

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -1,6 +1,439 @@
/* ###### 4.7 Modal ###### */ /* ###### 4.7 Modal ###### */
// MODAL EFFECTS // MODAL EFFECTS
.modal {
} .modal {
position: fixed;
top: 0;
left: 0;
z-index: 1055;
display: none;
width: 100%;
height: 100%;
overflow-x: hidden;
overflow-y: auto;
outline: 0; }
.modal-dialog {
position: relative;
width: auto;
margin: 0.5rem;
pointer-events: none; }
.modal.fade .modal-dialog {
transition: transform 0.3s ease-out;
transform: translate(0, -50px); }
@media (prefers-reduced-motion: reduce) {
.modal.fade .modal-dialog {
transition: none; } }
.modal.show .modal-dialog {
transform: none; }
.modal.modal-static .modal-dialog {
transform: scale(1.02); }
.modal-dialog-scrollable {
height: calc(100% - 1rem); }
.modal-dialog-scrollable .modal-content {
max-height: 100%;
overflow: hidden; }
.modal-dialog-scrollable .modal-body {
overflow-y: auto; }
.modal-dialog-centered {
display: flex;
align-items: center;
min-height: calc(100% - 1rem); }
.modal-content {
position: relative;
display: flex;
flex-direction: column;
width: 100%;
pointer-events: auto;
background-color: #fff;
background-clip: padding-box;
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 0.3rem;
outline: 0; }
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
z-index: 1050;
width: 100vw;
height: 100vh;
background-color: #000; }
.modal-backdrop.fade {
opacity: 0; }
.modal-backdrop.show {
opacity: 0.5; }
.modal-header {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: space-between;
padding: 1rem 1rem;
border-bottom: 1px solid #cdd4e0;
border-top-left-radius: calc(0.3rem - 1px);
border-top-right-radius: calc(0.3rem - 1px); }
.modal-header .btn-close {
padding: 0.5rem 0.5rem;
margin: -0.5rem -0.5rem -0.5rem auto; }
.modal-title {
margin-bottom: 0;
line-height: 1.5; }
.modal-body {
position: relative;
flex: 1 1 auto;
padding: 1rem; }
.modal-footer {
display: flex;
flex-wrap: wrap;
flex-shrink: 0;
align-items: center;
justify-content: flex-end;
padding: 0.75rem;
border-top: 1px solid #cdd4e0;
border-bottom-right-radius: calc(0.3rem - 1px);
border-bottom-left-radius: calc(0.3rem - 1px); }
.modal-footer > * {
margin: 0.25rem; }
@media (min-width: 576px) {
.modal-dialog {
max-width: 500px;
margin: 1.75rem auto; }
.modal-dialog-scrollable {
height: calc(100% - 3.5rem); }
.modal-dialog-centered {
min-height: calc(100% - 3.5rem); }
.modal-sm {
max-width: 300px; } }
@media (min-width: 992px) {
.modal-lg,
.modal-xl {
max-width: 800px; } }
@media (min-width: 1200px) {
.modal-xl {
max-width: 1140px; } }
.modal-fullscreen {
width: 100vw;
max-width: none;
height: 100%;
margin: 0; }
.modal-fullscreen .modal-content {
height: 100%;
border: 0;
border-radius: 0; }
.modal-fullscreen .modal-header {
border-radius: 0; }
.modal-fullscreen .modal-body {
overflow-y: auto; }
.modal-fullscreen .modal-footer {
border-radius: 0; }
@media (max-width: 575.98px) {
.modal-fullscreen-sm-down {
width: 100vw;
max-width: none;
height: 100%;
margin: 0; }
.modal-fullscreen-sm-down .modal-content {
height: 100%;
border: 0;
border-radius: 0; }
.modal-fullscreen-sm-down .modal-header {
border-radius: 0; }
.modal-fullscreen-sm-down .modal-body {
overflow-y: auto; }
.modal-fullscreen-sm-down .modal-footer {
border-radius: 0; } }
@media (max-width: 767.98px) {
.modal-fullscreen-md-down {
width: 100vw;
max-width: none;
height: 100%;
margin: 0; }
.modal-fullscreen-md-down .modal-content {
height: 100%;
border: 0;
border-radius: 0; }
.modal-fullscreen-md-down .modal-header {
border-radius: 0; }
.modal-fullscreen-md-down .modal-body {
overflow-y: auto; }
.modal-fullscreen-md-down .modal-footer {
border-radius: 0; } }
@media (max-width: 991.98px) {
.modal-fullscreen-lg-down {
width: 100vw;
max-width: none;
height: 100%;
margin: 0; }
.modal-fullscreen-lg-down .modal-content {
height: 100%;
border: 0;
border-radius: 0; }
.modal-fullscreen-lg-down .modal-header {
border-radius: 0; }
.modal-fullscreen-lg-down .modal-body {
overflow-y: auto; }
.modal-fullscreen-lg-down .modal-footer {
border-radius: 0; } }
@media (max-width: 1199.98px) {
.modal-fullscreen-xl-down {
width: 100vw;
max-width: none;
height: 100%;
margin: 0; }
.modal-fullscreen-xl-down .modal-content {
height: 100%;
border: 0;
border-radius: 0; }
.modal-fullscreen-xl-down .modal-header {
border-radius: 0; }
.modal-fullscreen-xl-down .modal-body {
overflow-y: auto; }
.modal-fullscreen-xl-down .modal-footer {
border-radius: 0; } }
@media (max-width: 1399.98px) {
.modal-fullscreen-xxl-down {
width: 100vw;
max-width: none;
height: 100%;
margin: 0; }
.modal-fullscreen-xxl-down .modal-content {
height: 100%;
border: 0;
border-radius: 0; }
.modal-fullscreen-xxl-down .modal-header {
border-radius: 0; }
.modal-fullscreen-xxl-down .modal-body {
overflow-y: auto; }
.modal-fullscreen-xxl-down .modal-footer {
border-radius: 0; } }
/* ###### 3.12 Modal ###### */
.modal-backdrop {
background-color: #0c1019; }
.modal-backdrop.show {
opacity: .8; }
.modal-content {
border-radius: 0;
border-width: 0; }
.modal-content .close {
font-size: 28px;
padding: 0;
margin: 0;
line-height: .5;
border: none;
box-shadow: none;
background: transparent;
float: right; }
.modal-header {
align-items: center;
padding: 15px; }
@media (min-width: 576px) {
.modal-header {
padding: 15px 20px; } }
@media (min-width: 992px) {
.modal-header {
padding: 20px; } }
@media (min-width: 1200px) {
.modal-header {
padding: 20px 25px; } }
.modal-header .modal-title {
margin-bottom: 0; }
.modal-title {
font-size: 18px;
font-weight: 700;
color: #1c273c;
line-height: 1; }
.modal-body {
padding: 25px; }
/* ###### 4.7 Modal ###### */
.modal.animated .modal-dialog {
transform: translate(0, 0); }
.modal.effect-scale .modal-dialog {
transform: scale(0.7);
opacity: 0;
transition: all 0.3s; }
.modal.effect-scale.show .modal-dialog {
transform: scale(1);
opacity: 1; }
.modal.effect-slide-in-right .modal-dialog {
transform: translateX(20%);
opacity: 0;
transition: all 0.3s cubic-bezier(0.25, 0.5, 0.5, 0.9); }
.modal.effect-slide-in-right.show .modal-dialog {
transform: translateX(0);
opacity: 1; }
.modal.effect-slide-in-bottom .modal-dialog {
transform: translateY(20%);
opacity: 0;
transition: all 0.3s; }
.modal.effect-slide-in-bottom.show .modal-dialog {
transform: translateY(0);
opacity: 1; }
.modal.effect-newspaper .modal-dialog {
transform: scale(0) rotate(720deg);
opacity: 0; }
.modal.effect-newspaper.show ~ .modal-backdrop,
.modal.effect-newspaper .modal-dialog {
transition: all 0.5s; }
.modal.effect-newspaper.show .modal-dialog {
transform: scale(1) rotate(0deg);
opacity: 1; }
.modal.effect-fall {
-webkit-perspective: 1300px;
-moz-perspective: 1300px;
perspective: 1300px; }
.modal.effect-fall .modal-dialog {
-moz-transform-style: preserve-3d;
transform-style: preserve-3d;
transform: translateZ(600px) rotateX(20deg);
opacity: 0; }
.modal.effect-fall.show .modal-dialog {
transition: all 0.3s ease-in;
transform: translateZ(0px) rotateX(0deg);
opacity: 1; }
.modal.effect-flip-horizontal {
perspective: 1300px; }
.modal.effect-flip-horizontal .modal-dialog {
-moz-transform-style: preserve-3d;
transform-style: preserve-3d;
transform: rotateY(-70deg);
transition: all 0.3s;
opacity: 0; }
.modal.effect-flip-horizontal.show .modal-dialog {
transform: rotateY(0deg);
opacity: 1; }
.modal.effect-flip-vertical {
perspective: 1300px; }
.modal.effect-flip-vertical .modal-dialog {
-moz-transform-style: preserve-3d;
transform-style: preserve-3d;
transform: rotateX(-70deg);
transition: all 0.3s;
opacity: 0; }
.modal.effect-flip-vertical.show .modal-dialog {
transform: rotateX(0deg);
opacity: 1; }
.modal.effect-super-scaled .modal-dialog {
transform: scale(2);
opacity: 0;
transition: all 0.3s; }
.modal.effect-super-scaled.show .modal-dialog {
transform: scale(1);
opacity: 1; }
.modal.effect-sign {
perspective: 1300px; }
.modal.effect-sign .modal-dialog {
-moz-transform-style: preserve-3d;
transform-style: preserve-3d;
transform: rotateX(-60deg);
transform-origin: 50% 0;
opacity: 0;
transition: all 0.3s; }
.modal.effect-sign.show .modal-dialog {
transform: rotateX(0deg);
opacity: 1; }
.modal.effect-rotate-bottom {
perspective: 1300px; }
.modal.effect-rotate-bottom .modal-dialog {
-moz-transform-style: preserve-3d;
transform-style: preserve-3d;
transform: translateY(100%) rotateX(90deg);
transform-origin: 0 100%;
opacity: 0;
transition: all 0.3s ease-out; }
.modal.effect-rotate-bottom.show .modal-dialog {
transform: translateY(0%) rotateX(0deg);
opacity: 1; }
.modal.effect-rotate-left {
perspective: 1300px; }
.modal.effect-rotate-left .modal-dialog {
-moz-transform-style: preserve-3d;
transform-style: preserve-3d;
transform: translateZ(100px) translateX(-30%) rotateY(90deg);
transform-origin: 0 100%;
opacity: 0;
transition: all 0.3s; }
.modal.effect-rotate-left.show .modal-dialog {
transform: translateZ(0px) translateX(0%) rotateY(0deg);
opacity: 1; }
.modal.effect-just-me .modal-dialog {
transform: scale(0.8);
opacity: 0;
transition: all 0.3s; }
.modal.effect-just-me .modal-content {
background-color: transparent; }
.modal.effect-just-me .close {
text-shadow: none;
color: #fff; }
.modal.effect-just-me .modal-header {
background-color: transparent;
border-bottom-color: rgba(255, 255, 255, 0.1);
padding-left: 0;
padding-right: 0; }
.modal.effect-just-me .modal-header h6, .modal.effect-just-me .modal-header .h6 {
color: #fff;
font-weight: 500; }
.modal.effect-just-me .modal-body {
color: rgba(255, 255, 255, 0.8);
padding-left: 0;
padding-right: 0; }
.modal.effect-just-me .modal-body h6, .modal.effect-just-me .modal-body .h6 {
color: #fff; }
.modal.effect-just-me .modal-footer {
background-color: transparent;
padding-left: 0;
padding-right: 0;
border-top-color: rgba(255, 255, 255, 0.1); }
.modal.effect-just-me.show ~ .modal-backdrop {
opacity: .96; }
.modal.effect-just-me.show .modal-dialog {
transform: scale(1);
opacity: 1; }

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -143,6 +143,7 @@
@import "./assets/scss/custom/variables"; @import "./assets/scss/custom/variables";
@import "./assets/scss/custom/mixins"; @import "./assets/scss/custom/mixins";
/* ########## BOOTSTRAP OVERRIDES ########## */ /* ########## BOOTSTRAP OVERRIDES ########## */
@import "./assets/scss/bootstrap/accordion"; @import "./assets/scss/bootstrap/accordion";
@import "./assets/scss/bootstrap/alerts"; @import "./assets/scss/bootstrap/alerts";
@ -167,6 +168,7 @@
@import "./assets/scss/custom/image"; @import "./assets/scss/custom/image";
@import "./assets/scss/custom/list"; @import "./assets/scss/custom/list";
@import "./assets/scss/custom/nav"; @import "./assets/scss/custom/nav";
@import "./assets/scss/custom/modal";
/* ############### CUSTOM VENDOR STYLES ############### */ /* ############### CUSTOM VENDOR STYLES ############### */
@import "./assets/scss/lib/select2"; @import "./assets/scss/lib/select2";
@ -228,3 +230,4 @@
@import '~@swimlane/ngx-datatable/index.css'; @import '~@swimlane/ngx-datatable/index.css';
@import '~@swimlane/ngx-datatable/themes/bootstrap.css'; @import '~@swimlane/ngx-datatable/themes/bootstrap.css';
@import '~@swimlane/ngx-datatable/assets/icons.css'; @import '~@swimlane/ngx-datatable/assets/icons.css';