├── .npmrc ├── src ├── lib │ ├── HealthLinkOverviewItem.svelte │ ├── types.ts │ ├── util.ts │ ├── app.d.ts │ ├── issuer.private.jwks.json │ ├── config.ts │ ├── HealthLinkOverview.svelte │ ├── managementClient.ts │ ├── AddFile.svelte │ └── HealthLink.svelte ├── routes │ ├── +layout.ts │ ├── +page.svelte │ ├── view │ │ └── [id] │ │ │ └── +page.svelte │ ├── create │ │ └── +page.svelte │ ├── home │ │ └── +page.svelte │ └── +layout.svelte └── app.html ├── static ├── favicon.ico ├── favicon.png ├── img │ ├── menu.png │ ├── favicon-SMART.ico │ ├── favicon-SMART.png │ ├── waverifylogo.png │ ├── doh_logo_doh-black.png │ ├── waverifypluslogo.png │ ├── waverifypluslogobold.png │ └── smart-logo.svg ├── ips │ ├── assets │ │ ├── js │ │ │ ├── config.js │ │ │ ├── retreiveIPS.js │ │ │ └── renderIPS.js │ │ ├── html │ │ │ ├── footer.html │ │ │ └── header.html │ │ ├── html-waverify │ │ │ ├── footer.html │ │ │ └── header.html │ │ └── css │ │ │ └── custom.css │ ├── templates │ │ ├── Text.html │ │ ├── Composition.html │ │ ├── Patient.html │ │ ├── Immunizations.html │ │ ├── Problems.html │ │ ├── Allergies.html │ │ ├── Checks.html │ │ ├── AdvanceDirectives.html │ │ ├── Observations.html │ │ └── Medications.html │ ├── templates-waverify │ │ ├── Text.html │ │ ├── Patient.html │ │ ├── Composition.html │ │ ├── Immunizations.html │ │ ├── Problems.html │ │ ├── Allergies.html │ │ ├── Checks.html │ │ ├── AdvanceDirectives.html │ │ ├── Observations.html │ │ └── Medications.html │ └── index.html ├── color guide.html └── banner.js ├── default.env ├── fix-popper.sh ├── .gitignore ├── docker-compose.yaml ├── .prettierignore ├── vite.config.ts ├── .prettierrc ├── Dockerfile ├── tsconfig.json ├── svelte.config.js ├── package.json ├── .github └── workflows │ └── deploy.yml └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /src/lib/HealthLinkOverviewItem.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/routes/+layout.ts: -------------------------------------------------------------------------------- 1 | export const prerender = false; 2 | export const ssr = false; 3 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jddamore/shl-ips/main/static/favicon.ico -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jddamore/shl-ips/main/static/favicon.png -------------------------------------------------------------------------------- /default.env: -------------------------------------------------------------------------------- 1 | # Port to expose to internet; use in SERVER_NAME (portal.env) 2 | # EXTERNAL_PORT= -------------------------------------------------------------------------------- /static/img/menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jddamore/shl-ips/main/static/img/menu.png -------------------------------------------------------------------------------- /static/img/favicon-SMART.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jddamore/shl-ips/main/static/img/favicon-SMART.ico -------------------------------------------------------------------------------- /static/img/favicon-SMART.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jddamore/shl-ips/main/static/img/favicon-SMART.png -------------------------------------------------------------------------------- /static/img/waverifylogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jddamore/shl-ips/main/static/img/waverifylogo.png -------------------------------------------------------------------------------- /fix-popper.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | sed -i '/2\.11\.6/a \ \ "type": "module",' node_modules/@popperjs/core/package.json 3 | -------------------------------------------------------------------------------- /static/img/doh_logo_doh-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jddamore/shl-ips/main/static/img/doh_logo_doh-black.png -------------------------------------------------------------------------------- /static/img/waverifypluslogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jddamore/shl-ips/main/static/img/waverifypluslogo.png -------------------------------------------------------------------------------- /static/img/waverifypluslogobold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jddamore/shl-ips/main/static/img/waverifypluslogobold.png -------------------------------------------------------------------------------- /static/ips/assets/js/config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | html_dir: "/ips/assets/html-waverify/", 3 | template_dir: "/ips/templates-waverify/" 4 | } 5 | 6 | export default config; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | vite.config.js.timestamp-* 10 | vite.config.ts.timestamp-* 11 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: "3.4" 3 | services: 4 | shlips: 5 | build: ./ 6 | ports: 7 | - "127.0.0.1:${EXTERNAL_PORT:-3000}:3000" 8 | env_file: 9 | - .env -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig(({ mode }) => ({ 5 | plugins: [sveltekit()], 6 | server: { 7 | host: true, 8 | port: 3000 9 | } 10 | })); 11 | -------------------------------------------------------------------------------- /src/lib/types.ts: -------------------------------------------------------------------------------- 1 | export type Bundle = unknown; 2 | export interface SHCRetrieveEvent { 3 | shc: SHCFile; 4 | label?: string; 5 | content: Bundle; 6 | exp?: number; 7 | } 8 | 9 | export interface SHCFile { 10 | verifiableCredential: string[]; 11 | } 12 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100, 6 | "plugins": ["prettier-plugin-svelte"], 7 | "pluginSearchDirs": ["."], 8 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/util.ts: -------------------------------------------------------------------------------- 1 | import * as jose from 'jose'; 2 | 3 | export const base64url = jose.base64url; 4 | 5 | export function randomStringWithEntropy(entropy = 32): string { 6 | const b = new Uint8Array(entropy); 7 | crypto.getRandomValues(b); 8 | return base64url.encode(b); 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface Platform {} 9 | } 10 | } 11 | 12 | export {}; 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18 as build-deps 2 | 3 | EXPOSE 3000 4 | 5 | # ENV DIRPATH /opt/app 6 | ENV NODE_ENV production 7 | 8 | WORKDIR /opt/app 9 | 10 | COPY package*.json ./ 11 | RUN npm clean-install --include=dev 12 | 13 | COPY ./fix-popper.sh ./ 14 | RUN ./fix-popper.sh 15 | 16 | COPY . . 17 | RUN npm run build 18 | 19 | RUN cp build/404.html build/index.html 20 | 21 | CMD ["npm", "run", "start"] -------------------------------------------------------------------------------- /src/lib/issuer.private.jwks.json: -------------------------------------------------------------------------------- 1 | { 2 | "keys": [ 3 | { 4 | "kty": "EC", 5 | "kid": "3Kfdg-XwP-7gXyywtUfUADwBumDOPKMQx-iELL11W9s", 6 | "use": "sig", 7 | "alg": "ES256", 8 | "crv": "P-256", 9 | "x": "11XvRWy1I2S0EyJlyf_bWfw_TQ5CJJNLw78bHXNxcgw", 10 | "y": "eZXwxvO1hvCY0KucrPfKo7yAyMT6Ajc3N7OkAB6VYy8", 11 | "d": "FvOOk6hMixJ2o9zt4PCfan_UW7i4aOEnzj76ZaCI9Og" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | %sveltekit.head% 9 | 10 | 11 |
%sveltekit.body%
12 | 13 | 14 | -------------------------------------------------------------------------------- /src/lib/config.ts: -------------------------------------------------------------------------------- 1 | // import {PUBLIC_BASE_URL} from '$env/static/public'; 2 | import { dev } from '$app/environment'; 3 | 4 | export const API_BASE = 'https://smart-health-links-server.cirg.washington.edu/api'; 5 | export const VIEWER_BASE = new URL( 6 | `/ips${dev ? '/index.html' : ''}#`, 7 | window.location.href 8 | ).toString(); 9 | export const EXAMPLE_IPS = 10 | 'https://ips.health/fhir/Patient/98549f1a-e0d5-4454-849c-f5b97d3ed299/$summary'; 11 | -------------------------------------------------------------------------------- /static/ips/templates/Text.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | {{titulo}} 7 |
8 |
9 | {{div}} 10 |
11 |
-------------------------------------------------------------------------------- /static/ips/templates-waverify/Text.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | {{titulo}} 7 |
8 |
9 | {{div}} 10 |
11 |
-------------------------------------------------------------------------------- /src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true 12 | } 13 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 14 | // 15 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 16 | // from the referenced tsconfig.json - TypeScript does not merge them in 17 | } 18 | -------------------------------------------------------------------------------- /static/ips/templates-waverify/Patient.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | Patient 7 |
8 |
9 |

10 | Name: {{name[0].given}}, {{name[0].family}} 11 |
12 | Birth Date: {{birthDate}} 13 |

14 |
15 |
-------------------------------------------------------------------------------- /static/ips/templates/Composition.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | Document (Composition) 7 |
8 |
9 |
{{title}}
10 |

11 | Summary Date: {{date}} 12 |
13 |

14 |
15 |
-------------------------------------------------------------------------------- /src/routes/view/[id]/+page.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | {#if shl} 15 | 16 | {:else} 17 | SHLink {$page.params.id} Not Found 18 | {/if} 19 | -------------------------------------------------------------------------------- /static/ips/templates-waverify/Composition.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | Document (Composition) 7 |
8 |
9 |
{{title}}
10 |

11 | Summary Date: {{date}} 12 |
13 |

14 |
15 |
-------------------------------------------------------------------------------- /static/ips/assets/html/footer.html: -------------------------------------------------------------------------------- 1 |
2 | Originally created by Alejandro Lopez Osornio, Diego Kaminker and Fernando Campos. Modified 2021-2023 by John 3 | D'Amore 4 |
5 | Based on prior work from this repository 7 |
8 | Licensed according to Apache 2.0. See current code 9 | repository for details 10 |
11 | Hosted by More Informatics, Inc. 12 | -------------------------------------------------------------------------------- /static/ips/templates/Patient.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | Patient 7 |
8 |
9 |
10 |

11 | Birth Date: {{birthDate}} 12 |
13 | Name: {{name[0].given}}, {{name[0].family}} 14 |

15 |
16 |
-------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from '@sveltejs/kit/vite'; 2 | import adapter from '@sveltejs/adapter-static'; 3 | 4 | const dev = process.argv.includes('dev'); 5 | 6 | /** @type {import('@sveltejs/kit').Config} */ 7 | export default { 8 | preprocess: vitePreprocess(), 9 | kit: { 10 | adapter: adapter({ 11 | // default options are shown. On some platforms 12 | // these options are set automatically — see below 13 | pages: 'build', 14 | assets: 'build', 15 | fallback: "404.html", 16 | precompress: false, 17 | strict: true, 18 | paths: { 19 | base: dev ? '' : '/shlips' 20 | } 21 | }) 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /static/ips/assets/html-waverify/footer.html: -------------------------------------------------------------------------------- 1 |
2 | Originally created by Alejandro Lopez Osornio, Diego Kaminker and Fernando Campos. Modified 2021-2023 by John 3 | D'Amore, and 2023 by CIRG 4 |
5 | Based on prior work from this repository 7 |
8 | Licensed according to Apache 2.0. See current code 9 | repository for details 10 |
11 | Hosted by the University of Washington's Clinical Informatics Research Group -------------------------------------------------------------------------------- /static/color guide.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | orange: rgb(200, 76, 14)

7 | blue: rgb(34, 72, 156)

8 | error red: rgb(179, 0, 0);

9 | Note: 10 |


11 | Button Style 12 | 13 | 14 | -------------------------------------------------------------------------------- /static/ips/templates/Immunizations.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | Immunizations 7 |
8 |
9 |
    10 | {{each(options.immunizations)}} 11 |
  • 12 | {{@this.occurrenceDateTime}}
    13 | {{each(options.immunizations[@index].vaccineCode.coding)}} 14 | {{@this.system}} {{@this.display}} ({{@this.code}})
    15 | {{/each}} 16 |
  • 17 | {{/each}} 18 |
19 |
20 |
-------------------------------------------------------------------------------- /static/ips/assets/js/retreiveIPS.js: -------------------------------------------------------------------------------- 1 | import * as shlClient from 'https://smart-health-links-demo.cirg.washington.edu/index.js'; 2 | import { verify } from 'https://smart-health-links-demo.cirg.washington.edu/shc-decoder.js'; 3 | import { update } from "./renderIPS.js"; 4 | 5 | const shl = window.location.hash.match(/shlink:\/.*/)?.[0]; 6 | if (shl) { 7 | retrieve() 8 | } 9 | 10 | async function retrieve(){ 11 | const recipient = "WA Verify+ IPS Viewer"; 12 | 13 | let passcode; 14 | const needPasscode = shlClient.flag({ shl}).includes('P'); 15 | if (needPasscode) { 16 | passcode = prompt("Enter passcode for SMART Health Link"); 17 | } 18 | 19 | const retrieved = await shlClient.retrieve({ 20 | shl, 21 | passcode, 22 | recipient 23 | }); 24 | 25 | const decoded = await Promise.all(retrieved.shcs.map(verify)); 26 | const data = decoded[0].fhirBundle 27 | 28 | $('#ipsInput').val(JSON.stringify(data, null, 2)); 29 | update(data); 30 | } -------------------------------------------------------------------------------- /src/routes/create/+page.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | { 22 | const newShl = await newShlFromShc(detail); 23 | $shlStore = [...$shlStore, newShl]; 24 | goto(`/view/${newShl.id}`); 25 | }} 26 | /> 27 | -------------------------------------------------------------------------------- /static/ips/templates-waverify/Immunizations.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | Immunizations 7 |
8 |
9 |
    10 | {{each(options.immunizations)}} 11 |
  • 12 | {{@this.occurrenceDateTime}}
    13 | 14 | {{if(@this.vaccineCode.coding[0].display)}} 15 | {{@this.vaccineCode.coding[0].display}} 16 | {{#else}} 17 | {{@this.vaccineCode.text}} np 18 | {{/if}} 19 | 20 | {{@this.vaccineCode.coding[0].system}} 21 | {{@this.vaccineCode.coding[0].code}}
    22 |
  • 23 | {{/each}} 24 |
25 |
26 |
-------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shlips", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "start": "npm install serve && serve build/", 7 | "dev": "vite dev", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 11 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 12 | "lint": "prettier --plugin-search-dir . --check .", 13 | "format": "prettier --plugin-search-dir . --write ." 14 | }, 15 | "devDependencies": { 16 | "@sveltejs/adapter-auto": "^2.0.0", 17 | "@sveltejs/adapter-static": "^2.0.0", 18 | "@sveltejs/kit": "^1.5.0", 19 | "prettier": "^2.8.0", 20 | "prettier-plugin-svelte": "^2.8.1", 21 | "svelte": "^3.55.1", 22 | "svelte-check": "^3.0.1", 23 | "tslib": "^2.4.1", 24 | "typescript": "^4.9.3", 25 | "vite": "^4.0.0" 26 | }, 27 | "type": "module", 28 | "dependencies": { 29 | "@types/pako": "^2.0.0", 30 | "@types/qrcode": "^1.5.0", 31 | "base64url": "^3.0.1", 32 | "bootstrap": "^5.2.3", 33 | "jose": "^4.11.4", 34 | "pako": "^2.1.0", 35 | "qrcode": "^1.5.1", 36 | "serve": "^14.2.0", 37 | "sveltestrap": "^5.10.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /static/ips/templates/Problems.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | Problems / Diagnoses 7 |
8 |
9 |
    10 | {{each(options.problems)}} 11 |
  • 12 | {{if(options.problems[@index].onsetDateTime)}} 13 | {{/if}} 14 | {{if(options.problems[@index].code && options.problems[@index].code.coding && options.problems[@index].code.coding[0])}} 15 | {{@this.code.coding[0].system}} 16 | {{@this.code.coding[0].display}} ({{@this.code.coding[0].code}}) 17 | {{/if}} 18 | {{if(options.problems[@index].code && options.problems[@index].code.text)}} 19 | [Uncoded text shown]: {{@this.code.text}} 20 | {{/if}} 21 |
  • 22 | {{/each}} 23 |
24 |
25 |
-------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | on: 3 | push: 4 | branches: [ main ] 5 | workflow_dispatch: 6 | 7 | permissions: 8 | contents: read 9 | pages: write 10 | id-token: write 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Check out your repository using git 17 | uses: actions/checkout@v2 18 | 19 | - name: Use Node.js 18 20 | uses: actions/setup-node@v2 21 | with: 22 | node-version: '18' 23 | cache: 'npm' 24 | 25 | - name: Install dependencies 26 | run: npm ci 27 | 28 | - name: Patch per https://github.com/trasherdk/sveltestrap/issues/5 29 | run: ./fix-popper.sh 30 | 31 | - name: Build SvelteKit 32 | run: npm run build 33 | 34 | - name: Avoid 404 on main load 35 | run: cp build/404.html build/index.html 36 | 37 | - name: Upload artifact 38 | uses: actions/upload-pages-artifact@v1 39 | with: 40 | path: ./build 41 | 42 | deploy: 43 | environment: 44 | name: github-pages 45 | url: ${{ steps.deployment.outputs.page_url }} 46 | runs-on: ubuntu-latest 47 | needs: build 48 | steps: 49 | - name: Deploy to GitHub Pages 50 | id: deployment 51 | uses: actions/deploy-pages@v1 -------------------------------------------------------------------------------- /static/ips/templates-waverify/Problems.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | Problems / Diagnoses 7 |
8 |
9 |
    10 | {{each(options.problems)}} 11 |
  • 12 | {{if(options.problems[@index].onsetDateTime)}} 13 | {{/if}} 14 | {{if(options.problems[@index].code && options.problems[@index].code.coding && options.problems[@index].code.coding[0])}} 15 | {{@this.code.coding[0].system}} 16 |
    17 | {{@this.code.coding[0].display}} ({{@this.code.coding[0].code}}) 18 | {{/if}} 19 | {{if(options.problems[@index].code && options.problems[@index].code.text)}} 20 | [Uncoded text shown]: {{@this.code.text}} 21 | {{/if}} 22 |
  • 23 | {{/each}} 24 |
25 |
26 |
-------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # create-svelte 2 | 3 | Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte). 4 | 5 | ## Creating a project 6 | 7 | If you're seeing this, you've probably already done this step. Congrats! 8 | 9 | ```bash 10 | # create a new project in the current directory 11 | npm create svelte@latest 12 | 13 | # create a new project in my-app 14 | npm create svelte@latest my-app 15 | ``` 16 | 17 | ## Developing 18 | 19 | Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: 20 | 21 | ```bash 22 | npm run dev 23 | 24 | # or start the server and open the app in a new browser tab 25 | npm run dev -- --open 26 | ``` 27 | 28 | ## Building 29 | 30 | To create a production version of your app: 31 | 32 | ```bash 33 | npm run build 34 | ``` 35 | 36 | You can preview the production build with `npm run preview`. 37 | 38 | > To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment. 39 | 40 | ## Docker 41 | 42 | Environment 43 | 44 | ```bash 45 | cp default.env .env 46 | ``` 47 | 48 | Starting the docker container 49 | 50 | ```bash 51 | docker-compose build && docker-compose up -d 52 | ``` 53 | -------------------------------------------------------------------------------- /static/ips/templates/Allergies.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | Allergies and Intolerances 7 |
8 |
9 |
    10 | {{each(options.allergies)}} 11 |
  • 12 | {{if(@this.criticality === "high")}} 13 | 14 | {{@this.type}} - {{@this.category[0]}} - Criticality: High 15 | 16 | {{#else}} 17 | 18 | {{@this.type}} - {{@this.category[0]}} - Criticality: {{@this.criticality}} 19 | 20 | {{/if}} 21 | {{if(@this.code && @this.code.coding)}} 22 |
    23 | {{@this.code.coding[0].display}} ({{@this.code.coding[0].code}}) 24 |
    25 | {{/if}} 26 |
  • 27 | {{/each}} 28 |
29 |
30 |
-------------------------------------------------------------------------------- /static/ips/templates-waverify/Allergies.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | Allergies and Intolerancies 7 |
8 |
9 |
    10 | {{each(options.allergies)}} 11 |
  • 12 | {{if(@this.criticality)}} 13 | {{if(@this.criticality === "high")}} 14 | 15 | {{#else}} 16 | 17 | {{/if}} 18 | {{#else}} 19 | 20 | {{/if}} 21 | {{@this.type}} - {{@this.category[0]}} - Criticality: {{@this.criticality}} 22 | 23 | {{if(@this.code && @this.code.coding)}} 24 |
    25 | {{@this.code.coding[0].display}} ({{@this.code.coding[0].code}}) 26 |
    27 | {{/if}} 28 |
  • 29 | {{/each}} 30 |
31 |
32 |
-------------------------------------------------------------------------------- /static/ips/templates/Checks.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | Simple Data Checks (not complete FHIR validation) 7 |
8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {{each(options.data)}} 21 | 22 | 23 | 24 | 25 | 26 | {{/each}} 27 |
SectionEntriesNarrative
{{@this.display}}{{@this.entries}}{{@this.narrative}}
28 |
    29 | {{each(options.errors)}} 30 |
  • 31 | {{@this}} 32 |
  • 33 | {{/each}} 34 |
35 |
36 |
-------------------------------------------------------------------------------- /static/ips/templates-waverify/Checks.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | Simple Data Checks (not complete FHIR validation) 7 |
8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {{each(options.data)}} 21 | 22 | 23 | 24 | 25 | 26 | {{/each}} 27 |
SectionEntriesNarrative
{{@this.display}}{{@this.entries}}{{@this.narrative}}
28 |
    29 | {{each(options.errors)}} 30 |
  • 31 | {{@this}} 32 |
  • 33 | {{/each}} 34 |
35 |
36 |
-------------------------------------------------------------------------------- /static/banner.js: -------------------------------------------------------------------------------- 1 | document.write(` 2 |
3 |
4 |
5 |
6 |
7 |
8 | Washington State Department of Health Logo 9 |
10 | 11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | 27 |
28 |
29 |
30 |

WA Verify+ International Patient Summary Viewer

31 |
32 | `); -------------------------------------------------------------------------------- /src/lib/HealthLinkOverview.svelte: -------------------------------------------------------------------------------- 1 | 35 | 36 | {#if $shlStore.length > 0} 37 | 38 |

Your Stored SHLinks

39 |
40 | {#each $shlStore as shl, i} 41 | 42 | {shl.label || `SHLink ${i + 1}`} 43 | 44 | 45 | {/each} 46 | 47 | 48 |
49 | {/if} 50 |
51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 |
59 | 60 | -------------------------------------------------------------------------------- /static/ips/templates/AdvanceDirectives.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | Advance Directives 7 |
8 |
9 |
    10 | {{each(options.ad)}} 11 |
  • 12 | Type: {{@this.resourceType}} 13 |
    14 | Text: 15 | {{if (@this.text && @this.text.div)}} 16 | {{@this.text.div}} 17 | {{#else}} 18 | No text provided in resource 19 | {{/if}} 20 |
    21 | Category: 22 | {{if (@this.category && @this.category[0] && @this.category[0].coding && @this.category[0].coding[0])}} 23 | {{@this.category[0].coding[0].display}} 24 | {{/if}} 25 |
    26 | Intent: 27 | {{if (@this.provision && @this.provision.code && @this.provision.code[0] && @this.provision.code[0].coding && @this.provision.code[0].coding[0]) }} 28 | {{@this.provision.code[0].coding[0].display}} 29 | {{/if}} 30 | {{if (@this.description && @this.description.text)}} 31 | {{@this.description.text}} 32 | {{/if}} 33 |
  • 34 | {{/each}} 35 |
36 |
37 |
-------------------------------------------------------------------------------- /static/ips/templates-waverify/AdvanceDirectives.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | Advance Directives 7 |
8 |
9 |
    10 | {{each(options.ad)}} 11 |
  • 12 | Type: {{@this.resourceType}} 13 |
    14 | Text: 15 | {{if (@this.text && @this.text.div)}} 16 | {{@this.text.div}} 17 | {{#else}} 18 | No text provided in resource 19 | {{/if}} 20 |
    21 | Category: 22 | {{if (@this.category && @this.category[0] && @this.category[0].coding && @this.category[0].coding[0])}} 23 | {{@this.category[0].coding[0].display}} 24 | {{/if}} 25 |
    26 | Intent: 27 | {{if (@this.provision && @this.provision.code && @this.provision.code[0] && @this.provision.code[0].coding && @this.provision.code[0].coding[0]) }} 28 | {{@this.provision.code[0].coding[0].display}} 29 | {{/if}} 30 | {{if (@this.description && @this.description.text)}} 31 | {{@this.description.text}} 32 | {{/if}} 33 |
  • 34 | {{/each}} 35 |
36 |
37 |
-------------------------------------------------------------------------------- /static/ips/assets/html-waverify/header.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 | Washington State Department of Health Logo 8 |
9 | 10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | 26 |
27 |
28 |
29 |

WA Verify+ International Patient Summary Viewer

30 |
31 | -------------------------------------------------------------------------------- /static/ips/assets/html/header.html: -------------------------------------------------------------------------------- 1 |

International Patient Summary (IPS) Viewer for Connectathon

2 |
3 | Links to published Implementation Guide and the latest CI build 5 |
6 |
7 | Please note that this tool is an open-source project under development. It only renders the following sections of 8 | IPS bundles: Advance Directives, Allergies, Immunizations, Medications, Problems and Results. Rendering of FHIR resources 9 | may be incomplete and using the narrative may be necessary in some circumstances. 10 |
11 |
12 |
13 |
14 |
15 |

Submit Data

16 |
17 |
18 |
19 |
20 | 21 |
22 | 23 |
24 |
25 |
26 |
27 |
28 | 29 |
30 | 34 |
35 | 36 |
37 |
38 | 39 |
40 |
41 |
42 |
43 | 44 |
45 |
46 |
47 |
-------------------------------------------------------------------------------- /static/ips/assets/css/custom.css: -------------------------------------------------------------------------------- 1 | .card { 2 | margin-bottom: 20px; 3 | } 4 | 5 | .icon-action { 6 | margin-top: 5px; 7 | float: right; 8 | font-size: 80%; 9 | } 10 | 11 | .list-group-item .title { 12 | margin-top: 5px; 13 | margin-bottom: 12px; 14 | font-weight: 600; 15 | } 16 | 17 | .textBody table { 18 | table-layout: fixed; 19 | width: 100%; 20 | 21 | } 22 | 23 | .buttonSpacer { 24 | padding-left: 20px; 25 | padding-top: 20px; 26 | } 27 | 28 | .tdLeft { 29 | text-align: left; 30 | } 31 | 32 | .tdCenter { 33 | text-align: center; 34 | } 35 | 36 | .message { 37 | color: red; 38 | font-size: 1.0rem; 39 | } 40 | 41 | .loader { 42 | width: 100%; 43 | height: 150px; 44 | margin: 40px; 45 | display: block; 46 | position: relative; 47 | background: #FFF; 48 | box-sizing: border-box; 49 | } 50 | .loader::after { 51 | content: ''; 52 | width: calc(100% - 30px); 53 | height: calc(100% - 30px); 54 | top: 15px; 55 | left: 15px; 56 | position: absolute; 57 | background-image: linear-gradient(100deg, transparent, rgba(255, 255, 255, 0.5) 50%, transparent 80%), 58 | linear-gradient(#DDD 56px, transparent 0), /* box 1 */ 59 | linear-gradient(#DDD 24px, transparent 0), /* box 2 */ 60 | linear-gradient(#DDD 18px, transparent 0), /* box 3 */ 61 | linear-gradient(#DDD 66px, transparent 0); /* box 4 */ 62 | background-repeat: no-repeat; 63 | background-size: 75px 130px, /* wave */ 64 | 55px 56px, /* box 1 */ 65 | 160px 30px, /* box 2 */ 66 | 220px 20px, /* box 3 */ 67 | 290px 56px; /* box 4 */ 68 | background-position: 0% 0, /* box 1 */ 69 | 0px 0px, /* box 1 */ 70 | 70px 5px, /* box 1 */ 71 | 70px 38px, /* box 1 */ 72 | 0px 66px; /* box 1 */ 73 | box-sizing: border-box; 74 | animation: animloader 1s linear infinite; 75 | } 76 | 77 | @keyframes animloader { 78 | 0% { 79 | background-position: 0% 0, 0 0, 70px 5px, 70px 38px, 0px 66px; 80 | } 81 | 100% { 82 | background-position: 150% 0, 0 0, 70px 5px, 70px 38px, 0px 66px; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/routes/home/+page.svelte: -------------------------------------------------------------------------------- 1 | 30 | 31 |
32 |

WA Verify+ International Patient Summary

33 | 34 |

Welcome to a demonstration of WA State’s ability to allow individuals in WA State to share their health data, including state immunization records. 35 |

36 | 37 |

The WA Verify+ system builds on the WA Verify vaccination verification system to allow people to access and share their own health data, stored for them by the Washington State Department of Health, including immunizations, advance directives, and other data specified by the International Patient Summary. This data may be helpful to those traveling away from home, to parents and caregivers, and to anyone who wants to be able to see their own records, or securely share their data with healthcare providers or others of their choosing. 38 |

39 | 40 |

WA Verify+ uses the secure, patient-controlled SMART Health Link standard. If you would like to share your records, you may use either the electronic or a printed version of the QR Code you’ll get from the system. We recommend waiting 3-7 days for any new immunization to show up in the State system, and therefore be available in this International Patient Summary.

41 | 42 |

You can start by entering a name, date of birth and cell phone number to generate a QR Code to access your records. You will receive a code on your cell phone which you will need to enter each time you sign in.

43 |
44 |
45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /static/ips/templates/Observations.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | Observations (Results) 7 |
8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {{each(options.observations)}} 19 | 20 | 29 | 34 | 46 | {{if(options.observations[@index].category && options.observations[@index].category[0] && options.observations[@index].category[0].coding && options.observations[@index].category[0].coding[0])}} 47 | 50 | {{/if}} 51 | 52 | {{/each}} 53 |
NameDateValueCategory
21 | {{if(options.observations[@index].code && options.observations[@index].code.coding)}} 22 | {{@this.code.coding[0].display}} 23 | ({{@this.code.coding[0].code}}) 24 | {{/if}} 25 | {{if(options.observations[@index].code.text)}} 26 | [Uncoded text shown]: {{@this.code.text}} 27 | {{/if}} 28 | 30 | {{if(options.observations[@index].effectiveDateTime)}} 31 | {{@this.effectiveDateTime}} 32 | {{/if}} 33 | 35 | {{if(options.observations[@index].valueCodeableConcept)}} 36 | {{@this.valueCodeableConcept.coding[0].display}} 37 | {{/if}} 38 | {{if(options.observations[@index].valueQuantity)}} 39 | {{@this.valueQuantity.value}} 40 | {{@this.valueQuantity.unit}} 41 | {{/if}} 42 | {{if(options.observations[@index].valueString)}} 43 | {{@this.valueString}} 44 | {{/if}} 45 | 48 | {{@this.category[0].coding[0].code}} 49 |
54 |
55 |
-------------------------------------------------------------------------------- /static/ips/templates-waverify/Observations.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | Observations (Results) 7 |
8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {{each(options.observations)}} 19 | 20 | 29 | 34 | 46 | {{if(options.observations[@index].category && options.observations[@index].category[0] && options.observations[@index].category[0].coding && options.observations[@index].category[0].coding[0])}} 47 | 50 | {{/if}} 51 | 52 | {{/each}} 53 |
NameDateValueCategory
21 | {{if(options.observations[@index].code && options.observations[@index].code.coding)}} 22 | {{@this.code.coding[0].display}} 23 | ({{@this.code.coding[0].code}}) 24 | {{/if}} 25 | {{if(options.observations[@index].code.text)}} 26 | [Uncoded text shown]: {{@this.code.text}} 27 | {{/if}} 28 | 30 | {{if(options.observations[@index].effectiveDateTime)}} 31 | {{@this.effectiveDateTime}} 32 | {{/if}} 33 | 35 | {{if(options.observations[@index].valueCodeableConcept)}} 36 | {{@this.valueCodeableConcept.coding[0].display}} 37 | {{/if}} 38 | {{if(options.observations[@index].valueQuantity)}} 39 | {{@this.valueQuantity.value}} 40 | {{@this.valueQuantity.unit}} 41 | {{/if}} 42 | {{if(options.observations[@index].valueString)}} 43 | {{@this.valueString}} 44 | {{/if}} 45 | 48 | {{@this.category[0].coding[0].code}} 49 |
54 |
55 |
-------------------------------------------------------------------------------- /src/lib/managementClient.ts: -------------------------------------------------------------------------------- 1 | import { randomStringWithEntropy, base64url } from './util'; 2 | import { API_BASE, VIEWER_BASE } from './config'; 3 | import * as jose from 'jose'; 4 | 5 | type ConfigForServer = Pick; 6 | 7 | export interface SHLAdminParams { 8 | id: string; 9 | managementToken: string; 10 | encryptionKey: string; 11 | files: { 12 | contentEncrypted: string; 13 | contentType: string; 14 | }[]; 15 | passcode?: string; 16 | exp?: number; 17 | label?: string; 18 | v?: number; 19 | } 20 | 21 | export class SHLClient { 22 | async toLink(shl: SHLAdminParams): Promise { 23 | const shlinkJsonPayload = { 24 | url: `${API_BASE}/shl/${shl.id}`, 25 | exp: shl.exp || undefined, 26 | flag: shl.passcode ? 'P' : '', 27 | key: shl.encryptionKey, 28 | label: shl.label 29 | }; 30 | 31 | const encodedPayload: string = base64url.encode(JSON.stringify(shlinkJsonPayload)); 32 | const shlinkBare = VIEWER_BASE + `shlink:/` + encodedPayload; 33 | return shlinkBare; 34 | } 35 | 36 | async createShl(config: ConfigForServer = {}): Promise { 37 | const ek = randomStringWithEntropy(); 38 | const create = await fetch(`${API_BASE}/shl`, { 39 | method: 'POST', 40 | headers: { 41 | 'content-type': 'application/json' 42 | }, 43 | body: JSON.stringify(config) 44 | }); 45 | const { id, managementToken } = await create.json(); 46 | return { 47 | id, 48 | managementToken, 49 | encryptionKey: ek, 50 | files: [], 51 | ...config 52 | }; 53 | } 54 | 55 | async deleteShl(shl: SHLAdminParams): Promise { 56 | const req = await fetch(`${API_BASE}/shl/${shl.id}`, { 57 | method: 'DELETE', 58 | headers: { 59 | authorization: `Bearer ${shl.managementToken}` 60 | } 61 | }); 62 | const res = await req.json(); 63 | return true; 64 | } 65 | 66 | async resetShl(shl: SHLAdminParams): Promise { 67 | const req = await fetch(`${API_BASE}/shl/${shl.id}`, { 68 | method: 'PUT', 69 | body: JSON.stringify({ passcode: shl.passcode, exp: shl.exp }), 70 | headers: { 71 | authorization: `Bearer ${shl.managementToken}` 72 | } 73 | }); 74 | const res = await req.json(); 75 | return true; 76 | } 77 | 78 | async addFile( 79 | shl: SHLAdminParams, 80 | content: unknown, 81 | contentType: string 82 | ): Promise { 83 | let contentEncrypted = await new jose.CompactEncrypt( 84 | new TextEncoder().encode(JSON.stringify(content)) 85 | ) 86 | .setProtectedHeader({ 87 | alg: 'dir', 88 | enc: 'A256GCM' 89 | }) 90 | .encrypt(jose.base64url.decode(shl.encryptionKey)); 91 | 92 | new TextEncoder().encode(contentEncrypted), shl.files.push({ contentEncrypted, contentType }); 93 | const add = await fetch(`${API_BASE}/shl/${shl.id}/file`, { 94 | method: 'POST', 95 | headers: { 96 | 'content-type': contentType, 97 | authorization: `Bearer ${shl.managementToken}` 98 | }, 99 | body: contentEncrypted 100 | }); 101 | return shl; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/lib/AddFile.svelte: -------------------------------------------------------------------------------- 1 | 90 | 91 |
fetchIps()}> 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 119 |
120 | 121 | 123 | -------------------------------------------------------------------------------- /static/ips/templates/Medications.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | Current Medications 7 |
8 |
9 |
    10 | {{each(options.medications)}} 11 |
  • 12 | {{@this.statement.resourceType}} 13 |
    14 | {{if(options.medications[@index].medication.code && options.medications[@index].medication.code.coding && options.medications[@index].medication.code.coding.length)}} 15 | {{each(options.medications[@index].medication.code.coding)}} 16 | {{@this.system}} 17 | {{@this.display}} 18 | {{if (@this.code)}} 19 | ({{@this.code}}) 20 | {{ /if }} 21 |
    22 | {{/each}} 23 | {{#else}} 24 | (Uncoded {{@this.medication.code.text}})
    25 | {{/if}} 26 | {{if(options.medications[@index].medication.ingredient && options.medications[@index].medication.ingredient.itemCodeableConcept)}} 27 | {{each(options.medications[@index].medication.ingredient)}} 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
    Composition
    IngredientStrength Numerator QtyUnitStrength Denominator QtyStrength Denominator Unit
    {{@this.itemCodeableConcept.coding[0].display}}{{@this.strength.numerator.value}}{{@this.strength.numerator.unit}}{{@this.strength.denominator.value}}{{@this.strength.denominator.unit}}
    47 | {{/each}} 48 | {{/if}} 49 | {{if(options.medications[@index].statement.dosage && options.medications[@index].statement.dosage[0].route && options.medications[@index].statement.dosage[0].route.coding && options.medications[@index].statement.dosage[0].doseAndRate)}} 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | {{if(options.medications[@index].statement.dosage[0].timing && options.medications[@index].statement.dosage[0].timing.repeat)}} 66 | 67 | 68 | {{/if}} 69 | 70 |
    Dosage
    RouteQtyUnitFreq. QtyFreq. Period
    {{@this.statement.dosage[0].route.coding[0].display}}{{@this.statement.dosage[0].doseAndRate[0].doseQuantity.value}}{{@this.statement.dosage[0].doseAndRate[0].doseQuantity.unit}}{{@this.statement.dosage[0].timing.repeat.count}}{{@this.statement.dosage[0].timing.repeat.periodUnit}}
    71 |
  • 72 | {{/if}} 73 | 74 | {{/each}} 75 |
76 |
77 |
-------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 47 | 48 | 49 | 50 | 51 | 52 | Washington State Department of Health Logo 53 | 54 | (isOpen = !isOpen)} /> 55 | 56 | 82 | 83 | 84 | 85 | 86 | WA Verify Logo 87 |
International Patient Summary
88 | 89 |
90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 112 | 113 | 114 |
115 | 116 | 139 | -------------------------------------------------------------------------------- /static/ips/templates-waverify/Medications.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | Current Medications 7 |
8 |
9 |
    10 | {{each(options.medications)}} 11 |
  • 12 | 13 | 14 | {{if(options.medications[@index].medication.code && options.medications[@index].medication.code.coding && options.medications[@index].medication.code.coding.length)}} 15 | {{each(options.medications[@index].medication.code.coding)}} 16 | {{@this.system}} 17 |
    18 | {{@this.display}} 19 | {{if (@this.code)}} 20 | ({{@this.code}}) 21 | {{ /if }} 22 |
    23 | {{/each}} 24 | {{#else}} 25 | uncoded 26 |
    27 | {{if(@this.medication.code && @this.medication.code.text)}} 28 | {{@this.medication.code.text}} 29 | {{#else}} 30 | {{@this.statement.medicationReference.display}} 31 | {{/if}} 32 |
    33 | {{/if}} 34 | {{if(options.medications[@index].medication.ingredient && options.medications[@index].medication.ingredient.itemCodeableConcept)}} 35 | {{each(options.medications[@index].medication.ingredient)}} 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 |
    Composition
    IngredientStrength Numerator QtyUnitStrength Denominator QtyStrength Denominator Unit
    {{@this.itemCodeableConcept.coding[0].display}}{{@this.strength.numerator.value}}{{@this.strength.numerator.unit}}{{@this.strength.denominator.value}}{{@this.strength.denominator.unit}}
    55 | {{/each}} 56 | {{/if}} 57 | {{if(options.medications[@index].statement.dosage && options.medications[@index].statement.dosage[0].route && options.medications[@index].statement.dosage[0].route.coding && options.medications[@index].statement.dosage[0].doseAndRate)}} 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | {{if(options.medications[@index].statement.dosage[0].timing && options.medications[@index].statement.dosage[0].timing.repeat)}} 74 | 75 | 76 | {{/if}} 77 | 78 |
    Dosage
    RouteQtyUnitFreq. QtyFreq. Period
    {{@this.statement.dosage[0].route.coding[0].display}}{{@this.statement.dosage[0].doseAndRate[0].doseQuantity.value}}{{@this.statement.dosage[0].doseAndRate[0].doseQuantity.unit}}{{@this.statement.dosage[0].timing.repeat.count}}{{@this.statement.dosage[0].timing.repeat.periodUnit}}
    79 |
  • 80 | {{/if}} 81 | 82 | {{/each}} 83 |
84 |
85 |
-------------------------------------------------------------------------------- /static/img/smart-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /static/ips/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 13 | 14 | 15 | 16 | 19 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | IPS Viewer 31 | 32 | 33 | 34 | 102 | 103 | 104 | 105 |
106 | 108 |
109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /src/lib/HealthLink.svelte: -------------------------------------------------------------------------------- 1 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | {shl.label} 76 | 77 | 78 | {#if shl.exp} 79 | Expires: {new Date(shl.exp * 1000).toISOString().slice(0, 10)} 81 | 82 | {/if} 83 | 84 | 85 | {#await qrCode then dataUrl} 86 | 90 | {/await} 91 | 92 | 93 | 94 | {#if canShare} 95 | 102 | {/if} 103 | 111 | {#await href then href} 112 | 115 | {/await} 116 | 117 | 118 | 119 | 120 | 121 | 122 | 129 | 139 | 140 | 141 | 142 | 149 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 200 | -------------------------------------------------------------------------------- /static/ips/assets/js/renderIPS.js: -------------------------------------------------------------------------------- 1 | // check variable to see if header, which includes data loading active 2 | let headerLength = 0; 3 | 4 | import config from "./config.js"; 5 | 6 | export { update }; 7 | 8 | let sectionCount = 0; 9 | 10 | // Set the mode for the default mode for data presentation 11 | // Entries = machine readable FHIR resources, Narrative = Compostion.section.text.div 12 | let mode = "Entries"; 13 | 14 | // Sqrl setting. See https://v7--squirrellyjs.netlify.app/docs/v7/auto-escaping 15 | Sqrl.autoEscaping(false); 16 | 17 | // Load header and footer on document ready and attach button actions 18 | $(document).ready(function () { 19 | headerLength = $("#header").length; 20 | let footerLength = $("#footer").length; 21 | if (headerLength === 1) { 22 | $("#header").load(config.html_dir + "header.html", function () { 23 | $("#submit").click(function () { 24 | updateFromText(); 25 | }); 26 | $('#clearSample').click(function () { 27 | clearData(); 28 | }) 29 | $("#loadSample").click(function () { 30 | $.getJSON('./samples/sample.json', function () { 31 | console.log("success"); 32 | }) 33 | .done(function (data) { 34 | $('#ipsInput').val(JSON.stringify(data)); 35 | update(data); 36 | }) 37 | .fail(function (e) { 38 | console.log("error", e); 39 | }); 40 | }); 41 | }); 42 | } 43 | $('#FhirDropdown').attr('href', "javascript:mode='Entries'; $('#mode').html('Displaying FHIR Entries'); updateFromText();"); 44 | $('#NarrativeDropdown').attr('href', "javascript:mode='Text'; $('#mode').html('Displaying Narrative'); updateFromText();"); 45 | if (footerLength === 1) { 46 | $("#footer").load(config.html_dir + "footer.html", function () { 47 | // no actions on footer currently 48 | }); 49 | } 50 | }); 51 | 52 | $(window).on('load', function () { 53 | $("#content").show(); 54 | }); 55 | 56 | // Clear data button function. Should be called on all new data loads 57 | const clearData = function () { 58 | // clear textbox 59 | $("#ipsInput").val(""); 60 | // clear prior message 61 | $("#renderMessage").hide(); 62 | // clear all viewer data and data checks table 63 | $('.data').empty(); 64 | } 65 | 66 | // Update the contents from new JSON pasted in TextBox 67 | const updateFromText = function () { 68 | var ipsTxt = $('#ipsInput').val(); 69 | if (ipsTxt) { 70 | try { 71 | var ips = JSON.parse(ipsTxt); 72 | update(ips); 73 | } catch (e) { 74 | console.log(e); 75 | alert("Invalid IPS - " + e); 76 | } 77 | } 78 | else { 79 | alert('Invalid content - Enter IPS Bundle (JSON) in "Paste Your IPS Here" box'); 80 | } 81 | }; 82 | 83 | // Update the data in viewer based on mode and data 84 | const render = function (templateName, data, targetLocation) { 85 | let entryCheck = 0; 86 | sectionCount++; 87 | if (templateName === 'Patient') { 88 | if (!data.custodian) data.custodian = {}; 89 | if (!data.custodian.name) data.custodian.name = '[NOT FOUND]'; 90 | if (!data.custodian.address || !data.custodian.address[0]) { 91 | data.custodian.address = [{ city: '', country: '' }]; 92 | } 93 | entryCheck = 1; 94 | } 95 | else if (data.entry) { 96 | entryCheck = data.entry.length 97 | } 98 | if (mode == "Entries" && templateName !== "Other") { 99 | var jqxhr = $.get(config.template_dir + templateName + ".html", function () { }) 100 | .done(function (template) { 101 | // console.log(template); 102 | console.log(data); 103 | var templateResult = Sqrl.Render(template, data); 104 | $("#" + targetLocation).html(templateResult); 105 | }).fail(function (e) { 106 | console.log("error", e); 107 | }); 108 | } 109 | else { 110 | // if the mode was intended as Entries and narrative fallback used, display message 111 | if (mode === "Entries") $("#renderMessage").attr("style", "display:inline"); 112 | else $("#renderMessage").hide(); 113 | var content = { titulo: data.title, div: "No text defined.", index: sectionCount }; 114 | if (!content.titulo) content.titulo = data.resourceType; 115 | if (data.text) content.div = data.text.div; 116 | var jqxhr = $.get(config.template_dir + "Text.html", function () { }) 117 | .done(function (template) { 118 | var templateResult = Sqrl.Render(template, content); 119 | $("#" + targetLocation).html(templateResult); 120 | }).fail(function (e) { 121 | console.log("error", e); 122 | }); 123 | } 124 | }; 125 | 126 | // This is the header table for some basic data checks 127 | const renderTable = function (data) { 128 | let jqxhr = $.get(config.template_dir + "Checks.html", function () { }) 129 | .done(function (template) { 130 | $("#ips-loader").hide(); 131 | let templateResult = Sqrl.Render(template, data); 132 | console.log(data); 133 | $("#checksTable").html(templateResult); 134 | }); 135 | } 136 | 137 | // For machine-readable content, use the reference in the Composition.section.entry to retrieve resource from Bundle 138 | const getEntry = function (ips, fullUrl) { 139 | var result; 140 | ips.entry.forEach(function (entry) { 141 | if (entry.fullUrl.includes(fullUrl)) { 142 | console.log(`match ${fullUrl}`); 143 | result = entry.resource; 144 | } 145 | // Attempt to match based on resource and uuid 146 | else { 147 | let newMatch = fullUrl 148 | if (entry.resource && entry.resource.resourceType) { 149 | // remove the resource from reference 150 | newMatch = newMatch.replace(entry.resource.resourceType, ''); 151 | // remove slash 152 | newMatch = newMatch.replace(/\//g, ''); 153 | // console.log(newMatch); 154 | } 155 | if (entry.fullUrl.includes(newMatch)) { 156 | console.log(`match uuid ${newMatch}`); 157 | result = entry.resource; 158 | } 159 | } 160 | }); 161 | if (!result) { 162 | console.log(`missing reference ${fullUrl}`); 163 | result = {}; 164 | } 165 | return result; 166 | }; 167 | 168 | // Primary function to traverse the Bundle and get data 169 | // Calls the render function to display contents 170 | const update = function (ips) { 171 | sectionCount = 0; 172 | $(".output").html(""); 173 | $("#renderMessage").hide(); 174 | ips.entry.forEach(function (entry) { 175 | if (!entry.resource) console.log(entry); 176 | if (entry.resource.resourceType == "Composition") { 177 | var composition = entry.resource; 178 | let patient = {}; 179 | if (composition.custodian && composition.custodian.reference) { 180 | console.log(composition.custodian.reference); 181 | composition.custodian = getEntry(ips, composition.custodian.reference); 182 | } 183 | else { 184 | console.log('no custodian reference'); 185 | composition.custodian = {}; 186 | } 187 | if (composition.subject && composition.subject.reference) { 188 | console.log(composition.subject.reference); 189 | patient = getEntry(ips, composition.subject.reference); 190 | } 191 | else console.log('no subject reference'); 192 | render("Composition", composition, "Composition"); 193 | console.log('Patient Card'); 194 | if (patient) { 195 | console.log(patient) 196 | render("Patient", patient, "Patient"); 197 | } 198 | let alertMissingComposition = false; 199 | composition.section.forEach(function (section) { 200 | if (!section || !section.code || !section.code.coding || !section.code.coding[0]) { 201 | alertMissingComposition = true; 202 | console.log('Section is missing coding information'); 203 | } 204 | else if (section.code.coding[0].code == "11450-4") { 205 | console.log('Problems Section'); 206 | section.problems = []; 207 | section.entry.forEach(function (problem) { 208 | console.log(problem.reference) 209 | section.problems.push(getEntry(ips, problem.reference)); 210 | }); 211 | render("Problems", section, "Problems"); 212 | } 213 | 214 | else if (section.code.coding[0].code == "48765-2") { 215 | console.log('Allergies Section'); 216 | section.allergies = []; 217 | section.entry.forEach(function (allergy) { 218 | console.log(allergy.reference) 219 | let allergy2 = getEntry(ips, allergy.reference); 220 | if (!allergy2.category) allergy2.category = [' ']; 221 | if (!allergy2.type) allergy2.type = ' '; 222 | section.allergies.push(allergy2); 223 | }); 224 | render("Allergies", section, "Allergies"); 225 | } 226 | 227 | else if (section.code.coding[0].code == "10160-0") { 228 | console.log('Medications Section'); 229 | section.medications = []; 230 | section.entry.forEach(function (medication) { 231 | console.log(medication.reference); 232 | // while variable name is Statement, this may be either MedicationStatement or MedicationRequest 233 | let statement = getEntry(ips, medication.reference); 234 | let medicationReference; 235 | // Either MedicationRequest or MedicationStatement may have a reference to Medication 236 | if (statement.medicationReference && statement.medicationReference.reference) { 237 | medicationReference = getEntry(ips, statement.medicationReference.reference); 238 | 239 | } else if (statement.medicationCodeableConcept) { 240 | medicationReference = { code: statement.medicationCodeableConcept }; 241 | } else { 242 | medicationReference = {code: { coding: [ { system: '', display: '', code: '' } ] } }; 243 | } 244 | // MedicationStatement has dosage while MedicationRequest has dosageInstruction. Use alias to simplify template 245 | if (statement.dosageInstruction) statement.dosage = statement.dosageInstruction; 246 | section.medications.push({ 247 | statement: statement, 248 | medication: medicationReference 249 | }); 250 | }); 251 | render("Medications", section, "Medications"); 252 | } 253 | else if (section.code.coding[0].code == "11369-6") { 254 | console.log('Immunizations Section'); 255 | section.immunizations = []; 256 | section.entry.forEach(function (immunization) { 257 | console.log(immunization.reference); 258 | section.immunizations.push(getEntry(ips, immunization.reference)); 259 | }); 260 | render("Immunizations", section, "Immunizations"); 261 | } 262 | else if (section.code.coding[0].code == "30954-2") { 263 | console.log('Observations Section'); 264 | section.observations = []; 265 | section.entry.forEach(function (observation) { 266 | console.log(observation.reference); 267 | section.observations.push(getEntry(ips, observation.reference)); 268 | }); 269 | render("Observations", section, "Observations"); 270 | } 271 | else if (section.code.coding[0].code == "42348-3") { 272 | console.log('Advance Directives Section'); 273 | section.ad = []; 274 | section.entry.forEach(function (ad) { 275 | console.log(ad.reference); 276 | section.ad.push(getEntry(ips, ad.reference)); 277 | }); 278 | render("AdvanceDirectives", section, "AdvanceDirectives"); 279 | } 280 | else { 281 | render("Other", section, "Other"); 282 | console.log(`Section with code: ${section.code.coding[0].code} not rendered since no template`); 283 | } 284 | }); 285 | if (alertMissingComposition) alert('Missing coding information in Composition resource. Rendering may be incomplete.') 286 | } 287 | }); 288 | //don't need to do anything if the header is not shown 289 | if (headerLength === 1) { 290 | checks(ips) 291 | } 292 | }; 293 | 294 | // Updates the header data for simple data checks. Note that this is NOT full FHIR validation 295 | const checks = function (ips) { 296 | let composition = ips.entry[0]; 297 | let data = { 298 | data: [], 299 | errors: [] 300 | }; 301 | if (composition.resource.resourceType === "Composition" && composition.resource.section) { 302 | let sections = { 303 | allergies: false, 304 | medications: false, 305 | problems: false 306 | }; 307 | for (let i = 0; i < composition.resource.section.length; i++) { 308 | let section = composition.resource.section[i] 309 | let newData = {}; 310 | newData.display = section.title; 311 | if (section.code.coding[0].code == "48765-2") sections.allergies = true; 312 | if (section.code.coding[0].code == "10160-0") sections.medications = true; 313 | if (section.code.coding[0].code == "11450-4") sections.problems = true; 314 | if (section.entry) { 315 | newData.entries = section.entry.length; 316 | newData.entriesColor = "green"; 317 | } 318 | else { 319 | newData.entries = 0; 320 | newData.entriesColor = "red"; 321 | } 322 | if (section.text && section.text.div) { 323 | newData.narrative = "✓" 324 | newData.narrativeColor = "green"; 325 | } 326 | else { 327 | newData.narrative = "✗" 328 | newData.narrativeColor = "red"; 329 | } 330 | data.data.push(newData); 331 | } 332 | if (!sections.allergies) data.errors.push("Missing required allergies section"); 333 | if (!sections.medications) data.errors.push("Missing required medications section"); 334 | if (!sections.problems) data.errors.push("Missing required problems section"); 335 | } 336 | renderTable(data); 337 | } --------------------------------------------------------------------------------