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) // Retrieve a list of all fhir resources (vertex), and a list of all associations (edge)
// Generate a graph // Generate a graph
// return list of root nodes, and their flattened related resources. // 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) currentUser, currentUserErr := gr.GetCurrentUser(ctx)
if currentUserErr != nil { if currentUserErr != nil {
return nil, nil, currentUserErr return nil, currentUserErr
}
//initialize the graph results metadata
resourceGraphMetadata := models.ResourceGraphMetadata{
TotalElements: 0,
PageSize: 20, //TODO: replace this with pkg.DefaultPageSize
Page: options.Page,
} }
// Get list of all (non-reciprocal) relationships // Get list of all (non-reciprocal) relationships
@ -52,18 +45,13 @@ func (gr *GormRepository) GetFlattenedResourceGraph(ctx context.Context, graphTy
}). }).
Find(&relatedResourceRelationships) Find(&relatedResourceRelationships)
if result.Error != nil { if result.Error != nil {
return nil, nil, result.Error return nil, result.Error
} }
log.Printf("found %d related resources", len(relatedResourceRelationships))
//Generate Graph //Generate Graph
// TODO optimization: eventually cache the graph in a database/storage, and update when new resources are added. // 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()) g := graph.New(resourceVertexId, graph.Directed(), graph.Rooted())
//// Get list of all resources TODO - REPLACED THIS
//wrappedResourceModels, err := gr.ListResources(ctx, models.ListResourceQueryOptions{})
//if err != nil {
// return nil, err
//}
//add vertices to the graph (must be done first) //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. //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, &resourcePlaceholder,
) )
if err != nil { 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() adjacencyMap, err := g.AdjacencyMap()
if err != nil { 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 // 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. // ie. "empty" verticies in this map are "root" nodes.
predecessorMap, err := g.PredecessorMap() predecessorMap, err := g.PredecessorMap()
if err != nil { 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. // 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{} resourcePlaceholderListDictionary := map[string][]*VertexResourcePlaceholder{}
sources, _, sourceFlattenLevel := getSourcesAndSinksForGraphType(graphType) 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 // 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. // 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 { 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 // 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) { flattenRelatedResourcesFn := func(resource *models.ResourceBase) {
@ -220,22 +207,30 @@ func (gr *GormRepository) GetFlattenedResourceGraph(ctx context.Context, graphTy
resource.RelatedResource = []*models.ResourceBase{} 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 //get all the resource placeholders associated with this node
//TODO: handle error? //TODO: handle error?
graph.DFS(g, vertexId, func(relatedVertexId string) bool { graph.DFS(g, vertexId, func(relatedVertexId string) bool {
relatedResourcePlaceholder, _ := g.Vertex(relatedVertexId) relatedResourcePlaceholder, _ := g.Vertex(relatedVertexId)
//skip the current resourcePlaceholder if it's referenced in this list. //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) //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) relatedResource, err := gr.GetResourceByResourceTypeAndId(ctx, relatedResourcePlaceholder.ResourceType, relatedResourcePlaceholder.ResourceID)
if err != nil { if err != nil {
gr.Logger.Warnf("ignoring, cannot safely handle error which occurred while getting related resource: %v", err) gr.Logger.Warnf("ignoring, cannot safely handle error which occurred while getting related resource (%s/%s): %v", relatedResourcePlaceholder.ResourceType, relatedResourcePlaceholder.ResourceID, err)
return true return false
} }
resource.RelatedResource = append( resource.RelatedResource = append(
resource.RelatedResource, resource.RelatedResource,
relatedResource, relatedResource,
) )
visited[resourceVertexId(relatedResourcePlaceholder)] = true
} }
return false return false
}) })
@ -288,36 +283,50 @@ func (gr *GormRepository) GetFlattenedResourceGraph(ctx context.Context, graphTy
// Step 5: return the populated resource list dictionary // 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) // - first, it will load all the "source" resources (eg. Encounter, Condition, etc)
// - sort the root resources by date, desc // - sort the root resources by date, desc
// - use the page number + page size to determine which root resources to return // - use the page number + page size to determine which root resources to return
// - return a dictionary of "source" resource lists // - return a dictionary of "source" resource lists
func (gr *GormRepository) InflateResourceGraphAtPage(resourcePlaceholderListDictionary map[string][]*VertexResourcePlaceholder, page int) (map[string][]*models.ResourceBase, int, error) { func (gr *GormRepository) InflateSelectedResourcesInResourceGraph(currentUser *models.User, resourcePlaceholderListDictionary map[string][]*VertexResourcePlaceholder, options models.ResourceGraphOptions) (map[string][]*models.ResourceBase, 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. // 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? //TODO: maybe its more performant to query each resource by type/id/source, since they are indexed already?
rootWrappedResourceModels := []models.ResourceBase{} rootWrappedResourceModels := []models.ResourceBase{}
for resourceType, _ := range resourcePlaceholderListDictionary { for resourceType, _ := range selectedResourceIdsByResourceType {
// resourcePlaceholderListDictionary contains top level resource types (eg. Encounter, Condition, etc) // 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{}{} selectList := [][]interface{}{}
for ndx, _ := range resourcePlaceholderListDictionary[resourceType] { for ndx, _ := range selectedResourceIdsByResourceType[resourceType] {
selectedResource := selectedResourceIdsByResourceType[resourceType][ndx]
selectList = append(selectList, []interface{}{ selectList = append(selectList, []interface{}{
resourcePlaceholderListDictionary[resourceType][ndx].UserID, currentUser.ID,
resourcePlaceholderListDictionary[resourceType][ndx].SourceID, selectedResource.SourceID,
resourcePlaceholderListDictionary[resourceType][ndx].ResourceType, selectedResource.SourceResourceType,
resourcePlaceholderListDictionary[resourceType][ndx].ResourceID, selectedResource.SourceResourceID,
}) })
} }
tableName, err := databaseModel.GetTableNameByResourceType(resourceType) tableName, err := databaseModel.GetTableNameByResourceType(resourceType)
if err != nil { if err != nil {
return nil, totalElements, err return nil, err
} }
var tableWrappedResourceModels []models.ResourceBase var tableWrappedResourceModels []models.ResourceBase
gr.GormClient. gr.GormClient.
@ -332,13 +341,7 @@ func (gr *GormRepository) InflateResourceGraphAtPage(resourcePlaceholderListDict
//sort //sort
rootWrappedResourceModels = utils.SortResourceListByDate(rootWrappedResourceModels) rootWrappedResourceModels = utils.SortResourceListByDate(rootWrappedResourceModels)
//calculate total elements // Step 3c: now that we have the selected root resources, lets generate a dictionary of resource lists, keyed by resource type
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
resourceListDictionary := map[string][]*models.ResourceBase{} resourceListDictionary := map[string][]*models.ResourceBase{}
for ndx, _ := range rootWrappedResourceModels { for ndx, _ := range rootWrappedResourceModels {
resourceType := rootWrappedResourceModels[ndx].SourceResourceType resourceType := rootWrappedResourceModels[ndx].SourceResourceType
@ -349,7 +352,7 @@ func (gr *GormRepository) InflateResourceGraphAtPage(resourcePlaceholderListDict
} }
// Step 4: return the populated resource list dictionary // Step 4: return the populated resource list dictionary
return resourceListDictionary, totalElements, nil return resourceListDictionary, nil
} }
// We need to support the following types of graphs: // We need to support the following types of graphs:
@ -445,11 +448,10 @@ func getSourcesAndSinksForGraphType(graphType pkg.ResourceGraphType) ([][]string
switch graphType { switch graphType {
case pkg.ResourceGraphTypeMedicalHistory: case pkg.ResourceGraphTypeMedicalHistory:
sources = [][]string{ sources = [][]string{
{"condition", "composition"},
{"encounter", "explanationofbenefit"}, {"encounter", "explanationofbenefit"},
} }
sinks = [][]string{ 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"}, {"binary"},
} }
sourceFlattenRelated = map[string]bool{ 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 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 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) 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 AddResourceComposition(ctx context.Context, compositionTitle string, resources []*models.ResourceBase) error
//UpsertProfile(context.Context, *models.Profile) error //UpsertProfile(context.Context, *models.Profile) error
//UpsertOrganziation(context.Context, *models.Organization) 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 package models
type ResourceGraphOptions struct { 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}) 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) { func GetResourceFhir(c *gin.Context) {
logger := c.MustGet(pkg.ContextKeyTypeLogger).(*logrus.Entry) logger := c.MustGet(pkg.ContextKeyTypeLogger).(*logrus.Entry)
databaseRepo := c.MustGet(pkg.ContextKeyTypeDatabase).(database.DatabaseRepository) databaseRepo := c.MustGet(pkg.ContextKeyTypeDatabase).(database.DatabaseRepository)
@ -129,18 +129,20 @@ func GetResourceFhirGraph(c *gin.Context) {
graphType := strings.Trim(c.Param("graphType"), "/") graphType := strings.Trim(c.Param("graphType"), "/")
graphOptions := models.ResourceGraphOptions{} var graphOptions models.ResourceGraphOptions
if len(c.Query("page")) > 0 { if err := c.ShouldBindJSON(&graphOptions); err != nil {
pageNumb, err := strconv.Atoi(c.Query("page")) logger.Errorln("An error occurred while parsing resource graph query options", err)
if err != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false})
logger.Errorln("An error occurred while calculating page number", err) return
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
return
}
graphOptions.Page = pageNumb
} }
resourceListDictionary, resourceListMetadata, err := databaseRepo.GetFlattenedResourceGraph(c, pkg.ResourceGraphType(graphType), graphOptions) if len(graphOptions.ResourcesIds) == 0 {
logger.Errorln("No resource ids specified")
c.JSON(http.StatusBadRequest, gin.H{"success": false})
return
}
resourceListDictionary, err := databaseRepo.GetFlattenedResourceGraph(c, pkg.ResourceGraphType(graphType), graphOptions)
if err != nil { if err != nil {
logger.Errorln("An error occurred while retrieving list of resources", err) logger.Errorln("An error occurred while retrieving list of resources", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false}) c.JSON(http.StatusInternalServerError, gin.H{"success": false})
@ -148,7 +150,6 @@ func GetResourceFhirGraph(c *gin.Context) {
} }
c.JSON(http.StatusOK, gin.H{"success": true, "data": map[string]interface{}{ c.JSON(http.StatusOK, gin.H{"success": true, "data": map[string]interface{}{
"results": resourceListDictionary, "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.POST("/source/:sourceId/sync", handler.SourceSync)
secure.GET("/source/:sourceId/summary", handler.GetSourceSummary) secure.GET("/source/:sourceId/summary", handler.GetSourceSummary)
secure.GET("/resource/fhir", handler.ListResourceFhir) secure.GET("/resource/fhir", handler.ListResourceFhir)
secure.GET("/resource/graph/:graphType", handler.GetResourceFhirGraph) secure.POST("/resource/graph/:graphType", handler.GetResourceFhirGraph)
secure.GET("/resource/fhir/:sourceId/:resourceId", handler.GetResourceFhir) secure.GET("/resource/fhir/:sourceId/:resourceId", handler.GetResourceFhir)
secure.POST("/resource/composition", handler.CreateResourceComposition) 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 {PractitionerComponent} from '../resources/practitioner/practitioner.component';
import {DocumentReferenceComponent} from '../resources/document-reference/document-reference.component'; import {DocumentReferenceComponent} from '../resources/document-reference/document-reference.component';
import {MediaComponent} from '../resources/media/media.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({ @Component({
selector: 'fhir-resource', selector: 'fhir-resource',
@ -119,6 +123,9 @@ export class FhirResourceComponent implements OnInit, OnChanges {
case "Immunization": { case "Immunization": {
return ImmunizationComponent; return ImmunizationComponent;
} }
case "Location": {
return LocationComponent;
}
case "Media": { case "Media": {
return MediaComponent; return MediaComponent;
} }
@ -137,9 +144,12 @@ export class FhirResourceComponent implements OnInit, OnChanges {
// case "NutritionOrder": { // case "NutritionOrder": {
// return ListNutritionOrderComponent; // return ListNutritionOrderComponent;
// } // }
// case "Observation": { case "Observation": {
// return ListObservationComponent; return ObservationComponent;
// } }
case "Organization": {
return OrganizationComponent;
}
case "Procedure": { case "Procedure": {
return ProcedureComponent; return ProcedureComponent;
} }

View File

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

View File

@ -32,12 +32,12 @@ export class DiagnosticReportComponent implements OnInit, FhirResourceComponentI
data: this.displayModel?.issued, data: this.displayModel?.issued,
enabled: !!this.displayModel?.issued, enabled: !!this.displayModel?.issued,
}, },
{ // {
label: 'Category', // label: 'Category',
data: this.displayModel?.category_coding, // data: this.displayModel?.category_coding,
data_type: TableRowItemDataType.CodingList, // data_type: TableRowItemDataType.CodingList,
enabled: this.displayModel?.has_category_coding, // enabled: this.displayModel?.has_category_coding,
}, // },
{ {
label: 'Performer', label: 'Performer',
data: this.displayModel?.performer, data: this.displayModel?.performer,
@ -50,6 +50,15 @@ export class DiagnosticReportComponent implements OnInit, FhirResourceComponentI
enabled: !!this.displayModel?.conclusion, 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(){ markForCheck(){
this.changeRef.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="container ht-100p pd-t-0-f">
<div class="d-sm-flex justify-content-center justify-content-sm-between py-2 w-100"> <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="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>
</div><!-- container --> </div><!-- container -->
</div><!-- az-footer --> </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 // https://stackoverflow.com/questions/62711919/chart-js-horizontal-lines-per-bar
chartHeight = 45 chartHeight = 60
barChartData =[ barChartData =[
// { // {
@ -127,7 +127,7 @@ export class ReportLabsObservationComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
let currentValues = [] let currentValues: number[] = []
let referenceRanges = [] let referenceRanges = []
@ -144,6 +144,12 @@ export class ReportLabsObservationComponent implements OnInit {
) )
//get current value //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]) currentValues.push(fhirpath.evaluate(observation.resource_raw, "Observation.valueQuantity.value")[0])
//set chart x-axis label //set chart x-axis label
@ -158,6 +164,13 @@ export class ReportLabsObservationComponent implements OnInit {
//add low/high ref value blocks //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([ referenceRanges.push([
fhirpath.evaluate(observation.resource_raw, "Observation.referenceRange.low.value")[0], fhirpath.evaluate(observation.resource_raw, "Observation.referenceRange.low.value")[0],
fhirpath.evaluate(observation.resource_raw, "Observation.referenceRange.high.value")[0] fhirpath.evaluate(observation.resource_raw, "Observation.referenceRange.high.value")[0]
@ -169,6 +182,13 @@ export class ReportLabsObservationComponent implements OnInit {
// @ts-ignore // @ts-ignore
this.barChartData[0].data = referenceRanges this.barChartData[0].data = referenceRanges
this.barChartData[1].data = currentValues.map(v => [v, v]) 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){ if(currentValues.length > 1){
this.chartHeight = 30 * currentValues.length this.chartHeight = 30 * currentValues.length

View File

@ -91,9 +91,6 @@
</ng-template> </ng-template>
</li> </li>
</ul> </ul>
</div> </div>

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 {PipesModule} from '../pipes/pipes.module';
import {ResourceListOutletDirective} from './resource-list/resource-list-outlet.directive'; import {ResourceListOutletDirective} from './resource-list/resource-list-outlet.directive';
import {DirectivesModule} from '../directives/directives.module'; 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({ @NgModule({
imports: [ imports: [
@ -102,27 +106,30 @@ import {DirectivesModule} from '../directives/directives.module';
DirectivesModule, DirectivesModule,
//standalone components //standalone components
LoadingSpinnerComponent,
GlossaryLookupComponent,
BadgeComponent,
TableComponent,
CodingComponent,
AllergyIntoleranceComponent, AllergyIntoleranceComponent,
MedicationComponent, BadgeComponent,
MedicationRequestComponent,
PractitionerComponent,
ProcedureComponent,
ImmunizationComponent,
BinaryTextComponent,
HtmlComponent,
ImgComponent,
PdfComponent,
MarkdownComponent,
DicomComponent,
BinaryComponent, BinaryComponent,
BinaryTextComponent,
CodableConceptComponent,
CodingComponent,
DicomComponent,
GlossaryLookupComponent,
GridstackComponent, GridstackComponent,
GridstackItemComponent, GridstackItemComponent,
CodableConceptComponent HtmlComponent,
ImgComponent,
ImmunizationComponent,
LoadingSpinnerComponent,
LocationComponent,
MarkdownComponent,
MedicationComponent,
MedicationRequestComponent,
OrganizationComponent,
ObservationComponent,
PdfComponent,
PractitionerComponent,
ProcedureComponent,
TableComponent,
], ],
declarations: [ declarations: [
@ -165,6 +172,7 @@ import {DirectivesModule} from '../directives/directives.module';
ReportMedicalHistoryEditorComponent, ReportMedicalHistoryEditorComponent,
ReportMedicalHistoryConditionComponent, ReportMedicalHistoryConditionComponent,
ReportLabsObservationComponent, ReportLabsObservationComponent,
ReportMedicalHistoryTimelinePanelComponent,
FhirResourceComponent, FhirResourceComponent,
FhirResourceOutletDirective, FhirResourceOutletDirective,
@ -235,23 +243,28 @@ import {DirectivesModule} from '../directives/directives.module';
UtilitiesSidebarComponent, UtilitiesSidebarComponent,
MedicalSourcesCardComponent, MedicalSourcesCardComponent,
MedicalSourcesConnectedComponent, MedicalSourcesConnectedComponent,
//standalone components ReportMedicalHistoryTimelinePanelComponent,
BadgeComponent,
TableComponent, //standalone components
CodingComponent,
LoadingSpinnerComponent,
GlossaryLookupComponent,
AllergyIntoleranceComponent, AllergyIntoleranceComponent,
MedicationComponent, BadgeComponent,
MedicationRequestComponent,
PractitionerComponent,
ProcedureComponent,
ImmunizationComponent,
BinaryComponent, BinaryComponent,
CodableConceptComponent,
CodingComponent,
GlossaryLookupComponent,
GridstackComponent, GridstackComponent,
GridstackItemComponent, GridstackItemComponent,
ImmunizationComponent,
LoadingSpinnerComponent,
LocationComponent,
MedicalSourcesCategoryLookupPipe, MedicalSourcesCategoryLookupPipe,
CodableConceptComponent, MedicationComponent,
MedicationRequestComponent,
OrganizationComponent,
ObservationComponent,
PractitionerComponent,
ProcedureComponent,
TableComponent,
] ]
}) })

View File

@ -2,11 +2,4 @@ import {ResourceFhir} from './resource_fhir';
export class ResourceGraphResponse { export class ResourceGraphResponse {
results: {[resource_type: string]: ResourceFhir[]} 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 --> <!-- Header Row -->
<report-header [reportHeaderTitle]="'Medical History'" [reportHeaderSubTitle]="'Organized by conditions, describes the scope and breadth of medical care'"></report-header> <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> <ng-template #report>
<!-- Editor Button --> <!-- Editor Button -->
@ -16,7 +16,7 @@
<strong>Warning!</strong> Fasten has detected medical Encounters that are not associated with a Condition. <strong>Warning!</strong> Fasten has detected medical Encounters that are not associated with a Condition.
They are grouped under the "Unassigned" section below. They are grouped under the "Unassigned" section below.
<br/> <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> </div>
</div> </div>
@ -32,17 +32,23 @@
</div> </div>
<!-- Condition List --> <!-- Condition List -->
<app-report-medical-history-condition *ngFor="let condition of conditions; let i = index" [conditionGroup]="condition"></app-report-medical-history-condition> <ul class="timeline">
<app-report-medical-history-explanation-of-benefit *ngFor="let eob of explanationOfBenefits; let i = index" [explanationOfBenefitGroup]="eob"></app-report-medical-history-explanation-of-benefit> <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="row">
<div class="col-12 d-flex justify-content-center flex-nowrap"> <div class="col-12 d-flex justify-content-center flex-nowrap">
<ngb-pagination <ngb-pagination
*ngIf="resourceGraphMetadata.total_elements > resourceGraphMetadata.page_size" [collectionSize]="allEncounterGroups.length"
[(page)]="resourceGraphMetadata.page" [(page)]="currentPage"
(pageChange)="pageChange($event)" [pageSize]="pageSize"
[pageSize]="resourceGraphMetadata.page_size" (pageChange)="pageChange()"
[collectionSize]="resourceGraphMetadata.total_elements"></ngb-pagination> >
</ngb-pagination>
</div> </div>
</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 { ModalDismissReasons, NgbModal } from '@ng-bootstrap/ng-bootstrap';
import {ReportMedicalHistoryEditorComponent} from '../../components/report-medical-history-editor/report-medical-history-editor.component'; import {ReportMedicalHistoryEditorComponent} from '../../components/report-medical-history-editor/report-medical-history-editor.component';
import {forkJoin} from 'rxjs'; 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'; // import {ReportEditorRelatedComponent} from '../../components/report-editor-related/report-editor-related.component';
@Component({ @Component({
@ -15,18 +15,19 @@ import {ResourceGraphMetadata, ResourceGraphResponse} from '../../models/fasten/
export class MedicalHistoryComponent implements OnInit { export class MedicalHistoryComponent implements OnInit {
loading: boolean = false loading: boolean = false
currentPage: number = 1 //1-based index due to the way the pagination component works
pageSize: number = 10
allEncounterGroups: string[] = []
closeResult = ''; closeResult = '';
conditions: ResourceFhir[] = [] // conditions: ResourceFhir[] = []
explanationOfBenefits: ResourceFhir[] = [] // explanationOfBenefits: ResourceFhir[] = []
//
// unassigned_encounters: ResourceFhir[] = []
// resourceLookup: {[name: string]: ResourceFhir} = {}
unassigned_encounters: ResourceFhir[] = [] encounters: ResourceFhir[] = []
resourceLookup: {[name: string]: ResourceFhir} = {}
resourceGraphMetadata: ResourceGraphMetadata = {
total_elements: 0,
page_size: 0,
page: 1
}
constructor( constructor(
private fastenApi: FastenApiService, private fastenApi: FastenApiService,
@ -36,78 +37,110 @@ export class MedicalHistoryComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
//load the first page //load the first page
this.loading = true
this.pageChange(1) this.pageChange(1)
} }
pageChange(page: number){ pageChange(page: number){
this.loading = true this.loading = true
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.fastenApi.getResources('Encounter').subscribe(
this.unassigned_encounters = response.results["Encounter"] || [] (response: ResourceFhir[]) => {
this.explanationOfBenefits = response.results["ExplanationOfBenefit"] || []
//populate a lookup table with all resources let selectedResourceIds = response.map((resource: ResourceFhir): Partial<ResourceFhir> => {
for(let condition of this.conditions){ return {
this.recPopulateResourceLookup(condition) 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
console.log("FLATTENED RESOURCES RESPONSE", graphResponse)
this.encounters = graphResponse.results["Encounter"] || []
},
error => {
this.loading = false
})
},
error => {
this.loading = false
} }
)
// this.fastenApi.getResourceGraph(null, page).subscribe((response: ResourceGraphResponse) => {
if(this.unassigned_encounters.length > 0){ // this.loading = false
console.log("Found mapping:", this.resourceLookup) // this.resourceGraphMetadata = response.metadata
console.log("Found unassigned encounters:", this.unassigned_encounters.length, this.unassigned_encounters) // //page counter is 1 indexed but the backend is 0 indexed
this.conditions.push({ // this.resourceGraphMetadata.page = this.resourceGraphMetadata.page + 1
fhir_version: '', //
resource_raw: { // this.conditions = [].concat(response.results["Condition"] || [], response.results["Composition"] || [])
resourceType: "Condition", // this.unassigned_encounters = response.results["Encounter"] || []
code:{ // this.explanationOfBenefits = response.results["ExplanationOfBenefit"] || []
text: "UNASSIGNED", //
} // //populate a lookup table with all resources
}, // for(let condition of this.conditions){
source_id: 'UNASSIGNED', // this.recPopulateResourceLookup(condition)
source_resource_id: 'UNASSIGNED', // }
source_resource_type: 'Condition', //
related_resources: this.unassigned_encounters //
} as any) // 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({
}, error => { // fhir_version: '',
this.loading = false // 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 { // openEditorRelated(): void {
const modalRef = this.modalService.open(ReportMedicalHistoryEditorComponent, { // const modalRef = this.modalService.open(ReportMedicalHistoryEditorComponent, {
size: 'xl', // size: 'xl',
}); // });
modalRef.componentInstance.conditions = this.conditions; // modalRef.componentInstance.conditions = this.conditions;
} // }
recPopulateResourceLookup(resourceFhir: ResourceFhir) { // recPopulateResourceLookup(resourceFhir: ResourceFhir) {
if(!resourceFhir){ // if(!resourceFhir){
return // return
} // }
this.resourceLookup[`${resourceFhir.source_id}/${resourceFhir.source_resource_type}/${resourceFhir.source_resource_id}`] = resourceFhir // this.resourceLookup[`${resourceFhir.source_id}/${resourceFhir.source_resource_type}/${resourceFhir.source_resource_id}`] = resourceFhir
//
if(!resourceFhir.related_resources){ // if(!resourceFhir.related_resources){
return // return
} else { // } else {
//
for(let relatedResourceFhir of resourceFhir.related_resources){ // for(let relatedResourceFhir of resourceFhir.related_resources){
this.recPopulateResourceLookup(relatedResourceFhir) // this.recPopulateResourceLookup(relatedResourceFhir)
} // }
//
return // 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) 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){ if(!graphType){
graphType = "MedicalHistory" 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( .pipe(
map((response: ResponseWrapper) => { map((response: ResponseWrapper) => {
console.log("RESPONSE", response) console.log("RESPONSE", response)
@ -253,12 +248,11 @@ export class FastenApiService {
let resourceType = "Binary" let resourceType = "Binary"
let resourceId = "" let resourceId = ""
let binaryUrl = attachmentModel.url let binaryUrl = attachmentModel.url
//strip out the urn prefix (if this is an embedded id, eg. urn:uuid:2a35e080-c5f7-4dde-b0cf-8210505708f1) //strip out the urn prefix (if this is an embedded id, eg. urn:uuid:2a35e080-c5f7-4dde-b0cf-8210505708f1)
if (binaryUrl.startsWith(urnPrefix)) { if (binaryUrl.startsWith(urnPrefix)) {
// PREFIX is exactly at the beginning // PREFIX is exactly at the beginning
resourceId = binaryUrl.slice(urnPrefix.length); 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) //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/"); let urlParts = binaryUrl.split("Binary/");
if(urlParts.length > 1){ if(urlParts.length > 1){
@ -269,7 +263,6 @@ export class FastenApiService {
resourceId = btoa(binaryUrl) resourceId = btoa(binaryUrl)
} }
} }
return this.getResourceBySourceId(sourceId, resourceId).pipe( return this.getResourceBySourceId(sourceId, resourceId).pipe(
map((resourceFhir: ResourceFhir) => { map((resourceFhir: ResourceFhir) => {
return new BinaryModel(resourceFhir.resource_raw) return new BinaryModel(resourceFhir.resource_raw)

View File

@ -13,7 +13,7 @@ export class DiagnosticReportModel extends FastenDisplayModel {
title: string | undefined title: string | undefined
status: string | undefined status: string | undefined
effective_datetime: string | undefined effective_datetime: string | undefined
category_coding: CodingModel[] | undefined category_coding: CodableConceptModel[] | undefined
code_coding: CodingModel[] | undefined code_coding: CodingModel[] | undefined
has_category_coding: boolean | undefined has_category_coding: boolean | undefined
has_performer: boolean | undefined has_performer: boolean | undefined
@ -21,6 +21,8 @@ export class DiagnosticReportModel extends FastenDisplayModel {
performer: ReferenceModel | undefined performer: ReferenceModel | undefined
issued: string | undefined issued: string | undefined
presented_form: AttachmentModel[] | undefined presented_form: AttachmentModel[] | undefined
is_category_lab_report: boolean = false
constructor(fhirResource: any, fhirVersion?: fhirVersions, fastenOptions?: FastenOptions) { constructor(fhirResource: any, fhirVersion?: fhirVersions, fastenOptions?: FastenOptions) {
super(fastenOptions) super(fastenOptions)
@ -36,12 +38,16 @@ export class DiagnosticReportModel extends FastenDisplayModel {
_.get(fhirResource, 'code.coding.0.display', null); _.get(fhirResource, 'code.coding.0.display', null);
this.status = _.get(fhirResource, 'status', ''); this.status = _.get(fhirResource, 'status', '');
this.effective_datetime = _.get(fhirResource, 'effectiveDateTime'); 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.code_coding = _.get(fhirResource, 'code.coding');
this.has_category_coding = Array.isArray(this.category_coding); this.has_category_coding = Array.isArray(this.category_coding);
this.conclusion = _.get(fhirResource, 'conclusion'); this.conclusion = _.get(fhirResource, 'conclusion');
this.issued = _.get(fhirResource, 'issued'); 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){ dstu2DTO(fhirResource:any){

View File

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

View File

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

View File

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