diff --git a/.eslintrc.json b/.eslintrc.json index 8d3de317..6b5b686a 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -41,13 +41,14 @@ }, "plugins": [ "react", - "compat" + "react-hooks" ], "rules": { "semi": "error", - "compat/compat": "warn", "react/react-in-jsx-scope": "off", "react/display-name": "off", - "no-useless-escape": "off" + "no-useless-escape": "off", + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "warn" } } diff --git a/conftest.py b/conftest.py new file mode 100644 index 00000000..e69de29b diff --git a/package-lock.json b/package-lock.json index 3730df27..2ef0bac0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2937,6 +2937,7 @@ "version": "7.16.5", "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.16.5.tgz", "integrity": "sha512-F1pMwvTiUNSAM8mc45kccMQxj31x3y3P+tA/X8hKNWp3/hUsxdGxZ3D3H8JIkxtfA8qGkaBTKvcmvStaYseAFw==", + "dev": true, "requires": { "core-js-pure": "^3.19.0", "regenerator-runtime": "^0.13.4" @@ -3640,6 +3641,28 @@ } } }, + "@material-table/core": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@material-table/core/-/core-5.0.5.tgz", + "integrity": "sha512-0kdCYwL77XHEHjekPSDEUpTi/+knOtAYMYT0j4u49p2Zj53OxXpdyZsXu151Lf61+XV5T3ewNT9MqWJ2/Pjgsw==", + "requires": { + "@babel/runtime": "^7.12.5", + "@date-io/date-fns": "^1.3.13", + "@material-ui/icons": "^4.11.2", + "@material-ui/pickers": "^3.2.10", + "@material-ui/styles": "^4.11.4", + "@react-forked/dnd": "^14.0.2", + "classnames": "^2.2.6", + "date-fns": "^2.16.1", + "debounce": "^1.2.0", + "deepmerge-ts": "^4.0.3", + "fast-deep-equal": "^3.1.3", + "prop-types": "^15.7.2", + "react-double-scrollbar": "0.0.15", + "uuid": "^3.4.0", + "zustand": "^4.0.0-rc.1" + } + }, "@material-ui/core": { "version": "4.12.3", "resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.12.3.tgz", @@ -3747,6 +3770,30 @@ "integrity": "sha512-n2RC9d6XatVbWFdHLimzzUJxJ1KY8LdjqrW6YvGPiRmsHkhOUx74/Ct10x5Yo7bC/Jvqx7cDEW8IMPv/+vwEzA==", "dev": true }, + "@react-forked/dnd": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/@react-forked/dnd/-/dnd-14.0.2.tgz", + "integrity": "sha512-5+w0c/jTRSbjBPii/gfQ5hzvaNr8mTuYSbVxxR6Uh0DJOjvKQWGmKHafKOm/ci4r8dCK3+RxEKnUe5dOGhVnUw==", + "requires": { + "@babel/runtime": "^7.17.9", + "css-box-model": "^1.2.1", + "memoize-one": "^6.0.0", + "raf-schd": "^4.0.3", + "react-redux": "^7.2.8", + "redux": "^4.1.2", + "use-memo-one": "^1.1.2" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.9.tgz", + "integrity": "sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + } + } + }, "@sinonjs/commons": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.0.tgz", @@ -3917,12 +3964,6 @@ "integrity": "sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ==", "dev": true }, - "@types/raf": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.0.tgz", - "integrity": "sha512-taW5/WYqo36N7V39oYyHP9Ipfd5pNFvGTIQsNGj86xV88YQ7GnI30/yMfKDF7Zgin0m3e+ikX88FvImnK4RjGw==", - "optional": true - }, "@types/react": { "version": "16.9.36", "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.36.tgz", @@ -3933,9 +3974,9 @@ } }, "@types/react-redux": { - "version": "7.1.16", - "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.16.tgz", - "integrity": "sha512-f/FKzIrZwZk7YEO9E1yoxIuDNRiDducxkFlkw/GNMGEnK9n4K8wJzlJBghpSuOVDgEUHoDkDF7Gi9lHNQR4siw==", + "version": "7.1.24", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.24.tgz", + "integrity": "sha512-7FkurKcS1k0FHZEtdbbgN8Oc6b+stGSfZYjQGicofJ0j4U0qIn/jaSvnP2pLwZKiai3/17xqqxkkrxTgN8UNbQ==", "requires": { "@types/hoist-non-react-statics": "^3.3.0", "@types/react": "*", @@ -4975,7 +5016,8 @@ "atob": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true }, "aws-sign2": { "version": "0.7.0", @@ -5340,12 +5382,6 @@ } } }, - "base64-arraybuffer": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz", - "integrity": "sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==", - "optional": true - }, "base64-js": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", @@ -5632,11 +5668,6 @@ "node-int64": "^0.4.0" } }, - "btoa": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", - "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==" - }, "buffer": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", @@ -5828,20 +5859,6 @@ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001292.tgz", "integrity": "sha512-jnT4Tq0Q4ma+6nncYQVe7d73kmDmE9C3OGTx3MvW7lBM/eY1S1DZTMBON7dqV481RhNiS5OxD7k9JQvmDOTirw==" }, - "canvg": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.7.tgz", - "integrity": "sha512-4sq6iL5Q4VOXS3PL1BapiXIZItpxYyANVzsAKpTPS5oq4u3SKbGfUcbZh2gdLCQ3jWpG/y5wRkMlBBAJhXeiZA==", - "optional": true, - "requires": { - "@babel/runtime-corejs3": "^7.9.6", - "@types/raf": "^3.4.0", - "raf": "^3.4.1", - "rgbcolor": "^1.0.1", - "stackblur-canvas": "^2.0.0", - "svg-pathdata": "^5.0.5" - } - }, "capture-exit": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", @@ -6602,7 +6619,8 @@ "core-js": { "version": "3.20.1", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.20.1.tgz", - "integrity": "sha512-btdpStYFQScnNVQ5slVcr858KP0YWYjV16eGJQw8Gg7CWtu/2qNvIM3qVRIR3n1pK2R9NNOrTevbvAYxajwEjg==" + "integrity": "sha512-btdpStYFQScnNVQ5slVcr858KP0YWYjV16eGJQw8Gg7CWtu/2qNvIM3qVRIR3n1pK2R9NNOrTevbvAYxajwEjg==", + "dev": true }, "core-js-compat": { "version": "3.20.0", @@ -6623,7 +6641,8 @@ "core-js-pure": { "version": "3.20.0", "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.20.0.tgz", - "integrity": "sha512-qsrbIwWSEEYOM7z616jAVgwhuDDtPLwZSpUsU3vyUkHYqKTf/uwOJBZg2V7lMurYWkpVlaVOxBrfX0Q3ppvjfg==" + "integrity": "sha512-qsrbIwWSEEYOM7z616jAVgwhuDDtPLwZSpUsU3vyUkHYqKTf/uwOJBZg2V7lMurYWkpVlaVOxBrfX0Q3ppvjfg==", + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -6813,15 +6832,6 @@ "timsort": "^0.3.0" } }, - "css-line-break": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-1.1.1.tgz", - "integrity": "sha512-1feNVaM4Fyzdj4mKPIQNL2n70MmuYzAXZ1aytlROFX1JsOo070OsugwGjj7nl6jnDJWHDM8zRZswkmeYVWZJQA==", - "optional": true, - "requires": { - "base64-arraybuffer": "^0.2.0" - } - }, "css-loader": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.6.0.tgz", @@ -7089,9 +7099,9 @@ "integrity": "sha512-sj+J0Mo2p2X1e306MHq282WS4/A8Pz/95GIFcsPNMPMZVI3EUrAdSv90al1k+p74WGLCruMXk23bfEDZa71X9Q==" }, "debounce": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.0.tgz", - "integrity": "sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg==" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==" }, "debug": { "version": "4.1.1", @@ -7130,8 +7140,7 @@ "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" }, "deepmerge": { "version": "4.2.2", @@ -7139,6 +7148,11 @@ "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", "dev": true }, + "deepmerge-ts": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-4.2.1.tgz", + "integrity": "sha512-xzJLiUo4z1dD2nggSfaMvHo5qWLoy/JVa9rKuktC6FrQQEBI8Qnj7KwuCYZhqBoGOOpGqs6+3MR2ZhSMcTr4BA==" + }, "default-gateway": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz", @@ -8015,7 +8029,6 @@ "version": "1.14.2", "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.2.tgz", "integrity": "sha512-InuOIiKk8wwuOFg6x9BQXbzjrQhtyXh46K9bqVTPzSo2FnyMBaYGBMC6PhQy7yxxil9vIedFBweQBMK74/7o8A==", - "dev": true, "requires": { "esprima": "^4.0.1", "estraverse": "^4.2.0", @@ -8028,7 +8041,6 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, "requires": { "prelude-ls": "~1.1.2", "type-check": "~0.3.2" @@ -8038,7 +8050,6 @@ "version": "0.8.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dev": true, "requires": { "deep-is": "~0.1.3", "fast-levenshtein": "~2.0.6", @@ -8051,21 +8062,18 @@ "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, "optional": true }, "type-check": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, "requires": { "prelude-ls": "~1.1.2" } @@ -8473,6 +8481,12 @@ } } }, + "eslint-plugin-react-hooks": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", + "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "dev": true + }, "eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -8553,8 +8567,7 @@ "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" }, "esquery": { "version": "1.4.0", @@ -8593,8 +8606,7 @@ "estraverse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" }, "esutils": { "version": "2.0.3", @@ -9029,8 +9041,7 @@ "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-json-stable-stringify": { "version": "2.1.0", @@ -9041,8 +9052,7 @@ "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" }, "faye-websocket": { "version": "0.11.4", @@ -9077,11 +9087,6 @@ "flat-cache": "^3.0.4" } }, - "filefy": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/filefy/-/filefy-0.1.10.tgz", - "integrity": "sha512-VgoRVOOY1WkTpWH+KBy8zcU1G7uQTVsXqhWEgzryB9A5hg2aqCyZ6aQ/5PSzlqM5+6cnVrX6oYV0XqD3HZSnmQ==" - }, "filemanager-webpack-plugin": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/filemanager-webpack-plugin/-/filemanager-webpack-plugin-2.0.5.tgz", @@ -10134,15 +10139,6 @@ } } }, - "html2canvas": { - "version": "1.0.0-rc.7", - "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.0.0-rc.7.tgz", - "integrity": "sha512-yvPNZGejB2KOyKleZspjK/NruXVQuowu8NnV2HYG7gW7ytzl+umffbtUI62v2dCHQLDdsK6HIDtyJZ0W3neerA==", - "optional": true, - "requires": { - "css-line-break": "1.1.1" - } - }, "htmlparser2": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", @@ -12413,24 +12409,23 @@ "graceful-fs": "^4.1.6" } }, - "jspdf": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.1.0.tgz", - "integrity": "sha512-NQygqZEKhSw+nExySJxB72Ge/027YEyIM450Vh/hgay/H9cgZNnkXXOQPRspe9EuCW4sq92zg8hpAXyyBdnaIQ==", + "jsonpath": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.1.1.tgz", + "integrity": "sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w==", "requires": { - "atob": "^2.1.2", - "btoa": "^1.2.1", - "canvg": "^3.0.6", - "core-js": "^3.6.0", - "dompurify": "^2.0.12", - "html2canvas": "^1.0.0-rc.5" + "esprima": "1.2.2", + "static-eval": "2.0.2", + "underscore": "1.12.1" + }, + "dependencies": { + "esprima": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.2.tgz", + "integrity": "sha512-+JpPZam9w5DuJ3Q67SqsMGtiHKENSMRVoxvArfJZK01/BfLEObtZ6orJa/MtoGNR/rfMgp5837T41PAmTwAv/A==" + } } }, - "jspdf-autotable": { - "version": "3.5.9", - "resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-3.5.9.tgz", - "integrity": "sha512-ZRfiI5P7leJuWmvC0jGVXu227m68C2Jfz1dkDckshmDYDeVFCGxwIBYdCUXJ8Eb2CyFQC2ok82fEWO+xRDovDQ==" - }, "jsprim": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", @@ -12842,72 +12837,6 @@ "object-visit": "^1.0.0" } }, - "material-table": { - "version": "1.69.3", - "resolved": "https://registry.npmjs.org/material-table/-/material-table-1.69.3.tgz", - "integrity": "sha512-UT/eQbUqfAg6XstcnM8dOQNgWmWbH5tbRmwqfGfPKQUQQXq/jb1z2spFHXPJBhEZFmk+Q95HlopiE6nAHymLMw==", - "requires": { - "@date-io/date-fns": "1.1.0", - "@material-ui/pickers": "3.2.2", - "classnames": "2.2.6", - "date-fns": "2.0.0-alpha.27", - "debounce": "1.2.0", - "fast-deep-equal": "2.0.1", - "filefy": "0.1.10", - "jspdf": "2.1.0", - "jspdf-autotable": "3.5.9", - "prop-types": "15.6.2", - "react-beautiful-dnd": "13.0.0", - "react-double-scrollbar": "0.0.15" - }, - "dependencies": { - "@date-io/date-fns": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@date-io/date-fns/-/date-fns-1.1.0.tgz", - "integrity": "sha512-FMRhYWfoGiIXdN4xWAArpkdEbqsg2Fr+6Yda7Np2eVWCNx6gSMYsHIM51IIcI+3762ajYbhoEYjHYXVFNZIk1g==", - "requires": { - "@date-io/core": "^1.1.0" - } - }, - "@material-ui/pickers": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@material-ui/pickers/-/pickers-3.2.2.tgz", - "integrity": "sha512-on/J1yyKeJ4CkLnItpf/jPDKMZVWvHDklkh5FS7wkZ0s1OPoqTsPubLWfA7eND6xREnVRyLFzVTlE3VlWYdQWw==", - "requires": { - "@babel/runtime": "^7.2.0", - "@types/styled-jsx": "^2.2.8", - "clsx": "^1.0.2", - "react-transition-group": "^4.0.0", - "rifm": "^0.7.0", - "tslib": "^1.9.3" - } - }, - "classnames": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", - "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==" - }, - "date-fns": { - "version": "2.0.0-alpha.27", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.0.0-alpha.27.tgz", - "integrity": "sha512-cqfVLS+346P/Mpj2RpDrBv0P4p2zZhWWvfY5fuWrXNR/K38HaAGEkeOwb47hIpQP9Jr/TIxjZ2/sNMQwdXuGMg==" - }, - "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" - }, - "prop-types": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz", - "integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==", - "requires": { - "loose-envify": "^1.3.1", - "object-assign": "^4.1.1" - } - } - } - }, "math-random": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz", @@ -12938,9 +12867,9 @@ "dev": true }, "memoize-one": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", - "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" }, "memory-fs": { "version": "0.4.1", @@ -14250,7 +14179,8 @@ "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true }, "picocolors": { "version": "1.0.0", @@ -15177,6 +15107,7 @@ "version": "3.4.1", "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "dev": true, "requires": { "performance-now": "^2.1.0" } @@ -15273,20 +15204,6 @@ "resolved": "https://registry.npmjs.org/react-attr-converter/-/react-attr-converter-0.3.1.tgz", "integrity": "sha512-dSxo2Mn6Zx4HajeCeQNLefwEO4kNtV/0E682R1+ZTyFRPqxDa5zYb5qM/ocqw9Bxr/kFQO0IUiqdV7wdHw+Cdg==" }, - "react-beautiful-dnd": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz", - "integrity": "sha512-87It8sN0ineoC3nBW0SbQuTFXM6bUqM62uJGY4BtTf0yzPl8/3+bHMWkgIe0Z6m8e+gJgjWxefGRVfpE3VcdEg==", - "requires": { - "@babel/runtime": "^7.8.4", - "css-box-model": "^1.2.0", - "memoize-one": "^5.1.1", - "raf-schd": "^4.0.2", - "react-redux": "^7.1.1", - "redux": "^4.0.4", - "use-memo-one": "^1.1.1" - } - }, "react-dom": { "version": "16.14.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", @@ -15301,7 +15218,7 @@ "react-double-scrollbar": { "version": "0.0.15", "resolved": "https://registry.npmjs.org/react-double-scrollbar/-/react-double-scrollbar-0.0.15.tgz", - "integrity": "sha1-6RWrjLO5WYdwdfSUNt6/2wQoj+Q=" + "integrity": "sha512-dLz3/WBIpgFnzFY0Kb4aIYBMT2BWomHuW2DH6/9jXfS6/zxRRBUFQ04My4HIB7Ma7QoRBpcy8NtkPeFgcGBpgg==" }, "react-is": { "version": "16.13.1", @@ -15309,25 +15226,22 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "react-redux": { - "version": "7.2.4", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.4.tgz", - "integrity": "sha512-hOQ5eOSkEJEXdpIKbnRyl04LhaWabkDPV+Ix97wqQX3T3d2NQ8DUblNXXtNMavc7DpswyQM6xfaN4HQDKNY2JA==", + "version": "7.2.8", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.8.tgz", + "integrity": "sha512-6+uDjhs3PSIclqoCk0kd6iX74gzrGc3W5zcAjbrFgEdIjRSQObdIwfx80unTkVUYvbQ95Y8Av3OvFHq1w5EOUw==", "requires": { - "@babel/runtime": "^7.12.1", - "@types/react-redux": "^7.1.16", + "@babel/runtime": "^7.15.4", + "@types/react-redux": "^7.1.20", "hoist-non-react-statics": "^3.3.2", "loose-envify": "^1.4.0", "prop-types": "^15.7.2", - "react-is": "^16.13.1" + "react-is": "^17.0.2" }, "dependencies": { - "@babel/runtime": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.0.tgz", - "integrity": "sha512-JELkvo/DlpNdJ7dlyw/eY7E0suy5i5GQH+Vlxaq1nsNJ+H7f4Vtv3jMeCEgRhZZQFXTjldYfQgv2qmM6M1v5wA==", - "requires": { - "regenerator-runtime": "^0.13.4" - } + "react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" } } }, @@ -15611,9 +15525,9 @@ "dev": true }, "redux": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.0.tgz", - "integrity": "sha512-uI2dQN43zqLWCt6B/BMGRMY6db7TTY4qeHHfGeKb3EOhmOKjU3KdWvNLJyqaHRksv/ErdNH7cFZWg9jXtewy4g==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.0.tgz", + "integrity": "sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==", "requires": { "@babel/runtime": "^7.9.2" } @@ -15952,12 +15866,6 @@ "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=", "dev": true }, - "rgbcolor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", - "integrity": "sha1-1lBezbMEplldom+ktDMHMGd1lF0=", - "optional": true - }, "rifm": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/rifm/-/rifm-0.7.0.tgz", @@ -16972,11 +16880,13 @@ "integrity": "sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==", "dev": true }, - "stackblur-canvas": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.5.0.tgz", - "integrity": "sha512-EeNzTVfj+1In7aSLPKDD03F/ly4RxEuF/EX0YcOG0cKoPXs+SLZxDawQbexQDBzwROs4VKLWTOaZQlZkGBFEIQ==", - "optional": true + "static-eval": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz", + "integrity": "sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg==", + "requires": { + "escodegen": "^1.8.1" + } }, "static-extend": { "version": "0.1.2", @@ -17448,12 +17358,6 @@ } } }, - "svg-pathdata": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-5.0.5.tgz", - "integrity": "sha512-TAAvLNSE3fEhyl/Da19JWfMAdhSXTYeviXsLSoDT1UM76ADj5ndwAPX1FKQEgB/gFMPavOy6tOqfalXKUiXrow==", - "optional": true - }, "svgo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", @@ -17774,9 +17678,9 @@ "dev": true }, "tiny-invariant": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz", - "integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz", + "integrity": "sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg==" }, "tiny-warning": { "version": "1.0.3", @@ -17873,11 +17777,6 @@ "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==", "dev": true }, - "tslib": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", - "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==" - }, "tty-browserify": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", @@ -17965,6 +17864,11 @@ } } }, + "underscore": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", + "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==" + }, "unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", @@ -18192,9 +18096,14 @@ "dev": true }, "use-memo-one": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.2.tgz", - "integrity": "sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ==" + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz", + "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==" + }, + "use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==" }, "util": { "version": "0.11.1", @@ -18245,8 +18154,7 @@ "uuid": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "dev": true + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" }, "v8-compile-cache": { "version": "2.3.0", @@ -19650,8 +19558,7 @@ "word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" }, "worker-farm": { "version": "1.7.0", @@ -19850,6 +19757,14 @@ "compress-commons": "^2.1.1", "readable-stream": "^3.4.0" } + }, + "zustand": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.1.1.tgz", + "integrity": "sha512-h4F3WMqsZgvvaE0n3lThx4MM81Ls9xebjvrABNzf5+jb3/03YjNTSgZXeyrvXDArMeV9untvWXRw1tY+ntPYbA==", + "requires": { + "use-sync-external-store": "1.2.0" + } } } } diff --git a/package.json b/package.json index 5de7f5a2..c90cee1e 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@babel/preset-react": "7.0.0", "@babel/runtime": "^7.16.5", "@date-io/date-fns": "^1.3.13", + "@material-table/core": "^5.0.5", "@material-ui/core": "^4.12.3", "@material-ui/icons": "^4.11.2", "@material-ui/lab": "^4.0.0-alpha.60", @@ -29,7 +30,7 @@ "date-fns": "^2.27.0", "dompurify": "^2.3.4", "es6-promise": "^4.2.8", - "material-table": "^1.69.3", + "jsonpath": "^1.1.1", "react": "^16.14.0", "react-dom": "^16.14.0", "react-render-html": "^0.6.0", @@ -52,6 +53,7 @@ "eslint": "^7.32.0", "eslint-plugin-compat": "^3.13.0", "eslint-plugin-react": "^7.28.0", + "eslint-plugin-react-hooks": "^4.6.0", "filemanager-webpack-plugin": "^2.0.5", "html-webpack-plugin": "^4.5.2", "jest": "^25.3.0", diff --git a/patientsearch/api.py b/patientsearch/api.py index fcf07b8e..17ed9411 100644 --- a/patientsearch/api.py +++ b/patientsearch/api.py @@ -195,14 +195,16 @@ def validate_token(): valid=True, access_expires_in=access_token["exp"] - now, refresh_expires_in=refresh_token["exp"] - now, + access_token=access_token, ) @api_blueprint.route("/favicon.ico") def favicon(): + favicon = "_".join((current_app.config.get("PROJECT_NAME"), "favicon.ico")) return send_from_directory( current_app.config.get("STATIC_DIR") or current_app.static_folder, - "favicon.ico", + favicon, mimetype="image/vnd.microsoft.icon", ) @@ -251,9 +253,9 @@ def resource_bundle(resource_type): return jsonify_abort(status_code=400, message=str(error)) -@api_blueprint.route("/fhir/", methods=["POST"]) +@api_blueprint.route("/fhir/", methods=["POST", "PUT"]) def post_resource(resource_type): - """Delegate request to POST given resource in post body to HAPI + """Delegate request to PUT/POST given resource in post body to HAPI NB not decorated with `@oidc.require_login` as that does an implicit redirect. Client should watch for 401 and redirect appropriately. @@ -273,10 +275,11 @@ def post_resource(resource_type): f"{resource['resourceType']} != {resource_type}" ) - method = "POST" + method = request.method audit_HAPI_change( user_info=current_user_info(token), method=method, + params=request.args, resource=resource, resource_type=resource_type, ) @@ -284,6 +287,7 @@ def post_resource(resource_type): HAPI_request( token=token, method=method, + params=request.args, resource_type=resource_type, resource=resource, ) diff --git a/patientsearch/audit.py b/patientsearch/audit.py index defec930..73c98902 100644 --- a/patientsearch/audit.py +++ b/patientsearch/audit.py @@ -36,7 +36,7 @@ def audit_entry(message, level="info", extra=None): def audit_HAPI_change( - user_info, method, resource=None, resource_type=None, resource_id=None + user_info, method, params=None, resource=None, resource_type=None, resource_id=None ): rt = resource_type or resource and resource.get("resourceType") id = resource_id or resource and resource.get("_id", "") @@ -48,4 +48,6 @@ def audit_HAPI_change( elif resource: extra["resource"] = resource + if params: + extra["params"] = params audit_entry(message=msg, extra=extra) diff --git a/patientsearch/config.py b/patientsearch/config.py index 62437117..46d1cc3d 100644 --- a/patientsearch/config.py +++ b/patientsearch/config.py @@ -14,6 +14,10 @@ def load_json_config(potential_json_string): return json.loads(potential_json_string) +APPLICATION_TITLE = os.getenv( + "APPLICATION_TITLE", "Clinical Opioid Summary with Rx Integration" +) + ENABLE_INACTIVITY_TIMEOUT = ( os.getenv("ENABLE_INACTIVITY_TIMEOUT", "true").lower() == "true" ) @@ -21,6 +25,7 @@ def load_json_config(potential_json_string): "FORBIDDEN_TEXT", "Your account is not authorized for access, please contact an administrator", ) + LANDING_INTRO = os.getenv("LANDING_INTRO", "") LANDING_BUTTON_TEXT = os.getenv("LANDING_BUTTON_TEXT", "") LANDING_BODY = os.getenv("LANDING_BODY", "") @@ -56,9 +61,8 @@ def load_json_config(potential_json_string): EXTERNAL_FHIR_API = os.getenv("EXTERNAL_FHIR_API", "") MAP_API = os.getenv("MAP_API") - -SOF_CLIENT_LAUNCH_URL = os.getenv("SOF_CLIENT_LAUNCH_URL") SOF_HOST_FHIR_URL = os.getenv("SOF_HOST_FHIR_URL") +SOF_CLIENTS = json.loads(os.getenv("SOF_CLIENTS", "[]")) # build flask-oidc config from our own granular environment variables, if present if os.getenv("OIDC_CLIENT_ID"): @@ -82,5 +86,6 @@ def load_json_config(potential_json_string): OIDC_ID_TOKEN_COOKIE_SECURE = False OIDC_REQUIRE_VERIFIED_EMAIL = False OIDC_SCOPES = ["email", "openid", "roles"] +PROJECT_NAME = os.getenv("PROJECT_NAME", "COSRI") REQUIRED_ROLES = json.loads(os.getenv("REQUIRED_ROLES", "[]")) UDS_LAB_TYPES = json.loads(os.getenv("UDS_LAB_TYPES", "[]")) diff --git a/patientsearch/models/sync.py b/patientsearch/models/sync.py index f846a77a..a63bb18f 100644 --- a/patientsearch/models/sync.py +++ b/patientsearch/models/sync.py @@ -71,9 +71,13 @@ def HAPI_request( current_app.logger.exception(error) raise RuntimeError("EMR FHIR store inaccessible") elif VERB == "POST": - resp = requests.post(url, auth=BearerAuth(token), json=resource, timeout=30) + resp = requests.post( + url, auth=BearerAuth(token), params=params, json=resource, timeout=30 + ) elif VERB == "PUT": - resp = requests.put(url, auth=BearerAuth(token), json=resource, timeout=30) + resp = requests.put( + url, auth=BearerAuth(token), params=params, json=resource, timeout=30 + ) elif VERB == "DELETE": # Only enable deletion of resource by id if not resource_id: @@ -193,24 +197,28 @@ def different(src, dest): return internal_patient else: internal_patient["identifier"] = src_patient["identifier"] + params = patient_as_search_params(internal_patient) return HAPI_request( token=token, method="PUT", + params=params, resource_type="Patient", resource=internal_patient, resource_id=internal_patient["id"], ) -def internal_patient_search(token, patient): - """Look up given patient from "internal" HAPI store, returns bundle""" +def patient_as_search_params(patient): + """Generate HAPI search params from patient resource""" # Use same parameters sent to external src looking for existing Patient - # Note FHIR uses list for 'given', common parameter use defines just one + # Note FHIR uses list for 'name' and 'given', common parameter use defines just one search_map = ( ("name.family", "family", ""), + ("name[0].family", "family", ""), ("name.given", "given", ""), ("name.given[0]", "given", ""), + ("name[0].given[0]", "given", ""), ("birthDate", "birthdate", "eq"), ) search_params = {} @@ -219,9 +227,14 @@ def internal_patient_search(token, patient): match = json_search(path, patient) if match and isinstance(match, str): search_params[queryterm] = compstr + match + return search_params + +def internal_patient_search(token, patient): + """Look up given patient from "internal" HAPI store, returns bundle""" + params = patient_as_search_params(patient) return HAPI_request( - token=token, method="GET", resource_type="Patient", params=search_params + token=token, method="GET", resource_type="Patient", params=params ) diff --git a/patientsearch/src/js/App.js b/patientsearch/src/js/App.js deleted file mode 100644 index 364bfae7..00000000 --- a/patientsearch/src/js/App.js +++ /dev/null @@ -1,49 +0,0 @@ -import "core-js/stable"; -import "regenerator-runtime/runtime"; -import React, { Component } from "react"; -import CssBaseline from "@material-ui/core/CssBaseline"; -import { ThemeProvider } from "@material-ui/styles"; -import Header from "./components/Header"; -import PatientListTable from "./components/PatientListTable"; -import TimeoutModal from "./components/TimeoutModal.js"; -import SystemBanner from "./components/SystemBanner"; -import Version from "./components/Version"; -import SettingContextProvider from "./context/SettingContextProvider"; -import theme from "./context/theme"; -import "../styles/app.scss"; - -export default class App extends Component { - constructor(props) { - super(props); - this.state = { hasError: false }; - } - - static getDerivedStateFromError() { - //force rendering of fallback UI after an error has been thrown by app - return { hasError: true }; - } - - render () { - if (this.state.hasError) { - return
-

Application Error - Something went wrong.

-

See console for detail

-
Refresh
-
; - } - return ( - - - - - -
- - - - - - - ); - } -} diff --git a/patientsearch/src/js/Landing.js b/patientsearch/src/js/Landing.js deleted file mode 100644 index 9d3484ab..00000000 --- a/patientsearch/src/js/Landing.js +++ /dev/null @@ -1,25 +0,0 @@ -import React from "react"; -import { render } from "react-dom"; -import CssBaseline from "@material-ui/core/CssBaseline"; -import { ThemeProvider } from "@material-ui/styles"; -import Header from "./components/Header"; -import Info from "./components/Info"; -import Version from "./components/Version"; -import SettingContextProvider from "./context/SettingContextProvider"; -import theme from "./context/theme"; -import "../styles/app.scss"; - -// entry point for pre-authenticated access -render( - - - - -
-
- - -
-
-
-
, document.getElementById("content")); diff --git a/patientsearch/src/js/Logout.js b/patientsearch/src/js/Logout.js deleted file mode 100644 index 1a72bcc0..00000000 --- a/patientsearch/src/js/Logout.js +++ /dev/null @@ -1,52 +0,0 @@ -import React from "react"; -import { render } from "react-dom"; -import CssBaseline from "@material-ui/core/CssBaseline"; -import { ThemeProvider } from "@material-ui/styles"; -import Button from "@material-ui/core/Button"; -import Typography from "@material-ui/core/Typography"; -import Alert from "./components/Alert"; -import Header from "./components/Header"; -import {getUrlParameter} from "./components/Utility"; -import SettingContextProvider from "./context/SettingContextProvider"; -import { getAppSettings } from "./context/SettingContextProvider"; -import theme from "./context/theme"; -import "../styles/app.scss"; - -// Error message, e.g. forbidden error -const AlertMessage = () => { - if (!getUrlParameter("forbidden")) return ""; // look for forbidden message for now, can be others as well - const appSettings = getAppSettings(); - const [message, setMessage] = React.useState(""); - const [ready, setReady] = React.useState(false); - React.useEffect(() => { - if (appSettings && appSettings["FORBIDDEN_TEXT"]) { - setMessage(appSettings["FORBIDDEN_TEXT"]); - setReady(true); - } - }, [appSettings]); - return
{ready &&
}
; -}; -const getMessage = () => { - if (getUrlParameter("user_initiated")) return "You have been logged out as requested."; - if (getUrlParameter("timeout")) return "Your session has expired. For security purposes, we recommend closing your browser window. You can always log back in."; - return "You have been logged out."; -}; -// logout entry point -render( - - - - -
-
- - {getMessage()} - - -
- -
- - - , -document.getElementById("content")); diff --git a/patientsearch/src/js/components/Agreement.js b/patientsearch/src/js/components/Agreement.js index c9d57af8..05f47042 100644 --- a/patientsearch/src/js/components/Agreement.js +++ b/patientsearch/src/js/components/Agreement.js @@ -32,94 +32,94 @@ import { getShortDateFromISODateString, isAdult, padDateString, -} from "./Utility"; -import theme from "../context/theme"; +} from "../helpers/utility"; +import theme from "../themes/theme"; const LOINC_SYSTEM_URL = "https://loinc.org"; const CONTRACT_CODE = "94136-9"; const useStyles = makeStyles({ container: { paddingLeft: theme.spacing(3), paddingRight: theme.spacing(3), - paddingTop: theme.spacing(1) + paddingTop: theme.spacing(1), }, contentContainer: { - position: "relative" + position: "relative", }, addContainer: { position: "relative", marginBottom: theme.spacing(2), - padding: theme.spacing(2) + padding: theme.spacing(2), }, addTitle: { display: "inline-block", color: theme.palette.dark.main, fontWeight: 500, borderBottom: `2px solid ${theme.palette.primary.lightest}`, - marginBottom: theme.spacing(2.5) + marginBottom: theme.spacing(2.5), }, buttonsContainer: { marginTop: theme.spacing(2), - position: "relative" + position: "relative", }, progressContainer: { - position: "absolute", - left: 0, - right: 0, - top: 0, - bottom: 0, - background: "hsl(0deg 0% 100% / 80%)", - zIndex: 99 + position: "absolute", + left: 0, + right: 0, + top: 0, + bottom: 0, + background: "hsl(0deg 0% 100% / 80%)", + zIndex: 99, }, progressIcon: { position: "absolute", top: "15%", - left: "15%" + left: "15%", }, addButton: { - marginRight: theme.spacing(1) + marginRight: theme.spacing(1), }, editInput: { - width: theme.spacing(10) + width: theme.spacing(10), }, dateInput: { minWidth: "248px", }, dateLabel: { fontSize: "12px", - marginBottom: theme.spacing(0.25) + marginBottom: theme.spacing(0.25), }, historyContainer: { position: "relative", marginBottom: theme.spacing(2), padding: theme.spacing(2), - minHeight: theme.spacing(9) + minHeight: theme.spacing(9), }, historyTitle: { display: "inline-block", color: theme.palette.dark.main, fontWeight: 500, borderBottom: `2px solid ${theme.palette.primary.lightest}`, - marginBottom: theme.spacing(1) + marginBottom: theme.spacing(1), }, errorContainer: { maxWidth: "100%", marginTop: theme.spacing(3), }, totalEntriesContainer: { - marginTop: theme.spacing(1) + marginTop: theme.spacing(1), }, expandIcon: { marginLeft: theme.spacing(2), verticalAlign: "middle", - fontSize: "12px" + fontSize: "12px", }, endIcon: { marginLeft: "-4px", - position: "relative" + position: "relative", }, tableContainer: { - position: "relative" - } + position: "relative", + }, }); export default function Agreement(props) { @@ -127,17 +127,38 @@ export default function Agreement(props) { const [date, setDate] = React.useState(null); const [editDate, setEditDate] = React.useState(null); const [editMode, setEditMode] = React.useState(false); - const [lastEntryId, setLastEntryId] = React.useState(null); - const [lastAgreementDate, setLastAgreementDate] = React.useState(""); + const lastEntryReducer = (state, action) => { + if (action.type == "reset") { + return { + id: null, + date: "", + }; + } + if (action.type === "update") { + return { + ...state, + ...action.data, + }; + } + return state; + }; + const [lastEntry, lastEntryDispatch] = React.useReducer(lastEntryReducer, { + id: null, + date: "", + }); const [dateInput, setDateInput] = React.useState(null); const [addInProgress, setAddInProgress] = React.useState(false); const [updateInProgress, setUpdateInProgress] = React.useState(false); const [historyInitialized, setHistoryInitialized] = React.useState(false); const [history, setHistory] = React.useState([]); - const [showHistory, setShowHistory] = React.useState(false); + const [expandHistory, setExpandHistory] = React.useState(false); const [error, setError] = React.useState(""); const [snackOpen, setSnackOpen] = React.useState(false); - const rowData = props.rowData ? props.rowData : {}; + const { rowData } = props; + const getPatientId = React.useCallback( + () => (rowData ? rowData.id : ""), + [rowData] + ); const clearDate = () => { setDate(null); setDateInput(""); @@ -148,11 +169,7 @@ export default function Agreement(props) { }; const clearHistory = () => { setHistory([]); - setLastAgreementDate(""); - setLastEntryId(null); - }; - const resetEdits = () => { - setEditDate(""); + lastEntryDispatch({ type: "reset" }); }; const hasValues = () => { return date; @@ -165,27 +182,27 @@ export default function Agreement(props) { const resourceId = params.id || ""; const contractDate = params.date || dateInput; return { - "id": resourceId, - "type": { - "coding": [ + id: resourceId, + type: { + coding: [ { - "system": params.system ? params.system : LOINC_SYSTEM_URL, - "code": params.code ? params.code: CONTRACT_CODE, - "display": "Controlled substance agreement", + system: params.system ? params.system : LOINC_SYSTEM_URL, + code: params.code ? params.code : CONTRACT_CODE, + display: "Controlled substance agreement", }, ], }, - "subject": { - "reference": "Patient/"+(params.patientId ? params.patientId : rowData.id) + subject: { + reference: + "Patient/" + (params.patientId ? params.patientId : getPatientId()), }, - "resourceType": "DocumentReference", - "date": padDateString(contractDate) + resourceType: "DocumentReference", + date: padDateString(contractDate), }; - }; const handleUpdate = (params, callback) => { params = params || {}; - callback = callback || function() {}; + callback = callback || function () {}; const resourceId = params.id || null; const method = params.method || "POST"; const contractDate = params.date || dateInput; @@ -196,23 +213,27 @@ export default function Agreement(props) { } setError(""); let resource = submitDataFormatter(params); - fetchData("/fhir/DocumentReference"+(resourceId?"/"+resourceId:""), { - method: method, - headers: { - Accept: "application/json", - "Content-Type": "application/json", - cache: "no-cache" + fetchData( + "/fhir/DocumentReference" + (resourceId ? "/" + resourceId : ""), + { + method: method, + headers: { + Accept: "application/json", + "Content-Type": "application/json", + cache: "no-cache", + }, + body: JSON.stringify(resource), }, - body: JSON.stringify(resource) - }, (e) => { - if (e) { - handleSubmissionError(); + (e) => { + if (e) { + handleSubmissionError(); + } + callback(e); } - callback(e); - }) + ) .then(() => { setSnackOpen(true); - setTimeout(() => getHistory(callback), 50); + setTimeout(() => getHistory(callback), 150); }) .catch((e) => { console.log("error submtting request ", e); @@ -239,25 +260,39 @@ export default function Agreement(props) { }; const handleEditSave = (params) => { params = params || {}; - if (!Object.keys(params).length) params = { - id: lastEntryId, - date: editDate - }; + if (!Object.keys(params).length) + params = { + id: lastEntry.id, + date: editDate || lastEntry.date, + }; setUpdateInProgress(true); - handleUpdate({...params, ...{ - method:"PUT" - }}, () => setTimeout(setUpdateInProgress(false), 350)); + handleUpdate( + { + ...params, + ...{ + method: "PUT", + }, + }, + () => setTimeout(setUpdateInProgress(false), 250) + ); }; const handleDelete = (params) => { params = params || {}; - if (!Object.keys(params).length) params = { - id: lastEntryId, - date: editDate - }; + if (!Object.keys(params).length) + params = { + id: lastEntry.id, + date: editDate ? editDate : lastEntry.date, + }; setUpdateInProgress(true); - handleUpdate({...params, ...{ - method:"DELETE" - }}, () => setTimeout(setUpdateInProgress(false), 350)); + handleUpdate( + { + ...params, + ...{ + method: "DELETE", + }, + }, + () => setTimeout(setUpdateInProgress(false), 250) + ); }; const handleSubmissionError = () => { setError("Data submission failed. Unable to process your request."); @@ -267,123 +302,135 @@ export default function Agreement(props) { const hasHistory = () => { return history && history.length > 0; }; - const getHistory = (callback) => { - callback = callback || function() {}; - if (!rowData || !rowData.id) { - setHistoryInitialized(true); - callback(); - return []; - } - setHistoryInitialized(false); - /* - * retrieve agreement history - */ - sendRequest( - "/fhir/DocumentReference?patient=" + rowData.id + "&_sort=-date", - { nocache: true } - ).then( - (response) => { - let data = null; - try { - data = JSON.parse(response); - } catch (e) { - console.log("Error parsing pain agreement request data ", e); - } - if (!data || !data.entry || !data.entry.length) { - clearHistory(); - callback(); - setTimeout(() => { + const getHistory = React.useCallback( + (callback) => { + callback = callback || function () {}; + if (!rowData) { + setHistoryInitialized(true); + callback(); + return []; + } + setHistoryInitialized(false); + /* + * retrieve agreement history + */ + sendRequest( + "/fhir/DocumentReference?patient=" + rowData.id + "&_sort=-date", + { nocache: true } + ).then( + (response) => { + let data = null; + try { + data = JSON.parse(response); + } catch (e) { + console.log("Error parsing pain agreement request data ", e); + } + if (!data || !data.entry || !data.entry.length) { + clearHistory(); + callback(); setEditMode(false); setHistoryInitialized(true); - }, 300); - return; - } - let agreementData = data.entry.filter((item) => { - let resource = item.resource; - if (!resource) return false; - if ( - !resource.type || - !resource.type.coding || - !resource.type.coding.length - ) - return false; - return resource.type.coding[0].code === CONTRACT_CODE; - }); - agreementData = agreementData.sort(function (a, b) { - return dateTimeCompare(a.resource.date, b.resource.date); - }); - if (agreementData.length) { - const formattedData = createHistoryData(agreementData); - resetEdits(); - setHistory(formattedData); - setLastEntryId(formattedData[0].id); - setLastAgreementDate(formattedData[0].date); - } else clearHistory(); - setTimeout(() => { + return; + } + let agreementData = data.entry.filter((item) => { + let resource = item.resource; + if (!resource) return false; + if ( + !resource.type || + !resource.type.coding || + !resource.type.coding.length + ) + return false; + return resource.type.coding[0].code === CONTRACT_CODE; + }); + agreementData = agreementData.sort(function (a, b) { + return dateTimeCompare(a.resource.date, b.resource.date); + }); + if (agreementData.length) { + const formattedData = createHistoryData(agreementData); + setEditDate(null); + setHistory(formattedData); + lastEntryDispatch({ + type: "update", + data: { + id: formattedData[0].id, + date: formattedData[0].date, + }, + }); + } else clearHistory(); setEditMode(false); setHistoryInitialized(true); - }, 300); - callback(); - }, - (error) => { - setHistoryInitialized(true); - callback(error); - console.log("Failed to retrieve data", error); - } - ); - return ""; - }; - const createHistoryData = (data) => { - if (!data) return []; - return data.map((item,index) => { + callback(); + }, + (error) => { + setHistoryInitialized(true); + callback(error); + console.log("Failed to retrieve data", error); + } + ); + return ""; + }, + [rowData, createHistoryData] + ); + const createHistoryData = React.useCallback( + (data) => { + if (!data) return []; + return data.map((item, index) => { const resource = item.resource; if (!resource) return {}; let date = getShortDateFromISODateString(resource.date); return { - id: resource.id, - date: date, - index: index, - patientId: rowData.id, - system: LOINC_SYSTEM_URL, - code: CONTRACT_CODE + id: resource.id, + date: date, + index: index, + patientId: getPatientId(), + system: LOINC_SYSTEM_URL, + code: CONTRACT_CODE, }; - }); - }; - const displayHistory = () => { + }); + }, + [getPatientId] + ); + const displayMostRecent = () => { if (!hasHistory()) return ""; return ( "Last controlled substance agreement signed on " + - lastAgreementDate + + lastEntry.date + "" ); }; const displayEditHistory = () => { if (!hasHistory()) return null; return ( -
Last controlled substance agreement signed on handleEditChange(e)} - handleKeyDown={() => handleEditSave()} - error={hasError()}>
+
+ Last controlled substance agreement signed on{" "} + handleEditChange(e)} + handleKeyDown={() => handleEditSave()} + error={hasError()} + > +
); }; const handleEnableEditMode = () => { - setEditDate(lastAgreementDate); + setEditDate(lastEntry.date); setError(""); setEditMode(true); }; const handleDisableEditMode = () => { setError(""); - resetEdits(); + setEditDate(null); setEditMode(false); }; const handleEditChange = (event) => { setEditDate(event.target.value); }; const isValidEditDate = () => { - let dateObj = new Date(editDate).setHours(0,0,0,0); - let today = new Date().setHours(0,0,0,0); + let dateObj = new Date(editDate).setHours(0, 0, 0, 0); + let today = new Date().setHours(0, 0, 0, 0); return isValid(dateObj) && !(dateObj > today); }; const handleSnackClose = (event, reason) => { @@ -394,22 +441,27 @@ export default function Agreement(props) { }; const columns = [ { - field: "id", - hidden: true + field: "id", + hidden: true, }, { - title: "Agreement Date", - field: "date", - emptyValue: "--", - cellStyle: { - "padding": "4px 24px 4px 16px" - }, - editComponent: params => params.onChange(e.target.value)}> + title: "Agreement Date", + field: "date", + emptyValue: "--", + cellStyle: { + padding: "4px 24px 4px 16px", + }, + editComponent: (params) => ( + params.onChange(e.target.value)} + > + ), }, ]; React.useEffect(() => { getHistory(); - }, []); + }, [getHistory]); return (
@@ -425,12 +477,18 @@ export default function Agreement(props) {

{`Controlled Substance Agreement for ${rowData.first_name} ${rowData.last_name}`}

{/* add new agreement UI */} - - Add New + + Add New {/* order date field */} - Agreement Date + + Agreement Date + { setDateInput(dateString); if (!event || !isValid(event)) { - if (event && String(dateInput).replace(/[-_]/g, "").length >= 8) + if ( + event && + String(dateInput).replace(/[-_]/g, "").length >= 8 + ) setDate(event); return; } @@ -498,87 +559,143 @@ export default function Agreement(props) { {/* history UI */} - {!historyInitialized &&
- -
} - {updateInProgress &&
- -
} - - Latest Controlled Substance Agreement - - {historyInitialized && hasHistory() &&
- {!editMode && } - {editMode && displayEditHistory()} - handleEditSave()} - handleDelete={() => handleDelete()} - entryDescription={`Controlled substance agreement signed on ${lastAgreementDate}`} - > -
} - {!isAdult(rowData.dob) && !hasHistory() &&
No previously recorded controlled substance agreement
} - {/* alerts */} - {isAdult(rowData.dob) && ( - + {(!historyInitialized || updateInProgress) && ( +
+ +
)} -
- { hasHistory() && historyInitialized && + {historyInitialized && ( + + + Latest Controlled Substance Agreement + + {!hasHistory() && ( +
No previously recorded controlled substance agreement
+ )} + {/* most recent entry */} + {hasHistory() && ( +
+
+ {!editMode && ( + + )} + {editMode && displayEditHistory()} + handleEditSave()} + handleDelete={() => handleDelete()} + entryDescription={`Controlled substance agreement signed on ${lastEntry.date}`} + > +
+ {/* alerts */} + {isAdult(rowData.birth_date) && ( + + )} +
+ )} +
+ )} +
+ {hasHistory() && ( +
- - History + + History
- {history.length} record(s) - {!showHistory && } - {showHistory && + )} + {expandHistory && ( + } + className={classes.expandIcon} + > + Hide + + )}
- {showHistory &&
getHistory()} - onRowDelete={() => getHistory()} - options= { - { + {expandHistory && ( +
+ getHistory()} + onRowDelete={() => getHistory()} + options={{ actionsCellStyle: { width: "80%", - textAlign: "left" - } - } - } - >
} + textAlign: "left", + }, + }} + >
+
+ )}
-
} + + )} {/* submission feedback UI */} - - + +
{error && } @@ -589,5 +706,5 @@ export default function Agreement(props) { } Agreement.propTypes = { - rowData: PropTypes.object.isRequired + rowData: PropTypes.object.isRequired, }; diff --git a/patientsearch/src/js/components/DetailPanel.js b/patientsearch/src/js/components/DetailPanel.js new file mode 100644 index 00000000..c288c5fa --- /dev/null +++ b/patientsearch/src/js/components/DetailPanel.js @@ -0,0 +1,35 @@ +import PropTypes from "prop-types"; +import Paper from "@material-ui/core/Paper"; +import { makeStyles } from "@material-ui/core/styles"; +import theme from "../themes/theme"; + +const useStyles = makeStyles({ + detailPanelWrapper: { + backgroundColor: "#dde7e6", + padding: theme.spacing(0.25), + }, + detailPanelContainer: { + position: "relative", + minHeight: theme.spacing(8), + backgroundColor: "#fbfbfb", + }, +}); + +export default function DetailPanel(props) { + const classes = useStyles(); + return ( +
+ + {props.children} + +
+ ); +} + +DetailPanel.propTypes = { + children: PropTypes.oneOfType([PropTypes.element, PropTypes.array]), +}; diff --git a/patientsearch/src/js/components/DialogBox.js b/patientsearch/src/js/components/DialogBox.js new file mode 100644 index 00000000..91dd3878 --- /dev/null +++ b/patientsearch/src/js/components/DialogBox.js @@ -0,0 +1,60 @@ +import React from "react"; +import PropTypes from "prop-types"; +import Button from "@material-ui/core/Button"; +import Dialog from "@material-ui/core/Dialog"; +import DialogActions from "@material-ui/core/DialogActions"; +import DialogContent from "@material-ui/core/DialogContent"; +import DialogTitle from "@material-ui/core/DialogTitle"; +import { makeStyles } from "@material-ui/core/styles"; +import theme from "../themes/theme"; + +const useStyles = makeStyles({ + diaglogTitle: { + backgroundColor: theme.palette.primary.lightest, + }, + diaglogContent: { + marginTop: theme.spacing(3), + }, +}); + +export default function DialogBox(props) { + const classes = useStyles(); + const [open, setOpen] = React.useState(false); + const handleClose = () => { + setOpen(false); + if (props.onClose) props.onClose(); + }; + React.useEffect(() => { + setOpen(props.open); + }, [props.open]); + return ( + + + {props.title} + + + {props.body} + + + + + + ); +} + +DialogBox.propTypes = { + open: PropTypes.bool, + title: PropTypes.string, + body: PropTypes.element, + onClose: PropTypes.func +}; diff --git a/patientsearch/src/js/components/Dropdown.js b/patientsearch/src/js/components/Dropdown.js index a9286342..956573a6 100644 --- a/patientsearch/src/js/components/Dropdown.js +++ b/patientsearch/src/js/components/Dropdown.js @@ -1,6 +1,5 @@ import React from "react"; import PropTypes from "prop-types"; -import theme from "../context/theme"; import { makeStyles, styled } from "@material-ui/core/styles"; import AddCircleOutlineIcon from "@material-ui/icons/AddCircleOutline"; import Button from "@material-ui/core/Button"; @@ -8,6 +7,7 @@ import ListItemIcon from "@material-ui/core/ListItemIcon"; import Menu from "@material-ui/core/Menu"; import MenuItem from "@material-ui/core/MenuItem"; import Typography from "@material-ui/core/Typography"; +import theme from "../themes/theme"; const useStyles = makeStyles({ menu: { diff --git a/patientsearch/src/js/components/EditButtonGroup.js b/patientsearch/src/js/components/EditButtonGroup.js index f41ddfaa..494136ee 100644 --- a/patientsearch/src/js/components/EditButtonGroup.js +++ b/patientsearch/src/js/components/EditButtonGroup.js @@ -9,7 +9,7 @@ import SaveIcon from "@material-ui/icons/Save"; import Button from "@material-ui/core/Button"; import ButtonGroup from "@material-ui/core/ButtonGroup"; import Modal from "@material-ui/core/Modal"; -import theme from "../context/theme"; +import theme from "../themes/theme"; const useStyles = makeStyles({ buttonGroupContainer: { diff --git a/patientsearch/src/js/components/FilterRow.js b/patientsearch/src/js/components/FilterRow.js index f5f6c10b..b98f430f 100644 --- a/patientsearch/src/js/components/FilterRow.js +++ b/patientsearch/src/js/components/FilterRow.js @@ -14,7 +14,7 @@ import { MuiPickersUtilsProvider, KeyboardDatePicker, } from "@material-ui/pickers"; -import theme from "../context/theme"; +import theme from "../themes/theme"; const useStyles = makeStyles({ row: { @@ -45,7 +45,7 @@ const useStyles = makeStyles({ paddingRight: theme.spacing(1), }, empty: { - width: "20%", + width: "24px", backgroundColor: "#f7f7f7", }, }); @@ -70,7 +70,7 @@ export default function FilterRow(props) { value: lastName, }, { - field: "dob", + field: "birth_date", value: isValid(new Date(dateInput)) ? dateInput : "", }, ]); @@ -88,7 +88,7 @@ export default function FilterRow(props) { value: firstName, }, { - field: "dob", + field: "birth_date", value: isValid(new Date(dateInput)) ? dateInput : "", }, ]); @@ -107,12 +107,12 @@ export default function FilterRow(props) { return { first_name: firstName, last_name: lastName, - dob: dateInput, + birth_date: dateInput, }; }; const clearDate = () => { - setDate(null); setDateInput(""); + setDate(null); props.onFiltersDidChange([ { field: "first_name", @@ -123,16 +123,19 @@ export default function FilterRow(props) { value: lastName, }, { - field: "dob", + field: "birth_date", value: null, }, ]); }; + const handleClear = () => { + clearFields(); + props.onFiltersDidChange(null); + }; const clearFields = () => { setFirstName(""); setLastName(""); clearDate(); - props.onFiltersDidChange(null, true); }; const getLaunchButtonLabel = () => { return props.launchButtonLabel @@ -141,7 +144,7 @@ export default function FilterRow(props) { }; const handleKeyDown = (e) => { if (String(e.key).toLowerCase() === "enter") { - props.launchFunc(e, getFilterData()); + props.launchFunc(getFilterData()); return; } return false; @@ -213,8 +216,9 @@ export default function FilterRow(props) { style={{ order: 2, padding: 0 }} aria-label="Clear date" title="Clear date" + tabIndex={-1} > - + ), @@ -234,7 +238,6 @@ export default function FilterRow(props) { if (!event || !isValid(event)) { if (event && String(dateInput).replace(/[-_]/g, "").length >= 8) setDate(event); - // props.onFilterChanged(3, null, "dob"); props.onFiltersDidChange([ { field: "first_name", @@ -245,14 +248,13 @@ export default function FilterRow(props) { value: lastName, }, { - field: "dob", + field: "birth_date", value: null, }, ]); return; } setDate(event); - // props.onFilterChanged(3, dateString, "dob"); props.onFiltersDidChange([ { field: "first_name", @@ -263,7 +265,7 @@ export default function FilterRow(props) { value: lastName, }, { - field: "dob", + field: "birth_date", value: dateString, }, ]); @@ -276,7 +278,6 @@ export default function FilterRow(props) { {/* toolbar go button */} @@ -293,7 +294,7 @@ export default function FilterRow(props) {
diff --git a/patientsearch/src/js/components/LoadingModal.js b/patientsearch/src/js/components/LoadingModal.js new file mode 100644 index 00000000..e6f70c44 --- /dev/null +++ b/patientsearch/src/js/components/LoadingModal.js @@ -0,0 +1,53 @@ +import React from "react"; +import PropTypes from "prop-types"; +import CircularProgress from "@material-ui/core/CircularProgress"; +import Modal from "@material-ui/core/Modal"; +import { makeStyles } from "@material-ui/core/styles"; +import theme from "../themes/theme"; + +const useStyles = makeStyles({ + flex: { + display: "flex", + alignItems: "center", + justifyContent: "center", + flexWrap: "wrap", + width: "250px", + backgroundColor: "#FFF", + position: "relative", + top: "40%", + border: `1px solid ${theme.palette.primary.main}`, + margin: "auto", + padding: theme.spacing(2), + fontSize: "1.1rem" + }, + loadingText: { + display: "inline-block", + marginRight: theme.spacing(1.5), + }, +}); + +export default function LoadingModal(props) { + const classes = useStyles(); + const [open, setOpen] = React.useState(false); + React.useEffect(() => { + setOpen(props.open); + }, [props.open]); + return ( + +
+ Loading ... + +
+
+ ); +} + +LoadingModal.propTypes = { + open: PropTypes.bool +}; diff --git a/patientsearch/src/js/components/OverdueAlert.js b/patientsearch/src/js/components/OverdueAlert.js index 0c7c08f7..a377da44 100644 --- a/patientsearch/src/js/components/OverdueAlert.js +++ b/patientsearch/src/js/components/OverdueAlert.js @@ -3,14 +3,14 @@ import PropTypes from "prop-types"; import { makeStyles } from "@material-ui/core/styles"; import AssignmentLateIcon from "@material-ui/icons/AssignmentLate"; import Typography from "@material-ui/core/Typography"; -import theme from "../context/theme"; +import theme from "../themes/theme"; import { addYearsToDate, getLocalDateTimeString, getShortDateFromISODateString, isInMonthPeriod, isDateInPast, -} from "./Utility"; +} from "../helpers/utility"; const useStyles = makeStyles({ alertIcon: { diff --git a/patientsearch/src/js/components/OverlayElement.js b/patientsearch/src/js/components/OverlayElement.js new file mode 100644 index 00000000..b893ae6a --- /dev/null +++ b/patientsearch/src/js/components/OverlayElement.js @@ -0,0 +1,31 @@ +import PropTypes from "prop-types"; +import { makeStyles } from "@material-ui/core/styles"; + +const useStyles = makeStyles({ + overlayContainer: { + display: "table", + width: "100%", + height: "100%", + background: "rgb(255 255 255 / 70%)", + }, + overlayElement: { + display: "table-cell", + width: "100%", + height: "100%", + verticalAlign: "middle", + textAlign: "center", + }, +}); + +export default function OverlayElement(props) { + const classes = useStyles(); + return ( +
+
{props.children}
+
+ ); +} + +OverlayElement.propTypes = { + children: PropTypes.oneOfType([PropTypes.element, PropTypes.array]) +}; diff --git a/patientsearch/src/js/components/PatientListTable.js b/patientsearch/src/js/components/PatientListTable.js index a01502c2..7c1c6cc4 100644 --- a/patientsearch/src/js/components/PatientListTable.js +++ b/patientsearch/src/js/components/PatientListTable.js @@ -1,29 +1,36 @@ import React from "react"; import { makeStyles } from "@material-ui/core/styles"; -import MaterialTable from "material-table"; +import jsonpath from "jsonpath"; +import DOMPurify from "dompurify"; +import MaterialTable from "@material-table/core"; import RefreshIcon from "@material-ui/icons/Refresh"; import MoreHorizIcon from "@material-ui/icons/MoreHoriz"; import CircularProgress from "@material-ui/core/CircularProgress"; import Button from "@material-ui/core/Button"; import Container from "@material-ui/core/Container"; -import Modal from "@material-ui/core/Modal"; -import Paper from "@material-ui/core/Paper"; import TablePagination from "@material-ui/core/TablePagination"; import Tooltip from "@material-ui/core/Tooltip"; +import DetailPanel from "./DetailPanel"; +import DialogBox from "./DialogBox"; import Dropdown from "./Dropdown"; import Error from "./Error"; import FilterRow from "./FilterRow"; +import LoadingModal from "./LoadingModal"; +import OverlayElement from "./OverlayElement"; import UrineScreen from "./UrineScreen"; import Agreement from "./Agreement"; -import {tableIcons} from "../context/consts"; -import theme from "../context/theme"; +import { useSettingContext } from "../context/SettingContextProvider"; +import { tableIcons } from "../constants/consts"; +import theme from "../themes/theme"; import { fetchData, getLocalDateTimeString, - getSettings, getUrlParameter, + getRolesFromToken, + getClientsByRequiredRoles, isString, -} from "./Utility"; + validateToken, +} from "../helpers/utility"; const useStyles = makeStyles({ container: { @@ -31,32 +38,24 @@ const useStyles = makeStyles({ marginRight: "auto", marginBottom: theme.spacing(2), marginTop: 148, - maxWidth: "1080px", - }, - overlayContainer: { - display: "table", - width: "100%", - height: "100%", - background: "rgb(255 255 255 / 70%)" - }, - overlayElement: { - display: "table-cell", - width: "100%", - height: "100%", - verticalAlign: "middle", - textAlign: "center" }, filterTable: { marginBottom: theme.spacing(1), marginTop: theme.spacing(20), ["@media (min-width:639px)"]: { - marginTop: 0 - } + marginTop: 0, + }, }, table: { minWidth: 320, maxWidth: "100%", }, + detailPanelCloseButton: { + position: "absolute", + top: theme.spacing(1.5), + right: theme.spacing(6), + color: theme.palette.primary.main, + }, paper: { backgroundColor: theme.palette.background.paper, boxShadow: theme.shadows[5], @@ -70,15 +69,8 @@ const useStyles = makeStyles({ justifyContent: "center", flexWrap: "wrap", }, - modal: { - display: "flex", - alignItems: "center", - justifyContent: "center", - border: 0, - }, - loadingText: { - marginRight: theme.spacing(1.5), - fontSize: "18px", + flexButton: { + marginRight: theme.spacing(1), }, label: { marginRight: theme.spacing(1.5), @@ -89,7 +81,6 @@ const useStyles = makeStyles({ color: "#FFF", fontSize: "12px", borderRadius: "4px", - width: "120px", fontWeight: 600, textTransform: "uppercase", border: 0, @@ -141,46 +132,66 @@ const useStyles = makeStyles({ minWidth: "20px", minHeight: "20px", }, - detailPanelWrapper: { - backgroundColor: "#dde7e6", - padding: theme.spacing(0.25), - }, - detailPanelContainer: { - position: "relative", - minHeight: theme.spacing(8), - backgroundColor: "#fbfbfb", - }, - detailPanelCloseButton: { - position: "absolute", - top: theme.spacing(1.5), - right: theme.spacing(6), - color: theme.palette.primary.main, + moreIcon: { + marginRight: theme.spacing(1), }, }); -let appSettings = {}; let filterIntervalId = 0; export default function PatientListTable() { const classes = useStyles(); - const [settingInitialized, setSettingInitialized] = React.useState(false); - const [initialized, setInitialized] = React.useState(false); + const appSettings = useSettingContext().appSettings; + const [appClients, setAppClients] = React.useState(null); const [data, setData] = React.useState([]); const defaultFilters = { first_name: "", last_name: "", - dob: "", + birth_date: "", + }; + const defaultPagination = { + pageSize: 20, + pageNumber: 0, + prevPageNumber: 0, + disablePrevButton: true, + disableNextButton: true, + totalCount: 0, + nextPageURL: "", + prevPageURL: "", }; + const paginationReducer = (state, action) => { + if (action.type === "empty") { + return { + ...state, + pageNumber: 0, + prevPageNumber: 0, + disablePrevButton: true, + disableNextButton: true, + totalCount: 0, + nextPageURL: "", + prevPageURL: "", + }; + } + if (action.type === "reset") { + return { + ...state, + pageNumber: 0, + nextPageURL: "", + prevPageURL: "", + }; + } + return { + ...state, + ...action.payload, + }; + }; + const [pagination, paginationDispatch] = React.useReducer( + paginationReducer, + defaultPagination + ); const [currentFilters, setCurrentFilters] = React.useState(defaultFilters); - const [pageSize, setPageSize] = React.useState(20); - const [pageNumber, setPageNumber] = React.useState(0); - const [prevPageNumber, setPrevPageNumber] = React.useState(0); - const [disablePrevButton, setDisablePrevButton] = React.useState(true); - const [disableNextButton, setDisableNextButton] = React.useState(true); - const [totalCount, setTotalCount] = React.useState(0); - const [nextPageURL, setNextPageURL] = React.useState(""); - const [prevPageURL, setPrevPageURL] = React.useState(""); const [openLoadingModal, setOpenLoadingModal] = React.useState(false); + const [openLaunchInfoModal, setOpenLaunchInfoModal] = React.useState(false); const [errorMessage, setErrorMessage] = React.useState(""); const [containNoPMPRow, setContainNoPMPRow] = React.useState(false); const [anchorEl, setAnchorEl] = React.useState(false); @@ -191,8 +202,8 @@ export default function PatientListTable() { const tableRef = React.useRef(); const LAUNCH_BUTTON_LABEL = "VIEW"; const CREATE_BUTTON_LABEL = "CREATE"; - const TOOLBAR_ACTION_BUTTON_ID = "toolbarGoButton"; const MORE_MENU_KEY = "MORE_MENU"; + const noCacheParam = { cache: "no-cache" }; const menuItems = [ { text: "Add Urine Tox Screen", @@ -208,253 +219,210 @@ export default function PatientListTable() { const FieldNameMaps = { first_name: "given", last_name: "family", - dob: "birthdate", - lastUpdated: "_lastUpdated", + birth_date: "birthdate", + last_accessed: "_lastUpdated", }; - const columns = [ - //default sort by id in descending order - { field: "id", hidden: true, filtering: false }, - { - title: "First Name", - field: "first_name", - filterPlaceholder: "First Name", - emptyValue: "--", - }, + const default_columns = [ { - title: "Last Name", - field: "last_name", - filterPlaceholder: "Last Name", - emptyValue: "--", + label: "First Name", + expr: "$.name[0].given[0]", }, { - title: "Birth Date", - field: "dob", - filterPlaceholder: "YYYY-MM-DD", - emptyValue: "--", + label: "Last Name", + expr: "$.name[0].family", }, - /* the field for last accessed is patient.meta.lastupdated? */ { - title: "Last Accessed", - field: "lastUpdated", - filtering: false, - align: "center", - defaultSort: "desc", + label: "Birth Date", + expr: "$.birthDate", }, ]; const errorStyle = { display: errorMessage ? "block" : "none" }; const toTop = () => { window.scrollTo(0, 0); }; - const setAppSettings = function (settings) { - appSettings = settings; + const hasAppSettings = () => + appSettings && Object.keys(appSettings).length > 0; + const getAppSettingByKey = (key) => { + if (!hasAppSettings()) return ""; + return appSettings[key]; + }; + const getColumns = () => { + const configColumns = getAppSettingByKey("DASHBOARD_COLUMNS"); + let cols = configColumns ? configColumns : default_columns; + const hasIdField = cols.filter((col) => col.field === "id").length > 0; + //columns must include an id field, add if not present + if (!hasIdField) + cols.push({ + label: "id", + hidden: true, + expr: "$.id", + }); + return cols.map((column) => { + column.title = column.label; + column.field = column.label.toLowerCase().replace(/\s/g, "_"); + column.emptyValue = "--"; + return column; + }); + }; + const columns = getColumns(); + const existsIndata = (rowData) => { + if (!data || !rowData) return false; + return ( + data.filter((item) => { + return parseInt(item.id) === parseInt(rowData.id); + }).length > 0 + ); }; - const getPatientPMPSearchURL = (data) => { - if (data.id && data.identifier) return `/fhir/Patient/${data.id}`; - const dataURL = "/external_search/Patient"; - const params = [ - `subject:Patient.name.given=${data.first_name}`, - `subject:Patient.name.family=${data.last_name}`, - `subject:Patient.birthdate=eq${data.dob}`, - ]; - return `${dataURL}?${params.join("&")}`; + const addDataRow = (rowData) => { + if (!rowData || !rowData.id) return false; + let newData = formatData(rowData); + if (newData && !existsIndata(newData[0])) { + setData([newData[0], ...data]); + } }; - const getLaunchBaseURL = function () { - return appSettings["SOF_CLIENT_LAUNCH_URL"]; + const needExternalAPILookup = () => { + return getAppSettingByKey("EXTERNAL_FHIR_API"); }; - const getISS = function () { - return appSettings["SOF_HOST_FHIR_URL"]; + const getPatientSearchURL = (data) => { + if (needExternalAPILookup()) { + const dataURL = "/external_search/Patient"; + const params = [ + `subject:Patient.name.given=${data.first_name}`, + `subject:Patient.name.family=${data.last_name}`, + `subject:Patient.birthdate=eq${data.birth_date}`, + ]; + return `${dataURL}?${params.join("&")}`; + } + return `/fhir/Patient?given=${String( + data.first_name + ).trim()}&family=${String(data.last_name).trim()}&birthdate=${ + data.birth_date + }`; }; - const getLaunchURL = function (patientId) { + const getLaunchURL = (patientId, launchParams) => { if (!patientId) { console.log("Missing information: patient Id"); return ""; } - let baseURL = getLaunchBaseURL(); - let iss = getISS(); + launchParams = launchParams || {}; + const baseURL = launchParams["launch_url"]; + const iss = getAppSettingByKey("SOF_HOST_FHIR_URL"); if (!baseURL || !iss) { console.log("Missing ISS launch base URL"); return ""; } - let launchParam = btoa(JSON.stringify({ b: patientId })); - return `${baseURL}?launch=${launchParam}&iss=${iss}`; + return `${baseURL}?patient=${patientId}&launch=${btoa( + JSON.stringify({ a: 1, b: patientId }) + )}&iss=${encodeURIComponent(iss)}`; }; - const noCacheParam = { cache: "no-cache" }; - - const existsIndata = function (rowData) { - if (!data) return false; - if (!rowData) return false; - return ( - data.filter((item) => { - return parseInt(item.id) === parseInt(rowData.id); - }).length > 0 - ); + const hasSoFClients = () => { + return appClients && appClients.length > 0; }; - const addDataRow = function (rowData) { - if (!rowData || !rowData.id) return false; - let newData = formatData(rowData); - if (newData && !existsIndata(newData[0])) { - setData([newData[0], ...data]); - } + const hasMultipleSoFClients = () => { + return appClients && appClients.length > 1; }; - const handleExpiredSession = function () { + const handleLaunchApp = (rowData, launchParams) => { + + if (!launchParams) { + // if only one SoF client, use its launch params + launchParams = + hasSoFClients() && appClients.length === 1 ? appClients[0] : null; + } + // if no launch params specifieid, need to handle multiple SoF clients that can be launched + // open a dialog here so user can select which one to launch? + if (!launchParams && hasMultipleSoFClients()) { + setCurrentRow(rowData); + setOpenLoadingModal(false); + setOpenLaunchInfoModal(true); + return; + } + + let launchURL = getLaunchURL(rowData.id, launchParams); + if (!launchURL) { + handleLaunchError( + "Unable to launch application. Missing launch URL. Missing configurations." + ); + return false; + } + setOpenLoadingModal(true); sessionStorage.clear(); - setTimeout(() => { - // /home is a protected endpoint, the backend will request a new Access Token from Keycloak if able, else prompt a user to log in again - window.location = "/home"; - }, 0); + window.location = launchURL; + }; + const handleLaunchError = (message) => { + setErrorMessage(message || "Unable to launch application."); + setOpenLoadingModal(false); + toTop(); + return false; }; - const handleSearch = function (event, rowData) { + const onLaunchDialogClose = () => { + setOpenLaunchInfoModal(false); + handleRefresh(); + }; + const handleSearch = (rowData) => { if (!rowData) { - console.log("No valid data to perform patient search"); + handleLaunchError("No patient data to proceed."); return false; } + // search parameters + const searchBody = rowData.resource + ? JSON.stringify(rowData.resource) + : JSON.stringify({ + resourceType: "Patient", + name: [ + { + family: rowData.last_name.trim(), + given: [rowData.first_name.trim()], + }, + ], + birthDate: rowData.birth_date, + }); + // error message when no result returned + const noResultErrorMessage = needExternalAPILookup() + ? "
The patient was not found in the PMP. This could be due to:
  • No previous controlled substance medications dispensed
  • Incorrect spelling of name or incorrect date of birth.
Please double check name spelling and date of birth.
" + : "No matched patient found"; + // error message for API error + const fetchErrorMessage = needExternalAPILookup() + ? "

COSRI is unable to return PMP information. This may be due to PMP system being down or a problem with the COSRI connection to PMP.

" + : "Server error when looking up patient"; setOpenLoadingModal(true); - setErrorMessage(""); - const urls = [getPatientPMPSearchURL(rowData), "./validate_token"]; - Promise.allSettled([ - fetch(urls[0], { + fetchData( + getPatientSearchURL(rowData), + { ...{ method: "PUT", headers: { Accept: "application/json", - "Content-Type": "application/json" + "Content-Type": "application/json", }, - body: rowData.resource ? JSON.stringify(rowData.resource) : null + body: searchBody, }, ...noCacheParam, - }), - fetch(urls[1]), - ]) - .then(async ([searchResult, tokenResult]) => { - const searchResponse = searchResult.value; - const tokenResponse = tokenResult.value; - - //dealing with unauthorized error, status code = 401 - if (!searchResponse.ok || !tokenResponse.ok) { - if (parseInt(searchResponse.status) === 401 || - parseInt(tokenResponse.status) === 401) { - //redirect to home - handleExpiredSession(); - throw "Unauthorized"; - } - } - - //fetch returns a simple flag of ok to indicate whether whether an HTTP response’s status code is in the successful range or not - if (!searchResponse.ok) { - //check if error response is text/html first - let responseText = - typeof searchResponse.text !== "undefined" - ? await searchResponse.text() - : ""; - if (!responseText) { - //check if error response is in JSON - responseText = - typeof searchResponse.json !== "undefined" - ? await searchResponse.json() - : ""; - } - throw responseText ? responseText : searchResponse.statusText; //throw error so can be caught later - } - try { - return [await searchResponse.json(), await tokenResponse.json()]; - } catch (e) { - console.log( - "Error processing patient search and validating token ", - e - ); - } - return false; - }) - .then((results) => { - if (!results || !results.length) { - setErrorMessage("Data processing error in [handleSearch]"); - toTop(); - setOpenLoadingModal(false); - return false; - } - if ( - !results[1] || - (results[1] && - (!results[1].valid || - parseInt(results[1].access_expires_in) <= 0 || - parseInt(results[1].refresh_expires_in) <= 0)) - ) { - //invalid token, force redirecting - console.log("Redirecting..."); - handleExpiredSession(); - setOpenLoadingModal(true); - return false; + }, + (e) => handleErrorCallback(e) + ) + .then((result) => { + let response = result; + if (result && result.entry && result.entry[0]) { + response = result.entry[0]; } - let response = results[0]; - //response can be an array or just object now - if (results[0] && results[0].entry && results[0].entry[0]) - response = results[0].entry[0]; - if (!response) { - setErrorMessage( - "
The patient was not found in the PMP. This could be due to:
  • No previous controlled substance medications dispensed
  • Incorrect spelling of name or incorrect date of birth.
Please double check name spelling and date of birth.
" - ); - toTop(); - setOpenLoadingModal(false); + if (!response || !response.id) { + handleLaunchError(noResultErrorMessage); + handleRefresh(); return false; } + //add new table row where applicable try { addDataRow(response); } catch (e) { console.log("Error occurred adding row to table ", e); } - setErrorMessage(""); - let launchURL = ""; - let launchID = response.id; - if (!launchID) { - setErrorMessage( - "
The patient was not found in the PMP. This could be due to:
  • No previous controlled substance medications dispensed
  • Incorrect spelling of name or incorrect date of birth.
Please double check name spelling and date of birth.
" - ); - toTop(); - setOpenLoadingModal(false); - return false; - } - try { - launchURL = rowData.url || getLaunchURL(launchID); - } catch (e) { - setErrorMessage( - "Unable to launch application. Invalid launch URL. Missing configurations." - ); - toTop(); - setOpenLoadingModal(false); - //log error to console - console.log(`Launch URL error: ${e}`); - return false; - } - if (!launchURL) { - setErrorMessage( - "Unable to launch application. Missing launch URL. Missing configurations." - ); - toTop(); - setOpenLoadingModal(false); - return false; - } - setTimeout(function () { - sessionStorage.clear(); - window.location = launchURL; - }, 50); + handleRefresh(); + handleLaunchApp(formatData(response)[0]); }) .catch((e) => { - let returnedError = e; - try { - returnedError = JSON.parse(e); - returnedError = returnedError.message - ? returnedError.message - : returnedError; - } catch (e) { - console.log("error parsing error message ", e); - } - setErrorMessage( - `

COSRI is unable to return PMP information. This may be due to PMP system being down or a problem with the COSRI connection to PMP.

Error returned from the system: ${returnedError}

` - ); //log error to console console.log(`Patient search error: ${e}`); - toTop(); - setOpenLoadingModal(false); + handleLaunchError(fetchErrorMessage + `

See console for detail.

`); }); }; const formatData = (data) => { @@ -464,36 +432,38 @@ export default function PatientListTable() { } return data && Array.isArray(data) ? data.map((item) => { - let source = item.resource ? item.resource : item; - let patientId = source && source["id"] ? source["id"] : ""; - return { - first_name: - source && source.name && source.name[0] - ? source.name[0]["given"][0] - : "", - last_name: - source && source.name && source.name[0] - ? source.name[0]["family"] - : "", - dob: source && source["birthDate"] ? source["birthDate"] : "", - url: getLaunchURL(patientId), - identifier: - source && source.identifier && source.identifier.length - ? source.identifier - : null, - lastUpdated: - source && source.meta && source.meta.lastUpdated - ? getLocalDateTimeString(source.meta.lastUpdated) - : "", - gender: source && source["gender"] ? source["gender"] : "", + const source = item.resource ? item.resource : item; + const cols = columns; + let rowData = { + id: jsonpath.value(source, "$.id"), resource: source, - id: patientId, + identifier: jsonpath.value(source, "$.identifier") || [], }; + cols.forEach((col) => { + let value = jsonpath.value(source, col.expr) || null; + if (col.dataType === "date") { + value = getLocalDateTimeString(value); + } + rowData[col.field] = value; + }); + return rowData; }) : data; }; - function setNoPMPFlag(data) { + const inPDMP = (rowData) => { + if (!rowData) return false; + return ( + rowData.identifier && + rowData.identifier.filter((item) => { + return ( + item.system === "https://github.com/uwcirg/script-fhir-facade" && + item.value + ); + }).length + ); + }; + const setNoPMPFlag = (data) => { if (!data || !data.length) return false; let hasNoPMPRow = data.filter((rowData) => { @@ -501,81 +471,54 @@ export default function PatientListTable() { }).length > 0; //legend will display if contain no pmp row flag is set if (hasNoPMPRow) setContainNoPMPRow(true); - } - - function containEmptyFilter(filters) { - if (!filters || !filters.length) return true; - return getNonEmptyFilters(filters).length === 0; - } - function getNonEmptyFilters(filters) { + }; + const containEmptyFilter = (filters) => + getNonEmptyFilters(filters).length === 0; + const getNonEmptyFilters = (filters) => { if (!filters) return []; - return filters.filter(item => item.value && item.value !== ""); - } - - function handleActionLabel(filters) { - setActionLabel(getNonEmptyFilters(filters).length === 3? CREATE_BUTTON_LABEL: LAUNCH_BUTTON_LABEL); - } - function handleNoDataText(filters) { - let text = ""; + return filters.filter((item) => item.value && item.value !== ""); + }; + const handleActionLabel = (filters) => { + setActionLabel( + getNonEmptyFilters(filters).length === 3 + ? CREATE_BUTTON_LABEL + : LAUNCH_BUTTON_LABEL + ); + }; + const handleNoDataText = (filters) => { + let text = "No matching record found.
"; const nonEmptyFilters = getNonEmptyFilters(filters); if (nonEmptyFilters.length < 3) { - text = "Try entering all First name, Last name and Birth Date."; + text += "Try entering all First name, Last name and Birth Date."; } else if (nonEmptyFilters.length === 3) { - text = `Click on ${CREATE_BUTTON_LABEL} button to create new patient`; + text += `Click on ${CREATE_BUTTON_LABEL} button to create new patient`; } setNoDataText(text); - } - - function onFiltersDidChange(filters, clearAll) { + }; + const onFiltersDidChange = (filters) => { clearTimeout(filterIntervalId); filterIntervalId = setTimeout(function () { + setErrorMessage(""); handleNoDataText(filters); handleActionLabel(filters); - if (filters && filters.length) { - setCurrentFilters(filters); - resetPaging(); - if (containEmptyFilter(filters)) { - handleRefresh(); - return filters; - } - if (tableRef && tableRef.current) tableRef.current.onQueryChange(); + if (!filters || !filters.length || containEmptyFilter(filters)) { + handleRefresh(); return filters; - } else { - if (clearAll) { - setCurrentFilters(defaultFilters); - resetPaging(); - if (tableRef && tableRef.current) tableRef.current.onQueryChange(); - return defaultFilters; - } - return defaultFilters; } + setCurrentFilters(filters); + resetPaging(); + if (tableRef && tableRef.current) tableRef.current.onQueryChange(); + return filters; }, 200); - } - - function inPDMP(rowData) { - if (!rowData) return false; - return ( - rowData.identifier && - rowData.identifier.filter((item) => { - return ( - item.system === "https://github.com/uwcirg/script-fhir-facade" && - item.value === "found" - ); - }).length - ); - } - - function patientListInitialized() { - return initialized; - } - - function handleErrorCallback(e) { + }; + const handleErrorCallback = (e) => { if (e && e.status === 401) { setErrorMessage("Unauthorized."); window.location = "/logout?unauthorized=true"; return; } if (e && e.status === 403) { + setErrorMessage("Forbidden."); window.location = "/logout?forbidden=true"; return; } @@ -586,31 +529,35 @@ export default function PatientListTable() { ? e.message : "Error occurred processing data" ); - } + }; const resetPaging = () => { - setNextPageURL(""); - setPrevPageURL(""); - setPageNumber(0); - setPageSize(pageSize); + paginationDispatch({ type: "reset" }); }; - const handleChangePage = (event, newPage) => { - setPrevPageNumber(pageNumber); - setPageNumber(newPage); + paginationDispatch({ + payload: { + prevPageNumber: pagination.pageNumber, + pageNumber: newPage, + }, + }); if (tableRef && tableRef.current) tableRef.current.onQueryChange(); }; const handleChangeRowsPerPage = (event) => { - setPageSize(parseInt(event.target.value, 10)); - setNextPageURL(""); - setPrevPageURL(""); - setPageNumber(0); + paginationDispatch({ + payload: { + pageSize: parseInt(event.target.value, 10), + nextPageURL: "", + prevPageURL: "", + pageNumber: 0, + }, + }); if (tableRef && tableRef.current) tableRef.current.onQueryChange(); }; - const handleRefresh = () => { - document.querySelector("#btnClear").click(); - setErrorMessage(""); + setCurrentFilters(defaultFilters); + resetPaging(); + if (tableRef && tableRef.current) tableRef.current.onQueryChange(); }; const handleMenuClick = (event, rowData) => { event.stopPropagation(); @@ -630,24 +577,20 @@ export default function PatientListTable() { } setTimeout(function () { currentRow.tableData.showDetailPanel = true; - tableRef.current.onToggleDetailPanel( - [currentRow.tableData.id], - tableRef.current.props.detailPanel[0].render - ); + handleToggleDetailPanel(currentRow); }, 200); }; - - const getMoreMenuSetting = () => { - return appSettings[MORE_MENU_KEY] ? appSettings[MORE_MENU_KEY] : []; - }; const shouldHideMoreMenu = () => { + if (!hasAppSettings()) return true; return ( - Object.keys(appSettings).length && - (!appSettings[MORE_MENU_KEY] || appSettings[MORE_MENU_KEY].length === 0) + !appSettings[MORE_MENU_KEY] || + appSettings[MORE_MENU_KEY].filter((item) => item && item !== "") + .length === 0 ); }; const shouldShowMenuItem = (id) => { - let arrMenu = getMoreMenuSetting(); + let arrMenu = getAppSettingByKey(MORE_MENU_KEY); + if (!Array.isArray(arrMenu)) return false; return ( arrMenu.filter((item) => item.toLowerCase() === id.toLowerCase()).length > 0 @@ -662,6 +605,16 @@ export default function PatientListTable() { } return null; }; + const handleToggleDetailPanel = (rowData) => { + tableRef.current.onToggleDetailPanel( + [ + tableRef.current.dataManager.sortedData.findIndex( + (item) => item.id === rowData.id + ), + ], + tableRef.current.props.detailPanel[0].render + ); + }; const getPatientList = (query) => { let sortField = query.orderBy && query.orderBy.field @@ -684,15 +637,17 @@ export default function PatientListTable() { page: 0, totalCount: 0, }; - const resetAll = () => { - resetPaging(); - setInitialized(true); - }; - let apiURL = `/fhir/Patient?_include=Patient:link&_total=accurate&_count=${pageSize}`; - if (pageNumber > prevPageNumber && nextPageURL) { - apiURL = nextPageURL; - } else if (pageNumber < prevPageNumber && prevPageURL) { - apiURL = prevPageURL; + let apiURL = `/fhir/Patient?_include=Patient:link&_total=accurate&_count=${pagination.pageSize}`; + if ( + pagination.pageNumber > pagination.prevPageNumber && + pagination.nextPageURL + ) { + apiURL = pagination.nextPageURL; + } else if ( + pagination.pageNumber < pagination.prevPageNumber && + pagination.prevPageURL + ) { + apiURL = pagination.prevPageURL; } if (searchString && apiURL.indexOf("contains") === -1) apiURL += `&${searchString}`; @@ -704,20 +659,19 @@ export default function PatientListTable() { */ return new Promise((resolve) => { fetchData(apiURL, noCacheParam, function (e) { - resetAll(); + paginationDispatch({ type: "empty" }); handleErrorCallback(e); resolve(defaults); }) .then((response) => { if (!response || !response.entry || !response.entry.length) { - resetAll(); + paginationDispatch({ type: "empty" }); resolve(defaults); return; } - setInitialized(true); let responseData = formatData(response.entry); setData(responseData || []); - setNoPMPFlag(responseData); + if (needExternalAPILookup()) setNoPMPFlag(responseData); let responsePageoffset = 0; let responseSelfLink = response.link ? response.link.filter((item) => { @@ -742,7 +696,7 @@ export default function PatientListTable() { ); } let currentPage = responsePageoffset - ? responsePageoffset / pageSize + ? responsePageoffset / pagination.pageSize : 0; let hasNextLink = responseNextLink && responseNextLink.length; let hasPrevLink = responsePrevLink && responsePrevLink.length; @@ -752,11 +706,15 @@ export default function PatientListTable() { : hasSelfLink ? responseSelfLink[0].url : ""; - setNextPageURL(newNextURL); - setPrevPageURL(newPrevURL); - setDisableNextButton(!hasNextLink); - setDisablePrevButton(pageNumber === 0); - setTotalCount(response.total); + paginationDispatch({ + payload: { + nextPageURL: newNextURL, + prevPageURL: newPrevURL, + disableNextButton: !hasNextLink, + disablePrevButton: pagination.pageNumber === 0, + totalCount: response.total, + }, + }); resolve({ data: responseData, page: currentPage, @@ -772,269 +730,300 @@ export default function PatientListTable() { error && error.status ? "Error status " + error.status : error }` ); - resetAll(); resolve(defaults); }); }); }; + const handlePageUnload = () => { + setTimeout(() => setOpenLoadingModal(false), 500); + }; + React.useEffect(() => { //when page unloads, remove loading indicator - window.addEventListener("beforeunload", function () { - setTimeout(() => setOpenLoadingModal(false), 50); - }); - getSettings((data) => { - if (data.error) { - handleErrorCallback(data.error); - setSettingInitialized(true); - setErrorMessage(`Error retrieving app setting: ${data.error}`); - return; + window.addEventListener("beforeunload", handlePageUnload); + validateToken().then( + (token) => { + if (!token) { + console.log("Redirecting..."); + window.location = "/clear_session"; + return false; + } + if (appSettings) { + const clients = getClientsByRequiredRoles( + appSettings["SOF_CLIENTS"], + getRolesFromToken(token) + ); + if (!clients || !clients.length) { + setErrorMessage("No SoF client match the user role(s) found"); + } else { + setAppClients(clients); + } + } + }, + (e) => { + console.log("token validation error ", e); + handleErrorCallback(e); } - setSettingInitialized(true); - setAppSettings(data); - }, true); //no caching - }, []); //retrieval of settings should occur prior to patient list being rendered/initialized + ); + return () => { + window.removeEventListener("beforeunload", handlePageUnload); + }; + }, [appSettings]); //retrieval of settings should occur prior to patient list being rendered/initialized return ( - - -

COSRI Patient Search

- - - - - -
- {settingInitialized &&
- getPatientList(query) - } - tableRef={tableRef} - hideSortIcon={false} - detailPanel={[ - { - render: (rowData) => { - return ( -
- - {getSelectedItemComponent(selectedMenuItem, rowData)} - - -
- ); - }, - isFreeAction: false, - }, - ]} - //overlay - components={{ - OverlayLoading: () => ( -
-
- -
-
- ) - }} - actions={[ - { - icon: () => ( - - {LAUNCH_BUTTON_LABEL} - - ), - onClick: (event, rowData) => handleSearch(event, rowData), - tooltip: "Launch COSRI application for the user", - }, - { - icon: () => ( - - ), - onClick: (event, rowData) => handleMenuClick(event, rowData), - tooltip: "More", - }, - ]} - options={{ - paginationTypestepped: "stepped", - showFirstLastPageButtons: false, - paging: false, - padding: "dense", - emptyRowsWhenPaging: false, - debounceInterval: 300, - detailPanelColumnAlignment: "right", - toolbar: false, - filtering: false, - sorting: true, - search: false, - showTitle: false, - headerStyle: { - backgroundColor: theme.palette.primary.lightest, - padding: theme.spacing(1, 2, 1), - }, - rowStyle: (rowData) => ({ - backgroundColor: !inPDMP(rowData) - ? theme.palette.primary.disabled - : "#FFF", - }), - actionsCellStyle: { - paddingLeft: theme.spacing(2), - paddingRight: theme.spacing(2), - minWidth: "25%", - justifyContent: "center", - }, - actionsColumnIndex: -1, - }} - icons={tableIcons} - onRowClick={(event, rowData) => { - event.stopPropagation(); - handleSearch(event, rowData); - }} - editable={{ - onRowDelete: (oldData) => - fetchData("/fhir/Patient/" + oldData.id, { method: "DELETE" }) - .then(() => { - setTimeout(() => { - const dataDelete = [...data]; - const index = oldData.tableData.id; - dataDelete.splice(index, 1); - setData([...dataDelete]); - setErrorMessage(""); - }, 500); - }) - .catch(() => { - setErrorMessage( - "Unable to remove patient from the list." - ); - }), - }} - localization={{ - header: { - actions: "", - }, - pagination: { - labelRowsSelect: "rows", - }, - body: { - deleteTooltip: "Remove from the list", - editRow: { - deleteText: - "Are you sure you want to remove this patient from the list? (You can add them back later by searching for them)", - saveTooltip: "OK", - }, - emptyDataSourceMessage: ( -
-
-
No matching patient found.
-
{noDataText}
-
-
- ), - }, - }} - /> -
- } -
- {patientListInitialized() && containNoPMPRow && ( -
- Not in PMP -
- )} - {patientListInitialized() && !containNoPMPRow && ( -
- )} -
- {patientListInitialized() && ( -
-
- + +

Patient Search

+ + {/* patient search row */} + + + + +
+ {/* patient list table */} +
+ getPatientList(query) + } + tableRef={tableRef} + hideSortIcon={false} + detailPanel={[ + { + render: (data) => { + return ( + + {getSelectedItemComponent(selectedMenuItem, data.rowData)} - -
- + ); + }, + isFreeAction: false, + }, + ]} + //overlay + components={{ + OverlayLoading: () => ( + + + + ), + }} + actions={[ + ...(appClients && appClients.length + ? appClients.map((client, index) => { + return { + icon: () => ( + + {client.label} + + ), + onClick: (event, rowData) => { + event.stopPropagation(); + handleLaunchApp(rowData, client); + }, + tooltip: `Launch ${client.id} application for the user`, + }; + }) + : []), + { + icon: () => + !shouldHideMoreMenu() && ( + + ), + onClick: (event, rowData) => handleMenuClick(event, rowData), + tooltip: shouldHideMoreMenu() ? "" : "More", + }, + ]} + options={{ + paginationTypestepped: "stepped", + showFirstLastPageButtons: false, + paging: false, + padding: "dense", + emptyRowsWhenPaging: false, + debounceInterval: 300, + detailPanelColumnAlignment: "right", + toolbar: false, + filtering: false, + sorting: true, + search: false, + showTitle: false, + headerStyle: { + backgroundColor: theme.palette.primary.lightest, + padding: theme.spacing(1, 2, 1), + }, + rowStyle: (rowData) => ({ + backgroundColor: + needExternalAPILookup() && !inPDMP(rowData) + ? theme.palette.primary.disabled + : "#FFF", + }), + actionsCellStyle: { + paddingLeft: theme.spacing(1), + paddingRight: theme.spacing(1), + justifyContent: "center", + }, + actionsColumnIndex: -1, + }} + icons={tableIcons} + onRowClick={(event, rowData) => { + event.stopPropagation(); + if (!hasSoFClients()) return; + handleLaunchApp(rowData); + }} + editable={{ + onRowDelete: (oldData) => + fetchData("/fhir/Patient/" + oldData.id, { + method: "DELETE", + }) + .then(() => { + setTimeout(() => { + const dataDelete = [...data]; + const target = dataDelete.find( + (el) => el.id === oldData.id + ); + const index = dataDelete.indexOf(target); + dataDelete.splice(index, 1); + setData([...dataDelete]); + setErrorMessage(""); + }, 500); + }) + .catch(() => { + setErrorMessage("Unable to remove patient from the list."); + }), + }} + localization={{ + header: { + actions: "", + }, + pagination: { + labelRowsSelect: "rows", + }, + body: { + deleteTooltip: "Remove from the list", + editRow: { + deleteText: + "Are you sure you want to remove this patient from the list? (You can add them back later by searching for them)", + saveTooltip: "OK", + }, + emptyDataSourceMessage: ( +
-
- )} + >
+ ), + }, + }} + /> +
+
+ {containNoPMPRow && ( +
+ Not in PMP +
+ )} + {!containNoPMPRow &&
} +
+
+ + +
+ {data.length > 0 && ( + + )}
- -
-
- Loading ...{" "} - -
+
+ + onLaunchDialogClose()} + title={ + currentRow ? `${currentRow.last_name}, ${currentRow.first_name}` : "" + } + body={ +
+ {hasSoFClients() && + appClients.map((appClient, index) => { + return ( + + ); + })}
-
- shouldShowMenuItem(item.id))} - > - - + } + > + shouldShowMenuItem(item.id))} + > + ); } diff --git a/patientsearch/src/js/components/SiteLogo.js b/patientsearch/src/js/components/SiteLogo.js index 29291983..d1157aae 100644 --- a/patientsearch/src/js/components/SiteLogo.js +++ b/patientsearch/src/js/components/SiteLogo.js @@ -1,9 +1,9 @@ import React from "react"; import PropTypes from "prop-types"; import { makeStyles } from "@material-ui/core/styles"; -import { imageOK } from "./Utility"; -import { getAppSettings } from "../context/SettingContextProvider"; -import theme from "../context/theme"; +import { imageOK } from "../helpers/utility"; +import { useSettingContext } from "../context/SettingContextProvider"; +import theme from "../themes/theme"; const useStyles = makeStyles({ container: { @@ -15,11 +15,11 @@ const useStyles = makeStyles({ export default function SiteLogo(props) { const classes = useStyles(); - const appSettings = props.appSettings ? props.appSettings : getAppSettings(); //provide default if none provided + const settingsCtx = useSettingContext(); + const appSettings = props.appSettings + ? props.appSettings + : settingsCtx.appSettings; //provide default if none provided const SITE_ID_STRING = "SITE_ID"; - React.useEffect(() => { - //wait for application settings - }, [appSettings]); function getSiteId() { if (props.siteID) return props.siteID; if (!Object.keys(appSettings)) return ""; diff --git a/patientsearch/src/js/components/SystemBanner.js b/patientsearch/src/js/components/SystemBanner.js index 5d918544..7be6589d 100644 --- a/patientsearch/src/js/components/SystemBanner.js +++ b/patientsearch/src/js/components/SystemBanner.js @@ -1,8 +1,8 @@ import React from "react"; import PropTypes from "prop-types"; import { makeStyles } from "@material-ui/core/styles"; -import theme from "../context/theme"; -import { getAppSettings } from "../context/SettingContextProvider"; +import theme from "../themes/theme"; +import { useSettingContext } from "../context/SettingContextProvider"; const useStyles = makeStyles({ container: { @@ -18,7 +18,10 @@ const useStyles = makeStyles({ export default function SystemBanner(props) { const classes = useStyles(); const SYSTEM_TYPE_STRING = "SYSTEM_TYPE"; - const appSettings = props.appSettings ? props.appSettings : getAppSettings(); //provide default if none provided + const settingsCtx = useSettingContext(); + const appSettings = props.appSettings + ? props.appSettings + : settingsCtx.appSettings; //provide default if none provided function getSystemType() { if (props.systemType) return props.systemType; if (!Object.keys(appSettings)) return ""; @@ -28,9 +31,6 @@ export default function SystemBanner(props) { let systemType = getSystemType(); return systemType && String(systemType.toLowerCase()) !== "production"; } - React.useEffect(() => { - //wait for application settings - }, [appSettings]); return ( /* display system type for non-production instances */
diff --git a/patientsearch/src/js/components/TimeoutModal.js b/patientsearch/src/js/components/TimeoutModal.js index 23a21962..6225b7fe 100644 --- a/patientsearch/src/js/components/TimeoutModal.js +++ b/patientsearch/src/js/components/TimeoutModal.js @@ -2,9 +2,9 @@ import React, { useEffect } from "react"; import { makeStyles } from "@material-ui/core/styles"; import Button from "@material-ui/core/Button"; import Modal from "@material-ui/core/Modal"; -import { sendRequest } from "./Utility"; -import theme from "../context/theme"; -import { getAppSettings } from "../context/SettingContextProvider"; +import { sendRequest } from "../helpers/utility"; +import theme from "../themes/theme"; +import { useSettingContext } from "../context/SettingContextProvider"; function getModalStyle() { const top = 50; @@ -48,12 +48,34 @@ export default function TimeoutModal() { const [open, setOpen] = React.useState(false); const [disabled, setDisabled] = React.useState(false); const trackInterval = 15000; - const appSettings = getAppSettings(); + const appCtx = useSettingContext(); + const appSettings = appCtx.appSettings; const clearExpiredIntervalId = () => { clearInterval(expiredIntervalId); }; const checkSessionValidity = () => { + + const reTry = () => { + //try again? + if (retryAttempts < 2) { + initTimeoutTracking(); + retryAttempts++; + return; + } + retryAttempts = 0; + clearExpiredIntervalId(); + }; + + const handleLogout = (userInitiated) => { + clearExpiredIntervalId(); + sessionStorage.clear(); + let param = userInitiated ? "user_initiated=true" : "timeout=true"; + setTimeout(() => { + window.location = `/logout?${param}`; + }, 0); + return false; + }; /* * when the expires in is less than the next track interval, the session will have expired, so just logout user */ @@ -73,7 +95,9 @@ export default function TimeoutModal() { let refreshTokenExpiresIn = parseFloat( tokenData["refresh_expires_in"] ); - let refreshTokenOnVentilator = (!tokenData["valid"] && refreshTokenExpiresIn === 0) || (refreshTokenExpiresIn < accessTokenExpiresIn); + let refreshTokenOnVentilator = + (!tokenData["valid"] && refreshTokenExpiresIn === 0) || + refreshTokenExpiresIn < accessTokenExpiresIn; //in seconds //1. check if refresh token will expire before access token first //2. check if access token will expire @@ -92,6 +116,7 @@ export default function TimeoutModal() { } else { reLoad(); } + clearExpiredIntervalId(); return; } if (tokenAboutToExpire) { @@ -116,6 +141,7 @@ export default function TimeoutModal() { console.log("Error returned ", error); if (error && error.status && error.status == 401) { console.log("Failed to retrieve token data: Unauthorized"); + clearExpiredIntervalId(); handleLogout(); return; } @@ -123,23 +149,11 @@ export default function TimeoutModal() { "Failed to retrieve token data", error && error.status ? "status " + error.status : "" ); - //attempt retry if error - reTry(); + clearExpiredIntervalId(); } ); }; - const reTry = () => { - //try again? - if (retryAttempts < 2) { - initTimeoutTracking(); - retryAttempts++; - return; - } - retryAttempts = 0; - clearExpiredIntervalId(); - }; - const initTimeoutTracking = () => { expiredIntervalId = setInterval( () => checkSessionValidity(), @@ -162,16 +176,6 @@ export default function TimeoutModal() { window.location = "/clear_session"; }; - const handleLogout = (userInitiated) => { - clearExpiredIntervalId(); - sessionStorage.clear(); - let param = userInitiated ? "user_initiated=true" : "timeout=true"; - setTimeout(() => { - window.location = `/logout?${param}`; - }, 0); - return false; - }; - const getExpiresInDisplay = (expiresIn) => { if (!expiresIn) return ""; return `${Math.floor(expiresIn)} seconds`; @@ -192,14 +196,25 @@ export default function TimeoutModal() { )} {expiresIn && expiresIn != 0 && ( - {!disabled && - Your session will expired in approximately - {getExpiresInDisplay(expiresIn)}. - } - {disabled &&
-
Your session is about to expire.
- {refresh &&
One moment while your browser session is refreshed....
} -
} + {!disabled && ( + + Your session will expired in approximately + + {getExpiresInDisplay(expiresIn)} + + . + + )} + {disabled && ( +
+
Your session is about to expire.
+ {refresh && ( +
+ One moment while your browser session is refreshed.... +
+ )} +
+ )}
)}
@@ -215,7 +230,10 @@ export default function TimeoutModal() { -
@@ -225,8 +243,9 @@ export default function TimeoutModal() { ); useEffect(() => { clearExpiredIntervalId(); - setDisabled(appSettings["ENABLE_INACTIVITY_TIMEOUT"]?false:true); + setDisabled(appSettings["ENABLE_INACTIVITY_TIMEOUT"] ? false : true); initTimeoutTracking(); + return () => clearExpiredIntervalId(); }, [appSettings]); return ( diff --git a/patientsearch/src/js/components/UrineScreen.js b/patientsearch/src/js/components/UrineScreen.js index 605fbf98..55eb840f 100644 --- a/patientsearch/src/js/components/UrineScreen.js +++ b/patientsearch/src/js/components/UrineScreen.js @@ -1,15 +1,15 @@ import React from "react"; import PropTypes from "prop-types"; import DOMPurify from "dompurify"; -import { makeStyles} from "@material-ui/core/styles"; +import { makeStyles } from "@material-ui/core/styles"; import DateFnsUtils from "@date-io/date-fns"; import isValid from "date-fns/isValid"; import ArrowDropDownIcon from "@material-ui/icons/ArrowDropDown"; import ClearIcon from "@material-ui/icons/Clear"; import Button from "@material-ui/core/Button"; import CircularProgress from "@material-ui/core/CircularProgress"; -import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; -import ExpandLessIcon from '@material-ui/icons/ExpandLess'; +import ExpandMoreIcon from "@material-ui/icons/ExpandMore"; +import ExpandLessIcon from "@material-ui/icons/ExpandLess"; import IconButton from "@material-ui/core/IconButton"; import InputAdornment from "@material-ui/core/InputAdornment"; import InputLabel from "@material-ui/core/InputLabel"; @@ -20,7 +20,10 @@ import Paper from "@material-ui/core/Paper"; import Select from "@material-ui/core/Select"; import Snackbar from "@material-ui/core/Snackbar"; import Typography from "@material-ui/core/Typography"; -import {MuiPickersUtilsProvider, KeyboardDatePicker } from "@material-ui/pickers"; +import { + MuiPickersUtilsProvider, + KeyboardDatePicker, +} from "@material-ui/pickers"; import Alert from "./Alert"; import EditButtonGroup from "./EditButtonGroup"; import Error from "./Error"; @@ -28,661 +31,909 @@ import HistoryTable from "./HistoryTable"; import FormattedInput from "./FormattedInput"; import OverdueAlert from "./OverdueAlert"; import { - fetchData, - dateTimeCompare, - getShortDateFromISODateString, - getSettings, - isAdult, - padDateString, - sendRequest, -} from "./Utility"; -import theme from "../context/theme"; + fetchData, + dateTimeCompare, + getShortDateFromISODateString, + isAdult, + padDateString, + sendRequest, +} from "../helpers/utility"; +import { useSettingContext } from "../context/SettingContextProvider"; +import theme from "../themes/theme"; const useStyles = makeStyles({ - container: { - paddingLeft: theme.spacing(3), - paddingRight: theme.spacing(3), - paddingTop: theme.spacing(1) - }, - contentContainer: { - position: "relative" - }, - addContainer: { - position: "relative", - marginBottom: theme.spacing(2), - padding: theme.spacing(2) - }, - typeContainer: { - position: "relative" - }, - textDisplay: { - marginTop: theme.spacing(3) - }, - buttonsContainer: { - marginTop: theme.spacing(2.5), - position: "relative" - }, - progressContainer: { - position: "absolute", - left: 0, - right: 0, - top: 0, - bottom: 0, - background: "hsl(0deg 0% 100% / 80%)", - zIndex: 500, - minHeight: theme.spacing(6), - boxShadow: "none" - }, - progressIcon: { - position: "absolute", - top: "10%", - left: "10%" - }, - historyContainer: { - position: "relative", - marginBottom: theme.spacing(2), - padding: theme.spacing(2), - minHeight: theme.spacing(9) - }, - historyTitle: { - display: "inline-block", - fontWeight: 500, - color: theme.palette.dark.main, - borderBottom: `2px solid ${theme.palette.primary.lightest}`, - marginBottom: theme.spacing(1) - }, - addTitle: { - display: "inline-block", - fontWeight: 500, - color: theme.palette.dark.main, - borderBottom: `2px solid ${theme.palette.primary.lightest}`, - marginBottom: theme.spacing(2.5) - }, - addButton: { - marginRight: theme.spacing(1) - }, - dateInput: { - minWidth: "248px" - }, - selectFormControl: { - marginBottom: theme.spacing(1) - }, - selectBox: { - minWidth: "248px", - fontSize: "14px", - marginRight: theme.spacing(0.5) - }, - dateLabel: { - fontSize: "12px", - marginBottom: theme.spacing(0.25) - }, - readonlyLabel: { - fontSize: "12px", - marginBottom: theme.spacing(0.5) - }, - menuItem: { - fontSize: "14px" - }, - editInput: { - width: theme.spacing(10) - }, - errorContainer: { - maxWidth: "100%", - marginTop: theme.spacing(3) - }, - expandIcon: { - marginLeft: theme.spacing(2), - verticalAlign: "middle", - fontSize: "12px" - }, - endIcon: { - marginLeft: "-4px", - position: "relative" - }, - tableContainer: { - position: "relative" - } + container: { + paddingLeft: theme.spacing(3), + paddingRight: theme.spacing(3), + paddingTop: theme.spacing(1), + }, + contentContainer: { + position: "relative", + }, + addContainer: { + position: "relative", + marginBottom: theme.spacing(2), + padding: theme.spacing(2), + }, + typeContainer: { + position: "relative", + }, + textDisplay: { + marginTop: theme.spacing(3), + }, + buttonsContainer: { + marginTop: theme.spacing(2.5), + position: "relative", + }, + progressContainer: { + position: "absolute", + left: 0, + right: 0, + top: 0, + bottom: 0, + background: "hsl(0deg 0% 100% / 80%)", + zIndex: 500, + minHeight: theme.spacing(6), + boxShadow: "none", + }, + progressIcon: { + position: "absolute", + top: "10%", + left: "10%", + }, + historyContainer: { + position: "relative", + marginBottom: theme.spacing(2), + padding: theme.spacing(2), + minHeight: theme.spacing(9), + }, + historyTitle: { + display: "inline-block", + fontWeight: 500, + color: theme.palette.dark.main, + borderBottom: `2px solid ${theme.palette.primary.lightest}`, + marginBottom: theme.spacing(1), + }, + addTitle: { + display: "inline-block", + fontWeight: 500, + color: theme.palette.dark.main, + borderBottom: `2px solid ${theme.palette.primary.lightest}`, + marginBottom: theme.spacing(2.5), + }, + addButton: { + marginRight: theme.spacing(1), + }, + dateInput: { + minWidth: "248px", + }, + selectFormControl: { + marginBottom: theme.spacing(1), + }, + selectBox: { + minWidth: "248px", + fontSize: "14px", + marginRight: theme.spacing(0.5), + }, + dateLabel: { + fontSize: "12px", + marginBottom: theme.spacing(0.25), + }, + readonlyLabel: { + fontSize: "12px", + marginBottom: theme.spacing(0.5), + }, + menuItem: { + fontSize: "14px", + }, + editInput: { + width: theme.spacing(10), + }, + errorContainer: { + maxWidth: "100%", + marginTop: theme.spacing(3), + }, + expandIcon: { + marginLeft: theme.spacing(2), + verticalAlign: "middle", + fontSize: "12px", + }, + endIcon: { + marginLeft: "-4px", + position: "relative", + }, + tableContainer: { + position: "relative", + }, + overDueContainer: { + marginBottom: theme.spacing(2) + } }); export default function UrineScreen(props) { - const classes = useStyles(); - const [type, setType] = React.useState(""); - const [date, setDate] = React.useState(null); - const [lastEntryId, setLastEntryId] = React.useState(null); - const [urineScreenTypes, setUrineScreenTypes] = React.useState([]); - const [selectTypeLookup, setSelectTypeLookup] = React.useState({}); - const [urineScreenTypesInitialized, setUrineScreenTypesInitialized] = React.useState(false); - const [lastUrineScreenDate, setLastUrineScreenDate] = React.useState(""); - const [lastType, setLastType] = React.useState(""); - const [dateInput, setDateInput] = React.useState(null); - const [history, setHistory] = React.useState([]); - const [addInProgress, setAddInProgress] = React.useState(false); - const [updateInProgress, setUpdateInProgress] = React.useState(false); - const [error, setError] = React.useState(""); - const [snackOpen, setSnackOpen] = React.useState(false); - const [historyInitialized, setHistoryInitialized] = React.useState(false); - const [showHistory, setShowHistory] = React.useState(false); - const [editMode, setEditMode] = React.useState(false); - const [editType, setEditType] = React.useState(""); - const [editDate, setEditDate] = React.useState(""); - const URINE_SCREEN_TYPE_LABEL = "Urine Drug Screen Name"; - const rowData = props.rowData ? props.rowData : {}; - const clearDate = () => { - setDate(null); - setDateInput(""); - }; - const clearHistory = () => { - setHistory([]); - setLastType(""); - setLastEntryId(null); - setLastUrineScreenDate(""); - }; - const resetEdits = () => { - setEditType(""); - setEditDate(null); - }; - const clearFields = () => { - clearDate(); - if (!onlyOneUrineScreenType()) setType(""); - setError(""); - }; - const handleTypeChange = (event) => { - setType(event.target.value); - }; - const handleEditTypeChange = (event) => { - setEditType(event.target.value); - }; - const hasValues = () => { - return type && date; - }; - const hasError = () => { - return error !== ""; - }; - const getHistory = (types, callback) => { - callback = callback || function() {}; - if (!rowData.id) { + const appCtx = useSettingContext(); + const appSettingsRef = React.useRef(appCtx.appSettings); + const classes = useStyles(); + const urineScreenTypes = (() => { + const appSettings = appSettingsRef.current; + return appSettings && appSettings["UDS_LAB_TYPES"] + ? appSettings["UDS_LAB_TYPES"] + : null; + })(); + const lastEntryReducer = (state, action) => { + if (action.type == "reset") { + return { + id: null, + date: "", + type: "", + }; + } + if (action.type === "update") { + return { + ...state, + ...action.data, + }; + } + return state; + }; + const [lastEntry, lastEntryDispatch] = React.useReducer(lastEntryReducer, { + id: null, + date: "", + type: "", + }); + const [type, setType] = React.useState( + urineScreenTypes && urineScreenTypes.length === 1 + ? urineScreenTypes[0].code + : "" + ); + const [date, setDate] = React.useState(null); + const [dateInput, setDateInput] = React.useState(null); + const [history, setHistory] = React.useState([]); + const [addInProgress, setAddInProgress] = React.useState(false); + const [updateInProgress, setUpdateInProgress] = React.useState(false); + const [error, setError] = React.useState(""); + const [snackOpen, setSnackOpen] = React.useState(false); + const [historyInitialized, setHistoryInitialized] = React.useState(false); + const [expandHistory, setExpandHistory] = React.useState(false); + const defaultValues = { + mode: false, + type: "", + date: "", + }; + const editReducer = (state, action) => { + if (action.key) { + return { + ...state, + [action.key]: action.value, + }; + } + if (action.type === "update") { + return { + ...state, + ...action.data, + }; + } + if (action.type === "reset") { + return defaultValues; + } + return state; + }; + const [editEntry, editDispatch] = React.useReducer( + editReducer, + defaultValues + ); + const URINE_SCREEN_TYPE_LABEL = "Urine Drug Screen Name"; + const { rowData } = props; + const getPatientId = React.useCallback(() => { + if (rowData) return rowData.id; + return null; + }, [rowData]); + const clearDate = () => { + setDate(null); + setDateInput(""); + }; + const clearHistory = () => { + setHistory([]); + lastEntryDispatch({ + type: "reset", + }); + }; + const clearFields = () => { + clearDate(); + if (!onlyOneUrineScreenType()) setType(""); + setError(""); + }; + const handleTypeChange = (event) => { + setType(event.target.value); + }; + const handleEditTypeChange = (event) => { + editDispatch({ + key: "type", + value: event.target.value, + }); + }; + const hasValues = () => { + return type && date; + }; + const hasError = () => { + return error !== ""; + }; + const getHistory = React.useCallback( + (callback) => { + callback = callback || function () {}; + if (!rowData) { + setHistoryInitialized(true); + callback(); + return []; + } + const types = urineScreenTypes; + + setHistoryInitialized(false); + /* + * retrieve urine screen history + */ + sendRequest("/fhir/ServiceRequest?patient=" + rowData.id, { + nocache: true, + }).then( + (response) => { + let data = null; + try { + data = JSON.parse(response); + } catch (e) { + console.log("Eerror parsing urine screen service request data ", e); + } + if (!data || !data.entry || !data.entry.length) { + clearHistory(); + editDispatch({ + key: "mode", + value: false, + }); setHistoryInitialized(true); callback(); - return []; - } - if (!types) types = urineScreenTypes; - - setHistoryInitialized(false); - /* - * retrieve urine screen history - */ - sendRequest("/fhir/ServiceRequest?patient="+rowData.id, {nocache: true}).then(response => { - let data = null; - try { - data = JSON.parse(response); - } catch(e) { - console.log("Eerror parsing urine screen service request data ", e); - } - if (!data || !data.entry || !data.entry.length) { - clearHistory(); - setTimeout(() => { - setEditMode(false); - setHistoryInitialized(true); - }, 300); - callback(); - return; - } - const availableCodes = types.map(item => item.code); - let urineScreenData = data.entry.filter(item => { - let resource = item.resource; - if (!resource) return false; - if (!resource.code || !resource.code.coding || !resource.code.coding.length) return false; - return availableCodes.indexOf(resource.code.coding[0].code) !== -1; + return; + } + const availableCodes = types.map((item) => item.code); + let urineScreenData = data.entry.filter((item) => { + let resource = item.resource; + if (!resource) return false; + if ( + !resource.code || + !resource.code.coding || + !resource.code.coding.length + ) + return false; + return availableCodes.indexOf(resource.code.coding[0].code) !== -1; + }); + if (urineScreenData.length) { + urineScreenData = urineScreenData.sort(function (a, b) { + return dateTimeCompare( + a.resource.authoredOn, + b.resource.authoredOn + ); }); - urineScreenData = urineScreenData.sort(function(a, b) { - return dateTimeCompare(a.resource.authoredOn, b.resource.authoredOn); + const formattedData = createHistoryData(urineScreenData); + setHistory(formattedData); + lastEntryDispatch({ + type: "update", + data: { + id: formattedData[0].id, + date: formattedData[0].date, + type: formattedData[0].type, + }, }); - if (urineScreenData.length) { - const formattedData = createHistoryData(urineScreenData); - setHistory(formattedData); - const resourceType = formattedData[0].type; - const resourceDate = formattedData[0].date; - setLastUrineScreenDate(resourceDate); - setLastType(resourceType); - setLastEntryId(formattedData[0].id); - resetEdits(); - } else clearHistory(); - setTimeout(() => { - setEditMode(false); - setHistoryInitialized(true); - }, 300); - callback(); - - }, error => { - console.log("Failed to retrieve data", error); - callback(error); - setHistoryInitialized(true); - }); - return ""; - }; - const handleAdd = (params) => { - setAddInProgress(true); - handleUpdate(params, () => { - clearFields(); - setTimeout(() => setAddInProgress(false), 250); - }); - }; - const submitDataFormatter = (params) => { - params = params || {}; - const testType = params.type ? params.type : type; - const testDate = params.date ? params.date : dateInput; - let typeMatch = urineScreenTypes.filter(item => { - return item.code === testType; - }); - var resource = { - "authoredOn": padDateString(testDate), - "code": { - "coding": [ - { - "code": testType, - "display": typeMatch[0].display, - "system": typeMatch[0].system - } - - ], - "text": typeMatch[0].text - }, - "resourceType": "ServiceRequest", - "subject": { - "reference": "Patient/"+(params.patientId ? params.patientId : rowData.id) - } - }; - //include id if present, necessary for PUT request - if (params.id) { - resource = {...resource, ...{"id": params.id}}; + } else { + clearHistory(); + } + editDispatch({ + key: "mode", + value: false, + }); + setHistoryInitialized(true); + callback(); + }, + (error) => { + console.log("Failed to retrieve data", error); + callback(error); + setHistoryInitialized(true); } - return resource; + ); + return ""; + }, + [rowData, urineScreenTypes, createHistoryData] + ); + const handleAdd = (params) => { + setAddInProgress(true); + handleUpdate(params, () => { + clearFields(); + setTimeout(() => setAddInProgress(false), 250); + }); + }; + const submitDataFormatter = (params) => { + params = params || {}; + const testType = params.type ? params.type : type; + const testDate = params.date ? params.date : dateInput; + let typeMatch = urineScreenTypes.filter((item) => { + return item.code === testType; + }); + var resource = { + authoredOn: padDateString(testDate), + code: { + coding: [ + { + code: testType, + display: typeMatch[0].display, + system: typeMatch[0].system, + }, + ], + text: typeMatch[0].text, + }, + resourceType: "ServiceRequest", + subject: { + reference: + "Patient/" + (params.patientId ? params.patientId : getPatientId()), + }, }; - const handleUpdate = (params, callback) => { - params = params || {}; - callback = callback || function() {}; - const testType = params.type ? params.type : type; - const testDate = params.date ? params.date : dateInput; - let typeMatch = urineScreenTypes.filter(item => { - return item.code === testType; - }); - if (String(params.method).toLowerCase() !== "delete") { - if(!testType || !typeMatch.length) { - setError("Unknown urine screen type " + testType); - callback(); - return false; - } - if (!testDate) { - setError("Missing urine screen date"); - callback(); - return false; - } + //include id if present, necessary for PUT request + if (params.id) { + resource = { ...resource, ...{ id: params.id } }; + } + return resource; + }; + const handleUpdate = (params, callback) => { + params = params || {}; + callback = callback || function () {}; + const testType = params.type ? params.type : type; + const testDate = params.date ? params.date : dateInput; + let typeMatch = urineScreenTypes.filter((item) => { + return item.code === testType; + }); + if (String(params.method).toLowerCase() !== "delete") { + if (!testType || !typeMatch.length) { + setError("Unknown urine screen type " + testType); + callback(); + return false; + } + if (!testDate) { + setError("Missing urine screen date"); + callback(); + return false; + } + } + let resource = submitDataFormatter(params); + setError(""); + fetchData( + "/fhir/ServiceRequest" + (params.id ? "/" + params.id : ""), + { + method: params.method ? params.method : "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + cache: "no-cache", + }, + body: JSON.stringify(resource), + }, + (e) => { + if (e) { + handleSubmissionError(); } - let resource = submitDataFormatter(params); - setError(""); - fetchData("/fhir/ServiceRequest"+(params.id?"/"+params.id:""), { - "method": params.method ? params.method : "POST", - headers: { - "Accept": "application/json", - "Content-Type": "application/json", - cache: "no-cache" - }, - body: JSON.stringify(resource) - }, (e) => { - if (e) { - handleSubmissionError(); - } - callback(e); - }) - .then(() => { - setSnackOpen(true); - setTimeout(() => { - getHistory(urineScreenTypes, callback); - }, 150); - }).catch(e => { - callback(e); - console.log("error submtting request ", e); - handleSubmissionError(); - }); - }; - const handleEditSave = (params) => { - params = params || {}; - if (!Object.keys(params).length) params = { - id: lastEntryId, - date: editDate, - type: editType - }; - setUpdateInProgress(true); - handleUpdate({...params, ...{ - method:"PUT" - }}, () => setTimeout(setUpdateInProgress(false), 350)); - }; - const handleDelete = (params) => { - params = params || {}; - if (!Object.keys(params).length) params = { - id: lastEntryId, - date: editDate, - type: editType - }; - setUpdateInProgress(true); - handleUpdate({...params, ...{ - method:"DELETE" - }}, () => setTimeout(setUpdateInProgress(false), 350)); - }; - const handleSubmissionError = () => { - setError("Data submission failed. Unable to process your request."); - setSnackOpen(false); - }; - const handleEnableEditMode = () => { - setEditType(lastType); - setEditDate(lastUrineScreenDate); - setError(""); - setEditMode(true); - }; - const handleDisableEditMode = () => { - resetEdits(); - setError(""); - setEditMode(false); - }; - const isValidEditType = () => { - if (onlyOneUrineScreenType()) return true; - return editType; - }; - const isValidEditDate = () => { - let dateObj = new Date(editDate).setHours(0,0,0,0); - let today = new Date().setHours(0,0,0,0); - return isValid(dateObj) && !(dateObj > today); - }; - const hasValidEditEntry = () => { - return isValidEditType() && isValidEditDate(); - }; - const handleEditChange = (event) => { - setEditDate(event.target.value); - }; - const hasHistory = () => { - return (history && history.length > 0); - }; - const createHistoryData = (data) => { - if (!data) return []; - return data.map((item,index) => { - const resource = item.resource; - if (!resource) return {}; - let text = resource.code ? resource.code.text : ""; - let date = getShortDateFromISODateString(resource.authoredOn); - let type = resource.code && resource.code.coding && resource.code.coding.length ? resource.code.coding[0].code : ""; - return { - id: resource.id, - type: type, - text: text, - date: date, - index: index, - patientId: rowData.id - }; - }); - }; - const displayHistory = () => { - if (!hasHistory()) return ""; - if (history[0].text) return history[0].text + " ordered on " + lastUrineScreenDate + ""; - return "Ordered on " + lastUrineScreenDate + ""; - }; - const displayEditHistoryByRow = (index, selectType, selectDate) => { - if (!hasHistory()) return null; - if (!index) index = 0; - selectType = selectType || history[index].type; - selectDate = selectDate || getShortDateFromISODateString(history[index].date); - const orderText = history[index].text ? history[index].text : ""; - return ( - - {onlyOneUrineScreenType()? - orderText: - - - {`(${URINE_SCREEN_TYPE_LABEL})`} - - } - ordered on -
handleEditChange(e)} - handleKeyDown={(e) => handleEditSave(e)} - inputClass={{input: classes.editInput}} - error={hasError()}>
-
- ); + callback(e); + } + ) + .then(() => { + setSnackOpen(true); + setTimeout(() => { + getHistory(callback); + }, 150); + }) + .catch((e) => { + callback(e); + console.log("error submtting request ", e); + handleSubmissionError(); + }); + }; + const handleEditSave = (params) => { + params = params || {}; + if (!Object.keys(params).length) + params = { + id: lastEntry.id, + date: editEntry.date, + type: editEntry.type, }; - const getOneUrineScreenDisplayText = () => { - let matchedType = urineScreenTypes[0]; - if (matchedType) return matchedType.text; - else return ""; - }; - const onlyOneUrineScreenType = () => { - return urineScreenTypes.length === 1; - }; - const initUrineScreenTypes = () => { - getSettings(data => { - if (data && data["UDS_LAB_TYPES"]) { - setUrineScreenTypes(data["UDS_LAB_TYPES"]); - let types = {}; - data["UDS_LAB_TYPES"].forEach(item => { - types[item.code] = item.text; - }); - setSelectTypeLookup(types); - } - setTimeout(() => setUrineScreenTypesInitialized(true), 150); - }); - }; - const hasUrineScreenTypes = () => { - return !onlyOneUrineScreenType() && !noUrineScreenTypes(); - }; - const noUrineScreenTypes = () => { - return !urineScreenTypes || !urineScreenTypes.length; - }; - const getUrineScreenTypeSelectList = () => { - return urineScreenTypes.map(item => { - return {item.text}; - }); - }; - const columns = [ - { - field: "id", - hidden: true + setUpdateInProgress(true); + handleUpdate( + { + ...params, + ...{ + method: "PUT", }, - { - title: "Urine Drug Screen Name", - field: "type", - emptyValue: "--", - cellStyle: { - "padding": "4px 24px 4px 16px" - }, - lookup: selectTypeLookup, - editable: !onlyOneUrineScreenType() ? "always" : "never" - }, - { - title: "Order Date", - field: "date", - emptyValue: "--", - cellStyle: { - "padding": "4px 24px 4px 16px" - }, - editComponent: params => params.onChange(e.target.value)}> + }, + () => setTimeout(setUpdateInProgress(false), 350) + ); + }; + const handleDelete = (params) => { + params = params || {}; + if (!Object.keys(params).length) + params = { + id: lastEntry.id, + date: editEntry.date ? editEntry.date : lastEntry.date, + type: editEntry.type ? editEntry.type : lastEntry.type, + }; + setUpdateInProgress(true); + handleUpdate( + { + ...params, + ...{ + method: "DELETE", }, - ]; - React.useEffect(() => { - initUrineScreenTypes(); - }, [!urineScreenTypesInitialized]); - React.useEffect(() => { - if (onlyOneUrineScreenType()) { - //set urine screen type if only one available - setType(urineScreenTypes[0].code); - } - getHistory(); - },[urineScreenTypesInitialized]); - const handleSnackClose = (event, reason) => { - if (reason === "clickaway") { - return; - } - setSnackOpen(false); - }; + }, + () => setTimeout(setUpdateInProgress(false), 350) + ); + }; + const handleSubmissionError = () => { + setError("Data submission failed. Unable to process your request."); + setSnackOpen(false); + }; + const handleEnableEditMode = () => { + editDispatch({ + type: "update", + data: { + type: lastEntry.type, + date: lastEntry.date, + mode: true, + }, + }); + setError(""); + }; + const handleDisableEditMode = () => { + editDispatch({ type: "reset" }); + setError(""); + }; + const isValidEditType = () => { + if (onlyOneUrineScreenType()) return true; + return editEntry.type; + }; + const isValidEditDate = () => { + let dateObj = new Date(editEntry.date).setHours(0, 0, 0, 0); + let today = new Date().setHours(0, 0, 0, 0); + return isValid(dateObj) && !(dateObj > today); + }; + const hasValidEditEntry = () => { + return isValidEditType() && isValidEditDate(); + }; + const handleEditDateChange = (event) => { + editDispatch({ + key: "date", + value: event.target.value, + }); + }; + const hasHistory = () => { + return history && history.length > 0; + }; + const createHistoryData = React.useCallback( + (data) => { + if (!data) return []; + return data.map((item, index) => { + const resource = item.resource; + if (!resource) return {}; + let text = resource.code ? resource.code.text : ""; + let date = getShortDateFromISODateString(resource.authoredOn); + let type = + resource.code && resource.code.coding && resource.code.coding.length + ? resource.code.coding[0].code + : ""; + return { + id: resource.id, + type: type, + text: text, + date: date, + index: index, + patientId: getPatientId(), + }; + }); + }, + [getPatientId] + ); + const displayMostRecentEntry = () => { + if (!hasHistory()) return ""; + if (history[0].text) + return history[0].text + " ordered on " + lastEntry.date + ""; + return "Ordered on " + lastEntry.date + ""; + }; + const displayEditHistoryByRow = (index) => { + if (!hasHistory()) return null; + if (!index) index = 0; + const selectType = history[index].type; + const selectDate = history[index].date; + const orderText = history[index].text || ""; return ( -
-

{`Urine Drug Toxicology Screen for ${rowData.first_name} ${rowData.last_name}`}

-
- {addInProgress &&
} - {/* UI to add new */} - - - Add New - - {/* urine screen date/datepicker */} -
- - {/* order date field */} - Order Date - - { clearDate(); }} style={{order: 2, padding: 0}} aria-label="Clear date" title="Clear date"> - - - - ), - className: classes.dateInput - }} - format="yyyy-MM-dd" - minDate={new Date("1950-01-01")} - maxDateMessage="Date must not be in the future" - invalidDateMessage="Date must be in YYYY-MM-DD format, e.g. 1977-01-12" - placeholder="YYYY-MM-DD" - value={date} - orientation="landscape" - onChange={(event, dateString) => { - setDateInput(dateString); - if (!event || !isValid(event)) { - if (event && ((String(dateInput).replace(/[-_]/g, "").length) >= 8)) setDate(event); - return; - } - setDate(event); - }} - KeyboardButtonProps={{color: "primary", title: "Date picker"}} - autoFocus - /> - -
- {/* urine screen type selector */} -
- {!urineScreenTypesInitialized &&
} - {urineScreenTypesInitialized &&
- {onlyOneUrineScreenType() &&
- {URINE_SCREEN_TYPE_LABEL} - {getOneUrineScreenDisplayText()} -
- } - {hasUrineScreenTypes() && - {URINE_SCREEN_TYPE_LABEL} - - } - {noUrineScreenTypes() &&
-
} -
} -
-
- - -
-
- {/* history */} - - {!historyInitialized &&
- -
} - {updateInProgress &&
} - - Last Urine Drug Screen - - {historyInitialized && hasHistory() &&
- {!editMode && } - {editMode && displayEditHistoryByRow(0)} - handleEditSave()} - handleDelete={() => handleDelete()} - entryDescription={displayHistory()} - > -
} - {!isAdult(rowData.dob) && !hasHistory() &&
No previously recorded urine drug screen
} - {/* alerts */} - {isAdult(rowData.dob) && } -
- { hasHistory() && historyInitialized && - - History - -
- {history.length} record(s) - {!showHistory && } - {showHistory && } -
-
- {showHistory &&
getHistory()} - onRowDelete={() => getHistory()} - >
} -
-
} - {/* feedback snack popup */} - - - - {/* error message UI */} + + {onlyOneUrineScreenType() ? ( + orderText + ) : ( + + + {`(${URINE_SCREEN_TYPE_LABEL})`} + + )} + ordered on +
+ {" "} + handleEditDateChange(e)} + handleKeyDown={(e) => handleEditSave(e)} + inputClass={{ input: classes.editInput }} + error={hasError()} + > +
+
+ ); + }; + const getOneUrineScreenDisplayText = () => { + let matchedType = urineScreenTypes[0]; + if (matchedType) return matchedType.text; + else return ""; + }; + const onlyOneUrineScreenType = React.useCallback(() => { + return urineScreenTypes.length === 1; + }, [urineScreenTypes]); + + const hasUrineScreenTypes = () => { + return !onlyOneUrineScreenType() && !noUrineScreenTypes(); + }; + const noUrineScreenTypes = () => { + return !urineScreenTypes || !urineScreenTypes.length; + }; + const getUrineScreenTypeSelectList = () => { + return urineScreenTypes.map((item) => { + return ( + + {item.text} + + ); + }); + }; + const getColumns = () => [ + { + field: "id", + hidden: true, + }, + { + title: "Urine Drug Screen Name", + field: "type", + emptyValue: "--", + cellStyle: { + padding: "4px 24px 4px 16px", + }, + lookup: getSelectLookupTypes(), + editable: !onlyOneUrineScreenType() ? "always" : "never", + }, + { + title: "Order Date", + field: "date", + emptyValue: "--", + cellStyle: { + padding: "4px 24px 4px 16px", + }, + editComponent: (params) => ( + params.onChange(e.target.value)} + > + ), + }, + ]; + const getSelectLookupTypes = () => { + if (!urineScreenTypes) return; + let types = {}; + urineScreenTypes.forEach((item) => { + types[item.code] = item.text; + }); + return types; + }; + const handleSnackClose = (event, reason) => { + if (event) event.stopPropagation(); + if (reason === "clickaway") { + return; + } + setSnackOpen(false); + }; + React.useEffect(() => { + getHistory(); + }, [getHistory]); + + return ( +
+

{`Urine Drug Toxicology Screen for ${rowData.first_name} ${rowData.last_name}`}

+
+ {addInProgress && ( +
+ +
+ )} + {/* UI to add new */} + + + Add New + + {/* urine screen date/datepicker */} +
+ + {/* order date field */} + Order Date + + { + clearDate(); + }} + style={{ order: 2, padding: 0 }} + aria-label="Clear date" + title="Clear date" + > + + + + ), + className: classes.dateInput, + }} + format="yyyy-MM-dd" + minDate={new Date("1950-01-01")} + maxDateMessage="Date must not be in the future" + invalidDateMessage="Date must be in YYYY-MM-DD format, e.g. 1977-01-12" + placeholder="YYYY-MM-DD" + value={date} + orientation="landscape" + onChange={(event, dateString) => { + setDateInput(dateString); + if (!event || !isValid(event)) { + if ( + event && + String(dateInput).replace(/[-_]/g, "").length >= 8 + ) + setDate(event); + return; + } + setDate(event); + }} + KeyboardButtonProps={{ color: "primary", title: "Date picker" }} + autoFocus + /> + +
+ {/* urine screen type selector */} +
+
+ {onlyOneUrineScreenType() && ( +
+ + {URINE_SCREEN_TYPE_LABEL} + + + {getOneUrineScreenDisplayText()} + +
+ )} + {hasUrineScreenTypes() && ( + + + {URINE_SCREEN_TYPE_LABEL} + + + + )} + {noUrineScreenTypes() && (
- {error && } +
+ )}
+
+ {!noUrineScreenTypes() && ( +
+ + +
+ )} +
+ {/* history */} + + {(!historyInitialized || updateInProgress) && ( +
+ +
+ )} + {historyInitialized && ( + + + Last Urine Drug Screen + + {!hasHistory() && ( +
No previously recorded urine drug screen
+ )} + {/* most recent entry */} + {hasHistory() && ( + +
+ {!editEntry.mode && ( + + )} + {editEntry.mode && displayEditHistoryByRow(0)} + handleEditSave()} + handleDelete={() => handleDelete()} + entryDescription={displayMostRecentEntry()} + > +
+ {/* alerts */} + {isAdult(rowData.birth_date) && ( +
+ +
+ )} +
+ )} + {/* history table */} + {hasHistory() && ( + + + History + +
+ + {history.length} record(s) + + {!expandHistory && ( + + )} + {expandHistory && ( + + )} +
+
+ {expandHistory && ( +
+ getHistory()} + onRowDelete={() => getHistory()} + > +
+ )} +
+
+ )} +
+ )} +
+ {/* feedback snack popup */} + + + + {/* error message UI */} +
+ {error && }
- ); - +
+
+ ); } - UrineScreen.propTypes = { - rowData: PropTypes.object.isRequired + rowData: PropTypes.object.isRequired, }; diff --git a/patientsearch/src/js/components/Version.js b/patientsearch/src/js/components/Version.js index ff5e240a..d835955d 100644 --- a/patientsearch/src/js/components/Version.js +++ b/patientsearch/src/js/components/Version.js @@ -1,8 +1,8 @@ import React from "react"; import PropTypes from "prop-types"; import { makeStyles } from "@material-ui/core/styles"; -import theme from "../context/theme"; -import { getAppSettings } from "../context/SettingContextProvider"; +import theme from "../themes/theme"; +import { useSettingContext } from "../context/SettingContextProvider"; const useStyles = makeStyles({ container: { @@ -22,7 +22,10 @@ const useStyles = makeStyles({ export default function Version(props) { const classes = useStyles(); const VERSION_STRING = "VERSION_STRING"; - const appSettings = props.appSettings ? props.appSettings : getAppSettings(); //provide default if none provided + const settingsCtx = useSettingContext(); + const appSettings = props.appSettings + ? props.appSettings + : settingsCtx.appSettings; //provide default if none provided const getVersionLink = () => { let version = getVersionString(); if (!version) return ""; @@ -51,9 +54,6 @@ export default function Version(props) { if (!Object.keys(appSettings)) return ""; return appSettings[VERSION_STRING]; } - React.useEffect(() => { - //wait for application settings - }, [appSettings]); return (
{getVersionString() &&
Version Number: {getVersionLink()}
} diff --git a/patientsearch/src/js/constants/consts.js b/patientsearch/src/js/constants/consts.js new file mode 100644 index 00000000..a453af92 --- /dev/null +++ b/patientsearch/src/js/constants/consts.js @@ -0,0 +1,42 @@ +import { forwardRef } from "react"; +import ArrowDownward from "@material-ui/icons/ArrowDownward"; +import Check from "@material-ui/icons/Check"; +import ChevronLeft from "@material-ui/icons/ChevronLeft"; +import ChevronRight from "@material-ui/icons/ChevronRight"; +import ClearIcon from "@material-ui/icons/Clear"; +import Delete from "@material-ui/icons/Delete"; +import Edit from "@material-ui/icons/Edit"; +import FirstPage from "@material-ui/icons/FirstPage"; +import LastPage from "@material-ui/icons/LastPage"; +import Search from "@material-ui/icons/Search"; + +export const tableIcons = { + Check: forwardRef((props, ref) => ( + + )), + Clear: forwardRef((props, ref) => ), + Filter: forwardRef((props, ref) => ( + + )), + FirstPage: forwardRef((props, ref) => ), + Edit: forwardRef((props, ref) => ( + + )), + LastPage: forwardRef((props, ref) => ), + DetailPanel: forwardRef((props, ref) => ( +
+ )), + NextPage: forwardRef((props, ref) => ), + PreviousPage: forwardRef((props, ref) => ( + + )), + SortArrow: forwardRef((props, ref) => ( + + )), + Delete: forwardRef((props, ref) => ( + + Remove + + )), +}; + diff --git a/patientsearch/src/js/containers/App.js b/patientsearch/src/js/containers/App.js new file mode 100644 index 00000000..fe4a5899 --- /dev/null +++ b/patientsearch/src/js/containers/App.js @@ -0,0 +1,36 @@ +import "core-js/stable"; +import "regenerator-runtime/runtime"; +import React, { Component } from "react"; +import Layout from "../layout/Layout"; +import PatientListTable from "../components/PatientListTable"; +import TimeoutModal from "../components/TimeoutModal"; +import Version from "../components/Version"; + +export default class App extends Component { + constructor(props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError() { + //force rendering of fallback UI after an error has been thrown by app + return { hasError: true }; + } + + render () { + if (this.state.hasError) { + return
+

Application Error - Something went wrong.

+

See console for detail

+ +
; + } + return ( + + + + + + ); + } +} diff --git a/patientsearch/src/js/Entry.js b/patientsearch/src/js/containers/Entry.js similarity index 100% rename from patientsearch/src/js/Entry.js rename to patientsearch/src/js/containers/Entry.js diff --git a/patientsearch/src/js/containers/Landing.js b/patientsearch/src/js/containers/Landing.js new file mode 100644 index 00000000..9ad0714f --- /dev/null +++ b/patientsearch/src/js/containers/Landing.js @@ -0,0 +1,16 @@ +import React from "react"; +import { render } from "react-dom"; +import Layout from "../layout/Layout"; +import Info from "../components/Info"; +import Version from "../components/Version"; + +// entry point for pre-authenticated access +render( + +
+ + +
+
, + document.getElementById("content") +); diff --git a/patientsearch/src/js/containers/Logout.js b/patientsearch/src/js/containers/Logout.js new file mode 100644 index 00000000..7503af03 --- /dev/null +++ b/patientsearch/src/js/containers/Logout.js @@ -0,0 +1,56 @@ +import React from "react"; +import { render } from "react-dom"; +import Button from "@material-ui/core/Button"; +import Typography from "@material-ui/core/Typography"; +import Layout from "../layout/Layout"; +import Alert from "../components/Alert"; +import { getUrlParameter } from "../helpers/utility"; +import { useSettingContext } from "../context/SettingContextProvider"; +import "../../styles/app.scss"; + +// Error message, e.g. forbidden error +const AlertMessage = () => { + const appSettings = useSettingContext().appSettings; + if (!getUrlParameter("forbidden")) return ""; // look for forbidden message for now, can be others as well + const message = + appSettings && appSettings["FORBIDDEN_TEXT"] + ? appSettings["FORBIDDEN_TEXT"] + : ""; + if (message) + return ( +
+ +
+ ); + return ""; +}; + +const getMessage = () => { + if (getUrlParameter("user_initiated")) + return "You have been logged out as requested."; + if (getUrlParameter("timeout")) + return "Your session has expired. For security purposes, we recommend closing your browser window. You can always log back in."; + return "You have been logged out."; +}; +// logout entry point +render( + +
+ + {getMessage()} + + +
+ +
+
, + document.getElementById("content") +); diff --git a/patientsearch/src/js/context/SettingContextProvider.js b/patientsearch/src/js/context/SettingContextProvider.js index ce8760f0..19668322 100644 --- a/patientsearch/src/js/context/SettingContextProvider.js +++ b/patientsearch/src/js/context/SettingContextProvider.js @@ -1,12 +1,13 @@ import React, { useContext, useState, useEffect } from "react"; import PropTypes from "prop-types"; -import {getSettings} from "../components/Utility"; -const SettingContext = React.createContext(); +import CircularProgress from "@material-ui/core/CircularProgress"; +import {getSettings} from "../helpers/utility"; +const SettingContext = React.createContext({}); /* * context provider component that allows application settings to be accessible to its children component(s) */ export default function SettingContextProvider({children}) { - const [appSettings, setAppSettings] = useState({}); + const [appSettings, setAppSettings] = useState(null); useEffect(() => { getSettings((data) => { if (data && !data.error) { @@ -18,10 +19,19 @@ export default function SettingContextProvider({children}) { value={React.useMemo(() => ({appSettings, setAppSettings}), [ appSettings, setAppSettings - ])}>{children}; + ])}> + {({appSettings}) => { + if (appSettings) return children; + return ( +
+ Loading... +
+ ); + }}
+ ; } SettingContextProvider.propTypes = { - children: PropTypes.element.isRequired + children: PropTypes.oneOfType([PropTypes.array, PropTypes.element]) }; /* * helper function to access application setting context @@ -30,22 +40,6 @@ export function useSettingContext() { const context = useContext(SettingContext); if (context === undefined) { throw new Error("Context must be used within a Provider"); - } - return context; -} - -/* - * helper function to access application setting object - */ -export function getAppSettings() { - let appSettings = {}; - let appCtx = null; - try { - appCtx = useSettingContext(); - appSettings = appCtx ? appCtx.appSettings : {}; - } catch(e) { - console.log("Error retrieving context ", e); - return appSettings; } - return appSettings; + return context; } diff --git a/patientsearch/src/js/context/consts.js b/patientsearch/src/js/context/consts.js deleted file mode 100644 index 43b4662b..00000000 --- a/patientsearch/src/js/context/consts.js +++ /dev/null @@ -1,39 +0,0 @@ -import { forwardRef } from "react"; -import ArrowDownward from "@material-ui/icons/ArrowDownward"; -import Check from "@material-ui/icons/Check"; -import ChevronLeft from "@material-ui/icons/ChevronLeft"; -import ChevronRight from "@material-ui/icons/ChevronRight"; -import ClearIcon from "@material-ui/icons/Clear"; -import Delete from "@material-ui/icons/Delete"; -import Edit from "@material-ui/icons/Edit"; -import FirstPage from "@material-ui/icons/FirstPage"; -import LastPage from "@material-ui/icons/LastPage"; -import Search from "@material-ui/icons/Search"; - -export const tableIcons = { - Check: forwardRef((props, ref) => ( - - )), - Clear: forwardRef((props, ref) => ), - Filter: forwardRef((props, ref) => ( - - )), - FirstPage: forwardRef((props, ref) => ), - Edit: forwardRef((props, ref) => ), - LastPage: forwardRef((props, ref) => ), - DetailPanel: forwardRef((props, ref) => ( -
- )), - NextPage: forwardRef((props, ref) => ), - PreviousPage: forwardRef((props, ref) => ( - - )), - SortArrow: forwardRef((props, ref) => ( - - )), - Delete: forwardRef((props, ref) => ( - - Remove - - )), -}; diff --git a/patientsearch/src/js/components/Utility.js b/patientsearch/src/js/helpers/utility.js similarity index 78% rename from patientsearch/src/js/components/Utility.js rename to patientsearch/src/js/helpers/utility.js index 4fff23c6..3868694c 100644 --- a/patientsearch/src/js/components/Utility.js +++ b/patientsearch/src/js/helpers/utility.js @@ -280,3 +280,77 @@ export function padDateString(dateString) { let day = pad(arrDate[2]); return [year, month, day].join("-"); } + +export async function validateToken() { + const response = await fetch("./validate_token"); + if (!response.ok) { + throw response; + } + const tokenData = await response.json(); + if ( + !tokenData || + (tokenData && + (!tokenData.valid || + parseInt(tokenData.access_expires_in) <= 0 || + parseInt(tokenData.refresh_expires_in) <= 0)) + ) { + return false; + } + return tokenData; +} + +export function getRolesFromToken(token) { + token = token || {}; + let roles = []; + const ACCESS_TOKEN_KEY = "access_token"; + const REALM_ACCESS_KEY = "realm_access"; + if (token[ACCESS_TOKEN_KEY] && token[ACCESS_TOKEN_KEY][REALM_ACCESS_KEY]) { + const realmAccessObj = token[ACCESS_TOKEN_KEY][REALM_ACCESS_KEY]; + if (realmAccessObj["roles"]) { + roles = [...roles, ...realmAccessObj["roles"]]; + } + } + return roles; +} + +export function getClientsByRequiredRoles(sofClients, currentRoles) { + if (!sofClients) { + return; + } + //CHECK user role(s) against each SoF client app's REQUIRED_ROLES + return sofClients.filter((item) => { + const requiredRoles = item["required_roles"] || item["REQUIRED_ROLES"]; + if (!requiredRoles) return true; + if (Array.isArray(requiredRoles) && !Array.isArray(currentRoles)) + return requiredRoles.indexOf(currentRoles) !== -1; + if (!Array.isArray(requiredRoles) && Array.isArray(currentRoles)) + return currentRoles.filter((role) => role === currentRoles).length > 0; + if (Array.isArray(requiredRoles) && Array.isArray(currentRoles)) + return ( + requiredRoles.filter((role) => currentRoles.indexOf(role) !== -1) + .length > 0 + ); + return requiredRoles === currentRoles; + }); +} + +export function handleExpiredSession() { + sessionStorage.clear(); + setTimeout(() => { + // /home is a protected endpoint, the backend will request a new Access Token from Keycloak if able, else prompt a user to log in again + window.location = "/home"; + }, 0); +} + +export function setDocumentTitle(title) { + if (!title) return; + document.title = title; +} + +export function setFavicon(href) { + if (!href) return; + let faviconEl = document.querySelector("link[rel*='icon']"); + if (!faviconEl) return; + faviconEl.href = href; +} + diff --git a/patientsearch/src/js/layout/Layout.js b/patientsearch/src/js/layout/Layout.js new file mode 100644 index 00000000..78c365be --- /dev/null +++ b/patientsearch/src/js/layout/Layout.js @@ -0,0 +1,27 @@ +import PropTypes from "prop-types"; +import React from "react"; +import CssBaseline from "@material-ui/core/CssBaseline"; +import { ThemeProvider } from "@material-ui/styles"; +import Header from "../components/Header"; +import SystemBanner from "../components/SystemBanner"; +import SettingContextProvider from "../context/SettingContextProvider"; +import theme from "../themes/theme"; +import "../../styles/app.scss"; + +export default function Layout({children}) { + return ( + + + + + +
+ {children} + + + + ); +} +Layout.propTypes = { + children: PropTypes.oneOfType([PropTypes.element, PropTypes.array]), +}; diff --git a/patientsearch/src/js/context/theme.js b/patientsearch/src/js/themes/theme.js similarity index 100% rename from patientsearch/src/js/context/theme.js rename to patientsearch/src/js/themes/theme.js diff --git a/patientsearch/src/styles/app.scss b/patientsearch/src/styles/app.scss index 8f0ed30e..c24bbe8f 100644 --- a/patientsearch/src/styles/app.scss +++ b/patientsearch/src/styles/app.scss @@ -33,6 +33,9 @@ html { .muted { color: $color-muted; } +.invisible { + visibility: hidden; +} .ghost { display: none; z-index: -1; @@ -260,17 +263,17 @@ button.disabled:focus, .MuiInputLabel-formControl { font-size: $small-font-size; } + .main { + tr { + td:last-child { + min-width: 96px; + } + } + } table { position: relative; overflow: hidden; } - tr { - td { - button:not(:first-of-type):not(:last-of-type) { - order: 10 - } - } - } } #patientListPagination { .MuiSelect-root { diff --git a/patientsearch/src/assets/favicon.ico b/patientsearch/static/COSRI_favicon.ico similarity index 100% rename from patientsearch/src/assets/favicon.ico rename to patientsearch/static/COSRI_favicon.ico diff --git a/patientsearch/static/DCW_favicon.ico b/patientsearch/static/DCW_favicon.ico new file mode 100644 index 00000000..950c61ff Binary files /dev/null and b/patientsearch/static/DCW_favicon.ico differ diff --git a/patientsearch/src/assets/logo_horizontal.png b/patientsearch/static/app/img/COSRI_logo.png similarity index 100% rename from patientsearch/src/assets/logo_horizontal.png rename to patientsearch/static/app/img/COSRI_logo.png diff --git a/patientsearch/static/app/img/DCW_logo.png b/patientsearch/static/app/img/DCW_logo.png new file mode 100644 index 00000000..0073f0b2 Binary files /dev/null and b/patientsearch/static/app/img/DCW_logo.png differ diff --git a/patientsearch/static/favicon.ico b/patientsearch/static/favicon.ico deleted file mode 100644 index b7758503..00000000 Binary files a/patientsearch/static/favicon.ico and /dev/null differ diff --git a/webpack.config.js b/webpack.config.js index 43f3f284..4ddfec40 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -5,9 +5,9 @@ const OptimizeCssAssetsPlugin = require("optimize-css-assets-webpack-plugin"); const HtmlWebpackPlugin = require("html-webpack-plugin"); const { CleanWebpackPlugin } = require("clean-webpack-plugin"); const FileManagerPlugin = require("filemanager-webpack-plugin"); -const appTitle = "COSRI Patient Search"; +/* document title is being populated at runtime, this is just a placeholder */ +const appTitle = "Patient Search"; const templateFilePath = path.join(__dirname, "/patientsearch/src/index.html"); -const faviconFilePath = path.join(__dirname, "/patientsearch/src/assets/favicon.ico"); module.exports = function(_env, argv) { const isProduction = argv.mode === "production"; @@ -21,9 +21,9 @@ module.exports = function(_env, argv) { return { entry: { - "index" : ["whatwg-fetch", path.join(__dirname, "/patientsearch/src/js/Entry.js")], - "info": ["whatwg-fetch", path.join(__dirname, "/patientsearch/src/js/Landing.js")], - "logout": ["whatwg-fetch", path.join(__dirname, "/patientsearch/src/js/Logout.js")] + "index" : ["whatwg-fetch", path.join(__dirname, "/patientsearch/src/js/containers/Entry.js")], + "info": ["whatwg-fetch", path.join(__dirname, "/patientsearch/src/js/containers/Landing.js")], + "logout": ["whatwg-fetch", path.join(__dirname, "/patientsearch/src/js/containers/Logout.js")] }, watchOptions: { aggregateTimeout: 300, @@ -87,21 +87,18 @@ module.exports = function(_env, argv) { title: appTitle, template: templateFilePath, filename: path.join(__dirname, `${templateDirectory}/index.html`), - favicon: faviconFilePath, chunks: ["index"] }), new HtmlWebpackPlugin({ title: appTitle, template: templateFilePath, filename: path.join(__dirname, `${templateDirectory}/home.html`), - favicon: faviconFilePath, chunks: ["info"] }), new HtmlWebpackPlugin({ title: appTitle, template: templateFilePath, filename: path.join(__dirname, `${templateDirectory}/logout.html`), - favicon: faviconFilePath, chunks: ["logout"] }), new webpack.ProvidePlugin({