Better Reporting (#12)

This commit is contained in:
Jason Kulatunga 2022-12-17 15:10:19 -08:00 committed by GitHub
parent f67c369a22
commit 6fd69575d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 1139 additions and 387 deletions

View File

@ -65,5 +65,7 @@ jobs:
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
FASTEN_ENV=${{ github.ref_name == 'main' && 'prod' || 'sandbox' }}
# cache-from: type=gha
# cache-to: type=gha,mode=max

View File

@ -2,6 +2,7 @@
# Frontend Build
#########################################################################################################
FROM node:18.9.0 as frontend-build
ARG FASTEN_ENV=sandbox
WORKDIR /usr/src/fastenhealth/frontend
#COPY frontend/package.json frontend/yarn.lock ./
COPY frontend/package.json ./
@ -9,7 +10,7 @@ COPY frontend/package.json ./
RUN yarn config set registry "http://registry.npmjs.org" \
&& yarn install --frozen-lockfile --network-timeout 100000
COPY frontend/ ./
RUN yarn run build -- --configuration sandbox --output-path=../dist
RUN yarn run build -- --configuration ${FASTEN_ENV} --output-path=../dist
#########################################################################################################
# Backend Build
@ -29,26 +30,13 @@ RUN CGO_ENABLED=0 go build -o /go/bin/fasten ./backend/cmd/fasten/
# create folder structure
RUN mkdir -p /opt/fasten/db \
&& mkdir -p /opt/fasten/web \
&& mkdir -p /opt/fasten/config \
&& curl -o /opt/fasten/db/fasten.db -L https://github.com/fastenhealth/testdata/raw/main/fasten.db
&& mkdir -p /opt/fasten/config
#########################################################################################################
# Distribution Build
#########################################################################################################
FROM couchdb:3.2
FROM gcr.io/distroless/static-debian11
ENV FASTEN_COUCHDB_ADMIN_USERNAME=admin
ENV FASTEN_COUCHDB_ADMIN_PASSWORD=mysecretpassword
ENV FASTEN_JWT_ISSUER_KEY=thisismysupersecuressessionsecretlength
ARG S6_ARCH=amd64
RUN curl https://github.com/just-containers/s6-overlay/releases/download/v1.21.8.0/s6-overlay-${S6_ARCH}.tar.gz -L -s --output /tmp/s6-overlay-${S6_ARCH}.tar.gz \
&& tar xzf /tmp/s6-overlay-${S6_ARCH}.tar.gz -C / \
&& rm -rf /tmp/s6-overlay-${S6_ARCH}.tar.gz
COPY /docker/couchdb/fasten.ini /opt/couchdb/etc/local.ini
COPY /docker/rootfs /
WORKDIR /opt/fasten/
COPY --from=backend-build /opt/fasten/ /opt/fasten/
@ -57,7 +45,7 @@ COPY --from=backend-build /go/bin/fasten /opt/fasten/fasten
COPY LICENSE.md /opt/fasten/LICENSE.md
COPY config.yaml /opt/fasten/config/config.yaml
ENTRYPOINT ["/init"]
CMD ["/opt/fasten/fasten", "start", "--config", "/opt/fasten/config/config.yaml"]

View File

@ -22,6 +22,8 @@ type DatabaseRepository interface {
GetResourceBySourceId(context.Context, string, string) (*models.ResourceFhir, error)
ListResources(context.Context, models.ListResourceQueryOptions) ([]models.ResourceFhir, error)
GetPatientForSources(ctx context.Context) ([]models.ResourceFhir, error)
AddResourceAssociation(ctx context.Context, source *models.SourceCredential, resourceType string, resourceId string, relatedSource *models.SourceCredential, relatedResourceType string, relatedResourceId string) error
RemoveResourceAssociation(ctx context.Context, source *models.SourceCredential, resourceType string, resourceId string, relatedSource *models.SourceCredential, relatedResourceType string, relatedResourceId string) error
//UpsertProfile(context.Context, *models.Profile) error
//UpsertOrganziation(context.Context, *models.Organization) error

View File

@ -194,7 +194,47 @@ func (sr *SqliteRepository) UpsertRawResource(ctx context.Context, sourceCredent
SourceResourceID: rawResource.SourceResourceID,
SourceResourceType: rawResource.SourceResourceType,
},
ResourceRaw: datatypes.JSON(rawResource.ResourceRaw),
ResourceRaw: datatypes.JSON(rawResource.ResourceRaw),
RelatedResourceFhir: nil,
}
//create associations
//note: we create the association in the related_resources table **before** the model actually exists.
if rawResource.ReferencedResources != nil {
//these are resources that are referenced by the current resource
relatedResources := []*models.ResourceFhir{}
//reciprocalRelatedResources := []*models.ResourceFhir{}
for _, referencedResource := range rawResource.ReferencedResources {
parts := strings.Split(referencedResource, "/")
if len(parts) == 2 {
relatedResource := &models.ResourceFhir{
OriginBase: models.OriginBase{
SourceID: source.ID,
SourceResourceType: parts[0],
SourceResourceID: parts[1],
},
RelatedResourceFhir: nil,
}
relatedResources = append(relatedResources, relatedResource)
//if the related resource is an Encounter or Condition, make sure we create a reciprocal association as well, just incase
if parts[0] == "Condition" || parts[0] == "Encounter" {
//manually create association (we've tried to create using Association.Append, and it doesnt work for some reason.
err := sr.AddResourceAssociation(ctx, &source, parts[0], parts[1], &source, wrappedResourceModel.SourceResourceType, wrappedResourceModel.SourceResourceID)
if err != nil {
sr.Logger.Errorf("Error when creating a reciprocal association for %s: %v", referencedResource, err)
}
}
}
}
//ignore errors when creating associations (we always get a 'WHERE conditions required' error, )
sr.GormClient.WithContext(ctx).Model(wrappedResourceModel).Association("RelatedResourceFhir").Append(relatedResources)
}
sr.Logger.Infof("insert/update (%v) %v", rawResource.SourceResourceType, rawResource.SourceResourceID)
@ -203,7 +243,7 @@ func (sr *SqliteRepository) UpsertRawResource(ctx context.Context, sourceCredent
SourceID: wrappedResourceModel.GetSourceID(),
SourceResourceID: wrappedResourceModel.GetSourceResourceID(),
SourceResourceType: wrappedResourceModel.GetSourceResourceType(), //TODO: and UpdatedAt > old UpdatedAt
}).FirstOrCreate(wrappedResourceModel)
}).Omit("RelatedResourceFhir.*").FirstOrCreate(wrappedResourceModel)
if createResult.Error != nil {
return false, createResult.Error
@ -211,7 +251,7 @@ func (sr *SqliteRepository) UpsertRawResource(ctx context.Context, sourceCredent
//at this point, wrappedResourceModel contains the data found in the database.
// check if the database resource matches the new resource.
if wrappedResourceModel.ResourceRaw.String() != string(rawResource.ResourceRaw) {
updateResult := createResult.Updates(wrappedResourceModel)
updateResult := createResult.Omit("RelatedResourceFhir.*").Updates(wrappedResourceModel)
return updateResult.RowsAffected > 0, updateResult.Error
} else {
return false, nil
@ -271,13 +311,20 @@ func (sr *SqliteRepository) ListResources(ctx context.Context, queryOptions mode
queryParam.OriginBase.SourceID = sourceUUID
}
if len(queryOptions.SourceResourceID) > 0 {
queryParam.OriginBase.SourceResourceID = queryOptions.SourceResourceID
}
manifestJson, _ := json.MarshalIndent(queryParam, "", " ")
sr.Logger.Infof("THE QUERY OBJECT===========> %v", string(manifestJson))
var wrappedResourceModels []models.ResourceFhir
results := sr.GormClient.WithContext(ctx).
Where(queryParam).
queryBuilder := sr.GormClient.WithContext(ctx)
if queryOptions.PreloadRelated {
//enable preload functionality in query
queryBuilder = queryBuilder.Preload("RelatedResourceFhir").Preload("RelatedResourceFhir.RelatedResourceFhir")
}
results := queryBuilder.Where(queryParam).
Find(&wrappedResourceModels)
return wrappedResourceModels, results.Error
@ -345,6 +392,48 @@ func (sr *SqliteRepository) GetPatientForSources(ctx context.Context) ([]models.
return wrappedResourceModels, results.Error
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Resource Associations
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
func (sr *SqliteRepository) AddResourceAssociation(ctx context.Context, source *models.SourceCredential, resourceType string, resourceId string, relatedSource *models.SourceCredential, relatedResourceType string, relatedResourceId string) error {
//ensure that the sources are "owned" by the same user
if source.UserID != relatedSource.UserID {
return fmt.Errorf("user id's must match when adding associations")
} else if source.UserID != sr.GetCurrentUser(ctx).ID {
return fmt.Errorf("user id's must match current user")
}
//manually create association (we've tried to create using Association.Append, and it doesnt work for some reason.
return sr.GormClient.WithContext(ctx).Table("related_resources").Create(map[string]interface{}{
"resource_fhir_source_id": source.ID,
"resource_fhir_source_resource_type": resourceType,
"resource_fhir_source_resource_id": resourceId,
"related_resource_fhir_source_id": relatedSource.ID,
"related_resource_fhir_source_resource_type": relatedResourceType,
"related_resource_fhir_source_resource_id": relatedResourceId,
}).Error
}
func (sr *SqliteRepository) RemoveResourceAssociation(ctx context.Context, source *models.SourceCredential, resourceType string, resourceId string, relatedSource *models.SourceCredential, relatedResourceType string, relatedResourceId string) error {
if source.UserID != relatedSource.UserID {
return fmt.Errorf("user id's must match when adding associations")
} else if source.UserID != sr.GetCurrentUser(ctx).ID {
return fmt.Errorf("user id's must match current user")
}
//manually create association (we've tried to create using Association.Append, and it doesnt work for some reason.
return sr.GormClient.WithContext(ctx).Table("related_resources").Delete(map[string]interface{}{
"resource_fhir_source_id": source.ID,
"resource_fhir_source_resource_type": resourceType,
"resource_fhir_source_resource_id": resourceId,
"related_resource_fhir_source_id": relatedSource.ID,
"related_resource_fhir_source_resource_type": relatedResourceType,
"related_resource_fhir_source_resource_id": relatedResourceId,
}).Error
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// SourceCredential
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

View File

@ -0,0 +1,15 @@
package models
type ResourceAssociation struct {
SourceID string `json:"source_id"`
SourceResourceType string `json:"source_resource_type"`
SourceResourceID string `json:"source_resource_id"`
OldRelatedSourceID string `json:"old_related_source_id"`
OldRelatedSourceResourceType string `json:"old_related_source_resource_type"`
OldRelatedSourceResourceID string `json:"old_related_source_resource_id"`
NewRelatedSourceID string `json:"new_related_source_id"`
NewRelatedSourceResourceType string `json:"new_related_source_resource_type"`
NewRelatedSourceResourceID string `json:"new_related_source_resource_id"`
}

View File

@ -9,9 +9,15 @@ type ResourceFhir struct {
//embedded data
ResourceRaw datatypes.JSON `json:"resource_raw" gorm:"resource_raw"`
//relationships
RelatedResourceFhir []*ResourceFhir `json:"related_resources" gorm:"many2many:related_resources;ForeignKey:source_id,source_resource_type,source_resource_id;references:source_id,source_resource_type,source_resource_id;"`
}
type ListResourceQueryOptions struct {
SourceID string
SourceResourceType string
SourceResourceID string
PreloadRelated bool
}

View File

@ -20,6 +20,12 @@ func ListResourceFhir(c *gin.Context) {
if len(c.Query("sourceID")) > 0 {
listResourceQueryOptions.SourceID = c.Query("sourceID")
}
if len(c.Query("sourceResourceID")) > 0 {
listResourceQueryOptions.SourceResourceID = c.Query("sourceResourceID")
}
if len(c.Query("preloadRelated")) > 0 {
listResourceQueryOptions.PreloadRelated = true
}
wrappedResourceModels, err := databaseRepo.ListResources(c, listResourceQueryOptions)
@ -49,3 +55,55 @@ func GetResourceFhir(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": true, "data": wrappedResourceModel})
}
func ReplaceResourceAssociation(c *gin.Context) {
logger := c.MustGet("LOGGER").(*logrus.Entry)
databaseRepo := c.MustGet("REPOSITORY").(database.DatabaseRepository)
resourceAssociation := models.ResourceAssociation{}
if err := c.ShouldBindJSON(&resourceAssociation); err != nil {
logger.Errorln("An error occurred while parsing posted resource association data", err)
c.JSON(http.StatusBadRequest, gin.H{"success": false})
return
}
sourceCred, err := databaseRepo.GetSource(c, resourceAssociation.SourceID)
if err != nil {
logger.Errorln("An error occurred while retrieving source", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
return
}
if len(resourceAssociation.OldRelatedSourceID) > 0 {
oldRelatedSourceCred, err := databaseRepo.GetSource(c, resourceAssociation.OldRelatedSourceID)
if err != nil {
logger.Errorln("An error occurred while retrieving old related source", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
return
}
err = databaseRepo.RemoveResourceAssociation(c, sourceCred, resourceAssociation.SourceResourceType, resourceAssociation.SourceResourceID, oldRelatedSourceCred, resourceAssociation.OldRelatedSourceResourceType, resourceAssociation.OldRelatedSourceResourceID)
if err != nil {
logger.Errorln("An error occurred while deleting resource association", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
return
}
}
newRelatedSourceCred, err := databaseRepo.GetSource(c, resourceAssociation.NewRelatedSourceID)
if err != nil {
logger.Errorln("An error occurred while retrieving new related source", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
return
}
err = databaseRepo.AddResourceAssociation(c, sourceCred, resourceAssociation.SourceResourceType, resourceAssociation.SourceResourceID, newRelatedSourceCred, resourceAssociation.NewRelatedSourceResourceType, resourceAssociation.NewRelatedSourceResourceID)
if err != nil {
logger.Errorln("An error occurred while associating resource", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
return
}
c.JSON(http.StatusOK, gin.H{"success": true})
}

View File

@ -59,6 +59,7 @@ func (ae *AppEngine) Setup(logger *logrus.Entry) *gin.Engine {
secure.GET("/source/:sourceId/summary", handler.GetSourceSummary)
secure.GET("/resource/fhir", handler.ListResourceFhir) //
secure.GET("/resource/fhir/:sourceId/:resourceId", handler.GetResourceFhir)
secure.POST("/resource/association", handler.ReplaceResourceAssociation)
}
if ae.Config.GetString("log.level") == "DEBUG" {

View File

@ -22,4 +22,4 @@ web:
log:
file: '' #absolute or relative paths allowed, eg. web.log
level: DEBUG
level: INFO

View File

@ -1,20 +0,0 @@
#########################################################################################################
# CouchDB Build
# NOTE: the context for this build should be the root of the repository.
#########################################################################################################
FROM couchdb:3.2
ENV FASTEN_COUCHDB_ADMIN_USERNAME=admin
ENV FASTEN_COUCHDB_ADMIN_PASSWORD=mysecretpassword
ENV FASTEN_JWT_ISSUER_KEY=thisismysupersecuressessionsecretlength
ARG S6_ARCH=amd64
RUN curl https://github.com/just-containers/s6-overlay/releases/download/v1.21.8.0/s6-overlay-${S6_ARCH}.tar.gz -L -s --output /tmp/s6-overlay-${S6_ARCH}.tar.gz \
&& tar xzf /tmp/s6-overlay-${S6_ARCH}.tar.gz -C / \
&& rm -rf /tmp/s6-overlay-${S6_ARCH}.tar.gz
COPY /docker/couchdb/fasten.ini /opt/couchdb/etc/local.ini
COPY /docker/rootfs /
RUN rm -rf /etc/services.d/fasten #delete the fasten app from the couchdb-only container.
ENTRYPOINT ["/init"]

View File

@ -1,54 +0,0 @@
; CouchDB Configuration Settings
; Custom settings should be made in this file. They will override settings
; in default.ini, but unlike changes made to default.ini, this file won't be
; overwritten on server upgrade.
[couch_peruser]
; fasten requires that each user have a private database. These databases are writable only by the corresponding user.
; Databases are in the following form: userdb-{hex encoded username}
enable = true
[chttpd_auth]
; require_valid_user must be set to false because Fasten will check session endpoint to determine if user is authenticated.
; if this option is not disabled, user is prompted with basic auth.
require_valid_user = false
[httpd]
; enable CORS support, required because the database is hosted on a different node.
enable_cors = true
; ------------------------------------------ DOCKER MODIFICATIONS
; ------------------------------------------ DOCKER MODIFICATIONS
; ------------------------------------------ DOCKER MODIFICATIONS
; ------------------------------------------ DOCKER MODIFICATIONS
; always use single node in docker
[couchdb]
;max_document_size = 4294967296 ; bytes
;os_process_timeout = 5000
single_node = true
; when running in docker, allow cors for all domains
; TODO, we should find a more secure way to do this
[cors]
origins = *
headers = accept, authorization, content-type, origin, referer
credentials = true
methods = GET, PUT, POST, HEAD, DELETE
max_age = 3600
# make sure the databse is listening to all traffic, not just from localhost within the container.
[chttpd]
;port = 5984
;bind_address = 127.0.0.1
bind_address = 0.0.0.0
enable_cors = true
x_forwarded_host = X-Forwarded-Host
; require_valid_user must be set to false because Fasten will check session endpoint to determine if user is authenticated.
; if this option is not disabled, user is prompted with basic auth.
require_valid_user = false
; fasten uses JWT tokens to authenticate against the database. we override the authentication_handlers to add jwt_authentication_handler
authentication_handlers = {chttpd_auth, jwt_authentication_handler}, {chttpd_auth, cookie_authentication_handler}, {chttpd_auth, default_authentication_handler}

View File

@ -1,7 +0,0 @@
#!/usr/bin/with-contenv bash
if [ -n "${TZ}" ]
then
ln -snf "/usr/share/zoneinfo/${TZ}" /etc/localtime
echo "${TZ}" > /etc/timezone
fi

View File

@ -1,33 +0,0 @@
#!/usr/bin/with-contenv bash
if [ -f "/opt/couchdb/data/.config_complete" ]; then
echo "Couchdb config has already completed, skipping"
else
#echo -n means we dont pass newline to base64 (ugh). eg. hello = aGVsbG8K vs aGVsbG8=
FASTEN_JWT_ISSUER_KEY_BASE64=$(echo -n "${FASTEN_JWT_ISSUER_KEY}" | base64)
cat << EOF >> /opt/couchdb/etc/local.d/generated.ini
; ------------------------------------------ GENERATED MODIFICATIONS
; ------------------------------------------ GENERATED MODIFICATIONS
; ------------------------------------------ GENERATED MODIFICATIONS
;
[jwt_auth]
required_claims = exp, {iss, "docker-fastenhealth"}
[jwt_keys]
hmac:_default = ${FASTEN_JWT_ISSUER_KEY_BASE64}
; users should change this default password
[admins]
${FASTEN_COUCHDB_ADMIN_USERNAME} = ${FASTEN_COUCHDB_ADMIN_PASSWORD}
EOF
# create the config complete flag
echo "Couchdb config: complete"
touch /opt/couchdb/data/.config_complete
fi

View File

@ -1,35 +0,0 @@
#!/usr/bin/with-contenv bash
if [ -f "/opt/couchdb/data/.init_complete" ]; then
echo "Couchdb initialization has already completed, skipping"
else
# start couchdb as a background process (store PID)
echo "Couchdb initialization: start couchdb in background mode (non-standard port)"
# https://linux.die.net/man/1/couchdb
sed -i -e 's/;port = 5984/port = 5432/g' /opt/couchdb/etc/local.ini
sed -i -e 's/bind_address = 0.0.0.0/bind_address = 127.0.0.1/g' /opt/couchdb/etc/local.ini
/opt/couchdb/bin/couchdb -b &
COUCHDB_PID=$!
# wait for couchdb to be ready
until $(curl --output /dev/null --silent --head --fail http://127.0.0.1:5432/_up); do echo "couchdb not ready" && sleep 5; done
# create couch_peruser required system databases manually on startup
echo "couchdb ready, start creating system databases"
curl --fail -X PUT -u ${FASTEN_COUCHDB_ADMIN_USERNAME}:${FASTEN_COUCHDB_ADMIN_PASSWORD} http://127.0.0.1:5432/_users
curl --fail -X PUT -u ${FASTEN_COUCHDB_ADMIN_USERNAME}:${FASTEN_COUCHDB_ADMIN_PASSWORD} http://127.0.0.1:5432/_replicator
curl --fail -X PUT -u ${FASTEN_COUCHDB_ADMIN_USERNAME}:${FASTEN_COUCHDB_ADMIN_PASSWORD} http://127.0.0.1:5432/_global_changes
echo "system databases created successfully"
# gracefully stop couchdb process
echo "killing couchdb process"
/opt/couchdb/bin/couchdb -k
sed -i -e 's/port = 5432/;port = 5984/g' /opt/couchdb/etc/local.ini
sed -i -e 's/bind_address = 127.0.0.1/bind_address = 0.0.0.0/g' /opt/couchdb/etc/local.ini
# create the init complete flag
echo "Couchdb initialization: complete"
touch /opt/couchdb/data/.init_complete
fi

View File

@ -1,4 +0,0 @@
#!/usr/bin/with-contenv bash
echo "starting couchdb"
/docker-entrypoint.sh /opt/couchdb/bin/couchdb

View File

@ -1,7 +0,0 @@
#!/usr/bin/with-contenv bash
# wait for couchdb to be ready
until $(curl --output /dev/null --silent --head --fail http://127.0.0.1:5984/_up); do echo "couchdb not ready" && sleep 5; done
echo "starting fasten"
/opt/fasten/fasten start --config /opt/fasten/config/config.yaml

View File

@ -73,7 +73,7 @@
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"sourceMap": true,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
@ -82,7 +82,7 @@
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
"maximumError": "15mb"
},
{
"type": "anyComponentStyle",

View File

@ -23,6 +23,7 @@
"@angular/platform-browser": "^14.1.3",
"@angular/platform-browser-dynamic": "^14.1.3",
"@angular/router": "^14.1.3",
"@circlon/angular-tree-component": "^11.0.4",
"@fortawesome/angular-fontawesome": "^0.11.1",
"@fortawesome/fontawesome-svg-core": "^6.2.0",
"@fortawesome/free-regular-svg-icons": "^6.2.0",
@ -34,6 +35,8 @@
"chart.js": "2.9.4",
"crypto-pouch": "^4.0.1",
"fhirclient": "^2.5.1",
"fhirpath": "^3.3.0",
"fuse.js": "^6.6.2",
"garbados-crypt": "^3.0.0-beta",
"humanize-duration": "^3.27.3",
"idb": "^7.1.0",

View File

@ -29,6 +29,7 @@ import {AuthService} from './services/auth.service';
import { PatientProfileComponent } from './pages/patient-profile/patient-profile.component';
import { MedicalHistoryComponent } from './pages/medical-history/medical-history.component';
@NgModule({
declarations: [
AppComponent,

View File

@ -10,9 +10,13 @@
<a href="#" (click)="toggleHeaderMenu($event)" class="close">&times;</a>
</div><!-- az-header-menu-header -->
<ul class="nav">
<li class="nav-item" ngbDropdown [ngClass]="{ 'active': dashboard.isActive }">
<a routerLink="/dashboard" routerLinkActive="active" #dashboard="routerLinkActive" class="nav-link"><fa-icon [icon]="['fas', 'table-columns']"></fa-icon>&nbsp; Dashboard</a>
</li>
<li class="nav-item" ngbDropdown [ngClass]="{ 'active': medicalHistory.isActive }">
<a routerLink="/medical-history" routerLinkActive="active" #medicalHistory="routerLinkActive" class="nav-link"><fa-icon [icon]="['fas', 'book-medical']"></fa-icon>&nbsp; Medical History</a>
</li>
<li class="nav-item" ngbDropdown [ngClass]="{ 'active': sources.isActive }">
<a routerLink="/sources" routerLinkActive="active" #sources="routerLinkActive" class="nav-link"><fa-icon [icon]="['fas', 'hospital']"></fa-icon>&nbsp; Sources</a>
</li>

View File

@ -0,0 +1,39 @@
<div class="az-dashboard-one-title">
<div>
<h2 class="az-dashboard-title">{{reportHeaderTitle}}</h2>
<p class="az-dashboard-text">{{reportHeaderSubTitle}}</p>
</div>
<div class="az-content-header-right">
<div class="media">
<div class="media-body">
<label>Last Updated</label>
<h6>Oct 10, 2018</h6>
</div><!-- media-body -->
</div><!-- media -->
<div *ngIf="primaryCare" class="media">
<div class="media-body">
<label>Primary Care</label>
<h6>{{primaryCare | fhirPath: "Practitioner.name.family.first()"}}, {{primaryCare | fhirPath: "Practitioner.name.given.first()"}}</h6>
</div><!-- media-body -->
</div><!-- media -->
<div class="media">
<div class="media-body">
<label>Selected Sources</label>
<h6>All Sources</h6>
</div><!-- media-body -->
</div><!-- media -->
<a [routerLink]="'/sources'" class="btn btn-purple">Add Source</a>
</div>
</div><!-- az-dashboard-one-title -->
<div class="az-dashboard-nav">
<nav class="nav">
</nav>
<nav class="nav">
<a class="nav-link" routerLink="/" ngbTooltip="not yet implemented"><i class="far fa-save"></i> Save Report</a>
<a class="nav-link" routerLink="/" ngbTooltip="not yet implemented"><i class="far fa-file-pdf"></i> Export to PDF</a>
<a class="nav-link" routerLink="/" ngbTooltip="not yet implemented"><i class="far fa-envelope"></i>Send to Email</a>
<a class="nav-link" routerLink="/" ngbTooltip="not yet implemented"><i class="fas fa-ellipsis-h"></i></a>
</nav>
</div>

View File

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

View File

@ -0,0 +1,41 @@
import {Component, Input, OnInit} from '@angular/core';
import {ResourceFhir} from '../../models/fasten/resource_fhir';
import {FastenApiService} from '../../services/fasten-api.service';
import * as fhirpath from 'fhirpath';
@Component({
selector: 'report-header',
templateUrl: './report-header.component.html',
styleUrls: ['./report-header.component.scss']
})
export class ReportHeaderComponent implements OnInit {
patient: ResourceFhir = null
primaryCare: ResourceFhir = null
@Input() reportHeaderTitle: string = ""
@Input() reportHeaderSubTitle: string = "Organized by condition and encounters"
constructor(
private fastenApi: FastenApiService,
) { }
ngOnInit(): void {
this.fastenApi.getResources("Patient").subscribe(results => {
console.log(results)
this.patient = results[0]
let primaryCareId = fhirpath.evaluate(this.patient.resource_raw, "Patient.generalPractitioner.reference.first()")
console.log("GP:", primaryCareId)
if(primaryCareId){
let primaryCareIdStr = primaryCareId.join("")
let primaryCareIdParts = primaryCareIdStr.split("/")
if(primaryCareIdParts.length == 2) {
console.log(primaryCareIdParts)
this.fastenApi.getResources(primaryCareIdParts[0], this.patient.source_id, primaryCareIdParts[1]).subscribe(primaryResults => {
this.primaryCare = primaryResults[0]
})
}
}
})
}
}

View File

@ -0,0 +1,110 @@
<div class="card card-dashboard-seven mb-3">
<div class="card-header tx-medium">
<div class="row" routerLink="/source/{{condition?.source_id}}/resource/{{condition?.source_resource_id}}">
<!-- Condition Header -->
<div class="col-6">
{{condition | fhirPath: "Condition.code.text.first()":"Condition.code.coding.display.first()"}}
</div>
<div class="col-6">
{{condition | fhirPath: "Condition.onsetPeriod.start":"Condition.onsetDateTime" | date }} - {{condition | fhirPath: "Condition.onsetPeriod.end" | date}}
</div>
</div>
</div><!-- card-header -->
<div class="card-body">
<div class="row">
<!-- Condition Details -->
<div class="col-6 mb-2">
<div class="row pl-3">
<div class="col-12 mt-3 mb-2 tx-indigo">
<p>Involved in Care</p>
</div>
<ng-container *ngFor="let careTeamEntry of careTeams | keyvalue">
<div class="col-6">
<strong>{{careTeamEntry.value | fhirPath: "CareTeam.participant.member.display"}}</strong>
</div>
<div class="col-6">
{{careTeamEntry.value | fhirPath: "CareTeam.participant.role.text"}}
</div>
</ng-container>
<ng-container *ngFor="let practitionerEntry of practitioners | keyvalue">
<div class="col-6">
<strong>{{practitionerEntry.value | fhirPath: "Practitioner.name.family"}}, {{practitionerEntry.value | fhirPath: "Practitioner.name.given"}}</strong>
</div>
<div class="col-6">
{{practitionerEntry.value | fhirPath: "Practitioner.name.prefix"}}
</div>
</ng-container>
<!-- <div class="col-12 mt-3 mb-2 tx-indigo">-->
<!-- <h5>Initial Presentation</h5>-->
<!-- </div>-->
<!-- <div class="col-12">-->
<!-- Acute right knee pain and tenderness around the joint line - this was likely caused by acute renal failure.-->
<!-- </div>-->
</div>
</div>
<div class="col-6 bg-gray-100">
<div class="row">
<ng-container *ngFor="let encounter of condition.related_resources | filter:'source_resource_type':'Encounter'">
<div routerLink="/source/{{encounter?.source_id}}/resource/{{encounter?.source_resource_id}}" class="col-6 mt-3 mb-2 tx-indigo">
<strong>{{encounter | fhirPath: "Encounter.period.start" | date}}</strong>
</div>
<div routerLink="/source/{{encounter?.source_id}}/resource/{{encounter?.source_resource_id}}" class="col-6 mt-3 mb-2 tx-indigo">
<small>{{encounter | fhirPath: "Encounter.location.first().location.display"}}</small>
</div>
<div *ngIf="encounter.related_resources | filter:'source_resource_type':'MedicationRequest' as medications" class="col-12 mt-2 mb-2">
<strong>Medications:</strong>
<ul>
<li routerLink="/source/{{medication?.source_id}}/resource/{{medication?.source_resource_id}}" *ngFor="let medication of medications">
{{medication | fhirPath: "MedicationRequest.medicationReference.display":"MedicationRequest.medicationCodeableConcept.text"}}
</li>
</ul>
</div>
<div *ngIf="encounter.related_resources | filter:'source_resource_type':'Procedure' as procedures" class="col-12 mt-2 mb-2">
<strong>Procedures:</strong>
<ul>
<li routerLink="/source/{{procedure?.source_id}}/resource/{{procedure?.source_resource_id}}" *ngFor="let procedure of procedures">
{{procedure | fhirPath: "Procedure.code.text"}}
</li>
</ul>
</div>
<div *ngIf="encounter.related_resources | filter:'source_resource_type':'DiagnosticReport' as diagnosticReports" class="col-12 mt-2 mb-2">
<strong>Tests and Examinations:</strong>
<ul>
<li routerLink="/source/{{diagnosticReport?.source_id}}/resource/{{diagnosticReport?.source_resource_id}}" *ngFor="let diagnosticReport of diagnosticReports">
{{diagnosticReport | fhirPath: "DiagnosticReport.code.text":"DiagnosticReport.code.coding.display"}}
</li>
</ul>
</div>
<div *ngIf="encounter.related_resources | filter:'source_resource_type':'Device' as devices" class="col-12 mt-2 mb-2">
<strong>Device:</strong>
<ul>
<li routerLink="/source/{{device?.source_id}}/resource/{{device?.source_resource_id}}" *ngFor="let device of devices">
{{device | fhirPath: "Device.code.text"}}
</li>
</ul>
</div>
</ng-container>
</div>
</div>
</div>
</div><!-- card-body -->
</div>

View File

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

View File

@ -0,0 +1,52 @@
import {Component, Input, OnInit} from '@angular/core';
import {ResourceFhir} from '../../models/fasten/resource_fhir';
@Component({
selector: 'app-report-medical-history-condition',
templateUrl: './report-medical-history-condition.component.html',
styleUrls: ['./report-medical-history-condition.component.scss']
})
export class ReportMedicalHistoryConditionComponent implements OnInit {
@Input() condition: ResourceFhir
careTeams: {[careTeamId: string]: ResourceFhir} = {}
practitioners: {[practitionerId: string]: ResourceFhir} = {}
encounters: {[encounterId: string]: ResourceFhir} = {}
constructor() { }
ngOnInit(): void {
for(let resource of this.condition.related_resources){
this.recExtractResources(resource)
}
// console.log("EXTRACTED CARETEAM", this.careTeams)
// console.log("EXTRACTED practitioners", this.practitioners)
// console.log("EXTRACTED encounters", this.encounters)
}
recExtractResources(resource: ResourceFhir){
if(resource.source_resource_type == "CareTeam"){
this.careTeams[this.genResourceId(resource)] = resource
} else if (resource.source_resource_type == "Practitioner"){
this.practitioners[this.genResourceId(resource)] = resource
} else if (resource.source_resource_type == "Encounter"){
this.encounters[this.genResourceId(resource)] = resource
}
if(!resource.related_resources){
return
}
for(let relatedResource of resource.related_resources){
this.recExtractResources(relatedResource)
}
}
genResourceId(relatedResource: ResourceFhir): string {
return `${relatedResource.source_id}/${relatedResource.source_resource_type}/${relatedResource.source_resource_id}`
}
}

View File

@ -0,0 +1,12 @@
<div class="modal-header">
<h4 class="modal-title"> Condition Editor </h4>
<button type="button" class="close" aria-label="Close" (click)="activeModal.dismiss('Cross click')"><span aria-hidden="true">×</span></button>
</div>
<div class="modal-body">
<tree-root [nodes]="nodes" [options]="options" (moveNode)="onResourceMoved($event)"></tree-root>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-dark" (click)="activeModal.close('Close click')">Close</button>
</div>

View File

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

View File

@ -0,0 +1,169 @@
import {Component, Input, OnInit} from '@angular/core';
import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap';
import {ResourceFhir} from '../../models/fasten/resource_fhir';
import {FastenApiService} from '../../services/fasten-api.service';
import * as fhirpath from 'fhirpath';
class RelatedNode {
name: string
resourceType: string
resourceId: string
sourceId: string
draggable: boolean
children: RelatedNode[]
}
@Component({
selector: 'app-report-medical-history-editor',
templateUrl: './report-medical-history-editor.component.html',
styleUrls: ['./report-medical-history-editor.component.scss']
})
export class ReportMedicalHistoryEditorComponent implements OnInit {
@Input() conditions: ResourceFhir[] = []
@Input() encounters: ResourceFhir[] = []
assignedEncounters: {[name: string]: ResourceFhir} = {}
nodes = [
// {
// id: 1,
// name: 'root1',
// children: [
// { id: 2, name: 'child1' },
// { id: 3, name: 'child2' }
// ]
// },
// {
// id: 4,
// name: 'root2',
// children: [
// { id: 5, name: 'child2.1' },
// {
// id: 6,
// name: 'child2.2',
// children: [
// { id: 7, name: 'subsub' }
// ]
// }
// ]
// }
];
options = {
allowDrag: (node) => {return node.data.draggable},
allowDrop: (element, { parent, index }) => {
// return true / false based on element, to.parent, to.index. e.g.
return parent.data.resourceType == "Condition";
},
}
constructor(
public activeModal: NgbActiveModal,
private fastenApi: FastenApiService,
) { }
ngOnInit(): void {
this.nodes = this.generateNodes(this.conditions)
}
onResourceMoved($event) {
this.fastenApi.replaceResourceAssociation({
source_id: $event.to.parent.sourceId,
source_resource_type: $event.to.parent.resourceType,
source_resource_id: $event.to.parent.resourceId,
new_related_source_id: $event.node.sourceId,
new_related_source_resource_type: $event.node.resourceType,
new_related_source_resource_id: $event.node.resourceId,
}).subscribe(results => {
console.log(results)
})
}
generateNodes(resouceFhirList: ResourceFhir[]): RelatedNode[] {
let relatedNodes = resouceFhirList.map((resourceFhir) => { return this.recGenerateNode(resourceFhir) })
//create an unassigned encounters "condition"
if(this.encounters.length > 0){
let unassignedCondition = {
name: "[Unassigned Encounters]",
resourceType: "Condition",
resourceId: "UNASSIGNED",
sourceId: "UNASSIGNED",
draggable: false,
children: []
}
for(let encounter of this.encounters){
let encounterId = `${encounter.source_id}/${encounter.source_resource_type}/${encounter.source_resource_id}`
if(!this.assignedEncounters[encounterId]){
this.assignedEncounters[encounterId] = encounter
unassignedCondition.children.push(this.recGenerateNode(encounter))
}
}
if(unassignedCondition.children.length > 0){
//only add the unassigned condition block if the subchildren list is populated.
relatedNodes.push(unassignedCondition)
}
}
console.log("NODES:", relatedNodes)
return relatedNodes
}
recGenerateNode(resourceFhir: ResourceFhir): RelatedNode {
let relatedNode = {
sourceId: resourceFhir.source_id,
name: `[${resourceFhir.source_resource_type}/${resourceFhir.source_resource_id}]`,
resourceId: resourceFhir.source_resource_id,
resourceType: resourceFhir.source_resource_type,
draggable: resourceFhir.source_resource_type == "Encounter" || resourceFhir.source_resource_type == "Condition",
children: [],
}
switch (resourceFhir.source_resource_type) {
case "Condition":
relatedNode.name += ` ${fhirpath.evaluate(resourceFhir.resource_raw, "Condition.onsetPeriod.start")} ${fhirpath.evaluate(resourceFhir.resource_raw, "Condition.code.text.first()")}`
break
case "Encounter":
relatedNode.name += ` ${fhirpath.evaluate(resourceFhir.resource_raw, "Encounter.period.start")} ${fhirpath.evaluate(resourceFhir.resource_raw, "Encounter.location.first().location.display")}`
break
case "CareTeam":
relatedNode.name += ` ${fhirpath.evaluate(resourceFhir.resource_raw, "CareTeam.participant.member.display")}`
break
case "Location":
relatedNode.name += ` ${fhirpath.evaluate(resourceFhir.resource_raw, "Location.name")}`
break
case "Organization":
relatedNode.name += ` ${fhirpath.evaluate(resourceFhir.resource_raw, "Organization.name")}`
break
case "Practitioner":
relatedNode.name += ` ${fhirpath.evaluate(resourceFhir.resource_raw, "Practitioner.name.family")}`
break
case "MedicationRequest":
relatedNode.name += ` ${fhirpath.evaluate(resourceFhir.resource_raw, "MedicationRequest.medicationReference.display")}`
break
}
this.assignedEncounters[`${resourceFhir.source_id}/${resourceFhir.source_resource_type}/${resourceFhir.source_resource_id}`] = resourceFhir
if(!resourceFhir.related_resources){
return relatedNode
} else {
relatedNode.children = resourceFhir.related_resources.map((relatedResourceFhir)=>{
return this.recGenerateNode(relatedResourceFhir)
})
return relatedNode
}
}
}

View File

@ -34,6 +34,12 @@ import { ListFallbackResourceComponent } from './list-fallback-resource/list-fal
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
import { ToastComponent } from './toast/toast.component';
import { MomentModule } from 'ngx-moment';
import { ReportHeaderComponent } from './report-header/report-header.component';
import { FhirPathPipe } from '../pipes/fhir-path.pipe';
import { ReportMedicalHistoryEditorComponent } from './report-medical-history-editor/report-medical-history-editor.component';
import { TreeModule } from '@circlon/angular-tree-component';
import {FilterPipe} from '../pipes/filter.pipe';
import { ReportMedicalHistoryConditionComponent } from './report-medical-history-condition/report-medical-history-condition.component';
@NgModule({
imports: [
@ -42,6 +48,7 @@ import { MomentModule } from 'ngx-moment';
NgxDatatableModule,
NgbModule,
MomentModule,
TreeModule,
],
declarations: [
ComponentsSidebarComponent,
@ -74,38 +81,49 @@ import { MomentModule } from 'ngx-moment';
ResourceListOutletDirective,
ListFallbackResourceComponent,
ToastComponent,
ReportHeaderComponent,
FhirPathPipe,
ReportMedicalHistoryEditorComponent,
FilterPipe,
ReportMedicalHistoryConditionComponent,
],
exports: [
ComponentsSidebarComponent,
ListAllergyIntoleranceComponent,
ListAdverseEventComponent,
ListCarePlanComponent,
ListCommunicationComponent,
ListConditionComponent,
ListEncounterComponent,
ListAppointmentComponent,
ListGenericResourceComponent,
ListImmunizationComponent,
ListMedicationAdministrationComponent,
ListMedicationComponent,
ListMedicationDispenseComponent,
ListMedicationRequestComponent,
ListNutritionOrderComponent,
ListObservationComponent,
ListPatientComponent,
ListProcedureComponent,
ListDeviceRequestComponent,
UtilitiesSidebarComponent,
ListCoverageComponent,
ListServiceRequestComponent,
ListDocumentReferenceComponent,
ListDeviceComponent,
ListDiagnosticReportComponent,
ListGoalComponent,
ResourceListComponent,
ResourceListOutletDirective,
ToastComponent,
]
exports: [
ComponentsSidebarComponent,
ListAllergyIntoleranceComponent,
ListAdverseEventComponent,
ListCarePlanComponent,
ListCommunicationComponent,
ListConditionComponent,
ListEncounterComponent,
ListAppointmentComponent,
ListGenericResourceComponent,
ListImmunizationComponent,
ListMedicationAdministrationComponent,
ListMedicationComponent,
ListMedicationDispenseComponent,
ListMedicationRequestComponent,
ListNutritionOrderComponent,
ListObservationComponent,
ListPatientComponent,
ListProcedureComponent,
ListDeviceRequestComponent,
UtilitiesSidebarComponent,
ListCoverageComponent,
ListServiceRequestComponent,
ListDocumentReferenceComponent,
ListDeviceComponent,
ListDiagnosticReportComponent,
ListGoalComponent,
ResourceListComponent,
ResourceListOutletDirective,
ToastComponent,
ReportHeaderComponent,
ReportMedicalHistoryEditorComponent,
FhirPathPipe,
FilterPipe,
ReportMedicalHistoryConditionComponent
]
})
export class SharedModule { }

View File

@ -0,0 +1,15 @@
export class ResourceAssociation {
source_id: string
source_resource_type: string
source_resource_id: string
old_related_source_id?: string
old_related_source_resource_type?: string
old_related_source_resource_id?: string
new_related_source_id: string
new_related_source_resource_type: string
new_related_source_resource_id: string
}

View File

@ -6,6 +6,7 @@ export class ResourceFhir {
fhir_version: string = ""
resource_raw: IResourceRaw
related_resources?: ResourceFhir[] = []
constructor(object?: any) {
return Object.assign(this, object)

View File

@ -1,10 +0,0 @@
import {Source} from '../../models/fasten/source';
export class SourceSyncMessage {
source: Source
current_user: string
auth_token: string
couchdb_endpoint_base: string
fasten_api_endpoint_base: string
response?: any
}

View File

@ -3,122 +3,31 @@
<div class="az-content-body">
<!-- Header Row -->
<div class="row">
<div class="col-6 bg-indigo tx-white d-flex align-items-center">
<h1>Medical History</h1>
</div>
<div class="col-3 mt-2 mb-2 tx-12">
<p class="mb-0">
<strong>Patient:</strong> Caldwell, Ruben<br/>
<strong>Address:</strong> 123 B Street<br/>Gainsville, FL, 94153<br/>
<strong>Date of Birth:</strong> June 20, 1929<br/>
<strong>Phone:</strong> 415-343-2342<br/>
<strong>Email:</strong> myemail@gmail.com
</p>
</div>
<div class="col-3 tx-indigo mt-2 mb-2 tx-12">
<p class="mb-0">
<strong>Primary Care:</strong> Bishop, J. ANRP<br/>
<strong>Address:</strong> Malcom Randall VA <br/>Medical Center Gainsville FL<br/>
<strong>Phone:</strong> 123-321-5532<br/>
<strong>Email:</strong> myemail@va.com<br/>
</p>
<report-header [reportHeaderTitle]="'Medical History'"></report-header>
<!-- Editor Button -->
<div class="row mt-5 mb-3">
<div class="col-12">
<div class="alert alert-warning" role="alert">
<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>
</div>
</div>
</div>
<!-- Conditions Title -->
<div class="row mt-5 mb-3">
<div class="col-6">
<h1>Condition</h1>
</div>
<div class="col-6 tx-indigo">
<h1>History</h1>
<h1 class="az-dashboard-title">Condition</h1>
</div>
</div>
<!-- Condition List -->
<div class="row bg-indigo tx-white pt-1 pb-1">
<!-- Condition Header -->
<div class="col-6 d-flex align-items-center">
<h3 class="mb-0">Gout</h3>
</div>
<div class="col-6 d-flex align-items-center">
<h3 class="mb-0">Nov 16, 2002 - Present</h3>
</div>
</div>
<div class="row">
<!-- Condition Details -->
<div class="col-6 mb-2">
<div class="row pl-3">
<div class="col-12 mt-3 mb-2 tx-indigo">
<h5>Involved in Care</h5>
</div>
<div class="col-6">
<strong>James Bishop</strong>, ANRP
</div>
<div class="col-6">
Primary Care
</div>
<div class="col-6">
<strong>Matthew Leonard</strong>, MD
</div>
<div class="col-6">
Diagnosing Physician
</div>
<div class="col-6">
<strong>Stephanie Wrenn</strong>, MD
</div>
<div class="col-6">
Dietitian
</div>
<div class="col-12 mt-3 mb-2 tx-indigo">
<h5>Initial Presentation</h5>
</div>
<div class="col-12">
Acute right knee pain and tenderness around the joint line - this was likely caused by acute renal failure.
</div>
</div>
</div>
<div class="col-6 mb-2 bg-gray-100">
<div class="row">
<div class="col-12 mt-3 mb-2 tx-indigo">
<h5>Nov 19, 2012</h5>
</div>
<div class="col-12 mt-2 mb-2">
<strong>Medications:</strong> Colchicine, as needed for gout attacks
</div>
<div class="col-12 mt-3 mb-2 tx-indigo">
<h5>Nov 16, 2012</h5>
</div>
<div class="col-12 mt-2 mb-2">
<strong>Procedures:</strong> The fluid in your right knee was drained.
</div>
<div class="col-12 mt-2 mb-2">
<strong>Tests and Examinations:</strong> The fluid tested prositive for gout crystals
</div>
<div class="col-12 mt-2 mb-2">
<strong>Medications:</strong> You were given a steroid injection to reduce inflammation and as short course prednistone to reduce pain and inflammation
</div>
</div>
</div>
</div>
<app-report-medical-history-condition *ngFor="let condition of conditions; let i = index" [condition]="condition"></app-report-medical-history-condition>
</div>
</div>
</div>

View File

@ -1,4 +1,10 @@
import { Component, OnInit } from '@angular/core';
import {Component, Input, OnInit} from '@angular/core';
import {FastenApiService} from '../../services/fasten-api.service';
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 {ReportEditorRelatedComponent} from '../../components/report-editor-related/report-editor-related.component';
@Component({
selector: 'app-medical-history',
@ -6,10 +12,96 @@ import { Component, OnInit } from '@angular/core';
styleUrls: ['./medical-history.component.scss']
})
export class MedicalHistoryComponent implements OnInit {
closeResult = '';
conditions: ResourceFhir[] = []
unassigned_encounters: ResourceFhir[] = []
resourceLookup: {[name: string]: ResourceFhir} = {}
constructor(
private fastenApi: FastenApiService,
private modalService: NgbModal
) { }
constructor() { }
ngOnInit(): void {
forkJoin([
this.fastenApi.getResources("Condition", null, null, true),
this.fastenApi.getResources("Encounter", null, null, true)
]).subscribe(results => {
this.conditions = results[0]
//populate a lookup table with all resources
for(let condition of this.conditions){
this.recPopulateResourceLookup(condition)
}
console.log("Populated resource lookup:", this.resourceLookup);
//find unassigned encounters
console.log("all encounters:", results[1].length, results[1]);
(results[1] || []).map((encounter) => {
if(!this.resourceLookup[`${encounter.source_id}/${encounter.source_resource_type}/${encounter.source_resource_id}`]){
this.unassigned_encounters.push(encounter)
}
})
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: 'UNASSIGNED',
related_resources: this.unassigned_encounters
} as any)
}
})
}
openEditorRelated(): void {
const modalRef = this.modalService.open(ReportMedicalHistoryEditorComponent);
modalRef.componentInstance.conditions = this.conditions;
modalRef.componentInstance.encounters = this.unassigned_encounters;
}
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
}
}
// private getDismissReason(reason: any): string {
// if (reason === ModalDismissReasons.ESC) {
// return 'by pressing ESC';
// } else if (reason === ModalDismissReasons.BACKDROP_CLICK) {
// return 'by clicking on a backdrop';
// } else {
// return `with: ${reason}`;
// }
// }
}

View File

@ -11,7 +11,6 @@ import {ActivatedRoute, Router} from '@angular/router';
import {Location} from '@angular/common';
import {ToastService} from '../../services/toast.service';
import {ToastNotification, ToastType} from '../../models/fasten/toast';
import {SourceSyncMessage} from '../../models/queue/source-sync-message';
import {environment} from '../../../environments/environment';
// If you dont import this angular will import the wrong "Location"

View File

@ -3,34 +3,13 @@
<div class="az-content-body">
<!-- Header Row -->
<div class="row">
<div class="col-6 bg-indigo tx-white d-flex align-items-center">
<h1>Patient Profile</h1>
</div>
<div class="col-3 mt-2 mb-2 tx-12">
<p class="mb-0">
<strong>Patient:</strong> Caldwell, Ruben<br/>
<strong>Address:</strong> 123 B Street<br/>Gainsville, FL, 94153<br/>
<strong>Date of Birth:</strong> June 20, 1929<br/>
<strong>Phone:</strong> 415-343-2342<br/>
<strong>Email:</strong> myemail@gmail.com
</p>
</div>
<div class="col-3 tx-indigo mt-2 mb-2 tx-12">
<p class="mb-0">
<strong>Primary Care:</strong> Bishop, J. ANRP<br/>
<strong>Address:</strong> Malcom Randall VA <br/>Medical Center Gainsville FL<br/>
<strong>Phone:</strong> 123-321-5532<br/>
<strong>Email:</strong> myemail@va.com<br/>
</p>
</div>
</div>
<report-header [reportHeaderTitle]="'Patient Profile'"></report-header>
<div class="pl-3 pr-3">
<!-- Patient Name Row -->
<div class="row mt-5 mb-3">
<div class="col-6">
<h1>Caldwell, Ruben</h1>
<h1>{{patient | fhirPath: "Patient.name.family.first()"}}, {{patient | fhirPath: "Patient.name.given.first()"}}</h1>
</div>
</div>
@ -44,28 +23,28 @@
<strong class="tx-indigo">First Name:</strong>
</div>
<div class="col-6">
Ruben
{{patient | fhirPath: "Patient.name.given.first()"}}
</div>
<div class="col-6">
<strong class="tx-indigo">Last Name:</strong>
</div>
<div class="col-6">
Caldwell
{{patient | fhirPath: "Patient.name.family.first()"}}
</div>
<div class="col-6">
<strong class="tx-indigo">Gender:</strong>
</div>
<div class="col-6">
M
{{patient | fhirPath: "Patient.gender" | titlecase}}
</div>
<div class="col-6">
<strong class="tx-indigo">Martial Status:</strong>
</div>
<div class="col-6">
Married
{{patient | fhirPath: "Patient.maritalStatus.text" | titlecase}}
</div>
<div class="col-6">
@ -86,35 +65,35 @@
<strong class="tx-indigo">Language:</strong>
</div>
<div class="col-6">
English
{{patient | fhirPath: "Patient.communication.language.text" | titlecase}}
</div>
<div class="col-6">
<strong class="tx-indigo">Address:</strong>
</div>
<div class="col-6">
123 B Street<br/>Gainsville, FL, 94153<br/>
{{patient | fhirPath: "Patient.address.line.first()"}}<br/>{{patient | fhirPath: "Patient.address.city.first()"}}, {{patient | fhirPath: "Patient.address.state.first()"}}, {{patient | fhirPath: "Patient.address.postalCode.first()"}}
</div>
<div class="col-6">
<strong class="tx-indigo">Date of Birth:</strong>
</div>
<div class="col-6">
June 20, 1929
{{patient | fhirPath: "Patient.birthDate" | date }}
</div>
<div class="col-6">
<strong class="tx-indigo">Phone:</strong>
</div>
<div class="col-6">
415-343-2342
{{patient | fhirPath: "Patient.telecom.where(system='phone').value.first()"}}
</div>
<div class="col-6">
<strong class="tx-indigo">Email:</strong>
</div>
<div class="col-6">
myemail@gmail.com
{{patient | fhirPath: "Patient.telecom.where(system='email').value.first()"}}
</div>
</div>
@ -163,14 +142,12 @@
<h2 class="tx-indigo">Immunizations</h2>
<p>
<strong class="tx-indigo">Phenumovax</strong><br/>
07/09/2022<br/>
<br/>
<strong class="tx-indigo">Hepatitis B Series [complete]</strong><br/>
01/12/2005<br/>
<br/>
<strong class="tx-indigo">Tetanus Toxoid [complete]</strong><br/>
02/02/2010<br/>
<ng-container *ngFor="let immunization of immunizations; let i = index">
<strong class="tx-indigo">{{immunization | fhirPath: "Immunization.vaccineCode.text" }}</strong><br/>
{{immunization | fhirPath: "Immunization.occurrenceDateTime" | date }}<br/>
{{immunization | fhirPath: "Immunization.location.display" }}<br/>
<br/>
</ng-container>
</p>
</div>
@ -178,6 +155,12 @@
<h2 class="tx-indigo">Allergies</h2>
<p>
<ng-container *ngFor="let allergy of allergyIntolerances; let i = index">
<strong class="tx-indigo">{{allergy | fhirPath: "AllergyIntolerance.code.text" }}</strong><br/>
{{allergy | fhirPath: "AllergyIntolerance.occurrenceDateTime" | date }}<br/>
<br/>
</ng-container>
<strong class="tx-indigo">Latex</strong><br/>
(AV/Historical) with rash and agitation<br/>
<br/>

View File

@ -1,4 +1,6 @@
import { Component, OnInit } from '@angular/core';
import {ResourceFhir} from '../../models/fasten/resource_fhir';
import {FastenApiService} from '../../services/fasten-api.service';
@Component({
selector: 'app-patient-profile',
@ -7,9 +9,28 @@ import { Component, OnInit } from '@angular/core';
})
export class PatientProfileComponent implements OnInit {
constructor() { }
patient: ResourceFhir = null
immunizations: ResourceFhir[] = []
allergyIntolerances: ResourceFhir[] = []
constructor(
private fastenApi: FastenApiService,
) { }
ngOnInit(): void {
this.fastenApi.getResources("Patient").subscribe(results => {
console.log(results)
this.patient = results[0]
})
this.fastenApi.getResources("Immunization").subscribe(results => {
console.log(results)
this.immunizations = results
})
this.fastenApi.getResources("AllergyIntolerance").subscribe(results => {
console.log(results)
this.allergyIntolerances = results
})
}
}

View File

@ -0,0 +1,8 @@
import { FhirPathPipe } from './fhir-path.pipe';
describe('FhirPathPipe', () => {
it('create an instance', () => {
const pipe = new FhirPathPipe();
expect(pipe).toBeTruthy();
});
});

View File

@ -0,0 +1,22 @@
import { Pipe, PipeTransform } from '@angular/core';
import {ResourceFhir} from '../models/fasten/resource_fhir';
import { evaluate } from 'fhirpath'
@Pipe({
name: 'fhirPath'
})
export class FhirPathPipe implements PipeTransform {
transform(resourceFhir: ResourceFhir, ...pathQueryWithFallback: string[]): string {
for(let pathQuery of pathQueryWithFallback){
let result = evaluate(resourceFhir.resource_raw, pathQuery).join(", ")
if(result){
return result
}
}
return null
}
}

View File

@ -0,0 +1,8 @@
import { FilterPipe } from './filter.pipe';
describe('FilterPipe', () => {
it('create an instance', () => {
const pipe = new FilterPipe();
expect(pipe).toBeTruthy();
});
});

View File

@ -0,0 +1,34 @@
// import { Pipe, PipeTransform } from '@angular/core';
//
// @Pipe({
// name: 'filter',
// })
// export class FilterPipe implements PipeTransform {
// transform(items: any[], filter: Record<string, any>): any {
// if (!items || !filter) {
// return items;
// }
//
// const key = Object.keys(filter)[0];
// const value = filter[key];
//
// return items.filter((e) => e[key].indexOf(value) !== -1);
// }
// }
import {Pipe, PipeTransform} from '@angular/core';
@Pipe({
name: 'filter'
})
export class FilterPipe implements PipeTransform {
transform(items: any[], field : string, value : string): any[] {
if (!items) return null;
if (!value || value.length == 0) return items;
let filtered = items.filter(it =>
it[field].toLowerCase().indexOf(value.toLowerCase()) !=-1);
return filtered.length > 0 ? filtered : null;
}
}

View File

@ -13,6 +13,7 @@ import {MetadataSource} from '../models/fasten/metadata-source';
import {AuthService} from './auth.service';
import {GetEndpointAbsolutePath} from '../../lib/utils/endpoint_absolute_path';
import {environment} from '../../environments/environment';
import {ResourceAssociation} from '../models/fasten/resource_association';
@Injectable({
providedIn: 'root'
@ -100,7 +101,7 @@ export class FastenApiService {
);
}
getResources(sourceResourceType?: string, sourceID?: string): Observable<ResourceFhir[]> {
getResources(sourceResourceType?: string, sourceID?: string, sourceResourceID?: string, preloadRelated?: boolean): Observable<ResourceFhir[]> {
let queryParams = {}
if(sourceResourceType){
queryParams["sourceResourceType"] = sourceResourceType
@ -109,6 +110,13 @@ export class FastenApiService {
queryParams["sourceID"] = sourceID
}
if(sourceResourceID){
queryParams["sourceResourceID"] = sourceResourceID
}
if(preloadRelated){
queryParams["preloadRelated"] = "true"
}
return this._httpClient.get<any>(`${GetEndpointAbsolutePath(globalThis.location, environment.fasten_api_endpoint_base)}/secure/resource/fhir`, {params: queryParams})
.pipe(
map((response: ResponseWrapper) => {
@ -128,4 +136,14 @@ export class FastenApiService {
})
);
}
replaceResourceAssociation(resourceAssociation: ResourceAssociation): Observable<any> {
return this._httpClient.post<any>(`${GetEndpointAbsolutePath(globalThis.location, environment.fasten_api_endpoint_base)}/secure/resource/association`, resourceAssociation)
.pipe(
map((response: ResponseWrapper) => {
console.log("RESPONSE", response)
return response.data
})
);
}
}

View File

@ -129,7 +129,7 @@ ng-fullcalendar {
@include media-breakpoint-up(sm) { font-size: 22px; }
}
.fc-left {
.fc-left {
display: flex;
}
.fc-right { order: 3; }
@ -449,7 +449,7 @@ ng-fullcalendar {
.fc-list-item {
flex: 0 0 calc(80% - 5px);
max-width: calc(80% - 5px);
dispLay: flex;
display: flex;
flex-direction: column;
border-left: 4px solid transparent;
background-color: #fff;

View File

@ -56,7 +56,7 @@
.tx-spacing-4 { letter-spacing: 2px; }
.tx-spacing-5 { letter-spacing: 2.5px; }
.tx-spacing-6 { letter-spacing: 3px; }
.tx-spacing-7 { letter-spacign: 3.5px; }
.tx-spacing-7 { letter-spacing: 3.5px; }
.tx-spacing-8 { letter-spacing: 4px; }
.tx-spacing--1 { letter-spacing: -0.5px; }

View File

@ -246,5 +246,5 @@
@import '~@swimlane/ngx-datatable/index.css';
@import '~@swimlane/ngx-datatable/themes/bootstrap.css';
@import '~@swimlane/ngx-datatable/assets/icons.css';
@import '~@circlon/angular-tree-component/css/angular-tree-component.css';
@import '~highlight.js/styles/github.css';

View File

@ -1222,6 +1222,14 @@
"@babel/helper-validator-identifier" "^7.18.6"
to-fast-properties "^2.0.0"
"@circlon/angular-tree-component@^11.0.4":
version "11.0.4"
resolved "https://registry.yarnpkg.com/@circlon/angular-tree-component/-/angular-tree-component-11.0.4.tgz#7fb5ca294331d45d4d2a01cb7afc7197a45aa905"
integrity sha512-Ck86mG6Z9eWG03RiOACDzrCjuzEDXU8rcEDi0aw0+Ku62x6ZY2mx8G0VX3CLEkS1BAXM2ef6luOIcoSKAKtDaA==
dependencies:
mobx "~4.14.1"
tslib "^2.0.0"
"@colors/colors@1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
@ -1450,6 +1458,22 @@
resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b"
integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==
"@lhncbc/ucum-lhc@^4.1.3":
version "4.1.6"
resolved "https://registry.yarnpkg.com/@lhncbc/ucum-lhc/-/ucum-lhc-4.1.6.tgz#7ca11266c3b79b8ef8c41f8c7cd9989a6e020e1f"
integrity sha512-5VWDRwPZAcytx19Mh2aVbqrMSoclWPRIn65niGas5OgyIWD8nB5UbpgLQPvmV6+Z7l+a2L4E+7QIqfiimTYvfg==
dependencies:
coffeescript "^2.7.0"
csv-parse "^4.4.6"
csv-stringify "^1.0.4"
escape-html "^1.0.3"
is-integer "^1.0.6"
jsonfile "^2.2.3"
stream "0.0.2"
stream-transform "^0.1.1"
string-to-stream "^1.1.0"
xmldoc "^0.4.0"
"@ng-bootstrap/ng-bootstrap@10.0.0":
version "10.0.0"
resolved "https://registry.yarnpkg.com/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-10.0.0.tgz#6022927bac7029bdd12d7f1e10b5b20074db06dc"
@ -2317,6 +2341,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0:
dependencies:
color-convert "^2.0.1"
antlr4@~4.9.3:
version "4.9.3"
resolved "https://registry.yarnpkg.com/antlr4/-/antlr4-4.9.3.tgz#268b844ff8ce97d022399a05d4b37aa6ab4047b2"
integrity sha512-qNy2odgsa0skmNMCuxzXhM4M8J1YDaPv3TI+vCdnOAanu0N982wBrSqziDKRDctEZLZy9VffqIZXc0UGjjSP/g==
anymatch@~3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
@ -2968,6 +2997,11 @@ codelyzer@^5.1.2:
source-map "^0.5.7"
sprintf-js "^1.1.2"
coffeescript@^2.7.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/coffeescript/-/coffeescript-2.7.0.tgz#a43ec03be6885d6d1454850ea70b9409c391279c"
integrity sha512-hzWp6TUE2d/jCcN67LrW1eh5b/rSDKQK6oD6VMLlggYVUUFexgTH9z3dNYihzX4RMhze5FTUsUmOXViJKFQR/A==
color-convert@^1.9.0, color-convert@^1.9.3:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
@ -3014,7 +3048,7 @@ combined-stream@^1.0.6, combined-stream@~1.0.5, combined-stream@~1.0.6:
dependencies:
delayed-stream "~1.0.0"
commander@^2.11.0, commander@^2.12.1, commander@^2.20.0:
commander@^2.11.0, commander@^2.12.1, commander@^2.18.0, commander@^2.20.0:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
@ -3290,6 +3324,18 @@ cssesc@^3.0.0:
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
csv-parse@^4.4.6:
version "4.16.3"
resolved "https://registry.yarnpkg.com/csv-parse/-/csv-parse-4.16.3.tgz#7ca624d517212ebc520a36873c3478fa66efbaf7"
integrity sha512-cO1I/zmz4w2dcKHVvpCr7JVRu8/FymG5OEpmvsZYlccYolPBLoVGKUHgNoc4ZGkFeFlWGEDmMyBM+TTqRdW/wg==
csv-stringify@^1.0.4:
version "1.1.2"
resolved "https://registry.yarnpkg.com/csv-stringify/-/csv-stringify-1.1.2.tgz#77a41526581bce3380f12b00d7c5bbac70c82b58"
integrity sha512-3NmNhhd+AkYs5YtM1GEh01VR6PKj6qch2ayfQaltx5xpcAdThjnbbI5eT8CzRVpXfGKAxnmrSYLsNl/4f3eWiw==
dependencies:
lodash.get "~4.4.2"
custom-event@~1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"
@ -3307,6 +3353,11 @@ dashdash@^1.12.0:
dependencies:
assert-plus "^1.0.0"
date-fns@^1.30.1:
version "1.30.1"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c"
integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==
date-format@^4.0.13:
version "4.0.13"
resolved "https://registry.yarnpkg.com/date-format/-/date-format-4.0.13.tgz#87c3aab3a4f6f37582c5f5f63692d2956fa67890"
@ -3513,6 +3564,11 @@ electron-to-chromium@^1.4.251:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.256.tgz#c735032f412505e8e0482f147a8ff10cfca45bf4"
integrity sha512-x+JnqyluoJv8I0U9gVe+Sk2st8vF0CzMt78SXxuoWCooLLY2k5VerIBdpvG7ql6GKI4dzNnPjmqgDJ76EdaAKw==
emitter-component@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/emitter-component/-/emitter-component-1.1.1.tgz#065e2dbed6959bf470679edabeaf7981d1003ab6"
integrity sha512-G+mpdiAySMuB7kesVRLuyvYRqDmshB7ReKEVuyBPkzQlmiDiLrt7hHHIy4Aff552bgknVN7B2/d3lzhGO5dvpQ==
emoji-regex@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
@ -3769,7 +3825,7 @@ escalade@^3.1.1:
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
escape-html@~1.0.3:
escape-html@^1.0.3, escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==
@ -4003,6 +4059,17 @@ fhirclient@^2.5.1:
jose "^4.6.0"
js-base64 "^3.7.2"
fhirpath@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/fhirpath/-/fhirpath-3.3.0.tgz#3fc68592b297985185665e5321b7faff5ffb662a"
integrity sha512-Fl+DUSoQIRg/gh4EKsD4Nd2Y7NrY9AoIbAtDWwe+pOjlWNmwl5lX6sx8DB8j0/9UaGrg72qxBU+c5eAbmC8u1w==
dependencies:
"@lhncbc/ucum-lhc" "^4.1.3"
antlr4 "~4.9.3"
commander "^2.18.0"
date-fns "^1.30.1"
js-yaml "^3.13.1"
figures@^3.0.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af"
@ -4153,6 +4220,11 @@ function-bind@^1.1.1:
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
fuse.js@^6.6.2:
version "6.6.2"
resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-6.6.2.tgz#fe463fed4b98c0226ac3da2856a415576dc9a111"
integrity sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==
garbados-crypt@^3.0.0-beta:
version "3.0.0-beta"
resolved "https://registry.yarnpkg.com/garbados-crypt/-/garbados-crypt-3.0.0-beta.tgz#9b70b62c31b9340be5c5a55c19dc3c14300af12c"
@ -4782,6 +4854,11 @@ is-finite-x@^3.0.2:
infinity-x "^1.0.1"
is-nan-x "^1.0.2"
is-finite@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.1.0.tgz#904135c77fb42c0641d6aa1bcdbc4daa8da082f3"
integrity sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==
is-fullwidth-code-point@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
@ -4819,6 +4896,13 @@ is-index-x@^1.0.0:
to-number-x "^2.0.0"
to-string-symbols-supported-x "^1.0.0"
is-integer@^1.0.6:
version "1.0.7"
resolved "https://registry.yarnpkg.com/is-integer/-/is-integer-1.0.7.tgz#6bde81aacddf78b659b6629d629cadc51a886d5c"
integrity sha512-RPQc/s9yBHSvpi+hs9dYiJ2cuFeU6x3TyyIp8O2H6SKEltIvJOzRj9ToyvcStDvPR/pS4rxgr1oBFajQjZ2Szg==
dependencies:
is-finite "^1.0.0"
is-interactive@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e"
@ -5182,6 +5266,13 @@ jsonc-parser@3.1.0:
resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.1.0.tgz#73b8f0e5c940b83d03476bc2e51a20ef0932615d"
integrity sha512-DRf0QjnNeCUds3xTjKlQQ3DpJD51GvDjJfnxUVWg6PZTo2otSm+slzNAxU/35hF8/oJIKoG9slq30JYOsF2azg==
jsonfile@^2.2.3:
version "2.4.0"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8"
integrity sha512-PKllAqbgLgxHaj8TElYymKCAgrASebJrWpTnEkOaTowt23VKXXN0sUeriJ+eh7y6ufb/CC5ap11pz71/cM0hUw==
optionalDependencies:
graceful-fs "^4.1.6"
jsonfile@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
@ -5494,6 +5585,11 @@ lodash.debounce@^4.0.8:
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
lodash.get@~4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==
lodash.isnull@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/lodash.isnull/-/lodash.isnull-3.0.0.tgz#fafbe59ea1dca27eed786534039dd84c2e07c56e"
@ -5795,6 +5891,11 @@ mkdirp@^1.0.3, mkdirp@^1.0.4:
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
mobx@~4.14.1:
version "4.14.1"
resolved "https://registry.yarnpkg.com/mobx/-/mobx-4.14.1.tgz#0dc5622523363f6f5b467a5a0c4c99846b789748"
integrity sha512-Oyg7Sr7r78b+QPYLufJyUmxTWcqeQ96S1nmtyur3QL8SeI6e0TqcKKcxbG+sVJLWANhHQkBW/mDmgG5DDC4fdw==
moment@^2.10.2, moment@^2.29.4:
version "2.29.4"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
@ -7184,7 +7285,7 @@ readable-stream@1.1.14:
string_decoder "^1.1.1"
util-deprecate "^1.0.1"
readable-stream@^2.0.1, readable-stream@~2.3.6:
readable-stream@^2.0.1, readable-stream@^2.1.0, readable-stream@~2.3.6:
version "2.3.7"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
@ -7513,6 +7614,11 @@ sax@>=0.6.0, sax@^1.2.4, sax@~1.2.4:
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
sax@~1.1.1:
version "1.1.6"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.1.6.tgz#5d616be8a5e607d54e114afae55b7eaf2fcc3240"
integrity sha512-8zci48uUQyfqynGDSkUMD7FCJB96hwLnlZOXlgs1l3TX+LW27t3psSWKUxC0fxVgA86i8tL4NwGcY1h/6t3ESg==
schema-utils@^2.6.5:
version "2.7.1"
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7"
@ -7907,6 +8013,18 @@ str2buf@^1.3.0:
resolved "https://registry.yarnpkg.com/str2buf/-/str2buf-1.3.0.tgz#a4172afff4310e67235178e738a2dbb573abead0"
integrity sha512-xIBmHIUHYZDP4HyoXGHYNVmxlXLXDrtFHYT0eV6IOdEj3VO9ccaF1Ejl9Oq8iFjITllpT8FhaXb4KsNmw+3EuA==
stream-transform@^0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/stream-transform/-/stream-transform-0.1.2.tgz#7d8e6b4e03ac4781778f8c79517501bfb0762a9f"
integrity sha512-3HXId/0W8sktQnQM6rOZf2LuDDMbakMgAjpViLk758/h0br+iGqZFFfUxxJSqEvGvT742PyFr4v/TBXUtowdCg==
stream@0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/stream/-/stream-0.0.2.tgz#7f5363f057f6592c5595f00bc80a27f5cec1f0ef"
integrity sha512-gCq3NDI2P35B2n6t76YJuOp7d6cN/C7Rt0577l91wllh0sY9ZBuw9KaSGqH/b0hzn3CWWJbpbW0W0WvQ1H/Q7g==
dependencies:
emitter-component "^1.1.1"
streamroller@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-3.1.2.tgz#abd444560768b340f696307cf84d3f46e86c0e63"
@ -7916,6 +8034,14 @@ streamroller@^3.1.2:
debug "^4.3.4"
fs-extra "^8.1.0"
string-to-stream@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/string-to-stream/-/string-to-stream-1.1.1.tgz#aba78f73e70661b130ee3e1c0192be4fef6cb599"
integrity sha512-QySF2+3Rwq0SdO3s7BAp4x+c3qsClpPQ6abAmb0DGViiSBAkT5kL6JT2iyzEVP+T1SmzHrQD1TwlP9QAHCc+Sw==
dependencies:
inherits "^2.0.1"
readable-stream "^2.1.0"
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
@ -8845,6 +8971,13 @@ xmlbuilder@~11.0.0:
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3"
integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==
xmldoc@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/xmldoc/-/xmldoc-0.4.0.tgz#d257224be8393eaacbf837ef227fd8ec25b36888"
integrity sha512-rJ/+/UzYCSlFNuAzGuRyYgkH2G5agdX1UQn4+5siYw9pkNC3Hu/grYNDx/dqYLreeSjnY5oKg74CMBKxJHSg6Q==
dependencies:
sax "~1.1.1"
xtend@^4.0.2, xtend@~4.0.0:
version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"

2
go.mod
View File

@ -4,7 +4,7 @@ go 1.18
require (
github.com/analogj/go-util v0.0.0-20210417161720-39b497cca03b
github.com/fastenhealth/fasten-sources v0.0.3
github.com/fastenhealth/fasten-sources v0.0.4
github.com/gin-gonic/gin v1.8.1
github.com/glebarez/sqlite v1.5.0
github.com/golang-jwt/jwt/v4 v4.4.2

4
go.sum
View File

@ -72,8 +72,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fastenhealth/fasten-sources v0.0.3 h1:g07Lk59hiOWO2PhJoaP7eC0OIeIsxE50e6wH6sW96J4=
github.com/fastenhealth/fasten-sources v0.0.3/go.mod h1:f6N8wLHEPXbz1QvI+JDQzIz9u1xu0Lp/j0LxYFHkpBM=
github.com/fastenhealth/fasten-sources v0.0.4 h1:yIauhnrczFheH/DIRabmlUhmahiRGUFsNJ0z/0XsLNQ=
github.com/fastenhealth/fasten-sources v0.0.4/go.mod h1:f6N8wLHEPXbz1QvI+JDQzIz9u1xu0Lp/j0LxYFHkpBM=
github.com/fastenhealth/gofhir-models v0.0.4 h1:Q//StwNXGfK+WAS2DckGBHAP1R4cHMRZEF/sLGgmR04=
github.com/fastenhealth/gofhir-models v0.0.4/go.mod h1:xB8ikGxu3bUq2b1JYV+CZpHqBaLXpOizFR0eFBCunis=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=