├── src
├── app
│ ├── app.html
│ ├── smart
│ │ ├── redirect.html
│ │ ├── launch.html
│ │ ├── smart_configuration.ts
│ │ ├── launch.component.ts
│ │ └── redirect.component.ts
│ ├── models
│ │ └── server.ts
│ ├── app.component.ts
│ ├── api
│ │ ├── api.component.ts
│ │ └── api.html
│ ├── directives
│ │ ├── highlight.directive.ts
│ │ └── auto-grow.directive.ts
│ ├── allergies
│ │ ├── allergies.html
│ │ └── allergies.component.ts
│ ├── conditions
│ │ ├── conditions.html
│ │ └── conditions.component.ts
│ ├── pipes
│ │ └── humanizeBytes.pipe.ts
│ ├── app-routing.module.ts
│ ├── patient
│ │ ├── patient.html
│ │ └── patient.component.ts
│ ├── services
│ │ ├── telemetry.service.ts
│ │ └── fhir.service.ts
│ ├── observations
│ │ ├── observations.component.ts
│ │ └── observations.html
│ ├── app.module.ts
│ ├── auth
│ │ └── auth-config.ts
│ └── home
│ │ ├── home.component.ts
│ │ └── home.html
├── version.json
├── images
│ └── textures
│ │ ├── xv.png
│ │ ├── xv_@2X.png
│ │ ├── crossword.png
│ │ ├── ricepaper.png
│ │ ├── wild_oliva.png
│ │ ├── ricepaper_@2X.png
│ │ ├── diagonal-noise.png
│ │ ├── wild_oliva_@2X.png
│ │ ├── diagonal-noise_@2X.png
│ │ ├── tileable_wood_texture.png
│ │ └── tileable_wood_texture_@2X.png
├── main.ts
├── environments
│ ├── environment.prod.ts
│ └── environment.ts
├── assets
│ └── configuration.template.js
├── vendor.ts
├── index.html
├── styles
│ ├── styles.scss
│ └── ribbons.scss
└── polyfills.ts
├── doc
└── screenshots
│ └── 1.png
├── tsconfig.app.json
├── .gitignore
├── tsconfig.json
├── Dockerfile
├── README.md
├── package.json
└── angular.json
/src/app/app.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/smart/redirect.html:
--------------------------------------------------------------------------------
1 | Completing SMART launch process...
--------------------------------------------------------------------------------
/doc/screenshots/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/preston/angular-on-fhir/HEAD/doc/screenshots/1.png
--------------------------------------------------------------------------------
/src/version.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": {
3 | "major": 0,
4 | "minor": 0,
5 | "patch": 0
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/images/textures/xv.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/preston/angular-on-fhir/HEAD/src/images/textures/xv.png
--------------------------------------------------------------------------------
/src/images/textures/xv_@2X.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/preston/angular-on-fhir/HEAD/src/images/textures/xv_@2X.png
--------------------------------------------------------------------------------
/src/app/models/server.ts:
--------------------------------------------------------------------------------
1 | export class FhirServer {
2 | constructor(public name: string, public url: string) {
3 | }
4 | }
--------------------------------------------------------------------------------
/src/images/textures/crossword.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/preston/angular-on-fhir/HEAD/src/images/textures/crossword.png
--------------------------------------------------------------------------------
/src/images/textures/ricepaper.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/preston/angular-on-fhir/HEAD/src/images/textures/ricepaper.png
--------------------------------------------------------------------------------
/src/images/textures/wild_oliva.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/preston/angular-on-fhir/HEAD/src/images/textures/wild_oliva.png
--------------------------------------------------------------------------------
/src/images/textures/ricepaper_@2X.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/preston/angular-on-fhir/HEAD/src/images/textures/ricepaper_@2X.png
--------------------------------------------------------------------------------
/src/images/textures/diagonal-noise.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/preston/angular-on-fhir/HEAD/src/images/textures/diagonal-noise.png
--------------------------------------------------------------------------------
/src/images/textures/wild_oliva_@2X.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/preston/angular-on-fhir/HEAD/src/images/textures/wild_oliva_@2X.png
--------------------------------------------------------------------------------
/src/images/textures/diagonal-noise_@2X.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/preston/angular-on-fhir/HEAD/src/images/textures/diagonal-noise_@2X.png
--------------------------------------------------------------------------------
/src/images/textures/tileable_wood_texture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/preston/angular-on-fhir/HEAD/src/images/textures/tileable_wood_texture.png
--------------------------------------------------------------------------------
/src/images/textures/tileable_wood_texture_@2X.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/preston/angular-on-fhir/HEAD/src/images/textures/tileable_wood_texture_@2X.png
--------------------------------------------------------------------------------
/src/app/smart/launch.html:
--------------------------------------------------------------------------------
1 |
2 |
Launching application...
3 |
6 |
--------------------------------------------------------------------------------
/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import {Component} from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app',
5 | templateUrl: 'app.html'
6 | })
7 | export class AppComponent {
8 |
9 | constructor() {
10 | console.log("AppComponent has been initialized.");
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "extends": "./tsconfig.json",
4 | "compilerOptions": {
5 | "outDir": "./out-tsc/app",
6 | "types": []
7 | },
8 | "files": [
9 | "src/main.ts",
10 | "src/polyfills.ts"
11 | ],
12 | "include": [
13 | "src/**/*.d.ts"
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OS X crap
2 | .DS_Store
3 |
4 | # TypeScript cache
5 | .tscache
6 |
7 | # Runtime-generated files
8 | src/assets/configuration.js
9 |
10 | # Generated files
11 | .angular
12 | build
13 | dist
14 | *.css
15 | *.map
16 | *.zip
17 | *.tar.gz
18 |
19 | #Exclude bower components and fonts
20 | src/bower_components
21 |
22 | # Locally-installed dependencies
23 | node_modules
24 | bower_components
25 |
--------------------------------------------------------------------------------
/src/app/api/api.component.ts:
--------------------------------------------------------------------------------
1 | import {Component} from '@angular/core';
2 |
3 | @Component({
4 | selector: 'api',
5 | templateUrl: 'api.html'
6 | })
7 | export class ApiComponent {
8 |
9 | constructor() {
10 | console.log("ApiComponent has been initialized.");
11 | }
12 |
13 | stringify(obj: any): string {
14 | return JSON.stringify(obj, null, "\t").trim();
15 | }
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { enableProdMode } from '@angular/core';
2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
3 |
4 | import { AppModule } from './app/app.module';
5 | import { environment } from './environments/environment';
6 |
7 | if (environment.production) {
8 | enableProdMode();
9 | }
10 |
11 | platformBrowserDynamic().bootstrapModule(AppModule)
12 | .catch(err => console.error(err));
13 |
--------------------------------------------------------------------------------
/src/app/directives/highlight.directive.ts:
--------------------------------------------------------------------------------
1 | import {Directive, ElementRef, Renderer2, Input} from '@angular/core';
2 |
3 | @Directive({
4 | selector: '[highlight]'
5 | })
6 | export class HighlightDirective {
7 |
8 | @Input('highlightText') text: string = '';
9 |
10 | constructor(private el: ElementRef, private renderer: Renderer2) {
11 | console.log("Highlighting!");
12 | this.el.nativeElement.textContent = 'foo';
13 | }
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/src/app/allergies/allergies.html:
--------------------------------------------------------------------------------
1 |
2 | Allergies
3 |
4 |
5 |
6 | | What goeth here, say ye? |
7 |
8 |
9 |
10 |
11 | | Something |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | let w = (window as {[key: string]: any})
2 | if (!w['configuration']) {
3 | console.error('Missing generated runtime configuration from environment variables. Application will not function correctly. Please verify you are starting the application correctly.');
4 | }
5 | export const environment = {
6 | production: true,
7 | clientId: w["configuration"]["clientId"],
8 | debug: w["configuration"]["debug"] || false
9 | };
10 |
--------------------------------------------------------------------------------
/src/app/smart/smart_configuration.ts:
--------------------------------------------------------------------------------
1 | // Author: Preston Lee
2 |
3 | export class SmartConfiguration {
4 | authorization_endpoint!: string;
5 | token_endpoint!: string;
6 | registration_endpoint!: string;
7 | scopes_supported: string[] = [];
8 | response_types_supported: string[] = [];
9 | management_endpoint!: string;
10 | introspection_endpoint!: string;
11 | revocation_endpoint!: string;
12 | capabilities: string[] = [];
13 | }
14 |
15 |
--------------------------------------------------------------------------------
/src/assets/configuration.template.js:
--------------------------------------------------------------------------------
1 | // Since this is considered a static asset, it is not compiled but copied verbatim.
2 | // These values are set at application _start_ time but the server and then read by browser clients.
3 | (function(window) {
4 | window["configuration"] = window["configuration"] || {};
5 |
6 | // Environment variables
7 | window["configuration"]["clientId"] = "${FHIR_CLIENT_ID}";
8 | window["configuration"]["debug"] = "${FHIR_DEBUG}" == "true";
9 | })(this);
--------------------------------------------------------------------------------
/src/vendor.ts:
--------------------------------------------------------------------------------
1 | // Bootstrap
2 | import 'jquery/dist/jquery';
3 | import 'bootstrap/dist/js/bootstrap';
4 |
5 | // Angular dependencies
6 | import 'zone.js/dist/zone';
7 | import 'reflect-metadata';
8 |
9 | // Angular
10 | import '@angular/core';
11 | import '@angular/forms';
12 | import '@angular/common/http';
13 | import '@angular/platform-browser-dynamic';
14 | import '@angular/platform-browser';
15 | import '@angular/router';
16 |
17 | // Cookies
18 | import 'angular2-cookie/core'
19 |
--------------------------------------------------------------------------------
/src/app/directives/auto-grow.directive.ts:
--------------------------------------------------------------------------------
1 | import {Directive, ElementRef, Renderer2} from '@angular/core';
2 |
3 | @Directive({
4 | selector: '[autoGrow]',
5 | host: {
6 | '(focus)': 'onFocus()',
7 | '(blur)': 'onBlur()'
8 | }
9 | })
10 | export class AutoGrowDirective {
11 |
12 | constructor(private el: ElementRef, private renderer: Renderer2) {
13 | }
14 |
15 | onFocus() {
16 | // console.log('focus!');
17 | this.renderer.setStyle(this.el.nativeElement, 'width', '200px');
18 | }
19 | onBlur() {
20 | // console.log('blur');
21 | this.renderer.setStyle(this.el.nativeElement, 'width', '120px');
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/app/allergies/allergies.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, Input} from '@angular/core';
2 | import {FhirService} from '../services/fhir.service';
3 | // import {PatientService} from '../services/patient.service';
4 | import {AllergyIntolerance, Patient} from 'fhir/r4';
5 |
6 | @Component({
7 | selector: 'allergies',
8 | templateUrl: 'allergies.html'
9 | })
10 | export class AllergiesComponent {
11 |
12 | selected: AllergyIntolerance | undefined;
13 | allergies: Array = [];
14 | @Input() patient: Patient | undefined;
15 |
16 | constructor(private fhirService: FhirService) {
17 | console.log("AllergiesComponent created...");
18 | }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/src/app/conditions/conditions.html:
--------------------------------------------------------------------------------
1 | (none)
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | | Description |
11 | Status |
12 | Onset |
13 |
14 |
15 |
16 |
17 | | {{c?.code?.text}} |
18 | {{c.clinicalStatus}} |
19 | {{c.onsetDateTime | amTimeAgo}} |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/app/pipes/humanizeBytes.pipe.ts:
--------------------------------------------------------------------------------
1 | import {Pipe, PipeTransform} from '@angular/core';
2 |
3 | @Pipe({
4 | name: 'humanizeBytes'
5 | })
6 | export class HumanizeBytesPipe implements PipeTransform {
7 |
8 | transform(value: string, args: string[]) {
9 | return this.doIt(parseInt(value));
10 | }
11 |
12 | doIt(n: number): string {
13 | if (n < 1024) {
14 | return n.toString();
15 | }
16 | var si = ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'HiB'];
17 | var exp = Math.floor(Math.log(n) / Math.log(1024));
18 | var result: number = n / Math.pow(1024, exp);
19 | var readable: string = (result % 1 > (1 / Math.pow(1024, exp - 1))) ? result.toFixed(2) : result.toFixed(0);
20 | return readable + si[exp - 1];
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/app/app-routing.module.ts:
--------------------------------------------------------------------------------
1 | // Author: Preston Lee
2 |
3 | import { NgModule } from '@angular/core';
4 | import { RouterModule, Routes } from '@angular/router';
5 | import { ApiComponent } from './api/api.component';
6 | import { HomeComponent } from './home/home.component';
7 | import { SmartLaunchComponent } from './smart/launch.component';
8 | import { SmartRedirectComponent } from './smart/redirect.component';
9 |
10 | const routes: Routes = [
11 | { path: 'launch', component: SmartLaunchComponent },
12 | { path: 'redirect', component: SmartRedirectComponent },
13 | { path: '', component: HomeComponent },
14 | { path: 'api', component: ApiComponent }
15 | ]
16 |
17 | @NgModule({
18 | imports: [RouterModule.forRoot(routes)],
19 | exports: [RouterModule]
20 | })
21 | export class AppRoutingModule { }
22 |
--------------------------------------------------------------------------------
/src/app/api/api.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Developer API
5 |
HealthCreek supports a RESTful JSON API for building applications and integrating with existing systems.
6 |
7 |
8 |
9 |
10 |
GET a global list of stuff
11 |
TODO
13 |
stuff!
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Angular on FHIR
9 |
10 |
11 |
12 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/app/patient/patient.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "compileOnSave": false,
4 | "compilerOptions": {
5 | "baseUrl": "./",
6 | "outDir": "./dist/out-tsc",
7 | "forceConsistentCasingInFileNames": true,
8 | "strict": true,
9 | "noImplicitReturns": true,
10 | "noFallthroughCasesInSwitch": true,
11 | "sourceMap": true,
12 | "declaration": false,
13 | "downlevelIteration": true,
14 | "experimentalDecorators": true,
15 | "moduleResolution": "node",
16 | "importHelpers": true,
17 | "target": "ES2022",
18 | "module": "ES2022",
19 | "lib": [
20 | "es2018",
21 | "dom"
22 | ],
23 | "allowSyntheticDefaultImports": true
24 | },
25 | "angularCompilerOptions": {
26 | "enableI18nLegacyMessageIdFormat": false,
27 | "strictInjectionParameters": true,
28 | "strictInputAccessModifiers": true,
29 | "strictTemplates": true,
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/app/services/telemetry.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 |
3 | // import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
4 | // import { SimpleSpanProcessor, ConsoleSpanExporter, BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
5 | // import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
6 |
7 |
8 | @Injectable({
9 | providedIn: 'root'
10 | })
11 | export class TelemetryService {
12 |
13 | // public tracerProvider: WebTracerProvider;
14 |
15 | constructor() {
16 | // this.tracerProvider = new WebTracerProvider();
17 | // this.tracerProvider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter()));
18 |
19 | // let conf = {
20 | // url: 'http://localhost:4318/v1/traces',
21 | // headers: { "Content-Type": "application/json", "Accept": "application/json" }
22 | // };
23 | // const exporter = new OTLPTraceExporter(conf);
24 | // this.tracerProvider.addSpanProcessor(new BatchSpanProcessor(exporter));
25 | // this.tracerProvider.register();
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // This file can be replaced during build by using the `fileReplacements` array.
2 | // `ng build` replaces `environment.ts` with `environment.prod.ts`.
3 | // The list of file replacements can be found in `angular.json`.
4 |
5 | let w = (window as { [key: string]: any })
6 | if (!w['configuration']) {
7 | console.error('Missing generated runtime configuration from environment variables. Application will not function correctly. Please verify you are starting the application correctly.');
8 | }
9 | export const environment = {
10 | production: true,
11 | clientId: w["configuration"]["clientId"],
12 | debug: w["configuration"]["debug"] || false
13 | };
14 |
15 | /*
16 | * For easier debugging in development mode, you can import the following file
17 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
18 | *
19 | * This import should be commented out in production mode because it will have a negative impact
20 | * on performance if an error is thrown.
21 | */
22 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI.
23 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:19-alpine as builder
2 | LABEL maintainer="preston.lee@prestonlee.com"
3 |
4 | # Install dependencies first so they layer can be cached across builds.
5 | RUN mkdir /app
6 | WORKDIR /app
7 | COPY package.json package-lock.json ./
8 | RUN npm i
9 |
10 | # Build
11 | COPY . .
12 | RUN npm run ng build --production
13 | # -- --prod
14 |
15 | FROM nginx:stable-alpine
16 | # Copy our default nginx config
17 | # COPY nginx/default.conf /etc/nginx/conf.d/
18 |
19 | # We need to make a few changes to the default configuration file.
20 | COPY nginx.conf /etc/nginx/conf.d/default.conf
21 |
22 | # Remove any default nginx content
23 | RUN rm -rf /usr/share/nginx/html/*
24 |
25 | ## Copy build from "builder" stage, as well as runtime configuration script public folder
26 | COPY --from=builder /app/dist/marketplace-ui /usr/share/nginx/html
27 | # COPY --from=builder /app/configure-from-environment.sh /usr/share/nginx/html
28 | WORKDIR /usr/share/nginx/html
29 |
30 | # CMD ["./configure-from-environment.sh", "&&", "exec", "nginx", "-g", "'daemon off;'"]
31 | CMD envsubst < assets/configuration.template.js > assets/configuration.js && exec nginx -g 'daemon off;'
32 |
--------------------------------------------------------------------------------
/src/app/conditions/conditions.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, Input} from '@angular/core';
2 |
3 | import {FhirService} from '../services/fhir.service';
4 |
5 | import {Condition, Patient} from 'fhir/r4';
6 |
7 |
8 | @Component({
9 | selector: 'conditions',
10 | templateUrl: 'conditions.html'
11 | })
12 | export class ConditionsComponent {
13 |
14 | selected: Condition | undefined;
15 | conditions: Array = [];
16 | @Input() patient: Patient | undefined;
17 |
18 | constructor(private fhirService: FhirService) {
19 | console.log("ConditionsService created...");
20 | }
21 |
22 | ngOnChanges() {
23 | if (this.patient) {
24 | this.fhirService.client?.search({resourceType: 'Condition', compartment: {resourceType: 'Patient', id: this.patient.id!}}).then((data) => {
25 | if(data.entry) {
26 | this.conditions = >data.resource;
27 | console.log("Loaded " + this.conditions.length + " conditions.");
28 | } else {
29 | this.conditions = new Array();
30 | console.log("No conditions for patient.");
31 | }
32 | });
33 | }
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/src/app/observations/observations.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input } from '@angular/core';
2 | import { FhirService } from '../services/fhir.service';
3 | import { Bundle, Observation, Patient } from 'fhir/r4';
4 |
5 | @Component({
6 | selector: 'observations',
7 | templateUrl: 'observations.html'
8 | })
9 | export class ObservationsComponent {
10 |
11 | selected: Observation | undefined;
12 | // observations: Array = [];
13 | @Input() patient: Patient | undefined;
14 |
15 | public observationBundle: Bundle | undefined;
16 |
17 | constructor(private fhirService: FhirService) {
18 | console.log("ObservationsComponent created...");
19 | // this.fhirService.client?.compartmentSearch({resourceType: 'Observation', compartment: {resourceType: 'Patient', id: this.fhirService.patient!}}).then((obs: any) => {
20 | this.fhirService.client?.search({ resourceType: 'Observation', searchParams: { subject: 'Patient/' + this.fhirService.patient! } }).then((b: any) => {
21 | this.observationBundle = b;
22 | if (this.observationBundle?.entry?.length) {
23 | console.log("Loaded " + this.observationBundle.entry.length + " observations.");
24 | } else {
25 | console.log("No observations loaded for patient!");
26 | }
27 |
28 | });
29 | }
30 |
31 | ngOnChanges() {
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/src/app/smart/launch.component.ts:
--------------------------------------------------------------------------------
1 | import { HttpClient } from '@angular/common/http';
2 | import { Component, OnInit } from '@angular/core';
3 | import { ActivatedRoute, Router, UrlSerializer } from '@angular/router';
4 |
5 | import { environment } from 'src/environments/environment';
6 | import FHIR from 'fhirclient';
7 |
8 | @Component({
9 | selector: 'app-launch',
10 | templateUrl: 'launch.html'
11 | })
12 | export class SmartLaunchComponent implements OnInit {
13 |
14 | message: string = 'Standby for liftoff.';
15 |
16 |
17 | constructor(protected route: ActivatedRoute, protected router: Router, protected serializer: UrlSerializer, protected http: HttpClient) { }
18 |
19 |
20 | ngOnInit(): void {
21 |
22 | if (!environment.clientId || environment.clientId == '') {
23 | this.message = 'Application cannot launch due to a bad deployment configuration. The system administration needs to set a OAuth clientId set via the FHIR_CLIENT_ID environment variable. It is currently set to "' + environment.clientId + '".';
24 | console.error(this.message);
25 | }
26 | else {
27 | this.startLaunch();
28 | }
29 | }
30 |
31 | startLaunch() {
32 | FHIR.oauth2.authorize({ clientId: environment.clientId, scope: 'launch patient/*.read openid profile', redirectUri: '/redirect' });
33 | }
34 |
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/src/styles/styles.scss:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: none;
3 | background: url('/images/textures/wild_oliva.png') repeat;
4 | // padding-top: 60px;
5 | }
6 |
7 | // input:invalid,
8 | // textarea:invalid {
9 | // box-shadow: 0 10px 10px rgba(220, 13, 23, 0.5);
10 | // }
11 | header {
12 | // text-shadow: 0 4px 4px rgba(0, 0, 0, 0.2);
13 | // a {
14 | // transition: all 0.2s ease-out;
15 | // text-decoration: none;
16 | // &:default {
17 | // text-decoration: none;
18 | // text-shadow: -4px 4px 4px rgba(0, 0, 0, 0.4);
19 | // transition: all 0.2s ease-out;
20 | // }
21 | // }
22 | footer p>span {
23 | padding: 20px
24 | }
25 | // allergies h2, observations h2
26 | // border-bottom: 1px solid #eee font-style: italic aside#sidebar>section,
27 | allergies>section,
28 | observations>section {
29 | /*margin: 0 0px; */
30 | // padding: 8px;
31 | border-radius: 2px;
32 | box-shadow: 0 20px 40px rgba(0, 0, 0, 0.8); // background-color: none
33 | // background: url('/images/textures/crossword.png') repeat
34 | background: rgba(0, 0, 0, .4)
35 | }
36 | .highlight {
37 | background: rgba(230, 230, 0, 0.5)
38 | }
39 | }
40 |
41 | // @media (max-width: 768px)
42 | // .tagline
43 | // display: none
44 | // table.table tbody tr
45 | // &.selected
46 | // background-color: #a7c6d8
47 | // transition: all 0.2s ease-out
48 | // transition: all 0.2s ease-out
49 | // &:hover:not(.selected)
50 | // background-color: #c7e6f8
51 | // transition: all 0.2s ease-out
52 | // // h4
53 | // border-bottom: 1px solid #ccc
--------------------------------------------------------------------------------
/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import {enableProdMode} from '@angular/core';
2 |
3 | import {ApiComponent} from './api/api.component';
4 | import {AppComponent} from './app.component';
5 | import {HomeComponent} from './home/home.component';
6 | import {PatientComponent} from './patient/patient.component';
7 |
8 | import {ConditionsComponent} from './conditions/conditions.component';
9 | import {ObservationsComponent} from './observations/observations.component';
10 |
11 | import {FhirService} from './services/fhir.service';
12 |
13 | import {MomentModule} from 'ngx-moment';
14 |
15 | enableProdMode();
16 |
17 |
18 | import { NgModule } from '@angular/core';
19 | import { BrowserModule } from '@angular/platform-browser';
20 | import { FormsModule } from '@angular/forms';
21 | import { HttpClientModule } from '@angular/common/http';
22 |
23 | // const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes);
24 | import { AppRoutingModule } from './app-routing.module';
25 | import { AllergiesComponent } from './allergies/allergies.component';
26 | import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
27 |
28 | // import { OAuthModule } from 'angular-oauth2-oidc';
29 |
30 | @NgModule({
31 | imports: [
32 | BrowserModule,
33 | AppRoutingModule,
34 | FormsModule,
35 | MomentModule,
36 | HttpClientModule,
37 | NgbModule
38 | // OAuthModule.forRoot()
39 | ], // module dependencies
40 | declarations: [
41 | ApiComponent,
42 | AppComponent,
43 | HomeComponent,
44 | PatientComponent,
45 | ObservationsComponent,
46 | ConditionsComponent,
47 | AllergiesComponent
48 | ], // components and directives
49 | providers: [
50 | FhirService
51 | ], // services
52 | bootstrap: [AppComponent] // root component
53 | })
54 | export class AppModule {
55 | }
56 |
--------------------------------------------------------------------------------
/src/styles/ribbons.scss:
--------------------------------------------------------------------------------
1 | .corner-ribbon {
2 | width: 200px;
3 | background: #e43;
4 | position: absolute;
5 | top: 25px;
6 | left: -50px;
7 | text-align: center;
8 | // line-height: 50px
9 | line-height: 30px;
10 | letter-spacing: 1px;
11 | color: #f0f0f0;
12 | transform: rotate(-45deg);
13 | -webkit-transform: rotate(-45deg);
14 | }
15 |
16 | /* Custom styles */
17 |
18 | .corner-ribbon.sticky {
19 | position: fixed;
20 | }
21 |
22 | .corner-ribbon.shadow {
23 | box-shadow: 0 20px 20px rgba(0,0,0,.4);
24 | }
25 |
26 | /* Different positions */
27 |
28 | .corner-ribbon.top-left {
29 | top: 25px;
30 | left: -50px;
31 | transform: rotate(-45deg);
32 | -webkit-transform: rotate(-45deg);
33 | }
34 |
35 | .corner-ribbon.top-right {
36 | top: 25px;
37 | right: -50px;
38 | left: auto;
39 | transform: rotate(45deg);
40 | -webkit-transform: rotate(45deg);
41 | }
42 |
43 | .corner-ribbon.bottom-left {
44 | top: auto;
45 | bottom: 25px;
46 | left: -50px;
47 | transform: rotate(45deg);
48 | -webkit-transform: rotate(45deg);
49 | }
50 |
51 | .corner-ribbon.bottom-right {
52 | top: auto;
53 | right: -50px;
54 | bottom: 25px;
55 | left: auto;
56 | transform: rotate(-45deg);
57 | -webkit-transform: rotate(-45deg);
58 | }
59 |
60 | /* Colors */
61 |
62 | .corner-ribbon.white {
63 | background: #f0f0f0;
64 | color: #555;
65 | }
66 | .corner-ribbon.black {
67 | background: #333;
68 | }
69 | .corner-ribbon.grey {
70 | background: #999;
71 | }
72 | .corner-ribbon.blue {
73 | background: #39d;
74 | }
75 | .corner-ribbon.green {
76 | background: #2c7;
77 | }
78 | .corner-ribbon.turquoise {
79 | background: #1b9;
80 | }
81 | .corner-ribbon.purple {
82 | background: #95b;
83 | }
84 | .corner-ribbon.red {
85 | background: #e43;
86 | }
87 | .corner-ribbon.orange {
88 | background: #e82;
89 | }
90 | .corner-ribbon.yellow {
91 | background: #ec0;
92 | }
--------------------------------------------------------------------------------
/src/app/services/fhir.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 |
3 | import * as FKClient from 'fhir-kit-client';
4 |
5 | import * as SmartClient from 'fhirclient/lib/Client';
6 |
7 | import { FhirServer } from '../models/server';
8 | @Injectable({ providedIn: 'root' })
9 | export class FhirService {
10 |
11 | // public servers: Array<{ name: string, url: string }> = [
12 | // new FhirServer("Example Open Endpoint", "https://api.logicahealth.org/GraphiteTestV450/open")
13 | // ]
14 |
15 | // public current: FhirServer = this.servers[0];
16 | public clientId = (window as any)["configuration"]["clientId"];
17 | public debug = (window as any)["configuration"]["debug"] || false;
18 |
19 | public client: FKClient.default | undefined;
20 | public smartClient: SmartClient.default | undefined;
21 | // public client = new Client({ baseUrl: this.current.url });
22 |
23 | public patient: string | null | undefined;
24 |
25 | constructor() {
26 | // this.client = new FKClient.default();
27 | // this.client = new FKClient.default({ baseUrl: this.current.url });
28 | // this.reinitialize();
29 | // this.smartClient = new SmartClient.default();
30 | }
31 |
32 | reinitializeSmart() {
33 | // bearerToken: string
34 | this.client = new FKClient.default({
35 | baseUrl: this.smartClient?.state.serverUrl!,
36 | bearerToken: this.smartClient?.getAuthorizationHeader()!.substring('Bearer '.length),
37 | });
38 | // this.client.
39 | this.patient = this.smartClient?.patient.id;
40 | }
41 |
42 | reinitializeManually(url: string, token: string) {
43 | this.client = new FKClient.default({
44 | baseUrl: url,
45 | bearerToken: token
46 | });
47 | this.patient = null;
48 | this.smartClient = undefined;
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Angular on FHIR
2 |
3 | Angular 15 base project with Bootstrap 5 for SMART-on-FHIR-based UIs.
4 |
5 | ## Developer Quick Start
6 |
7 | This is an [AngularJS 15](https://angular.io) project using `npm` as the package manager and build system, [SCSS](http://sass-lang.com) for CSS and [Bootstrap](http://getbootstrap.com/) for layout.
8 |
9 | npm install # to install project development dependencies
10 | npm run start # to build and run in development mode
11 |
12 | Visiting the application directly at [http://localhost:4200](http://localhost:4200) **will not work**! You must launch it via the SMART protocol using a sandbox system (e.g. sandbox.logicahealth.org), EHR, or other SMART launch process. Configure your SMART launcher as follows:
13 |
14 | - Launch URL: http://localhost:4200/launch
15 | - Redirect URL: http://localhost:4200/
16 | - Scopes: launch patient/*.read openid profile
17 | - Standalone launch (as opposed to embedded)
18 | - Patient context, which will make the launcher send a patient ID to the app after launch
19 |
20 | Set and export the following environment variables in your shell:
21 |
22 | export FHIR_CLIENT_ID=
23 | export FHIR_DEBUG=true
24 |
25 | The application, when loaded, should look similar to the following screenshot:
26 | 
27 |
28 | ## Building for Production
29 |
30 | If you use [Docker](https://www.docker.com), you can build into an [nginx](http://nginx.org) web server container using the including Dockerfile with:
31 |
32 | docker build -t p3000/angular-on-fhir:latest . # use your own repo and tag strings :)
33 |
34 | ## Production Deployment
35 |
36 | Easy:
37 |
38 | docker run -d -p 9000:80 --restart unless-stopped p3000/angular-on-fhir:latest # or any official tag
39 |
40 | # License
41 |
42 | [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0)
43 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-on-fhir",
3 | "version": "1.2.0",
4 | "description": "Angular base project for SMART and other FHIR-based UIs.",
5 | "license": "Apache-2.0",
6 | "repository": "git@github.com:preston/angular-on-fhir.git",
7 | "scripts": {
8 | "ng": "ng",
9 | "start": "envsubst < src/assets/configuration.template.js > src/assets/configuration.js && ng serve",
10 | "build": "ng build",
11 | "watch": "envsubst < src/assets/configuration.template.js > src/assets/configuration.js && ng build --watch --configuration development",
12 | "analyze": "ng build --stats-json && webpack-bundle-analyzer dist/angular-on-fhir/stats.json -m static",
13 | "test": "ng test"
14 | },
15 | "author": "Preston Lee",
16 | "dependencies": {
17 | "@angular/animations": ">=16.0.4",
18 | "@angular/common": ">=16.0.4",
19 | "@angular/compiler": ">=16.0.4",
20 | "@angular/core": ">=16.0.4",
21 | "@angular/forms": ">=16.0.4",
22 | "@angular/localize": ">=16.0.4",
23 | "@angular/platform-browser": ">=16.0.4",
24 | "@angular/platform-browser-dynamic": ">=16.0.4",
25 | "@angular/router": ">=16.0.4",
26 | "@ng-bootstrap/ng-bootstrap": "^15.0.1",
27 | "jquery": "^3.7.0",
28 | "@popperjs/core": "^2.11.8",
29 | "bootstrap": "^5.3.0",
30 | "bootstrap-icons": "^1.10.5",
31 | "fhir-kit-client": "^1.9.2",
32 | "fhirclient": "^2.5.2",
33 | "moment": "^2.29.4",
34 | "ngx-moment": "^6.0.2",
35 | "rxjs": "^7.8.1",
36 | "zone.js": "^0.13.0"
37 | },
38 | "devDependencies": {
39 | "@angular-devkit/build-angular": ">=16.0.4",
40 | "@angular/cli": ">=16.0.4",
41 | "@angular/compiler-cli": ">=16.0.4",
42 | "@types/fhir": "^0.0.37",
43 | "@types/node": ">=20.2.5",
44 | "typescript": "< 5.1",
45 | "webpack-bundle-analyzer": "^4.9.0"
46 | }
47 | }
--------------------------------------------------------------------------------
/src/app/observations/observations.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Showing {{observationBundle.entry?.length || 'no'}} entries.
5 |
(none)
6 |
0">
7 |
8 |
9 | | Date |
10 | Categories |
11 | Codes |
12 | Value |
13 | Status |
14 |
15 |
16 |
17 |
18 |
19 | {{o?.resource?.effectiveDateTime}} {{o?.resource?.effectiveDateTime| amTimeAgo}} |
20 |
21 |
22 |
23 | {{coding.display}}
24 |
25 |
26 | |
27 | {{ o?.resource?.code?.text}}
28 |
31 | |
32 | {{ o.resource?.valueQuantity?.value}}
33 | {{o.resource?.valueQuantity?.unit}}{{o.resource?.valueCodeableConcept?.text}}
34 | |
35 | {{o.resource?.status}} |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/src/app/auth/auth-config.ts:
--------------------------------------------------------------------------------
1 | // import { AuthConfig } from 'angular-oauth2-oidc';
2 | // import { environment } from 'src/environments/environment';
3 |
4 | // // export const authConfig: AuthConfig = {
5 | // // issuer: '',
6 | // // clientId: '',
7 | // // // clientId: 'interactive.public', // The "Auth Code + PKCE" client
8 | // // responseType: 'code',
9 | // // redirectUri: window.location.origin + '/',
10 | // // silentRefreshRedirectUri: window.location.origin + '/silent-refresh.html',
11 | // // // scope: 'openid profile email api', // Ask offline_access to support refresh token refreshes
12 | // // scope: 'launch openid profile user/Patient.read patient/*.*',
13 | // // useSilentRefresh: true, // Needed for Code Flow to suggest using iframe-based refreshes
14 | // // silentRefreshTimeout: 5000, // For faster testing
15 | // // timeoutFactor: 0.25, // For faster testing
16 | // // sessionChecksEnabled: true,
17 | // // showDebugInformation: true, // Also requires enabling "Verbose" level in devtools
18 | // // clearHashAfterLogin: false, // https://github.com/manfredsteyer/angular-oauth2-oidc/issues/457#issuecomment-431807040,
19 | // // nonceStateSeparator : 'semicolon' // Real semicolon gets mangled by Duende ID Server's URI encoding
20 | // // };
21 |
22 |
23 | // export const authConfig: AuthConfig = {
24 | // // Url of the Identity Provider
25 | // issuer: '',
26 |
27 | // // URL of the SPA to redirect the user to after login
28 | // redirectUri: window.location.origin + '/index.html',
29 |
30 | // // The SPA's id. The SPA is registerd with this id at the auth-server
31 | // // clientId: 'server.code',
32 | // clientId: environment.clientId,
33 |
34 | // // Just needed if your auth server demands a secret. In general, this
35 | // // is a sign that the auth server is not configured with SPAs in mind
36 | // // and it might not enforce further best practices vital for security
37 | // // such applications.
38 | // // dummyClientSecret: 'secret',
39 |
40 | // responseType: 'code',
41 |
42 | // // set the scope for the permissions the client should request
43 | // // The first four are defined by OIDC.
44 | // // Important: Request offline_access to get a refresh token
45 | // // The api scope is a usecase specific one
46 | // scope: 'openid profile email offline_access api',
47 |
48 | // showDebugInformation: true,
49 | // };
--------------------------------------------------------------------------------
/src/app/smart/redirect.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 | import { Router } from '@angular/router';
3 |
4 | import { environment } from 'src/environments/environment';
5 | import * as FHIR from 'fhirclient';
6 | import { FhirService } from '../services/fhir.service';
7 |
8 | @Component({
9 | selector: 'smart-redirect',
10 | templateUrl: 'redirect.html'
11 | })
12 | export class SmartRedirectComponent implements OnInit {
13 |
14 | code: string | null = null;
15 | message: string = 'Standby for liftoff.';
16 |
17 | constructor(protected router: Router, protected fhirService: FhirService) { }
18 |
19 | // parseParams() {
20 | // this.route.queryParamMap.forEach(n => {
21 | // n.keys.forEach(key => {
22 | // switch (key) {
23 | // case 'code':
24 | // this.code = n.get(key);
25 | // break;
26 | // default:
27 | // console.log('Warning: Unknown SMART redirect key \'' + key + '\' with value \'' + n.get(key) + '\'');
28 | // break;
29 | // }
30 | // })
31 | // });
32 | // }
33 |
34 | ngOnInit(): void {
35 | // this.parseParams();
36 |
37 | if (!environment.clientId || environment.clientId == '') {
38 | this.message = 'Application cannot launch due to a bad deployment configuration. The system administration needs to set a OAuth clientId set via the FHIR_CLIENT_ID environment variable. It is currently set to "' + environment.clientId + '".';
39 | console.error(this.message);
40 | }
41 | // else if (this.code == null) {
42 | // this.message = "Application cannot launch because the OAuth authorization 'code' is null.";
43 | // console.error(this.message);
44 |
45 | // }
46 | else {
47 | this.doRedirect();
48 | }
49 | }
50 |
51 |
52 | doRedirect() {
53 | FHIR.oauth2.ready().then((client) => {
54 | console.log("OAuth redirect complete! Setting route to application home.");
55 | const token = client.getAuthorizationHeader();
56 | if (token) {
57 | console.log("Bearer token header: " + token);
58 | this.fhirService.smartClient = client;
59 | this.fhirService.reinitializeSmart();
60 | // this.fhirService.reinitialize(token);
61 | } else {
62 | console.error("Beaker token is null. This won't do at all! Protected FHIR data cannot be read without it.");
63 | }
64 | this.router.navigateByUrl('/');
65 | });
66 |
67 |
68 | }
69 |
70 | }
71 |
--------------------------------------------------------------------------------
/src/app/home/home.component.ts:
--------------------------------------------------------------------------------
1 | // Author: Preston Lee
2 |
3 | import { Component, OnInit } from '@angular/core';
4 |
5 | // const { ClientCredentials, ResourceOwnerPassword, AuthorizationCode } = require('simple-oauth2');
6 |
7 | import { TelemetryService } from '../services/telemetry.service';
8 | import { ActivatedRoute } from '@angular/router';
9 |
10 | import { FhirService } from '../services/fhir.service';
11 | import { Patient } from 'fhir/r4';
12 |
13 | @Component({
14 | selector: 'home',
15 | templateUrl: 'home.html'
16 | })
17 | export class HomeComponent implements OnInit {
18 |
19 | tab: string = 'reader';
20 |
21 | // protected tracer;
22 | protected code: string | null = null;
23 | // SMART launch stuff
24 | // public showPatientBanner = false;
25 | isWritable = false;
26 |
27 |
28 | public today = Date.now();
29 | public patient: Patient | undefined;
30 |
31 | public url: string | null = null;
32 | public token: string | null = null;
33 |
34 | public resource_types: { [key: string]: { type: string, path: string } } = {
35 | 'patient': { type: 'Patient', path: '/patient' },
36 | 'observation': { type: 'Observation', path: '/observation' }
37 | };
38 |
39 | public selected_resource_type: string = 'patient'; //this.resource_types[0];
40 |
41 | public search_text = '';
42 | public search_results: any[] = [];
43 |
44 | constructor(protected telemetryService: TelemetryService, protected route: ActivatedRoute, public fhirService: FhirService) {
45 | // this.tracer = telemetryService.tracerProvider.getTracer('angular-on-fhir-tracer');
46 | // Object.entries(this.resource_types).forEach(n => {
47 | // });
48 | for (const k in this.resource_types) {
49 | console.log("KEY: " + k);
50 | this.selected_resource_type = k;
51 | }
52 |
53 | // Handle manual initialization when launched via ?url=...&token=...
54 | this.url = this.route.snapshot.queryParamMap.get('url');
55 | this.token = this.route.snapshot.queryParamMap.get('token');
56 | if (this.url && this.token) {
57 | this.fhirService.reinitializeManually(this.url, this.token);
58 | } else {
59 |
60 |
61 | this.fhirService.client?.read({ resourceType: 'Patient', id: this.fhirService.patient! }).then((r: any) => {
62 | console.log("Patient read returned: " + r);
63 | this.patient = r;
64 | console.log("HomeComponent has been initialized.");
65 | });
66 | }
67 | }
68 |
69 | displayName() {
70 | let n = '';
71 | if (this.patient?.name?.length) {
72 | if (this.patient!.name![0].given) {
73 | n += this.patient.name[0].given;
74 | }
75 | if (this.patient!.name![0].family) {
76 | n += ' ' + this.patient.name[0].family;
77 | }
78 | }
79 | return n;
80 | }
81 |
82 | ngOnInit(): void {
83 | // let span = this.tracer.startSpan('home-component-initialization');
84 | console.log('Initializing home component.');
85 | }
86 |
87 | search() {
88 | console.log('Searching for ' + this.selected_resource_type + ' with: ' + this.search_text);
89 |
90 | }
91 |
92 | }
93 |
--------------------------------------------------------------------------------
/src/app/patient/patient.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input } from '@angular/core';
2 |
3 |
4 | import { Patient } from "fhir/r4";
5 | import { FhirService } from '../services/fhir.service';
6 |
7 | @Component({
8 | selector: 'patient',
9 | templateUrl: 'patient.html'
10 | })
11 | export class PatientComponent {
12 |
13 | @Input() patient: Patient | undefined;
14 | // patients: Array = [];
15 | // servers: FhirService[] = FhirService.servers;
16 |
17 | constructor(public fhirService: FhirService) {
18 | // this.compiler.clearCache();
19 | // this.selectServer(fhirService.current);
20 | this.loadData();
21 | }
22 |
23 | loadData() {
24 |
25 | }
26 |
27 | // loadPatients() {
28 | // this.fhirService.client?.search({ resourceType: 'Patient' }).then((data: any) => {
29 | // let b = data;
30 | // console.log("Loading " + JSON.stringify(b, null, "\t"));
31 | // this.patients = >data.resource;
32 | // console.log("Loaded " + this.total() + " patients.");
33 | // if (this.patients?.length > 0) {
34 | // this.select(this.patients[0].id!);
35 | // }
36 | // });
37 | // }
38 |
39 | // total(): number {
40 | // var t = 0;
41 | // if (this.patients) {
42 | // t = this.patients.length;
43 | // }
44 | // return t;
45 | // }
46 |
47 | // select(e: any) {
48 | // let patientId: string = e.target.value;
49 | // console.log("Selected patient: " + patientId);
50 | // this.fhirService.client?.read({ resourceType: 'Patient', id: patientId }).then((p) => {
51 | // console.log("Fetched: " + JSON.stringify(p));
52 | // this.patient = p;
53 | // });
54 | // this.fhirService.client.get(patientId).subscribe((d: any) => {
55 | // console.log(this.fhirService.current + " Fetching: " + d);
56 | // this.selected = d; //.entry['resource'];
57 | // });
58 | // }
59 |
60 | // selectServer(server: FhirServer | null) {
61 | // if (server) {
62 | // console.log("Setting server to: " + server.url);
63 | // this.fhirService.current = server;
64 | // // this.fhirService.setUrl(server.url);
65 | // } this.loadPatients();
66 | // }
67 |
68 |
69 | // selectServerForUrl(e: any) {
70 | // let url: string = e.target.value;
71 | // this.selectServer(this.serverFor(url));
72 | // }
73 |
74 | // serverFor(url: string) {
75 | // let s: FhirServer | null = null;
76 | // for (var server of this.fhirService.servers) {
77 | // if (server.url == url) {
78 | // s = server;
79 | // break;
80 | // }
81 | // }
82 | // return s;
83 | // }
84 |
85 | genderString(patient: Patient) {
86 | var s = 'Unknown';
87 | switch (patient.gender) {
88 | case 'female':
89 | s = 'Female';
90 | break;
91 | case 'male':
92 | s = 'Male';
93 | break;
94 | }
95 | return s;
96 | }
97 |
98 | }
99 |
--------------------------------------------------------------------------------
/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /***************************************************************************************************
2 | * Load `$localize` onto the global scope - used if i18n tags appear in Angular templates.
3 | */
4 | import '@angular/localize/init';
5 | /**
6 | * This file includes polyfills needed by Angular and is loaded before the app.
7 | * You can add your own extra polyfills to this file.
8 | *
9 | * This file is divided into 2 sections:
10 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
11 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
12 | * file.
13 | *
14 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
15 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
16 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
17 | *
18 | * Learn more in https://angular.io/guide/browser-support
19 | */
20 |
21 | /***************************************************************************************************
22 | * BROWSER POLYFILLS
23 | */
24 |
25 | /**
26 | * IE11 requires the following for NgClass support on SVG elements
27 | */
28 | // import 'classlist.js'; // Run `npm install --save classlist.js`.
29 |
30 | /**
31 | * Web Animations `@angular/platform-browser/animations`
32 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
33 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
34 | */
35 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`.
36 |
37 | /**
38 | * By default, zone.js will patch all possible macroTask and DomEvents
39 | * user can disable parts of macroTask/DomEvents patch by setting following flags
40 | * because those flags need to be set before `zone.js` being loaded, and webpack
41 | * will put import in the top of bundle, so user need to create a separate file
42 | * in this directory (for example: zone-flags.ts), and put the following flags
43 | * into that file, and then add the following code before importing zone.js.
44 | * import './zone-flags';
45 | *
46 | * The flags allowed in zone-flags.ts are listed here.
47 | *
48 | * The following flags will work for all browsers.
49 | *
50 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
51 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
52 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
53 | *
54 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
55 | * with the following flag, it will bypass `zone.js` patch for IE/Edge
56 | *
57 | * (window as any).__Zone_enable_cross_context_check = true;
58 | *
59 | */
60 |
61 | /***************************************************************************************************
62 | * Zone JS is required by default for Angular itself.
63 | */
64 | import 'zone.js'; // Included with Angular CLI.
65 |
66 |
67 | /***************************************************************************************************
68 | * APPLICATION IMPORTS
69 | */
70 |
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "cli": {
4 | "analytics": false
5 | },
6 | "version": 1,
7 | "newProjectRoot": "projects",
8 | "defaultProject": "angular-on-fhir",
9 | "projects": {
10 | "angular-on-fhir": {
11 | "projectType": "application",
12 | "schematics": {
13 | "@schematics/angular:component": {
14 | "style": "scss"
15 | },
16 | "@schematics/angular:application": {
17 | "strict": true
18 | }
19 | },
20 | "root": "",
21 | "sourceRoot": "src",
22 | "prefix": "app",
23 | "architect": {
24 | "build": {
25 | "builder": "@angular-devkit/build-angular:browser",
26 | "options": {
27 | "outputPath": "dist/angular-on-fhir",
28 | "index": "src/index.html",
29 | "main": "src/main.ts",
30 | "polyfills": "src/polyfills.ts",
31 | "tsConfig": "tsconfig.app.json",
32 | "inlineStyleLanguage": "scss",
33 | "assets": [
34 | "src/favicon.ico",
35 | "src/assets",
36 | "src/images"
37 | ],
38 | "styles": [
39 | "./node_modules/bootstrap/dist/css/bootstrap.css",
40 | "./node_modules/bootstrap-icons/font/bootstrap-icons.css",
41 | "src/styles/lux.bootstrap.min.css",
42 | "src/styles/styles.scss",
43 | "src/styles/ribbons.scss"
44 | ],
45 | "scripts": [
46 | "./node_modules/jquery/dist/jquery.min.js",
47 | "./node_modules/@popperjs/core/dist/umd/popper.min.js",
48 | "./node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"
49 | ]
50 | },
51 | "configurations": {
52 | "production": {
53 | "budgets": [{
54 | "type": "initial",
55 | "maximumWarning": "1mb",
56 | "maximumError": "4mb"
57 | },
58 | {
59 | "type": "anyComponentStyle",
60 | "maximumWarning": "2kb",
61 | "maximumError": "4kb"
62 | }
63 | ],
64 | "fileReplacements": [{
65 | "replace": "src/environments/environment.ts",
66 | "with": "src/environments/environment.prod.ts"
67 | }],
68 | "outputHashing": "all"
69 | },
70 | "development": {
71 | "buildOptimizer": false,
72 | "optimization": false,
73 | "vendorChunk": true,
74 | "extractLicenses": false,
75 | "sourceMap": true,
76 | "namedChunks": true
77 | }
78 | },
79 | "defaultConfiguration": "production"
80 | },
81 | "serve": {
82 | "builder": "@angular-devkit/build-angular:dev-server",
83 | "configurations": {
84 | "production": {
85 | "browserTarget": "angular-on-fhir:build:production"
86 | },
87 | "development": {
88 | "browserTarget": "angular-on-fhir:build:development"
89 | }
90 | },
91 | "defaultConfiguration": "development"
92 | },
93 | "extract-i18n": {
94 | "builder": "@angular-devkit/build-angular:extract-i18n",
95 | "options": {
96 | "browserTarget": "angular-on-fhir:build"
97 | }
98 | },
99 | "test": {
100 | "builder": "@angular-devkit/build-angular:karma",
101 | "options": {
102 | "main": "src/test.ts",
103 | "polyfills": "src/polyfills.ts",
104 | "tsConfig": "tsconfig.spec.json",
105 | "karmaConfig": "karma.conf.js",
106 | "inlineStyleLanguage": "scss",
107 | "assets": [
108 | "src/favicon.ico",
109 | "src/assets"
110 | ],
111 | "styles": [
112 | "src/styles.scss"
113 | ],
114 | "scripts": []
115 | }
116 | }
117 | }
118 | }
119 | }
120 | }
--------------------------------------------------------------------------------
/src/app/home/home.html:
--------------------------------------------------------------------------------
1 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | |
64 | |
65 | |
66 |
67 |
68 |
69 |
70 | |
71 | |
72 | |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
98 |
99 |
100 |
101 |
102 |
119 |
120 |
121 |
--------------------------------------------------------------------------------