├── .github └── workflows │ ├── run-tests.yml │ └── test-and-deploy.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE.txt ├── README.md ├── package-lock.json ├── package.json ├── public ├── app.css ├── bootstrap.min.css ├── callback.html ├── config │ ├── bluebutton_endpoints.json │ ├── config-override-demo.json │ ├── config.json │ ├── epic_endpoints_r4.json │ └── sandbox_endpoints.json ├── favicon.ico ├── index.html └── manifest.json ├── scripts └── get_epic_endpoints.sh ├── src ├── components │ ├── App.js │ ├── BlankSlate.js │ ├── Fetcher.js │ ├── FhirTree.js │ ├── FileUploader.js │ ├── GithubUploader.js │ ├── Header.js │ ├── Loader.js │ ├── ProviderForm.js │ ├── ProviderList.js │ ├── SettingsImportDropZone.js │ ├── TimeAgo.js │ ├── Toolbar.js │ └── Wizard │ │ ├── DownloadButton.js │ │ ├── Retrieve.js │ │ ├── Review.js │ │ ├── SelectProvider.js │ │ ├── Start.js │ │ ├── Upload.js │ │ └── index.js ├── index.js ├── schemas │ ├── config-schema.json │ ├── endpoint-list-schema.json │ ├── samples │ │ └── user-settings.json │ ├── upload-manifest-schema.json │ └── user-settings-schema.json ├── serviceWorker.js ├── smart │ ├── fhir.js │ ├── smart-core.js │ └── smart-popup.js ├── store │ ├── config-loader.js │ ├── csv-writer.js │ ├── fhir-loader.js │ ├── file-exporter.js │ ├── github-api.js │ ├── github-exporter.js │ ├── initial-state-dev.js │ ├── initial-state.js │ ├── merge-objects.js │ ├── spreadsheet-exporter.js │ ├── store.js │ ├── user-settings.js │ └── zip-exporter.js ├── sw-build.js ├── sw.js └── test │ ├── App.test.js │ ├── data │ ├── document1.js │ ├── document_reference.json │ ├── duck.js │ ├── josh │ │ ├── unity-point │ │ │ ├── allergy-intolerance.json │ │ │ ├── condition.json │ │ │ ├── document-reference-TksSRPcka4BoYduTTHg-k0jPC5rVuPExcfPtz4UQS-3uCXiMVAQgj2SsyEWKvUbPZ.jSROON4EThqtJZa5Az4PJuqIqOe6xg7cyGjo7YoRxEB.xml │ │ │ ├── document-reference-Tr-wsqP3I4ItgWRr1UtQWZHlE7rlv7nbeuTFnUqoLY4.wE7nA4Ugt7PuvMvct9ydxMXubL7BSwkb00UgR1HewAiIlpSl3AHRtkFgkT7XRf.IB.xml │ │ │ ├── document-reference.json │ │ │ ├── immunization.json │ │ │ ├── laboratory.json │ │ │ ├── medication-order.json │ │ │ ├── medication-statement.json │ │ │ ├── patient.json │ │ │ ├── procedure.json │ │ │ ├── social-history.json │ │ │ └── vital-signs.json │ │ └── uw │ │ │ ├── allergy-intolerance.json │ │ │ ├── condition.json │ │ │ ├── document-reference-TLFF7UrfbsbX.Of.6p86Rt6KAX0tYwTfligqU-QjdS78CAZHlHdbEzFy1YQvLdgFU2MvF6KOhqpGv.IGaD4leQBLPT55WRrZsm4pXroi.w2kB.xml │ │ │ ├── document-reference-TOIpsPdnnD8RBIcWAoT9Vmynd.xVLCSnQF7yCE56mN5hWinmeCT0v-6C1EpEhHsy9injJ361KLSVBvx6A2AIIRIeOsUTWWPx9yZN8SaobGnMB.xml │ │ │ ├── document-reference-TVmPYRkn9A.cSDZwweE61.Zpt7R6f6ekSxQqSl.qWZffg.pEYDV5OJRCBvvyp1iczssSv9tk9Uu518PR.kPn9ne.NNoNM.g4KBj1-BwCa6AQB.xml │ │ │ ├── document-reference-TcoDtNRyERbHqp4AjTVQYeoAzJOflQgYLHQOzlLZ0k1WyM0GwXqRw.mQdpsitgmVgm3fAMcsjfFJlb0Cix6AQsdVH5IYv.9g4vhAm-Aqf5jEB.xml │ │ │ ├── document-reference.json │ │ │ ├── immunization.json │ │ │ ├── laboratory.json │ │ │ ├── medication-order.json │ │ │ ├── medication-statement.json │ │ │ ├── patient.json │ │ │ ├── procedure.json │ │ │ ├── social-history.json │ │ │ └── vital-signs.json │ ├── metadata.json │ ├── observation-cerner.json │ ├── observation-epic.json │ ├── observation1.json │ ├── observation2.json │ ├── operation_outcome_404.json │ ├── patient.json │ ├── procedure.json │ └── smart-configuration.json │ ├── fhir.test.js │ ├── merge-objects.test.js │ ├── smart-core.test.js │ ├── spreadsheet.test.js │ └── util │ └── base64toblob.js └── upload-backends ├── node-server-aws ├── index.js ├── package-lock.json └── package.json └── node-server ├── index.js ├── package-lock.json └── package.json /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - master 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [18.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v1 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - name: npm install, build, and test 24 | run: | 25 | npm ci 26 | npm run build 27 | npm run test 28 | env: 29 | CI: true 30 | -------------------------------------------------------------------------------- /.github/workflows/test-and-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Test and Deploy Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [18.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - name: npm install, build, and test 24 | run: | 25 | npm ci 26 | npm run build 27 | npm run test 28 | env: 29 | CI: true 30 | 31 | build-deploy: 32 | runs-on: ubuntu-latest 33 | needs: test 34 | steps: 35 | - uses: actions/checkout@v2 36 | 37 | - name: Build 38 | run: npm install && npm run-script build 39 | 40 | - run: mv $GITHUB_WORKSPACE/build/config/config-override-demo.json $GITHUB_WORKSPACE/build/config/config-override.json 41 | shell: bash 42 | 43 | - name: Deploy 44 | uses: peaceiris/actions-gh-pages@v4 45 | with: 46 | github_token: ${{ secrets.GITHUB_TOKEN }} 47 | publish_dir: ./build 48 | cname: procure.syncfor.science -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | uploaded_files 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | notes.txt 28 | config-override.json 29 | config-override-dev.json -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "json.schemas": [ 3 | { 4 | "fileMatch": [ 5 | "config.json" 6 | ], 7 | "url": "./src/schemas/config-schema.json" 8 | }, 9 | { 10 | "fileMatch": [ 11 | "endpoints.json" 12 | ], 13 | "url": "./src/schemas/endpoint-list-schema.json" 14 | }, 15 | { 16 | "fileMatch": [ 17 | "procure-settings.json" 18 | ], 19 | "url": "./src/schemas/user-settings-schema.json" 20 | }, 21 | { 22 | "fileMatch": [ 23 | "upload-manifest.json" 24 | ], 25 | "url": "./src/schemas/upload-manifest-schema.json" 26 | }, 27 | ], 28 | "git.ignoreLimitWarning": true 29 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2020 President and Fellows of Harvard College 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "procure-wip", 3 | "version": "0.1.0", 4 | "private": false, 5 | "homepage": "https://procure.syncfor.science", 6 | "dependencies": { 7 | "@fortawesome/fontawesome-svg-core": "^1.2.19", 8 | "@fortawesome/free-brands-svg-icons": "^5.9.0", 9 | "@fortawesome/free-regular-svg-icons": "^5.9.0", 10 | "@fortawesome/free-solid-svg-icons": "^5.9.0", 11 | "@fortawesome/react-fontawesome": "^0.1.4", 12 | "cross-fetch": "^3.0.1", 13 | "file-saver": "^2.0.1", 14 | "jszip": "^3.2.1", 15 | "lodash": "^4.17.15", 16 | "match-url-wildcard": "0.0.4", 17 | "react": "^16.8.3", 18 | "react-dom": "^16.8.3", 19 | "react-json-tree": "^0.11.2", 20 | "react-select": "^3.0.8", 21 | "reactstrap": "^8.2.0", 22 | "sanitize-filename": "^1.6.1", 23 | "storeon": "^0.8.3", 24 | "strip-json-comments": "^2.0.1", 25 | "timeago.js": "^4.0.0-beta.2", 26 | "tv4": "^1.3.0", 27 | "xlsx-populate": "^1.20.1" 28 | }, 29 | "scripts": { 30 | "start": "react-scripts --openssl-legacy-provider start", 31 | "build-sw": "node ./src/sw-build.js", 32 | "build": "react-scripts --openssl-legacy-provider build && rm -rf build/config/config-override-dev.json && npm run build-sw", 33 | "analyze": "source-map-explorer 'build/static/js/*.js'", 34 | "test": "react-scripts test", 35 | "eject": "react-scripts eject" 36 | }, 37 | "eslintConfig": { 38 | "extends": "react-app" 39 | }, 40 | "browserslist": [ 41 | ">0.2%", 42 | "not dead", 43 | "not ie <= 11", 44 | "not op_mini all" 45 | ], 46 | "devDependencies": { 47 | "jest-fetch-mock": "^2.1.1", 48 | "react-scripts": "3.4.1", 49 | "react-test-renderer": "^16.8.6", 50 | "source-map-explorer": "^2.2.2", 51 | "workbox-build": "^4.3.1" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /public/app.css: -------------------------------------------------------------------------------- 1 | /* change the background color */ 2 | .navbar { 3 | background-color: #3182CE !important; 4 | } 5 | /* change the brand and text color */ 6 | .navbar .navbar-brand, 7 | .navbar .navbar-text { 8 | color: #ffffff !important; 9 | } 10 | /* change the link color */ 11 | .navbar .navbar-nav .nav-link { 12 | color: #ffffff !important; 13 | } 14 | /* change the color of active or hovered links */ 15 | .navbar .nav-item.active .nav-link, 16 | .navbar .nav-item:focus .nav-link, 17 | .navbar .nav-item:hover .nav-link { 18 | color: #ffffff !important; 19 | } 20 | 21 | /* Landscape phones and portrait tablets */ 22 | @media (max-width: 767px) { 23 | .unbox-sm { 24 | -webkit-box-shadow: none !important; 25 | -moz-box-shadow: none !important; 26 | box-shadow: none !important; 27 | } 28 | } 29 | @media (min-width: 768px) { 30 | .bg-light-md { 31 | background: #f8f9fa !important; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /public/callback.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /public/config/bluebutton_endpoints.json: -------------------------------------------------------------------------------- 1 | { 2 | "entry": [{ 3 | "name": "Sandbox - BB - Care Evolution", 4 | "clientId": "2df118d5-7058-4ddc-9fa9-a124fba2858f", 5 | "fhirEndpoint":"https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir-r4", 6 | // CEPatient / CEPatient2018 7 | "id": "care-evolution-sandbox" 8 | },{ 9 | "name": "Sandbox - BB - Medicare", 10 | "clientId": "FKCHfWRH8j1WdkObD9Beb6LSIyJf9gq1EGzYjsKw", 11 | "clientSecret": "MGJGlgkVckpyWjnnLovbm3cZEaMV6t2EkSIzrGiz0su925rJqB6934BYc0mPTsmItPrbjumA9jKnbh0anSNpNUX7WUckUuPVLORyyoJUnbXi13ifKet5lPYyKUAdzuj6", 12 | "fhirEndpoint": "https://sandbox.bluebutton.cms.gov/v1/fhir/", 13 | //BBUser00000, with password PW00000! 14 | "id": "cms-bb-sandbox", 15 | "scope": ["profile", "patient/Patient.read", "patient/ExplanationOfBenefit.read", "patient/Coverage.read"] 16 | }] 17 | } -------------------------------------------------------------------------------- /public/config/config-override-demo.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": "Procure Demo", 3 | "noCustomEndpoints": true, 4 | "upload": { 5 | "simulate": true 6 | , "label": "Share with Test Project" 7 | , "name": "Test Project" 8 | , "uploadUrl":"http://localhost:8000/123-1570639420963.zip" 9 | , "infoUrl":"http://example.com" 10 | , "successMessage":"Thank you - your information has been transmitted!" 11 | , "continueUrl": "http://example.com" 12 | }, 13 | "_endpointLists": { 14 | "sandbox_endpoints": { 15 | "path": "./config/sandbox_endpoints.json", 16 | "defaults": { 17 | "queryProfile": "uscdi", 18 | "scope": ["patient/*.read", "launch/patient"] 19 | } 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /public/config/sandbox_endpoints.json: -------------------------------------------------------------------------------- 1 | { 2 | "entry": [{ 3 | //MyChart Login Username: fhirderrick 4 | //MyChart Login Password: epicepic1 5 | //see full list at: https://fhir.epic.com/Documentation?docId=testpatients 6 | "name": "Sandbox - Epic", 7 | "fhirEndpoint":"https://fhir.epic.com/interconnect-fhir-oauth/api/FHIR/R4/", 8 | "id": "epic-sandbox-secure-r4", 9 | "credentialId": "epic_sandbox_secure_r4", 10 | "scope": ["launch/patient", "patient/*.read"], 11 | "queryProfile": "uscdi_epic" 12 | },{ 13 | //https://launch.smarthealthit.org/?auth_error=&fhir_version_2=r4&iss=&launch_pt=1&launch_url=&patient=&prov_skip_auth=1&provider=&pt_skip_auth=1&pt_skip_login=0&public_key=&sde=&token_lifetime=15&user_pt=75e95f92-3bfa-454f-aa99-99375173f201 14 | "name": "Sandbox - SMART", 15 | "fhirEndpoint": "https://launch.smarthealthit.org/v/r4/sim/eyJrIjoiMSIsImoiOiIxIiwiYiI6Ijc1ZTk1ZjkyLTNiZmEtNDU0Zi1hYTk5LTk5Mzc1MTczZjIwMSJ9/fhir", 16 | "credentialId": "smart_sandbox_secure", 17 | "queryProfile": "uscdi" 18 | },{ 19 | //open issues: can't read practitioner references even with correct scopes 20 | //also, in general, sandbox server token times out on large requests, so can only test a few 21 | //queries at a time. 22 | //username: nancysmart, password: Cerner01 23 | //see full list at: https://docs.google.com/document/d/10RnVyF1etl_17pyCyK96tyhUWRbrTyEcqpwzW-Z-Ybs/edit 24 | "name": "Sandbox - Cerner", 25 | "fhirEndpoint": "https://fhir-myrecord.cerner.com/r4/ec2458f2-1e24-41c8-b71b-0e701af7583d/", 26 | "queryProfile": "uscdi_cerner", 27 | "credentialId": "cerner_sandbox_secure", 28 | "scope": [ 29 | "launch/patient", "profile", "openid", 30 | "user/Binary.read", "user/Practitioner.read", "patient/AllergyIntolerance.read", "patient/Appointment.read", 31 | "patient/Binary.read", "patient/CarePlan.read", "patient/CareTeam.read", "patient/Condition.read", 32 | "patient/Consent.read", "patient/Contract.read", "patient/Coverage.read", "patient/Device.read", 33 | "patient/DiagnosticReport.read", "patient/DocumentReference.read", "patient/Encounter.read", 34 | "patient/FamilyMemberHistory.read", "patient/Goal.read", "patient/Immunization.read", 35 | "patient/InsurancePlan.read", "patient/MedicationAdministration.read", "patient/MedicationDispense.read", 36 | "patient/MedicationOrder.read", "patient/MedicationRequest.read", "patient/MedicationStatement.read", 37 | "patient/NutritionOrder.read", "patient/Observation.read", "patient/Patient.read", "patient/Person.read", 38 | "patient/Procedure.read", "patient/ProcedureRequest.read", "patient/Provenance.read", 39 | "patient/Questionnaire.read", "patient/QuestionnaireResponse.read", "patient/RelatedPerson.read", 40 | "patient/Schedule.read", "patient/ServiceRequest.read", "patient/Slot.read" 41 | ] 42 | // },{ 43 | //login info is tied to user's google account and is restricted by contract, so requires sign up to get a client id 44 | // "name": "Sandbox - Secure - Meditech", 45 | // "fhirEndpoint": "https://greenfield-apis.meditech.com/v1/argonaut/v1/", 46 | // "queryProfile": "argonaut_meditech", 47 | // "scope": ["launch/patient", "openid", "profile", "patient/*.read"], 48 | // "credentialId": "meditech_sandbox_secure" 49 | // },{ 50 | //Getting 504 error when retrieving resources. 51 | //see: https://groups.google.com/forum/#!category-topic/nextgenapiusers/application-questions--issues/wPmbUIaflPM 52 | //username: wwrist, password: wwrist!123 53 | // "name": "Sandbox - Secure - NextGen", 54 | // "fhirEndpoint": "https://apigw-west.nextgen.com/nge/staging/fhirapi/dstu2/v1.0", 55 | // "scope": ["launch/patient", "openid", "profile", "patient/*.read"], 56 | // "credentialId": "nextgen_sandbox_secure", 57 | // "queryProfile": "demographics" 58 | // },{ 59 | //Allscripts does not seem to support CORS see issue: 60 | //https://developer.allscripts.com/Forums#/discussion/2665/cors-error-in-patient-facing-app/p1?new=1 61 | //username: bill.aaron@open.allscripts.com, password: Password#1 62 | // "name": "Sandbox - Secure - Allscripts Prof (FMH)", 63 | // "fhirEndpoint": "https://pro171fmh.open.allscripts.com/open", 64 | // "scope": ["launch/patient", "openid", "profile", "patient/*.read"], 65 | // "credentialId": "allscripts_sandbox_secure", 66 | // "queryProfile": "argonaut_spec" 67 | }] 68 | } 69 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sync-for-science/procure-wip/83884e339c4f81e5cfb1b6c8e2a7beaf6b960d4b/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 17 | 18 | 27 | Loading... 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /scripts/get_epic_endpoints.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | curl https://open.epic.com/Endpoints/R4 -o ../public/config/epic_endpoints_r4.json -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | /* eslint jsx-a11y/anchor-is-valid: 0 */ //disable to support bootstrap link styling 2 | 3 | import React, {useEffect} from "react"; 4 | import useStoreon from "storeon/react"; 5 | import {Alert, Container, Row, Col} from 'reactstrap'; 6 | import _ from "lodash"; 7 | import ProviderForm from "./ProviderForm"; 8 | import ProviderList from "./ProviderList"; 9 | import Fetcher from "./Fetcher"; 10 | import Loader from "./Loader" 11 | import GithubUploader from "./GithubUploader"; 12 | import Header from "./Header"; 13 | import FhirTree from "./FhirTree"; 14 | import Toolbar from "./Toolbar"; 15 | import BlankSlate from "./BlankSlate"; 16 | import FileUploader from "./FileUploader"; 17 | import Wizard from "./Wizard/"; 18 | 19 | const App = () => { 20 | 21 | //global state 22 | const { uiState, showWizard, upload, providers, appName } = useStoreon("uiState", "upload", "showWizard", "providers", "appName"); 23 | 24 | useEffect(() => { 25 | document.title = appName || "Procure"; 26 | }, [appName]); 27 | 28 | if (uiState.mode === "loading") 29 | return ; 30 | 31 | if (uiState.mode === "globalError") 32 | return 33 | 34 | Failed to load configuration information.
35 | {uiState.error.toString()} 36 |
37 |
; 38 | 39 | const inlineError = 40 | {uiState.error} 41 | 42 | 43 | const renderProviderList = () => 44 | 45 | 46 | 47 | const renderData = (hasResources) => 48 | 49 | { hasResources && } 50 | 51 | 52 | const renderBlankSlate = () => 53 | 54 | 55 | 56 | const hasProviders = providers && providers.length; 57 | const hasData = hasProviders && _.find(providers, p => { 58 | return p.selected && p.data && p.data.entry && (p.data.entry.length || p.data.errorLog.length); 59 | }); 60 | const hasResources = hasProviders && _.find(providers, p => { 61 | return p.selected && p.data && p.data.entry && p.data.entry.length 62 | }); 63 | 64 | const fullUi =
65 |
66 | 67 | 68 | { uiState.error && uiState.mode === "ready" && inlineError } 69 | { hasProviders ? renderProviderList() : renderBlankSlate() } 70 | { hasData ? renderData(hasResources) : null } 71 | 72 | { uiState.mode === "editProvider" && } 73 | { uiState.mode === "loadData" && } 74 | { uiState.mode === "githubExport" && } 75 | { uiState.mode === "fileUpload" && } 76 | 77 |
78 | 79 | const wizardUi = showWizard && 80 | upload && (upload.manifestUrl || upload.uploadUrl) && 81 | 82 | { uiState.error && uiState.mode === "ready" && 83 | {inlineError} 84 | } 85 | 86 | 87 | 88 | if (wizardUi) { 89 | document.body.classList.add("bg-light-md"); 90 | return wizardUi; 91 | } else { 92 | document.body.classList = ""; 93 | return fullUi; 94 | } 95 | 96 | } 97 | export default App; -------------------------------------------------------------------------------- /src/components/BlankSlate.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import useStoreon from "storeon/react"; 3 | import {Button} from 'reactstrap'; 4 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" 5 | import { faPlus } from "@fortawesome/free-solid-svg-icons" 6 | 7 | export default () => { 8 | 9 | const { dispatch } = useStoreon(); 10 | 11 | const handleAddProvider = (e) => { 12 | e.preventDefault(); 13 | dispatch("uiState/set", {mode: "editProvider"}); 14 | } 15 | 16 | return 22 | 23 | } -------------------------------------------------------------------------------- /src/components/Fetcher.js: -------------------------------------------------------------------------------- 1 | /* eslint jsx-a11y/anchor-is-valid: 0 */ //disable to support bootstrap link styling 2 | 3 | import React, { useMemo, useEffect, useCallback } from "react"; 4 | import useStoreon from "storeon/react"; 5 | import {Button, Spinner, Modal, ModalBody, ModalFooter} from 'reactstrap'; 6 | import { saveAs } from "file-saver"; 7 | 8 | export default () => { 9 | 10 | const { uiState, providers, dispatch } = useStoreon("uiState", "providers"); 11 | 12 | const provider = useMemo( () => { 13 | return providers.find(p => p.id === uiState.id) 14 | }, [uiState.id, providers]); 15 | 16 | 17 | const handleCancel = useCallback( e => { 18 | e.preventDefault(); 19 | dispatch("fhir/cancelLoad"); 20 | }, [dispatch]); 21 | 22 | const handleClose = useCallback( e => { 23 | if (e) e.preventDefault(); 24 | dispatch("uiState/set", {mode: "ready"}); 25 | }, [dispatch]); 26 | 27 | //escape key effect 28 | //creates a warning due to a reactjs bug: https://github.com/facebook/react/pull/15650 29 | useEffect(() => { 30 | const downHandler = e => { 31 | if (e.keyCode !== 27) return; 32 | if (uiState.submode === "authorizing" || uiState.submode === "loading") { 33 | handleCancel(e); 34 | } else { 35 | handleClose(e); 36 | } 37 | }; 38 | window.addEventListener("keydown", downHandler); 39 | return () => { 40 | window.removeEventListener("keydown", downHandler); 41 | }; 42 | }, [handleCancel, handleClose, uiState.submode]); 43 | 44 | const handleSave = e => { 45 | e.preventDefault(); 46 | dispatch("providers/update", { 47 | id: provider.id, 48 | lastUpdated: new Date(), 49 | data: uiState.data 50 | }); 51 | handleClose(); 52 | } 53 | 54 | const handleLogDownload = e => { 55 | e.preventDefault(); 56 | const blob = new Blob( 57 | [JSON.stringify(uiState.data.errorLog, null, 2)], 58 | {type: "application/json"} 59 | ); 60 | saveAs(blob, "errors.json"); 61 | } 62 | 63 | const renderAuthorizing = () =>
64 |
Waiting for authorization...
65 |
66 | 67 |
68 |
69 | 70 | const pluralizeEn = (text, len) => len === 1 ? text : text+"s"; 71 | 72 | const renderLoading = () =>
73 |
74 | {uiState.requestCount} {pluralizeEn("request", uiState.requestCount)} 75 |   /   76 | {uiState.errorCount} {pluralizeEn("error", uiState.errorCount)} 77 |
78 |
79 | 80 |
81 |
82 | 83 |
84 |
85 | 86 | const renderLoaded = () =>
87 |
88 | {uiState.data.entry.length} {pluralizeEn("resource", uiState.data.entry.length)} retrieved
89 | {uiState.data.files.length || "No"} {pluralizeEn("attachment", uiState.data.files.length)} downloaded
90 | {uiState.data.errorLog.length || "No"} {pluralizeEn("error", uiState.data.errorLog.length)} occurred 91 |
92 |
93 | 94 | const renderGlobalError = () =>
95 |
An error occurred: { uiState.status }
96 |
97 | 98 |
99 |
100 | 101 | 102 | const renderLoadedButtons = () => { 103 | const downloadLink = uiState.data.errorLog.length > 0 && 104 | ; 105 | return 106 | {downloadLink} 107 | 108 | 109 | 110 | } 111 | 112 | return 113 | 114 |

{provider.name}

115 | { uiState.submode === "authorizing" && renderAuthorizing() } 116 | { uiState.submode === "loading" && renderLoading() } 117 | { uiState.submode === "loaded" && renderLoaded() } 118 | { uiState.submode === "error" && renderGlobalError() } 119 |
120 | { uiState.submode === "loaded" && renderLoadedButtons() } 121 |
122 | 123 | 124 | } -------------------------------------------------------------------------------- /src/components/FhirTree.js: -------------------------------------------------------------------------------- 1 | /* eslint jsx-a11y/anchor-is-valid: 0 */ //disable to support bootstrap link styling 2 | 3 | import React, { useState, useMemo, useRef } from "react"; 4 | import useStoreon from "storeon/react"; 5 | import Select from "react-select"; 6 | import JSONTree from "react-json-tree"; 7 | import {FormGroup, Button} from "reactstrap"; 8 | import _ from "lodash"; 9 | import { saveAs } from "file-saver"; 10 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" 11 | import { faChevronLeft, faChevronRight } from "@fortawesome/free-solid-svg-icons" 12 | 13 | export default () => { 14 | 15 | //app state 16 | const { providers, dateSortElements } = useStoreon("providers", "dateSortElements"); 17 | 18 | //component state 19 | const [resourceType, setResourceType] = useState(); 20 | const [position, setPosition] = useState(1); 21 | const [overlayResources, setOverlayResources] = useState([]); 22 | 23 | const componentTopRef = useRef(); 24 | 25 | let resourceTypeOptions = useMemo( () => { 26 | return _.chain(providers) 27 | .filter( p => p.data && p.selected ) 28 | .map( p => p.data.entry.map(e => e.resource.resourceType) ) 29 | .flatten().uniq() 30 | .map(o => ({label: o, value: o})) 31 | .sortBy(["label"]) 32 | .value(); 33 | }, [providers]); 34 | 35 | let resources = useMemo( () => { 36 | return _.chain(providers) 37 | .filter( p => p.data && p.data.entry && p.selected ) 38 | .map( p => p.data.entry ).flatten() 39 | .filter( e => (!resourceType || resourceType.value === e.resource.resourceType) ) 40 | .sortBy( e => { 41 | const sortElements = dateSortElements[e.resource.resourceType] || []; 42 | const sortElement = _.find( sortElements, element => e.resource[element]); 43 | const dateValue = 44 | (sortElement && (e.resource[sortElement].start || e.resource[sortElement])) || 45 | (e.resource.meta && e.resource.meta.lastUpdated) || ""; 46 | return dateValue; 47 | }) 48 | .reverse() 49 | .value(); 50 | }, [providers, resourceType, dateSortElements]); 51 | 52 | //remove leading slashes, trailing slashes and protocol 53 | const simplifyUrl = (url) => url.replace(/^\/|https?:\/\/|\/*$/g, "") 54 | 55 | const referenceIndex = useMemo( () => { 56 | let index = {}; 57 | _.each(providers, (provider, i) => { 58 | _.each(((provider.data && provider.data.entry) || []), (entry, j) => { 59 | index[simplifyUrl(entry.fullUrl)] = [i, j, "resource"] 60 | }) 61 | _.each(((provider.data && provider.data.files) || []), (file, j) => { 62 | index[file.fileName] = [i, j, "file"] 63 | }) 64 | }); 65 | return index; 66 | }, [providers]); 67 | 68 | const handlePosChange = (dir, e) => { 69 | e.preventDefault(); 70 | setPosition(position+dir); 71 | } 72 | 73 | const filters = 74 | handleFieldChange("token", e) } 143 | /> 144 | 145 | 146 | 147 | 148 | 149 | 150 | handleFieldChange("owner", e) } 153 | placeholder="Owner" data-lpignore="true" 154 | /> 155 | 156 | 157 | 158 | handleFieldChange("project", e) } 161 | placeholder="Project" data-lpignore="true" 162 | /> 163 | 164 | 165 | 166 | 167 | 175 | 176 | 177 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | return 196 | Push to Github Repository 197 | { uiState.submode === "push" ? renderLoading() : renderForm() } 198 | 199 | 200 | } -------------------------------------------------------------------------------- /src/components/Header.js: -------------------------------------------------------------------------------- 1 | /* eslint jsx-a11y/anchor-is-valid: 0 */ //disable to support bootstrap link styling 2 | 3 | import React, {useRef, useEffect} from "react"; 4 | import useStoreon from "storeon/react"; 5 | import {Navbar, NavbarBrand, Nav, NavItem, NavLink} from 'reactstrap'; 6 | import SettingsImportDropZone from "./SettingsImportDropZone"; 7 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" 8 | import { faMedkit } from "@fortawesome/free-solid-svg-icons" 9 | 10 | export default () => { 11 | 12 | const { providers, isDirty, warnOnPageNavigate, appName, dispatch } = useStoreon("providers", "isDirty", "warnOnPageNavigate", "appName"); 13 | const fileInput = useRef(); 14 | 15 | useEffect( () => { 16 | window.onbeforeunload = () => (isDirty && warnOnPageNavigate) ? true : null; 17 | }); 18 | 19 | const handleImportSettings = e => { 20 | e.preventDefault(); 21 | fileInput.current.value = ""; 22 | fileInput.current.click(); 23 | } 24 | 25 | const handleFileSelected = e => { 26 | e.preventDefault(); 27 | e.stopPropagation(); 28 | if (e.target.files.length === 1) 29 | readFile(e.target.files[0]); 30 | } 31 | 32 | const handleDownloadSettings = e => { 33 | e.preventDefault(); 34 | dispatch("export/settings"); 35 | } 36 | 37 | const readFile = (file) => { 38 | const replace = providers.length === 0 || window.confirm( 39 | "Current providers in your list will be replaced. Continue?" 40 | ) 41 | if (replace) { 42 | dispatch("import/settings", file) 43 | dispatch("refreshDirty", true); 44 | }; 45 | } 46 | 47 | return
48 | 54 | 55 | 56 | 57 | {appName || "Procure"} 58 | 59 | 69 | 70 |
71 | 72 | 73 | } -------------------------------------------------------------------------------- /src/components/Loader.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {Spinner} from 'reactstrap'; 3 | 4 | export default () => { 5 | 6 | return
7 | 8 |

Loading

9 |
10 | 11 | } -------------------------------------------------------------------------------- /src/components/ProviderList.js: -------------------------------------------------------------------------------- 1 | /* eslint jsx-a11y/anchor-is-valid: 0 */ //disable to support bootstrap link styling 2 | 3 | import React from "react"; 4 | import useStoreon from "storeon/react"; 5 | import TimeAgo from "./TimeAgo" 6 | import { Button, CardBody, Card, Row, Col } from "reactstrap"; 7 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" 8 | import { faPlus, faTrash, faEdit, faCheckSquare } from "@fortawesome/free-solid-svg-icons" 9 | import { faSquare } from "@fortawesome/free-regular-svg-icons" 10 | 11 | export default () => { 12 | 13 | const { providers, dispatch } = useStoreon("providers"); 14 | 15 | const handleDeleteProvider = (id, e) => { 16 | e.preventDefault(); 17 | dispatch("providers/remove", id); 18 | dispatch("refreshDirty"); 19 | } 20 | 21 | const handleEditProvider = (id, e) => { 22 | e.preventDefault(); 23 | dispatch("uiState/set", { 24 | mode: "editProvider", id 25 | }); 26 | } 27 | 28 | const handleLoadRecords = (id, e) => { 29 | e.preventDefault(); 30 | dispatch("fhir/loadData", id); 31 | } 32 | 33 | const handleAddProvider = (e) => { 34 | e.preventDefault(); 35 | dispatch("uiState/set", {mode: "editProvider"}); 36 | } 37 | 38 | const toggleProviderSelection = (id, newValue, e) => { 39 | dispatch("providers/update", { 40 | id: id, selected: newValue 41 | }); 42 | } 43 | 44 | const pluralizeEn = (text, len) => len === 1 ? text : text+"s"; 45 | 46 | const renderCard = (provider) => { 47 | 48 | 49 | const renderDataDetails = () =>

50 | { provider.data.entry.length} { pluralizeEn("resource", provider.data.entry.length) }{" | "} 51 | { provider.data.files.length} { pluralizeEn("file", provider.data.files.length) }{" | "} 52 | { provider.data.errorLog.length} { pluralizeEn("error", provider.data.errorLog.length) } 53 |
loaded 54 |

55 | 56 | const controls = 79 | 80 | return 81 | 82 | 83 |
84 | toggleProviderSelection(provider.id, !provider.selected)} 89 | /> 90 | 93 |
94 | 95 |

toggleProviderSelection(provider.id, !provider.selected, e)} 98 | > 99 | {provider.name} 100 |

101 | { provider.lastUpdated && renderDataDetails() } 102 | { controls } 103 | 104 |
105 |
106 | 107 | }; 108 | 109 | return
110 |
111 | 118 |
119 | { providers.map(renderCard) } 120 |
121 | 122 | } -------------------------------------------------------------------------------- /src/components/SettingsImportDropZone.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | export default ({ children, dropHint, fileDropHandler }) => { 4 | 5 | const [dragCounter, setDragCounter] = useState(0); 6 | 7 | const isFileDrag = e => { 8 | return !e.dataTransfer.types || 9 | e.dataTransfer.types[0] === "Files" || 10 | e.dataTransfer.types[0] === "application/x-moz-file"; 11 | } 12 | 13 | const handleDrop = e => { 14 | e.preventDefault(); 15 | e.stopPropagation(); 16 | if (e.dataTransfer.files && e.dataTransfer.files.length === 1) 17 | fileDropHandler(e.dataTransfer.files[0]); 18 | // e.dataTransfer.clearData(); 19 | setDragCounter(0); 20 | } 21 | 22 | const handleDragOver = e => { 23 | e.preventDefault(); 24 | e.stopPropagation(); 25 | } 26 | 27 | const handleDragEnter = e => { 28 | e.preventDefault(); 29 | e.stopPropagation(); 30 | if (isFileDrag(e)) 31 | setDragCounter(dragCounter+1); 32 | } 33 | 34 | const handleDragLeave = e => { 35 | e.preventDefault(); 36 | e.stopPropagation(); 37 | if (isFileDrag(e)) 38 | setDragCounter(dragCounter-1); 39 | } 40 | 41 | const overlay =
51 |
52 | { dropHint || "Drop settings file here"} 53 |
54 |
55 | 56 | return
63 | { dragCounter > 0 && overlay } 64 | { children } 65 |
66 | 67 | 68 | } -------------------------------------------------------------------------------- /src/components/TimeAgo.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {format} from "timeago.js"; 3 | 4 | export default class TimeAgo extends React.Component { 5 | 6 | constructor(props) { 7 | super(props); 8 | this.state= {timeAgo: format(this.props.time)} 9 | } 10 | 11 | componentDidMount() { 12 | this.interval = setInterval(() => { 13 | this.setState({timeAgo: format(this.props.time)}); 14 | }, (this.props.refreshMinutes||1)*60000); 15 | } 16 | 17 | componentWillUnmount() { 18 | clearInterval(this.interval); 19 | } 20 | 21 | render() { 22 | return this.state.timeAgo 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /src/components/Toolbar.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from "react"; 2 | import {Button, Dropdown, DropdownToggle, DropdownMenu, DropdownItem} from 'reactstrap'; 3 | import useStoreon from "storeon/react"; 4 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" 5 | import { faTable, faFileDownload, faFileUpload } from "@fortawesome/free-solid-svg-icons" 6 | import { faGithub } from "@fortawesome/free-brands-svg-icons" 7 | 8 | export default (props) => { 9 | 10 | const { 11 | spreadsheetTemplates, 12 | upload, 13 | nonModalUi, dispatch 14 | } = useStoreon( 15 | "spreadsheetTemplates", 16 | "upload", 17 | "nonModalUi" 18 | ); 19 | 20 | const [showSpreadsheetOptions, setShowSpreadsheetOptions] = useState(false); 21 | 22 | const handleExportData = (e) => { 23 | e.preventDefault(); 24 | dispatch("export/download"); 25 | } 26 | 27 | const handleFileUpload = (e) => { 28 | e.preventDefault() 29 | dispatch("export/upload"); 30 | } 31 | 32 | const handleShowGithubExport = (e) => { 33 | e.preventDefault() 34 | dispatch("uiState/set", {mode: "githubExport"}); 35 | } 36 | 37 | const handelExportSpreadsheet = (templateId, format, e) => { 38 | e.preventDefault() 39 | dispatch("export/spreadsheet", {templateId, format}); 40 | } 41 | 42 | const renderSpreadsheetExport = () => { 43 | if (!Object.keys(spreadsheetTemplates||{}).length) 44 | return null; 45 | let menuItems = []; 46 | Object.keys(spreadsheetTemplates).forEach( templateId => { 47 | menuItems.push( 48 | 52 | {spreadsheetTemplates[templateId].name + " (Excel)"} 53 | , 54 | 58 | {spreadsheetTemplates[templateId].name + " (CSV)"} 59 | 60 | ) 61 | }); 62 | return setShowSpreadsheetOptions(!showSpreadsheetOptions)} 67 | > 68 | 73 | 74 | Export as Spreadsheet 75 | 76 | {menuItems} 77 | 78 | } 79 | 80 | const uploadButton = ; 88 | 89 | const downloadAllButton = ; 98 | 99 | const ghExportButton = ; 107 | 108 | return
109 | {upload && (upload.manifestUrl || upload.uploadUrl) && uploadButton} 110 | {downloadAllButton} 111 | {props.hasResources && renderSpreadsheetExport()} 112 | {ghExportButton} 113 |
114 | 115 | } -------------------------------------------------------------------------------- /src/components/Wizard/DownloadButton.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from "react"; 2 | import {Dropdown, DropdownToggle, DropdownMenu, DropdownItem} from 'reactstrap'; 3 | import useStoreon from "storeon/react"; 4 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" 5 | import { faFileDownload } from "@fortawesome/free-solid-svg-icons" 6 | 7 | export default (props) => { 8 | 9 | const { 10 | spreadsheetTemplates, 11 | nonModalUi, 12 | dispatch 13 | } = useStoreon( 14 | "spreadsheetTemplates", 15 | "nonModalUi" 16 | ); 17 | 18 | const [showDownloadOptions, setShowDownloadOptions] = useState(false); 19 | 20 | const handelExportSpreadsheet = (templateId, format, e) => { 21 | e.preventDefault() 22 | dispatch("export/spreadsheet", {templateId, format}); 23 | } 24 | 25 | const handleExportData = (e) => { 26 | e.preventDefault(); 27 | dispatch("export/download"); 28 | } 29 | 30 | let menuItems = [ 31 | FHIR Format 35 | ]; 36 | 37 | (Object.keys(spreadsheetTemplates) || []).forEach( templateId => { 38 | menuItems.push( 39 | 43 | Spreadsheet of {spreadsheetTemplates[templateId].name} 44 | 45 | ) 46 | }); 47 | 48 | return setShowDownloadOptions(!showDownloadOptions)} 54 | > 55 | 60 | 61 | Download copy of data 62 | 63 | {menuItems} 64 | 65 | 66 | } -------------------------------------------------------------------------------- /src/components/Wizard/Retrieve.js: -------------------------------------------------------------------------------- 1 | /* eslint jsx-a11y/anchor-is-valid: 0 */ //disable to support bootstrap link styling 2 | 3 | import React, { useMemo, useEffect, useCallback } from "react"; 4 | import useStoreon from "storeon/react"; 5 | import {Button, Spinner} from 'reactstrap'; 6 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" 7 | import { faChevronLeft } from "@fortawesome/free-solid-svg-icons" 8 | 9 | export default () => { 10 | 11 | const { uiState, providers, dispatch } = useStoreon("uiState", "providers"); 12 | 13 | const provider = useMemo( () => { 14 | return providers.find(p => p.id === uiState.id) 15 | }, [uiState.id, providers]); 16 | 17 | const handleCancel = useCallback( e => { 18 | e.preventDefault(); 19 | dispatch("fhir/cancelLoad", "editProvider"); 20 | dispatch("providers/remove", provider.id) 21 | }, [dispatch, provider]); 22 | 23 | const handleBack = e => { 24 | dispatch("providers/remove", provider.id) 25 | dispatch("uiState/set", {mode: "editProvider"}); 26 | } 27 | 28 | //escape key effect 29 | //creates a warning due to a reactjs bug: https://github.com/facebook/react/pull/15650 30 | useEffect(() => { 31 | const downHandler = e => { 32 | if (e.keyCode !== 27) return; 33 | if (uiState.submode === "authorizing" || uiState.submode === "loading") { 34 | handleCancel(e); 35 | } 36 | }; 37 | window.addEventListener("keydown", downHandler); 38 | return () => { 39 | window.removeEventListener("keydown", downHandler); 40 | }; 41 | }, [handleCancel, uiState.submode]); 42 | 43 | //TODO: this is kind of a hack to build on non-wizard modes (need to restructure modes to work with both) 44 | if (uiState.mode !== "loadData") return null; 45 | if (uiState.submode === "loaded") { 46 | dispatch("providers/update", { 47 | id: provider.id, 48 | lastUpdated: new Date(), 49 | data: uiState.data 50 | }); 51 | dispatch("uiState/set", {mode: "review"}); 52 | }; 53 | 54 | const renderWorking = (status) =>
55 |
{status}
56 |
57 | 58 |
59 |
60 | 61 |
62 |
63 | 64 | const renderGlobalError = () =>
65 |
Error
66 |

An error occurred loading your record: {uiState.status}

67 |
68 | 72 |
73 |
74 | 75 | return
76 |

{provider.name}

77 | { uiState.submode === "authorizing" && renderWorking("Waiting for Portal Login...") } 78 | { uiState.submode === "loading" && renderWorking("Retrieving Healthcare Records...") } 79 | { uiState.submode === "error" && renderGlobalError() } 80 |
81 | 82 | 83 | } -------------------------------------------------------------------------------- /src/components/Wizard/Review.js: -------------------------------------------------------------------------------- 1 | /* eslint jsx-a11y/anchor-is-valid: 0 */ //disable to support bootstrap link styling 2 | 3 | import React from "react"; 4 | import { Button } from 'reactstrap'; 5 | import useStoreon from "storeon/react"; 6 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" 7 | import { faChevronRight } from "@fortawesome/free-solid-svg-icons" 8 | import DownloadButton from "./DownloadButton"; 9 | import _ from "lodash"; 10 | 11 | export default () => { 12 | 13 | const {providers, upload, dispatch} = useStoreon("providers", "upload"); 14 | 15 | const handleAddProvider = e => { 16 | e.preventDefault(); 17 | dispatch("uiState/set", {mode: "editProvider"}); 18 | } 19 | const handleFileUpload = e => { 20 | e.preventDefault() 21 | dispatch("export/upload/send"); 22 | } 23 | 24 | const handleDeleteProvider = (id, e) => { 25 | e.preventDefault(); 26 | dispatch("providers/remove", id); 27 | dispatch("refreshDirty"); 28 | } 29 | 30 | const renderProvider = (provider) => { 31 | return
  • 32 | {provider.name}{" "} 33 | ( handleDeleteProvider(provider.id, e)}>remove) 34 |
  • 35 | }; 36 | 37 | const providerList = _.chain(providers) 38 | .sortBy( p => p.lastUpdated) 39 | .map(renderProvider) 40 | .value(); 41 | 42 | return
    43 |

    Review Data to Share

    44 |

    You have collected records from the following healthcare providers:

    45 |
      { providerList }
    46 | 47 |
    48 |
    49 | 50 |
    51 |
    52 | 64 | 78 |
    79 |
    80 | 81 | 82 |
    83 | 84 | } -------------------------------------------------------------------------------- /src/components/Wizard/SelectProvider.js: -------------------------------------------------------------------------------- 1 | /* eslint jsx-a11y/anchor-is-valid: 0 */ //disable to support bootstrap link styling 2 | 3 | import React, { useState, useEffect } from "react"; 4 | import useStoreon from "storeon/react"; 5 | import { Button } from 'reactstrap'; 6 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" 7 | import { faChevronRight } from "@fortawesome/free-solid-svg-icons" 8 | import Select, { createFilter } from "react-select"; 9 | import _ from "lodash"; 10 | 11 | export default () => { 12 | 13 | //app state 14 | const { 15 | uiState, providers, credentials, 16 | organizations, orgDefaults, redirectUri, dispatch 17 | } = useStoreon( 18 | "uiState", "providers", "credentials", 19 | "organizations", "orgDefaults", "redirectUri" 20 | ); 21 | 22 | //component state 23 | const [provider, setProvider] = useState({}); 24 | 25 | const orgOptions = _.chain(organizations) 26 | .map( o => ({label: o.name, value: o.orgId || o.fhirEndpoint}) ) 27 | .sortBy( o => o.label ) 28 | .value(); 29 | 30 | useEffect( () => { 31 | if (!provider.id && uiState.id && uiState.mode === "editProvider") { 32 | const currentProvider = providers.find(p => p.id === uiState.id); 33 | setProvider(currentProvider) 34 | } 35 | }, [uiState.id, uiState.mode, provider.id, providers]) 36 | 37 | const handleSubmit = e => { 38 | e.preventDefault(); 39 | dispatch("providers/upsertAndLoad", provider); 40 | dispatch("refreshDirty"); 41 | } 42 | 43 | const handleSkip = e => { 44 | e.preventDefault(); 45 | dispatch("uiState/set", {mode: "review"}); 46 | } 47 | 48 | const handleOrgSelection = (selection) => { 49 | let orgConfig = organizations.find( 50 | o => selection.value === o.orgId || selection.value === o.fhirEndpoint 51 | ); 52 | 53 | const defaults = orgDefaults[orgConfig.defaultId] || {}; 54 | if (orgConfig.defaultId !== undefined) 55 | orgConfig = {...defaults, ...orgConfig}; 56 | 57 | //get credential details if included by reference 58 | if (orgConfig.credentialId) 59 | orgConfig = {...credentials[orgConfig.credentialId], ...orgConfig}; 60 | 61 | //update provider with new defaults 62 | setProvider({ 63 | ...orgConfig, 64 | redirectUri, 65 | id: provider.id, 66 | orgId: selection.value, 67 | queryProfile: orgConfig.queryProfile || "argonaut_spec" 68 | }); 69 | } 70 | 71 | const renderOrgSelector = () => { 72 | 73 | //workaround since component uses both the value and label to match and we just want to store the value 74 | const value = provider.orgId ? orgOptions.find(o => o.value === provider.orgId) : null; 75 | 76 | return