├── .gitignore ├── Dockerfile ├── README.md ├── angular.json ├── doc └── screenshots │ └── 1.png ├── package-lock.json ├── package.json ├── src ├── app │ ├── allergies │ │ ├── allergies.component.ts │ │ └── allergies.html │ ├── api │ │ ├── api.component.ts │ │ └── api.html │ ├── app-routing.module.ts │ ├── app.component.ts │ ├── app.html │ ├── app.module.ts │ ├── auth │ │ └── auth-config.ts │ ├── conditions │ │ ├── conditions.component.ts │ │ └── conditions.html │ ├── directives │ │ ├── auto-grow.directive.ts │ │ └── highlight.directive.ts │ ├── home │ │ ├── home.component.ts │ │ └── home.html │ ├── models │ │ └── server.ts │ ├── observations │ │ ├── observations.component.ts │ │ └── observations.html │ ├── patient │ │ ├── patient.component.ts │ │ └── patient.html │ ├── pipes │ │ └── humanizeBytes.pipe.ts │ ├── services │ │ ├── fhir.service.ts │ │ └── telemetry.service.ts │ └── smart │ │ ├── launch.component.ts │ │ ├── launch.html │ │ ├── redirect.component.ts │ │ ├── redirect.html │ │ └── smart_configuration.ts ├── assets │ └── configuration.template.js ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── images │ └── textures │ │ ├── crossword.png │ │ ├── diagonal-noise.png │ │ ├── diagonal-noise_@2X.png │ │ ├── ricepaper.png │ │ ├── ricepaper_@2X.png │ │ ├── tileable_wood_texture.png │ │ ├── tileable_wood_texture_@2X.png │ │ ├── wild_oliva.png │ │ ├── wild_oliva_@2X.png │ │ ├── xv.png │ │ └── xv_@2X.png ├── index.html ├── main.ts ├── polyfills.ts ├── styles │ ├── ribbons.scss │ └── styles.scss ├── vendor.ts └── version.json ├── tsconfig.app.json └── tsconfig.json /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | ![Metadata](https://raw.githubusercontent.com/preston/angular-on-fhir/master/doc/screenshots/1.png) 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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /doc/screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/preston/angular-on-fhir/b04b804989c069d36238a92e6d7195ff41c135c1/doc/screenshots/1.png -------------------------------------------------------------------------------- /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/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/allergies/allergies.html: -------------------------------------------------------------------------------- 1 |
2 |

Allergies

3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
What goeth here, say ye?
Something
15 |
-------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /src/app/app.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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/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/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/conditions/conditions.html: -------------------------------------------------------------------------------- 1 |

(none)

2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
DescriptionStatusOnset
{{c?.code?.text}}{{c.clinicalStatus}}{{c.onsetDateTime | amTimeAgo}}
-------------------------------------------------------------------------------- /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/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/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/home/home.html: -------------------------------------------------------------------------------- 1 | 39 | 40 |
41 | 42 | 43 |
44 | 45 |
46 |
47 |
48 | 52 | 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 | 98 | 99 | 100 | 101 | 102 | 119 |
120 |
121 |
-------------------------------------------------------------------------------- /src/app/models/server.ts: -------------------------------------------------------------------------------- 1 | export class FhirServer { 2 | constructor(public name: string, public url: string) { 3 | } 4 | } -------------------------------------------------------------------------------- /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/observations/observations.html: -------------------------------------------------------------------------------- 1 |
2 |
Observations
3 |
4 |
Showing {{observationBundle.entry?.length || 'no'}} entries.
5 |

(none)

6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 27 | 32 | 35 | 36 | 37 | 38 |
DateCategoriesCodesValueStatus
19 | {{o?.resource?.effectiveDateTime}}
{{o?.resource?.effectiveDateTime| amTimeAgo}}
21 |
22 |

23 | {{coding.display}} 24 |

25 |
26 |
{{ o?.resource?.code?.text}} 28 |
29 |

{{c.display}}

30 |
31 |
{{ o.resource?.valueQuantity?.value}} 33 | {{o.resource?.valueQuantity?.unit}}{{o.resource?.valueCodeableConcept?.text}} 34 | {{o.resource?.status}}
39 |
40 |
-------------------------------------------------------------------------------- /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/app/patient/patient.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 | 9 | 10 | 18 | 19 | 20 |
-------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/app/smart/launch.html: -------------------------------------------------------------------------------- 1 |
2 |

Launching application...

3 |
4 |

{{message}}

5 |
6 |
-------------------------------------------------------------------------------- /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/smart/redirect.html: -------------------------------------------------------------------------------- 1 | Completing SMART launch process... -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /src/images/textures/crossword.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/preston/angular-on-fhir/b04b804989c069d36238a92e6d7195ff41c135c1/src/images/textures/crossword.png -------------------------------------------------------------------------------- /src/images/textures/diagonal-noise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/preston/angular-on-fhir/b04b804989c069d36238a92e6d7195ff41c135c1/src/images/textures/diagonal-noise.png -------------------------------------------------------------------------------- /src/images/textures/diagonal-noise_@2X.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/preston/angular-on-fhir/b04b804989c069d36238a92e6d7195ff41c135c1/src/images/textures/diagonal-noise_@2X.png -------------------------------------------------------------------------------- /src/images/textures/ricepaper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/preston/angular-on-fhir/b04b804989c069d36238a92e6d7195ff41c135c1/src/images/textures/ricepaper.png -------------------------------------------------------------------------------- /src/images/textures/ricepaper_@2X.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/preston/angular-on-fhir/b04b804989c069d36238a92e6d7195ff41c135c1/src/images/textures/ricepaper_@2X.png -------------------------------------------------------------------------------- /src/images/textures/tileable_wood_texture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/preston/angular-on-fhir/b04b804989c069d36238a92e6d7195ff41c135c1/src/images/textures/tileable_wood_texture.png -------------------------------------------------------------------------------- /src/images/textures/tileable_wood_texture_@2X.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/preston/angular-on-fhir/b04b804989c069d36238a92e6d7195ff41c135c1/src/images/textures/tileable_wood_texture_@2X.png -------------------------------------------------------------------------------- /src/images/textures/wild_oliva.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/preston/angular-on-fhir/b04b804989c069d36238a92e6d7195ff41c135c1/src/images/textures/wild_oliva.png -------------------------------------------------------------------------------- /src/images/textures/wild_oliva_@2X.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/preston/angular-on-fhir/b04b804989c069d36238a92e6d7195ff41c135c1/src/images/textures/wild_oliva_@2X.png -------------------------------------------------------------------------------- /src/images/textures/xv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/preston/angular-on-fhir/b04b804989c069d36238a92e6d7195ff41c135c1/src/images/textures/xv.png -------------------------------------------------------------------------------- /src/images/textures/xv_@2X.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/preston/angular-on-fhir/b04b804989c069d36238a92e6d7195ff41c135c1/src/images/textures/xv_@2X.png -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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/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/version.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": { 3 | "major": 0, 4 | "minor": 0, 5 | "patch": 0 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------