Storybook Integration for Component testing. (#142)

Co-authored-by: Serinus1 <junkmayle670@yahoo.com>
This commit is contained in:
Jason Kulatunga 2023-05-05 09:06:33 -07:00 committed by GitHub
parent 2e53ce79c7
commit 2eced4fe91
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
184 changed files with 9917 additions and 932 deletions

View File

@ -25,6 +25,8 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: "Populate frontend version information"
run: "cd frontend && ./git.version.sh"
- name: Set up depot.dev multi-arch runner - name: Set up depot.dev multi-arch runner
uses: depot/setup-action@v1 uses: depot/setup-action@v1
# Login against a Docker registry except on PR # Login against a Docker registry except on PR

1
.gitignore vendored
View File

@ -60,3 +60,4 @@ test.go
/.couchdb /.couchdb
config.dev.yaml config.dev.yaml
.cache/

View File

@ -227,3 +227,9 @@ curl -X POST http://localhost:9090/api/auth/signin -H 'Content-Type: application
curl -H "Authorization: Bearer ${JWT_TOKEN_HERE}" http://localhost:5984/_session curl -H "Authorization: Bearer ${JWT_TOKEN_HERE}" http://localhost:5984/_session
``` ```
# Run Component Storybook
```bash
ng run fastenhealth:storybook
```

View File

@ -7,6 +7,7 @@
# Fasten - On Premise/Self-Hosted # Fasten - On Premise/Self-Hosted
[![CI](https://github.com/fastenhealth/fasten-onprem/actions/workflows/ci.yaml/badge.svg)](https://github.com/fastenhealth/fasten-onprem/actions/workflows/ci.yaml) [![CI](https://github.com/fastenhealth/fasten-onprem/actions/workflows/ci.yaml/badge.svg)](https://github.com/fastenhealth/fasten-onprem/actions/workflows/ci.yaml)
[![codecov](https://codecov.io/gh/fastenhealth/fasten-onprem/branch/main/graph/badge.svg?token=6O0ZUABEHT&style=flat-square)](https://codecov.io/gh/fastenhealth/fasten-onprem)
[![GitHub license](https://img.shields.io/github/license/fastenhealth/fasten-onprem?style=flat-square)](https://github.com/fastenhealth/fasten-onprem/blob/main/LICENSE.md) [![GitHub license](https://img.shields.io/github/license/fastenhealth/fasten-onprem?style=flat-square)](https://github.com/fastenhealth/fasten-onprem/blob/main/LICENSE.md)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/fastenhealth/fasten-onprem?style=flat-square)](https://github.com/fastenhealth/fasten-onprem/releases/latest) [![GitHub release (latest by date)](https://img.shields.io/github/v/release/fastenhealth/fasten-onprem?style=flat-square)](https://github.com/fastenhealth/fasten-onprem/releases/latest)
[![Discord Join](https://img.shields.io/badge/discord-join-blueviolet?style=flat-square&logo=discord)](https://discord.gg/Bykz6BAN8p) [![Discord Join](https://img.shields.io/badge/discord-join-blueviolet?style=flat-square&logo=discord)](https://discord.gg/Bykz6BAN8p)

View File

@ -301,10 +301,10 @@ func getSourcesAndSinksForGraphType(graphType pkg.ResourceGraphType) ([][]string
case pkg.ResourceGraphTypeMedicalHistory: case pkg.ResourceGraphTypeMedicalHistory:
sources = [][]string{ sources = [][]string{
{"condition", "composition"}, {"condition", "composition"},
{"encounter"}, {"encounter", "explanationofbenefit"},
} }
sinks = [][]string{ sinks = [][]string{
{"location", "device", "organization", "practitioner", "medication", "patient"}, //resources that are shared across multiple conditions {"location", "device", "organization", "practitioner", "medication", "patient", "coverage"}, //resources that are shared across multiple conditions
{"binary"}, {"binary"},
} }
sourceFlattenRelated = map[string]bool{ sourceFlattenRelated = map[string]bool{

View File

@ -39,7 +39,7 @@ func FindCodeSystem(codeSystem string) (string, error) {
if codeSystemId, ok := codeSystemIds[codeSystem]; ok { if codeSystemId, ok := codeSystemIds[codeSystem]; ok {
return codeSystemId, nil return codeSystemId, nil
} else { } else {
return "", fmt.Errorf("Code System not found") return "", fmt.Errorf("Code System not found: %s", codeSystem)
} }
} }

View File

@ -80,7 +80,7 @@ func (ae *AppEngine) Setup(logger *logrus.Entry) *gin.Engine {
ae.Logger.Warningln("***UNSAFE***") ae.Logger.Warningln("***UNSAFE***")
unsafe := api.Group("/unsafe") unsafe := api.Group("/unsafe")
{ {
//http://localhost:9090/api/raw/test@test.com/436d7277-ad56-41ce-9823-44e353d1b3f6/Patient/smart-1288992 //http://localhost:9090/api/unsafe/testuser1/3508f8cf-6eb9-4e4b-8174-dd69a493a2b4/Patient/smart-1288992
unsafe.GET("/:username/:sourceId/*path", handler.UnsafeRequestSource) unsafe.GET("/:username/:sourceId/*path", handler.UnsafeRequestSource)
unsafe.GET("/:username/graph/:graphType", handler.UnsafeResourceGraph) unsafe.GET("/:username/graph/:graphType", handler.UnsafeResourceGraph)

View File

@ -0,0 +1,17 @@
import type { StorybookConfig } from "@storybook/angular";
const config: StorybookConfig = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions",
],
framework: {
name: "@storybook/angular",
options: {},
},
docs: {
autodocs: "tag",
},
};
export default config;

View File

@ -0,0 +1,29 @@
import type { Preview } from "@storybook/angular";
import { setCompodocJson } from "@storybook/addon-docs/angular";
import docJson from "../documentation.json";
import {applicationConfig} from "@storybook/angular";
import {importProvidersFrom} from "@angular/core";
import {HttpClientModule} from "@angular/common/http";
setCompodocJson(docJson);
// see: https://github.com/storybookjs/storybook/issues/21942#issuecomment-1516177565
const decorators = [
// applicationConfig({
// providers: [importProvidersFrom(HttpClientModule)]
// })
];
const preview: Preview = {
parameters: {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
decorators: decorators
};
export default preview;

View File

@ -0,0 +1,11 @@
{
"extends": "../tsconfig.app.json",
"compilerOptions": {
"types": ["node"],
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true
},
"exclude": ["../src/test.ts", "../src/**/*.spec.ts"],
"include": ["../src/**/*", "./preview.ts"],
"files": ["./typings.d.ts"]
}

4
frontend/.storybook/typings.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
declare module '*.md' {
const content: string;
export default content;
}

View File

@ -23,7 +23,10 @@
"main": "src/main.ts", "main": "src/main.ts",
"polyfills": "src/polyfills.ts", "polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json", "tsConfig": "tsconfig.app.json",
"allowedCommonJsDependencies": ["chart.js", "highlight.js"], "allowedCommonJsDependencies": [
"chart.js",
"highlight.js"
],
"aot": true, "aot": true,
"assets": [ "assets": [
"src/favicon.ico", "src/favicon.ico",
@ -32,14 +35,13 @@
"glob": "**/*", "glob": "**/*",
"input": "./node_modules/dwv/decoders/", "input": "./node_modules/dwv/decoders/",
"output": "/assets/dwv/decoders/" "output": "/assets/dwv/decoders/"
}, }
], ],
"styles": [ "styles": [
"src/styles.scss" "src/styles.scss"
], ],
"scripts": [ "scripts": [
"node_modules/@panva/oauth4webapi/build/index.js", "node_modules/@panva/oauth4webapi/build/index.js"
] ]
}, },
"configurations": { "configurations": {
@ -125,7 +127,6 @@
} }
] ]
} }
} }
}, },
"serve": { "serve": {
@ -193,6 +194,43 @@
"devServerTarget": "fastenhealth:serve:prod" "devServerTarget": "fastenhealth:serve:prod"
} }
} }
},
"storybook": {
"builder": "@storybook/angular:start-storybook",
"options": {
"configDir": ".storybook",
"browserTarget": "fastenhealth:build",
"compodoc": true,
"compodocArgs": [
"-e",
"json",
"-d",
"."
],
"assets": [
{
"glob": "**/*",
"input": "./node_modules/dwv/decoders/",
"output": "/assets/dwv/decoders/"
}
],
"port": 6006
}
},
"build-storybook": {
"builder": "@storybook/angular:build-storybook",
"options": {
"configDir": ".storybook",
"browserTarget": "fastenhealth:build",
"compodoc": true,
"compodocArgs": [
"-e",
"json",
"-d",
"."
],
"outputDir": "storybook-static"
}
} }
} }
} }

29
frontend/git.version.sh Executable file
View File

@ -0,0 +1,29 @@
#!/usr/bin/env bash
if [[ -z "${CI}" ]]; then
echo "running locally (not in Github Actions). generating version file from git client"
GIT_TAG=`git describe --tags`
GIT_BRANCH=`git rev-parse --abbrev-ref HEAD`
if [[ "$GIT_BRANCH" == "main" ]]; then
VERSION_INFO="${GIT_TAG}"
else
VERSION_INFO="${GIT_BRANCH}#${GIT_TAG}"
fi
else
echo "running in Github Actions, generating version file from environmental variables"
# https://docs.github.com/en/actions/learn-github-actions/environment-variables
VERSION_INFO="${GITHUB_REF_NAME}"
if [[ "$GITHUB_REF_TYPE" == "branch" ]]; then
VERSION_INFO="${VERSION_INFO}#${GITHUB_SHA::7}"
fi
fi
echo "writing version file (version: ${VERSION_INFO})"
cat <<EOT > src/environments/versions.ts
// this file is automatically generated by git.version.ts script
export const versionInfo = {
version: '${VERSION_INFO}',
};
EOT

View File

@ -9,7 +9,9 @@
"test": "ng test", "test": "ng test",
"lint": "ng lint", "lint": "ng lint",
"e2e": "ng e2e", "e2e": "ng e2e",
"dist": "ng build --watch --output-path=../dist" "dist": "ng build --watch --output-path=../dist",
"storybook": "ng run fastenhealth:storybook",
"build-storybook": "ng run fastenhealth:build-storybook"
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
@ -38,6 +40,7 @@
"chart.js": "2.9.4", "chart.js": "2.9.4",
"dwv": "^0.31.0", "dwv": "^0.31.0",
"fhirpath": "^3.3.0", "fhirpath": "^3.3.0",
"gridstack": "7.3.0",
"humanize-duration": "^3.27.3", "humanize-duration": "^3.27.3",
"idb": "^7.1.0", "idb": "^7.1.0",
"jose": "^4.10.4", "jose": "^4.10.4",
@ -58,6 +61,13 @@
"@angular/cli": "^14.1.3", "@angular/cli": "^14.1.3",
"@angular/compiler-cli": "^14.1.3", "@angular/compiler-cli": "^14.1.3",
"@angular/language-service": "^14.1.3", "@angular/language-service": "^14.1.3",
"@compodoc/compodoc": "^1.1.19",
"@storybook/addon-essentials": "^7.0.7",
"@storybook/addon-interactions": "^7.0.7",
"@storybook/addon-links": "^7.0.7",
"@storybook/angular": "^7.0.7",
"@storybook/blocks": "^7.0.7",
"@storybook/testing-library": "^0.0.14-next.2",
"@types/jasmine": "~3.5.0", "@types/jasmine": "~3.5.0",
"@types/jasminewd2": "~2.0.3", "@types/jasminewd2": "~2.0.3",
"codelyzer": "^5.1.2", "codelyzer": "^5.1.2",
@ -70,8 +80,14 @@
"karma-jasmine": "~4.0.0", "karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0", "karma-jasmine-html-reporter": "^1.5.0",
"protractor": "~7.0.0", "protractor": "~7.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"storybook": "^7.0.7",
"ts-node": "~8.3.0", "ts-node": "~8.3.0",
"tslint": "~6.1.0", "tslint": "~6.1.0",
"typescript": "~4.6.4" "typescript": "~4.6.4"
},
"resolutions": {
"webpack": "5.74.0"
} }
} }

View File

@ -2,7 +2,7 @@ import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module'; import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import {HttpClientModule, HTTP_INTERCEPTORS, HttpClient} from '@angular/common/http';
import { NgbModule } from "@ng-bootstrap/ng-bootstrap"; import { NgbModule } from "@ng-bootstrap/ng-bootstrap";
import { HeaderComponent } from './components/header/header.component'; import { HeaderComponent } from './components/header/header.component';
import { FooterComponent } from './components/footer/footer.component'; import { FooterComponent } from './components/footer/footer.component';
@ -32,6 +32,8 @@ import {PipesModule} from './pipes/pipes.module';
import { ResourceCreatorComponent } from './pages/resource-creator/resource-creator.component'; import { ResourceCreatorComponent } from './pages/resource-creator/resource-creator.component';
import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { NgSelectModule } from '@ng-select/ng-select'; import { NgSelectModule } from '@ng-select/ng-select';
import {HTTP_CLIENT_TOKEN} from "./dependency-injection";
import {WidgetsModule} from './widgets/widgets.module';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -64,9 +66,14 @@ import { NgSelectModule } from '@ng-select/ng-select';
MomentModule, MomentModule,
PipesModule, PipesModule,
InfiniteScrollModule, InfiniteScrollModule,
NgSelectModule NgSelectModule,
WidgetsModule
], ],
providers: [ providers: [
{
provide: HTTP_CLIENT_TOKEN,
useClass: HttpClient,
},
{ {
provide: HTTP_INTERCEPTORS, provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptorService, useClass: AuthInterceptorService,

View File

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

View File

@ -1,6 +1,6 @@
import {Component, Input, OnInit} from '@angular/core'; import {Component, Input, OnInit} from '@angular/core';
@Component({ @Component({
standalone: true,
selector: 'fhir-ui-badge', selector: 'fhir-ui-badge',
templateUrl: './badge.component.html', templateUrl: './badge.component.html',
styleUrls: ['./badge.component.scss'] styleUrls: ['./badge.component.scss']

View File

@ -0,0 +1,45 @@
import type { Meta, StoryObj } from '@storybook/angular';
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',
component: BadgeComponent,
decorators: [
// moduleMetadata({
// imports: [AppModule]
// })
// applicationConfig({
// providers: [importProvidersFrom(AppModule)],
// }),
],
tags: ['autodocs'],
render: (args: BadgeComponent) => ({
props: {
backgroundColor: null,
...args,
},
}),
argTypes: {
status: {
control: 'text',
},
},
};
export default meta;
type Story = StoryObj<BadgeComponent>;
// More on writing stories with args: https://storybook.js.org/docs/angular/writing-stories/args
export const Primary: Story = {
args: {
status: "active",
}
};
export const Secondary: Story = {
args: {
status: 'refuted'
},
};

View File

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

View File

@ -1,9 +1,14 @@
import {Component, Input, OnInit} from '@angular/core'; import {Component, Input, OnInit} from '@angular/core';
import {TableRowItem} from './table-row-item'; import {TableRowItem} from './table-row-item';
import {BinaryModel} from '../../../../../lib/models/resources/binary-model';
import {FastenDisplayModel} from '../../../../../lib/models/fasten/fasten-display-model'; import {FastenDisplayModel} from '../../../../../lib/models/fasten/fasten-display-model';
import {CommonModule} from "@angular/common";
import {CodingComponent} from "../../datatypes/coding/coding.component";
import {Router, RouterModule} from "@angular/router";
@Component({ @Component({
standalone: true,
imports: [CommonModule, CodingComponent, RouterModule],
providers: [RouterModule],
selector: 'fhir-ui-table', selector: 'fhir-ui-table',
templateUrl: './table.component.html', templateUrl: './table.component.html',
styleUrls: ['./table.component.scss'] styleUrls: ['./table.component.scss']

View File

@ -0,0 +1,92 @@
import type { Meta, StoryObj } from '@storybook/angular';
import {TableComponent} from "./table.component";
import {TableRowItem} from "./table-row-item";
import {FastenDisplayModel} from "../../../../../lib/models/fasten/fasten-display-model";
// 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',
component: TableComponent,
decorators: [
// moduleMetadata({
// imports: [AppModule]
// })
// applicationConfig({
// providers: [importProvidersFrom(AppModule)],
// }),
],
tags: ['autodocs'],
render: (args: TableComponent) => ({
props: {
backgroundColor: null,
...args,
},
}),
argTypes: {
},
};
export default meta;
type Story = StoryObj<TableComponent>;
// More on writing stories with args: https://storybook.js.org/docs/angular/writing-stories/args
export const String: Story = {
args: {
tableData: [
{
enabled: true,
label: 'hello',
data: 'world'
},
{
enabled: true,
label: 'hello',
data: 'world'
},
{
enabled: true,
label: 'hello',
data: 'world'
},
],
}
};
export const Ref: Story = {
args: {
displayModel: {
source_id: '123-456-789',
} as FastenDisplayModel,
tableData: [
{
enabled: true,
label: 'hello',
data_type: 'reference',
data: {
reference: 'Patient/123',
display: 'John Doe'
}
},
{
enabled: true,
label: 'hello',
data_type: 'reference',
data: {
reference: 'Patient/123',
display: 'John Doe'
}
},
{
enabled: true,
label: 'hello',
data_type: 'reference',
data: {
reference: 'Patient/123',
display: 'John Doe'
}
}
] as TableRowItem[]
},
};

View File

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

View File

@ -1,7 +1,10 @@
import {Component, Input, OnInit} from '@angular/core'; import {Component, Input, OnInit} from '@angular/core';
import {BinaryModel} from '../../../../../lib/models/resources/binary-model'; import {BinaryModel} from '../../../../../lib/models/resources/binary-model';
import {CommonModule} from "@angular/common";
@Component({ @Component({
standalone: true,
imports: [CommonModule],
selector: 'fhir-binary-text', selector: 'fhir-binary-text',
templateUrl: './binary-text.component.html', templateUrl: './binary-text.component.html',
styleUrls: ['./binary-text.component.scss'] styleUrls: ['./binary-text.component.scss']

View File

@ -0,0 +1,47 @@
import type { Meta, StoryObj } from '@storybook/angular';
import {fhirVersions} from "../../../../../lib/models/constants";
import R4Example1Json from "../../../../../lib/fixtures/r4/resources/binary/exampleText.json";
import {BinaryTextComponent} from "./binary-text.component";
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',
component: BinaryTextComponent,
decorators: [
// moduleMetadata({
// imports: [AppModule]
// })
// applicationConfig({
// providers: [importProvidersFrom(AppModule)],
// }),
],
tags: ['autodocs'],
render: (args: BinaryTextComponent) => ({
props: {
backgroundColor: null,
...args,
},
}),
argTypes: {
displayModel: {
control: 'object',
},
},
};
export default meta;
type Story = StoryObj<BinaryTextComponent>;
// 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

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

View File

@ -1,7 +1,10 @@
import {Component, Input, OnInit} from '@angular/core'; import {Component, Input, OnInit} from '@angular/core';
import {CodingModel} from '../../../../../lib/models/datatypes/coding-model'; import {CodingModel} from '../../../../../lib/models/datatypes/coding-model';
import {CommonModule} from "@angular/common";
@Component({ @Component({
standalone: true,
imports: [CommonModule],
selector: 'fhir-coding', selector: 'fhir-coding',
templateUrl: './coding.component.html', templateUrl: './coding.component.html',
styleUrls: ['./coding.component.scss'] styleUrls: ['./coding.component.scss']

View File

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

View File

@ -1,8 +1,9 @@
import {Component, Input, OnInit, TemplateRef} from '@angular/core'; import {Component, Input, OnInit, TemplateRef} from '@angular/core';
import * as dwv from 'dwv'; import * as dwv from 'dwv';
import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; import {NgbModal, NgbModalModule, NgbPaginationModule, NgbTooltipModule} from '@ng-bootstrap/ng-bootstrap';
import { VERSION } from '@angular/core'; import { VERSION } from '@angular/core';
import {BinaryModel} from '../../../../../lib/models/resources/binary-model'; import {BinaryModel} from '../../../../../lib/models/resources/binary-model';
import {CommonModule} from "@angular/common";
// Copied from https://raw.githubusercontent.com/ivmartel/dwv-angular/master/src/app/dwv/dwv.component.ts // Copied from https://raw.githubusercontent.com/ivmartel/dwv-angular/master/src/app/dwv/dwv.component.ts
// gui overrides // gui overrides
@ -17,6 +18,9 @@ dwv.image.decoderScripts = {
@Component({ @Component({
standalone: true,
imports: [CommonModule, NgbModalModule, NgbTooltipModule, NgbPaginationModule],
providers: [NgbModalModule],
selector: 'fhir-dicom', selector: 'fhir-dicom',
templateUrl: './dicom.component.html', templateUrl: './dicom.component.html',
styleUrls: ['./dicom.component.scss'] styleUrls: ['./dicom.component.scss']

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/exampleDicom.json";
import {BinaryModel} from "../../../../../lib/models/resources/binary-model";
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',
component: DicomComponent,
decorators: [
// moduleMetadata({
// imports: [AppModule]
// })
// applicationConfig({
// providers: [importProvidersFrom(AppModule)],
// }),
],
tags: ['autodocs'],
render: (args: DicomComponent) => ({
props: {
backgroundColor: null,
...args,
},
}),
argTypes: {
displayModel: {
control: 'object',
},
},
};
export default meta;
type Story = StoryObj<DicomComponent>;
// 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

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

View File

@ -1,8 +1,11 @@
import {Component, Input, OnInit} from '@angular/core'; import {Component, Input, OnInit} from '@angular/core';
import {BinaryModel} from '../../../../../lib/models/resources/binary-model'; import {BinaryModel} from '../../../../../lib/models/resources/binary-model';
import {DomSanitizer, SafeHtml} from '@angular/platform-browser'; import {DomSanitizer, SafeHtml} from '@angular/platform-browser';
import {CommonModule} from "@angular/common";
@Component({ @Component({
standalone: true,
imports: [CommonModule],
selector: 'fhir-html', selector: 'fhir-html',
templateUrl: './html.component.html', templateUrl: './html.component.html',
styleUrls: ['./html.component.scss'] styleUrls: ['./html.component.scss']
@ -13,6 +16,7 @@ export class HtmlComponent implements OnInit {
constructor(private sanitized: DomSanitizer) { } constructor(private sanitized: DomSanitizer) { }
ngOnInit(): void { ngOnInit(): void {
//TODO: safely display html content
this.contentMarkup = this.sanitized.bypassSecurityTrustHtml(this.displayModel?.content); this.contentMarkup = this.sanitized.bypassSecurityTrustHtml(this.displayModel?.content);
} }

View File

@ -0,0 +1,47 @@
import type { Meta, StoryObj } from '@storybook/angular';
import {fhirVersions} from "../../../../../lib/models/constants";
import R4Example1Json from "../../../../../lib/fixtures/r4/resources/binary/exampleHtml.json";
import {BinaryModel} from "../../../../../lib/models/resources/binary-model";
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',
component: HtmlComponent,
decorators: [
// moduleMetadata({
// imports: [AppModule]
// })
// applicationConfig({
// providers: [importProvidersFrom(AppModule)],
// }),
],
tags: ['autodocs'],
render: (args: HtmlComponent) => ({
props: {
backgroundColor: null,
...args,
},
}),
argTypes: {
displayModel: {
control: 'object',
},
},
};
export default meta;
type Story = StoryObj<HtmlComponent>;
// 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

@ -1,3 +1,3 @@
<div> <div>
<img src="data:${{displayModel?.content_type}};base64,${{displayModel?.data}}" alt=""/> <img src="data:{{displayModel?.content_type}};base64,{{displayModel?.data}}" alt=""/>
</div> </div>

View File

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

View File

@ -1,7 +1,10 @@
import {Component, Input, OnInit} from '@angular/core'; import {Component, Input, OnInit} from '@angular/core';
import {BinaryModel} from '../../../../../lib/models/resources/binary-model'; import {BinaryModel} from '../../../../../lib/models/resources/binary-model';
import {CommonModule} from "@angular/common";
@Component({ @Component({
standalone: true,
imports: [CommonModule],
selector: 'fhir-img', selector: 'fhir-img',
templateUrl: './img.component.html', templateUrl: './img.component.html',
styleUrls: ['./img.component.scss'] styleUrls: ['./img.component.scss']

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/exampleJpeg.json";
import {BinaryModel} from "../../../../../lib/models/resources/binary-model";
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',
component: ImgComponent,
decorators: [
// moduleMetadata({
// imports: [AppModule]
// })
// applicationConfig({
// providers: [importProvidersFrom(AppModule)],
// }),
],
tags: ['autodocs'],
render: (args: ImgComponent) => ({
props: {
backgroundColor: null,
...args,
},
}),
argTypes: {
displayModel: {
control: 'object',
},
},
};
export default meta;
type Story = StoryObj<ImgComponent>;
// 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

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

View File

@ -1,7 +1,10 @@
import {Component, Input, OnInit} from '@angular/core'; import {Component, Input, OnInit} from '@angular/core';
import {BinaryModel} from '../../../../../lib/models/resources/binary-model'; import {BinaryModel} from '../../../../../lib/models/resources/binary-model';
import {CommonModule} from "@angular/common";
@Component({ @Component({
standalone: true,
imports: [CommonModule],
selector: 'fhir-markdown', selector: 'fhir-markdown',
templateUrl: './markdown.component.html', templateUrl: './markdown.component.html',
styleUrls: ['./markdown.component.scss'] styleUrls: ['./markdown.component.scss']

View File

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

View File

@ -1,8 +1,11 @@
import {Component, Input, OnInit} from '@angular/core'; import {Component, Input, OnInit} from '@angular/core';
import {BinaryModel} from '../../../../../lib/models/resources/binary-model'; import {BinaryModel} from '../../../../../lib/models/resources/binary-model';
import {DomSanitizer, SafeUrl} from '@angular/platform-browser'; import {DomSanitizer, SafeUrl} from '@angular/platform-browser';
import {CommonModule} from "@angular/common";
@Component({ @Component({
standalone: true,
imports: [CommonModule],
selector: 'fhir-pdf', selector: 'fhir-pdf',
templateUrl: './pdf.component.html', templateUrl: './pdf.component.html',
styleUrls: ['./pdf.component.scss'] styleUrls: ['./pdf.component.scss']

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/examplePdf.json";
import {BinaryModel} from "../../../../../lib/models/resources/binary-model";
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',
component: PdfComponent,
decorators: [
// moduleMetadata({
// imports: [AppModule]
// })
// applicationConfig({
// providers: [importProvidersFrom(AppModule)],
// }),
],
tags: ['autodocs'],
render: (args: PdfComponent) => ({
props: {
backgroundColor: null,
...args,
},
}),
argTypes: {
displayModel: {
control: 'object',
},
},
};
export default meta;
type Story = StoryObj<PdfComponent>;
// 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

@ -4,5 +4,10 @@ import {FastenDisplayModel} from '../../../../lib/models/fasten/fasten-display-m
export interface FhirResourceComponentInterface { export interface FhirResourceComponentInterface {
displayModel: FastenDisplayModel; displayModel: FastenDisplayModel;
showDetails: boolean; showDetails: boolean;
//these are used to populate the description of the resource. May not be available for all resources
resourceCode?: string;
resourceCodeSystem?: string;
markForCheck() markForCheck()
} }

View File

@ -28,7 +28,7 @@ import {MediaComponent} from '../resources/media/media.component';
@Component({ @Component({
selector: 'fhir-resource', selector: 'fhir-resource',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.Default,
templateUrl: './fhir-resource.component.html', templateUrl: './fhir-resource.component.html',
styleUrls: ['./fhir-resource.component.scss'] styleUrls: ['./fhir-resource.component.scss']
}) })

View File

@ -2,6 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AllergyIntoleranceComponent } from './allergy-intolerance.component'; import { AllergyIntoleranceComponent } from './allergy-intolerance.component';
import {NgbCollapseModule} from '@ng-bootstrap/ng-bootstrap'; import {NgbCollapseModule} from '@ng-bootstrap/ng-bootstrap';
import {AuthService} from '../../../../services/auth.service';
describe('AllergyIntoleranceComponent', () => { describe('AllergyIntoleranceComponent', () => {
let component: AllergyIntoleranceComponent; let component: AllergyIntoleranceComponent;
@ -9,8 +10,8 @@ describe('AllergyIntoleranceComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ AllergyIntoleranceComponent ], imports: [AllergyIntoleranceComponent, NgbCollapseModule],
imports: [NgbCollapseModule] providers: [AuthService]
}) })
.compileComponents(); .compileComponents();

View File

@ -3,8 +3,14 @@ import {FhirResourceComponentInterface} from '../../fhir-resource/fhir-resource-
import {TableRowItem, TableRowItemDataType} from '../../common/table/table-row-item'; import {TableRowItem, TableRowItemDataType} from '../../common/table/table-row-item';
import {Router} from '@angular/router'; import {Router} from '@angular/router';
import {AllergyIntoleranceModel} from '../../../../../lib/models/resources/allergy-intolerance-model'; import {AllergyIntoleranceModel} from '../../../../../lib/models/resources/allergy-intolerance-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";
@Component({ @Component({
standalone: true,
imports: [NgbCollapseModule, CommonModule, BadgeComponent, TableComponent],
selector: 'fhir-allergy-intolerance', selector: 'fhir-allergy-intolerance',
templateUrl: './allergy-intolerance.component.html', templateUrl: './allergy-intolerance.component.html',
styleUrls: ['./allergy-intolerance.component.scss'] styleUrls: ['./allergy-intolerance.component.scss']

View File

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

View File

@ -4,6 +4,9 @@ import { BinaryComponent } from './binary.component';
import {NgbCollapseModule} from '@ng-bootstrap/ng-bootstrap'; import {NgbCollapseModule} from '@ng-bootstrap/ng-bootstrap';
import {FastenApiService} from '../../../../services/fasten-api.service'; import {FastenApiService} from '../../../../services/fasten-api.service';
import {RouterTestingModule} from '@angular/router/testing'; import {RouterTestingModule} from '@angular/router/testing';
import {HTTP_CLIENT_TOKEN} from '../../../../dependency-injection';
import {HttpClient} from '@angular/common/http';
import {HttpClientTestingModule} from '@angular/common/http/testing';
describe('BinaryComponent', () => { describe('BinaryComponent', () => {
let component: BinaryComponent; let component: BinaryComponent;
@ -14,12 +17,17 @@ describe('BinaryComponent', () => {
mockedFastenApiService = jasmine.createSpyObj('FastenApiService', ['getBinaryModel']) mockedFastenApiService = jasmine.createSpyObj('FastenApiService', ['getBinaryModel'])
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ BinaryComponent ], imports: [HttpClientTestingModule, BinaryComponent, NgbCollapseModule, RouterTestingModule],
imports: [NgbCollapseModule, RouterTestingModule], providers: [
providers: [{ {
provide: FastenApiService, provide: FastenApiService,
useValue: mockedFastenApiService useValue: mockedFastenApiService
}] },
{
provide: HTTP_CLIENT_TOKEN,
useClass: HttpClient,
},
]
}) })
.compileComponents(); .compileComponents();

View File

@ -4,8 +4,34 @@ import {FhirResourceComponentInterface} from '../../fhir-resource/fhir-resource-
import {Router} from '@angular/router'; import {Router} from '@angular/router';
import {AttachmentModel} from '../../../../../lib/models/datatypes/attachment-model'; import {AttachmentModel} from '../../../../../lib/models/datatypes/attachment-model';
import {FastenApiService} from '../../../../services/fasten-api.service'; import {FastenApiService} from '../../../../services/fasten-api.service';
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 {PdfComponent} from "../../datatypes/pdf/pdf.component";
import {ImgComponent} from "../../datatypes/img/img.component";
import {HtmlComponent} from "../../datatypes/html/html.component";
import {MarkdownComponent} from "../../datatypes/markdown/markdown.component";
import {BinaryTextComponent} from "../../datatypes/binary-text/binary-text.component";
import {DicomComponent} from "../../datatypes/dicom/dicom.component";
import {HighlightModule} from "ngx-highlightjs";
import {HttpClient, HttpClientModule} from "@angular/common/http";
import {AuthService} from "../../../../services/auth.service";
@Component({ @Component({
standalone: true,
imports: [
NgbCollapseModule,
CommonModule,
PdfComponent,
ImgComponent,
HtmlComponent,
MarkdownComponent,
BinaryTextComponent,
DicomComponent,
HighlightModule,
],
providers: [FastenApiService, AuthService],
selector: 'fhir-binary', selector: 'fhir-binary',
templateUrl: './binary.component.html', templateUrl: './binary.component.html',
styleUrls: ['./binary.component.scss'] styleUrls: ['./binary.component.scss']

View File

@ -0,0 +1,106 @@
import type { Meta, StoryObj } from '@storybook/angular';
import {fhirVersions} from "../../../../../lib/models/constants";
import R4ExampleJpegJson from "../../../../../lib/fixtures/r4/resources/binary/exampleJpeg.json";
import R4ExampleHtmlJson from "../../../../../lib/fixtures/r4/resources/binary/exampleHtml.json";
import R4ExampleDicomJson from "../../../../../lib/fixtures/r4/resources/binary/exampleDicom.json";
import R4ExamplePdfJson from "../../../../../lib/fixtures/r4/resources/binary/examplePdf.json";
import R4ExampleTextJson from "../../../../../lib/fixtures/r4/resources/binary/exampleText.json";
import R4ExampleXmlJson from "../../../../../lib/fixtures/r4/resources/binary/exampleXml.json";
import {BinaryComponent} from "./binary.component";
import {BinaryModel} from "../../../../../lib/models/resources/binary-model";
import {moduleMetadata} from "@storybook/angular";
import {BrowserModule} from "@angular/platform-browser";
import {HttpClient, HttpClientModule} from "@angular/common/http";
import {CommonModule} from "@angular/common";
// More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction
const meta: Meta<BinaryComponent> = {
title: 'Fhir/Binary',
component: BinaryComponent,
decorators: [
moduleMetadata({
imports: [CommonModule, HttpClientModule],
providers: [{ provide: HttpClient, useClass: HttpClient }],
}),
// applicationConfig({
// providers: [importProvidersFrom(AppModule)],
// }),
],
tags: ['autodocs'],
render: (args: BinaryComponent) => ({
props: {
backgroundColor: null,
...args,
},
}),
argTypes: {
displayModel: {
control: 'object',
},
showDetails: {
control: 'boolean',
}
},
};
export default meta;
type Story = StoryObj<BinaryComponent>;
// More on writing stories with args: https://storybook.js.org/docs/angular/writing-stories/args
let r4ExampleJpegDisplayModel = new BinaryModel(R4ExampleJpegJson, fhirVersions.R4)
r4ExampleJpegDisplayModel.source_id = '123-456-789'
r4ExampleJpegDisplayModel.source_resource_id = '123-456-789'
export const R4ExampleJpeg: Story = {
args: {
displayModel: r4ExampleJpegDisplayModel
}
};
let r4ExampleHtmlDisplayModel = new BinaryModel(R4ExampleHtmlJson, fhirVersions.R4)
r4ExampleHtmlDisplayModel.source_id = '123-456-789'
r4ExampleHtmlDisplayModel.source_resource_id = '123-456-789'
export const R4ExampleHtml: Story = {
args: {
displayModel: r4ExampleHtmlDisplayModel
}
};
let r4ExampleDicomDisplayModel = new BinaryModel(R4ExampleDicomJson, fhirVersions.R4)
r4ExampleDicomDisplayModel.source_id = '123-456-789'
r4ExampleDicomDisplayModel.source_resource_id = '123-456-789'
export const R4ExampleDicom: Story = {
args: {
displayModel: r4ExampleDicomDisplayModel
}
};
let r4ExamplePdfDisplayModel = new BinaryModel(R4ExamplePdfJson, fhirVersions.R4)
r4ExamplePdfDisplayModel.source_id = '123-456-789'
r4ExamplePdfDisplayModel.source_resource_id = '123-456-789'
export const R4ExamplePdf: Story = {
args: {
displayModel: r4ExamplePdfDisplayModel
}
};
let r4ExampleTextDisplayModel = new BinaryModel(R4ExampleTextJson, fhirVersions.R4)
r4ExampleTextDisplayModel.source_id = '123-456-789'
r4ExampleTextDisplayModel.source_resource_id = '123-456-789'
export const R4ExampleText: Story = {
args: {
displayModel: r4ExampleTextDisplayModel
}
};
let r4ExampleXmlDisplayModel = new BinaryModel(R4ExampleXmlJson, fhirVersions.R4)
r4ExampleXmlDisplayModel.source_id = '123-456-789'
r4ExampleXmlDisplayModel.source_resource_id = '123-456-789'
export const R4ExampleXml: Story = {
args: {
displayModel: r4ExampleXmlDisplayModel
}
};

View File

@ -12,10 +12,14 @@
<!-- </div>--> <!-- </div>-->
</div> </div>
<div #collapse="ngbCollapse" [(ngbCollapse)]="isCollapsed" class="card-body"> <div #collapse="ngbCollapse" [(ngbCollapse)]="isCollapsed" class="card-body">
<p class="az-content-text mg-b-20">An action that is or was performed on or for a patient, practitioner, device, organization, or location. For example, this can be a physical intervention on a patient like an operation, or less invasive like long term services, counseling, or hypnotherapy.</p> <p class="az-content-text mg-b-20" *ngIf="!(resourceCode && resourceCodeSystem); else lookupCode">An action that is or was performed on or for a patient, practitioner, device, organization, or location. For example, this can be a physical intervention on a patient like an operation, or less invasive like long term services, counseling, or hypnotherapy.</p>
<fhir-ui-table [displayModel]="displayModel" [tableData]="tableData"></fhir-ui-table> <fhir-ui-table [displayModel]="displayModel" [tableData]="tableData"></fhir-ui-table>
</div> </div>
<div *ngIf="showDetails" class="card-footer"> <div *ngIf="showDetails" class="card-footer">
<a class="float-right" routerLink="/source/{{displayModel?.source_id}}/resource/{{displayModel?.source_resource_id}}">details</a> <a class="float-right" routerLink="/source/{{displayModel?.source_id}}/resource/{{displayModel?.source_resource_id}}">details</a>
</div> </div>
</div> </div>
<ng-template #lookupCode>
<app-glossary-lookup class="az-content-text mg-b-20" [code]="resourceCode" [codeSystem]="resourceCodeSystem"></app-glossary-lookup>
</ng-template>

View File

@ -12,6 +12,10 @@ import {DiagnosticReportModel} from '../../../../../lib/models/resources/diagnos
export class DiagnosticReportComponent implements OnInit, FhirResourceComponentInterface { export class DiagnosticReportComponent implements OnInit, FhirResourceComponentInterface {
@Input() displayModel: DiagnosticReportModel @Input() displayModel: DiagnosticReportModel
@Input() showDetails: boolean = true @Input() showDetails: boolean = true
//these are used to populate the description of the resource. May not be available for all resources
resourceCode?: string;
resourceCodeSystem?: string;
isCollapsed: boolean = false isCollapsed: boolean = false
tableData: TableRowItem[] = [] tableData: TableRowItem[] = []
@ -19,6 +23,9 @@ export class DiagnosticReportComponent implements OnInit, FhirResourceComponentI
ngOnInit(): void { ngOnInit(): void {
this.resourceCode = this.displayModel?.code_coding?.[0]?.code
this.resourceCodeSystem = this.displayModel?.code_coding?.[0]?.system
this.tableData = [ this.tableData = [
{ {
label: 'Issued', label: 'Issued',

View File

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

View File

@ -4,8 +4,14 @@ import {Router} from '@angular/router';
import {ImmunizationModel} from '../../../../../lib/models/resources/immunization-model'; import {ImmunizationModel} from '../../../../../lib/models/resources/immunization-model';
import {TableRowItem, TableRowItemDataType} from '../../common/table/table-row-item'; import {TableRowItem, TableRowItemDataType} from '../../common/table/table-row-item';
import * as _ from "lodash"; import * as _ from "lodash";
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";
@Component({ @Component({
standalone: true,
imports: [NgbCollapseModule, CommonModule, BadgeComponent, TableComponent],
selector: 'fhir-immunization', selector: 'fhir-immunization',
templateUrl: './immunization.component.html', templateUrl: './immunization.component.html',
styleUrls: ['./immunization.component.scss'] styleUrls: ['./immunization.component.scss']

View File

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

View File

@ -12,10 +12,14 @@
<!-- </div>--> <!-- </div>-->
</div> </div>
<div #collapse="ngbCollapse" [(ngbCollapse)]="isCollapsed" class="card-body"> <div #collapse="ngbCollapse" [(ngbCollapse)]="isCollapsed" class="card-body">
<p class="az-content-text mg-b-20">An order or request for both supply of the medication and the instructions for administration of the medication to a patient.</p> <p class="az-content-text mg-b-20" *ngIf="!(resourceCode && resourceCodeSystem); else lookupCode">An order or request for both supply of the medication and the instructions for administration of the medication to a patient.</p>
<fhir-ui-table [displayModel]="displayModel" [tableData]="tableData"></fhir-ui-table> <fhir-ui-table [displayModel]="displayModel" [tableData]="tableData"></fhir-ui-table>
</div> </div>
<div *ngIf="showDetails" class="card-footer"> <div *ngIf="showDetails" class="card-footer">
<a class="float-right" routerLink="/source/{{displayModel?.source_id}}/resource/{{displayModel?.source_resource_id}}">details</a> <a class="float-right" routerLink="/source/{{displayModel?.source_id}}/resource/{{displayModel?.source_resource_id}}">details</a>
</div> </div>
</div> </div>
<ng-template #lookupCode>
<app-glossary-lookup class="az-content-text mg-b-20" [code]="resourceCode" [codeSystem]="resourceCodeSystem"></app-glossary-lookup>
</ng-template>

View File

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

View File

@ -3,8 +3,14 @@ import {FhirResourceComponentInterface} from '../../fhir-resource/fhir-resource-
import {TableRowItem, TableRowItemDataType} from '../../common/table/table-row-item'; import {TableRowItem, TableRowItemDataType} from '../../common/table/table-row-item';
import {Router} from '@angular/router'; import {Router} from '@angular/router';
import {MedicationRequestModel} from '../../../../../lib/models/resources/medication-request-model'; import {MedicationRequestModel} from '../../../../../lib/models/resources/medication-request-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";
@Component({ @Component({
standalone: true,
imports: [NgbCollapseModule, CommonModule, BadgeComponent, TableComponent],
selector: 'fhir-medication-request', selector: 'fhir-medication-request',
templateUrl: './medication-request.component.html', templateUrl: './medication-request.component.html',
styleUrls: ['./medication-request.component.scss'] styleUrls: ['./medication-request.component.scss']
@ -12,6 +18,10 @@ import {MedicationRequestModel} from '../../../../../lib/models/resources/medica
export class MedicationRequestComponent implements OnInit, FhirResourceComponentInterface { export class MedicationRequestComponent implements OnInit, FhirResourceComponentInterface {
@Input() displayModel: MedicationRequestModel | null @Input() displayModel: MedicationRequestModel | null
@Input() showDetails: boolean = true @Input() showDetails: boolean = true
//these are used to populate the description of the resource. May not be available for all resources
resourceCode?: string;
resourceCodeSystem?: string;
isCollapsed: boolean = false isCollapsed: boolean = false
tableData: TableRowItem[] = [] tableData: TableRowItem[] = []
@ -20,6 +30,9 @@ export class MedicationRequestComponent implements OnInit, FhirResourceComponent
ngOnInit(): void { ngOnInit(): void {
this.resourceCode = this.displayModel?.medication_codeable_concept?.code
this.resourceCodeSystem = this.displayModel?.medication_codeable_concept?.system
this.tableData = [ this.tableData = [
{ {
label: 'Medication', label: 'Medication',

View File

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

View File

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

View File

@ -3,8 +3,14 @@ import {FhirResourceComponentInterface} from '../../fhir-resource/fhir-resource-
import {TableRowItem, TableRowItemDataType} from '../../common/table/table-row-item'; import {TableRowItem, TableRowItemDataType} from '../../common/table/table-row-item';
import {Router} from '@angular/router'; import {Router} from '@angular/router';
import {MedicationModel} from '../../../../../lib/models/resources/medication-model'; import {MedicationModel} from '../../../../../lib/models/resources/medication-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";
@Component({ @Component({
standalone: true,
imports: [NgbCollapseModule, CommonModule, BadgeComponent, TableComponent],
selector: 'fhir-medication', selector: 'fhir-medication',
templateUrl: './medication.component.html', templateUrl: './medication.component.html',
styleUrls: ['./medication.component.scss'] styleUrls: ['./medication.component.scss']

View File

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

View File

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

View File

@ -4,8 +4,14 @@ import {ImmunizationModel} from '../../../../../lib/models/resources/immunizatio
import {TableRowItem} from '../../common/table/table-row-item'; import {TableRowItem} from '../../common/table/table-row-item';
import {Router} from '@angular/router'; import {Router} from '@angular/router';
import {PractitionerModel} from '../../../../../lib/models/resources/practitioner-model'; import {PractitionerModel} from '../../../../../lib/models/resources/practitioner-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";
@Component({ @Component({
standalone: true,
imports: [NgbCollapseModule, CommonModule, BadgeComponent, TableComponent],
selector: 'app-practitioner', selector: 'app-practitioner',
templateUrl: './practitioner.component.html', templateUrl: './practitioner.component.html',
styleUrls: ['./practitioner.component.scss'] styleUrls: ['./practitioner.component.scss']

View File

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

View File

@ -12,10 +12,14 @@
<!-- </div>--> <!-- </div>-->
</div> </div>
<div #collapse="ngbCollapse" [(ngbCollapse)]="isCollapsed" class="card-body"> <div #collapse="ngbCollapse" [(ngbCollapse)]="isCollapsed" class="card-body">
<p class="az-content-text mg-b-20">An action that is or was performed on or for a patient, practitioner, device, organization, or location. For example, this can be a physical intervention on a patient like an operation, or less invasive like long term services, counseling, or hypnotherapy.</p> <p class="az-content-text mg-b-20" *ngIf="!(resourceCode && resourceCodeSystem); else lookupCode">An action that is or was performed on or for a patient, practitioner, device, organization, or location. For example, this can be a physical intervention on a patient like an operation, or less invasive like long term services, counseling, or hypnotherapy.</p>
<fhir-ui-table [displayModel]="displayModel" [tableData]="tableData"></fhir-ui-table> <fhir-ui-table [displayModel]="displayModel" [tableData]="tableData"></fhir-ui-table>
</div> </div>
<div *ngIf="showDetails" class="card-footer"> <div *ngIf="showDetails" class="card-footer">
<a class="float-right" routerLink="/source/{{displayModel?.source_id}}/resource/{{displayModel?.source_resource_id}}">details</a> <a class="float-right" routerLink="/source/{{displayModel?.source_id}}/resource/{{displayModel?.source_resource_id}}">details</a>
</div> </div>
</div> </div>
<ng-template #lookupCode>
<app-glossary-lookup class="az-content-text mg-b-20" [code]="resourceCode" [codeSystem]="resourceCodeSystem"></app-glossary-lookup>
</ng-template>

View File

@ -10,8 +10,7 @@ describe('ProcedureComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ ProcedureComponent ], imports: [ProcedureComponent, NgbCollapseModule],
imports: [NgbCollapseModule],
providers: [RouterTestingModule] providers: [RouterTestingModule]
}) })

View File

@ -3,8 +3,14 @@ import {FhirResourceComponentInterface} from '../../fhir-resource/fhir-resource-
import {TableRowItem, TableRowItemDataType} from '../../common/table/table-row-item'; import {TableRowItem, TableRowItemDataType} from '../../common/table/table-row-item';
import {Router} from '@angular/router'; import {Router} from '@angular/router';
import {ProcedureModel} from '../../../../../lib/models/resources/procedure-model'; import {ProcedureModel} from '../../../../../lib/models/resources/procedure-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";
@Component({ @Component({
standalone: true,
imports: [NgbCollapseModule, CommonModule, BadgeComponent, TableComponent],
selector: 'fhir-procedure', selector: 'fhir-procedure',
templateUrl: './procedure.component.html', templateUrl: './procedure.component.html',
styleUrls: ['./procedure.component.scss'] styleUrls: ['./procedure.component.scss']
@ -12,6 +18,11 @@ import {ProcedureModel} from '../../../../../lib/models/resources/procedure-mode
export class ProcedureComponent implements OnInit, FhirResourceComponentInterface { export class ProcedureComponent implements OnInit, FhirResourceComponentInterface {
@Input() displayModel: ProcedureModel | null @Input() displayModel: ProcedureModel | null
@Input() showDetails: boolean = true @Input() showDetails: boolean = true
//these are used to populate the description of the resource. May not be available for all resources
resourceCode?: string;
resourceCodeSystem?: string;
isCollapsed: boolean = false isCollapsed: boolean = false
tableData: TableRowItem[] = [] tableData: TableRowItem[] = []
@ -20,6 +31,15 @@ export class ProcedureComponent implements OnInit, FhirResourceComponentInterfac
ngOnInit(): void { ngOnInit(): void {
//medline only supports CPT procedure codes - "http://www.ama-assn.org/go/cpt", "2.16.840.1.113883.6.12"
for(let coding of this.displayModel?.coding ?? []){
if(coding.system == "http://www.ama-assn.org/go/cpt" || coding.system == "2.16.840.1.113883.6.12"){
this.resourceCode = coding.code
this.resourceCodeSystem = coding.system
break
}
}
this.tableData = [ this.tableData = [
{ {
label: 'Identification', label: 'Identification',

View File

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

View File

@ -1,7 +1,7 @@
<div class="az-footer ht-40 page-footer fixed-bottom"> <div class="az-footer ht-40 page-footer fixed-bottom">
<div class="container ht-100p pd-t-0-f"> <div class="container ht-100p pd-t-0-f">
<div class="d-sm-flex justify-content-center justify-content-sm-between py-2 w-100"> <div class="d-sm-flex justify-content-center justify-content-sm-between py-2 w-100">
<span class="text-muted text-center text-sm-left d-block d-sm-inline-block">Copyright © Fasten 2022</span> <span class="text-muted text-center text-sm-left d-block d-sm-inline-block">Copyright © Fasten 2022 | {{appVersion}}</span>
<span class="float-none float-sm-right d-block mt-1 mt-sm-0 text-center">Open Source personal electronic medical record system. <a href="https://www.fastenhealth.com/" target="_blank">It's your health. Own it.</a></span> <span class="float-none float-sm-right d-block mt-1 mt-sm-0 text-center">Open Source personal electronic medical record system. <a href="https://www.fastenhealth.com/" target="_blank">It's your health. Own it.</a></span>
</div> </div>
</div><!-- container --> </div><!-- container -->

View File

@ -1,4 +1,5 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import {versionInfo} from '../../../environments/versions';
@Component({ @Component({
selector: 'app-footer', selector: 'app-footer',
@ -6,8 +7,11 @@ import { Component, OnInit } from '@angular/core';
styleUrls: ['./footer.component.scss'] styleUrls: ['./footer.component.scss']
}) })
export class FooterComponent implements OnInit { export class FooterComponent implements OnInit {
appVersion: string;
constructor() { } constructor() {
this.appVersion = versionInfo.version
}
ngOnInit() { ngOnInit() {
} }

View File

@ -3,6 +3,9 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { GlossaryLookupComponent } from './glossary-lookup.component'; import { GlossaryLookupComponent } from './glossary-lookup.component';
import {FastenApiService} from '../../services/fasten-api.service'; import {FastenApiService} from '../../services/fasten-api.service';
import {of} from 'rxjs'; import {of} from 'rxjs';
import {HTTP_CLIENT_TOKEN} from '../../dependency-injection';
import {HttpClient} from '@angular/common/http';
import {HttpClientTestingModule} from '@angular/common/http/testing';
describe('GlossaryLookupComponent', () => { describe('GlossaryLookupComponent', () => {
let component: GlossaryLookupComponent; let component: GlossaryLookupComponent;
@ -13,11 +16,17 @@ describe('GlossaryLookupComponent', () => {
mockedFastenApiService = jasmine.createSpyObj('FastenApiService', ['getGlossarySearchByCode']) mockedFastenApiService = jasmine.createSpyObj('FastenApiService', ['getGlossarySearchByCode'])
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ GlossaryLookupComponent ], imports: [ GlossaryLookupComponent, HttpClientTestingModule ],
providers: [{ providers: [
provide: FastenApiService, {
useValue: mockedFastenApiService provide: FastenApiService,
}] useValue: mockedFastenApiService
},
{
provide: HTTP_CLIENT_TOKEN,
useClass: HttpClient,
},
]
}) })
.compileComponents(); .compileComponents();
mockedFastenApiService.getGlossarySearchByCode.and.returnValue(of({ mockedFastenApiService.getGlossarySearchByCode.and.returnValue(of({

View File

@ -1,11 +1,17 @@
import {Component, Input, OnInit} from '@angular/core'; import {Component, Input, OnInit} from '@angular/core';
import {FastenApiService} from '../../services/fasten-api.service'; import {FastenApiService} from '../../services/fasten-api.service';
import {DomSanitizer, SafeHtml} from '@angular/platform-browser'; import {DomSanitizer, SafeHtml} from '@angular/platform-browser';
import {LoadingSpinnerComponent} from "../loading-spinner/loading-spinner.component";
import {AuthService} from "../../services/auth.service";
import {CommonModule} from "@angular/common";
@Component({ @Component({
standalone: true,
imports: [LoadingSpinnerComponent, CommonModule],
providers: [FastenApiService, AuthService],
selector: 'app-glossary-lookup', selector: 'app-glossary-lookup',
templateUrl: './glossary-lookup.component.html', templateUrl: './glossary-lookup.component.html',
styleUrls: ['./glossary-lookup.component.scss'] styleUrls: ['./glossary-lookup.component.scss'],
}) })
export class GlossaryLookupComponent implements OnInit { export class GlossaryLookupComponent implements OnInit {
@ -33,4 +39,5 @@ export class GlossaryLookupComponent implements OnInit {
}) })
} }
} }

View File

@ -0,0 +1,161 @@
import type { Meta, StoryObj } from '@storybook/angular';
import {GlossaryLookupComponent} from "./glossary-lookup.component";
import {applicationConfig, moduleMetadata} from "@storybook/angular";
import { DecoratorFunction, StoryContext } from '@storybook/types';
import {HttpClient, HttpClientModule} from "@angular/common/http";
import {BrowserModule} from "@angular/platform-browser";
import {Observable, of} from "rxjs";
import {HTTP_CLIENT_TOKEN} from "../../dependency-injection";
//Use decorators to override Angular dependency injection for HttpClient
// https://github.com/storybookjs/storybook/blob/d64e49ba1a715db73a3d6c697517fe6a85a8f8ef/examples/angular-cli/src/stories/addons/toolbars/locales/translate.service.ts
// https://www.tektutorialshub.com/angular/injection-token-in-angular/
// https://medium.com/ngconf/configure-your-angular-apps-with-an-injection-token-be16eee59c40
const withHttpClientProvider: DecoratorFunction<any> = (storyFunc, context) => {
const { httpClientResp } = context.parameters;
let { code, codeSystem } = context.args;
// uses `moduleMetadata` decorator to cleanly add locale provider into module metadata
// It is also possible to do it directly in story with
// ```
// const sotry = storyFunc();
// sotry.moduleMetadata = {
// ...sotry.moduleMetadata,
// providers: [
// ...(sotry.moduleMetadata?.providers ?? []),
// { provide: DEFAULT_LOCALE, useValue: locale },
// ],
// };
// return sotry;
// ```
// but more verbose
class MockHttpClient extends HttpClient {
get(): Observable<any> {
// console.log("CALLED getGlossarySearchByCode in MockFastenApiService")
return of(httpClientResp)
}
}
// console.log("Inside withHttpClientProvider DecoratorFunction", code, codeSystem)
return moduleMetadata({ providers: [{ provide: HTTP_CLIENT_TOKEN, useClass: MockHttpClient }] })(
storyFunc,
context
);
};
// More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction
const meta: Meta<GlossaryLookupComponent> = {
title: 'Components/GlossaryLookup',
component: GlossaryLookupComponent,
decorators: [
withHttpClientProvider,
moduleMetadata({
imports: [BrowserModule, HttpClientModule],
}),
// applicationConfig({
// // imports: [BrowserModule, HttpClientModule],
// providers: [{ provide: FastenApiService, useValue: MockFastenApiService }],
// }),
],
tags: ['autodocs'],
render: (args: GlossaryLookupComponent) => ({
props: {
backgroundColor: null,
...args,
},
}),
argTypes: {
code: {
control: 'text',
},
codeSystem: {
control: 'text',
},
},
};
export default meta;
type Story = StoryObj<GlossaryLookupComponent>;
// More on writing stories with args: https://storybook.js.org/docs/angular/writing-stories/args
export const Empty: Story = {
};
export const PlainText: Story = {
args: {
code: "36955009",
codeSystem: "2.16.840.1.113883.6.96"
},
parameters: {
httpClientResp: {
url: "https://www.nidcr.nih.gov/health-info/taste-disorders/more-info?utm_source=medlineplus-connect&utm_medium=website&utm_campaign=mlp-connect",
publisher: "U.S. National Library of Medicine",
description: "Problems with the sense of taste can have a big impact on life. Taste stimulates the desire to eat and therefore plays a key role in nutrition. The sense of taste also helps keep us healthy by helping us detect spoiled food or drinks.",
}
}
};
export const Html: Story = {
args: {
code: "21000-5",
codeSystem: "2.16.840.1.113883.6.96"
},
parameters: {
httpClientResp: {
url: "https://medlineplus.gov/lab-tests/rdw-red-cell-distribution-width?utm_source=mplusconnect&utm_medium=service",
publisher: "U.S. National Library of Medicine",
description: "<h2>What is a Red Cell Distribution Width Test?</h2>\n" +
"<p>A red cell distribution width (RDW) test measures the differences in the volume and size of your red blood cells (erythrocytes). Red blood cells carry oxygen from your lungs to every cell in your body. Your cells need oxygen to grow, make new cells, and stay healthy.</p>\n" +
"<p>Normally, your red blood cells are all about the same size. A high RDW means that there's a major difference between the size of your smallest and largest red blood cells. This may be a sign of a medical condition.</p>\n" +
"<p>Other names: RDW-SD (standard deviation) test, Erythrocyte Distribution Width</p><h2>What is it used for?</h2>\n" +
"<p>The RDW blood test is often part of a <a data-pid=\"728\" href=\"https://medlineplus.gov/lab-tests/complete-blood-count-cbc/\">complete blood count (CBC)</a>, a test that measures many different parts of your blood, including red cells. The RDW test is commonly used to help diagnose <a data-tid=\"139\" href=\"https://medlineplus.gov/anemia.html\">anemia</a>, a condition in which your red blood cells can't carry enough oxygen to the rest of your body.</p>\n" +
"<p>The RDW test may also be used with other tests to help diagnose other conditions, including <a data-tid=\"4239\" href=\"https://medlineplus.gov/thalassemia.html\">thalassemia</a>, an inherited disease that can cause severe anemia.</p><h2>Why do I need an RDW test?</h2>\n" +
"<p>Your health care provider may have ordered a complete blood count, which includes an RDW test, as part of a routine exam, or if you have:</p>\n" +
"<ul>\n" +
"<li>Symptoms of anemia, including weakness, <a data-tid=\"216\" href=\"https://medlineplus.gov/dizzinessandvertigo.html\">dizziness</a>, pale skin, and cold hands and feet</li>\n" +
"<li>A family history of thalassemia, <a data-tid=\"402\" href=\"https://medlineplus.gov/sicklecelldisease.html\">sickle cell anemia</a>, or other inherited blood disorder</li>\n" +
"<li>A chronic illness such as <a data-tid=\"119\" href=\"https://medlineplus.gov/crohnsdisease.html\">Crohn's disease</a>, diabetes, or <a data-tid=\"1\" href=\"https://medlineplus.gov/hivaids.html\">HIV/AIDS</a></li>\n" +
"<li>A diet low in <a data-tid=\"5542\" href=\"https://medlineplus.gov/iron.html\">iron</a> and other <a data-tid=\"4298\" href=\"https://medlineplus.gov/minerals.html\">minerals</a></li>\n" +
"<li>A long-term infection</li>\n" +
"<li>Excessive <a data-tid=\"6039\" href=\"https://medlineplus.gov/bleeding.html\">blood loss</a> from an injury or surgical procedure</li>\n" +
"</ul><h2>What happens during an RDW test?</h2>\n" +
"<p>A health care professional will take a blood sample from a vein in your arm, using a small needle. After the needle is inserted, a small amount of blood will be collected into a test tube or vial. You may feel a little sting when the needle goes in or out. This process generally takes less than five minutes.</p><h2>Will I need to do anything to prepare for the test?</h2>\n" +
"<p>No special preparation is necessary.</p><h2>Are there any risks to the test?</h2>\n" +
"<p>There is very little risk to a blood test. You may experience slight pain or bruising at the spot where the needle was put in, but most symptoms go away quickly.</p><h2>What do the results mean?</h2>\n" +
"<p>RDW results help your provider understand how much your red blood cells vary in size and volume. Even if your RDW results are normal, you may still have a medical condition that needs treatment. That's why your provider will usually look at your RDW results along with the results of other blood tests. The combined test results can show a more complete picture of your red blood cells to help diagnose a variety of conditions, including:</p>\n" +
"<ul>\n" +
"<li>Iron deficiency</li>\n" +
"<li>Different types of anemia</li>\n" +
"<li>Thalassemia</li>\n" +
"<li>Sickle cell anemia</li>\n" +
"</ul>\n" +
"<p>A high RDW result can also be a sign of other conditions, such as:</p>\n" +
"<ul>\n" +
"<li>Chronic <a data-tid=\"310\" href=\"https://medlineplus.gov/liverdiseases.html\">liver disease</a></li>\n" +
"<li><a data-tid=\"277\" href=\"https://medlineplus.gov/heartdiseases.html\">Heart disease</a></li>\n" +
"<li><a data-tid=\"4\" href=\"https://medlineplus.gov/diabetes.html\">Diabetes</a></li>\n" +
"<li><a data-tid=\"91\" href=\"https://medlineplus.gov/kidneydiseases.html\">Kidney disease</a></li>\n" +
"<li><a data-tid=\"25\" href=\"https://medlineplus.gov/cancer.html\">Cancer</a>, especially <a data-tid=\"88\" href=\"https://medlineplus.gov/colorectalcancer.html\">colorectal cancer</a></li>\n" +
"</ul>\n" +
"<p>Your provider will most likely need more tests to confirm a diagnosis.</p>\n" +
"<p>Learn more about <a data-pid=\"806\" href=\"https://medlineplus.gov/lab-tests/how-to-understand-your-lab-results/\">laboratory tests, reference ranges, and understanding results</a>.</p><h2>Is there anything else I need to know about a red cell distribution width test?</h2>\n" +
"<p>If your test results indicate you have a chronic blood disorder, such as anemia, you may be put on a treatment plan to increase the amount of oxygen that your red blood cells can carry. Depending on your specific condition, your provider may recommend iron supplements, medicines, and/or changes in your diet.</p>\n" +
"<p>Be sure to talk to your provider before taking any supplements or making any changes in your eating plan.</p><h2>References</h2>\n" +
"<ol>\n" +
"<li>Lee H, Kong S, Sohn Y, Shim H, Youn H, Lee S, Kim H, Eom H. Elevated Red Blood Cell Distribution Width as a Simple Prognostic Factor in Patients with Symptomatic Multiple Myeloma. Biomed Research International [Internet]. 2014 May 21 [cited 2017 Jan 24]; 2014(Article ID 145619, 8 pages). Available from: <a href=\"https://www.hindawi.com/journals/bmri/2014/145619/cta/\" target=\"bibliowin\">https://www.hindawi.com/journals/bmri/2014/145619/cta/</a></li>\n" +
"<li>May Jori E, Marques Marisa B, Reddy Vishnu VB, Gangaraju Radhika. Three neglected numbers in the CBC: The RDW, MPV, and NRBC count. Cleveland Clinic Journal of Medicine [Internet]. 2019 Mar [cited 2021 Dec 22];86(3):167-172. Available from: <a href=\"https://www.ccjm.org/content/86/3/167 doi: 10.3949/ccjm.86a.18072\" target=\"bibliowin\">https://www.ccjm.org/content/86/3/167 doi: 10.3949/ccjm.86a.18072</a></li>\n" +
"<li>Mayo Clinic [Internet].Mayo Foundation for Medical Education and Research; c1998-2021. Macrocytosis: What causes it?; 6 [cited 2021 Dec 22]; [about 3 screens]. Available from: <a href=\"http://www.mayoclinic.org/macrocytosis/expert-answers/faq-20058234.\" target=\"bibliowin\">http://www.mayoclinic.org/macrocytosis/expert-answers/faq-20058234.</a></li>\n" +
"<li>National Heart, Lung, and Blood Institute [Internet]. Bethesda (MD): U.S. Department of Health and Human Services; Thalessemias; [cited 2021 Dec 22]; [about 27 screens]. Available from: <a href=\"https://www.nhlbi.nih.gov/health/health-topics/topics/thalassemia/\" target=\"bibliowin\">https://www.nhlbi.nih.gov/health/health-topics/topics/thalassemia/</a></li>\n" +
"<li>National Heart, Lung, and Blood Institute [Internet]. Bethesda (MD): U.S. Department of Health and Human Services; Anemia: Overview; [updated 2012 May 18; cited 2021 Dec 22]; [about 2 screens]. Available from: <a href=\"https://www.nhlbi.nih.gov/health/health-topics/topics/anemia/treatment\" target=\"bibliowin\">https://www.nhlbi.nih.gov/health/health-topics/topics/anemia/treatment</a></li>\n" +
"<li>National Heart, Lung, and Blood Institute [Internet]. Bethesda (MD): U.S. Department of Health and Human Services; Blood Tests; [updated 2012; cited 2021 Dec 22]; [about 19 screens]. Available from: <a href=\"https://www.nhlbi.nih.gov/health-topics/blood-tests\" target=\"bibliowin\">https://www.nhlbi.nih.gov/health-topics/blood-tests</a></li>\n" +
"<li>Salvagno G, Sanchis-Gomar F, Picanza A, Lippi G. Red blood cell distribution width: A simple parameter with multiple clinical applications. Critical Reviews in Laboratory Science [Internet]. 2014 Dec 23 [cited 2017 Jan 24]; 52 (2): 86-105. Available from: <a href=\"http://www.tandfonline.com/doi/full/10.3109/10408363.2014.992064\" target=\"bibliowin\">http://www.tandfonline.com/doi/full/10.3109/10408363.2014.992064</a></li>\n" +
"<li>Song Y, Huang Z, Kang Y, Lin Z, Lu P, Cai Z, Cao Y, ZHuX. Clinical Usefulness and Prognostic Value of Red Cell Distribution Width in Colorectal Cancer. Biomed Res Int [Internet]. 2018 Dec [cited 2019 Jan 27]; 2018 Article ID, 9858943. Available from: <a href=\"https://www.ncbi.nlm.nih.gov/pmc/articles/PMC6311266\" target=\"bibliowin\">https://www.ncbi.nlm.nih.gov/pmc/articles/PMC6311266</a></li>\n" +
"<li>Thame M, Grandison Y, Mason K Higgs D, Morris J, Serjeant B, Serjeant G. The red cell distribution width in sickle cell disease—is it of clinical value? International Journal of Laboratory Hematology [Internet]. 1991 Sep [cited 2017 Jan 24]; 13 (3): 229-237. Available from: <a href=\"http://onlinelibrary.wiley.com/wol1/doi/10.1111/j.1365-2257.1991.tb00277.x/abstract\" target=\"bibliowin\">http://onlinelibrary.wiley.com/wol1/doi/10.1111/j.1365-2257.1991.tb00277.x/abstract</a></li>\n" +
"</ol>",
}
}
};

View File

@ -0,0 +1,73 @@
/**
* gridstack-item.component.ts 7.3.0
* Copyright (c) 2022 Alain Dumesny - see GridStack root license
*/
import { ChangeDetectionStrategy, Component, ElementRef, Input, ViewChild, ViewContainerRef, OnDestroy } from '@angular/core';
import { GridItemHTMLElement, GridStackNode } from 'gridstack';
import {CommonModule} from '@angular/common';
/** store element to Ng Class pointer back */
export interface GridItemCompHTMLElement extends GridItemHTMLElement {
_gridItemComp?: GridstackItemComponent;
}
/**
* HTML Component Wrapper for gridstack items, in combination with GridstackComponent for parent grid
*/
@Component({
standalone: true,
imports: [CommonModule],
selector: 'gridstack-item',
template: `
<div class="grid-stack-item-content">
<!-- this is where you would create the right component based on some internal type or id. doing .content for demo purpose -->
{{options.content}}
<ng-content></ng-content>
<!-- where dynamic items go (like sub-grids) -->
<ng-template #container></ng-template>
</div>`,
styles: [`
:host { display: block; }
`],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class GridstackItemComponent implements OnDestroy {
/** container to append items dynamically */
@ViewChild('container', { read: ViewContainerRef, static: true}) public container?: ViewContainerRef;
/** list of options for creating/updating this item */
@Input() public set options(val: GridStackNode) {
if (this.el.gridstackNode?.grid) {
// already built, do an update...
this.el.gridstackNode.grid.update(this.el, val);
} else {
// store our custom element in options so we can update it and not re-create a generic div!
val.el = this.el;
this._options = val;
}
}
/** return the latest grid options (from GS once built, otherwise initial values) */
public get options(): GridStackNode {
return this.el.gridstackNode || this._options || {el: this.el};
}
private _options?: GridStackNode;
/** return the native element that contains grid specific fields as well */
public get el(): GridItemCompHTMLElement { return this.elementRef.nativeElement; }
/** clears the initial options now that we've built */
public clearOptions() {
delete this._options;
}
constructor(private readonly elementRef: ElementRef<GridItemHTMLElement>) {
this.el._gridItemComp = this;
}
public ngOnDestroy(): void {
delete this.el._gridItemComp;
}
}

View File

@ -0,0 +1,237 @@
/**
* gridstack.component.ts 7.3.0
* Copyright (c) 2022 Alain Dumesny - see GridStack root license
*/
import { AfterContentInit, ChangeDetectionStrategy, Component, ContentChildren, ElementRef, EventEmitter, Input,
NgZone, OnDestroy, OnInit, Output, QueryList, ViewChild, ViewContainerRef } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { GridHTMLElement, GridItemHTMLElement, GridStack, GridStackNode, GridStackOptions, GridStackWidget } from 'gridstack';
import { GridItemCompHTMLElement, GridstackItemComponent } from './gridstack-item.component';
import {CommonModule} from '@angular/common';
/** events handlers emitters signature for different events */
export type eventCB = {event: Event};
export type elementCB = {event: Event, el: GridItemHTMLElement};
export type nodesCB = {event: Event, nodes: GridStackNode[]};
export type droppedCB = {event: Event, previousNode: GridStackNode, newNode: GridStackNode};
/** store element to Ng Class pointer back */
export interface GridCompHTMLElement extends GridHTMLElement {
_gridComp?: GridstackComponent;
}
/**
* HTML Component Wrapper for gridstack, in combination with GridstackItemComponent for the items
*/
@Component({
standalone: true,
imports: [CommonModule, GridstackItemComponent],
selector: 'gridstack',
template: `
<!-- content to show when when grid is empty, like instructions on how to add widgets -->
<ng-content select="[empty-content]" *ngIf="isEmpty"></ng-content>
<!-- where dynamic items go -->
<ng-template #container></ng-template>
<!-- where template items go -->
<ng-content></ng-content>
`,
styles: [`
:host { display: block; }
`],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class GridstackComponent implements OnInit, AfterContentInit, OnDestroy {
/** track list of TEMPLATE grid items so we can sync between DOM and GS internals */
@ContentChildren(GridstackItemComponent) public gridstackItems?: QueryList<GridstackItemComponent>;
/** container to append items dynamically */
@ViewChild('container', { read: ViewContainerRef, static: true}) public container?: ViewContainerRef;
/** initial options for creation of the grid */
@Input() public set options(val: GridStackOptions) { this._options = val; }
/** return the current running options */
public get options(): GridStackOptions { return this._grid?.opts || this._options || {}; }
/** true while ng-content with 'no-item-content' should be shown when last item is removed from a grid */
@Input() public isEmpty?: boolean;
/** individual list of GridStackEvent callbacks handlers as output
* otherwise use this.grid.on('name1 name2 name3', callback) to handle multiple at once
* see https://github.com/gridstack/gridstack.js/blob/master/demo/events.js#L4
*
* Note: camel casing and 'CB' added at the end to prevent @angular-eslint/no-output-native
* eg: 'change' would trigger the raw CustomEvent so use different name.
*/
@Output() public addedCB = new EventEmitter<nodesCB>();
@Output() public changeCB = new EventEmitter<nodesCB>();
@Output() public disableCB = new EventEmitter<eventCB>();
@Output() public dragCB = new EventEmitter<elementCB>();
@Output() public dragStartCB = new EventEmitter<elementCB>();
@Output() public dragStopCB = new EventEmitter<elementCB>();
@Output() public droppedCB = new EventEmitter<droppedCB>();
@Output() public enableCB = new EventEmitter<eventCB>();
@Output() public removedCB = new EventEmitter<nodesCB>();
@Output() public resizeCB = new EventEmitter<elementCB>();
@Output() public resizeStartCB = new EventEmitter<elementCB>();
@Output() public resizeStopCB = new EventEmitter<elementCB>();
/** return the native element that contains grid specific fields as well */
public get el(): GridCompHTMLElement { return this.elementRef.nativeElement; }
/** return the GridStack class */
public get grid(): GridStack | undefined { return this._grid; }
private _options?: GridStackOptions;
private _grid?: GridStack;
private loaded?: boolean;
private ngUnsubscribe: Subject<void> = new Subject();
constructor(
private readonly zone: NgZone,
private readonly elementRef: ElementRef<GridCompHTMLElement>,
) {
this.el._gridComp = this;
}
public ngOnInit(): void {
// inject our own addRemove so we can create GridItemComponent instead of simple divs
const opts: GridStackOptions = this._options || {};
opts.addRemoveCB = GridstackComponent._addRemoveCB;
// init ourself before any template children are created since we track them below anyway - no need to double create+update widgets
this.loaded = !!this.options?.children?.length;
this._grid = GridStack.init(opts, this.el);
delete this._options; // GS has it now
}
/** wait until after all DOM is ready to init gridstack children (after angular ngFor and sub-components run first) */
public ngAfterContentInit(): void {
this.zone.runOutsideAngular(() => {
// track whenever the children list changes and update the layout...
this.gridstackItems?.changes
.pipe(takeUntil(this.ngUnsubscribe))
.subscribe(() => this.updateAll());
// ...and do this once at least unless we loaded children already
if (!this.loaded) this.updateAll();
this.hookEvents(this.grid);
});
}
public ngOnDestroy(): void {
this.ngUnsubscribe.next();
this.ngUnsubscribe.complete();
this.grid?.destroy();
delete this._grid;
delete this.el._gridComp;
}
/**
* called when the TEMPLATE list of items changes - get a list of nodes and
* update the layout accordingly (which will take care of adding/removing items changed by Angular)
*/
public updateAll() {
if (!this.grid) return;
const layout: GridStackWidget[] = [];
this.gridstackItems?.forEach(item => {
layout.push(item.options);
item.clearOptions();
});
this.grid.load(layout); // efficient that does diffs only
}
/** check if the grid is empty, if so show alternative content */
public checkEmpty() {
if (!this.grid) return;
this.isEmpty = !this.grid.engine.nodes.length;
}
/** get all known events as easy to use Outputs for convenience */
private hookEvents(grid?: GridStack) {
if (!grid) return;
grid
.on('added', (event: Event, nodes: GridStackNode[]) => this.zone.run(() => { this.checkEmpty(); this.addedCB.emit({event, nodes}); }))
.on('change', (event: Event, nodes: GridStackNode[]) => this.zone.run(() => this.changeCB.emit({event, nodes})))
.on('disable', (event: Event) => this.zone.run(() => this.disableCB.emit({event})))
.on('drag', (event: Event, el: GridItemHTMLElement) => this.zone.run(() => this.dragCB.emit({event, el})))
.on('dragstart', (event: Event, el: GridItemHTMLElement) => this.zone.run(() => this.dragStartCB.emit({event, el})))
.on('dragstop', (event: Event, el: GridItemHTMLElement) => this.zone.run(() => this.dragStopCB.emit({event, el})))
.on('dropped', (event: Event, previousNode: GridStackNode, newNode: GridStackNode) => this.zone.run(() => this.droppedCB.emit({event, previousNode, newNode})))
.on('enable', (event: Event) => this.zone.run(() => this.enableCB.emit({event})))
.on('removed', (event: Event, nodes: GridStackNode[]) => this.zone.run(() => { this.checkEmpty(); this.removedCB.emit({event, nodes}); }))
.on('resize', (event: Event, el: GridItemHTMLElement) => this.zone.run(() => this.resizeCB.emit({event, el})))
.on('resizestart', (event: Event, el: GridItemHTMLElement) => this.zone.run(() => this.resizeStartCB.emit({event, el})))
.on('resizestop', (event: Event, el: GridItemHTMLElement) => this.zone.run(() => this.resizeStopCB.emit({event, el})))
}
/** called by GS when a new item needs to be created, which we do as a Angular component, or deleted (skip) */
private static _addRemoveCB(parent: GridCompHTMLElement | HTMLElement, w: GridStackWidget | GridStackOptions, add: boolean, isGrid: boolean): HTMLElement | undefined {
if (add) {
if (!parent) return;
// create the grid item dynamically - see https://angular.io/docs/ts/latest/cookbook/dynamic-component-loader.html
if (isGrid) {
const gridItemComp = (parent.parentElement as GridItemCompHTMLElement)._gridItemComp;
const grid = gridItemComp?.container?.createComponent(GridstackComponent)?.instance;
if (grid) grid.options = w as GridStackOptions;
return grid?.el;
} else {
// TODO: use GridStackWidget to define what type of component to create as child, or do it in GridstackItemComponent template...
const gridComp = (parent as GridCompHTMLElement)._gridComp;
const gridItem = gridComp?.container?.createComponent(GridstackItemComponent)?.instance;
return gridItem?.el;
}
}
return;
}
}
// /**
// * Simplest Angular Example using GridStack API directly
// */
// import { Component, OnInit } from '@angular/core';
//
// import { GridStack, GridStackWidget } from 'gridstack';
//
// @Component({
// selector: 'gridstack',
// template: `
// <p><b>SIMPLEST</b>: angular example using GridStack API directly, so not really using any angular construct per say other than waiting for DOM rendering</p>
// <button (click)="add()">add item</button>
// <button (click)="delete()">remove item</button>
// <button (click)="change()">modify item</button>
// <div class="grid-stack"></div>
// `,
// // gridstack.min.css and other custom styles should be included in global styles.scss
// })
// export class GridstackComponent implements OnInit {
// public items: GridStackWidget[] = [
// { x: 0, y: 3, w: 12, h: 6, content: '0' },
// { x: 0, y: 0, w: 4, h: 3, content: '1' },
// { x: 4, y: 0, w: 4, h: 3, content: '2' },
// { x: 8, y: 0, w: 4, h: 3, content: '3' },
// ];
// private grid!: GridStack;
//
// constructor() {}
//
// // simple div above doesn't require Angular to run, so init gridstack here
// public ngOnInit() {
// this.grid = GridStack.init({
// cellHeight: 70,
// })
// .load(this.items); // and load our content directly (will create DOM)
// }
//
// public add() {
// this.grid.addWidget({w: 3, content: 'new content'});
// }
// public delete() {
// this.grid.removeWidget(this.grid.engine.nodes[0].el!);
// }
// public change() {
// this.grid.update(this.grid.engine.nodes[0].el!, {w: 1});
// }
// }

View File

@ -4,6 +4,8 @@ import { HeaderComponent } from './header.component';
import {HttpClientTestingModule} from '@angular/common/http/testing'; import {HttpClientTestingModule} from '@angular/common/http/testing';
import {RouterModule} from '@angular/router'; import {RouterModule} from '@angular/router';
import {RouterTestingModule} from '@angular/router/testing'; import {RouterTestingModule} from '@angular/router/testing';
import {HTTP_CLIENT_TOKEN} from '../../dependency-injection';
import {HttpClient} from '@angular/common/http';
describe('HeaderComponent', () => { describe('HeaderComponent', () => {
let component: HeaderComponent; let component: HeaderComponent;
@ -12,7 +14,13 @@ describe('HeaderComponent', () => {
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ HttpClientTestingModule, RouterTestingModule, RouterModule ], imports: [ HttpClientTestingModule, RouterTestingModule, RouterModule ],
declarations: [ HeaderComponent ] declarations: [ HeaderComponent ],
providers: [
{
provide: HTTP_CLIENT_TOKEN,
useClass: HttpClient,
},
]
}) })
.compileComponents(); .compileComponents();
})); }));

View File

@ -8,7 +8,7 @@
<br/> <br/>
You can click on a row to see the raw data. You can click on a row to see the raw data.
<br/> <br/>
If you have any feedback regarding the data displayed, please <a href="https://github.com/fastenhealth/docs/issues">file a ticket <i class="fab fa-github"></i></a> If you have any feedback regarding the data displayed, please <a href="https://github.com/fastenhealth/fasten-onprem/issues">file a ticket <i class="fab fa-github"></i></a>
</div> </div>
<ngx-datatable <ngx-datatable

View File

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

View File

@ -1,6 +1,7 @@
import {Component, Input, OnInit} from '@angular/core'; import {Component, Input, OnInit} from '@angular/core';
@Component({ @Component({
standalone: true,
selector: 'app-loading-spinner', selector: 'app-loading-spinner',
templateUrl: './loading-spinner.component.html', templateUrl: './loading-spinner.component.html',
styleUrls: ['./loading-spinner.component.scss'] styleUrls: ['./loading-spinner.component.scss']

View File

@ -0,0 +1,44 @@
import type { Meta, StoryObj } from '@storybook/angular';
import {LoadingSpinnerComponent} from './loading-spinner.component';
// More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction
const meta: Meta<LoadingSpinnerComponent> = {
title: 'Components/LoadingSpinner',
component: LoadingSpinnerComponent,
decorators: [
// moduleMetadata({
// imports: [AppModule]
// })
// applicationConfig({
// providers: [importProvidersFrom(AppModule)],
// }),
],
tags: ['autodocs'],
render: (args: LoadingSpinnerComponent) => ({
props: {
backgroundColor: null,
...args,
},
}),
argTypes: {
loadingTitle: {
control: 'text',
},
loadingSubTitle: {
control: 'text',
},
},
};
export default meta;
type Story = StoryObj<LoadingSpinnerComponent>;
// More on writing stories with args: https://storybook.js.org/docs/angular/writing-stories/args
export const Primary: Story = {};
export const Secondary: Story = {
args: {
loadingTitle: "Custom loading title",
},
};

View File

@ -12,6 +12,8 @@ import {DiagnosticReportModel} from '../../../lib/models/resources/diagnostic-re
import {FastenDisplayModel} from '../../../lib/models/fasten/fasten-display-model'; import {FastenDisplayModel} from '../../../lib/models/fasten/fasten-display-model';
import * as _ from "lodash"; import * as _ from "lodash";
import {ConditionModel} from '../../../lib/models/resources/condition-model'; import {ConditionModel} from '../../../lib/models/resources/condition-model';
import {RecResourceRelatedDisplayModel} from '../../../lib/utils/resource_related_display_model';
import {CodingModel} from '../../../lib/models/datatypes/coding-model';
@Component({ @Component({
selector: 'app-report-medical-history-condition', selector: 'app-report-medical-history-condition',
@ -71,7 +73,9 @@ export class ReportMedicalHistoryConditionComponent implements OnInit {
} }
//add resources to the lookup table, ensure uniqueness. //add resources to the lookup table, ensure uniqueness.
this.conditionDisplayModel = this.recExtractResources(this.conditionGroup) let result = RecResourceRelatedDisplayModel(this.conditionGroup)
this.resourcesLookup = result.resourcesLookup
this.conditionDisplayModel = result.displayModel
let involvedInCareMap: {[resource_id: string]: {displayName: string, role?: string, email?: string}} = {} let involvedInCareMap: {[resource_id: string]: {displayName: string, role?: string, email?: string}} = {}
@ -99,7 +103,7 @@ export class ReportMedicalHistoryConditionComponent implements OnInit {
let telecomEmails =_.find(practitionerModel.telecom, {"system": "email"}) let telecomEmails =_.find(practitionerModel.telecom, {"system": "email"})
let email = _.get(telecomEmails, '[0].value') let email = _.get(telecomEmails, '[0].value')
let qualification = _.find(practitionerModel.qualification, {"system": "http://nucc.org/provider-taxonomy"}) let qualification = _.find(practitionerModel.qualification, {"system": "http://nucc.org/provider-taxonomy"}) as CodingModel
involvedInCareMap[id] = _.mergeWith( involvedInCareMap[id] = _.mergeWith(
{}, {},
@ -138,42 +142,4 @@ export class ReportMedicalHistoryConditionComponent implements OnInit {
} }
} }
/*
This function flattens all resources
*/
recExtractResources(resource: ResourceFhir): FastenDisplayModel{
let resourceId = this.genResourceId(resource)
let resourceDisplayModel: FastenDisplayModel = this.resourcesLookup[resourceId]
//ensure display model is populated
if(!resourceDisplayModel){
try{
resourceDisplayModel = fhirModelFactory(resource?.source_resource_type as ResourceType, resource)
this.resourcesLookup[resourceId] = resourceDisplayModel
}catch(e){
console.error(e) //failed to parse a model
return null
}
}
if(!resource.related_resources){
return resourceDisplayModel
} else {
for(let relatedResource of resource.related_resources){
resourceDisplayModel.related_resources[relatedResource.source_resource_type] = resourceDisplayModel.related_resources[relatedResource.source_resource_type] || []
let relatedResourceDisplayModel = this.recExtractResources(relatedResource)
if(relatedResourceDisplayModel){
resourceDisplayModel.related_resources[relatedResource.source_resource_type].push(relatedResourceDisplayModel)
}
}
}
return resourceDisplayModel
}
genResourceId(relatedResource: ResourceFhir): string {
return `/source/${relatedResource?.source_id}/resource/${relatedResource?.source_resource_type}/${relatedResource?.source_resource_id}`
}
} }

View File

@ -0,0 +1,145 @@
<div class="card card-dashboard-seven mb-3">
<div class="card-header tx-medium">
<div class="row cursor-pointer" routerLink="/source/{{eobDisplayModel?.source_id}}/resource/{{eobDisplayModel?.source_resource_id}}">
<!-- Condition Header -->
<div class="col-6">
{{eobDisplayModel?.sort_title ? eobDisplayModel?.sort_title : condition?.display ? condition?.display : 'unknown'}}
</div>
<div class="col-6">
{{eobDisplayModel?.billablePeriod?.start | date }} <span *ngIf="eobDisplayModel?.billablePeriod?.end">- {{eobDisplayModel?.billablePeriod?.end | date}}</span>
</div>
</div>
</div><!-- card-header -->
<div class="card-body">
<div class="row">
<!-- Condition Details -->
<!-- {{conditionDisplayModel | json}}-->
<div class="col-6 mb-2">
<div *ngIf="involvedInCare.length > 0" class="row pl-3">
<div class="col-12 mt-3 mb-2 tx-indigo">
<p>Involved in Care</p>
</div>
<ng-container *ngFor="let practitioner of involvedInCare">
<div class="col-6">
<strong>{{practitioner.displayName}}</strong>
</div>
<div class="col-6">
{{practitioner.role}}
<!-- TODO: add email address link here -->
</div>
</ng-container>
<div *ngIf="condition" class="col-12 mt-3 mb-2">
<p class="tx-indigo">Definition</p>
<app-glossary-lookup [code]="condition?.code" [codeSystem]="condition?.system"></app-glossary-lookup>
</div>
</div>
<div class="row pt-2" *ngIf="explanationOfBenefitGroup?.related_resources?.length > 0">
<div class="col-12">
<a class="cursor-pointer tx-indigo" (click)="collapse.toggle()">show all</a>
<div #collapse="ngbCollapse" [ngbCollapse]="true">
<ul>
<li class="cursor-pointer tx-indigo" *ngFor="let resourceEntry of resourcesLookup | keyvalue" [routerLink]="resourceEntry.key">{{resourceEntry.value.source_resource_type}} {{resourceEntry.value.sort_title ? '- '+resourceEntry.value.sort_title : '' }} </li>
</ul>
</div>
</div>
</div>
</div>
<div class="col-6 bg-gray-100">
<div class="row">
<div class="col-6 mt-3 mb-2 tx-indigo">
<strong>{{eobDisplayModel?.billablePeriod?.start | date}}</strong>
</div>
<div class="col-6 mt-3 mb-2 tx-indigo">
<small>{{locations?.[0]?.name || 'unknown'}}</small>
</div>
<div *ngIf="eobDisplayModel?.related_resources?.MedicationRequest || eobDisplayModel?.related_resources?.Medication" class="col-12 mt-2 mb-2">
<strong>Medications:</strong>
<ul>
<li class="cursor-pointer" [ngbPopover]="medicationRequestPopoverContent" placement="top-left" popoverClass="card-fhir-resource-popover" *ngFor="let medication of eobDisplayModel?.related_resources?.MedicationRequest">
{{medication.display }}
<ng-template #medicationRequestPopoverContent>
<fhir-resource [displayModel]="medication"></fhir-resource>
</ng-template>
</li>
<li class="cursor-pointer" [ngbPopover]="medicationPopoverContent" placement="top-left" popoverClass="card-fhir-resource-popover" *ngFor="let medication of eobDisplayModel?.related_resources?.Medication">
{{medication.title}}
<ng-template #medicationPopoverContent>
<fhir-resource [displayModel]="medication"></fhir-resource>
</ng-template>
</li>
</ul>
</div>
<div *ngIf="procedures.length > 0" class="col-12 mt-2 mb-2">
<strong>Procedures:</strong>
<ul>
<li class="cursor-pointer" [ngbPopover]="procedurePopoverContent" placement="top-left" popoverClass="card-fhir-resource-popover" *ngFor="let procedure of procedures">
{{procedure.display}}
<ng-template #procedurePopoverContent>
<fhir-resource [displayModel]="procedure"></fhir-resource>
</ng-template>
</li>
</ul>
</div>
<div *ngIf="eobDisplayModel?.related_resources?.DiagnosticReport as diagnosticReports" class="col-12 mt-2 mb-2">
<strong>Tests and Examinations:</strong>
<ul>
<li class="cursor-pointer" [ngbPopover]="diagnosticReportPopoverContent" placement="top-left" popoverClass="card-fhir-resource-popover" *ngFor="let diagnosticReport of diagnosticReports">
{{diagnosticReport.title}}
<ng-template #diagnosticReportPopoverContent>
<fhir-resource [displayModel]="diagnosticReport"></fhir-resource>
</ng-template>
</li>
</ul>
</div>
<div *ngIf="eobDisplayModel?.related_resources?.DocumentReference as documentReferences" class="col-12 mt-2 mb-2">
<strong>Attachments:</strong>
<ul>
<li class="cursor-pointer" [ngbPopover]="documentReferencePopoverContent" placement="top-left" popoverClass="card-fhir-resource-popover" *ngFor="let documentReference of documentReferences">
{{documentReference.sort_title}}
<ng-template #documentReferencePopoverContent>
<fhir-resource [displayModel]="documentReference"></fhir-resource>
</ng-template>
</li>
</ul>
</div>
<div *ngIf="eobDisplayModel?.related_resources?.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.model}}
</li>
</ul>
</div>
</div>
</div>
</div>
</div><!-- card-body -->
</div>

View File

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

View File

@ -0,0 +1,161 @@
import {Component, Input, OnInit} from '@angular/core';
import {ResourceFhir} from '../../models/fasten/resource_fhir';
import {FastenDisplayModel} from '../../../lib/models/fasten/fasten-display-model';
import {ResourceType} from '../../../lib/models/constants';
import {CareTeamModel} from '../../../lib/models/resources/care-team-model';
import {PractitionerModel} from '../../../lib/models/resources/practitioner-model';
import {RecResourceRelatedDisplayModel} from '../../../lib/utils/resource_related_display_model';
import {EncounterModel} from '../../../lib/models/resources/encounter-model';
import * as _ from "lodash";
import {ExplanationOfBenefitModel} from '../../../lib/models/resources/explanation-of-benefit-model';
import {MedicationModel} from '../../../lib/models/resources/medication-model';
import {ProcedureModel} from '../../../lib/models/resources/procedure-model';
import {DiagnosticReportModel} from '../../../lib/models/resources/diagnostic-report-model';
import {DeviceModel} from '../../../lib/models/resources/device-model';
import {CodingModel} from '../../../lib/models/datatypes/coding-model';
import {LocationModel} from '../../../lib/models/resources/location-model';
@Component({
selector: 'app-report-medical-history-explanation-of-benefit',
templateUrl: './report-medical-history-explanation-of-benefit.component.html',
styleUrls: ['./report-medical-history-explanation-of-benefit.component.scss']
})
export class ReportMedicalHistoryExplanationOfBenefitComponent implements OnInit {
@Input() explanationOfBenefitGroup: ResourceFhir
eobDisplayModel: Partial<ExplanationOfBenefitModel>
//lookup table for all resources
resourcesLookup: {[name:string]: FastenDisplayModel} = {}
condition: CodingModel
//EOB embeds multiple resource type references
involvedInCare: {displayName: string, role?: string, email?: string}[] = []
locations: LocationModel[] = []
encounters: EncounterModel[] = []
medications: {[resourceId: string]: MedicationModel[]} = {}
procedures: ProcedureModel[] = []
diagnosticReports: {[encounterResourceId: string]: DiagnosticReportModel[]} = {}
device: {[encounterResourceId: string]: DeviceModel[]} = {}
constructor() { }
ngOnInit(): void {
if(!this.explanationOfBenefitGroup){
return
}
//add resources to the lookup table, ensure uniqueness.
let result = RecResourceRelatedDisplayModel(this.explanationOfBenefitGroup)
this.resourcesLookup = result.resourcesLookup
this.eobDisplayModel = result.displayModel
console.log("Resources Lookup", this.resourcesLookup)
let involvedInCareMap: {[resource_id: string]: {displayName: string, role?: string, email?: string}} = {}
//extract data from EOB directly
this.condition = this.eobDisplayModel.diagnosis?.[0]?.diagnosisCodeableConcept?.coding?.[0]
this.eobDisplayModel.careTeam?.forEach((careTeam) => {
if(careTeam.provider.reference){
return
}
let id = careTeam.role + careTeam.provider.display
involvedInCareMap[id] = _.mergeWith(
{},
involvedInCareMap[id],
{
displayName: careTeam.provider?.display,
role: careTeam.role?.[0]?.text,
},
)
})
this.eobDisplayModel.procedures?.forEach((procedure) => {
let procedureModel = new ProcedureModel({})
procedureModel.performed_datetime = procedure.date
procedureModel.coding = procedure.procedureCodeableConcept.coding
procedureModel.display = procedure.procedureCodeableConcept.text || procedure.procedureCodeableConcept.coding?.[0]?.display
this.procedures.push(procedureModel)
})
console.log("CONDITION", this.condition)
// this.medications = this.eobDisplayModel.prescription
//loop though all resources, process display data
for(let resourceId in this.resourcesLookup){
let resource = this.resourcesLookup[resourceId]
switch(resource.source_resource_type){
case ResourceType.CareTeam:
for(let participant of (resource as CareTeamModel).participants){
let id = participant.reference.reference || participant.display
involvedInCareMap[id] = _.mergeWith(
{},
involvedInCareMap[id],
{
displayName: participant.display,
role: participant.role
},
)
}
break
case ResourceType.Practitioner:
let practitionerModel = resource as PractitionerModel
let id = `${resource.source_resource_type}/${resource.source_resource_id}`
let telecomEmails = _.find(practitionerModel.telecom, {"system": "email"})
let email = _.get(telecomEmails, '[0].value')
let qualification = _.find(practitionerModel.qualification, {"system": "http://nucc.org/provider-taxonomy"}) as CodingModel
involvedInCareMap[id] = _.mergeWith(
{},
involvedInCareMap[id],
{
displayName: practitionerModel.name?.family && practitionerModel.name?.given ? `${practitionerModel.name?.family }, ${practitionerModel.name?.given}` : practitionerModel.name?.text,
role: qualification?.display || practitionerModel.name?.prefix || practitionerModel.name?.suffix,
email: email,
},
)
break
case ResourceType.Encounter:
this.encounters.push(resource as EncounterModel);
(resource as EncounterModel).participant?.map((participant) => {
let id = participant.reference.reference
involvedInCareMap[id] = _.mergeWith(
{},
involvedInCareMap[id],
{
displayName: participant.display,
role: participant.role,
},
)
})
break
case ResourceType.Location:
this.locations.push(resource as LocationModel)
// case ResourceType.ExplanationOfBenefit:
// let eobDisplayModel = (resource as ExplanationOfBenefitModel)
// involvedInCareMap[eobDisplayModel.provider.reference] =
break
case ResourceType.Procedure:
this.procedures.push(resource as ProcedureModel)
break
}
}
console.log("GENERATED INVOLVED IN CARE MAP", involvedInCareMap)
for(let resourceId in involvedInCareMap){
this.involvedInCare.push(involvedInCareMap[resourceId])
}
}
}

View File

@ -3,6 +3,9 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ResourceListComponent } from './resource-list.component'; import { ResourceListComponent } from './resource-list.component';
import {HttpClientTestingModule} from '@angular/common/http/testing'; import {HttpClientTestingModule} from '@angular/common/http/testing';
import {ResourceListOutletDirective} from './resource-list-outlet.directive'; import {ResourceListOutletDirective} from './resource-list-outlet.directive';
import {FastenApiService} from '../../services/fasten-api.service';
import {HTTP_CLIENT_TOKEN} from '../../dependency-injection';
import {HttpClient} from '@angular/common/http';
describe('ResourceListComponent', () => { describe('ResourceListComponent', () => {
let component: ResourceListComponent; let component: ResourceListComponent;
@ -11,7 +14,14 @@ describe('ResourceListComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [HttpClientTestingModule], imports: [HttpClientTestingModule],
declarations: [ ResourceListComponent, ResourceListOutletDirective ] declarations: [ ResourceListComponent, ResourceListOutletDirective ],
providers: [
FastenApiService,
{
provide: HTTP_CLIENT_TOKEN,
useClass: HttpClient,
},
]
}) })
.compileComponents(); .compileComponents();

View File

@ -68,6 +68,9 @@ import { DocumentReferenceComponent } from './fhir/resources/document-reference/
import { DicomComponent } from './fhir/datatypes/dicom/dicom.component'; import { DicomComponent } from './fhir/datatypes/dicom/dicom.component';
import { MediaComponent } from './fhir/resources/media/media.component'; import { MediaComponent } from './fhir/resources/media/media.component';
import { GlossaryLookupComponent } from './glossary-lookup/glossary-lookup.component'; import { GlossaryLookupComponent } from './glossary-lookup/glossary-lookup.component';
import { ReportMedicalHistoryExplanationOfBenefitComponent } from './report-medical-history-explanation-of-benefit/report-medical-history-explanation-of-benefit.component';
import {GridstackComponent} from './gridstack/gridstack.component';
import {GridstackItemComponent} from './gridstack/gridstack-item.component';
@NgModule({ @NgModule({
imports: [ imports: [
@ -83,6 +86,29 @@ import { GlossaryLookupComponent } from './glossary-lookup/glossary-lookup.compo
ChartsModule, ChartsModule,
HighlightModule, HighlightModule,
PipesModule, PipesModule,
//standalone components
LoadingSpinnerComponent,
GlossaryLookupComponent,
BadgeComponent,
TableComponent,
CodingComponent,
AllergyIntoleranceComponent,
MedicationComponent,
MedicationRequestComponent,
PractitionerComponent,
ProcedureComponent,
ImmunizationComponent,
BinaryTextComponent,
HtmlComponent,
ImgComponent,
PdfComponent,
MarkdownComponent,
DicomComponent,
BinaryComponent,
GridstackComponent,
GridstackItemComponent,
], ],
declarations: [ declarations: [
ComponentsSidebarComponent, ComponentsSidebarComponent,
@ -119,31 +145,15 @@ import { GlossaryLookupComponent } from './glossary-lookup/glossary-lookup.compo
ReportMedicalHistoryEditorComponent, ReportMedicalHistoryEditorComponent,
ReportMedicalHistoryConditionComponent, ReportMedicalHistoryConditionComponent,
ReportLabsObservationComponent, ReportLabsObservationComponent,
LoadingSpinnerComponent,
BinaryComponent,
PdfComponent,
ImgComponent,
BinaryTextComponent,
MarkdownComponent,
HtmlComponent,
FhirResourceComponent, FhirResourceComponent,
FhirResourceOutletDirective, FhirResourceOutletDirective,
FallbackComponent, FallbackComponent,
ImmunizationComponent,
BadgeComponent,
TableComponent,
CodingComponent,
AllergyIntoleranceComponent,
MedicationComponent,
MedicationRequestComponent,
ProcedureComponent,
DiagnosticReportComponent, DiagnosticReportComponent,
PractitionerComponent,
NlmTypeaheadComponent, NlmTypeaheadComponent,
DocumentReferenceComponent, DocumentReferenceComponent,
GlossaryLookupComponent,
DicomComponent,
MediaComponent, MediaComponent,
ReportMedicalHistoryExplanationOfBenefitComponent,
], ],
exports: [ exports: [
ComponentsSidebarComponent, ComponentsSidebarComponent,
@ -178,25 +188,36 @@ import { GlossaryLookupComponent } from './glossary-lookup/glossary-lookup.compo
ReportHeaderComponent, ReportHeaderComponent,
ReportMedicalHistoryEditorComponent, ReportMedicalHistoryEditorComponent,
ReportMedicalHistoryConditionComponent, ReportMedicalHistoryConditionComponent,
ReportMedicalHistoryExplanationOfBenefitComponent,
ReportLabsObservationComponent, ReportLabsObservationComponent,
LoadingSpinnerComponent,
BinaryComponent, BinaryComponent,
FhirResourceComponent, FhirResourceComponent,
FhirResourceOutletDirective, FhirResourceOutletDirective,
FallbackComponent, FallbackComponent,
ImmunizationComponent, ImmunizationComponent,
BadgeComponent,
TableComponent,
CodingComponent,
AllergyIntoleranceComponent,
MedicationComponent,
MedicationRequestComponent, MedicationRequestComponent,
ProcedureComponent, ProcedureComponent,
DiagnosticReportComponent, DiagnosticReportComponent,
PractitionerComponent, PractitionerComponent,
NlmTypeaheadComponent, NlmTypeaheadComponent,
DocumentReferenceComponent, DocumentReferenceComponent,
GlossaryLookupComponent
//standalone components
BadgeComponent,
TableComponent,
CodingComponent,
LoadingSpinnerComponent,
GlossaryLookupComponent,
AllergyIntoleranceComponent,
MedicationComponent,
MedicationRequestComponent,
PractitionerComponent,
ProcedureComponent,
ImmunizationComponent,
BinaryComponent,
GridstackComponent,
GridstackItemComponent,
] ]
}) })

View File

@ -0,0 +1,3 @@
import {InjectionToken} from "@angular/core";
export const HTTP_CLIENT_TOKEN = new InjectionToken<string>('__HTTP_CLIENT_TOKEN__');

View File

@ -0,0 +1,27 @@
export class DashboardWidgetConfig {
id: string
item_type: "bar_chart" | "bubble_chart" | "doughnut_chart" | "line_chart" | "pie_chart" | "scatter_chart" | "calendar" | "striped_table" | "basic_table"
title_text: string
description_text: string
queries: {
q: string,
"aggregator": "avg",
"conditional_formats": [],
"type": "line",
"style": {
"palette": "grey" | "pastel" | "light" | "default"
}
}[]
//used for display purposes within the Dashboard, not for the actual chart
minWidth?: number
minHeight?: number
width: number
height: number
x?: number
y?: number
}

View File

@ -5,6 +5,8 @@ import {HttpClientTestingModule} from '@angular/common/http/testing';
import {RouterTestingModule} from '@angular/router/testing'; import {RouterTestingModule} from '@angular/router/testing';
import {RouterModule} from '@angular/router'; import {RouterModule} from '@angular/router';
import {FormsModule} from '@angular/forms'; import {FormsModule} from '@angular/forms';
import {HTTP_CLIENT_TOKEN} from '../../dependency-injection';
import {HttpClient} from '@angular/common/http';
describe('AuthSigninComponent', () => { describe('AuthSigninComponent', () => {
let component: AuthSigninComponent; let component: AuthSigninComponent;
@ -14,6 +16,12 @@ describe('AuthSigninComponent', () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ AuthSigninComponent ], declarations: [ AuthSigninComponent ],
imports: [HttpClientTestingModule, FormsModule, RouterTestingModule], imports: [HttpClientTestingModule, FormsModule, RouterTestingModule],
providers: [
{
provide: HTTP_CLIENT_TOKEN,
useClass: HttpClient,
},
]
}) })
.compileComponents(); .compileComponents();

View File

@ -3,6 +3,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AuthSignupComponent } from './auth-signup.component'; import { AuthSignupComponent } from './auth-signup.component';
import {HttpClientTestingModule} from '@angular/common/http/testing'; import {HttpClientTestingModule} from '@angular/common/http/testing';
import {FormsModule} from '@angular/forms'; import {FormsModule} from '@angular/forms';
import {HTTP_CLIENT_TOKEN} from '../../dependency-injection';
import {HttpClient} from '@angular/common/http';
describe('AuthSignupComponent', () => { describe('AuthSignupComponent', () => {
let component: AuthSignupComponent; let component: AuthSignupComponent;
@ -12,6 +14,12 @@ describe('AuthSignupComponent', () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ AuthSignupComponent ], declarations: [ AuthSignupComponent ],
imports: [HttpClientTestingModule, FormsModule], imports: [HttpClientTestingModule, FormsModule],
providers: [
{
provide: HTTP_CLIENT_TOKEN,
useClass: HttpClient,
},
]
}) })
.compileComponents(); .compileComponents();

View File

@ -31,165 +31,77 @@
</nav> </nav>
<nav class="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-save"></i> Export</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-file-pdf"></i> Create</a>
<a class="nav-link" routerLink="/" ngbTooltip="not yet implemented"><i class="far fa-envelope"></i>Send to Email</a> <a class="nav-link" (click)="toggleEditableGrid()"><i class="fas fa-edit"></i> Edit </a>
<a class="nav-link" routerLink="/" ngbTooltip="not yet implemented"><i class="fas fa-ellipsis-h"></i></a> <a class="nav-link"><i class="fas fa-ellipsis"></i></a>
</nav> </nav>
</div> </div>
<!-- Summary Cards --> <div class="row mt-5 mb-3">
<div class="row row-sm"> <div class="col-12">
<div class="col-md-4 col-lg-4 mg-b-20 mg-md-b-0 mg-lg-b-20">
<div class="card card-dashboard-five">
<div class="card-header">
<h6 class="card-title">Medical Records</h6>
<span class="card-text">Summary of medical encounters and records stored in Fasten</span>
</div><!-- card-header -->
<div class="card-body row row-sm">
<div class="col-6 d-sm-flex align-items-center">
<div class="card-chart bg-primary">
<canvas baseChart class="w-50" [chartType]="'bar'" [datasets]="acquisitionOneChartData" [labels]="acquisitionOneChartLabels" [options]="acquisitionOneChartOptions" [colors]="acquisitionOneChartColors"></canvas>
</div>
<div>
<label>Encounters</label>
<h4>{{encounterCount}}</h4>
</div>
</div><!-- col -->
<div class="col-6 d-sm-flex align-items-center">
<div class="card-chart bg-purple">
<canvas baseChart class="w-50" [chartType]="'bar'" [datasets]="acquisitionTwoChartData" [labels]="acquisitionTwoChartLabels" [options]="acquisitionTwoChartOptions" [colors]="acquisitionTwoChartColors"></canvas>
</div>
<div>
<label>All Records</label>
<h4>{{recordsCount}}</h4>
</div>
</div><!-- col -->
</div><!-- card-body -->
</div><!-- card-dashboard-five -->
</div><!-- col -->
<div class="col-md-4 col-lg-4">
<div class="card card-dashboard-five">
<div class="card-header">
<h6 class="card-title">Sources</h6>
<span class="card-text"> A source is a medical provider, hospital or insurance company that Fasten can import your data from</span>
</div><!-- card-header -->
<div class="card-body row row-sm">
<div class="col-6">
<div class="d-flex align-items-center">
<div class="mg-b-10 mg-sm-b-0 mg-sm-r-10 wd-50">
<canvas baseChart [chartType]="'doughnut'" [datasets]="sessionsChartOneData" [labels]="sessionsChartOneLabels" [options]="sessionsChartOneOptions" height="45"></canvas>
</div>
<div>
<label>Sources</label>
<h4>{{sources.length}}</h4>
</div>
</div>
</div><!-- col -->
<div class="col-6 d-flex align-items-center">
<div class="d-flex align-items-center">
<div class="mg-b-10 mg-sm-b-0 mg-sm-r-10 wd-50">
<canvas baseChart [chartType]="'doughnut'" [datasets]="sessionsChartTwoData" [labels]="sessionsChartTwoLabels" [options]="sessionsChartTwoOptions" height="45"></canvas>
</div>
<div>
<label>Updates</label>
<h4>19</h4>
</div>
</div>
</div><!-- col -->
</div><!-- card-body -->
</div><!-- card-dashboard-five -->
</div><!-- col -->
</div><!-- row -->
<div *ngIf="!loading else isLoadingTemplate" class="card card-dashboard-seven">
<div class="card-header">
<div class="row row-sm">
<div class="col-6 col-md-4 col-xl">
<div class="media">
<div ><fa-icon [icon]="['fas', 'calendar']"></fa-icon></div>
<div class="media-body"><label >Start Date</label>
<div class="date"><span >Sept 01, 2018</span>
<a ngbTooltip="not yet implemented"><fa-icon [icon]="['fas', 'caret-down']"></fa-icon></a>
</div>
</div>
</div>
</div>
<div class="col-6 col-md-4 col-xl">
<div class="media">
<div ><fa-icon [icon]="['fas', 'calendar']"></fa-icon></div>
<div class="media-body"><label >End Date</label>
<div class="date"><span >Sept 30, 2018</span><a ngbTooltip="not yet implemented"><fa-icon [icon]="['fas', 'caret-down']"></fa-icon></a>
</div>
</div>
</div>
</div>
<div class="col-6 col-md-4 col-xl mg-t-15 mg-md-t-0">
<div class="media">
<div ><fa-icon [icon]="['fas', 'hospital']"></fa-icon></div>
<div class="media-body"><label >Source Type</label>
<div class="date"><span >All</span><a ngbTooltip="not yet implemented"><fa-icon [icon]="['fas', 'caret-down']"></fa-icon></a>
</div>
</div>
</div>
</div>
<div class="alert alert-warning" role="alert">
<strong>Warning!</strong> Fasten Health is in the process of implementing a customizable widget based dashboard.
<br/>
<ul>
<li>Users will be able to add, remove, and re-organize widgets on their dashboard.</li>
<li>Users will be able to create multiple dashboards, and switch between them.</li>
<li>Users will be able to share their dashboards with other users.</li>
</ul>
<br/>
<strong>This functionality is not yet available</strong>, but this example dashboard below will give you a sense of what this may look like. This dashboard only contains placeholder data.
</div> </div>
</div>
<div class="card-body">
<div *ngIf="sources.length; else emptyDashboard" class="table-responsive">
<table class="table mg-b-0">
<tbody>
<tr *ngFor="let source of sources" (click)="selectSource(source)" class="alert cursor-pointer" role="alert">
<td class="align-middle">
<div class="media">
<img [src]="'assets/sources/'+source.source_type+'.png'"
alt="{{source.source_type}}"
class="mr-3"
style="width:100px;">
<div class="media-body">
<h5>{{metadataSource[source.source_type]?.display}}</h5>
<p>
{{getPatientSummary(patientForSource[source.id]?.resource_raw)}}
</p>
</div>
</div>
</td>
<td class="align-middle"><p><small class="tx-gray-600">status:</small><br/> {{isActive(source)}}</p></td>
<td class="align-middle"><p><small class="tx-gray-600">last updated:</small><br/> <span [ngbTooltip]="source.updated_at | date">{{source.updated_at | amTimeAgo}}</span></p></td>
<td class="align-middle"><p><small class="tx-gray-600">expires:</small><br/> <span [ngbTooltip]="source.expires_at | amFromUnix | date">{{source.expires_at | amFromUnix | amTimeAgo}}</span></p></td>
<td class="align-middle"><p><i class="fas fa-chevron-right"></i></td>
</tr>
</tbody>
</table>
</div><!-- table-responsive -->
<ng-template #emptyDashboard>
<div class="modal-body tx-center pd-y-20 pd-x-20">
<h4 class="tx-purple mg-b-20">No Sources Connected!</h4>
<p class="mg-b-20 mg-x-20">
Fasten securely connects your healthcare providers together, creating a single location to access your entire medical history.
</p>
<p class="mg-b-20 mg-x-20">
Click below to add a new healthcare provider to Fasten.
</p>
<button [routerLink]="'/sources'" type="button" class="btn btn-purple pd-x-25">Add Source</button>
</div><!-- modal-body -->
</ng-template>
</div> </div>
</div> </div>
<ng-template #isLoadingTemplate> <gridstack id="gridstack" #gridComp [options]="gridOptions">
<div class="row"> <gridstack-item class="grid-stack-item" gs-w="8" gs-h="5" gs-x="0" gs-y="0">
<div class="col-12"> <app-complex-line-widget></app-complex-line-widget>
<app-loading-spinner [loadingTitle]="'Please wait, loading sources...'"></app-loading-spinner> </gridstack-item>
</div>
</div>
</ng-template> <gridstack-item class="grid-stack-item" gs-w="2" gs-h="2" gs-x="8" gs-y="0">
<app-simple-line-chart-widget></app-simple-line-chart-widget>
</gridstack-item>
<gridstack-item class="grid-stack-item" gs-w="2" gs-h="2" gs-x="10" gs-y="0">
<app-simple-line-chart-widget></app-simple-line-chart-widget>
</gridstack-item>
<gridstack-item class="grid-stack-item" gs-w="4" gs-h="3" gs-x="8" gs-y="2">
<app-grouped-bar-chart-widget></app-grouped-bar-chart-widget>
</gridstack-item>
<gridstack-item class="grid-stack-item" gs-w="4" gs-h="5" gs-x="0" gs-y="5">
<app-patient-vitals-widget></app-patient-vitals-widget>
</gridstack-item>
<gridstack-item class="grid-stack-item" gs-w="8" gs-h="5" gs-x="4" gs-y="5">
<app-donut-chart-widget></app-donut-chart-widget>
</gridstack-item>
<gridstack-item class="grid-stack-item" gs-w="4" gs-h="2" gs-x="0" gs-y="10">
<app-dual-gauges-widget></app-dual-gauges-widget>
</gridstack-item>
<gridstack-item class="grid-stack-item" gs-w="8" gs-h="4" gs-x="4" gs-y="10">
<app-table-widget></app-table-widget>
</gridstack-item>
<gridstack-item *ngFor="let item of dashboardItems" class="grid-stack-item"
[attr.gs-w]="item.width"
[attr.gs-h]="item.height"
[attr.gs-x]="item.x"
[attr.gs-y]="item.y"
[attr.gs-min-w]="item.minWidth"
[attr.gs-min-h]="item.minHeight"
>
<app-dashboard-widget [widgetConfig]="item"></app-dashboard-widget>
</gridstack-item>
</gridstack>
</div><!-- az-content-body --> </div><!-- az-content-body -->
</div> </div>

View File

@ -4,6 +4,8 @@ import { DashboardComponent } from './dashboard.component';
import {HttpClientTestingModule} from '@angular/common/http/testing'; import {HttpClientTestingModule} from '@angular/common/http/testing';
import {RouterTestingModule} from '@angular/router/testing'; import {RouterTestingModule} from '@angular/router/testing';
import {RouterModule} from '@angular/router'; import {RouterModule} from '@angular/router';
import {HTTP_CLIENT_TOKEN} from '../../dependency-injection';
import {HttpClient} from '@angular/common/http';
describe('DashboardComponent', () => { describe('DashboardComponent', () => {
let component: DashboardComponent; let component: DashboardComponent;
@ -12,7 +14,13 @@ describe('DashboardComponent', () => {
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [HttpClientTestingModule, RouterTestingModule, RouterModule], imports: [HttpClientTestingModule, RouterTestingModule, RouterModule],
declarations: [ DashboardComponent ] declarations: [ DashboardComponent ],
providers: [
{
provide: HTTP_CLIENT_TOKEN,
useClass: HttpClient,
},
]
}) })
.compileComponents(); .compileComponents();
})); }));

View File

@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core'; import { Component, ComponentFactoryResolver, EmbeddedViewRef, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
import {Source} from '../../models/fasten/source'; import {Source} from '../../models/fasten/source';
import {Router} from '@angular/router'; import {Router} from '@angular/router';
import {ResourceFhir} from '../../models/fasten/resource_fhir'; import {ResourceFhir} from '../../models/fasten/resource_fhir';
@ -7,6 +7,15 @@ import {MetadataSource} from '../../models/fasten/metadata-source';
import {FastenApiService} from '../../services/fasten-api.service'; import {FastenApiService} from '../../services/fasten-api.service';
import {Summary} from '../../models/fasten/summary'; import {Summary} from '../../models/fasten/summary';
import {LighthouseService} from '../../services/lighthouse.service'; import {LighthouseService} from '../../services/lighthouse.service';
import { GridStack, GridStackOptions, GridStackWidget } from 'gridstack';
import { GridstackComponent } from '../../components/gridstack/gridstack.component';
import {DashboardWidgetComponent} from '../../widgets/dashboard-widget/dashboard-widget.component';
import {DashboardWidgetConfig} from '../../models/widget/dashboard-widget-config';
// unique ids sets for each item for correct ngFor updating
//TODO: fix this
let ids = 1;
@Component({ @Component({
selector: 'app-dashboard', selector: 'app-dashboard',
@ -23,10 +32,14 @@ export class DashboardComponent implements OnInit {
metadataSource: { [name: string]: MetadataSource } metadataSource: { [name: string]: MetadataSource }
@ViewChild(GridstackComponent) gridComp?: GridstackComponent;
constructor( constructor(
private lighthouseApi: LighthouseService, private lighthouseApi: LighthouseService,
private fastenApi: FastenApiService, private fastenApi: FastenApiService,
private router: Router private router: Router,
private componentFactoryResolver: ComponentFactoryResolver,
private vcRef: ViewContainerRef,
) { } ) { }
ngOnInit() { ngOnInit() {
@ -89,470 +102,81 @@ export class DashboardComponent implements OnInit {
return expiresDate < currentDate ? 'active' : 'expired' return expiresDate < currentDate ? 'active' : 'expired'
} }
pageViewChartData = [{
label: 'This week',
data: [36.57, 38.9, 42.3, 41.8, 37.4, 32.5, 28.1, 24.7, 23.4, 20.4, 16.5, 12.1, 9.2, 5.1, 9.6, 10.8, 13.2, 18.2, 13.9, 18.7, 13.7, 11.3, 13.7, 15.8, 12.9, 17.5, 21.9, 18.2, 14.3, 18.2, 14.8, 13.01, 14.5, 15.4, 16.6, 19.4, 14.5, 17.7, 13.8, 9.4, 11.9, 9.7, 6.1, 1.4, 2.3, 2.3, 4.5, 3.7, 5.7, 5.08, 1.9, 8.2,
7.9, 5.02, 2.8, 6.8, 6.2, 9.8, 9.3, 11.9, 10, 9, 6, 4.5, 2.7, 4.3, 3.6, 4.2, 2, 1.4, 3.7, 1.5, 5.7, 4.9, 1, 4.7, 6.3, 4.2, 5.1, 5.2, 3.8, 8.2, 7.2, 6.5, 1.7, 11.4, 10.5, 3.8, 4.7, 8.5, 10.2, 11, 15.6, 19.7, 18.1, 13.5, 12, 7.5, 3.7, 9.7, 9.2, 13.4, 18.4, 22.4, 18.7, 15.2, 14.5, 14.4, 12, 13.7, 13.3, 15.4,
15.8, 17.7, 14.3, 10.6, 12.7, 14.7, 18.6, 22.9, 18, 22.8, 23.8, 27.1, 24.7, 20, 22.7, 20.9, 16.6, 15.1, 13.1, 10.7, 11.4, 13.1, 10.1, 9.2, 9.2, 10.3, 15.2, 12.5, 14, 18.2, 16.3, 17.7, 18.9, 15.3, 18.1, 16.3, 14.8, 10 ],
borderWidth: 2,
fill: true
},
{
label: 'Current week',
data: [53, 50.3, 49.4, 47.7, 49, 50.6, 48.7, 48.8, 53.5, 52.9, 49, 50.2, 48.3, 44.8, 40.7, 41.2, 45.6, 44.6, 41.3, 38.2, 39.6, 41, 39.4, 35.6, 38.5, 38.5, 40.6, 38.7, 42.9, 46.3, 43.5, 40.6, 36.5, 31.7, 28.9, 29.6, 29.5, 33.1, 37, 35.8, 37.6, 39.6, 39, 34.1, 37.4, 39.2, 38.4, 37.7, 40.1, 35.8, 31.5, 31.8,
30.5, 25.7, 28.2, 28.4, 30, 32.1, 32.9, 37.6, 35.2, 39.1, 41.3, 41.4, 43.7, 39.4, 39.2, 43.8, 42.4, 43.6, 38.7 , 43.5, 41.8, 44.8, 46.1, 47.6, 49, 46.4, 51.2, 50.1, 53.6, 56, 52.7, 56.6, 60.2, 58.3, 56.5, 55.7, 54.7, 54.2, 58.6, 57, 60.5, 57.6, 56.1, 55.1, 54.3, 52.3, 54.5, 54.1, 51.9, 51.1, 46.3, 48.3,
45.8, 48.2, 43.3, 45.8, 43.4, 41.3, 40.9, 38.4, 40.1, 44.8, 44, 41.4, 37.8, 39.2, 35.2, 32.1, 35.6, 38, 37.9, 38.7, 37.4, 37.5, 33.1, 35, 33.1, 31.8, 29.1, 31.9, 34.3, 32.9, 33.1, 37.1, 32.6, 36.9, 35.9, 38.1, 42.5, 41.5, 45.5, 46.3, 45.7, 45.4, 42.5, 44.4, 39.7, 44.7],
borderWidth: 2,
fill: true
}];
pageViewChartLabels = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '30', '31', '32', '33', '34', '35', '36', '37', '38', '39', '40', '41', '42', '43', '44', '45', '46', '47', '48', '49', // GridStack options and configuration for tesitng.
'50', '51', '52', '53', '54', '55', '56', '57', '58', '59', '60', '61', '62', '63', '64', '65', '66', '67', '68', '69', '70', '71', '72', '73', '74', '75', '76', '77', '78', '79', '80', '81', '82', '83', '84', '85', '86', '87', '88', '89', '90', '91', '92', '93', '94', '95', '96', '97', '98', '99',
'100', '101', '102', '103', '104', '105', '106', '107', '108', '109', '110', '111', '112', '113', '114', '115', '116', '117', '118', '119', '120', '121', '122', '123', '124', '125', '126', '127', '128', '129', '130', '131', '132', '133', '134', '135', '136', '137', '138', '139', '140', '141', '142', '143', '144', '145', '146', '147', '148', '149'];
pageViewChartOptions = { public gridEditDisabled = true;
responsive: true, public gridOptions: GridStackOptions = {
maintainAspectRatio: false, margin: 5,
scales: { float: false,
yAxes: [{ minRow: 1,
display: true, acceptWidgets: false,
gridLines: { alwaysShowResizeHandle: true,
drawBorder: false,
display: true,
drawTicks: false,
color: '#eef0fa',
zeroLineColor: 'rgba(90, 113, 208, 0)',
},
ticks: {
display: false,
beginAtZero: true,
min: 0,
max: 100,
stepSize: 32,
padding: 10,
}
}],
xAxes: [{
display: false,
position: 'bottom',
gridLines: {
drawBorder: false,
display: false,
drawTicks: false,
},
ticks: {
beginAtZero: true,
stepSize: 10,
fontColor: "#a7afb7",
padding: 10,
}
}],
},
legend: {
display: false,
},
elements: {
point: {
radius: 0
},
line: {
tension: 0
}
},
tooltips: {
backgroundColor: 'rgba(2, 171, 254, 1)',
},
};
pageViewChartColors = [ //these 2 options can be used to enable/disable editability
{ // disableDrag: true,
backgroundColor: [ // disableResize: true
'rgba(255, 255, 255, 1)', // children: [
], // // {x: 0, y: 0, minW: 2},
borderColor: [ // // {x: 1, y: 1},
'rgb(0, 123, 255)' // // {x: 2, y: 2},
] // ],
}, }
{
backgroundColor: [ public toggleEditableGrid() {
'rgba(86, 11, 208, .05)', this.gridEditDisabled = !this.gridEditDisabled;
], console.log('toggle - is disabled', this.gridEditDisabled)
borderColor: [
'rgb(86, 11, 208)' this.gridEditDisabled ? this.gridComp.grid?.disable(true) : this.gridComp.grid?.enable(true);
],
} }
];
public dashboardItems: DashboardWidgetConfig[] = []
/**
* TEST dynamic grid operations - uses grid API directly (since we don't track structure that gets out of sync)
*/
public add(gridComp: GridstackComponent) {
// TODO: BUG the content doesn't appear until widget is moved around (or another created). Need to force
// angular detection changes...
// gridComp.grid?.addWidget({x:3, y:0, w:2, content:`item ${ids}`, id:String(ids++)});
this.dashboardItems.push({x:3, y:0, width:4, height:3, id:String(ids++)} as DashboardWidgetConfig)
// this.makeWidget(gridComp);
}
public delete(gridComp: GridstackComponent) {
gridComp.grid?.removeWidget(gridComp.grid.engine.nodes[0]?.el!);
}
public modify(gridComp: GridstackComponent) {
gridComp.grid?.update(gridComp.grid.engine.nodes[0]?.el!, {w:3})
}
public newLayout(gridComp: GridstackComponent) {
this.dashboardItems = [
{x:0, y:0, id:'1', width:4, height: 4, item_type: "calendar"}, // new size/constrain
{x:4, y:0, id:'2', width:4, height: 4, item_type: "basic_table"},
{x:8, y:0, id:'3', width:3, height: 3, item_type: "line_chart"}, // delete item
{x:3, y:5, w:2, width:4, height:3}, // new item
] as DashboardWidgetConfig[];
}
//
bounceRateChartData = [{ // getRootNodeFromParsedComponent(component: any) {
label: 'This week', // const componentFactory =
data: [27.2, 29.9, 29.6, 25.7, 25.9, 29.3, 31.1, 27.9, 28.4, 25.4, 23.2, 18.2, 14, 12.7, 11, 13.7, 9.7, 12.6, 10.9, 12.7, 13.8, 12.9, 13.8, 10.2, 5.8, 7.6, 8.8, 5.6, 5.6, 6.3, 4.2, 3.6, 5.4, 6.5, 8.1, 10.9, 7.6, 9.7, 10.9, 9.5, 5.4, 4.9, .7, 2.3, 5.5, 10, 10.6, 8.3, 8.4, 8.5, 5.8 ], // this.componentFactoryResolver.resolveComponentFactory(component);
borderWidth: 2, // let ref = this.vcRef?.createComponent(componentFactory);
fill: true //
}]; // const hostView = <EmbeddedViewRef<any>>ref?.hostView;
// return hostView.rootNodes[0];
bounceRateChartLabels = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '30', '31', '32', '33', '34', '35', '36', '37', '38', '39', '40', '41', '42', '43', '44', '45', '46', '47', '48', '49', '50', '51']; // }
//
bounceRateChartOptions = { // makeWidget(gridComp: GridstackComponent) {
// console.log('called makeWidget');
responsive:true, //
maintainAspectRatio:false, // gridComp.grid?.el.appendChild(
scales: { // this.getRootNodeFromParsedComponent(DashboardItemComponent)
yAxes: [{ // );
display: false, // gridComp.grid?.makeWidget('#widget1');
gridLines: { // }
drawBorder: false,
display: true,
drawTicks: false,
},
ticks: {
display: false,
beginAtZero: true,
min: 0,
max: 40,
stepSize: 10,
}
}],
xAxes: [{
display: false,
position: 'bottom',
gridLines: {
drawBorder: false,
display: false,
drawTicks: false,
},
ticks: {
beginAtZero: true,
stepSize: 10,
fontColor: "#a7afb7",
padding: 10,
}
}],
},
legend: {
display: false,
},
elements: {
point: {
radius: 0
},
line: {
tension: 0
}
},
tooltips: {
backgroundColor: 'rgba(2, 171, 254, 1)',
},
};
bounceRateChartColors = [
{
backgroundColor: [
'rgba(0, 204, 212, .2)',
],
borderColor: [
'rgb(0, 204, 212)'
]
}
];
// Total users chart
usersBarChartData = [{
label: '# of Votes',
data: [27.2, 29.9, 29.6, 25.7, 25.9, 29.3, 31.1, 27.9, 28.4, 25.4, 23.2, 18.2, 14, 12.7, 11, 13.7, 9.7, 12.6, 10.9, 12.7, 13.8],
borderWidth: 1,
fill: false
}];
usersBarChartLabels = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18'];
usersBarChartOptions = {
responsive:true,
maintainAspectRatio:false,
scales: {
yAxes: [{
display: false,
ticks: {
display: false,
},
gridLines: {
drawBorder: false,
display: false
}
}],
xAxes: [{
display: false,
barThickness: 5.5,
ticks: {
display: false,
},
gridLines: {
drawBorder: false,
display: false
}
}]
},
legend: {
display: false
},
elements: {
point: {
radius: 0
}
}
};
usersBarChartColors = [
{
backgroundColor: '#007bff',
borderColor: '#007bff'
}
];
// Total users chart
sessionsChartData = [{
label: '# of Votes',
data: [2, 4, 10, 20, 45, 40, 35, 18],
borderWidth: 1,
fill: false
},
{
label: '# of Rate',
data: [3, 6, 15, 35, 50, 45, 35, 25],
borderWidth: 1,
fill: false
}];
sessionsChartLabels = [0,1,2,3,4,5,6,7];
sessionsChartOptions = {
responsive:true,
maintainAspectRatio:false,
scales: {
yAxes: [{
display: false,
ticks: {
beginAtZero:true,
fontSize: 11,
max: 80
},
gridLines: {
drawBorder: false,
}
}],
xAxes: [{
barPercentage: 0.6,
gridLines: {
color: 'rgba(0,0,0,0.08)',
drawBorder: false
},
ticks: {
beginAtZero:true,
fontSize: 11,
display: false
}
}]
},
legend: {
display: false
},
elements: {
point: {
radius: 0
}
}
};
sessionsChartColors = [
{
backgroundColor: '#560bd0'
},
{
backgroundColor: '#cad0e8'
}
];
// Sessions by channel doughnut chart
sessionsByChannelChartData = [{
data: [25,20,30,15,10],
backgroundColor: ['#6f42c1', '#007bff','#17a2b8','#00cccc','#adb2bd'],
}];
sessionsByChannelChartLabels: ['Search', 'Email', 'Referral', 'Social', 'Other'];
sessionsByChannelChartOptions = {
cutoutPercentage: 50,
maintainAspectRatio: false,
responsive: true,
legend: {
display: false,
},
animation: {
animateScale: true,
animateRotate: true
}
};
// Sessions by channel doughnut chart
sessionsChartOneData = [{
data: [40,60],
backgroundColor: ['#007bff', '#cad0e8'],
borderColor: ['#007bff', '#cad0e8'],
}];
sessionsChartOneLabels: ['Search', 'Email'];
sessionsChartOneOptions = {
cutoutPercentage: 78,
maintainAspectRatio: false,
responsive: true,
legend: {
display: false,
},
animation: {
animateScale: true,
animateRotate: true
}
};
// Sessions by channel doughnut chart
sessionsChartTwoData = [{
data: [25,75],
backgroundColor: ['#00cccc', '#cad0e8'],
borderColor: ['#00cccc', '#cad0e8']
}];
sessionsChartTwoLabels: ['Search', 'Email'];
sessionsChartTwoOptions = {
cutoutPercentage: 78,
maintainAspectRatio: false,
responsive: true,
legend: {
display: false,
},
animation: {
animateScale: true,
animateRotate: true
}
};
// Acquisition chart one
acquisitionOneChartData = [{
label: '# of Votes',
data: [4,2.5,5,3,5],
borderWidth: 1,
fill: false
}];
acquisitionOneChartLabels = ['1', '2', '3', '4', '5'];
acquisitionOneChartOptions = {
responsive:true,
maintainAspectRatio:false,
scales: {
yAxes: [{
display: false,
ticks: {
display: false,
},
gridLines: {
drawBorder: false,
display: false
}
}],
xAxes: [{
display: false,
barThickness: 5.5,
ticks: {
display: false,
},
gridLines: {
drawBorder: false,
display: false
}
}]
},
legend: {
display: false
},
elements: {
point: {
radius: 0
}
}
};
acquisitionOneChartColors = [
{
backgroundColor: '#fff',
borderColor: '#fff'
}
];
// Acquisition chart two
acquisitionTwoChartData = [{
label: '# of Votes',
data: [5,2,3,5,1.5],
borderWidth: 1,
fill: false
}];
acquisitionTwoChartLabels = ['1', '2', '3', '4', '5'];
acquisitionTwoChartOptions = {
responsive:true,
maintainAspectRatio:false,
scales: {
yAxes: [{
display: false,
ticks: {
display: false,
},
gridLines: {
drawBorder: false,
display: false
}
}],
xAxes: [{
display: false,
barThickness: 5.5,
ticks: {
display: false,
},
gridLines: {
drawBorder: false,
display: false
}
}]
},
legend: {
display: false
},
elements: {
point: {
radius: 0
}
}
};
acquisitionTwoChartColors = [
{
backgroundColor: '#fff',
borderColor: '#fff'
}
];
} }

View File

@ -5,7 +5,7 @@
<!-- Header Row --> <!-- Header Row -->
<report-header [reportHeaderTitle]="'Medical History'" [reportHeaderSubTitle]="'Organized by conditions, describes the scope and breadth of medical care'"></report-header> <report-header [reportHeaderTitle]="'Medical History'" [reportHeaderSubTitle]="'Organized by conditions, describes the scope and breadth of medical care'"></report-header>
<ng-container [ngTemplateOutlet]="loading ? isLoadingTemplate : (conditions.length == 0 && unassigned_encounters.length == 0) ? emptyReport : report"></ng-container> <ng-container [ngTemplateOutlet]="loading ? isLoadingTemplate : (conditions.length == 0 && unassigned_encounters.length == 0 && explanationOfBenefits.length == 0) ? emptyReport : report"></ng-container>
<ng-template #report> <ng-template #report>
<!-- Editor Button --> <!-- Editor Button -->
@ -33,6 +33,7 @@
<!-- Condition List --> <!-- Condition List -->
<app-report-medical-history-condition *ngFor="let condition of conditions; let i = index" [conditionGroup]="condition"></app-report-medical-history-condition> <app-report-medical-history-condition *ngFor="let condition of conditions; let i = index" [conditionGroup]="condition"></app-report-medical-history-condition>
<app-report-medical-history-explanation-of-benefit *ngFor="let eob of explanationOfBenefits; let i = index" [explanationOfBenefitGroup]="eob"></app-report-medical-history-explanation-of-benefit>
</ng-template> </ng-template>
<ng-template #emptyReport> <ng-template #emptyReport>

View File

@ -16,6 +16,7 @@ export class MedicalHistoryComponent implements OnInit {
closeResult = ''; closeResult = '';
conditions: ResourceFhir[] = [] conditions: ResourceFhir[] = []
explanationOfBenefits: ResourceFhir[] = []
unassigned_encounters: ResourceFhir[] = [] unassigned_encounters: ResourceFhir[] = []
resourceLookup: {[name: string]: ResourceFhir} = {} resourceLookup: {[name: string]: ResourceFhir} = {}
@ -32,6 +33,7 @@ export class MedicalHistoryComponent implements OnInit {
this.loading = false this.loading = false
this.conditions = [].concat(results["Condition"] || [], results["Composition"] || []) this.conditions = [].concat(results["Condition"] || [], results["Composition"] || [])
this.unassigned_encounters = results["Encounter"] || [] this.unassigned_encounters = results["Encounter"] || []
this.explanationOfBenefits = results["ExplanationOfBenefit"] || []
//populate a lookup table with all resources //populate a lookup table with all resources
for(let condition of this.conditions){ for(let condition of this.conditions){

View File

@ -13,7 +13,7 @@
<strong>Work-in-Progress!</strong> Some sources may not be implemented correctly. <strong>Work-in-Progress!</strong> Some sources may not be implemented correctly.
Some sources may require frequent re-connection, as background refresh has not been implemented yet. Some sources may require frequent re-connection, as background refresh has not been implemented yet.
<br/> <br/>
If you have feedback regarding healthcare sources, please <a href="https://github.com/fastenhealth/docs/issues">file a ticket <i class="fab fa-github"></i></a> If you have feedback regarding healthcare sources, please <a href="https://github.com/fastenhealth/fasten-onprem/issues">file a ticket <i class="fab fa-github"></i></a>
<span *ngIf="environment_name == 'sandbox'"> <span *ngIf="environment_name == 'sandbox'">
<br/> <br/>

View File

@ -3,6 +3,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MedicalSourcesComponent } from './medical-sources.component'; import { MedicalSourcesComponent } from './medical-sources.component';
import {HttpClientTestingModule} from '@angular/common/http/testing'; import {HttpClientTestingModule} from '@angular/common/http/testing';
import {RouterTestingModule} from '@angular/router/testing'; import {RouterTestingModule} from '@angular/router/testing';
import {HTTP_CLIENT_TOKEN} from '../../dependency-injection';
import {HttpClient} from '@angular/common/http';
describe('MedicalSourcesComponent', () => { describe('MedicalSourcesComponent', () => {
let component: MedicalSourcesComponent; let component: MedicalSourcesComponent;
@ -12,6 +14,12 @@ describe('MedicalSourcesComponent', () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ MedicalSourcesComponent ], declarations: [ MedicalSourcesComponent ],
imports: [HttpClientTestingModule, RouterTestingModule], imports: [HttpClientTestingModule, RouterTestingModule],
providers: [
{
provide: HTTP_CLIENT_TOKEN,
useClass: HttpClient,
},
]
}) })
.compileComponents(); .compileComponents();

View File

@ -4,6 +4,8 @@ import { ResourceCreatorComponent } from './resource-creator.component';
import {HttpClientTestingModule} from '@angular/common/http/testing'; import {HttpClientTestingModule} from '@angular/common/http/testing';
import {RouterTestingModule} from '@angular/router/testing'; import {RouterTestingModule} from '@angular/router/testing';
import {NgbCollapseModule, NgbDatepickerModule} from '@ng-bootstrap/ng-bootstrap'; import {NgbCollapseModule, NgbDatepickerModule} from '@ng-bootstrap/ng-bootstrap';
import {HTTP_CLIENT_TOKEN} from '../../dependency-injection';
import {HttpClient} from '@angular/common/http';
describe('ResourceCreatorComponent', () => { describe('ResourceCreatorComponent', () => {
let component: ResourceCreatorComponent; let component: ResourceCreatorComponent;
@ -12,7 +14,13 @@ describe('ResourceCreatorComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [HttpClientTestingModule, RouterTestingModule, NgbDatepickerModule, NgbCollapseModule], imports: [HttpClientTestingModule, RouterTestingModule, NgbDatepickerModule, NgbCollapseModule],
declarations: [ ResourceCreatorComponent ] declarations: [ ResourceCreatorComponent ],
providers: [
{
provide: HTTP_CLIENT_TOKEN,
useClass: HttpClient,
},
]
}) })
.compileComponents(); .compileComponents();

View File

@ -4,6 +4,8 @@ import { ResourceDetailComponent } from './resource-detail.component';
import {HttpClientTestingModule} from '@angular/common/http/testing'; import {HttpClientTestingModule} from '@angular/common/http/testing';
import {RouterTestingModule} from '@angular/router/testing'; import {RouterTestingModule} from '@angular/router/testing';
import {ActivatedRoute, convertToParamMap} from '@angular/router'; import {ActivatedRoute, convertToParamMap} from '@angular/router';
import {HTTP_CLIENT_TOKEN} from '../../dependency-injection';
import {HttpClient} from '@angular/common/http';
describe('ResourceDetailComponent', () => { describe('ResourceDetailComponent', () => {
let component: ResourceDetailComponent; let component: ResourceDetailComponent;
@ -17,7 +19,11 @@ describe('ResourceDetailComponent', () => {
{ {
provide: ActivatedRoute, provide: ActivatedRoute,
useValue: {snapshot: {paramMap: convertToParamMap( { 'resource_id': 'b64.cmVzb3VyY2VfZmhpcjpiNjQuYzI5MWNtTmxPbUZsZEc1aE9qRXlNelExTmpjNE9UQXhNak0wTlRZM01ETT06UGF0aWVudDoxMjM0NTY3ODkwMTIzNDU2NzAz' } )}} useValue: {snapshot: {paramMap: convertToParamMap( { 'resource_id': 'b64.cmVzb3VyY2VfZmhpcjpiNjQuYzI5MWNtTmxPbUZsZEc1aE9qRXlNelExTmpjNE9UQXhNak0wTlRZM01ETT06UGF0aWVudDoxMjM0NTY3ODkwMTIzNDU2NzAz' } )}}
} },
{
provide: HTTP_CLIENT_TOKEN,
useClass: HttpClient,
},
] ]
}) })
.compileComponents(); .compileComponents();

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