WIP adding medical history timeline view. (#325)
This commit is contained in:
parent
a161f41998
commit
2061684aed
|
@ -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{
|
||||
|
|
|
@ -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))
|
||||
|
||||
}
|
|
@ -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
|
@ -1,5 +1,5 @@
|
|||
package models
|
||||
|
||||
type ResourceGraphOptions struct {
|
||||
Page int
|
||||
ResourcesIds []OriginBase `json:"resource_ids"`
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}})
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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>
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
|
@ -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 -->
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -91,9 +91,6 @@
|
|||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
|
|
@ -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:;"> </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 -->
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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 + '/'
|
||||
}
|
||||
|
||||
}
|
|
@ -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,
|
||||
|
||||
]
|
||||
})
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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){
|
||||
|
|
|
@ -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){
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue