sort all resources by "sort_date" column during query

return units when querying
rename all usages of "source" page to "explore"
adding new explore page.
fix dashboard widgets to correctly
This commit is contained in:
Jason Kulatunga 2023-08-03 20:03:24 -07:00
parent 28cb91817a
commit 0397a2f0cb
30 changed files with 252 additions and 45 deletions

View File

@ -113,6 +113,7 @@ func (sr *SqliteRepository) QueryResources(ctx context.Context, query models.Que
Select(fmt.Sprintf("%s.*", TABLE_ALIAS)).
Where(strings.Join(whereClauses, " AND "), whereNamedParameters).
Group(fmt.Sprintf("%s.id", TABLE_ALIAS)).
Order(fmt.Sprintf("%s.sort_date asc", TABLE_ALIAS)).
Table(strings.Join(fromClauses, ", ")).
Find(&results)

View File

@ -25,6 +25,7 @@
"q": {
"select": [
"valueQuantity.value as data",
"valueQuantity.unit as unit",
"(effectiveDateTime | issued).first() as label"
],
"from": "Observation",
@ -50,6 +51,7 @@
"q": {
"select": [
"valueQuantity.value as data",
"valueQuantity.unit as unit",
"(effectiveDateTime | issued).first() as label"
],
"from": "Observation",
@ -75,7 +77,8 @@
{
"q": {
"select": [
"component.where(code.coding.system = 'http://loinc.org' and code.coding.code = '8462-4').valueQuantity.value as data"
"component.where(code.coding.system = 'http://loinc.org' and code.coding.code = '8462-4').valueQuantity.value as data",
"component.where(code.coding.system = 'http://loinc.org' and code.coding.code = '8462-4').valueQuantity.unit as unit"
],
"from": "Observation",
"where": {
@ -89,7 +92,8 @@
{
"q": {
"select": [
"component.where(code.coding.system = 'http://loinc.org' and code.coding.code = '8480-6').valueQuantity.value as data"
"component.where(code.coding.system = 'http://loinc.org' and code.coding.code = '8480-6').valueQuantity.value as data",
"component.where(code.coding.system = 'http://loinc.org' and code.coding.code = '8480-6').valueQuantity.unit as unit"
],
"from": "Observation",
"where": {

View File

@ -13,6 +13,7 @@ import {PatientProfileComponent} from './pages/patient-profile/patient-profile.c
import {MedicalHistoryComponent} from './pages/medical-history/medical-history.component';
import {ReportLabsComponent} from './pages/report-labs/report-labs.component';
import {ResourceCreatorComponent} from './pages/resource-creator/resource-creator.component';
import {ExploreComponent} from './pages/explore/explore.component';
const routes: Routes = [
@ -23,9 +24,13 @@ const routes: Routes = [
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
{ path: 'dashboard', component: DashboardComponent, canActivate: [ IsAuthenticatedAuthGuard] },
{ path: 'source/:source_id', component: SourceDetailComponent, canActivate: [ IsAuthenticatedAuthGuard] },
{ path: 'source/:source_id/resource/:resource_id', component: ResourceDetailComponent, canActivate: [ IsAuthenticatedAuthGuard] },
{ path: 'source/:source_id/resource/:resource_type/:resource_id', component: ResourceDetailComponent, canActivate: [ IsAuthenticatedAuthGuard] },
//explore page will replace source/* pages
{ path: 'explore', component: ExploreComponent, canActivate: [ IsAuthenticatedAuthGuard] },
{ path: 'explore/:source_id', component: SourceDetailComponent, canActivate: [ IsAuthenticatedAuthGuard] },
{ path: 'explore/:source_id/resource/:resource_id', component: ResourceDetailComponent, canActivate: [ IsAuthenticatedAuthGuard] },
{ path: 'explore/:source_id/resource/:resource_type/:resource_id', component: ResourceDetailComponent, canActivate: [ IsAuthenticatedAuthGuard] },
{ path: 'sources', component: MedicalSourcesComponent, canActivate: [ IsAuthenticatedAuthGuard] },
{ path: 'sources/callback/:source_type', component: MedicalSourcesComponent, canActivate: [ IsAuthenticatedAuthGuard] },
{ path: 'resource/create', component: ResourceCreatorComponent, canActivate: [ IsAuthenticatedAuthGuard] },

View File

@ -34,6 +34,7 @@ import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { NgSelectModule } from '@ng-select/ng-select';
import {HTTP_CLIENT_TOKEN} from "./dependency-injection";
import {WidgetsModule} from './widgets/widgets.module';
import { ExploreComponent } from './pages/explore/explore.component';
@NgModule({
declarations: [
@ -50,6 +51,7 @@ import {WidgetsModule} from './widgets/widgets.module';
MedicalHistoryComponent,
ReportLabsComponent,
ResourceCreatorComponent,
ExploreComponent,
],
imports: [
FormsModule,

View File

@ -19,7 +19,7 @@
<fhir-coding *ngFor="let coding of rowItem.data" [coding]="coding"></fhir-coding>
</ng-template>
<ng-template #dataTypeReference>
<a routerLink="/source/{{displayModel.source_id}}/resource/{{rowItem.data.reference}}">{{rowItem.data.display}}</a>
<a routerLink="/explore/{{displayModel.source_id}}/resource/{{rowItem.data.reference}}">{{rowItem.data.display}}</a>
</ng-template>
<ng-template #dataTypeString>{{rowItem.data}}</ng-template>
</td>

View File

@ -11,6 +11,6 @@
<fhir-ui-table [displayModel]="displayModel" [tableData]="tableData"></fhir-ui-table>
</div>
<div *ngIf="showDetails" class="card-footer">
<a class="float-right" routerLink="/source/{{displayModel?.source_id}}/resource/{{displayModel?.source_resource_id}}">details</a>
<a class="float-right" routerLink="/explore/{{displayModel?.source_id}}/resource/{{displayModel?.source_resource_id}}">details</a>
</div>
</div>

View File

@ -16,7 +16,7 @@
<fhir-ui-table [displayModel]="displayModel" [tableData]="tableData"></fhir-ui-table>
</div>
<div *ngIf="showDetails" class="card-footer">
<a class="float-right" routerLink="/source/{{displayModel?.source_id}}/resource/{{displayModel?.source_resource_id}}">details</a>
<a class="float-right" routerLink="/explore/{{displayModel?.source_id}}/resource/{{displayModel?.source_resource_id}}">details</a>
</div>
</div>

View File

@ -20,6 +20,6 @@
</div>
</div>
<div *ngIf="showDetails" class="card-footer">
<a class="float-right" routerLink="/source/{{displayModel?.source_id}}/resource/{{displayModel?.source_resource_id}}">details</a>
<a class="float-right" routerLink="/explore/{{displayModel?.source_id}}/resource/{{displayModel?.source_resource_id}}">details</a>
</div>
</div>

View File

@ -16,6 +16,6 @@
<fhir-ui-table [displayModel]="displayModel" [tableData]="tableData"></fhir-ui-table>
</div>
<div *ngIf="showDetails" class="card-footer">
<a class="float-right" routerLink="/source/{{displayModel?.source_id}}/resource/{{displayModel?.source_resource_id}}">details</a>
<a class="float-right" routerLink="/explore/{{displayModel?.source_id}}/resource/{{displayModel?.source_resource_id}}">details</a>
</div>
</div>

View File

@ -20,6 +20,6 @@
</div>
</div>
<div *ngIf="showDetails" class="card-footer">
<a class="float-right" routerLink="/source/{{displayModel?.source_id}}/resource/{{displayModel?.source_resource_id}}">details</a>
<a class="float-right" routerLink="/explore/{{displayModel?.source_id}}/resource/{{displayModel?.source_resource_id}}">details</a>
</div>
</div>

View File

@ -16,7 +16,7 @@
<fhir-ui-table [displayModel]="displayModel" [tableData]="tableData"></fhir-ui-table>
</div>
<div *ngIf="showDetails" class="card-footer">
<a class="float-right" routerLink="/source/{{displayModel?.source_id}}/resource/{{displayModel?.source_resource_id}}">details</a>
<a class="float-right" routerLink="/explore/{{displayModel?.source_id}}/resource/{{displayModel?.source_resource_id}}">details</a>
</div>
</div>

View File

@ -16,7 +16,7 @@
<fhir-ui-table [displayModel]="displayModel" [tableData]="tableData"></fhir-ui-table>
</div>
<div *ngIf="showDetails" class="card-footer">
<a class="float-right" routerLink="/source/{{displayModel?.source_id}}/resource/{{displayModel?.source_resource_id}}">details</a>
<a class="float-right" routerLink="/explore/{{displayModel?.source_id}}/resource/{{displayModel?.source_resource_id}}">details</a>
</div>
</div>

View File

@ -16,6 +16,6 @@
<fhir-ui-table [displayModel]="displayModel" [tableData]="tableData"></fhir-ui-table>
</div>
<div *ngIf="showDetails" class="card-footer">
<a class="float-right" routerLink="/source/{{displayModel?.source_id}}/resource/{{displayModel?.source_resource_id}}">details</a>
<a class="float-right" routerLink="/explore/{{displayModel?.source_id}}/resource/{{displayModel?.source_resource_id}}">details</a>
</div>
</div>

View File

@ -16,7 +16,7 @@
<fhir-ui-table [displayModel]="displayModel" [tableData]="tableData"></fhir-ui-table>
</div>
<div *ngIf="showDetails" class="card-footer">
<a class="float-right" routerLink="/source/{{displayModel?.source_id}}/resource/{{displayModel?.source_resource_id}}">details</a>
<a class="float-right" routerLink="/explore/{{displayModel?.source_id}}/resource/{{displayModel?.source_resource_id}}">details</a>
</div>
</div>

View File

@ -3,7 +3,7 @@
<div class="row" >
<!-- Condition Header -->
<div class="col-6">
<span routerLink="/source/{{firstObservation?.source_id}}/resource/{{firstObservation?.source_resource_id}}">{{observationTitle}}</span>
<span routerLink="/explore/{{firstObservation?.source_id}}/resource/{{firstObservation?.source_resource_id}}">{{observationTitle}}</span>
</div>
<div class="col-6">
{{firstObservation | fhirPath: "Observation.effectiveDateTime": "Observation.issued" | date}}
@ -36,7 +36,7 @@
<div #collapse="ngbCollapse" [ngbCollapse]="true">
<ul>
<li class="cursor-pointer tx-indigo" *ngFor="let observation of observations" routerLink="/source/{{observation?.source_id}}/resource/{{observation?.source_resource_id}}">Observation: {{observation | fhirPath: "Observation.effectiveDateTime": "Observation.issued" | date}}</li>
<li class="cursor-pointer tx-indigo" *ngFor="let observation of observations" routerLink="/explore/{{observation?.source_id}}/resource/{{observation?.source_resource_id}}">Observation: {{observation | fhirPath: "Observation.effectiveDateTime": "Observation.issued" | date}}</li>
</ul>
</div>

View File

@ -76,7 +76,7 @@ export class DashboardComponent implements OnInit {
}
selectSource(selectedSource: Source){
this.router.navigateByUrl(`/source/${selectedSource.id}`, {
this.router.navigateByUrl(`/explore/${selectedSource.id}`, {
state: selectedSource
});
}

View File

@ -0,0 +1,73 @@
<div class="az-content">
<div class="container">
<div class="az-content-body pd-lg-l-40 d-flex flex-column">
<!-- Header Row -->
<report-header [reportHeaderTitle]="'Explore'" [reportHeaderSubTitle]="'Explore your Medical Records'"></report-header>
<ng-container [ngTemplateOutlet]="loading ? isLoadingTemplate : (connectedSources.length == 0) ? emptyReport : report"></ng-container>
<ng-template #report>
<div class="row">
<app-medical-sources-card class="col-sm-3 mg-b-20 px-3"
*ngFor="let sourceData of connectedSources"
[sourceInfo]="sourceData"
(onClick)="exploreSource($event)"
></app-medical-sources-card>
</div>
</ng-template>
<ng-template #emptyReport>
<div class="d-flex align-items-center" style="height:100%">
<div class="modal-body tx-center pd-y-20 pd-x-20">
<h4 class="tx-purple mg-b-20">No Sources Found!</h4>
<p class="mg-b-20 mg-x-20">
Fasten was unable to find any connected sources. You will need to connect a medical source before you can use this page.
</p>
<p class="mg-b-20 mg-x-20">
Click below to add a new healthcare provider to Fasten.
</p>
<button [routerLink]="'/sources'" type="button" class="btn btn-purple pd-x-25">Add Source</button>
<button [routerLink]="'/resource/create'" type="button" class="btn btn-purple mg-l-10 pd-x-25">Add Condition</button>
</div><!-- modal-body -->
</div>
</ng-template>
<ng-template #isLoadingTemplate>
<div class="row">
<div class="col-12">
<app-loading-spinner [loadingTitle]="'Please wait, loading report...'"></app-loading-spinner>
</div>
</div>
</ng-template>
<ng-template #contentModalRef let-modal>
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title"></h4>
<button type="button" class="btn 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)="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" class="btn disabled btn-outline-danger">Delete</button>
<button (click)="modal.dismiss('Close click')" type="button" class="btn btn-outline-light">Close</button>
</div>
</ng-template>
</div>
</div>
</div>

View File

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

View File

@ -0,0 +1,62 @@
import { Component, OnInit } from '@angular/core';
import {FastenApiService} from '../../services/fasten-api.service';
import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
import {Source} from '../../models/fasten/source';
import {forkJoin} from 'rxjs';
import {LighthouseService} from '../../services/lighthouse.service';
import {SourceListItem} from '../medical-sources/medical-sources.component';
import {Router} from '@angular/router';
@Component({
selector: 'app-explore',
templateUrl: './explore.component.html',
styleUrls: ['./explore.component.scss']
})
export class ExploreComponent implements OnInit {
loading: boolean = false
connectedSources: SourceListItem[] = []
constructor(
private fastenApi: FastenApiService,
private lighthouseApi: LighthouseService,
private router: Router
) { }
ngOnInit(): void {
this.loading = true
this.fastenApi.getSources().subscribe(results => {
this.loading = false
//handle connected sources sources
const connectedSources = results as Source[]
forkJoin(connectedSources.map((source) => this.lighthouseApi.getLighthouseSource(source.source_type))).subscribe((connectedMetadata) => {
for(const ndx in connectedSources){
this.connectedSources.push({source: connectedSources[ndx], metadata: connectedMetadata[ndx]})
}
})
}, error => {
this.loading = false
})
}
public exploreSource(sourceListItem: SourceListItem, ) {
this.router.navigateByUrl(`/source/${sourceListItem.source.id}`, {
state: sourceListItem.source
});
// if(this.status[sourceListItem.metadata.source_type] || !sourceListItem.source){
// //if this source is currently "loading" dont open the modal window
// return
// }
//
// this.modalSelectedSourceListItem = sourceListItem
// this.modalService.open(contentModalRef, {ariaLabelledBy: 'modal-basic-title'}).result.then((result) => {
// this.modalSelectedSourceListItem = null
// this.modalCloseResult = `Closed with: ${result}`;
// }, (reason) => {
// this.modalSelectedSourceListItem = null
// this.modalCloseResult = `Dismissed ${this.getDismissReason(reason)}`;
// });
}
}

View File

@ -2,7 +2,7 @@
<div class="container">
<div class="az-content-body">
<div class="az-content-breadcrumb">
<span class="cursor-pointer" routerLink="/source/{{sourceId}}">{{sourceName}}</span>
<span class="cursor-pointer" routerLink="/explore/{{sourceId}}">{{sourceName}}</span>
<span>Resource</span>
<span>{{resource?.source_resource_type}}</span>
<span>{{resource?.source_resource_id}}</span>

View File

@ -18,7 +18,7 @@
<div class="patient-row row">
<div class="col-7 patient-name"><h3 class="pull-left text-primary">{{getPatientName()}}</h3></div>
<div class="col-5">
<a routerLink="/source/{{selectedSource?.id}}/resource/{{selectedPatient?.source_resource_id}}" class="btn btn-indigo btn-icon float-right">
<a routerLink="/explore/{{selectedSource?.id}}/resource/{{selectedPatient?.source_resource_id}}" class="btn btn-indigo btn-icon float-right">
<i class="fas fa-info-circle"></i>
</a>
</div>

View File

@ -0,0 +1,8 @@
import { DatasetLatestEntryPipe } from './dataset-latest-entry.pipe';
describe('DatasetLatestEntryPipe', () => {
it('create an instance', () => {
const pipe = new DatasetLatestEntryPipe();
expect(pipe).toBeTruthy();
});
});

View File

@ -0,0 +1,44 @@
import { Pipe, PipeTransform } from '@angular/core';
import {ChartDataset} from 'chart.js';
import * as _ from 'lodash';
@Pipe({
name: 'datasetLatestEntry'
})
export class DatasetLatestEntryPipe implements PipeTransform {
transform(dataset: ChartDataset<'line'>, round?: number, valLookupKey?: string, unitLookupKey?: string): string {
if(!round){
round = 0 //round to nearest whole number
}
let lastItem = dataset?.data?.[dataset?.data?.length -1] || ''
// let valueKey = this.chartOptions?.parsing?.['yAxisKey'] || dataset?.parsing?.['key']
console.log('latestEntryConfig', lastItem, valLookupKey, unitLookupKey, round)
let lastItemUnit = ""
let lastItemValue
if(Array.isArray(lastItem)){
lastItemValue = _.flatten(lastItem?.[0]?.[valLookupKey])?.[0] as string
lastItemUnit = _.flatten(lastItem?.[0]?.[unitLookupKey])?.[0] as string
} else if(typeof lastItem === 'object'){
console.log('lastItem-object', lastItem?.[valLookupKey])
lastItemValue = lastItem?.[valLookupKey]
lastItemUnit = lastItem?.[unitLookupKey]
} else {
//do nothing
}
lastItemValue = this.roundToDecimalPlaces(lastItemValue, round)
if(lastItemUnit){
return lastItemValue + ' ' + lastItemUnit
} else {
return lastItemValue.toString()
}
}
roundToDecimalPlaces(value: string, decimalPlaces: number): string {
return parseFloat(value).toFixed(decimalPlaces).toString()
}
}

View File

@ -5,6 +5,7 @@ import { NgModule } from '@angular/core';
import {FhirPathPipe} from './fhir-path.pipe';
import {FilterPipe} from './filter.pipe';
import { ShortDomainPipe } from './short-domain.pipe';
import { DatasetLatestEntryPipe } from './dataset-latest-entry.pipe';
@NgModule({
declarations: [
@ -12,6 +13,7 @@ import { ShortDomainPipe } from './short-domain.pipe';
FhirPathPipe,
FilterPipe,
ShortDomainPipe,
DatasetLatestEntryPipe,
],
imports: [
@ -19,7 +21,8 @@ import { ShortDomainPipe } from './short-domain.pipe';
exports: [
FhirPathPipe,
FilterPipe,
ShortDomainPipe
ShortDomainPipe,
DatasetLatestEntryPipe
]
})
export class PipesModule {}

View File

@ -121,26 +121,6 @@ export class DashboardWidgetComponent implements OnInit, DashboardWidgetComponen
console.log(`Loading COmpleted for ${this.widgetConfig.title_text}, ${this.loading}`)
}
getLastDatasetValue(dataset: ChartDataset<'line'>): string {
let lastItem = dataset?.data?.[dataset?.data?.length -1] || ''
let valueKey = this.chartOptions?.parsing?.['yAxisKey'] || dataset?.parsing?.['key']
console.log('current', lastItem, valueKey)
if(typeof lastItem === 'string'){
console.log('lastItem-string', lastItem)
return lastItem
} else if(Array.isArray(lastItem)){
return _.flatten(lastItem?.[0]?.[valueKey])?.[0] as string
} else if(typeof lastItem === 'object'){
console.log('lastItem-object', lastItem?.[valueKey])
return lastItem?.[valueKey]
} else {
return lastItem.toString()
}
}
// This function will process the raw response from the Dashboard Query API call, which requires frontend processing of the select clause.
// it will call the fhirPathMapQueryFn which will extract FHIRPath values from the resource_raw field of the ResourceFhir object

View File

@ -12,7 +12,7 @@
<div class="card card-dashboard-three h-100">
<div class="card-header">
<p>{{widgetConfig.title_text}}</p>
<h6>120/80 <small class="tx-success"><i class="icon ion-md-arrow-up"></i>mmHg</small></h6>
<h6>{{chartDatasets?.[0] | datasetLatestEntry: 0:'data' }}/{{chartDatasets?.[1] | datasetLatestEntry: 0:'data' }} <small class="tx-success"><i class="icon ion-md-arrow-up"></i>mmHg</small></h6>
<small>{{widgetConfig.description_text}}</small>
</div><!-- card-header -->
<div class="card-body d-flex flex-column">

View File

@ -5,10 +5,11 @@ import {DashboardWidgetComponent} from '../dashboard-widget/dashboard-widget.com
import {CommonModule} from '@angular/common';
import {LoadingWidgetComponent} from '../loading-widget/loading-widget.component';
import {EmptyWidgetComponent} from '../empty-widget/empty-widget.component';
import {PipesModule} from '../../pipes/pipes.module';
@Component({
standalone: true,
imports: [NgChartsModule, CommonModule, LoadingWidgetComponent, EmptyWidgetComponent],
imports: [NgChartsModule, CommonModule, LoadingWidgetComponent, EmptyWidgetComponent, PipesModule],
selector: 'grouped-bar-chart-widget',
templateUrl: './grouped-bar-chart-widget.component.html',
styleUrls: ['./grouped-bar-chart-widget.component.scss']

View File

@ -11,7 +11,7 @@
<ng-template #showChart>
<div class="card card-dashboard-two h-100">
<div class="card-header">
<h6>{{getLastDatasetValue(chartDatasets?.[0])}} kg <i class="icon ion-md-trending-up tx-success"></i> <small>18.02%</small></h6>
<h6>{{chartDatasets?.[0] | datasetLatestEntry: 2:'data':'unit' }} <i class="icon ion-md-trending-up tx-success"></i> <small>18.02%</small></h6>
<p>{{widgetConfig?.title_text}}</p>
</div><!-- card-header -->
<div class="card-body d-flex flex-column">

View File

@ -7,10 +7,11 @@ import {ChartConfiguration, ChartDataset, ChartOptions} from 'chart.js';
import {CommonModule} from '@angular/common';
import {LoadingWidgetComponent} from '../loading-widget/loading-widget.component';
import {EmptyWidgetComponent} from '../empty-widget/empty-widget.component';
import {PipesModule} from '../../pipes/pipes.module';
@Component({
standalone: true,
imports: [NgChartsModule, CommonModule, LoadingWidgetComponent, EmptyWidgetComponent],
imports: [NgChartsModule, CommonModule, LoadingWidgetComponent, EmptyWidgetComponent, PipesModule],
selector: 'simple-line-chart-widget',
templateUrl: './simple-line-chart-widget.component.html',
styleUrls: ['./simple-line-chart-widget.component.scss']