WIP adding medical history timeline view. (#325)

This commit is contained in:
Jason Kulatunga 2023-11-23 08:51:01 -08:00 committed by GitHub
parent a161f41998
commit 2061684aed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 1450 additions and 215 deletions

View File

@ -29,17 +29,10 @@ func (rp *VertexResourcePlaceholder) ID() string {
// Retrieve a list of all fhir resources (vertex), and a list of all associations (edge)
// Generate a graph
// return list of root nodes, and their flattened related resources.
func (gr *GormRepository) GetFlattenedResourceGraph(ctx context.Context, graphType pkg.ResourceGraphType, options models.ResourceGraphOptions) (map[string][]*models.ResourceBase, *models.ResourceGraphMetadata, error) {
func (gr *GormRepository) GetFlattenedResourceGraph(ctx context.Context, graphType pkg.ResourceGraphType, options models.ResourceGraphOptions) (map[string][]*models.ResourceBase, error) {
currentUser, currentUserErr := gr.GetCurrentUser(ctx)
if currentUserErr != nil {
return nil, nil, currentUserErr
}
//initialize the graph results metadata
resourceGraphMetadata := models.ResourceGraphMetadata{
TotalElements: 0,
PageSize: 20, //TODO: replace this with pkg.DefaultPageSize
Page: options.Page,
return nil, currentUserErr
}
// Get list of all (non-reciprocal) relationships
@ -52,18 +45,13 @@ func (gr *GormRepository) GetFlattenedResourceGraph(ctx context.Context, graphTy
}).
Find(&relatedResourceRelationships)
if result.Error != nil {
return nil, nil, result.Error
return nil, result.Error
}
log.Printf("found %d related resources", len(relatedResourceRelationships))
//Generate Graph
// TODO optimization: eventually cache the graph in a database/storage, and update when new resources are added.
g := graph.New(resourceVertexId, graph.Directed(), graph.Acyclic(), graph.Rooted())
//// Get list of all resources TODO - REPLACED THIS
//wrappedResourceModels, err := gr.ListResources(ctx, models.ListResourceQueryOptions{})
//if err != nil {
// return nil, err
//}
g := graph.New(resourceVertexId, graph.Directed(), graph.Rooted())
//add vertices to the graph (must be done first)
//we don't want to request all resources from the database, so we will create a placeholder vertex for each resource.
@ -105,7 +93,7 @@ func (gr *GormRepository) GetFlattenedResourceGraph(ctx context.Context, graphTy
&resourcePlaceholder,
)
if err != nil {
return nil, nil, fmt.Errorf("an error occurred while adding vertex: %v", err)
return nil, fmt.Errorf("an error occurred while adding vertex: %v", err)
}
}
@ -142,7 +130,7 @@ func (gr *GormRepository) GetFlattenedResourceGraph(ctx context.Context, graphTy
// }
adjacencyMap, err := g.AdjacencyMap()
if err != nil {
return nil, nil, fmt.Errorf("error while generating AdjacencyMap: %v", err)
return nil, fmt.Errorf("error while generating AdjacencyMap: %v", err)
}
// For a directed graph, PredecessorMap is the complement of AdjacencyMap. This is because in a directed graph, only
@ -151,12 +139,12 @@ func (gr *GormRepository) GetFlattenedResourceGraph(ctx context.Context, graphTy
// ie. "empty" verticies in this map are "root" nodes.
predecessorMap, err := g.PredecessorMap()
if err != nil {
return nil, nil, fmt.Errorf("error while generating PredecessorMap: %v", err)
return nil, fmt.Errorf("error while generating PredecessorMap: %v", err)
}
// Doing this in one massive function, because passing graph by reference is difficult due to generics.
// Step 1: use predecessorMap to find all "root" resources (eg. MedicalHistory - encounters and conditions). store those nodes in their respective lists.
// Step 1: use predecessorMap to find all "root" resources (eg. MedicalHistory - encounters and EOB). store those nodes in their respective lists.
resourcePlaceholderListDictionary := map[string][]*VertexResourcePlaceholder{}
sources, _, sourceFlattenLevel := getSourcesAndSinksForGraphType(graphType)
@ -201,11 +189,10 @@ func (gr *GormRepository) GetFlattenedResourceGraph(ctx context.Context, graphTy
// Step 2: now that we've created a relationship graph using placeholders, we need to determine which page of resources to return
// and look up the actual resources from the database.
resourceListDictionary, totalElements, err := gr.InflateResourceGraphAtPage(resourcePlaceholderListDictionary, options.Page)
resourceListDictionary, err := gr.InflateSelectedResourcesInResourceGraph(currentUser, resourcePlaceholderListDictionary, options)
if err != nil {
return nil, nil, fmt.Errorf("error while paginating & inflating resource graph: %v", err)
return nil, fmt.Errorf("error while paginating & inflating resource graph: %v", err)
}
resourceGraphMetadata.TotalElements = totalElements
// Step 3: define a function. When given a resource, should find all related resources, flatten the heirarchy and set the RelatedResourceFhir list
flattenRelatedResourcesFn := func(resource *models.ResourceBase) {
@ -220,22 +207,30 @@ func (gr *GormRepository) GetFlattenedResourceGraph(ctx context.Context, graphTy
resource.RelatedResource = []*models.ResourceBase{}
//make sure we don't keep traversing the same node over and over again
visited := map[string]bool{
vertexId: true,
}
//get all the resource placeholders associated with this node
//TODO: handle error?
graph.DFS(g, vertexId, func(relatedVertexId string) bool {
relatedResourcePlaceholder, _ := g.Vertex(relatedVertexId)
//skip the current resourcePlaceholder if it's referenced in this list.
//skip any "visted" nodes
//also skip the current resourcePlaceholder if its a Binary resourcePlaceholder (which is a special case)
if vertexId != resourceVertexId(relatedResourcePlaceholder) && relatedResourcePlaceholder.ResourceType != "Binary" {
if _, hasVisited := visited[resourceVertexId(relatedResourcePlaceholder)]; !hasVisited && relatedResourcePlaceholder.ResourceType != "Binary" {
relatedResource, err := gr.GetResourceByResourceTypeAndId(ctx, relatedResourcePlaceholder.ResourceType, relatedResourcePlaceholder.ResourceID)
if err != nil {
gr.Logger.Warnf("ignoring, cannot safely handle error which occurred while getting related resource: %v", err)
return true
gr.Logger.Warnf("ignoring, cannot safely handle error which occurred while getting related resource (%s/%s): %v", relatedResourcePlaceholder.ResourceType, relatedResourcePlaceholder.ResourceID, err)
return false
}
resource.RelatedResource = append(
resource.RelatedResource,
relatedResource,
)
visited[resourceVertexId(relatedResourcePlaceholder)] = true
}
return false
})
@ -288,36 +283,50 @@ func (gr *GormRepository) GetFlattenedResourceGraph(ctx context.Context, graphTy
// Step 5: return the populated resource list dictionary
return resourceListDictionary, &resourceGraphMetadata, nil
return resourceListDictionary, nil
}
// LoadResourceGraphAtPage - this function will take a dictionary of placeholder "sources" graph and load the actual resources from the database, for a specific page
// InflateSelectedResourcesInResourceGraph - this function will take a dictionary of placeholder "sources" graph and load the selected resources (and their descendants) from the database.
// - first, it will load all the "source" resources (eg. Encounter, Condition, etc)
// - sort the root resources by date, desc
// - use the page number + page size to determine which root resources to return
// - return a dictionary of "source" resource lists
func (gr *GormRepository) InflateResourceGraphAtPage(resourcePlaceholderListDictionary map[string][]*VertexResourcePlaceholder, page int) (map[string][]*models.ResourceBase, int, error) {
totalElements := 0
// Step 3a: since we cant calulate the sort order until the resources are loaded, we need to load all the root resources first.
func (gr *GormRepository) InflateSelectedResourcesInResourceGraph(currentUser *models.User, resourcePlaceholderListDictionary map[string][]*VertexResourcePlaceholder, options models.ResourceGraphOptions) (map[string][]*models.ResourceBase, error) {
// Step 3a: group the selected resources by type, so we only need to do 1 query per type
selectedResourceIdsByResourceType := map[string][]models.OriginBase{}
for _, resourceId := range options.ResourcesIds {
if _, ok := selectedResourceIdsByResourceType[resourceId.SourceResourceType]; !ok {
selectedResourceIdsByResourceType[resourceId.SourceResourceType] = []models.OriginBase{}
}
selectedResourceIdsByResourceType[resourceId.SourceResourceType] = append(selectedResourceIdsByResourceType[resourceId.SourceResourceType], resourceId)
}
// Step 3b: query the database for all the selected resources
//TODO: maybe its more performant to query each resource by type/id/source, since they are indexed already?
rootWrappedResourceModels := []models.ResourceBase{}
for resourceType, _ := range resourcePlaceholderListDictionary {
// resourcePlaceholderListDictionary contains top level resource types (eg. Encounter, Condition, etc)
for resourceType, _ := range selectedResourceIdsByResourceType {
// selectedResourceIdsByResourceType contains selected resources grouped by ty[e types (eg. Encounter, Condition, etc)
//convert these to a list of interface{} for the query
selectList := [][]interface{}{}
for ndx, _ := range resourcePlaceholderListDictionary[resourceType] {
for ndx, _ := range selectedResourceIdsByResourceType[resourceType] {
selectedResource := selectedResourceIdsByResourceType[resourceType][ndx]
selectList = append(selectList, []interface{}{
resourcePlaceholderListDictionary[resourceType][ndx].UserID,
resourcePlaceholderListDictionary[resourceType][ndx].SourceID,
resourcePlaceholderListDictionary[resourceType][ndx].ResourceType,
resourcePlaceholderListDictionary[resourceType][ndx].ResourceID,
currentUser.ID,
selectedResource.SourceID,
selectedResource.SourceResourceType,
selectedResource.SourceResourceID,
})
}
tableName, err := databaseModel.GetTableNameByResourceType(resourceType)
if err != nil {
return nil, totalElements, err
return nil, err
}
var tableWrappedResourceModels []models.ResourceBase
gr.GormClient.
@ -332,13 +341,7 @@ func (gr *GormRepository) InflateResourceGraphAtPage(resourcePlaceholderListDict
//sort
rootWrappedResourceModels = utils.SortResourceListByDate(rootWrappedResourceModels)
//calculate total elements
totalElements = len(rootWrappedResourceModels)
//paginate (by calculating window for the slice)
rootWrappedResourceModels = utils.PaginateResourceList(rootWrappedResourceModels, page, 20) //todo: replace size with pkg.ResourceListPageSize
// Step 3b: now that we have the root resources, lets generate a dictionary of resource lists, keyed by resource type
// Step 3c: now that we have the selected root resources, lets generate a dictionary of resource lists, keyed by resource type
resourceListDictionary := map[string][]*models.ResourceBase{}
for ndx, _ := range rootWrappedResourceModels {
resourceType := rootWrappedResourceModels[ndx].SourceResourceType
@ -349,7 +352,7 @@ func (gr *GormRepository) InflateResourceGraphAtPage(resourcePlaceholderListDict
}
// Step 4: return the populated resource list dictionary
return resourceListDictionary, totalElements, nil
return resourceListDictionary, nil
}
// We need to support the following types of graphs:
@ -445,11 +448,10 @@ func getSourcesAndSinksForGraphType(graphType pkg.ResourceGraphType) ([][]string
switch graphType {
case pkg.ResourceGraphTypeMedicalHistory:
sources = [][]string{
{"condition", "composition"},
{"encounter", "explanationofbenefit"},
}
sinks = [][]string{
{"location", "device", "organization", "practitioner", "medication", "patient", "coverage"}, //resources that are shared across multiple conditions
{"condition", "composition", "location", "device", "organization", "practitioner", "medication", "patient", "coverage"}, //resources that are shared across multiple conditions
{"binary"},
}
sourceFlattenRelated = map[string]bool{

View File

@ -0,0 +1,192 @@
package database
import (
"context"
"fmt"
"github.com/fastenhealth/fasten-onprem/backend/pkg"
mock_config "github.com/fastenhealth/fasten-onprem/backend/pkg/config/mock"
"github.com/fastenhealth/fasten-onprem/backend/pkg/event_bus"
"github.com/fastenhealth/fasten-onprem/backend/pkg/models"
sourceFactory "github.com/fastenhealth/fasten-sources/clients/factory"
sourcePkg "github.com/fastenhealth/fasten-sources/pkg"
"github.com/golang/mock/gomock"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"io/ioutil"
"log"
"os"
"testing"
)
// Define the suite, and absorb the built-in basic suite
// functionality from testify - including a T() method which
// returns the current testing context
type RepositoryGraphTestSuite struct {
suite.Suite
MockCtrl *gomock.Controller
TestDatabase *os.File
}
// BeforeTest has a function to be executed right before the test starts and receives the suite and test names as input
func (suite *RepositoryGraphTestSuite) BeforeTest(suiteName, testName string) {
suite.MockCtrl = gomock.NewController(suite.T())
dbFile, err := ioutil.TempFile("", fmt.Sprintf("%s.*.db", testName))
if err != nil {
log.Fatal(err)
}
suite.TestDatabase = dbFile
}
// AfterTest has a function to be executed right after the test finishes and receives the suite and test names as input
func (suite *RepositoryGraphTestSuite) AfterTest(suiteName, testName string) {
suite.MockCtrl.Finish()
os.Remove(suite.TestDatabase.Name())
os.Remove(suite.TestDatabase.Name() + "-shm")
os.Remove(suite.TestDatabase.Name() + "-wal")
}
// In order for 'go test' to run this suite, we need to create
// a normal test function and pass our suite to suite.Run
func TestRepositoryGraphTestSuite(t *testing.T) {
suite.Run(t, new(RepositoryGraphTestSuite))
}
func (suite *RepositoryGraphTestSuite) TestGetFlattenedResourceGraph() {
//setup
fakeConfig := mock_config.NewMockInterface(suite.MockCtrl)
fakeConfig.EXPECT().GetString("database.location").Return(suite.TestDatabase.Name()).AnyTimes()
fakeConfig.EXPECT().GetString("database.type").Return("sqlite").AnyTimes()
fakeConfig.EXPECT().IsSet("database.encryption.key").Return(false).AnyTimes()
fakeConfig.EXPECT().GetString("log.level").Return("INFO").AnyTimes()
dbRepo, err := NewRepository(fakeConfig, logrus.WithField("test", suite.T().Name()), event_bus.NewNoopEventBusServer())
require.NoError(suite.T(), err)
userModel := &models.User{
Username: "test_username",
Password: "testpassword",
Email: "test@test.com",
}
err = dbRepo.CreateUser(context.Background(), userModel)
require.NoError(suite.T(), err)
require.NotEmpty(suite.T(), userModel.ID)
require.NotEqual(suite.T(), uuid.Nil, userModel.ID)
authContext := context.WithValue(context.Background(), pkg.ContextKeyTypeAuthUsername, "test_username")
testSourceCredential := models.SourceCredential{
ModelBase: models.ModelBase{
ID: uuid.New(),
},
UserID: userModel.ID,
}
err = dbRepo.CreateSource(authContext, &testSourceCredential)
require.NoError(suite.T(), err)
bundleFile, err := os.Open("./testdata/Abraham100_Heller342_262b819a-5193-404a-9787-b7f599358035.json")
require.NoError(suite.T(), err)
testLogger := logrus.WithFields(logrus.Fields{
"type": "test",
})
manualClient, err := sourceFactory.GetSourceClient(sourcePkg.FastenLighthouseEnvSandbox, sourcePkg.SourceTypeManual, authContext, testLogger, &testSourceCredential)
summary, err := manualClient.SyncAllBundle(dbRepo, bundleFile, sourcePkg.FhirVersion401)
require.NoError(suite.T(), err)
require.Equal(suite.T(), 198, summary.TotalResources)
require.Equal(suite.T(), 234, len(summary.UpdatedResources))
//test
options := models.ResourceGraphOptions{
ResourcesIds: []models.OriginBase{
{
SourceID: testSourceCredential.ID,
SourceResourceType: "Encounter",
SourceResourceID: "d9a7c76f-dfe4-41c6-9924-82a405613f44",
},
},
}
flattenedGraph, err := dbRepo.GetFlattenedResourceGraph(authContext, pkg.ResourceGraphTypeMedicalHistory, options)
require.NoError(suite.T(), err)
require.NotNil(suite.T(), flattenedGraph)
//validated using http://clinfhir.com/bundleVisualizer.html
require.Equal(suite.T(), 1, len(flattenedGraph["Encounter"]))
require.Equal(suite.T(), 27, len(flattenedGraph["Encounter"][0].RelatedResource))
}
// Bug: the Epic enounter graph was not consistently return the same related resources
func (suite *RepositoryGraphTestSuite) TestGetFlattenedResourceGraph_NDJson() {
//setup
fakeConfig := mock_config.NewMockInterface(suite.MockCtrl)
fakeConfig.EXPECT().GetString("database.location").Return(suite.TestDatabase.Name()).AnyTimes()
fakeConfig.EXPECT().GetString("database.type").Return("sqlite").AnyTimes()
fakeConfig.EXPECT().IsSet("database.encryption.key").Return(false).AnyTimes()
fakeConfig.EXPECT().GetString("log.level").Return("INFO").AnyTimes()
dbRepo, err := NewRepository(fakeConfig, logrus.WithField("test", suite.T().Name()), event_bus.NewNoopEventBusServer())
require.NoError(suite.T(), err)
userModel := &models.User{
Username: "test_username",
Password: "testpassword",
Email: "test@test.com",
}
err = dbRepo.CreateUser(context.Background(), userModel)
require.NoError(suite.T(), err)
require.NotEmpty(suite.T(), userModel.ID)
require.NotEqual(suite.T(), uuid.Nil, userModel.ID)
authContext := context.WithValue(context.Background(), pkg.ContextKeyTypeAuthUsername, "test_username")
testSourceCredential := models.SourceCredential{
ModelBase: models.ModelBase{
ID: uuid.New(),
},
UserID: userModel.ID,
}
err = dbRepo.CreateSource(authContext, &testSourceCredential)
require.NoError(suite.T(), err)
bundleFile, err := os.Open("./testdata/epic_fhircamila.ndjson")
require.NoError(suite.T(), err)
testLogger := logrus.WithFields(logrus.Fields{
"type": "test",
})
manualClient, err := sourceFactory.GetSourceClient(sourcePkg.FastenLighthouseEnvSandbox, sourcePkg.SourceTypeManual, authContext, testLogger, &testSourceCredential)
summary, err := manualClient.SyncAllBundle(dbRepo, bundleFile, sourcePkg.FhirVersion401)
require.NoError(suite.T(), err)
require.Equal(suite.T(), 58, summary.TotalResources)
require.Equal(suite.T(), 58, len(summary.UpdatedResources))
//test
options := models.ResourceGraphOptions{
ResourcesIds: []models.OriginBase{
{
SourceID: testSourceCredential.ID,
SourceResourceType: "Encounter",
SourceResourceID: "eGmO0h.1.UQQrExl4bfM7OQ3",
},
},
}
flattenedGraph, err := dbRepo.GetFlattenedResourceGraph(authContext, pkg.ResourceGraphTypeMedicalHistory, options)
require.NoError(suite.T(), err)
require.NotNil(suite.T(), flattenedGraph)
//validated using http://clinfhir.com/bundleVisualizer.html
require.Equal(suite.T(), 1, len(flattenedGraph["Encounter"]))
//REGRESSION: in some cases the flattened graph was not correctly returning 7 related, instead only retuning 1 or 4. Bug in Graph generation
for ndx, found := range flattenedGraph["Encounter"][0].RelatedResource {
suite.T().Logf("ndx: %d, found: %s/%s", ndx, found.SourceResourceType, found.SourceResourceID)
}
require.Equal(suite.T(), 7, len(flattenedGraph["Encounter"][0].RelatedResource))
}

View File

@ -28,7 +28,7 @@ type DatabaseRepository interface {
AddResourceAssociation(ctx context.Context, source *models.SourceCredential, resourceType string, resourceId string, relatedSource *models.SourceCredential, relatedResourceType string, relatedResourceId string) error
RemoveResourceAssociation(ctx context.Context, source *models.SourceCredential, resourceType string, resourceId string, relatedSource *models.SourceCredential, relatedResourceType string, relatedResourceId string) error
FindResourceAssociationsByTypeAndId(ctx context.Context, source *models.SourceCredential, resourceType string, resourceId string) ([]models.RelatedResource, error)
GetFlattenedResourceGraph(ctx context.Context, graphType pkg.ResourceGraphType, options models.ResourceGraphOptions) (map[string][]*models.ResourceBase, *models.ResourceGraphMetadata, error)
GetFlattenedResourceGraph(ctx context.Context, graphType pkg.ResourceGraphType, options models.ResourceGraphOptions) (map[string][]*models.ResourceBase, error)
AddResourceComposition(ctx context.Context, compositionTitle string, resources []*models.ResourceBase) error
//UpsertProfile(context.Context, *models.Profile) error
//UpsertOrganziation(context.Context, *models.Organization) error

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
package models
type ResourceGraphOptions struct {
Page int
ResourcesIds []OriginBase `json:"resource_ids"`
}

View File

@ -76,7 +76,7 @@ func ListResourceFhir(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": true, "data": wrappedResourceModels})
}
//this endpoint retrieves a specific resource by its ID
// this endpoint retrieves a specific resource by its ID
func GetResourceFhir(c *gin.Context) {
logger := c.MustGet(pkg.ContextKeyTypeLogger).(*logrus.Entry)
databaseRepo := c.MustGet(pkg.ContextKeyTypeDatabase).(database.DatabaseRepository)
@ -129,18 +129,20 @@ func GetResourceFhirGraph(c *gin.Context) {
graphType := strings.Trim(c.Param("graphType"), "/")
graphOptions := models.ResourceGraphOptions{}
if len(c.Query("page")) > 0 {
pageNumb, err := strconv.Atoi(c.Query("page"))
if err != nil {
logger.Errorln("An error occurred while calculating page number", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
var graphOptions models.ResourceGraphOptions
if err := c.ShouldBindJSON(&graphOptions); err != nil {
logger.Errorln("An error occurred while parsing resource graph query options", err)
c.JSON(http.StatusBadRequest, gin.H{"success": false})
return
}
graphOptions.Page = pageNumb
if len(graphOptions.ResourcesIds) == 0 {
logger.Errorln("No resource ids specified")
c.JSON(http.StatusBadRequest, gin.H{"success": false})
return
}
resourceListDictionary, resourceListMetadata, err := databaseRepo.GetFlattenedResourceGraph(c, pkg.ResourceGraphType(graphType), graphOptions)
resourceListDictionary, err := databaseRepo.GetFlattenedResourceGraph(c, pkg.ResourceGraphType(graphType), graphOptions)
if err != nil {
logger.Errorln("An error occurred while retrieving list of resources", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
@ -149,6 +151,5 @@ func GetResourceFhirGraph(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": true, "data": map[string]interface{}{
"results": resourceListDictionary,
"metadata": resourceListMetadata,
}})
}

View File

@ -72,7 +72,7 @@ func (ae *AppEngine) Setup() (*gin.RouterGroup, *gin.Engine) {
secure.POST("/source/:sourceId/sync", handler.SourceSync)
secure.GET("/source/:sourceId/summary", handler.GetSourceSummary)
secure.GET("/resource/fhir", handler.ListResourceFhir)
secure.GET("/resource/graph/:graphType", handler.GetResourceFhirGraph)
secure.POST("/resource/graph/:graphType", handler.GetResourceFhirGraph)
secure.GET("/resource/fhir/:sourceId/:resourceId", handler.GetResourceFhir)
secure.POST("/resource/composition", handler.CreateResourceComposition)

View File

@ -25,6 +25,10 @@ import {DiagnosticReportComponent} from '../resources/diagnostic-report/diagnost
import {PractitionerComponent} from '../resources/practitioner/practitioner.component';
import {DocumentReferenceComponent} from '../resources/document-reference/document-reference.component';
import {MediaComponent} from '../resources/media/media.component';
import {LocationComponent} from '../resources/location/location.component';
import {OrganizationComponent} from '../resources/organization/organization.component';
import {ObservationComponent} from '../resources/observation/observation.component';
@Component({
selector: 'fhir-resource',
@ -119,6 +123,9 @@ export class FhirResourceComponent implements OnInit, OnChanges {
case "Immunization": {
return ImmunizationComponent;
}
case "Location": {
return LocationComponent;
}
case "Media": {
return MediaComponent;
}
@ -137,9 +144,12 @@ export class FhirResourceComponent implements OnInit, OnChanges {
// case "NutritionOrder": {
// return ListNutritionOrderComponent;
// }
// case "Observation": {
// return ListObservationComponent;
// }
case "Observation": {
return ObservationComponent;
}
case "Organization": {
return OrganizationComponent;
}
case "Procedure": {
return ProcedureComponent;
}

View File

@ -56,6 +56,7 @@ export class BinaryComponent implements OnInit, FhirResourceComponentInterface {
this.markForCheck()
}, (error) => {
this.loading = false
console.error("Failed to lookup binary resource from attachment:", error)
this.markForCheck()
})
}

View File

@ -32,12 +32,12 @@ export class DiagnosticReportComponent implements OnInit, FhirResourceComponentI
data: this.displayModel?.issued,
enabled: !!this.displayModel?.issued,
},
{
label: 'Category',
data: this.displayModel?.category_coding,
data_type: TableRowItemDataType.CodingList,
enabled: this.displayModel?.has_category_coding,
},
// {
// label: 'Category',
// data: this.displayModel?.category_coding,
// data_type: TableRowItemDataType.CodingList,
// enabled: this.displayModel?.has_category_coding,
// },
{
label: 'Performer',
data: this.displayModel?.performer,
@ -50,6 +50,15 @@ export class DiagnosticReportComponent implements OnInit, FhirResourceComponentI
enabled: !!this.displayModel?.conclusion,
},
];
for(let categoryCodeable of (this.displayModel?.category_coding || [])){
this.tableData.push({
label: `Category`,
data_type: TableRowItemDataType.CodableConcept,
data: categoryCodeable,
enabled: true,
})
}
}
markForCheck(){
this.changeRef.markForCheck()

View File

@ -0,0 +1,20 @@
<div class="card card-fhir-resource" >
<div class="card-header" (click)="isCollapsed = ! isCollapsed">
<div>
<h6 class="card-title">{{displayModel?.name}}</h6>
</div>
<fhir-ui-badge class="float-right" [status]="displayModel?.status">{{displayModel?.status}}</fhir-ui-badge>
<!-- <div class="btn-group">-->
<!-- <button class="btn active">Day</button>-->
<!-- <button class="btn">Week</button>-->
<!-- <button class="btn">Month</button>-->
<!-- </div>-->
</div>
<div #collapse="ngbCollapse" [(ngbCollapse)]="isCollapsed" class="card-body">
<p class="az-content-text mg-b-20">Details and position information for a physical place where services are provided and resources and participants may be stored, found, contained, or accommodated.</p>
<fhir-ui-table [displayModel]="displayModel" [tableData]="tableData"></fhir-ui-table>
</div>
<div *ngIf="showDetails" class="card-footer">
<a class="float-right" routerLink="/explore/{{displayModel?.source_id}}/resource/{{displayModel?.source_resource_id}}">details</a>
</div>
</div>

View File

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

View File

@ -0,0 +1,77 @@
import {ChangeDetectorRef, Component, Input, OnInit} from '@angular/core';
import {NgbCollapseModule} from '@ng-bootstrap/ng-bootstrap';
import {CommonModule} from '@angular/common';
import {BadgeComponent} from '../../common/badge/badge.component';
import {TableComponent} from '../../common/table/table.component';
import {Router, RouterModule} from '@angular/router';
import {FhirResourceComponentInterface} from '../../fhir-resource/fhir-resource-component-interface';
import {ImmunizationModel} from '../../../../../lib/models/resources/immunization-model';
import {TableRowItem, TableRowItemDataType} from '../../common/table/table-row-item';
import * as _ from 'lodash';
import {LocationModel} from '../../../../../lib/models/resources/location-model';
@Component({
standalone: true,
imports: [NgbCollapseModule, CommonModule, BadgeComponent, TableComponent, RouterModule],
selector: 'fhir-location',
templateUrl: './location.component.html',
styleUrls: ['./location.component.scss']
})
export class LocationComponent implements OnInit, FhirResourceComponentInterface {
@Input() displayModel: LocationModel
@Input() showDetails: boolean = true
isCollapsed: boolean = false
tableData: TableRowItem[] = []
constructor(public changeRef: ChangeDetectorRef, public router: Router) { }
ngOnInit(): void {
this.tableData.push( {
label: 'Type',
data: this.displayModel?.type?.[0],
data_type: TableRowItemDataType.CodableConcept,
enabled: !!this.displayModel?.type?.[0],
},
{
label: 'Physical Type',
data: this.displayModel?.physical_type,
data_type: TableRowItemDataType.CodableConcept,
enabled: !!this.displayModel?.physical_type && this.displayModel?.physical_type?.coding?.length > 0,
},
{
label: 'Location Mode',
data: this.displayModel?.mode,
enabled: !!this.displayModel?.mode,
},
{
label: 'Description',
data: this.displayModel?.description,
enabled: !!this.displayModel?.description,
},
// {
// label: 'Address',
// data: this.displayModel?.address,
// data_type: TableRowItemDataType.Reference,
// enabled: !!this.displayModel?.address,
// },
{
label: 'Telecom',
data: this.displayModel?.telecom,
data_type: TableRowItemDataType.Reference,
enabled: !!this.displayModel?.telecom,
},
{
label: 'Managing Organization',
data: this.displayModel?.managing_organization,
data_type: TableRowItemDataType.Reference,
enabled: !!this.displayModel?.managing_organization,
})
}
markForCheck(){
this.changeRef.markForCheck()
}
}

View File

@ -0,0 +1,28 @@
<div class="card card-fhir-resource" >
<div class="card-header" (click)="isCollapsed = ! isCollapsed">
<div>
<h6 class="card-title">{{displayModel?.sort_title}}</h6>
</div>
<fhir-ui-badge class="float-right" [status]="displayModel?.status">{{displayModel?.status}}</fhir-ui-badge>
<!-- <div class="btn-group">-->
<!-- <button class="btn active">Day</button>-->
<!-- <button class="btn">Week</button>-->
<!-- <button class="btn">Month</button>-->
<!-- </div>-->
</div>
<div #collapse="ngbCollapse" [(ngbCollapse)]="isCollapsed" class="card-body">
<p class="az-content-text mg-b-20">Observations are a central element in healthcare, used to support diagnosis, monitor progress, determine baselines and patterns and even capture demographic characteristics.</p>
<fhir-ui-table [displayModel]="displayModel" [tableData]="tableData"></fhir-ui-table>
<canvas baseChart
[height]="chartHeight"
[type]="'bar'"
[datasets]="barChartData"
[labels]="barChartLabels"
[options]="barChartOptions"
></canvas>
</div>
<div *ngIf="showDetails" class="card-footer">
<a class="float-right" routerLink="/explore/{{displayModel?.source_id}}/resource/{{displayModel?.source_resource_id}}">details</a>
</div>
</div>

View File

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

View File

@ -0,0 +1,154 @@
import {ChangeDetectorRef, Component, Input, OnInit} from '@angular/core';
import {NgbCollapseModule} from '@ng-bootstrap/ng-bootstrap';
import {CommonModule, formatDate} from '@angular/common';
import {BadgeComponent} from '../../common/badge/badge.component';
import {TableComponent} from '../../common/table/table.component';
import {Router, RouterModule} from '@angular/router';
import {LocationModel} from '../../../../../lib/models/resources/location-model';
import {TableRowItem, TableRowItemDataType} from '../../common/table/table-row-item';
import {ObservationModel} from '../../../../../lib/models/resources/observation-model';
import {ChartConfiguration} from 'chart.js';
import fhirpath from 'fhirpath';
import {NgChartsModule} from 'ng2-charts';
@Component({
standalone: true,
imports: [NgbCollapseModule, CommonModule, BadgeComponent, TableComponent, RouterModule, NgChartsModule],
selector: 'fhir-observation',
templateUrl: './observation.component.html',
styleUrls: ['./observation.component.scss']
})
export class ObservationComponent implements OnInit {
@Input() displayModel: ObservationModel
@Input() showDetails: boolean = true
isCollapsed: boolean = false
tableData: TableRowItem[] = []
//observation chart data
chartHeight = 45
barChartData =[
{
label: "Reference",
data: [[55,102], [44,120]],
backgroundColor: "rgba(91, 71, 251,0.6)",
hoverBackgroundColor: "rgba(91, 71, 251,0.2)"
},{
label: "Current",
data: [[80,81], [130,131]],
borderColor: "rgba(0,0,0,1)",
backgroundColor: "rgba(0,0,0,1)",
hoverBackgroundColor: "rgba(0,0,0,1)",
minBarLength: 3,
barPercentage: 1,
tooltip: {
}
}
] as ChartConfiguration<'bar'>['data']['datasets']
barChartLabels = [] // ["2020", "2018"] //["1","2","3","4","5","6","7","8"]
barChartOptions = {
indexAxis: 'y',
legend:{
display: false,
},
autoPadding: true,
//add padding to fix tooltip cutoff
layout: {
padding: {
left: 0,
right: 4,
top: 0,
bottom: 10
}
},
scales: {
y: {
stacked: true,
ticks: {
beginAtZero: true,
fontSize: 10,
min: 0,
// max: 80
},
},
x: {
scaleLabel:{
display: false,
labelString: "xaxis",
padding: 4,
},
// stacked: true,
ticks: {
beginAtZero: true,
fontSize: 10,
min: 0,
// max: 80
},
},
}
} as ChartConfiguration<'bar'>['options']
barChartColors = [
{
backgroundColor: 'white'
}
];
constructor(public changeRef: ChangeDetectorRef, public router: Router) { }
ngOnInit(): void {
this.tableData.push( {
label: 'Issued on',
data: this.displayModel?.effective_date,
enabled: !!this.displayModel?.effective_date,
},
{
label: 'Subject',
data: this.displayModel?.subject,
data_type: TableRowItemDataType.Reference,
enabled: !!this.displayModel?.subject ,
},
{
label: 'Coding',
data: this.displayModel?.code,
data_type: TableRowItemDataType.Coding,
enabled: !!this.displayModel?.code,
},
{
label: 'Value',
data: [this.displayModel?.value_quantity_value,this.displayModel?.value_quantity_unit].join(" "),
enabled: !!this.displayModel?.value_quantity_value,
},
{
label: 'Reference',
data: [this.displayModel?.reference_range?.[0]?.low?.value,this.displayModel?.reference_range?.[0]?.high?.value].join(" "),
enabled: !!this.displayModel?.reference_range,
})
//populate chart data
this.barChartLabels.push(
formatDate(this.displayModel.effective_date, "mediumDate", "en-US", undefined)
)
this.barChartData[0].data = [[this.displayModel.reference_range?.[0]?.low?.value, this.displayModel.reference_range?.[0]?.high?.value]]
this.barChartData[1].data = [[this.displayModel.value_quantity_value as number, this.displayModel.value_quantity_value as number]]
let suggestedMax = (this.displayModel.value_quantity_value as number) * 1.1;
this.barChartOptions.scales['x']['suggestedMax'] = suggestedMax
console.log("Observation chart data: ", this.barChartData[0].data, this.barChartData[1].data)
}
markForCheck(){
this.changeRef.markForCheck()
}
}

View File

@ -0,0 +1,19 @@
<div class="card card-fhir-resource" >
<div class="card-header" (click)="isCollapsed = ! isCollapsed">
<div>
<h6 class="card-title">{{displayModel?.name}}</h6>
</div>
<!-- <div class="btn-group">-->
<!-- <button class="btn active">Day</button>-->
<!-- <button class="btn">Week</button>-->
<!-- <button class="btn">Month</button>-->
<!-- </div>-->
</div>
<div #collapse="ngbCollapse" [(ngbCollapse)]="isCollapsed" class="card-body">
<p class="az-content-text mg-b-20">A formally or informally recognized grouping of people or organizations formed for the purpose of achieving some form of collective action. Includes companies, institutions, corporations, departments, community groups, healthcare practice groups, payer/insurer, etc.</p>
<fhir-ui-table [displayModel]="displayModel" [tableData]="tableData"></fhir-ui-table>
</div>
<div *ngIf="showDetails" class="card-footer">
<a class="float-right" routerLink="/explore/{{displayModel?.source_id}}/resource/{{displayModel?.source_resource_id}}">details</a>
</div>
</div>

View File

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

View File

@ -0,0 +1,77 @@
import {ChangeDetectorRef, Component, Input, OnInit} from '@angular/core';
import {NgbCollapseModule} from '@ng-bootstrap/ng-bootstrap';
import {CommonModule} from '@angular/common';
import {BadgeComponent} from '../../common/badge/badge.component';
import {TableComponent} from '../../common/table/table.component';
import {Router, RouterModule} from '@angular/router';
import {LocationModel} from '../../../../../lib/models/resources/location-model';
import {TableRowItem, TableRowItemDataType} from '../../common/table/table-row-item';
import {OrganizationModel} from '../../../../../lib/models/resources/organization-model';
@Component({
standalone: true,
imports: [NgbCollapseModule, CommonModule, BadgeComponent, TableComponent, RouterModule],
selector: 'fhir-organization',
templateUrl: './organization.component.html',
styleUrls: ['./organization.component.scss']
})
export class OrganizationComponent implements OnInit {
@Input() displayModel: OrganizationModel
@Input() showDetails: boolean = true
isCollapsed: boolean = false
tableData: TableRowItem[] = []
constructor(public changeRef: ChangeDetectorRef, public router: Router) { }
ngOnInit(): void {
for(let idCoding of (this.displayModel?.identifier || [])){
this.tableData.push({
label: `Identifier (${idCoding.system})`,
data: idCoding.display || idCoding.value,
enabled: true,
})
}
for(let address of (this.displayModel?.addresses || [])){
let addressParts = []
if(address.line){
addressParts.push(address.line.join(' '))
}
if(address.city){
addressParts.push(address.city)
}
if(address.state){
addressParts.push(address.state)
}
if(address.postalCode){
addressParts.push(address.postalCode)
}
this.tableData.push({
label: 'Address',
data: addressParts.join(", "),
enabled: !!addressParts,
})
}
this.tableData.push( {
label: 'Contacts',
data: this.displayModel?.telecom,
data_type: TableRowItemDataType.CodingList,
enabled: !!this.displayModel?.telecom,
},
{
label: 'Type',
data: this.displayModel?.type_codings,
data_type: TableRowItemDataType.CodableConcept,
enabled: !!this.displayModel?.type_codings && this.displayModel.type_codings.length > 0,
})
}
markForCheck(){
this.changeRef.markForCheck()
}
}

View File

@ -2,7 +2,7 @@
<div class="container ht-100p pd-t-0-f">
<div class="d-sm-flex justify-content-center justify-content-sm-between py-2 w-100">
<span class="text-muted text-center text-sm-left d-block d-sm-inline-block">Copyright © Fasten 2022 | {{appVersion}}</span>
<span class="float-none float-sm-right d-block mt-1 mt-sm-0 text-center"><a href="https://www.fastenhealth.com/" externalLink>Fasten Health: Your Journey, Your Data, Your Control</a></span>
<span class="float-none float-sm-right d-block mt-1 mt-sm-0 text-center"><a href="https://www.fastenhealth.com/" externalLink>Fasten Health: Your Journey, Your Records, Your Control</a></span>
</div>
</div><!-- container -->
</div><!-- az-footer -->

View File

@ -23,7 +23,7 @@ export class ReportLabsObservationComponent implements OnInit {
// https://stackoverflow.com/questions/62711919/chart-js-horizontal-lines-per-bar
chartHeight = 45
chartHeight = 60
barChartData =[
// {
@ -127,7 +127,7 @@ export class ReportLabsObservationComponent implements OnInit {
ngOnInit(): void {
let currentValues = []
let currentValues: number[] = []
let referenceRanges = []
@ -144,6 +144,12 @@ export class ReportLabsObservationComponent implements OnInit {
)
//get current value
// let currentValue = fhirpath.evaluate(observation.resource_raw, "Observation.valueQuantity.value")[0]
// if(currentValue != null){
// currentValues.push([currentValue, currentValue])
// } else {
// currentValues.push([])
// }
currentValues.push(fhirpath.evaluate(observation.resource_raw, "Observation.valueQuantity.value")[0])
//set chart x-axis label
@ -158,6 +164,13 @@ export class ReportLabsObservationComponent implements OnInit {
//add low/high ref value blocks
// let referenceLow = fhirpath.evaluate(observation.resource_raw, "Observation.referenceRange.low.value")[0]
// let referenceHigh = fhirpath.evaluate(observation.resource_raw, "Observation.referenceRange.high.value")[0]
// if (referenceLow != null && referenceHigh != null){
// referenceRanges.push([referenceLow, referenceHigh])
// } else {
// referenceRanges.push([0,0])
// }
referenceRanges.push([
fhirpath.evaluate(observation.resource_raw, "Observation.referenceRange.low.value")[0],
fhirpath.evaluate(observation.resource_raw, "Observation.referenceRange.high.value")[0]
@ -169,6 +182,13 @@ export class ReportLabsObservationComponent implements OnInit {
// @ts-ignore
this.barChartData[0].data = referenceRanges
this.barChartData[1].data = currentValues.map(v => [v, v])
// this.barChartData[1].data = currentValues
let suggestedMax = Math.max(...currentValues) * 1.1;
this.barChartOptions.scales['x']['suggestedMax'] = suggestedMax
console.log(this.observationTitle, this.barChartData[0].data, this.barChartData[1].data)
if(currentValues.length > 1){
this.chartHeight = 30 * currentValues.length

View File

@ -0,0 +1,157 @@
<!-- begin timeline-time -->
<div class="timeline-time">
<span class="date">{{displayModel.period_start | amDateFormat:'YYYY' }}</span>
<span class="time">{{displayModel.period_start | amDateFormat:'MMM DD' }}</span>
</div>
<!-- end timeline-time -->
<!-- begin timeline-icon -->
<div class="timeline-icon">
<a href="javascript:;">&nbsp;</a>
</div>
<!-- end timeline-icon -->
<!-- begin timeline-body -->
<div class="timeline-body card">
<div class="timeline-header">
<span class="username">
<a style="color:black;font-size: 1.3125rem;font-weight: 500;" routerLink="/explore/{{displayModel?.source_id}}/resource/{{displayModel?.source_resource_id}}">
{{displayModel.sort_title}}
</a>
<small></small>
</span>
<span class="float-right text-muted">
<span class="badge badge-primary"><i class="fas fa-folder-plus"></i> | Primary</span>
</span>
<br/>
<span>
<small class="cursor-pointer text-muted pd-r-15" placement="bottom-left" popoverClass="card-fhir-resource-popover" [ngbPopover]="practitionerPopoverContent" *ngFor="let practitioner of displayModel.related_resources['Practitioner']">
<i class="fas fa-user-md"></i> {{practitioner.sort_title}}
<ng-template #practitionerPopoverContent>
<fhir-resource [displayModel]="practitioner"></fhir-resource>
</ng-template>
</small>
<small class="cursor-pointer text-muted pd-r-15" placement="bottom-left" popoverClass="card-fhir-resource-popover" [ngbPopover]="organizationPopoverContent" *ngFor="let organization of displayModel.related_resources['Organization']">
<i class="fas fa-hospital"></i> {{organization.sort_title}}
<ng-template #organizationPopoverContent>
<fhir-resource [displayModel]="organization"></fhir-resource>
</ng-template>
</small>
<small class="cursor-pointer text-muted pd-r-15" placement="bottom-left" popoverClass="card-fhir-resource-popover" [ngbPopover]="locationPopoverContent" *ngFor="let location of displayModel.related_resources['Location']">
<i class="fas fa-map-marker-alt"></i> {{location.sort_title}}
<ng-template #locationPopoverContent>
<fhir-resource [displayModel]="location"></fhir-resource>
</ng-template>
</small>
</span>
</div>
<div class="timeline-content">
<div *ngIf="displayModel?.related_resources?.MedicationRequest || displayModel?.related_resources?.Medication">
<strong>Medications:</strong>
<ul>
<li class="cursor-pointer" [ngbPopover]="medicationRequestPopoverContent" placement="top-left" popoverClass="card-fhir-resource-popover" *ngFor="let medication of displayModel?.related_resources?.MedicationRequest">
{{medication.display }}
<ng-template #medicationRequestPopoverContent>
<fhir-resource [displayModel]="medication"></fhir-resource>
</ng-template>
</li>
<li class="cursor-pointer" [ngbPopover]="medicationPopoverContent" placement="top-left" popoverClass="card-fhir-resource-popover" *ngFor="let medication of displayModel?.related_resources?.Medication">
{{medication.title }}
<ng-template #medicationPopoverContent>
<fhir-resource [displayModel]="medication"></fhir-resource>
</ng-template>
</li>
</ul>
</div>
<div *ngIf="displayModel?.related_resources?.Procedure as procedures">
<strong>Procedures:</strong>
<ul>
<li class="cursor-pointer" [ngbPopover]="procedurePopoverContent" placement="top-left" popoverClass="card-fhir-resource-popover" *ngFor="let procedure of procedures">
{{procedure.display}}
<ng-template #procedurePopoverContent>
<fhir-resource [displayModel]="procedure"></fhir-resource>
</ng-template>
</li>
</ul>
</div>
<div *ngIf="displayModel?.related_resources?.Immunization as immunizations">
<strong>Immunizations:</strong>
<ul>
<li class="cursor-pointer" [ngbPopover]="immunizationPopoverContent" placement="top-left" popoverClass="card-fhir-resource-popover" *ngFor="let immunization of immunizations">
{{immunization.sort_title}}
<ng-template #immunizationPopoverContent>
<fhir-resource [displayModel]="immunization"></fhir-resource>
</ng-template>
</li>
</ul>
</div>
<div *ngIf="displayModel?.related_resources?.Device as devices">
<strong>Device:</strong>
<ul>
<li routerLink="/explore/{{device?.source_id}}/resource/{{device?.source_resource_id}}" *ngFor="let device of devices" role="link">
{{device.model}}
</li>
</ul>
</div>
</div>
<div class="timeline-footer">
<div *ngIf="displayModel?.related_resources?.['DocumentReference']?.length > 0 || displayModel?.related_resources?.['DiagnosticReport']?.length > 0" ngbDropdown class="d-inline-block dropdown ml-3">
<button type="button" class="btn btn-outline-indigo" id="dropdownReports" ngbDropdownToggle>
Clinical Reports
</button>
<div ngbDropdownMenu aria-labelledby="dropdownReports">
<a class="dropdown-item"
*ngFor="let documentReference of displayModel.related_resources['DocumentReference']" ngbDropdownItem
routerLink="/explore/{{documentReference?.source_id}}/resource/{{documentReference?.source_resource_id}}"
>{{documentReference?.sort_title}}</a>
<a class="dropdown-item"
*ngFor="let diagnosticReport of displayModel.related_resources['DiagnosticReport']" ngbDropdownItem
[routerLink]="diagnosticReportLink(diagnosticReport)"
>Lab Report - {{diagnosticReport?.sort_title}}</a>
</div>
</div>
<div ngbDropdown class="d-inline-block dropdown ml-3 float-right">
<button type="button" class="btn text-muted" id="dropdownAll" ngbDropdownToggle>
View Related
</button>
<div ngbDropdownMenu aria-labelledby="dropdownReports">
<ng-container *ngFor="let resourceEntry of displayModel?.related_resources | keyvalue">
<a class="dropdown-item"
*ngFor="let resourceListItem of resourceEntry.value" ngbDropdownItem
routerLink="/explore/{{resourceListItem?.source_id}}/resource/{{resourceListItem?.source_resource_id}}"
>{{resourceListItem.source_resource_type}} {{resourceListItem.sort_title ? '- '+resourceListItem.sort_title : '' }}</a>
</ng-container>
<ng-container *ngIf="!displayModel?.related_resources?.Condition">
<a class="dropdown-item" ngbDropdownItem [disabled]="true">-----</a>
<a class="dropdown-item" ngbDropdownItem>
<i class="fas fa-folder-plus"></i> Add Condition
</a>
</ng-container>
</div>
</div>
</div>
<div class="timeline-comment-box">
<div class="input">
<form action="">
<div ngbTooltip="not yet implemented" class="input-group">
<span class="input-group-btn p-l-10">
<button class="btn f-s-12 text-muted rounded-corner" type="button"><i class="cursor-pointer fas fa-lg fa-paperclip"></i></button>
</span>
<input type="text" class="form-control rounded-corner" placeholder="Write a private note...">
</div>
</form>
</div>
</div>
</div>
<!-- end timeline-body -->

View File

@ -0,0 +1,205 @@
.timeline-time {
position: absolute;
left: 0;
width: 18%;
text-align: right;
top: 30px
}
.timeline-time .date,
.timeline-time .time {
display: block;
font-weight: 600
}
.timeline-time .date {
line-height: 16px;
font-size: 12px
}
.timeline-time .time {
line-height: 24px;
font-size: 20px;
color: #242a30
}
.timeline-icon {
left: 15%;
position: absolute;
width: 10%;
text-align: center;
top: 40px
}
.timeline-icon a {
text-decoration: none;
width: 20px;
height: 20px;
display: inline-block;
border-radius: 20px;
background: #d9e0e7;
line-height: 10px;
color: #fff;
font-size: 14px;
border: 5px solid #2d353c;
transition: border-color .2s linear
}
.timeline-body {
margin-left: 23%;
background: #fff;
position: relative;
padding: 20px 25px;
border-radius: 6px
}
.timeline-body:before {
content: '';
display: block;
position: absolute;
border: 10px solid transparent;
border-right-color: #fff;
left: -20px;
top: 20px
}
.timeline-body>div+div {
margin-top: 15px
}
.timeline-body>div+div:last-child {
margin-bottom: -20px;
padding-bottom: 20px;
border-radius: 0 0 6px 6px
}
.timeline-header {
padding-bottom: 10px;
border-bottom: 1px solid #e2e7eb;
line-height: 30px
}
.timeline-header .userimage {
float: left;
width: 34px;
height: 34px;
border-radius: 40px;
overflow: hidden;
margin: -2px 10px -2px 0
}
.timeline-header .username {
font-size: 16px;
font-weight: 600
}
.timeline-header .username,
.timeline-header .username a {
color: #2d353c
}
.timeline-body img {
max-width: 100%;
display: block
}
.timeline-content {
letter-spacing: .25px;
line-height: 18px;
font-size: 13px;
width: 100%;
}
.timeline-content:after,
.timeline-content:before {
content: '';
display: table;
clear: both
}
.timeline-title {
margin-top: 0
}
.timeline-footer {
background: #fff;
border-top: 1px solid #e2e7ec;
padding-top: 15px
}
.timeline-footer a:not(.btn) {
color: #575d63
}
.timeline-footer a:not(.btn):focus,
.timeline-footer a:not(.btn):hover {
color: #2d353c
}
.timeline-likes {
color: #6d767f;
font-weight: 600;
font-size: 12px
}
.timeline-likes .stats-right {
float: right
}
.timeline-likes .stats-total {
display: inline-block;
line-height: 20px
}
.timeline-likes .stats-icon {
float: left;
margin-right: 5px;
font-size: 9px
}
.timeline-likes .stats-icon+.stats-icon {
margin-left: -2px
}
.timeline-likes .stats-text {
line-height: 20px
}
.timeline-likes .stats-text+.stats-text {
margin-left: 15px
}
.timeline-comment-box {
background: #f2f3f4;
margin-left: -25px;
margin-right: -25px;
padding: 20px 25px
}
.timeline-comment-box .user {
float: left;
width: 34px;
height: 34px;
overflow: hidden;
border-radius: 30px
}
.timeline-comment-box .user img {
max-width: 100%;
max-height: 100%
}
.timeline-comment-box .user+.input {
margin-left: 44px
}
.lead {
margin-bottom: 20px;
font-size: 21px;
font-weight: 300;
line-height: 1.4;
}
.text-danger, .text-red {
color: #ff5b57!important;
}

View File

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

View File

@ -0,0 +1,38 @@
import {Component, Input, OnInit} from '@angular/core';
import {ResourceFhir} from '../../models/fasten/resource_fhir';
import {EncounterModel} from '../../../lib/models/resources/encounter-model';
import {RecResourceRelatedDisplayModel} from '../../../lib/utils/resource_related_display_model';
import {LocationModel} from '../../../lib/models/resources/location-model';
import {PractitionerModel} from '../../../lib/models/resources/practitioner-model';
import {OrganizationModel} from '../../../lib/models/resources/organization-model';
import {fhirModelFactory} from '../../../lib/models/factory';
import {ResourceType} from '../../../lib/models/constants';
import {DiagnosticReportModel} from '../../../lib/models/resources/diagnostic-report-model';
import {FastenDisplayModel} from '../../../lib/models/fasten/fasten-display-model';
@Component({
selector: 'app-report-medical-history-timeline-panel',
templateUrl: './report-medical-history-timeline-panel.component.html',
styleUrls: ['./report-medical-history-timeline-panel.component.scss']
})
export class ReportMedicalHistoryTimelinePanelComponent implements OnInit {
@Input() resourceFhir: ResourceFhir
displayModel: EncounterModel
constructor() { }
ngOnInit(): void {
let parsed = RecResourceRelatedDisplayModel(this.resourceFhir)
this.displayModel = parsed.displayModel as EncounterModel
}
diagnosticReportLink(diagnosticReportRaw: FastenDisplayModel): string {
console.log('diagnosticReportRaw', diagnosticReportRaw)
let diagnosticReport = diagnosticReportRaw as DiagnosticReportModel
return diagnosticReport?.is_category_lab_report ?
'/labs/report/'+ diagnosticReport?.source_id + '/' + diagnosticReport?.source_resource_type + '/' + diagnosticReport?.source_resource_id :
'/explore/'+ diagnosticReport?.source_id + '/resource/' + diagnosticReport?.source_resource_id + '/'
}
}

View File

@ -82,6 +82,10 @@ import {NgbCollapseModule, NgbModule, NgbDropdownModule, NgbAccordionModule} fro
import {PipesModule} from '../pipes/pipes.module';
import {ResourceListOutletDirective} from './resource-list/resource-list-outlet.directive';
import {DirectivesModule} from '../directives/directives.module';
import { ReportMedicalHistoryTimelinePanelComponent } from './report-medical-history-timeline-panel/report-medical-history-timeline-panel.component';
import { OrganizationComponent } from './fhir/resources/organization/organization.component';
import { LocationComponent } from './fhir/resources/location/location.component';
import { ObservationComponent } from './fhir/resources/observation/observation.component';
@NgModule({
imports: [
@ -102,27 +106,30 @@ import {DirectivesModule} from '../directives/directives.module';
DirectivesModule,
//standalone components
LoadingSpinnerComponent,
GlossaryLookupComponent,
BadgeComponent,
TableComponent,
CodingComponent,
AllergyIntoleranceComponent,
MedicationComponent,
MedicationRequestComponent,
PractitionerComponent,
ProcedureComponent,
ImmunizationComponent,
BinaryTextComponent,
HtmlComponent,
ImgComponent,
PdfComponent,
MarkdownComponent,
DicomComponent,
BadgeComponent,
BinaryComponent,
BinaryTextComponent,
CodableConceptComponent,
CodingComponent,
DicomComponent,
GlossaryLookupComponent,
GridstackComponent,
GridstackItemComponent,
CodableConceptComponent
HtmlComponent,
ImgComponent,
ImmunizationComponent,
LoadingSpinnerComponent,
LocationComponent,
MarkdownComponent,
MedicationComponent,
MedicationRequestComponent,
OrganizationComponent,
ObservationComponent,
PdfComponent,
PractitionerComponent,
ProcedureComponent,
TableComponent,
],
declarations: [
@ -165,6 +172,7 @@ import {DirectivesModule} from '../directives/directives.module';
ReportMedicalHistoryEditorComponent,
ReportMedicalHistoryConditionComponent,
ReportLabsObservationComponent,
ReportMedicalHistoryTimelinePanelComponent,
FhirResourceComponent,
FhirResourceOutletDirective,
@ -235,23 +243,28 @@ import {DirectivesModule} from '../directives/directives.module';
UtilitiesSidebarComponent,
MedicalSourcesCardComponent,
MedicalSourcesConnectedComponent,
ReportMedicalHistoryTimelinePanelComponent,
//standalone components
BadgeComponent,
TableComponent,
CodingComponent,
LoadingSpinnerComponent,
GlossaryLookupComponent,
AllergyIntoleranceComponent,
MedicationComponent,
MedicationRequestComponent,
PractitionerComponent,
ProcedureComponent,
ImmunizationComponent,
BadgeComponent,
BinaryComponent,
CodableConceptComponent,
CodingComponent,
GlossaryLookupComponent,
GridstackComponent,
GridstackItemComponent,
ImmunizationComponent,
LoadingSpinnerComponent,
LocationComponent,
MedicalSourcesCategoryLookupPipe,
CodableConceptComponent,
MedicationComponent,
MedicationRequestComponent,
OrganizationComponent,
ObservationComponent,
PractitionerComponent,
ProcedureComponent,
TableComponent,
]
})

View File

@ -2,11 +2,4 @@ import {ResourceFhir} from './resource_fhir';
export class ResourceGraphResponse {
results: {[resource_type: string]: ResourceFhir[]}
metadata: ResourceGraphMetadata
}
export class ResourceGraphMetadata {
total_elements: number
page_size: number
page: number
}

View File

@ -5,7 +5,7 @@
<!-- Header Row -->
<report-header [reportHeaderTitle]="'Medical History'" [reportHeaderSubTitle]="'Organized by conditions, describes the scope and breadth of medical care'"></report-header>
<ng-container [ngTemplateOutlet]="loading ? isLoadingTemplate : (conditions.length == 0 && unassigned_encounters.length == 0 && explanationOfBenefits.length == 0) ? emptyReport : report"></ng-container>
<ng-container [ngTemplateOutlet]="loading ? isLoadingTemplate : (encounters.length == 0) ? emptyReport : report"></ng-container>
<ng-template #report>
<!-- Editor Button -->
@ -16,7 +16,7 @@
<strong>Warning!</strong> Fasten has detected medical Encounters that are not associated with a Condition.
They are grouped under the "Unassigned" section below.
<br/>
You can re-organize your conditions & encounters by using the <a class="alert-link cursor-pointer" (click)="openEditorRelated()">report editor</a>
You can re-organize your conditions & encounters by using the <a class="alert-link cursor-pointer">report editor</a>
</div>
</div>
</div>
@ -32,17 +32,23 @@
</div>
<!-- Condition List -->
<app-report-medical-history-condition *ngFor="let condition of conditions; let i = index" [conditionGroup]="condition"></app-report-medical-history-condition>
<app-report-medical-history-explanation-of-benefit *ngFor="let eob of explanationOfBenefits; let i = index" [explanationOfBenefitGroup]="eob"></app-report-medical-history-explanation-of-benefit>
<ul class="timeline">
<li *ngFor="let encounter of encounters">
<app-report-medical-history-timeline-panel [resourceFhir]="encounter" ></app-report-medical-history-timeline-panel>
</li>
</ul>
<!-- Pagination -->
<div class="row">
<div class="col-12 d-flex justify-content-center flex-nowrap">
<ngb-pagination
*ngIf="resourceGraphMetadata.total_elements > resourceGraphMetadata.page_size"
[(page)]="resourceGraphMetadata.page"
(pageChange)="pageChange($event)"
[pageSize]="resourceGraphMetadata.page_size"
[collectionSize]="resourceGraphMetadata.total_elements"></ngb-pagination>
[collectionSize]="allEncounterGroups.length"
[(page)]="currentPage"
[pageSize]="pageSize"
(pageChange)="pageChange()"
>
</ngb-pagination>
</div>
</div>

View File

@ -0,0 +1,23 @@
.timeline {
list-style-type: none;
margin: 0;
padding: 0;
position: relative
}
.timeline:before {
content: '';
position: absolute;
top: 5px;
bottom: 5px;
width: 5px;
background: #2d353c;
left: 20%;
margin-left: -2.5px
}
.timeline>li {
position: relative;
min-height: 50px;
padding: 20px 0
}

View File

@ -4,7 +4,7 @@ import {ResourceFhir} from '../../models/fasten/resource_fhir';
import { ModalDismissReasons, NgbModal } from '@ng-bootstrap/ng-bootstrap';
import {ReportMedicalHistoryEditorComponent} from '../../components/report-medical-history-editor/report-medical-history-editor.component';
import {forkJoin} from 'rxjs';
import {ResourceGraphMetadata, ResourceGraphResponse} from '../../models/fasten/resource-graph-response';
import {ResourceGraphResponse} from '../../models/fasten/resource-graph-response';
// import {ReportEditorRelatedComponent} from '../../components/report-editor-related/report-editor-related.component';
@Component({
@ -15,18 +15,19 @@ import {ResourceGraphMetadata, ResourceGraphResponse} from '../../models/fasten/
export class MedicalHistoryComponent implements OnInit {
loading: boolean = false
currentPage: number = 1 //1-based index due to the way the pagination component works
pageSize: number = 10
allEncounterGroups: string[] = []
closeResult = '';
conditions: ResourceFhir[] = []
explanationOfBenefits: ResourceFhir[] = []
// conditions: ResourceFhir[] = []
// explanationOfBenefits: ResourceFhir[] = []
//
// unassigned_encounters: ResourceFhir[] = []
// resourceLookup: {[name: string]: ResourceFhir} = {}
unassigned_encounters: ResourceFhir[] = []
resourceLookup: {[name: string]: ResourceFhir} = {}
encounters: ResourceFhir[] = []
resourceGraphMetadata: ResourceGraphMetadata = {
total_elements: 0,
page_size: 0,
page: 1
}
constructor(
private fastenApi: FastenApiService,
@ -36,78 +37,110 @@ export class MedicalHistoryComponent implements OnInit {
ngOnInit(): void {
//load the first page
this.loading = true
this.pageChange(1)
}
pageChange(page: number){
this.loading = true
this.fastenApi.getResourceGraph(null, page).subscribe((response: ResourceGraphResponse) => {
this.fastenApi.getResources('Encounter').subscribe(
(response: ResourceFhir[]) => {
let selectedResourceIds = response.map((resource: ResourceFhir): Partial<ResourceFhir> => {
return {
source_id: resource.source_id,
source_resource_type: resource.source_resource_type,
source_resource_id: resource.source_resource_id,
}
})
this.fastenApi.getResourceGraph(null, selectedResourceIds).subscribe((graphResponse: ResourceGraphResponse) => {
this.loading = false
this.resourceGraphMetadata = response.metadata
//page counter is 1 indexed but the backend is 0 indexed
this.resourceGraphMetadata.page = this.resourceGraphMetadata.page + 1
this.conditions = [].concat(response.results["Condition"] || [], response.results["Composition"] || [])
this.unassigned_encounters = response.results["Encounter"] || []
this.explanationOfBenefits = response.results["ExplanationOfBenefit"] || []
console.log("FLATTENED RESOURCES RESPONSE", graphResponse)
this.encounters = graphResponse.results["Encounter"] || []
//populate a lookup table with all resources
for(let condition of this.conditions){
this.recPopulateResourceLookup(condition)
}
if(this.unassigned_encounters.length > 0){
console.log("Found mapping:", this.resourceLookup)
console.log("Found unassigned encounters:", this.unassigned_encounters.length, this.unassigned_encounters)
this.conditions.push({
fhir_version: '',
resource_raw: {
resourceType: "Condition",
code:{
text: "UNASSIGNED",
}
},
source_id: 'UNASSIGNED',
source_resource_id: 'UNASSIGNED',
source_resource_type: 'Condition',
related_resources: this.unassigned_encounters
} as any)
}
}, error => {
error => {
this.loading = false
})
},
error => {
this.loading = false
}
)
// this.fastenApi.getResourceGraph(null, page).subscribe((response: ResourceGraphResponse) => {
// this.loading = false
// this.resourceGraphMetadata = response.metadata
// //page counter is 1 indexed but the backend is 0 indexed
// this.resourceGraphMetadata.page = this.resourceGraphMetadata.page + 1
//
// this.conditions = [].concat(response.results["Condition"] || [], response.results["Composition"] || [])
// this.unassigned_encounters = response.results["Encounter"] || []
// this.explanationOfBenefits = response.results["ExplanationOfBenefit"] || []
//
// //populate a lookup table with all resources
// for(let condition of this.conditions){
// this.recPopulateResourceLookup(condition)
// }
//
//
// if(this.unassigned_encounters.length > 0){
// console.log("Found mapping:", this.resourceLookup)
// console.log("Found unassigned encounters:", this.unassigned_encounters.length, this.unassigned_encounters)
// this.conditions.push({
// fhir_version: '',
// resource_raw: {
// resourceType: "Condition",
// code:{
// text: "UNASSIGNED",
// }
// },
// source_id: 'UNASSIGNED',
// source_resource_id: 'UNASSIGNED',
// source_resource_type: 'Condition',
// related_resources: this.unassigned_encounters
// } as any)
// }
//
//
// }, error => {
// this.loading = false
// })
}
openEditorRelated(): void {
const modalRef = this.modalService.open(ReportMedicalHistoryEditorComponent, {
size: 'xl',
});
modalRef.componentInstance.conditions = this.conditions;
}
//
// openEditorRelated(): void {
// const modalRef = this.modalService.open(ReportMedicalHistoryEditorComponent, {
// size: 'xl',
// });
// modalRef.componentInstance.conditions = this.conditions;
// }
recPopulateResourceLookup(resourceFhir: ResourceFhir) {
if(!resourceFhir){
return
}
this.resourceLookup[`${resourceFhir.source_id}/${resourceFhir.source_resource_type}/${resourceFhir.source_resource_id}`] = resourceFhir
if(!resourceFhir.related_resources){
return
} else {
for(let relatedResourceFhir of resourceFhir.related_resources){
this.recPopulateResourceLookup(relatedResourceFhir)
}
return
}
}
// recPopulateResourceLookup(resourceFhir: ResourceFhir) {
// if(!resourceFhir){
// return
// }
// this.resourceLookup[`${resourceFhir.source_id}/${resourceFhir.source_resource_type}/${resourceFhir.source_resource_id}`] = resourceFhir
//
// if(!resourceFhir.related_resources){
// return
// } else {
//
// for(let relatedResourceFhir of resourceFhir.related_resources){
// this.recPopulateResourceLookup(relatedResourceFhir)
// }
//
// return
// }
// }

View File

@ -190,17 +190,12 @@ export class FastenApiService {
return this._httpClient.post<any>(`${GetEndpointAbsolutePath(globalThis.location, environment.fasten_api_endpoint_base)}/secure/query`, query)
}
getResourceGraph(graphType?: string, page?:number): Observable<ResourceGraphResponse> {
getResourceGraph(graphType?: string, selectedResourceIds?: Partial<ResourceFhir>[]): Observable<ResourceGraphResponse> {
if(!graphType){
graphType = "MedicalHistory"
}
let queryParams = {}
if(page){
//the backend is 0 indexed, but the frontend is 1 indexed
queryParams["page"] = page - 1
}
return this._httpClient.get<any>(`${GetEndpointAbsolutePath(globalThis.location, environment.fasten_api_endpoint_base)}/secure/resource/graph/${graphType}`, {params: queryParams})
return this._httpClient.post<any>(`${GetEndpointAbsolutePath(globalThis.location, environment.fasten_api_endpoint_base)}/secure/resource/graph/${graphType}`, {resource_ids: selectedResourceIds})
.pipe(
map((response: ResponseWrapper) => {
console.log("RESPONSE", response)
@ -253,12 +248,11 @@ export class FastenApiService {
let resourceType = "Binary"
let resourceId = ""
let binaryUrl = attachmentModel.url
//strip out the urn prefix (if this is an embedded id, eg. urn:uuid:2a35e080-c5f7-4dde-b0cf-8210505708f1)
if (binaryUrl.startsWith(urnPrefix)) {
// PREFIX is exactly at the beginning
resourceId = binaryUrl.slice(urnPrefix.length);
} else if(binaryUrl.startsWith("http://") || binaryUrl.startsWith("https://")){
} else if(binaryUrl.startsWith("http://") || binaryUrl.startsWith("https://") || binaryUrl.startsWith("Binary/")){
//this is an absolute URL (which could be a FHIR url with Binary/xxx-xxx-xxx-xxx or a direct link to a file)
let urlParts = binaryUrl.split("Binary/");
if(urlParts.length > 1){
@ -269,7 +263,6 @@ export class FastenApiService {
resourceId = btoa(binaryUrl)
}
}
return this.getResourceBySourceId(sourceId, resourceId).pipe(
map((resourceFhir: ResourceFhir) => {
return new BinaryModel(resourceFhir.resource_raw)

View File

@ -13,7 +13,7 @@ export class DiagnosticReportModel extends FastenDisplayModel {
title: string | undefined
status: string | undefined
effective_datetime: string | undefined
category_coding: CodingModel[] | undefined
category_coding: CodableConceptModel[] | undefined
code_coding: CodingModel[] | undefined
has_category_coding: boolean | undefined
has_performer: boolean | undefined
@ -21,6 +21,8 @@ export class DiagnosticReportModel extends FastenDisplayModel {
performer: ReferenceModel | undefined
issued: string | undefined
presented_form: AttachmentModel[] | undefined
is_category_lab_report: boolean = false
constructor(fhirResource: any, fhirVersion?: fhirVersions, fastenOptions?: FastenOptions) {
super(fastenOptions)
@ -36,12 +38,16 @@ export class DiagnosticReportModel extends FastenDisplayModel {
_.get(fhirResource, 'code.coding.0.display', null);
this.status = _.get(fhirResource, 'status', '');
this.effective_datetime = _.get(fhirResource, 'effectiveDateTime');
this.category_coding = _.get(fhirResource, 'category.coding');
this.category_coding = _.get(fhirResource, 'category');
this.code_coding = _.get(fhirResource, 'code.coding');
this.has_category_coding = Array.isArray(this.category_coding);
this.conclusion = _.get(fhirResource, 'conclusion');
this.issued = _.get(fhirResource, 'issued');
this.is_category_lab_report = _.some(fhirResource.category, function(codableConceptModel: CodableConceptModel){
return _.some(codableConceptModel.coding, function(codingModel: CodingModel){
return codingModel.code === 'LAB' && codingModel.system === 'http://terminology.hl7.org/CodeSystem/v2-0074';
})
});
};
dstu2DTO(fhirResource:any){

View File

@ -23,6 +23,8 @@ export class EncounterModel extends FastenDisplayModel {
periodStart?:string
}[] | undefined
reasonCode: CodableConceptModel[] | undefined
constructor(fhirResource: any, fhirVersion?: fhirVersions, fastenOptions?: FastenOptions) {
super(fastenOptions)
this.source_resource_type = ResourceType.Encounter
@ -39,6 +41,7 @@ export class EncounterModel extends FastenDisplayModel {
);
this.encounter_type = _.get(fhirResource, 'type');
this.has_participant = _.has(fhirResource, 'participant');
this.reasonCode = _.get(fhirResource, 'reasonCode');
};
dstu2DTO(fhirResource:any){

View File

@ -12,14 +12,22 @@ export class ObservationModel extends FastenDisplayModel {
effective_date: string
code_coding_display: string
code_text: string
value_quantity_value: string
value_quantity_value: number | string
value_quantity_unit: string
status: string
value_codeable_concept_text: string
value_codeable_concept_coding_display: string
value_codeable_concept_coding: string
value_quantity_value_number: string
subject: string
value_quantity_value_number: number
subject: ReferenceModel | undefined
reference_range: {
low: {
value: number
}
high: {
value: number
}
}[] | undefined
constructor(fhirResource: any, fhirVersion?: fhirVersions, fastenOptions?: FastenOptions) {
super(fastenOptions)
@ -46,8 +54,7 @@ export class ObservationModel extends FastenDisplayModel {
[],
);
this.value_quantity_value_number = this.value_quantity_value;
this.reference_range = _.get(fhirResource, 'referenceRange', []);
this.subject = _.get(fhirResource, 'subject');
}
}

View File

@ -5,13 +5,14 @@ import {ReferenceModel} from '../datatypes/reference-model';
import {CodingModel} from '../datatypes/coding-model';
import {FastenDisplayModel} from '../fasten/fasten-display-model';
import {FastenOptions} from '../fasten/fasten-options';
import {AddressModel} from '../datatypes/address-model';
export class OrganizationModel extends FastenDisplayModel {
identifier: string|undefined
identifier: CodingModel[]|undefined
name: string|undefined
addresses: string|undefined
telecom: string|undefined
addresses: AddressModel[]|undefined
telecom: { system?: string, value?: string, use?: string }[]|undefined
type_codings: any[]|undefined
constructor(fhirResource: any, fhirVersion?: fhirVersions, fastenOptions?: FastenOptions) {