diff --git a/backend/pkg/database/interface.go b/backend/pkg/database/interface.go index a7f8d830..53c56ee3 100644 --- a/backend/pkg/database/interface.go +++ b/backend/pkg/database/interface.go @@ -22,5 +22,6 @@ type DatabaseRepository interface { CreateSource(context.Context, *models.Source) error GetSource(context.Context, string) (*models.Source, error) + GetSourceSummary(context.Context, string) (*models.SourceSummary, error) GetSources(context.Context) ([]models.Source, error) } diff --git a/backend/pkg/database/sqlite_repository.go b/backend/pkg/database/sqlite_repository.go index f089ac12..85e4e8fd 100644 --- a/backend/pkg/database/sqlite_repository.go +++ b/backend/pkg/database/sqlite_repository.go @@ -231,6 +231,40 @@ func (sr *sqliteRepository) GetSource(ctx context.Context, sourceId string) (*mo return &sourceCred, results.Error } +func (sr *sqliteRepository) GetSourceSummary(ctx context.Context, sourceId string) (*models.SourceSummary, error) { + sourceUUID, err := uuid.Parse(sourceId) + if err != nil { + return nil, err + } + + sourceSummary := &models.SourceSummary{} + + source, err := sr.GetSource(ctx, sourceId) + if err != nil { + return nil, err + } + sourceSummary.Source = source + + //group by resource type and return counts + // SELECT source_resource_type as resource_type, COUNT(*) as count FROM resource_fhirs WHERE source_id = "53c1e930-63af-46c9-b760-8e83cbc1abd9" GROUP BY source_resource_type; + + var results []map[string]interface{} + + sr.gormClient.WithContext(ctx). + Model(models.ResourceFhir{}). + Select("source_id, source_resource_type as resource_type, count(*) as count"). + Group("source_resource_type"). + Where(models.OriginBase{ + UserID: sr.GetCurrentUser(ctx).ID, + SourceID: sourceUUID, + }). + Scan(&results) + + sourceSummary.ResourceTypeCounts = results + + return sourceSummary, nil +} + func (sr *sqliteRepository) GetSources(ctx context.Context) ([]models.Source, error) { var sourceCreds []models.Source diff --git a/backend/pkg/models/source_summary.go b/backend/pkg/models/source_summary.go new file mode 100644 index 00000000..dd9976d0 --- /dev/null +++ b/backend/pkg/models/source_summary.go @@ -0,0 +1,6 @@ +package models + +type SourceSummary struct { + Source *Source `json:"source,omitempty"` + ResourceTypeCounts []map[string]interface{} `json:"resource_type_counts,omitempty"` +} diff --git a/backend/pkg/web/handler/source.go b/backend/pkg/web/handler/source.go index 2c037dfb..35e00b35 100644 --- a/backend/pkg/web/handler/source.go +++ b/backend/pkg/web/handler/source.go @@ -118,6 +118,19 @@ func GetSource(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"success": true, "data": sourceCred}) } +func GetSourceSummary(c *gin.Context) { + logger := c.MustGet("LOGGER").(*logrus.Entry) + databaseRepo := c.MustGet("REPOSITORY").(database.DatabaseRepository) + + sourceSummary, err := databaseRepo.GetSourceSummary(c, c.Param("sourceId")) + if err != nil { + logger.Errorln("An error occurred while retrieving source summary", err) + c.JSON(http.StatusInternalServerError, gin.H{"success": false}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": sourceSummary}) +} + func ListSource(c *gin.Context) { logger := c.MustGet("LOGGER").(*logrus.Entry) databaseRepo := c.MustGet("REPOSITORY").(database.DatabaseRepository) diff --git a/backend/pkg/web/server.go b/backend/pkg/web/server.go index 3cc12006..18f60667 100644 --- a/backend/pkg/web/server.go +++ b/backend/pkg/web/server.go @@ -49,6 +49,7 @@ func (ae *AppEngine) Setup(logger *logrus.Entry) *gin.Engine { secure.POST("/source/manual", handler.CreateManualSource) secure.GET("/source", handler.ListSource) secure.GET("/source/:sourceId", handler.GetSource) + secure.GET("/source/:sourceId/summary", handler.GetSourceSummary) //in debug mode, this endpoint lets us request data directly from the source api secure.GET("/source/raw/:sourceType/*path", handler.RawRequestSource) diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 5e2896cb..575e3a6e 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -23,6 +23,8 @@ import { CanActivateAuthGuard } from './services/can-activate.auth-guard'; import {FastenApiService} from './services/fasten-api.service'; import {Router} from '@angular/router'; import { SourceDetailComponent } from './pages/source-detail/source-detail.component'; +import { ResourceListOutletDirective } from './directive/resource-list-outlet.directive'; +import {ResourceListComponent} from './components/resource-list/resource-list.component'; @NgModule({ declarations: [ AppComponent, diff --git a/frontend/src/app/components/list-care-plan/list-care-plan.component.html b/frontend/src/app/components/list-care-plan/list-care-plan.component.html deleted file mode 100644 index 62e422a9..00000000 --- a/frontend/src/app/components/list-care-plan/list-care-plan.component.html +++ /dev/null @@ -1,31 +0,0 @@ -

A clinical condition, problem, diagnosis, or other event, situation, issue, or clinical concept that has risen to a level of concern.

-
- - - - - - - - - - - - - - - - - -
CategoryReasonPeriodStatus
- {{careplan.category}} - -
- {{reason[0]}} - - - - {{reason[1] || "no data"}} - -
-
{{careplan.period?.start}} - {{careplan.period?.end}}{{careplan.status}}
-
diff --git a/frontend/src/app/components/list-care-plan/list-care-plan.component.ts b/frontend/src/app/components/list-care-plan/list-care-plan.component.ts deleted file mode 100644 index ed84b343..00000000 --- a/frontend/src/app/components/list-care-plan/list-care-plan.component.ts +++ /dev/null @@ -1,25 +0,0 @@ -import {Component, Input, OnInit} from '@angular/core'; -import {ResourceFhir} from '../../models/fasten/resource_fhir'; -import {CarePlan} from '../../models/display/care-plan'; - -@Component({ - selector: 'app-list-care-plan', - templateUrl: './list-care-plan.component.html', - styleUrls: ['./list-care-plan.component.scss'] -}) -export class ListCarePlanComponent implements OnInit { - - @Input() resourceList: ResourceFhir[] = [] - careplanList: CarePlan[] = [] - - constructor() { } - - ngOnInit(): void { - let _careplanList = this.careplanList - this.resourceList.forEach((resource) => { - let careplan = new CarePlan(resource.payload) - _careplanList.push(careplan) - }) - } - -} diff --git a/frontend/src/app/components/list-explanation-of-benefit/list-explanation-of-benefit.component.html b/frontend/src/app/components/list-explanation-of-benefit/list-explanation-of-benefit.component.html deleted file mode 100644 index 5e229a6c..00000000 --- a/frontend/src/app/components/list-explanation-of-benefit/list-explanation-of-benefit.component.html +++ /dev/null @@ -1 +0,0 @@ -

list-explanation-of-benefit works!

diff --git a/frontend/src/app/components/list-explanation-of-benefit/list-explanation-of-benefit.component.scss b/frontend/src/app/components/list-explanation-of-benefit/list-explanation-of-benefit.component.scss deleted file mode 100644 index e69de29b..00000000 diff --git a/frontend/src/app/components/list-explanation-of-benefit/list-explanation-of-benefit.component.spec.ts b/frontend/src/app/components/list-explanation-of-benefit/list-explanation-of-benefit.component.spec.ts deleted file mode 100644 index 673d3447..00000000 --- a/frontend/src/app/components/list-explanation-of-benefit/list-explanation-of-benefit.component.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { ListExplanationOfBenefitComponent } from './list-explanation-of-benefit.component'; - -describe('ListExplanationOfBenefitComponent', () => { - let component: ListExplanationOfBenefitComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [ ListExplanationOfBenefitComponent ] - }) - .compileComponents(); - - fixture = TestBed.createComponent(ListExplanationOfBenefitComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/frontend/src/app/components/list-explanation-of-benefit/list-explanation-of-benefit.component.ts b/frontend/src/app/components/list-explanation-of-benefit/list-explanation-of-benefit.component.ts deleted file mode 100644 index c7c83e1d..00000000 --- a/frontend/src/app/components/list-explanation-of-benefit/list-explanation-of-benefit.component.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Component, OnInit } from '@angular/core'; - -@Component({ - selector: 'app-list-explanation-of-benefit', - templateUrl: './list-explanation-of-benefit.component.html', - styleUrls: ['./list-explanation-of-benefit.component.scss'] -}) -export class ListExplanationOfBenefitComponent implements OnInit { - - constructor() { } - - ngOnInit(): void { - } - -} diff --git a/frontend/src/app/components/list-generic-resource/list-adverse-event.component.ts b/frontend/src/app/components/list-generic-resource/list-adverse-event.component.ts new file mode 100644 index 00000000..f5e8e4f0 --- /dev/null +++ b/frontend/src/app/components/list-generic-resource/list-adverse-event.component.ts @@ -0,0 +1,15 @@ +import {Component} from '@angular/core'; +import {GenericColumnDefn, ListGenericResourceComponent} from './list-generic-resource.component'; +import {attributeXTime} from './utils'; + +@Component({ + selector: 'app-list-adverse-event', + templateUrl: './list-generic-resource.component.html', + styleUrls: ['./list-generic-resource.component.scss'] +}) +export class ListAdverseEventComponent extends ListGenericResourceComponent { + columnDefinitions: GenericColumnDefn[] = [ + { title: 'Event', versions: '*', format: 'code', getter: a => a.event.coding[0] }, + { title: 'Date', versions: '*', format: 'date', getter: a => a.date } + ] +} diff --git a/frontend/src/app/components/list-generic-resource/list-allergy-intolerance.component.ts b/frontend/src/app/components/list-generic-resource/list-allergy-intolerance.component.ts new file mode 100644 index 00000000..557812b2 --- /dev/null +++ b/frontend/src/app/components/list-generic-resource/list-allergy-intolerance.component.ts @@ -0,0 +1,17 @@ +import {Component} from '@angular/core'; +import {GenericColumnDefn, ListGenericResourceComponent} from './list-generic-resource.component'; +import {attributeXTime} from './utils'; + +@Component({ + selector: 'app-list-allergy-intolerance', + templateUrl: './list-generic-resource.component.html', + styleUrls: ['./list-generic-resource.component.scss'] +}) +export class ListAllergyIntoleranceComponent extends ListGenericResourceComponent { + columnDefinitions: GenericColumnDefn[] = [ + { title: 'Allergy', versions: '*', format: 'code', getter: a => a.code.coding[0] }, + { title: 'Date Recorded', versions: '*', format: 'date', getter: a => a.assertedDate || a.recordedDate }, + { title: 'Onset', versions: '*', format: 'date', getter: a => a.onsetDateTime }, + { title: 'Resolution Age', versions: '*', format: 'date', getter: a => a.extension.resolutionAge } + ] +} diff --git a/frontend/src/app/components/list-generic-resource/list-care-plan.component.ts b/frontend/src/app/components/list-generic-resource/list-care-plan.component.ts new file mode 100644 index 00000000..c0723ae3 --- /dev/null +++ b/frontend/src/app/components/list-generic-resource/list-care-plan.component.ts @@ -0,0 +1,24 @@ +import {Component} from '@angular/core'; +import {GenericColumnDefn, ListGenericResourceComponent} from './list-generic-resource.component'; +import {attributeXTime} from './utils'; +import {getPath} from '../../fhir/utils'; + +@Component({ + selector: 'app-list-care-plan', + templateUrl: './list-generic-resource.component.html', + styleUrls: ['./list-generic-resource.component.scss'] +}) +export class ListCarePlanComponent extends ListGenericResourceComponent { + columnDefinitions: GenericColumnDefn[] = [ + { title: 'Category', versions: '*', format: 'code', getter: c => c.category[0].coding[0] }, + { title: 'Reason', versions: '*', getter: c => { + return (c.activity || []).map((a, i) => { + let reason = getPath(a, "detail.code.coding.0.display") || "" + return reason ? [reason, getPath(a, "detail.status") || "no data"] : [] + }) + } }, + { title: 'Period', versions: '*', format: 'period', getter: c => c.period }, + { title: 'Status', versions: '*', getter: a => a.status }, + + ] +} diff --git a/frontend/src/app/components/list-generic-resource/list-communication.component.ts b/frontend/src/app/components/list-generic-resource/list-communication.component.ts new file mode 100644 index 00000000..214051c0 --- /dev/null +++ b/frontend/src/app/components/list-generic-resource/list-communication.component.ts @@ -0,0 +1,17 @@ +import {Component} from '@angular/core'; +import {GenericColumnDefn, ListGenericResourceComponent} from './list-generic-resource.component'; +import {attributeXTime} from './utils'; + +@Component({ + selector: 'app-list-communication', + templateUrl: './list-generic-resource.component.html', + styleUrls: ['./list-generic-resource.component.scss'] +}) +export class ListCommunicationComponent extends ListGenericResourceComponent { + columnDefinitions: GenericColumnDefn[] = [ + { title: 'Reason', versions: '*', format: 'code', getter: c => c.reasonCode[0].coding[0] }, + { title: 'Sent', versions: '*', format: 'date', getter: c => c.sent }, + { title: 'Received', versions: '*', format: 'date', getter: c => c.received }, + { title: 'Status', versions: '*', getter: c => c.status } + ] +} diff --git a/frontend/src/app/components/list-generic-resource/list-condition.component.ts b/frontend/src/app/components/list-generic-resource/list-condition.component.ts index 186e32b7..c216e9da 100644 --- a/frontend/src/app/components/list-generic-resource/list-condition.component.ts +++ b/frontend/src/app/components/list-generic-resource/list-condition.component.ts @@ -1,5 +1,6 @@ -import {Component} from '@angular/core'; -import {GenericColumnDefn, ListGenericResourceComponent} from './list-generic-resource.component'; +import {Component, OnInit} from '@angular/core'; +import {GenericColumnDefn, ListGenericResourceComponent, ResourceListComponentInterface} from './list-generic-resource.component'; +import {FORMATTERS} from './utils'; @Component({ selector: 'app-list-condition', diff --git a/frontend/src/app/components/list-generic-resource/list-coverage.component.ts b/frontend/src/app/components/list-generic-resource/list-coverage.component.ts new file mode 100644 index 00000000..d05540c9 --- /dev/null +++ b/frontend/src/app/components/list-generic-resource/list-coverage.component.ts @@ -0,0 +1,15 @@ +import {Component} from '@angular/core'; +import {GenericColumnDefn, ListGenericResourceComponent} from './list-generic-resource.component'; +import {attributeXTime} from './utils'; + +@Component({ + selector: 'app-list-coverage', + templateUrl: './list-generic-resource.component.html', + styleUrls: ['./list-generic-resource.component.scss'] +}) +export class ListCoverageComponent extends ListGenericResourceComponent { + columnDefinitions: GenericColumnDefn[] = [ + { title: 'Type', versions: '*', format: 'code', getter: c => c.type.coding[0] }, + { title: 'Period', versions: '*', format: 'period', getter: c => c.period } + ] +} diff --git a/frontend/src/app/components/list-generic-resource/list-device-request.component.ts b/frontend/src/app/components/list-generic-resource/list-device-request.component.ts new file mode 100644 index 00000000..905b12bb --- /dev/null +++ b/frontend/src/app/components/list-generic-resource/list-device-request.component.ts @@ -0,0 +1,18 @@ +import {Component} from '@angular/core'; +import {GenericColumnDefn, ListGenericResourceComponent} from './list-generic-resource.component'; +import {attributeXTime} from './utils'; + +@Component({ + selector: 'app-list-device-request', + templateUrl: './list-generic-resource.component.html', + styleUrls: ['./list-generic-resource.component.scss'] +}) +export class ListDeviceRequestComponent extends ListGenericResourceComponent { + columnDefinitions: GenericColumnDefn[] = [ + { title: 'Device', versions: '*', format: 'code', getter: d => d.codeCodeableConcept.coding[0] }, + { title: 'Author Date', versions: '*', format: 'date', getter: d => d.authoredOn }, + { title: 'Do Not Perform', versions: '*', getter: d => d.modifierExtension.doNotPerform }, + { title: 'Do Not Perform Reason', versions: '*', format: 'code', getter: s => s.extension.doNotPerformReason.coding[0] } + + ] +} diff --git a/frontend/src/app/components/list-generic-resource/list-document-reference.component.ts b/frontend/src/app/components/list-generic-resource/list-document-reference.component.ts new file mode 100644 index 00000000..e55556a0 --- /dev/null +++ b/frontend/src/app/components/list-generic-resource/list-document-reference.component.ts @@ -0,0 +1,16 @@ +import {Component} from '@angular/core'; +import {GenericColumnDefn, ListGenericResourceComponent} from './list-generic-resource.component'; +import {attributeXTime} from './utils'; + +@Component({ + selector: 'app-list-document-reference', + templateUrl: './list-generic-resource.component.html', + styleUrls: ['./list-generic-resource.component.scss'] +}) +export class ListDocumentReferenceComponent extends ListGenericResourceComponent { + columnDefinitions: GenericColumnDefn[] = [ + { title: 'Date', versions: '*', format: 'date', getter: d => d.date }, + { title: 'Content', versions: '*', getter: d => atob(d.content[0].attachment.data) } + + ] +} diff --git a/frontend/src/app/components/list-generic-resource/list-encounter.component.ts b/frontend/src/app/components/list-generic-resource/list-encounter.component.ts index 78d1375b..8c9c1acc 100644 --- a/frontend/src/app/components/list-generic-resource/list-encounter.component.ts +++ b/frontend/src/app/components/list-generic-resource/list-encounter.component.ts @@ -1,12 +1,12 @@ -import {Component} from '@angular/core'; -import {GenericColumnDefn, ListGenericResourceComponent} from './list-generic-resource.component'; +import {Component, OnChanges, OnInit} from '@angular/core'; +import {GenericColumnDefn, ListGenericResourceComponent, ResourceListComponentInterface} from './list-generic-resource.component'; @Component({ selector: 'app-list-encounter', templateUrl: './list-generic-resource.component.html', styleUrls: ['./list-generic-resource.component.scss'] }) -export class ListEncounterComponent extends ListGenericResourceComponent { +export class ListEncounterComponent extends ListGenericResourceComponent { columnDefinitions: GenericColumnDefn[] = [ { title: 'Encounter', versions: '*', format: 'code', getter: e => e.type[0].coding[0] }, { title: 'Period', versions: '*', format: 'period', getter: e => e.period }, diff --git a/frontend/src/app/components/list-generic-resource/list-explanation-of-benefit.component.ts b/frontend/src/app/components/list-generic-resource/list-explanation-of-benefit.component.ts new file mode 100644 index 00000000..26be1b77 --- /dev/null +++ b/frontend/src/app/components/list-generic-resource/list-explanation-of-benefit.component.ts @@ -0,0 +1,11 @@ +import {Component, OnChanges, OnInit} from '@angular/core'; +import {GenericColumnDefn, ListGenericResourceComponent, ResourceListComponentInterface} from './list-generic-resource.component'; + +@Component({ + selector: 'app-list-explanation-of-benefit', + templateUrl: './list-generic-resource.component.html', + styleUrls: ['./list-generic-resource.component.scss'] +}) +export class ListExplanationOfBenefitComponent extends ListGenericResourceComponent { + columnDefinitions: GenericColumnDefn[] = [] +} diff --git a/frontend/src/app/components/list-generic-resource/list-generic-resource.component.html b/frontend/src/app/components/list-generic-resource/list-generic-resource.component.html index 49fd66d6..b0cf9d4e 100644 --- a/frontend/src/app/components/list-generic-resource/list-generic-resource.component.html +++ b/frontend/src/app/components/list-generic-resource/list-generic-resource.component.html @@ -1,4 +1,6 @@
+

INSIDE THE GERNERIC COMPONENT

+ { let column = {name: defn.title, prop: defn.title.replace(/[^A-Z0-9]/ig, "_")} return column @@ -44,10 +62,13 @@ export class ListGenericResourceComponent implements OnInit { let row = {} this.columnDefinitions.forEach((defn) => { - let resourceProp = defn.getter(resource.payload) - let resourceFormatted = defn.format ? FORMATTERS[defn.format](resourceProp) : resourceProp - row[defn.title.replace(/[^A-Z0-9]/ig, "_")] = resourceFormatted - //TODO: handle defaultValue + try{ + let resourceProp = defn.getter(resource.payload) + let resourceFormatted = defn.format ? FORMATTERS[defn.format](resourceProp) : resourceProp + row[defn.title.replace(/[^A-Z0-9]/ig, "_")] = resourceFormatted + }catch (e){ + //ignore + } }) console.log("ROW:", row) @@ -55,6 +76,7 @@ export class ListGenericResourceComponent implements OnInit { return row }) } + } /////////////////////////////////////////////////////////////////////////////////////// diff --git a/frontend/src/app/components/list-generic-resource/list-medication-administration.component.ts b/frontend/src/app/components/list-generic-resource/list-medication-administration.component.ts new file mode 100644 index 00000000..4d97f1ef --- /dev/null +++ b/frontend/src/app/components/list-generic-resource/list-medication-administration.component.ts @@ -0,0 +1,19 @@ +import {Component} from '@angular/core'; +import {GenericColumnDefn, ListGenericResourceComponent} from './list-generic-resource.component'; +import {attributeXTime} from './utils'; + +@Component({ + selector: 'app-list-medication-administration', + templateUrl: './list-generic-resource.component.html', + styleUrls: ['./list-generic-resource.component.scss'] +}) +export class ListMedicationAdministrationComponent extends ListGenericResourceComponent { + columnDefinitions: GenericColumnDefn[] = [ + { title: 'Medication', versions: '*', format: 'code', getter: m => m.medicationCodeableConcept.coding[0] }, + { title: 'Route', versions: '*', format: 'code', getter: m => m.dosage.route.coding[0] }, + { title: 'Effective', versions: '*', getter: m => attributeXTime(m,'effective')}, + { title: 'Status', versions: '*', getter: m => m.status}, + { title: 'Status Reason', versions: '*', format: 'code', getter: m => m.statusReason[0].coding[0] }, + { title: 'Recorded', versions: '*', format: 'date', getter: m => m.extension.recorded } + ] +} diff --git a/frontend/src/app/components/list-generic-resource/list-medication-dispense.component.ts b/frontend/src/app/components/list-generic-resource/list-medication-dispense.component.ts new file mode 100644 index 00000000..625b4991 --- /dev/null +++ b/frontend/src/app/components/list-generic-resource/list-medication-dispense.component.ts @@ -0,0 +1,15 @@ +import {Component} from '@angular/core'; +import {GenericColumnDefn, ListGenericResourceComponent} from './list-generic-resource.component'; +import {attributeXTime} from './utils'; + +@Component({ + selector: 'app-list-medication-dispense', + templateUrl: './list-generic-resource.component.html', + styleUrls: ['./list-generic-resource.component.scss'] +}) +export class ListMedicationDispenseComponent extends ListGenericResourceComponent { + columnDefinitions: GenericColumnDefn[] = [ + { title: 'Medication', versions: '*', format: 'code', getter: m => m.medicationCodeableConcept.coding[0] }, + { title: 'Handed Over Date', versions: '*', format: 'date', getter: m => m.whenHandedOver} + ] +} diff --git a/frontend/src/app/components/list-generic-resource/list-nutrition-order.component.ts b/frontend/src/app/components/list-generic-resource/list-nutrition-order.component.ts new file mode 100644 index 00000000..8e132c61 --- /dev/null +++ b/frontend/src/app/components/list-generic-resource/list-nutrition-order.component.ts @@ -0,0 +1,17 @@ +import {Component} from '@angular/core'; +import {GenericColumnDefn, ListGenericResourceComponent} from './list-generic-resource.component'; +import {attributeXTime} from './utils'; + +@Component({ + selector: 'app-list-nutrition-order', + templateUrl: './list-generic-resource.component.html', + styleUrls: ['./list-generic-resource.component.scss'] +}) +export class ListNutritionOrderComponent extends ListGenericResourceComponent { + columnDefinitions: GenericColumnDefn[] = [ + { title: 'Preference', versions: '*', format: 'code', getter: n => n.foodPreferenceModifier[0].coding[0] }, + { title: 'Exclusion', versions: '*', format: 'code', getter: n => n.excludeFoodModifier[0].coding[0] }, + { title: 'Date', versions: '*', format: 'date', getter: n => n.dateTime }, + { title: 'Status', versions: '*', getter: n => n.status } + ] +} diff --git a/frontend/src/app/components/list-generic-resource/list-service-request.component.ts b/frontend/src/app/components/list-generic-resource/list-service-request.component.ts new file mode 100644 index 00000000..bdf15248 --- /dev/null +++ b/frontend/src/app/components/list-generic-resource/list-service-request.component.ts @@ -0,0 +1,20 @@ +import {Component} from '@angular/core'; +import {GenericColumnDefn, ListGenericResourceComponent} from './list-generic-resource.component'; +import {attributeXTime} from './utils'; + +@Component({ + selector: 'app-list-service-request', + templateUrl: './list-generic-resource.component.html', + styleUrls: ['./list-generic-resource.component.scss'] +}) +export class ListServiceRequestComponent extends ListGenericResourceComponent { + columnDefinitions: GenericColumnDefn[] = [ + { title: 'Service', versions: '*', format: 'code', getter: s => s.code.coding[0] }, + { title: 'Author Date', versions: '*', format: 'date', getter: s => s.authoredOn }, + { title: 'Status', versions: '*', getter: s => s.status }, + { title: 'Reason', versions: '*', format: 'code', getter: s => s.reasonCode[0].coding[0] }, + { title: 'ID', versions: '*', getter: s => s.id }, + { title: 'Do Not Perform', versions: '*', getter: s => s.doNotPerform }, + { title: 'Reason Refused', versions: '*', format: 'code', getter: s => s.extension.reasonRefused.coding[0] } + ] +} diff --git a/frontend/src/app/components/resource-list/resource-list.component.html b/frontend/src/app/components/resource-list/resource-list.component.html new file mode 100644 index 00000000..8a3b0c18 --- /dev/null +++ b/frontend/src/app/components/resource-list/resource-list.component.html @@ -0,0 +1,3 @@ +

{{resourceListType}}

+ + diff --git a/frontend/src/app/components/list-care-plan/list-care-plan.component.scss b/frontend/src/app/components/resource-list/resource-list.component.scss similarity index 100% rename from frontend/src/app/components/list-care-plan/list-care-plan.component.scss rename to frontend/src/app/components/resource-list/resource-list.component.scss diff --git a/frontend/src/app/components/list-care-plan/list-care-plan.component.spec.ts b/frontend/src/app/components/resource-list/resource-list.component.spec.ts similarity index 51% rename from frontend/src/app/components/list-care-plan/list-care-plan.component.spec.ts rename to frontend/src/app/components/resource-list/resource-list.component.spec.ts index 4a897cae..a288d0e6 100644 --- a/frontend/src/app/components/list-care-plan/list-care-plan.component.spec.ts +++ b/frontend/src/app/components/resource-list/resource-list.component.spec.ts @@ -1,18 +1,18 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ListCarePlanComponent } from './list-care-plan.component'; +import { ResourceListComponent } from './resource-list.component'; -describe('ListCarePlanComponent', () => { - let component: ListCarePlanComponent; - let fixture: ComponentFixture; +describe('ResourceListComponent', () => { + let component: ResourceListComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ ListCarePlanComponent ] + declarations: [ ResourceListComponent ] }) .compileComponents(); - fixture = TestBed.createComponent(ListCarePlanComponent); + fixture = TestBed.createComponent(ResourceListComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/frontend/src/app/components/resource-list/resource-list.component.ts b/frontend/src/app/components/resource-list/resource-list.component.ts new file mode 100644 index 00000000..0661336d --- /dev/null +++ b/frontend/src/app/components/resource-list/resource-list.component.ts @@ -0,0 +1,152 @@ +import {ChangeDetectionStrategy, Component, Input, OnChanges, OnInit, SimpleChanges, Type, ViewChild} from '@angular/core'; +import {ResourceListOutletDirective} from '../../directive/resource-list-outlet.directive'; +import {FastenApiService} from '../../services/fasten-api.service'; +import {Source} from '../../models/fasten/source'; +import {Observable, of} from 'rxjs'; +import {ResourceFhir} from '../../models/fasten/resource_fhir'; +import {ListAdverseEventComponent} from '../list-generic-resource/list-adverse-event.component'; +import {ListCommunicationComponent} from '../list-generic-resource/list-communication.component'; +import {ListConditionComponent} from '../list-generic-resource/list-condition.component'; +import {ListCoverageComponent} from '../list-generic-resource/list-coverage.component'; +import {ListDeviceRequestComponent} from '../list-generic-resource/list-device-request.component'; +import {ListDocumentReferenceComponent} from '../list-generic-resource/list-document-reference.component'; +import {ListEncounterComponent} from '../list-generic-resource/list-encounter.component'; +import {ListImmunizationComponent} from '../list-generic-resource/list-immunization.component'; +import {ListMedicationComponent} from '../list-generic-resource/list-medication.component'; +import {ListMedicationAdministrationComponent} from '../list-generic-resource/list-medication-administration.component'; +import {ListMedicationDispenseComponent} from '../list-generic-resource/list-medication-dispense.component'; +import {ListMedicationRequestComponent} from '../list-generic-resource/list-medication-request.component'; +import {ListNutritionOrderComponent} from '../list-generic-resource/list-nutrition-order.component'; +import {ListObservationComponent} from '../list-generic-resource/list-observation.component'; +import {ListProcedureComponent} from '../list-generic-resource/list-procedure.component'; +import {ListServiceRequestComponent} from '../list-generic-resource/list-service-request.component'; +import {map} from 'rxjs/operators'; +import {ResponseWrapper} from '../../models/response-wrapper'; +import {ListGenericResourceComponent, ResourceListComponentInterface} from '../list-generic-resource/list-generic-resource.component'; +import {ListCarePlanComponent} from '../list-generic-resource/list-care-plan.component'; +import {ListAllergyIntoleranceComponent} from '../list-generic-resource/list-allergy-intolerance.component'; + +@Component({ + selector: 'source-resource-list', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './resource-list.component.html', + styleUrls: ['./resource-list.component.scss'] +}) +export class ResourceListComponent implements OnInit, OnChanges { + + @Input() source: Source; + @Input() resourceListType: string; + resourceListCache: { [name:string]: ResourceFhir[] } = {} + + + + //location to dynamically load the resource list + @ViewChild(ResourceListOutletDirective, {static: true}) resourceListOutlet!: ResourceListOutletDirective; + + + constructor(private fastenApi: FastenApiService) { } + + ngOnInit(): void { + this.loadComponent() + } + ngOnChanges(changes: SimpleChanges) { + this.loadComponent() + } + + loadComponent() { + //clear the current outlet + const viewContainerRef = this.resourceListOutlet.viewContainerRef; + viewContainerRef.clear(); + + this.getResources().subscribe((resourceList) => { + let componentType = this.typeLookup(this.resourceListType) + if(componentType != null){ + console.log("Attempting to create component", this.resourceListType, componentType) + const componentRef = viewContainerRef.createComponent(componentType); + componentRef.instance.resourceList = resourceList; + componentRef.instance.markForCheck() + + } + }) + } + + getResources(): Observable{ + + if(!this.resourceListCache[this.resourceListType]){ + // this resource type list has not been downloaded yet, do so now + + return this.fastenApi.getResources(this.resourceListType, this.source.id) + .pipe(map((resourceList: ResourceFhir[]) => { + //cache this response so we can skip the request next time + this.resourceListCache[this.resourceListType] = resourceList + return resourceList + })) + } else { + return of(this.resourceListCache[this.resourceListType]) + } + } + + typeLookup(resourceType: string): Type { + switch(resourceType) { + case "AllergyIntolerance": { + return ListAllergyIntoleranceComponent; + } + case "AdverseEvent": { + return ListAdverseEventComponent; + } + case "CarePlan": { + return ListCarePlanComponent; + } + case "Communication": { + return ListCommunicationComponent; + } + case "Condition": { + return ListConditionComponent; + } + case "Coverage": { + return ListCoverageComponent; + } + case "DeviceRequest": { + return ListDeviceRequestComponent; + } + case "DocumentReference": { + return ListDocumentReferenceComponent; + } + case "Encounter": { + return ListEncounterComponent; + } + case "Immunization": { + return ListImmunizationComponent; + } + case "Medication": { + return ListMedicationComponent; + } + case "MedicationAdministration": { + return ListMedicationAdministrationComponent; + } + case "MedicationDispense": { + return ListMedicationDispenseComponent; + } + case "MedicationRequest": { + return ListMedicationRequestComponent; + } + case "NutritionOrder": { + return ListNutritionOrderComponent; + } + case "Observation": { + return ListObservationComponent; + } + case "Procedure": { + return ListProcedureComponent; + } + case "ServiceRequest": { + return ListServiceRequestComponent; + } + default: { + console.error("UNKNOWN COMPONENT TYPE", resourceType) + return null + + } + } + } +} diff --git a/frontend/src/app/components/shared.module.ts b/frontend/src/app/components/shared.module.ts index 3adb13dc..1d4f7f42 100644 --- a/frontend/src/app/components/shared.module.ts +++ b/frontend/src/app/components/shared.module.ts @@ -1,58 +1,93 @@ -import { NgModule } from '@angular/core'; import { ComponentsSidebarComponent } from './components-sidebar/components-sidebar.component'; +import { ListExplanationOfBenefitComponent } from './list-explanation-of-benefit/list-explanation-of-benefit.component'; +import { ListGenericResourceComponent,} from './list-generic-resource/list-generic-resource.component'; +import { ListPatientComponent } from './list-patient/list-patient.component'; +import { NgModule } from '@angular/core'; +import { NgxDatatableModule } from '@swimlane/ngx-datatable'; import { RouterModule } from '@angular/router'; import { UtilitiesSidebarComponent } from './utilities-sidebar/utilities-sidebar.component'; -import { ListPatientComponent } from './list-patient/list-patient.component'; -import { ListExplanationOfBenefitComponent } from './list-explanation-of-benefit/list-explanation-of-benefit.component'; -import { ListCarePlanComponent } from './list-care-plan/list-care-plan.component'; import {BrowserModule} from '@angular/platform-browser'; -import { ListGenericResourceComponent,} from './list-generic-resource/list-generic-resource.component'; +import {ListAdverseEventComponent} from './list-generic-resource/list-adverse-event.component'; import {ListConditionComponent} from './list-generic-resource/list-condition.component' import {ListEncounterComponent} from './list-generic-resource/list-encounter.component' +import {ListImmunizationComponent} from './list-generic-resource/list-immunization.component' +import {ListMedicationAdministrationComponent} from './list-generic-resource/list-medication-administration.component'; import {ListMedicationComponent} from './list-generic-resource/list-medication.component' +import {ListMedicationDispenseComponent} from './list-generic-resource/list-medication-dispense.component'; +import {ListMedicationRequestComponent} from './list-generic-resource/list-medication-request.component' +import {ListNutritionOrderComponent} from './list-generic-resource/list-nutrition-order.component'; import {ListObservationComponent} from './list-generic-resource/list-observation.component' import {ListProcedureComponent} from './list-generic-resource/list-procedure.component' -import {ListImmunizationComponent} from './list-generic-resource/list-immunization.component' -import {ListMedicationRequestComponent} from './list-generic-resource/list-medication-request.component' - -import { NgxDatatableModule } from '@swimlane/ngx-datatable'; +import {ListCommunicationComponent} from './list-generic-resource/list-communication.component'; +import {ListDeviceRequestComponent} from './list-generic-resource/list-device-request.component'; +import {ListCoverageComponent} from './list-generic-resource/list-coverage.component'; +import {ListServiceRequestComponent} from './list-generic-resource/list-service-request.component'; +import {ListDocumentReferenceComponent} from './list-generic-resource/list-document-reference.component'; +import { ResourceListComponent } from './resource-list/resource-list.component'; +import {ResourceListOutletDirective} from '../directive/resource-list-outlet.directive'; +import {ListCarePlanComponent} from './list-generic-resource/list-care-plan.component'; +import {ListAllergyIntoleranceComponent} from './list-generic-resource/list-allergy-intolerance.component'; @NgModule({ imports: [ RouterModule, BrowserModule, - NgxDatatableModule + NgxDatatableModule, ], declarations: [ ComponentsSidebarComponent, UtilitiesSidebarComponent, - ListPatientComponent, - ListObservationComponent, - ListExplanationOfBenefitComponent, - ListImmunizationComponent, - ListEncounterComponent, + ListAllergyIntoleranceComponent, + ListAdverseEventComponent, ListCarePlanComponent, - ListGenericResourceComponent, - ListMedicationComponent, - ListProcedureComponent, + ListCommunicationComponent, ListConditionComponent, - ListMedicationRequestComponent + ListEncounterComponent, + ListExplanationOfBenefitComponent, + ListGenericResourceComponent, + ListImmunizationComponent, + ListMedicationAdministrationComponent, + ListMedicationComponent, + ListMedicationDispenseComponent, + ListMedicationRequestComponent, + ListNutritionOrderComponent, + ListObservationComponent, + ListPatientComponent, + ListDeviceRequestComponent, + ListProcedureComponent, + ListCoverageComponent, + ListServiceRequestComponent, + ListDocumentReferenceComponent, + ResourceListComponent, + ResourceListOutletDirective, + + ], exports: [ - ComponentsSidebarComponent, - UtilitiesSidebarComponent, - ListPatientComponent, - ListExplanationOfBenefitComponent, - ListImmunizationComponent, - ListEncounterComponent, - ListCarePlanComponent, - ListGenericResourceComponent, - ListMedicationComponent, - ListObservationComponent, - ListProcedureComponent, - ListConditionComponent, - ListMedicationRequestComponent - + ComponentsSidebarComponent, + ListAllergyIntoleranceComponent, + ListAdverseEventComponent, + ListCarePlanComponent, + ListCommunicationComponent, + ListConditionComponent, + ListEncounterComponent, + ListExplanationOfBenefitComponent, + ListGenericResourceComponent, + ListImmunizationComponent, + ListMedicationAdministrationComponent, + ListMedicationComponent, + ListMedicationDispenseComponent, + ListMedicationRequestComponent, + ListNutritionOrderComponent, + ListObservationComponent, + ListPatientComponent, + ListProcedureComponent, + ListDeviceRequestComponent, + UtilitiesSidebarComponent, + ListCoverageComponent, + ListServiceRequestComponent, + ListDocumentReferenceComponent, + ResourceListComponent ] }) diff --git a/frontend/src/app/directive/resource-list-outlet.directive.spec.ts b/frontend/src/app/directive/resource-list-outlet.directive.spec.ts new file mode 100644 index 00000000..2f16074f --- /dev/null +++ b/frontend/src/app/directive/resource-list-outlet.directive.spec.ts @@ -0,0 +1,8 @@ +import { ResourceListOutletDirective } from './resource-list-outlet.directive'; + +describe('ResourceListOutletDirective', () => { + it('should create an instance', () => { + const directive = new ResourceListOutletDirective(); + expect(directive).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/directive/resource-list-outlet.directive.ts b/frontend/src/app/directive/resource-list-outlet.directive.ts new file mode 100644 index 00000000..c8888430 --- /dev/null +++ b/frontend/src/app/directive/resource-list-outlet.directive.ts @@ -0,0 +1,10 @@ +import {Directive, ViewContainerRef} from '@angular/core'; + +@Directive({ + selector: '[resourceListOutlet]' +}) +export class ResourceListOutletDirective { + + constructor(public viewContainerRef: ViewContainerRef) { } + +} diff --git a/frontend/src/app/models/fasten/source-summary.ts b/frontend/src/app/models/fasten/source-summary.ts new file mode 100644 index 00000000..6510013e --- /dev/null +++ b/frontend/src/app/models/fasten/source-summary.ts @@ -0,0 +1,12 @@ +import {Source} from './source'; + +export class ResourceTypeCounts { + count: number + source_id: string + resource_type: string +} + +export class SourceSummary { + source: Source + resource_type_counts: ResourceTypeCounts[] +} diff --git a/frontend/src/app/pages/source-detail/source-detail.component.html b/frontend/src/app/pages/source-detail/source-detail.component.html index 0d46804b..b04bdf16 100644 --- a/frontend/src/app/pages/source-detail/source-detail.component.html +++ b/frontend/src/app/pages/source-detail/source-detail.component.html @@ -2,8 +2,8 @@
    -
  • - {{resourceGroup.key}} {{resourceGroup.value.length}} +
  • + {{resourceGroup.key}} {{resourceGroup.value}}
@@ -14,16 +14,7 @@ Details
-

{{selectedResourceType}}

- - - - - - - - - +
diff --git a/frontend/src/app/pages/source-detail/source-detail.component.ts b/frontend/src/app/pages/source-detail/source-detail.component.ts index 2f50726a..e8d04369 100644 --- a/frontend/src/app/pages/source-detail/source-detail.component.ts +++ b/frontend/src/app/pages/source-detail/source-detail.component.ts @@ -1,7 +1,8 @@ -import { Component, OnInit } from '@angular/core'; +import {Component, OnInit, ViewChild} from '@angular/core'; import {ActivatedRoute, Router} from '@angular/router'; import {Source} from '../../models/fasten/source'; import {FastenApiService} from '../../services/fasten-api.service'; +import {ResourceListOutletDirective} from '../../directive/resource-list-outlet.directive'; @Component({ @@ -13,9 +14,8 @@ export class SourceDetailComponent implements OnInit { selectedSource: Source = null selectedResourceType: string = null - selectedResources: any[] = [] - resourcesGroupedByType: { [name: string]: any[] } = {} + resourceTypeCounts: { [name: string]: number } = {} constructor(private fastenApi: FastenApiService, private router: Router, private route: ActivatedRoute) { //check if the current Source was sent over using the router state storage: @@ -25,26 +25,16 @@ export class SourceDetailComponent implements OnInit { } ngOnInit(): void { - if (this.selectedSource == null) { - //lookup the source by ID. - this.fastenApi.getSource(this.route.snapshot.paramMap.get('source_id')).subscribe((source) => { - this.selectedSource = source; - }); - } - - this.fastenApi.getResources(undefined, this.route.snapshot.paramMap.get('source_id')).subscribe((resourceList) => { - console.log("RESOURCES", resourceList) - let _resourcesGroupedByType = this.resourcesGroupedByType - resourceList.forEach(resource => { - let resourceTypeList = _resourcesGroupedByType[resource.source_resource_type] || []; - resourceTypeList.push(resource) - _resourcesGroupedByType[resource.source_resource_type] = resourceTypeList - }) - }) + //always request the source summary + this.fastenApi.getSourceSummary(this.route.snapshot.paramMap.get('source_id')).subscribe((sourceSummary) => { + this.selectedSource = sourceSummary.source; + for(let resourceTypeCount of sourceSummary.resource_type_counts){ + this.resourceTypeCounts[resourceTypeCount.resource_type] = resourceTypeCount.count + } + }); } selectResourceType(resourceType: string) { this.selectedResourceType = resourceType - this.selectedResources = this.resourcesGroupedByType[resourceType] } } diff --git a/frontend/src/app/services/fasten-api.service.ts b/frontend/src/app/services/fasten-api.service.ts index 46e11ad7..d837f61c 100644 --- a/frontend/src/app/services/fasten-api.service.ts +++ b/frontend/src/app/services/fasten-api.service.ts @@ -9,6 +9,7 @@ import {ResponseWrapper} from '../models/response-wrapper'; import {Source} from '../models/fasten/source'; import {User} from '../models/fasten/user'; import {ResourceFhir} from '../models/fasten/resource_fhir'; +import {SourceSummary} from '../models/fasten/source-summary'; @Injectable({ providedIn: 'root' @@ -110,6 +111,16 @@ export class FastenApiService { ); } + getSourceSummary(sourceId: string): Observable { + return this._httpClient.get(`${this.getBasePath()}/api/secure/source/${sourceId}/summary`) + .pipe( + map((response: ResponseWrapper) => { + console.log("SOURCE RESPONSE", response) + return response.data as SourceSummary + }) + ); + } + getResources(sourceResourceType?: string, sourceID?: string): Observable { let queryParams = {} if(sourceResourceType){