Timeline based Manual Entry form (#330)

This commit is contained in:
Jason Kulatunga 2023-12-03 18:40:16 -08:00 committed by GitHub
parent 7a77dcdcd6
commit d23af018e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
280 changed files with 5336 additions and 2828 deletions

View File

@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
sourcePkg "github.com/fastenhealth/fasten-sources/pkg"
"strings"
"time"
@ -39,6 +40,7 @@ func (gr *GormRepository) Close() error {
// User
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <editor-fold desc="User">
func (gr *GormRepository) CreateUser(ctx context.Context, user *models.User) error {
if err := user.HashPassword(user.Password); err != nil {
return err
@ -53,6 +55,17 @@ func (gr *GormRepository) CreateUser(ctx context.Context, user *models.User) err
if err != nil {
return err
}
//create Fasten source credential for this user.
fastenUserCred := models.SourceCredential{
UserID: user.ID,
SourceType: sourcePkg.SourceTypeFasten,
}
fastenUserCredResp := gr.GormClient.Create(&fastenUserCred)
if fastenUserCredResp.Error != nil {
return fastenUserCredResp.Error
}
return nil
}
func (gr *GormRepository) GetUserByUsername(ctx context.Context, username string) (*models.User, error) {
@ -94,10 +107,13 @@ func (gr *GormRepository) GetCurrentUser(ctx context.Context) (*models.User, err
return &currentUser, nil
}
//</editor-fold>
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Glossary
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <editor-fold desc="Glossary">
func (gr *GormRepository) CreateGlossaryEntry(ctx context.Context, glossaryEntry *models.Glossary) error {
record := gr.GormClient.WithContext(ctx).Create(glossaryEntry)
if record.Error != nil {
@ -114,6 +130,8 @@ func (gr *GormRepository) GetGlossaryEntry(ctx context.Context, code string, cod
return &foundGlossaryEntry, result.Error
}
//</editor-fold>
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Summary
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -180,6 +198,8 @@ func (gr *GormRepository) GetSummary(ctx context.Context) (*models.Summary, erro
// Resource
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <editor-fold desc="Resource">
// This function will create a new resource if it does not exist, or update an existing resource if it does exist.
// It will also create associations between fhir resources
// This function is called directly by fasten-sources
@ -210,30 +230,54 @@ func (gr *GormRepository) UpsertRawResource(ctx context.Context, sourceCredentia
//note: these associations are not reciprocal, (i.e. if Procedure references Location, Location may not reference Procedure)
if rawResource.ReferencedResources != nil && len(rawResource.ReferencedResources) > 0 {
for _, referencedResource := range rawResource.ReferencedResources {
parts := strings.Split(referencedResource, "/")
if len(parts) != 2 {
continue
}
relatedResource := &models.ResourceBase{
OriginBase: models.OriginBase{
SourceID: source.ID,
SourceResourceType: parts[0],
SourceResourceID: parts[1],
},
RelatedResource: nil,
}
err := gr.AddResourceAssociation(
ctx,
source,
wrappedResourceModel.SourceResourceType,
wrappedResourceModel.SourceResourceID,
source,
relatedResource.SourceResourceType,
relatedResource.SourceResourceID,
)
if err != nil {
return false, err
var relatedResource *models.ResourceBase
if strings.HasPrefix(referencedResource, sourcePkg.FASTENHEALTH_URN_PREFIX) {
gr.Logger.Infof("parsing external urn:fastenhealth-fhir reference: %v", referencedResource)
targetSourceId, targetResourceType, targetResourceId, err := sourcePkg.ParseReferenceUri(&referencedResource)
if err != nil {
gr.Logger.Warnf("could not parse urn:fastenhealth-fhir reference: %v", referencedResource)
continue
}
err = gr.UpsertRawResourceAssociation(
ctx,
source.ID.String(),
wrappedResourceModel.SourceResourceType,
wrappedResourceModel.SourceResourceID,
targetSourceId,
targetResourceType,
targetResourceId,
)
if err != nil {
return false, err
}
} else {
parts := strings.Split(referencedResource, "/")
if len(parts) != 2 {
continue
}
relatedResource = &models.ResourceBase{
OriginBase: models.OriginBase{
SourceID: source.ID,
SourceResourceType: parts[0],
SourceResourceID: parts[1],
},
RelatedResource: nil,
}
err := gr.AddResourceAssociation(
ctx,
source,
wrappedResourceModel.SourceResourceType,
wrappedResourceModel.SourceResourceID,
source,
relatedResource.SourceResourceType,
relatedResource.SourceResourceID,
)
if err != nil {
return false, err
}
}
}
}
@ -242,6 +286,44 @@ func (gr *GormRepository) UpsertRawResource(ctx context.Context, sourceCredentia
}
func (gr *GormRepository) UpsertRawResourceAssociation(
ctx context.Context,
sourceId string,
sourceResourceType string,
sourceResourceId string,
targetSourceId string,
targetResourceType string,
targetResourceId string,
) error {
if sourceId == targetSourceId && sourceResourceType == targetResourceType && sourceResourceId == targetResourceId {
gr.Logger.Warnf("cannot create self-referential association, ignoring")
return nil
}
var sourceCredential *models.SourceCredential
var targetSourceCredential *models.SourceCredential
var err error
if sourceId == targetSourceId {
sourceCredential, err = gr.GetSource(ctx, sourceId)
if err != nil {
return err
}
targetSourceCredential = sourceCredential
} else {
sourceCredential, err = gr.GetSource(ctx, sourceId)
if err != nil {
return err
}
targetSourceCredential, err = gr.GetSource(ctx, targetSourceId)
if err != nil {
return err
}
}
//SECURITY: sourceCredential and targetSourceCredential are guaranteed to be owned by the same user, and will be confirmed within the addAssociation function
return gr.AddResourceAssociation(ctx, sourceCredential, sourceResourceType, sourceResourceId, targetSourceCredential, targetResourceType, targetResourceId)
}
// UpsertResource
// this method will upsert a resource, however it will not create associations.
// UPSERT operation
@ -448,10 +530,14 @@ func (gr *GormRepository) GetPatientForSources(ctx context.Context) ([]models.Re
return wrappedResourceModels, results.Error
}
//</editor-fold>
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Resource Associations
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//<editor-fold desc="Resource Associations">
// verifyAssociationPermission ensure that the sources are "owned" by the same user, and that the user is the current user
func (gr *GormRepository) verifyAssociationPermission(ctx context.Context, sourceUserID uuid.UUID, relatedSourceUserID uuid.UUID) error {
currentUser, currentUserErr := gr.GetCurrentUser(ctx)
@ -542,11 +628,14 @@ func (gr *GormRepository) FindResourceAssociationsByTypeAndId(ctx context.Contex
ResourceBaseSourceID: source.ID,
ResourceBaseSourceResourceType: resourceType,
ResourceBaseSourceResourceID: resourceId,
RelatedResourceUserID: currentUser.ID,
}).
Find(&relatedResources)
return relatedResources, result.Error
}
//</editor-fold>
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Resource Composition (Grouping)
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -565,6 +654,8 @@ func (gr *GormRepository) FindResourceAssociationsByTypeAndId(ctx context.Contex
// - add AddResourceAssociation for all resources linked to the Composition resource
// - store the Composition resource
// TODO: determine if we should be using a List Resource instead of a Composition resource
//
// Deprecated: This method has been deprecated. It has been replaced in favor of Fasten SourceCredential & associations
func (gr *GormRepository) AddResourceComposition(ctx context.Context, compositionTitle string, resources []*models.ResourceBase) error {
currentUser, currentUserErr := gr.GetCurrentUser(ctx)
if currentUserErr != nil {
@ -718,6 +809,8 @@ func (gr *GormRepository) AddResourceComposition(ctx context.Context, compositio
// SourceCredential
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//<editor-fold desc="SourceCredential">
func (gr *GormRepository) CreateSource(ctx context.Context, sourceCreds *models.SourceCredential) error {
currentUser, currentUserErr := gr.GetCurrentUser(ctx)
if currentUserErr != nil {
@ -846,10 +939,10 @@ func (gr *GormRepository) GetSourceSummary(ctx context.Context, sourceId string)
Table(patientTableName).
First(&wrappedPatientResourceModel)
if patientResults.Error != nil {
return nil, patientResults.Error
//some sources may not have a patient resource (including the Fasten source)
if patientResults.Error == nil {
sourceSummary.Patient = &wrappedPatientResourceModel
}
sourceSummary.Patient = &wrappedPatientResourceModel
return sourceSummary, nil
}
@ -925,10 +1018,13 @@ func (gr *GormRepository) DeleteSource(ctx context.Context, sourceId string) (in
return rowsEffected, results.Error
}
//</editor-fold>
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Background Job
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <editor-fold desc="Background Job & Checkpoints">
func (gr *GormRepository) CreateBackgroundJob(ctx context.Context, backgroundJob *models.BackgroundJob) error {
currentUser, currentUserErr := gr.GetCurrentUser(ctx)
if currentUserErr != nil {
@ -1117,6 +1213,8 @@ func (gr *GormRepository) CancelAllLockedBackgroundJobsAndFail() error {
}
//</editor-fold>
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Utilities
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

View File

@ -1,8 +1,11 @@
package database
import (
"context"
"fmt"
"github.com/fastenhealth/fasten-onprem/backend/pkg/models"
databaseModel "github.com/fastenhealth/fasten-onprem/backend/pkg/models/database"
sourcePkg "github.com/fastenhealth/fasten-sources/pkg"
"github.com/go-gormigrate/gormigrate/v2"
"gorm.io/gorm"
)
@ -14,7 +17,7 @@ func (gr *GormRepository) Migrate() error {
gormMigrateOptions := gormigrate.DefaultOptions
gormMigrateOptions.UseTransaction = true
//use echo $(date '+%Y%m%d%H%M%S') to generate new ID's
//use "echo $(date '+%Y%m%d%H%M%S')" to generate new ID's
m := gormigrate.New(gr.GormClient, gormMigrateOptions, []*gormigrate.Migration{
{
ID: "20231017112246", // base database models //TODO: figure out how to version these correctly (SourceCredential is complicated)
@ -37,6 +40,31 @@ func (gr *GormRepository) Migrate() error {
return databaseModel.Migrate(tx)
},
},
{
ID: "20231201122541", // Adding Fasten Source Credential for each user
Migrate: func(tx *gorm.DB) error {
users := []models.User{}
results := tx.Find(&users)
if results.Error != nil {
return results.Error
}
for _, user := range users {
tx.Logger.Info(context.Background(), fmt.Sprintf("Creating Fasten Source Credential for user: %s", user.ID))
fastenUserCred := models.SourceCredential{
UserID: user.ID,
SourceType: sourcePkg.SourceTypeFasten,
}
fastenUserCredCreateResp := tx.Create(&fastenUserCred)
if fastenUserCredCreateResp.Error != nil {
tx.Logger.Error(context.Background(), fmt.Sprintf("An error occurred creating Fasten Source Credential for user: %s", user.ID))
return fastenUserCredCreateResp.Error
}
}
return nil
},
},
})
if err := m.Migrate(); err != nil {

View File

@ -1021,7 +1021,7 @@ func (suite *RepositoryTestSuite) TestGetSummary() {
{"count": int64(16), "resource_type": "Procedure"},
}, sourceSummary.ResourceTypeCounts)
require.Equal(suite.T(), 2, len(sourceSummary.Sources))
require.Equal(suite.T(), 3, len(sourceSummary.Sources))
require.Equal(suite.T(), 2, len(sourceSummary.Patients))
}

View File

@ -29,6 +29,8 @@ type DatabaseRepository interface {
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, error)
// Deprecated:This method has been deprecated. It has been replaced in favor of Fasten SourceCredential & associations
AddResourceComposition(ctx context.Context, compositionTitle string, resources []*models.ResourceBase) error
//UpsertProfile(context.Context, *models.Profile) error
//UpsertOrganziation(context.Context, *models.Organization) error
@ -55,6 +57,15 @@ type DatabaseRepository interface {
PopulateDefaultUserSettings(ctx context.Context, userId uuid.UUID) error
//used by fasten-sources Clients
UpsertRawResource(ctx context.Context, sourceCredentials sourcePkg.SourceCredential, rawResource sourcePkg.RawResourceFhir) (bool, error)
BackgroundJobCheckpoint(ctx context.Context, checkpointData map[string]interface{}, errorData map[string]interface{})
UpsertRawResource(ctx context.Context, sourceCredentials sourcePkg.SourceCredential, rawResource sourcePkg.RawResourceFhir) (bool, error)
UpsertRawResourceAssociation(
ctx context.Context,
sourceId string,
sourceResourceType string,
sourceResourceId string,
targetSourceId string,
targetResourceType string,
targetResourceId string,
) error
}

View File

@ -69,6 +69,9 @@ type SourceCredential struct {
func (s *SourceCredential) GetSourceType() sourcesPkg.SourceType {
return s.SourceType
}
func (s *SourceCredential) GetSourceId() string {
return s.ID.String()
}
func (s *SourceCredential) GetClientId() string {
return s.ClientId

View File

@ -17,14 +17,65 @@ import (
"time"
)
// BackgroundJobSyncResources is a background job that syncs all FHIR resource for a given source
// This function is used to sync resources from a source (via a callback function). The BackgroundJobSyncResourcesWrapper contains the logic for registering the background job tracking the sync.
func BackgroundJobSyncResources(
parentContext context.Context,
logger *logrus.Entry,
databaseRepo database.DatabaseRepository,
sourceCred *models.SourceCredential,
) (sourceModels.UpsertSummary, error) {
return BackgroundJobSyncResourcesWrapper(
parentContext,
logger,
databaseRepo,
sourceCred,
func(
_backgroundJobContext context.Context,
_logger *logrus.Entry,
_databaseRepo database.DatabaseRepository,
_sourceCred *models.SourceCredential,
) (sourceModels.SourceClient, sourceModels.UpsertSummary, error) {
// after creating the client, we should do a bulk import
sourceClient, err := factory.GetSourceClient(sourcePkg.GetFastenLighthouseEnv(), _sourceCred.SourceType, _backgroundJobContext, _logger, _sourceCred)
if err != nil {
resultErr := fmt.Errorf("an error occurred while initializing hub client using source credential: %w", err)
_logger.Errorln(resultErr)
return nil, sourceModels.UpsertSummary{}, resultErr
}
summary, err := sourceClient.SyncAll(_databaseRepo)
if err != nil {
resultErr := fmt.Errorf("an error occurred while bulk importing resources from source: %w", err)
_logger.Errorln(resultErr)
return sourceClient, summary, resultErr
}
return sourceClient, summary, nil
})
}
// BackgroundJobSyncResourcesWrapper is a background job that syncs all FHIR resource for a given source
// It is a blocking function that will return only when the sync is complete or has failed
// It will create a background job and associate it with the source
// It will also update the access token and refresh token if they have been updated
// It will return the sync summary and error if any
//
// It's a wrapper function that takes a callback function as an argument.
// The callback function is the actual sync operation that will be run in the background (regular source or manual source)
//
// TODO: run in background thread, or use https://gobyexample.com/tickers
// TODO: use goroutine to truely run in the background (how will that work with DatabaseRepository, is that thread safe?) Mutex needed?
func BackgroundJobSyncResources(parentContext context.Context, logger *logrus.Entry, databaseRepo database.DatabaseRepository, sourceCred *models.SourceCredential) (sourceModels.UpsertSummary, error) {
func BackgroundJobSyncResourcesWrapper(
parentContext context.Context,
logger *logrus.Entry,
databaseRepo database.DatabaseRepository,
sourceCred *models.SourceCredential,
callbackFn func(
_backgroundJobContext context.Context,
_logger *logrus.Entry,
_databaseRepo database.DatabaseRepository,
_sourceCred *models.SourceCredential,
) (sourceModels.SourceClient, sourceModels.UpsertSummary, error),
) (sourceModels.UpsertSummary, error) {
var resultErr error
var backgroundJob *models.BackgroundJob
@ -54,14 +105,6 @@ func BackgroundJobSyncResources(parentContext context.Context, logger *logrus.En
//we can safely ignore this error, because we'll be updating the status of the background job again later
}
// after creating the client, we should do a bulk import
sourceClient, err := factory.GetSourceClient(sourcePkg.GetFastenLighthouseEnv(), sourceCred.SourceType, backgroundJobContext, logger, sourceCred)
if err != nil {
resultErr = fmt.Errorf("an error occurred while initializing hub client using source credential: %w", err)
logger.Errorln(resultErr)
return sourceModels.UpsertSummary{}, resultErr
}
// BEGIN FINALIZER
defer func() {
//finalizer function - update the sync status to completed (or failed depending on the error status)
@ -124,10 +167,11 @@ func BackgroundJobSyncResources(parentContext context.Context, logger *logrus.En
}()
// END FINALIZER
summary, err := sourceClient.SyncAll(databaseRepo)
if err != nil {
resultErr = fmt.Errorf("an error occurred while bulk importing resources from source: %w", err)
logger.Errorln(resultErr)
var sourceClient sourceModels.SourceClient
var summary sourceModels.UpsertSummary
sourceClient, summary, resultErr = callbackFn(backgroundJobContext, logger, databaseRepo, sourceCred)
if resultErr != nil {
logger.Errorln("An error occurred while syncing resources, ignoring", resultErr)
return summary, resultErr
}

View File

@ -94,6 +94,7 @@ func GetResourceFhir(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": true, "data": wrappedResourceModel})
}
// deprecated - using Manual Resource Wizard instead
func CreateResourceComposition(c *gin.Context) {
logger := c.MustGet(pkg.ContextKeyTypeLogger).(*logrus.Entry)

View File

@ -0,0 +1,95 @@
package handler
import (
"context"
"fmt"
"github.com/fastenhealth/fasten-onprem/backend/pkg"
"github.com/fastenhealth/fasten-onprem/backend/pkg/database"
"github.com/fastenhealth/fasten-onprem/backend/pkg/event_bus"
"github.com/fastenhealth/fasten-onprem/backend/pkg/models"
"github.com/fastenhealth/fasten-sources/clients/factory"
sourceModels "github.com/fastenhealth/fasten-sources/clients/models"
sourcePkg "github.com/fastenhealth/fasten-sources/pkg"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"net/http"
)
// mimics functionality in CreateManualSource
func CreateRelatedResources(c *gin.Context) {
logger := c.MustGet(pkg.ContextKeyTypeLogger).(*logrus.Entry)
databaseRepo := c.MustGet(pkg.ContextKeyTypeDatabase).(database.DatabaseRepository)
eventBus := c.MustGet(pkg.ContextKeyTypeEventBusServer).(event_bus.Interface)
// store the bundle file locally
bundleFile, err := storeFileLocally(c)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
return
}
//step 2: find a reference to the Fasten source for this user
sourceCredentials, err := databaseRepo.GetSources(c)
var fastenSourceCredential *models.SourceCredential
for _, sourceCredential := range sourceCredentials {
if sourceCredential.SourceType == sourcePkg.SourceTypeFasten {
fastenSourceCredential = &sourceCredential
break
}
}
if fastenSourceCredential == nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "could not find Fasten source for this user"})
return
}
summary, err := BackgroundJobSyncResourcesWrapper(
c,
logger,
databaseRepo,
fastenSourceCredential,
func(
_backgroundJobContext context.Context,
_logger *logrus.Entry,
_databaseRepo database.DatabaseRepository,
_sourceCred *models.SourceCredential,
) (sourceModels.SourceClient, sourceModels.UpsertSummary, error) {
//step 3: create a "fasten" client, which we can use to parse resources to add to the database
fastenSourceClient, err := factory.GetSourceClient(sourcePkg.GetFastenLighthouseEnv(), sourcePkg.SourceTypeFasten, _backgroundJobContext, _logger, _sourceCred)
if err != nil {
resultErr := fmt.Errorf("could not create Fasten source client")
_logger.Errorln(resultErr)
return fastenSourceClient, sourceModels.UpsertSummary{}, resultErr
}
//step 4: parse the resources from the bundle
summary, err := fastenSourceClient.SyncAllBundle(_databaseRepo, bundleFile, sourcePkg.FhirVersion401)
if err != nil {
resultErr := fmt.Errorf("an error occurred while processing bundle: %v", err)
_logger.Errorln(resultErr)
return fastenSourceClient, summary, resultErr
}
return fastenSourceClient, summary, nil
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
return
}
//step 7 notify the event bus of the new resources
currentUser, _ := databaseRepo.GetCurrentUser(c)
err = eventBus.PublishMessage(
models.NewEventSourceComplete(
currentUser.ID.String(),
fastenSourceCredential.ID.String(),
),
)
if err != nil {
logger.Warnf("ignoring: an error occurred while publishing sync complete event: %v", err)
}
c.JSON(http.StatusOK, gin.H{"success": true, "data": summary, "source": fastenSourceCredential})
}

View File

@ -1,18 +1,21 @@
package handler
import (
"context"
"fmt"
"github.com/fastenhealth/fasten-onprem/backend/pkg"
"github.com/fastenhealth/fasten-onprem/backend/pkg/database"
"github.com/fastenhealth/fasten-onprem/backend/pkg/event_bus"
"github.com/fastenhealth/fasten-onprem/backend/pkg/models"
"github.com/fastenhealth/fasten-sources/clients/factory"
sourceModels "github.com/fastenhealth/fasten-sources/clients/models"
sourcePkg "github.com/fastenhealth/fasten-sources/pkg"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
"io/ioutil"
"net/http"
"os"
)
func CreateReconnectSource(c *gin.Context) {
@ -122,30 +125,17 @@ func SourceSync(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": true, "source": sourceCred, "data": summary})
}
// mimics functionality in CreateRelatedResources
// mimics functionality in SourceSync
func CreateManualSource(c *gin.Context) {
logger := c.MustGet(pkg.ContextKeyTypeLogger).(*logrus.Entry)
databaseRepo := c.MustGet(pkg.ContextKeyTypeDatabase).(database.DatabaseRepository)
eventBus := c.MustGet(pkg.ContextKeyTypeEventBusServer).(event_bus.Interface)
// single file
file, err := c.FormFile("file")
// store the bundle file locally
bundleFile, err := storeFileLocally(c)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "could not extract file from form"})
return
}
fmt.Printf("Uploaded filename: %s", file.Filename)
// create a temporary file to store this uploaded file
bundleFile, err := ioutil.TempFile("", file.Filename)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "could not create temp file"})
return
}
// Upload the file to specific bundleFile.
err = c.SaveUploadedFile(file, bundleFile.Name())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "could not save temp file"})
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
return
}
@ -178,23 +168,40 @@ func CreateManualSource(c *gin.Context) {
return
}
manualSourceClient, err := factory.GetSourceClient(sourcePkg.GetFastenLighthouseEnv(), sourcePkg.SourceTypeManual, c, logger, &manualSourceCredential)
if err != nil {
logger.Errorln("An error occurred while initializing hub client using manual source with credential", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
return
}
summary, err := BackgroundJobSyncResourcesWrapper(
c,
logger,
databaseRepo,
&manualSourceCredential,
func(
_backgroundJobContext context.Context,
_logger *logrus.Entry,
_databaseRepo database.DatabaseRepository,
_sourceCred *models.SourceCredential,
) (sourceModels.SourceClient, sourceModels.UpsertSummary, error) {
manualSourceClient, err := factory.GetSourceClient(sourcePkg.GetFastenLighthouseEnv(), sourcePkg.SourceTypeManual, _backgroundJobContext, _logger, _sourceCred)
if err != nil {
resultErr := fmt.Errorf("an error occurred while initializing hub client using manual source with credential: %w", err)
logger.Errorln(resultErr)
return manualSourceClient, sourceModels.UpsertSummary{}, resultErr
}
summary, err := manualSourceClient.SyncAllBundle(_databaseRepo, bundleFile, bundleType)
if err != nil {
resultErr := fmt.Errorf("an error occurred while processing bundle: %w", err)
logger.Errorln(resultErr)
return manualSourceClient, sourceModels.UpsertSummary{}, resultErr
}
return manualSourceClient, summary, nil
})
summary, err := manualSourceClient.SyncAllBundle(databaseRepo, bundleFile, bundleType)
if err != nil {
logger.Errorln("An error occurred while processing bundle", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
return
}
//publish event
currentUser, _ := databaseRepo.GetCurrentUser(c)
err = eventBus.PublishMessage(
models.NewEventSourceComplete(
currentUser.ID.String(),
@ -260,3 +267,29 @@ func DeleteSource(c *gin.Context) {
}
c.JSON(http.StatusOK, gin.H{"success": true, "data": rowsEffected})
}
// Helpers
func storeFileLocally(c *gin.Context) (*os.File, error) {
// single file
file, err := c.FormFile("file")
if err != nil {
//c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "could not extract file from form"})
return nil, fmt.Errorf("could not extract file from form")
}
fmt.Printf("Uploaded filename: %s", file.Filename)
// create a temporary file to store this uploaded file
bundleFile, err := ioutil.TempFile("", file.Filename)
if err != nil {
//c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "could not create temp file"})
return nil, fmt.Errorf("could not create temp file")
}
// Upload the file to specific bundleFile.
err = c.SaveUploadedFile(file, bundleFile.Name())
if err != nil {
//c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "could not save temp file"})
return nil, fmt.Errorf("could not save temp file")
}
return bundleFile, nil
}

View File

@ -74,7 +74,9 @@ func (ae *AppEngine) Setup() (*gin.RouterGroup, *gin.Engine) {
secure.GET("/resource/fhir", handler.ListResourceFhir)
secure.POST("/resource/graph/:graphType", handler.GetResourceFhirGraph)
secure.GET("/resource/fhir/:sourceId/:resourceId", handler.GetResourceFhir)
secure.POST("/resource/composition", handler.CreateResourceComposition)
secure.POST("/resource/related", handler.CreateRelatedResources)
secure.GET("/dashboards", handler.GetDashboard)
secure.POST("/dashboards", handler.AddDashboardLocation)

View File

@ -51,6 +51,7 @@
"ngx-highlightjs": "^7.0.1",
"ngx-infinite-scroll": "^14.0.0",
"ngx-moment": "^6.0.2",
"parse-full-name": "^1.2.6",
"rxjs": "~6.5.4",
"tslib": "^2.0.0",
"uuid": "^9.0.0",

View File

@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core';
import {NavigationEnd, Router} from '@angular/router';
import {Observable, of} from 'rxjs';
import {ToastService} from './services/toast.service';
import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
@Component({
selector: 'app-root',
@ -15,7 +16,11 @@ export class AppComponent implements OnInit {
showHeader:boolean = false;
showFooter:boolean = true;
constructor(private router: Router, private toastService: ToastService) {}
constructor(
private router: Router,
private toastService: ToastService,
private modalService: NgbModal
) {}
ngOnInit() {
@ -25,17 +30,21 @@ export class AppComponent implements OnInit {
document.querySelector('body').appendChild(navbarBackdrop);
//determine if we should show the header
this.router.events.subscribe(event => this.modifyHeader(event));
this.router.events.subscribe(event => this.routerEvent(event));
}
modifyHeader(event) {
routerEvent(event) {
if (event instanceof NavigationEnd) {
//modify header
if (event.url?.startsWith('/auth') || event.url?.startsWith('/desktop')) {
this.showHeader = false;
} else {
// console.log("NU")
this.showHeader = true;
}
// close all open modals when route change
this.modalService.dismissAll();
}
}
}

View File

@ -38,6 +38,8 @@ import { ExploreComponent } from './pages/explore/explore.component';
import {DirectivesModule} from './directives/directives.module';
import { DesktopCallbackComponent } from './pages/desktop-callback/desktop-callback.component';
import { BackgroundJobsComponent } from './pages/background-jobs/background-jobs.component';
import {FhirCardModule} from './components/fhir-card/fhir-card.module';
import {FhirDatatableModule} from './components/fhir-datatable/fhir-datatable.module';
@NgModule({
declarations: [
@ -64,6 +66,8 @@ import { BackgroundJobsComponent } from './pages/background-jobs/background-jobs
BrowserModule,
FontAwesomeModule,
SharedModule,
FhirCardModule,
FhirDatatableModule,
AppRoutingModule,
HttpClientModule,
NgbModule,

View File

@ -3,7 +3,7 @@ import {BadgeComponent} from "./badge.component";
// More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction
const meta: Meta<BadgeComponent> = {
title: 'Fhir/Common/Badge',
title: 'Fhir Card/Common/Badge',
component: BadgeComponent,
decorators: [
// moduleMetadata({

View File

@ -5,7 +5,7 @@ import {FastenDisplayModel} from "../../../../../lib/models/fasten/fasten-displa
// More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction
const meta: Meta<TableComponent> = {
title: 'Fhir/Common/Table',
title: 'Fhir Card/Common/Table',
component: TableComponent,
decorators: [
// moduleMetadata({

View File

@ -8,7 +8,7 @@ import {BinaryModel} from "../../../../../lib/models/resources/binary-model";
// More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction
const meta: Meta<BinaryTextComponent> = {
title: 'Fhir/Datatypes/BinaryText',
title: 'Fhir Card/Datatypes/BinaryText',
component: BinaryTextComponent,
decorators: [
// moduleMetadata({

View File

@ -6,7 +6,7 @@ import {DicomComponent} from "./dicom.component";
// More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction
const meta: Meta<DicomComponent> = {
title: 'Fhir/Datatypes/Dicom',
title: 'Fhir Card/Datatypes/Dicom',
component: DicomComponent,
decorators: [
// moduleMetadata({

View File

@ -8,7 +8,7 @@ import {HtmlComponent} from "./html.component";
// More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction
const meta: Meta<HtmlComponent> = {
title: 'Fhir/Datatypes/Html',
title: 'Fhir Card/Datatypes/Html',
component: HtmlComponent,
decorators: [
// moduleMetadata({

View File

@ -6,7 +6,7 @@ import {ImgComponent} from "./img.component";
// More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction
const meta: Meta<ImgComponent> = {
title: 'Fhir/Datatypes/Img',
title: 'Fhir Card/Datatypes/Img',
component: ImgComponent,
decorators: [
// moduleMetadata({

View File

@ -0,0 +1,45 @@
import type { Meta, StoryObj } from '@storybook/angular';
import {fhirVersions} from "../../../../../lib/models/constants";
import R4Example1Json from "../../../../../lib/fixtures/r4/resources/binary/exampleMarkdown.json";
import {BinaryModel} from "../../../../../lib/models/resources/binary-model";
import {MarkdownComponent} from "./markdown.component";
// More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction
const meta: Meta<MarkdownComponent> = {
title: 'Fhir Card/Datatypes/Markdown',
component: MarkdownComponent,
decorators: [
// moduleMetadata({
// imports: [AppModule]
// })
// applicationConfig({
// providers: [importProvidersFrom(AppModule)],
// }),
],
tags: ['autodocs'],
render: (args: MarkdownComponent) => ({
props: {
backgroundColor: null,
...args,
},
}),
argTypes: {
displayModel: {
control: 'object',
},
},
};
export default meta;
type Story = StoryObj<MarkdownComponent>;
// More on writing stories with args: https://storybook.js.org/docs/angular/writing-stories/args
let aiDisplayModel1 = new BinaryModel(R4Example1Json, fhirVersions.R4)
aiDisplayModel1.source_id = '123-456-789'
aiDisplayModel1.source_resource_id = '123-456-789'
export const R4Example1: Story = {
args: {
displayModel: aiDisplayModel1
}
};

View File

@ -6,7 +6,7 @@ import {PdfComponent} from "./pdf.component";
// More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction
const meta: Meta<PdfComponent> = {
title: 'Fhir/Datatypes/Pdf',
title: 'Fhir Card/Datatypes/Pdf',
component: PdfComponent,
decorators: [
// moduleMetadata({

View File

@ -0,0 +1,106 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import {BadgeComponent} from './common/badge/badge.component';
import {TableComponent} from './common/table/table.component';
import {BinaryTextComponent} from './datatypes/binary-text/binary-text.component';
import {CodableConceptComponent} from './datatypes/codable-concept/codable-concept.component';
import {CodingComponent} from './datatypes/coding/coding.component';
import {DicomComponent} from './datatypes/dicom/dicom.component';
import {HtmlComponent} from './datatypes/html/html.component';
import {ImgComponent} from './datatypes/img/img.component';
import {MarkdownComponent} from './datatypes/markdown/markdown.component';
import {PdfComponent} from './datatypes/pdf/pdf.component';
import {AllergyIntoleranceComponent} from './resources/allergy-intolerance/allergy-intolerance.component';
import {BinaryComponent} from './resources/binary/binary.component';
import {DiagnosticReportComponent} from './resources/diagnostic-report/diagnostic-report.component';
import {DocumentReferenceComponent} from './resources/document-reference/document-reference.component';
import {FallbackComponent} from './resources/fallback/fallback.component';
import {ImmunizationComponent} from './resources/immunization/immunization.component';
import {LocationComponent} from './resources/location/location.component';
import {MediaComponent} from './resources/media/media.component';
import {MedicationComponent} from './resources/medication/medication.component';
import {MedicationRequestComponent} from './resources/medication-request/medication-request.component';
import {ObservationComponent} from './resources/observation/observation.component';
import {OrganizationComponent} from './resources/organization/organization.component';
import {PractitionerComponent} from './resources/practitioner/practitioner.component';
import {ProcedureComponent} from './resources/procedure/procedure.component';
import {FhirCardComponent} from './fhir-card/fhir-card.component';
import {FhirCardOutletDirective} from './fhir-card/fhir-card-outlet.directive';
import { EncounterComponent } from './resources/encounter/encounter.component';
@NgModule({
imports: [
//common
CommonModule,
BadgeComponent,
//datatypes
TableComponent,
BinaryTextComponent,
CodableConceptComponent,
CodingComponent,
DicomComponent,
HtmlComponent,
ImgComponent,
MarkdownComponent,
PdfComponent,
//resources
AllergyIntoleranceComponent,
BinaryComponent,
DiagnosticReportComponent,
DocumentReferenceComponent,
EncounterComponent,
FallbackComponent,
ImmunizationComponent,
LocationComponent,
MediaComponent,
MedicationComponent,
MedicationRequestComponent,
ObservationComponent,
OrganizationComponent,
PractitionerComponent,
ProcedureComponent,
],
//TODO: every component in here should be migrated to a standalone component
declarations: [
FhirCardComponent,
FhirCardOutletDirective,
],
exports:[
//common
BadgeComponent,
TableComponent,
//datatypes
BinaryTextComponent,
CodableConceptComponent,
CodingComponent,
DicomComponent,
HtmlComponent,
ImgComponent,
MarkdownComponent,
PdfComponent,
//resources
AllergyIntoleranceComponent,
BinaryComponent,
DiagnosticReportComponent,
DocumentReferenceComponent,
EncounterComponent,
FallbackComponent,
ImmunizationComponent,
LocationComponent,
MediaComponent,
MedicationComponent,
MedicationRequestComponent,
ObservationComponent,
OrganizationComponent,
PractitionerComponent,
ProcedureComponent,
FhirCardComponent,
FhirCardOutletDirective,
]
})
export class FhirCardModule { }

View File

@ -1,9 +1,10 @@
import {FastenDisplayModel} from '../../../../lib/models/fasten/fasten-display-model';
//all Fhir Resource components must implement this Interface
export interface FhirResourceComponentInterface {
export interface FhirCardComponentInterface {
displayModel: FastenDisplayModel;
showDetails: boolean;
isCollapsed: boolean;
//these are used to populate the description of the resource. May not be available for all resources
resourceCode?: string;

View File

@ -1,4 +1,4 @@
import { FhirResourceOutletDirective } from './fhir-resource-outlet.directive';
import { FhirCardOutletDirective } from './fhir-card-outlet.directive';
import {
ComponentFactory, ComponentRef,
@ -66,11 +66,11 @@ class TestViewContainerRef extends ViewContainerRef {
}
describe('FhirResourceOutletDirective', () => {
describe('FhirCardOutletDirective', () => {
it('should create an instance', () => {
const directive = new FhirResourceOutletDirective(new TestViewContainerRef());
const directive = new FhirCardOutletDirective(new TestViewContainerRef());
expect(directive).toBeTruthy();
});
});

View File

@ -1,9 +1,9 @@
import {Directive, ViewContainerRef} from '@angular/core';
@Directive({
selector: '[resourceListOutlet]'
selector: '[fhirCardOutlet]'
})
export class ResourceListOutletDirective {
export class FhirCardOutletDirective {
constructor(public viewContainerRef: ViewContainerRef) { }

View File

@ -0,0 +1 @@
<ng-template fhirCardOutlet></ng-template>

View File

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

View File

@ -9,12 +9,12 @@ import {
ViewChild
} from '@angular/core';
import {BinaryModel} from '../../../../lib/models/resources/binary-model';
import {FhirResourceOutletDirective} from './fhir-resource-outlet.directive';
import {FhirCardOutletDirective} from './fhir-card-outlet.directive';
import {ResourceType} from '../../../../lib/models/constants';
import {FallbackComponent} from '../resources/fallback/fallback.component';
import {BinaryComponent} from '../resources/binary/binary.component';
import {FhirResourceComponentInterface} from './fhir-resource-component-interface';
import {FhirCardComponentInterface} from './fhir-card-component-interface';
import {ImmunizationComponent} from '../resources/immunization/immunization.component';
import {AllergyIntoleranceComponent} from '../resources/allergy-intolerance/allergy-intolerance.component';
import {MedicationComponent} from '../resources/medication/medication.component';
@ -28,21 +28,23 @@ 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';
import {EncounterComponent} from '../resources/encounter/encounter.component';
@Component({
selector: 'fhir-resource',
selector: 'fhir-card',
changeDetection: ChangeDetectionStrategy.Default,
templateUrl: './fhir-resource.component.html',
styleUrls: ['./fhir-resource.component.scss']
templateUrl: './fhir-card.component.html',
styleUrls: ['./fhir-card.component.scss']
})
export class FhirResourceComponent implements OnInit, OnChanges {
export class FhirCardComponent implements OnInit, OnChanges {
@Input() displayModel: FastenDisplayModel
@Input() showDetails: boolean = true
@Input() isCollapsed: boolean = false
//location to dynamically load the displayModel
@ViewChild(FhirResourceOutletDirective, {static: true}) fhirResourceOutlet!: FhirResourceOutletDirective;
@ViewChild(FhirCardOutletDirective, {static: true}) fhirCardOutlet!: FhirCardOutletDirective;
constructor() { }
@ -55,21 +57,22 @@ export class FhirResourceComponent implements OnInit, OnChanges {
loadComponent() {
//clear the current outlet
const viewContainerRef = this.fhirResourceOutlet.viewContainerRef;
const viewContainerRef = this.fhirCardOutlet.viewContainerRef;
viewContainerRef.clear();
let componentType = this.typeLookup(this.displayModel?.source_resource_type)
if(componentType != null){
console.log("Attempting to create fhir display component", this.displayModel, componentType)
const componentRef = viewContainerRef.createComponent<FhirResourceComponentInterface>(componentType);
const componentRef = viewContainerRef.createComponent<FhirCardComponentInterface>(componentType);
componentRef.instance.displayModel = this.displayModel;
componentRef.instance.showDetails = this.showDetails;
componentRef.instance.isCollapsed = this.isCollapsed;
componentRef.instance.markForCheck()
}
}
typeLookup(resourceType: ResourceType): Type<FhirResourceComponentInterface> {
typeLookup(resourceType: ResourceType): Type<FhirCardComponentInterface> {
if(!resourceType){
//dont try to render anything if the resourceType isnt set.
return null
@ -114,9 +117,9 @@ export class FhirResourceComponent implements OnInit, OnChanges {
case "DocumentReference": {
return DocumentReferenceComponent;
}
// case "Encounter": {
// return ListEncounterComponent;
// }
case "Encounter": {
return EncounterComponent;
}
// case "Goal": {
// return ListGoalComponent;
// }

View File

@ -1,5 +1,5 @@
import {ChangeDetectorRef, Component, Input, OnInit} from '@angular/core';
import {FhirResourceComponentInterface} from '../../fhir-resource/fhir-resource-component-interface';
import {FhirCardComponentInterface} from '../../fhir-card/fhir-card-component-interface';
import {TableRowItem, TableRowItemDataType} from '../../common/table/table-row-item';
import {Router, RouterModule} from '@angular/router';
import {AllergyIntoleranceModel} from '../../../../../lib/models/resources/allergy-intolerance-model';
@ -15,11 +15,10 @@ import {TableComponent} from "../../common/table/table.component";
templateUrl: './allergy-intolerance.component.html',
styleUrls: ['./allergy-intolerance.component.scss']
})
export class AllergyIntoleranceComponent implements OnInit, FhirResourceComponentInterface {
export class AllergyIntoleranceComponent implements OnInit, FhirCardComponentInterface {
@Input() displayModel: AllergyIntoleranceModel
@Input() showDetails: boolean = true
isCollapsed: boolean = false
@Input() isCollapsed: boolean = false
tableData: TableRowItem[] = []

View File

@ -10,7 +10,7 @@ import R4Example3Json from "../../../../../lib/fixtures/r4/resources/allergyInto
// More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction
const meta: Meta<AllergyIntoleranceComponent> = {
title: 'Fhir/AllergyIntolerance',
title: 'Fhir Card/AllergyIntolerance',
component: AllergyIntoleranceComponent,
decorators: [
// moduleMetadata({
@ -51,8 +51,8 @@ export const R4Example1: Story = {
};
let aiDisplayModel2 = new AllergyIntoleranceModel(R4Example2Json, fhirVersions.R4)
aiDisplayModel1.source_id = '123-456-789'
aiDisplayModel1.source_resource_id = '123-456-789'
aiDisplayModel2.source_id = '123-456-789'
aiDisplayModel2.source_resource_id = '123-456-789'
export const R4Example2: Story = {
args: {
displayModel: aiDisplayModel2
@ -60,8 +60,8 @@ export const R4Example2: Story = {
};
let aiDisplayModel3 = new AllergyIntoleranceModel(R4Example3Json, fhirVersions.R4)
aiDisplayModel1.source_id = '123-456-789'
aiDisplayModel1.source_resource_id = '123-456-789'
aiDisplayModel3.source_id = '123-456-789'
aiDisplayModel3.source_resource_id = '123-456-789'
export const R4Example3: Story = {
args: {
displayModel: aiDisplayModel3

View File

@ -1,6 +1,6 @@
import {ChangeDetectorRef, Component, Input, OnInit} from '@angular/core';
import {BinaryModel} from '../../../../../lib/models/resources/binary-model';
import {FhirResourceComponentInterface} from '../../fhir-resource/fhir-resource-component-interface';
import {FhirCardComponentInterface} from '../../fhir-card/fhir-card-component-interface';
import {Router, RouterModule} from '@angular/router';
import {AttachmentModel} from '../../../../../lib/models/datatypes/attachment-model';
import {FastenApiService} from '../../../../services/fasten-api.service';
@ -37,11 +37,12 @@ import {AuthService} from "../../../../services/auth.service";
templateUrl: './binary.component.html',
styleUrls: ['./binary.component.scss']
})
export class BinaryComponent implements OnInit, FhirResourceComponentInterface {
export class BinaryComponent implements OnInit, FhirCardComponentInterface {
@Input() displayModel: BinaryModel
@Input() showDetails: boolean = true
@Input() attachmentSourceId: string
@Input() attachmentModel: AttachmentModel //can only have attachmentModel or binaryModel, not both.
@Input() isCollapsed: boolean = false
loading: boolean = false
constructor(public changeRef: ChangeDetectorRef, public router: Router, public fastenApi: FastenApiService) {}

View File

@ -18,7 +18,7 @@ import {HTTP_CLIENT_TOKEN} from '../../../../dependency-injection';
// More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction
const meta: Meta<BinaryComponent> = {
title: 'Fhir/Binary',
title: 'Fhir Card/Binary',
component: BinaryComponent,
decorators: [
moduleMetadata({

View File

@ -2,6 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DiagnosticReportComponent } from './diagnostic-report.component';
import {NgbCollapseModule} from '@ng-bootstrap/ng-bootstrap';
import {RouterTestingModule} from '@angular/router/testing';
describe('DiagnosticReportComponent', () => {
let component: DiagnosticReportComponent;
@ -9,8 +10,7 @@ describe('DiagnosticReportComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ DiagnosticReportComponent ],
imports: [NgbCollapseModule]
imports: [NgbCollapseModule, DiagnosticReportComponent, RouterTestingModule]
})
.compileComponents();

View File

@ -1,22 +1,31 @@
import {ChangeDetectorRef, Component, Input, OnInit} from '@angular/core';
import {FhirResourceComponentInterface} from '../../fhir-resource/fhir-resource-component-interface';
import {FhirCardComponentInterface} from '../../fhir-card/fhir-card-component-interface';
import {TableRowItem, TableRowItemDataType} from '../../common/table/table-row-item';
import {Router} from '@angular/router';
import {Router, RouterModule} from '@angular/router';
import {DiagnosticReportModel} from '../../../../../lib/models/resources/diagnostic-report-model';
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 {BinaryComponent} from '../binary/binary.component';
import {GlossaryLookupComponent} from '../../../glossary-lookup/glossary-lookup.component';
@Component({
selector: 'app-diagnostic-report',
standalone: true,
imports: [NgbCollapseModule, CommonModule, BadgeComponent, TableComponent, RouterModule, BinaryComponent, GlossaryLookupComponent],
selector: 'fhir-diagnostic-report',
templateUrl: './diagnostic-report.component.html',
styleUrls: ['./diagnostic-report.component.scss']
})
export class DiagnosticReportComponent implements OnInit, FhirResourceComponentInterface {
export class DiagnosticReportComponent implements OnInit, FhirCardComponentInterface {
@Input() displayModel: DiagnosticReportModel
@Input() showDetails: boolean = true
@Input() isCollapsed: boolean = false
//these are used to populate the description of the resource. May not be available for all resources
resourceCode?: string;
resourceCodeSystem?: string;
isCollapsed: boolean = false
tableData: TableRowItem[] = []
constructor(public changeRef: ChangeDetectorRef, public router: Router) {}

View File

@ -2,6 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DocumentReferenceComponent } from './document-reference.component';
import {NgbCollapseModule} from '@ng-bootstrap/ng-bootstrap';
import {RouterTestingModule} from '@angular/router/testing';
describe('DocumentReferenceComponent', () => {
let component: DocumentReferenceComponent;
@ -9,8 +10,8 @@ describe('DocumentReferenceComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [NgbCollapseModule],
declarations: [ DocumentReferenceComponent ]
imports: [NgbCollapseModule, DocumentReferenceComponent, RouterTestingModule],
declarations: [ ]
})
.compileComponents();

View File

@ -1,19 +1,27 @@
import {ChangeDetectorRef, Component, Input, OnInit} from '@angular/core';
import {DiagnosticReportModel} from '../../../../../lib/models/resources/diagnostic-report-model';
import {TableRowItem, TableRowItemDataType} from '../../common/table/table-row-item';
import {Router} from '@angular/router';
import {Router, RouterModule} from '@angular/router';
import {DocumentReferenceModel} from '../../../../../lib/models/resources/document-reference-model';
import {FhirResourceComponentInterface} from '../../fhir-resource/fhir-resource-component-interface';
import {FhirCardComponentInterface} from '../../fhir-card/fhir-card-component-interface';
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 {BinaryComponent} from '../binary/binary.component';
import {GlossaryLookupComponent} from '../../../glossary-lookup/glossary-lookup.component';
@Component({
selector: 'app-document-reference',
standalone: true,
imports: [NgbCollapseModule, CommonModule, BadgeComponent, TableComponent, RouterModule, BinaryComponent],
selector: 'fhir-document-reference',
templateUrl: './document-reference.component.html',
styleUrls: ['./document-reference.component.scss']
})
export class DocumentReferenceComponent implements OnInit, FhirResourceComponentInterface {
export class DocumentReferenceComponent implements OnInit, FhirCardComponentInterface {
@Input() displayModel: DocumentReferenceModel
@Input() showDetails: boolean = true
isCollapsed: boolean = false
@Input() isCollapsed: boolean = false
tableData: TableRowItem[] = []
constructor(public changeRef: ChangeDetectorRef, public router: Router) {}

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?.sort_title || displayModel?.code?.text}}</h6>
<p class="card-text tx-gray-400" *ngIf="displayModel?.period_start"><strong>Start date</strong> {{displayModel?.period_start | date}}</p>
</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">
<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,24 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { EncounterComponent } from './encounter.component';
import {RouterTestingModule} from '@angular/router/testing';
describe('EncounterComponent', () => {
let component: EncounterComponent;
let fixture: ComponentFixture<EncounterComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ EncounterComponent, RouterTestingModule ]
})
.compileComponents();
fixture = TestBed.createComponent(EncounterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,50 @@
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 {GlossaryLookupComponent} from '../../../glossary-lookup/glossary-lookup.component';
import {Router, RouterModule} from '@angular/router';
import {TableRowItem, TableRowItemDataType} from '../../common/table/table-row-item';
import {FhirCardComponentInterface} from '../../fhir-card/fhir-card-component-interface';
import {EncounterModel} from '../../../../../lib/models/resources/encounter-model';
@Component({
standalone: true,
imports: [NgbCollapseModule, CommonModule, BadgeComponent, TableComponent, GlossaryLookupComponent, RouterModule],
selector: 'fhir-encounter',
templateUrl: './encounter.component.html',
styleUrls: ['./encounter.component.scss']
})
export class EncounterComponent implements OnInit, FhirCardComponentInterface {
@Input() displayModel: EncounterModel | null
@Input() showDetails: boolean = true
@Input() isCollapsed: boolean = false
//these are used to populate the description of the resource. May not be available for all resources
resourceCode?: string;
resourceCodeSystem?: string;
tableData: TableRowItem[] = []
constructor(public changeRef: ChangeDetectorRef, public router: Router) { }
ngOnInit(): void {
this.tableData = [
{
label: 'Type',
data: this.displayModel?.encounter_type?.[0],
data_type: TableRowItemDataType.CodableConcept,
enabled: !!this.displayModel?.encounter_type?.[0],
},
{
label: 'Location',
data: this.displayModel?.location_display,
enabled: !!this.displayModel?.location_display,
},
];
}
markForCheck(){
this.changeRef.markForCheck()
}
}

View File

@ -0,0 +1,70 @@
import type { Meta, StoryObj } from '@storybook/angular';
import {EncounterComponent} from "./encounter.component";
import {EncounterModel} from "../../../../../lib/models/resources/encounter-model";
import {fhirVersions} from "../../../../../lib/models/constants";
import R4Example1Json from "../../../../../lib/fixtures/r4/resources/encounter/example1.json";
import R4Example2Json from "../../../../../lib/fixtures/r4/resources/encounter/example2.json";
import R4Example3Json from "../../../../../lib/fixtures/r4/resources/encounter/example3.json";
// More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction
const meta: Meta<EncounterComponent> = {
title: 'Fhir Card/Encounter',
component: EncounterComponent,
decorators: [
// moduleMetadata({
// imports: [AppModule]
// })
// applicationConfig({
// providers: [importProvidersFrom(AppModule)],
// }),
],
tags: ['autodocs'],
render: (args: EncounterComponent) => ({
props: {
backgroundColor: null,
...args,
},
}),
argTypes: {
displayModel: {
control: 'object',
},
showDetails: {
control: 'boolean',
}
},
};
export default meta;
type Story = StoryObj<EncounterComponent>;
// More on writing stories with args: https://storybook.js.org/docs/angular/writing-stories/args
let encounterDisplayModel1 = new EncounterModel(R4Example1Json, fhirVersions.R4)
encounterDisplayModel1.source_id = '123-456-789'
encounterDisplayModel1.source_resource_id = '123-456-789'
export const R4Example1: Story = {
args: {
displayModel: encounterDisplayModel1
}
};
let encounterDisplayModel2 = new EncounterModel(R4Example2Json, fhirVersions.R4)
encounterDisplayModel2.source_id = '123-456-789'
encounterDisplayModel2.source_resource_id = '123-456-789'
export const R4Example2: Story = {
args: {
displayModel: encounterDisplayModel2
}
};
let encounterDisplayModel3 = new EncounterModel(R4Example3Json, fhirVersions.R4)
encounterDisplayModel3.source_id = '123-456-789'
encounterDisplayModel3.source_resource_id = '123-456-789'
export const R4Example3: Story = {
args: {
displayModel: encounterDisplayModel3
}
};

View File

@ -8,7 +8,7 @@ describe('FallbackComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ FallbackComponent ]
imports: [ FallbackComponent ]
})
.compileComponents();

View File

@ -1,16 +1,22 @@
import {ChangeDetectorRef, Component, Input, OnInit} from '@angular/core';
import {BinaryModel} from '../../../../../lib/models/resources/binary-model';
import {Router} from '@angular/router';
import {FhirResourceComponentInterface} from '../../fhir-resource/fhir-resource-component-interface';
import {FhirCardComponentInterface} from '../../fhir-card/fhir-card-component-interface';
import {NgbCollapseModule} from '@ng-bootstrap/ng-bootstrap';
import {CommonModule} from '@angular/common';
import {HighlightModule} from 'ngx-highlightjs';
@Component({
standalone: true,
imports: [NgbCollapseModule, HighlightModule, CommonModule],
selector: 'fhir-fallback',
templateUrl: './fallback.component.html',
styleUrls: ['./fallback.component.scss']
})
export class FallbackComponent implements OnInit, FhirResourceComponentInterface {
export class FallbackComponent implements OnInit, FhirCardComponentInterface {
@Input() displayModel: BinaryModel
@Input() showDetails: boolean = true
@Input() isCollapsed: boolean = false
constructor(public changeRef: ChangeDetectorRef, public router: Router) {}

Some files were not shown because too many files have changed in this diff Show More