From 15ab412c2fc1c77d64c44949b2fe795fa0287ae5 Mon Sep 17 00:00:00 2001 From: Marta Girdea Date: Sun, 21 Jul 2024 20:18:50 -0400 Subject: [PATCH 01/11] CARDS-2569: Allow configuring the menu items to be displayed for a Form --- .../main/frontend/src/questionnaire/Form.jsx | 115 +++++++++++------- 1 file changed, 69 insertions(+), 46 deletions(-) diff --git a/modules/data-entry/src/main/frontend/src/questionnaire/Form.jsx b/modules/data-entry/src/main/frontend/src/questionnaire/Form.jsx index d8ff90018d..99ff538cdf 100644 --- a/modules/data-entry/src/main/frontend/src/questionnaire/Form.jsx +++ b/modules/data-entry/src/main/frontend/src/questionnaire/Form.jsx @@ -469,14 +469,24 @@ function Form (props) { ); } + let isActionEnabled = (action) => (!!!actionSwitches || !!(actionSwitches[action]?.(data))); + + let isDropdnEnabled = () => ( + (isEdit && isActionEnabled("subject")) + || (!isEdit && isActionEnabled("text")) + || isActionEnabled("delete") + ); + let dropdownList = ( { isEdit ? - - - + ( isActionEnabled("subject") && + + + + ) : <> setActionsMenu(null)} /> - - - + ( isActionEnabled("text") && + + + + ) } + + { isActionEnabled("delete") && + } ) let formMenu = (
{isEdit ? - - - - - + ( isActionEnabled("save") && + + + + + + ) : - - - - - + ( isActionEnabled("edit") && + + + + + + ) + } + { isDropdnEnabled() && + <> + setActionsMenu(event.currentTarget)}> + + + + + { !actionsMenu &&
{ dropdownList }
} + setActionsMenu(null)} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'right', + }} + transformOrigin={{ + vertical: 'top', + horizontal: 'right', + }} + > + { dropdownList } + + } - setActionsMenu(event.currentTarget)}> - - - - - { !actionsMenu &&
{ dropdownList }
} - setActionsMenu(null)} - anchorOrigin={{ - vertical: 'bottom', - horizontal: 'right', - }} - transformOrigin={{ - vertical: 'top', - horizontal: 'right', - }} - > - { dropdownList } -
) From ad78683830b6e7333e883fea99395a468c6ff2ce Mon Sep 17 00:00:00 2001 From: Sergiu Dumitriu Date: Mon, 9 Jun 2025 17:02:26 -0400 Subject: [PATCH 02/11] CARDS-2461: Clinician-oriented subject and form views Move the clinician portal code in its own module --- modules/clinician-portal/pom.xml | 58 +++++++++++++++++++ .../src/main/features/feature.json | 25 ++++++++ .../src/main/frontend/assets.config | 8 +++ .../src/clinician-portal}/ClinicDashboard.jsx | 0 .../src/clinician-portal}/ClinicFormList.jsx | 0 .../src/clinician-portal}/ClinicForms.jsx | 0 .../src/clinician-portal}/ClinicStyle.jsx | 0 .../src/clinician-portal}/ClinicVisits.jsx | 0 .../DashboardSettingsConfiguration.jsx | 0 .../src/clinician-portal}/PrintHeader.jsx | 0 .../DashboardSettingsConfiguration.json | 2 +- .../Extensions/PrintHeader/PatientInfo.json | 2 +- .../Extensions/Views/ClinicDashboard.json | 2 +- .../Views/DashboardSettingsConfiguration.json | 2 +- .../cards/resources/assetDependencies.json | 6 ++ .../src/main/frontend/assets.config | 3 - .../cards/resources/assetDependencies.json | 6 +- modules/pom.xml | 1 + 18 files changed, 103 insertions(+), 12 deletions(-) create mode 100644 modules/clinician-portal/pom.xml create mode 100644 modules/clinician-portal/src/main/features/feature.json create mode 100644 modules/clinician-portal/src/main/frontend/assets.config rename modules/{patient-portal/src/main/frontend/src/patient-portal => clinician-portal/src/main/frontend/src/clinician-portal}/ClinicDashboard.jsx (100%) rename modules/{patient-portal/src/main/frontend/src/patient-portal => clinician-portal/src/main/frontend/src/clinician-portal}/ClinicFormList.jsx (100%) rename modules/{patient-portal/src/main/frontend/src/patient-portal => clinician-portal/src/main/frontend/src/clinician-portal}/ClinicForms.jsx (100%) rename modules/{patient-portal/src/main/frontend/src/patient-portal => clinician-portal/src/main/frontend/src/clinician-portal}/ClinicStyle.jsx (100%) rename modules/{patient-portal/src/main/frontend/src/patient-portal => clinician-portal/src/main/frontend/src/clinician-portal}/ClinicVisits.jsx (100%) rename modules/{patient-portal/src/main/frontend/src/patient-portal => clinician-portal/src/main/frontend/src/clinician-portal}/DashboardSettingsConfiguration.jsx (100%) rename modules/{patient-portal/src/main/frontend/src/patient-portal => clinician-portal/src/main/frontend/src/clinician-portal}/PrintHeader.jsx (100%) rename modules/{patient-portal => clinician-portal}/src/main/resources/SLING-INF/content/Extensions/AdminDashboard/DashboardSettingsConfiguration.json (80%) rename modules/{patient-portal => clinician-portal}/src/main/resources/SLING-INF/content/Extensions/PrintHeader/PatientInfo.json (73%) rename modules/{patient-portal => clinician-portal}/src/main/resources/SLING-INF/content/Extensions/Views/ClinicDashboard.json (73%) rename modules/{patient-portal => clinician-portal}/src/main/resources/SLING-INF/content/Extensions/Views/DashboardSettingsConfiguration.json (70%) create mode 100644 modules/clinician-portal/src/main/resources/SLING-INF/content/libs/cards/resources/assetDependencies.json diff --git a/modules/clinician-portal/pom.xml b/modules/clinician-portal/pom.xml new file mode 100644 index 0000000000..183ad76783 --- /dev/null +++ b/modules/clinician-portal/pom.xml @@ -0,0 +1,58 @@ + + + + 4.0.0 + + + io.uhndata.cards + cards-modules + 0.9.32-SNAPSHOT + + + cards-clinician-portal + bundle + CARDS - Clinician Portal + An alternative view of patients, visits and forms, more suited for clinicians + + + + + org.apache.sling + slingfeature-maven-plugin + + + + org.apache.felix + maven-bundle-plugin + true + + + + SLING-INF/content/libs/cards/resources/assetDependencies.json;path:=/libs/cards/resources/assetDependencies;overwriteProperties:=true, + SLING-INF/content/Extensions/AdminDashboard/;path:=/Extensions/AdminDashboard/;overwriteProperties:=true;uninstall:=true, + SLING-INF/content/Extensions/PrintHeader/;path:=/Extensions/PrintHeader/;overwriteProperties:=true;uninstall:=true, + SLING-INF/content/Extensions/Views/;path:=/Extensions/Views/;overwriteProperties:=true;uninstall:=true, + + + + + + + diff --git a/modules/clinician-portal/src/main/features/feature.json b/modules/clinician-portal/src/main/features/feature.json new file mode 100644 index 0000000000..f1d7c0be94 --- /dev/null +++ b/modules/clinician-portal/src/main/features/feature.json @@ -0,0 +1,25 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +{ + "bundles":[ + { + "id":"${project.groupId}:${project.artifactId}:${project.version}", + "start-order":"26" + } + ] +} diff --git a/modules/clinician-portal/src/main/frontend/assets.config b/modules/clinician-portal/src/main/frontend/assets.config new file mode 100644 index 0000000000..3aeda10246 --- /dev/null +++ b/modules/clinician-portal/src/main/frontend/assets.config @@ -0,0 +1,8 @@ +// These lines are aggregated as module.exports entries in webpack.config.js file by the webpack_script.py in aggregated-frontend module + +['clinician-portal.ClinicForms']: { 'dependOn': ['cards-dataentry.LiveTable'], 'import': './src/clinician-portal/ClinicForms.jsx' } +['clinician-portal.ClinicVisits']: { 'dependOn': ['cards-dataentry.LiveTable'], 'import': './src/clinician-portal/ClinicVisits.jsx' } +['clinician-portal.ClinicDashboard']: { 'dependOn': ['cards-dataentry.Questionnaires', 'clinician-portal.ClinicForms', 'clinician-portal.ClinicVisits'], 'import': './src/clinician-portal/ClinicDashboard.jsx' } +['clinician-portal.PrintHeader']: './src/clinician-portal/PrintHeader.jsx' +['clinician-portal.DashboardSettingsConfiguration']: { 'dependOn': ['cards-login.ReLoginDialog'], 'import': './src/clinician-portal/DashboardSettingsConfiguration.jsx' } +['clinician-portal.DashboardSettingsConfigurationIcon']: '@mui/icons-material/Dashboard' diff --git a/modules/patient-portal/src/main/frontend/src/patient-portal/ClinicDashboard.jsx b/modules/clinician-portal/src/main/frontend/src/clinician-portal/ClinicDashboard.jsx similarity index 100% rename from modules/patient-portal/src/main/frontend/src/patient-portal/ClinicDashboard.jsx rename to modules/clinician-portal/src/main/frontend/src/clinician-portal/ClinicDashboard.jsx diff --git a/modules/patient-portal/src/main/frontend/src/patient-portal/ClinicFormList.jsx b/modules/clinician-portal/src/main/frontend/src/clinician-portal/ClinicFormList.jsx similarity index 100% rename from modules/patient-portal/src/main/frontend/src/patient-portal/ClinicFormList.jsx rename to modules/clinician-portal/src/main/frontend/src/clinician-portal/ClinicFormList.jsx diff --git a/modules/patient-portal/src/main/frontend/src/patient-portal/ClinicForms.jsx b/modules/clinician-portal/src/main/frontend/src/clinician-portal/ClinicForms.jsx similarity index 100% rename from modules/patient-portal/src/main/frontend/src/patient-portal/ClinicForms.jsx rename to modules/clinician-portal/src/main/frontend/src/clinician-portal/ClinicForms.jsx diff --git a/modules/patient-portal/src/main/frontend/src/patient-portal/ClinicStyle.jsx b/modules/clinician-portal/src/main/frontend/src/clinician-portal/ClinicStyle.jsx similarity index 100% rename from modules/patient-portal/src/main/frontend/src/patient-portal/ClinicStyle.jsx rename to modules/clinician-portal/src/main/frontend/src/clinician-portal/ClinicStyle.jsx diff --git a/modules/patient-portal/src/main/frontend/src/patient-portal/ClinicVisits.jsx b/modules/clinician-portal/src/main/frontend/src/clinician-portal/ClinicVisits.jsx similarity index 100% rename from modules/patient-portal/src/main/frontend/src/patient-portal/ClinicVisits.jsx rename to modules/clinician-portal/src/main/frontend/src/clinician-portal/ClinicVisits.jsx diff --git a/modules/patient-portal/src/main/frontend/src/patient-portal/DashboardSettingsConfiguration.jsx b/modules/clinician-portal/src/main/frontend/src/clinician-portal/DashboardSettingsConfiguration.jsx similarity index 100% rename from modules/patient-portal/src/main/frontend/src/patient-portal/DashboardSettingsConfiguration.jsx rename to modules/clinician-portal/src/main/frontend/src/clinician-portal/DashboardSettingsConfiguration.jsx diff --git a/modules/patient-portal/src/main/frontend/src/patient-portal/PrintHeader.jsx b/modules/clinician-portal/src/main/frontend/src/clinician-portal/PrintHeader.jsx similarity index 100% rename from modules/patient-portal/src/main/frontend/src/patient-portal/PrintHeader.jsx rename to modules/clinician-portal/src/main/frontend/src/clinician-portal/PrintHeader.jsx diff --git a/modules/patient-portal/src/main/resources/SLING-INF/content/Extensions/AdminDashboard/DashboardSettingsConfiguration.json b/modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/AdminDashboard/DashboardSettingsConfiguration.json similarity index 80% rename from modules/patient-portal/src/main/resources/SLING-INF/content/Extensions/AdminDashboard/DashboardSettingsConfiguration.json rename to modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/AdminDashboard/DashboardSettingsConfiguration.json index 7f96902681..06ac8d1161 100644 --- a/modules/patient-portal/src/main/resources/SLING-INF/content/Extensions/AdminDashboard/DashboardSettingsConfiguration.json +++ b/modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/AdminDashboard/DashboardSettingsConfiguration.json @@ -3,7 +3,7 @@ "cards:extensionPointId": "cards/coreUI/admindashboard/entry", "cards:extensionName": "Clinic dashboard", "cards:targetURL": "/content.html/admin/DashboardSettingsConfiguration", - "cards:icon": "asset:patient-portal.DashboardSettingsConfigurationIcon.js", + "cards:icon": "asset:clinician-portal.DashboardSettingsConfigurationIcon.js", "cards:hint": "Configure how data is displayed on the clinic dashboard", "cards:defaultOrder": 420 } diff --git a/modules/patient-portal/src/main/resources/SLING-INF/content/Extensions/PrintHeader/PatientInfo.json b/modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/PrintHeader/PatientInfo.json similarity index 73% rename from modules/patient-portal/src/main/resources/SLING-INF/content/Extensions/PrintHeader/PatientInfo.json rename to modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/PrintHeader/PatientInfo.json index 098d00d958..5cdc3b1521 100644 --- a/modules/patient-portal/src/main/resources/SLING-INF/content/Extensions/PrintHeader/PatientInfo.json +++ b/modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/PrintHeader/PatientInfo.json @@ -2,6 +2,6 @@ "jcr:primaryType": "cards:Extension", "cards:extensionPointId": "cards/coreUI/print/header", "cards:extensionName": "Print header with patient information", - "cards:extensionRenderURL": "asset:patient-portal.PrintHeader.js", + "cards:extensionRenderURL": "asset:clinician-portal.PrintHeader.js", "cards:defaultOrder": 10 } diff --git a/modules/patient-portal/src/main/resources/SLING-INF/content/Extensions/Views/ClinicDashboard.json b/modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/Views/ClinicDashboard.json similarity index 73% rename from modules/patient-portal/src/main/resources/SLING-INF/content/Extensions/Views/ClinicDashboard.json rename to modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/Views/ClinicDashboard.json index 18799affc4..48614232bd 100644 --- a/modules/patient-portal/src/main/resources/SLING-INF/content/Extensions/Views/ClinicDashboard.json +++ b/modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/Views/ClinicDashboard.json @@ -2,7 +2,7 @@ "jcr:primaryType": "cards:Extension", "cards:extensionPointId": "cards/coreUI/view", "cards:extensionName": "Dashboard", - "cards:extensionRenderURL": "asset:patient-portal.ClinicDashboard.js", + "cards:extensionRenderURL": "asset:clinician-portal.ClinicDashboard.js", "cards:targetURL": "/content.html/Dashboard/*", "cards:exactURLMatch": false } diff --git a/modules/patient-portal/src/main/resources/SLING-INF/content/Extensions/Views/DashboardSettingsConfiguration.json b/modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/Views/DashboardSettingsConfiguration.json similarity index 70% rename from modules/patient-portal/src/main/resources/SLING-INF/content/Extensions/Views/DashboardSettingsConfiguration.json rename to modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/Views/DashboardSettingsConfiguration.json index cfbae2a806..126fe222ec 100644 --- a/modules/patient-portal/src/main/resources/SLING-INF/content/Extensions/Views/DashboardSettingsConfiguration.json +++ b/modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/Views/DashboardSettingsConfiguration.json @@ -3,5 +3,5 @@ "cards:extensionPointId": "cards/coreUI/view", "cards:extensionName": "Clinic dashboard", "cards:targetURL": "/content.html/admin/DashboardSettingsConfiguration", - "cards:extensionRenderURL": "asset:patient-portal.DashboardSettingsConfiguration.js" + "cards:extensionRenderURL": "asset:clinician-portal.DashboardSettingsConfiguration.js" } diff --git a/modules/clinician-portal/src/main/resources/SLING-INF/content/libs/cards/resources/assetDependencies.json b/modules/clinician-portal/src/main/resources/SLING-INF/content/libs/cards/resources/assetDependencies.json new file mode 100644 index 0000000000..bb3e38192f --- /dev/null +++ b/modules/clinician-portal/src/main/resources/SLING-INF/content/libs/cards/resources/assetDependencies.json @@ -0,0 +1,6 @@ +{ + "clinician-portal.ClinicDashboard.js": ["asset:cards-dataentry.Questionnaires.js", "asset:clinician-portal.ClinicForms.js", "asset:clinician-portal.ClinicVisits.js"], + "clinician-portal.ClinicForms.js": ["asset:cards-dataentry.LiveTable.js"], + "clinician-portal.ClinicVisits.js": ["asset:cards-dataentry.LiveTable.js"], + "clinician-portal.DashboardSettingsConfiguration.js": ["asset:cards-login.ReLoginDialog.js"] +} diff --git a/modules/patient-portal/src/main/frontend/assets.config b/modules/patient-portal/src/main/frontend/assets.config index 9732364ea6..43c872cc35 100644 --- a/modules/patient-portal/src/main/frontend/assets.config +++ b/modules/patient-portal/src/main/frontend/assets.config @@ -10,12 +10,9 @@ ['patient-portal.ClinicDashboard']: { 'dependOn': ['cards-dataentry.Questionnaires', 'patient-portal.ClinicForms', 'patient-portal.ClinicVisits'], 'import': './src/patient-portal/ClinicDashboard.jsx' } ['patient-portal.ClinicIcon']: '@mui/icons-material/Event' ['patient-portal.Clinics']: { 'dependOn': ['cards-dataentry.Questionnaires'], 'import': './src/patient-portal/Clinics.jsx' } -['patient-portal.PrintHeader']: './src/patient-portal/PrintHeader.jsx' ['patient-portal.PatientAccessConfiguration']: { 'dependOn': ['cards-login.ReLoginDialog'], 'import': './src/patient-portal/PatientAccessConfiguration.jsx' } ['patient-portal.PatientAccessConfigurationIcon']: '@mui/icons-material/MedicalInformation' ['patient-portal.ToUConfiguration']: { 'dependOn': ['cards-login.ReLoginDialog'], 'import': './src/patient-portal/ToUConfiguration.jsx' } ['patient-portal.ToUConfigurationIcon']: '@mui/icons-material/Handshake' ['patient-portal.SurveyInstructionsConfiguration']: { 'dependOn': ['cards-login.ReLoginDialog'], 'import': './src/patient-portal/SurveyInstructionsConfiguration.jsx' } ['patient-portal.SurveyInstructionsConfigurationIcon']: '@mui/icons-material/Quiz' -['patient-portal.DashboardSettingsConfiguration']: { 'dependOn': ['cards-login.ReLoginDialog'], 'import': './src/patient-portal/DashboardSettingsConfiguration.jsx' } -['patient-portal.DashboardSettingsConfigurationIcon']: '@mui/icons-material/Dashboard' diff --git a/modules/patient-portal/src/main/resources/SLING-INF/content/libs/cards/resources/assetDependencies.json b/modules/patient-portal/src/main/resources/SLING-INF/content/libs/cards/resources/assetDependencies.json index f6418842b9..16485da9bd 100644 --- a/modules/patient-portal/src/main/resources/SLING-INF/content/libs/cards/resources/assetDependencies.json +++ b/modules/patient-portal/src/main/resources/SLING-INF/content/libs/cards/resources/assetDependencies.json @@ -1,10 +1,6 @@ { - "patient-portal.ClinicDashboard.js": ["asset:cards-dataentry.Questionnaires.js", "asset:patient-portal.ClinicForms.js", "asset:patient-portal.ClinicVisits.js"], - "patient-portal.ClinicForms.js": ["asset:cards-dataentry.LiveTable.js"], - "patient-portal.ClinicVisits.js": ["asset:cards-dataentry.LiveTable.js"], "patient-portal.Clinics.js": ["asset:cards-dataentry.Questionnaires.js"], "patient-portal.PatientAccessConfiguration.js": ["asset:cards-login.ReLoginDialog.js"], "patient-portal.ToUConfiguration.js": ["asset:cards-login.ReLoginDialog.js"], - "patient-portal.SurveyInstructionsConfiguration.js": ["asset:cards-login.ReLoginDialog.js"], - "patient-portal.DashboardSettingsConfiguration.js": ["asset:cards-login.ReLoginDialog.js"] + "patient-portal.SurveyInstructionsConfiguration.js": ["asset:cards-login.ReLoginDialog.js"] } diff --git a/modules/pom.xml b/modules/pom.xml index 933274a8e5..a61b4e1441 100644 --- a/modules/pom.xml +++ b/modules/pom.xml @@ -43,6 +43,7 @@ data-entry data-model patient-portal + clinician-portal ldap-support saml-support utils From fcbd8ed69953cfee805735a87ce37d3cdd996f36 Mon Sep 17 00:00:00 2001 From: Sergiu Dumitriu Date: Tue, 24 Jun 2025 18:35:39 -0400 Subject: [PATCH 03/11] CARDS-2461: Clinician-oriented subject and form views --- .../src/main/frontend/package.json | 1 + .../src/main/frontend/assets.config | 3 + .../frontend/src/clinician-portal/Form.jsx | 37 ++ .../frontend/src/clinician-portal/Patient.jsx | 245 +++++++++++ .../frontend/src/clinician-portal/Visit.jsx | 414 ++++++++++++++++++ .../content/Extensions/Views/Form.json | 8 + .../content/Extensions/Views/Patient.json | 8 + .../content/Extensions/Views/Visit.json | 8 + .../cards/resources/assetDependencies.json | 3 + modules/patient-portal/pom.xml | 1 + .../Questionnaires/Patient information.json | 8 + .../Questionnaires/Visit information.json | 17 + .../SubjectTypes/Patient/ROOT.json | 8 + .../SubjectTypes/Patient/Visit.json | 13 + 14 files changed, 774 insertions(+) create mode 100644 modules/clinician-portal/src/main/frontend/src/clinician-portal/Form.jsx create mode 100644 modules/clinician-portal/src/main/frontend/src/clinician-portal/Patient.jsx create mode 100644 modules/clinician-portal/src/main/frontend/src/clinician-portal/Visit.jsx create mode 100644 modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/Views/Form.json create mode 100644 modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/Views/Patient.json create mode 100644 modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/Views/Visit.json create mode 100644 modules/patient-portal/src/main/resources/SLING-INF/content/apps/cards/config/CopyAnswers/Questionnaires/Patient information.json create mode 100644 modules/patient-portal/src/main/resources/SLING-INF/content/apps/cards/config/CopyAnswers/Questionnaires/Visit information.json create mode 100644 modules/patient-portal/src/main/resources/SLING-INF/content/apps/cards/config/CopyAnswers/SubjectTypes/Patient/ROOT.json create mode 100644 modules/patient-portal/src/main/resources/SLING-INF/content/apps/cards/config/CopyAnswers/SubjectTypes/Patient/Visit.json diff --git a/aggregated-frontend/src/main/frontend/package.json b/aggregated-frontend/src/main/frontend/package.json index bd08423341..cae4afaf52 100644 --- a/aggregated-frontend/src/main/frontend/package.json +++ b/aggregated-frontend/src/main/frontend/package.json @@ -35,6 +35,7 @@ "@uiw/react-md-editor": "4.0.5", "@mui/lab": "7.0.0-beta.11", "@mui/x-date-pickers": "7.28.3", + "@mui/x-data-grid": "8.5.1", "cornerstone-core": "2.6.1", "cornerstone-wado-image-loader": "4.13.2", "dicom-parser": "1.8.21", diff --git a/modules/clinician-portal/src/main/frontend/assets.config b/modules/clinician-portal/src/main/frontend/assets.config index 3aeda10246..03df735030 100644 --- a/modules/clinician-portal/src/main/frontend/assets.config +++ b/modules/clinician-portal/src/main/frontend/assets.config @@ -1,5 +1,8 @@ // These lines are aggregated as module.exports entries in webpack.config.js file by the webpack_script.py in aggregated-frontend module +['clinician-portal.Visit']: { 'dependOn': ['cards-login.ReLoginDialog'], 'import': './src/clinician-portal/Visit.jsx' } +['clinician-portal.Patient']: { 'dependOn': ['cards-login.ReLoginDialog'], 'import': './src/clinician-portal/Patient.jsx' } +['clinician-portal.Form']: { 'dependOn': ['cards-login.ReLoginDialog', 'cards-dataentry.Forms', 'cards-dataentry.FormView', 'cards-pedigree.Pedigree'], 'import': './src/clinician-portal/Form.jsx' } ['clinician-portal.ClinicForms']: { 'dependOn': ['cards-dataentry.LiveTable'], 'import': './src/clinician-portal/ClinicForms.jsx' } ['clinician-portal.ClinicVisits']: { 'dependOn': ['cards-dataentry.LiveTable'], 'import': './src/clinician-portal/ClinicVisits.jsx' } ['clinician-portal.ClinicDashboard']: { 'dependOn': ['cards-dataentry.Questionnaires', 'clinician-portal.ClinicForms', 'clinician-portal.ClinicVisits'], 'import': './src/clinician-portal/ClinicDashboard.jsx' } diff --git a/modules/clinician-portal/src/main/frontend/src/clinician-portal/Form.jsx b/modules/clinician-portal/src/main/frontend/src/clinician-portal/Form.jsx new file mode 100644 index 0000000000..78d23a35c0 --- /dev/null +++ b/modules/clinician-portal/src/main/frontend/src/clinician-portal/Form.jsx @@ -0,0 +1,37 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// +import React from "react"; + +import DefaultForm from "../questionnaire/FormView.jsx"; + +function Form(props) { + + const actionSwitches = { + edit: data => (!["SUBMITTED", "LOCKED"].some(f => data.statusFlags.includes(f))), + save: () => true, + print: () => true, + lock: () => true, + } + + return ( + + ); +} + +export default Form; diff --git a/modules/clinician-portal/src/main/frontend/src/clinician-portal/Patient.jsx b/modules/clinician-portal/src/main/frontend/src/clinician-portal/Patient.jsx new file mode 100644 index 0000000000..0a9cc9fdd6 --- /dev/null +++ b/modules/clinician-portal/src/main/frontend/src/clinician-portal/Patient.jsx @@ -0,0 +1,245 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// +import React, { useState, useEffect, useContext } from "react"; + +import { useNavigate } from "react-router"; + +import { + Alert, + AlertTitle, + Box, + CircularProgress, + Grid, + Tooltip, + Typography, +} from "@mui/material"; + +import ContactPageIcon from '@mui/icons-material/ContactPage'; +import EventNoteIcon from '@mui/icons-material/EventNote'; +import LockIcon from '@mui/icons-material/Lock'; + +import { DataGrid } from '@mui/x-data-grid'; + +import { DateTime } from "luxon"; + +import ResourceHeader from "../questionnaire/ResourceHeader.jsx"; +import DateQuestionUtilities from "../questionnaire/DateQuestionUtilities"; +import { getSubjectIdFromPath, getHierarchyAsList, getHomepageLink } from "../questionnaire/SubjectIdentifier"; +import { FORM_ENTRY_CONTAINER_PROPS } from "../questionnaire/QuestionnaireStyle.jsx"; + +import { fetchWithReLogin, GlobalLoginContext } from "../login/ReLoginDialog.js"; + +const statusIcons = { + "LOCKED" : , + "Default" : , +} + +const visitGridColumns = [ + { + field: 'status', + headerName: '', + width: 50, + renderCell: ({ value }) => ( + value && statusIcons[value] + ? {statusIcons[value]} + : statusIcons["Default"] + ) + }, + { + field: 'id', + headerName: 'Visit number', + width: 150 + }, + { + field: 'time', + headerName: "Date / time", + type: "dateTime", + width: 250, + valueFormatter: (value) => { + if (!value) return null; + if (value instanceof Date) { + value = value.toISOString(); + } + let dateTime = DateQuestionUtilities.toPrecision(DateQuestionUtilities.stripTimeZone(value)); + let dateTimeString = !dateTime?.isValid ? "" : dateTime.toLocaleString(DateTime.DATETIME_MED_WITH_WEEKDAY); + return dateTimeString; + } + }, + { + field: 'location', + headerName: 'Location', + width: 250, + }, + { + field: 'provider', + headerName: 'Provider', + width: 250, + }, +]; + +function Patient(props) { + const patientUuid = getSubjectIdFromPath(location.pathname); + + // Data already associated with the subject + const [ patientData, setPatientData ] = useState(); + // List of visits on record for this patient + const [ visits, setVisits ] = useState(); + // Visit data formatted for display in a DataGrid + const [ visitGridRows, setVisitGridRows ] = useState(); + // When something goes wrong: + const [ error, setError ] = useState(); + + const navigate = useNavigate(); + + const globalLoginDisplay = useContext(GlobalLoginContext); + + // -------------------------------------------------------------------------------------------------------------- + // Load and format the existing patient data, including any visits on record + + const fetchPatientData = () => { + fetchWithReLogin(globalLoginDisplay, `/Subjects/${patientUuid}.data.deep.json`) + .then((response) => response.ok ? response.json() : Promise.reject(response)) + .then((json) => { + setPatientData(json); + }) + .catch(() => setError("The patient record could not be loaded. Please try again later or contact the administrator for further assistance.")); + }; + + const fetchVisits = () => { + let url = new URL("/query", window.location.origin); + url.searchParams.set("query", `SELECT v.* from [cards:Subject] as v where ischildnode(v, '/Subjects/${patientUuid}')`); + url.searchParams.set("limit", 1000); + fetchWithReLogin(globalLoginDisplay, url) + .then(response => response.json()) + .then(result => { + setVisits(Array.from(result.rows)) + }) + .catch(() => setError("The visits could not be loaded for this patient. Please try again later or contact the administrator for further assistance.")); + }; + + const formatVisits = () => { + if (!visits) return; + setVisitGridRows( + visits?.map(visit => ({ + status: visit?.statusFlags?.find(f => f == "LOCKED"), + id: visit?.identifier, + path: visit?.["@path"], + time: visit?.time && new Date(visit?.time), + location: visit?.location, + provider: visit?.provider?.join(", "), + })) + ); + } + + useEffect(fetchPatientData, []); + useEffect(fetchVisits, [patientData]); + useEffect(formatVisits, [visits]); + + // -------------------------------------------------------------------------------------------------------------- + // Message screens are displayed if the data isn't loaded yet or if there's an error + + const displayMessageScreen = (message, type, icon) => ( + + + {message} + + + ); + + if (error) { + return displayMessageScreen(error, "error"); + } + + if (!patientData || !visits) { + return displayMessageScreen( + Loading..., + "info", + + ); + } + + // ---------------------------------------------------------------------------------------------------------------- + // + + const displayPatientInfo = () => { + let fName = patientData?.first_name; + let lName = patientData?.last_name; + let name = [lName, fName].filter(n => n).join(", "); + + let dobAnswer = patientData?.date_of_birth; + let dob = DateQuestionUtilities.toPrecision(DateQuestionUtilities.stripTimeZone(dobAnswer)); + let dobString = !dob?.isValid ? "" : dob.toLocaleString(DateTime.DATE_FULL); + let sex = patientData?.sex; + let birthInfo = [dobString, sex].filter(i => i).join(", "); + + return (name || birthInfo) ? + <> + {name ? {name} : null} + {birthInfo ? <> {birthInfo} : null} + + : null + }; + + const patientInfo = displayPatientInfo(); + + // ----------------------------------------------------------------------------------------------------------------- + // Render the patient record: + // * Resource header (sticky to the top) with no menu + // * Patient information + // * List of visits for this patient + + return ( + + + { patientInfo && + + }> + { patientInfo } + + + } + + + { + event?.preventDefault(); + navigate(`/content.html${params.row.path}`); + }} + pageSizeOptions={[5]} + /> + + + + ); +} + +export default Patient; diff --git a/modules/clinician-portal/src/main/frontend/src/clinician-portal/Visit.jsx b/modules/clinician-portal/src/main/frontend/src/clinician-portal/Visit.jsx new file mode 100644 index 0000000000..ea4aed2a4b --- /dev/null +++ b/modules/clinician-portal/src/main/frontend/src/clinician-portal/Visit.jsx @@ -0,0 +1,414 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// +import React, { useState, useEffect, useContext } from "react"; + +import { useNavigate } from "react-router"; + +import { + Alert, + AlertTitle, + Avatar, + Chip, + CircularProgress, + Divider, + Grid, + List, + ListItem, + ListItemAvatar, + ListItemButton, + ListItemText, + Typography, +} from "@mui/material"; + +import DoneIcon from '@mui/icons-material/Done'; +import WarningIcon from '@mui/icons-material/Warning'; +import SurveyIcon from '@mui/icons-material/Assignment'; +import LockIcon from '@mui/icons-material/Lock'; +import EventNoteIcon from '@mui/icons-material/EventNote'; + +import { makeStyles, withStyles } from 'tss-react/mui'; + +import { DateTime } from "luxon"; +import SurveyLinkButton from "./SurveyLinkButton"; +import EditButton from "../dataHomepage/EditButton"; +import PrintButton from "../dataHomepage/PrintButton"; +import SubjectLockAction from "../locking/SubjectLockAction"; +import FormattedText from "../components/FormattedText"; +import ResourceHeader from "../questionnaire/ResourceHeader"; +import { getSubjectIdFromPath, getHierarchyAsList, getTextHierarchy } from "../questionnaire/SubjectIdentifier"; +import DateQuestionUtilities from "../questionnaire/DateQuestionUtilities"; +import QuestionnaireStyle, { FORM_ENTRY_CONTAINER_PROPS } from "../questionnaire/QuestionnaireStyle"; +import { fetchWithReLogin, GlobalLoginContext } from "../login/ReLoginDialog.js"; + +const useStyles = makeStyles()(theme => ({ + formItem: { + "& .MuiListItemAvatar-root" : { + marginTop: 6, + zoom: 1, + }, + }, + stepIndicator : { + border: "1px solid " + theme.palette.action.disabled, + background: "transparent", + color: theme.palette.text.disabled, + fontSize: "small", + fontWeight: "bold", + }, + incompleteIndicator : { + border: "1px solid " + theme.palette.error.main, + background: "transparent", + color: theme.palette.error.main, + }, + doneIndicator : { + border: "1px solid " + theme.palette.success.main, + background: "transparent", + color: theme.palette.success.main, + } +})); + +function Visit(props) { + const id = getSubjectIdFromPath(location.pathname); + const [ , patientUuid, visitUuid ] = /^([^\/]+)\/([^\/]+)$/.exec(id); + + // Identifier of the questionnaire set used for the visit + const [ questionnaireSetId, setQuestionnaireSetId ] = useState(); + // Map questionnaire id -> title, path and optional time estimate (in minutes) for filling it out + const [ questionnaires, setQuestionnaires ] = useState(); + // The ids of the questionnaires in this set, in the order they must be filled in + const [ questionnaireSetIds, setQuestionnaireSetIds ] = useState(); + // The ids of the questionnaires displayed to the patient + const [ questionnaireIds, setQuestionnaireIds ] = useState(); + // All data already associated with this visit + const [ visit, setVisit ] = useState(); + // Survey data already associated with the subject + const [ surveyData, setSurveyData ] = useState(); + // The visit subject identifier (which coincides with the visit number) + const [ visitNumber, setVisitNumber ] = useState(); + // The visit subject node path + const [ visitPath, setVisitPath ] = useState(); + // The parent nodes of the visit subject (expected: one parent, a patient subject) + const [ parents, setParents ] = useState(); + // When something goes wrong: + const [ error, setError ] = useState(""); + // Visit information form + const [ visitInformation, setVisitInformation ] = useState(); + // If the current visit is locked + const [ isLocked, setLocked ] = useState(false); + + const VISIT_INFORMATION_FORM_TITLE = "Visit information"; + + const globalLoginDisplay = useContext(GlobalLoginContext); + + const navigate = useNavigate(); + + const { classes } = useStyles(); + + // -------------------------------------------------------------------------------------------------------------------------- + // Loading and parsing: + + const loadExistingData = () => { + fetchWithReLogin(globalLoginDisplay, `/Subjects/${patientUuid}/${visitUuid}.data.deep.json`) + .then((response) => response.ok ? response.json() : Promise.reject(response)) + .then((json) => { + setVisit(json); + if (!questionnaires) { + setVisitNumber(json["identifier"]); + setVisitPath(json["@path"]); + setParents(json["parents"]); + setVisitInformation(json[VISIT_INFORMATION_FORM_TITLE]?.[0] || {}); + let clinicPath = Object.values(json[VISIT_INFORMATION_FORM_TITLE]?.[0]).find(o => o?.question?.["@name"] == "clinic")?.value; + return fetchWithReLogin(globalLoginDisplay, `${clinicPath}.deep.json`) + .then((response) => response.ok ? response.json() : Promise.reject(response)) + .then((json) => { + setQuestionnaireSetId(json["survey"]); + }); + } + selectDataForQuestionnaireSet(json, questionnaires, questionnaireSetIds); + }) + .catch(() => setError("The survey data could not be loaded for this visit. Please try again later or contact the administrator for further assistance.")); + } + + const loadQuestionnaireSet = () => { + if (!!!questionnaireSetId) { + return; + } + fetchWithReLogin(globalLoginDisplay, `/Survey/${questionnaireSetId}.deep.json`) + .then((response) => response.ok ? response.json() : Promise.reject(response)) + .then((json) => { + parseQuestionnaireSet(json); + }) + .catch((response) => { + if (response.status == 404) { + setError("The survey you are trying to access does not exist. Please contact the administrator for further assistance."); + } else { + //setError("The survey could not be loaded at this time. Please try again later or contact the administrator for further assistance."); + } + //setQuestionnaires(null); + }); + } + + const parseQuestionnaireSet = (json) => { + // Map the relevant questionnaire info + let data = {}; + Object.entries(json || {}) + .filter(([key, value]) => value['jcr:primaryType'] == 'cards:QuestionnaireRef') + .forEach(([key, value]) => { + data[value.questionnaire['@name']] = { + 'title': value.questionnaire?.title || key, + 'targetUserType': value.targetUserType, + '@path': value.questionnaire?.['@path'], + } + }); + setQuestionnaires(data); + + let qids = Object.values(json || {}) + .filter(value => value['jcr:primaryType'] == 'cards:QuestionnaireRef') + .sort((a, b) => (a.order - b.order)) + .map(value => value.questionnaire['@name']) + setQuestionnaireSetIds(qids); + + selectDataForQuestionnaireSet(visit, data, qids); + }; + + const selectDataForQuestionnaireSet = (visit, questionnaireSet, questionnaireSetIds) => { + let ids = []; + let data = {}; + questionnaireSetIds.forEach(q => { + if (visit[questionnaireSet?.[q]?.title]?.[0]?.['jcr:primaryType'] == "cards:Form") { + data[q] = {...visit[questionnaireSet?.[q]?.title][0], targetUserType: questionnaireSet?.[q]?.targetUserType}; + ids.push(q); + } + }); + !questionnaireIds && setQuestionnaireIds(ids); + setSurveyData(data); + }; + + // ----------------------------------------------------------------------------------------------------------- + + // First, load the existing visit data to determine which questionnaire set is bound to the visit + useEffect(loadExistingData, []); + + // After the visit is loaded and we know the questionnaire set identifier, load all questionnaires that need to be filled out + useEffect(loadQuestionnaireSet, [questionnaireSetId]); + + // When a visit is loaded, record if it is locked + useEffect(() => { + setLocked(visit?.statusFlags && visit.statusFlags.includes("LOCKED")) + }, [visit]) + + + // -------------------------------------------------------------------------------------------------------------- + // Message screens are displayed if the data isn't loaded yet or if there's an error + + const displayMessageScreen = (message, type, icon) => ( + + + {message} + + + ); + + if (error) { + return displayMessageScreen(error, "error"); + } + + if (!questionnaireSetId || !questionnaireIds || !questionnaires || !visit) { + return displayMessageScreen( + Loading..., + "info", + + ); + } + + // ----------------------------------------------------------------------------------------------------------- + // Visit information - extract and format relevant info from the Visit information form associated with this visit + + const getVisitField = (qName) => { + let question = visitInformation?.questionnaire?.[qName]?.["jcr:uuid"]; + let answer = Object.values(visitInformation).find(value => value.question?.["jcr:uuid"] == question)?.value || null; + return answer; + } + + const displayVisitDateTime = () => { + let dateTimeAnswer = getVisitField("time"); + let dateTime = DateQuestionUtilities.toPrecision(DateQuestionUtilities.stripTimeZone(dateTimeAnswer)); + return !dateTime?.isValid ? "" : dateTime.toLocaleString(DateTime.DATETIME_MED_WITH_WEEKDAY); + } + + const displayVisitInfo = () => { + const dateTime = displayVisitDateTime(); + let location = getVisitField("location"); + let provider = getVisitField("provider"); + provider = provider && provider.length > 1 ? provider.join(", ") : provider; + return (dateTime || location || provider) ? + } sx={{marginTop: -2}}> + + {dateTime ? <> {dateTime} : null} + {location ? <> at {location} : null} + {provider ? <> with {provider} : null} + + + : null + } + + // --------------------------------------------------------------------------------------------------------------- + // Prepare the display of the survey list, including the status of each survey + + const surveyIndicator = ; + const doneIndicator = ; + const incompleteIndicator = ; + const lockedIndicator = ; + + const isFormComplete = (questionnaireId) => { + return surveyData?.[questionnaireId] && !surveyData[questionnaireId].statusFlags?.includes("INCOMPLETE"); + } + + const isFormSubmitted = (questionnaireId) => { + return surveyData?.[questionnaireId]?.statusFlags?.includes("SUBMITTED"); + } + + const isFormLocked = (questionnaireId) => { + return surveyData?.[questionnaireId]?.statusFlags?.includes("LOCKED"); + } + + const isFormNavigable = (questionnaireId) => { + return (surveyData?.[questionnaireId]?.questionnaire?.paginationVariant == "navigable"); + } + + const isPageComplete = (questionnaireId, section) => { + let answerSection = Object.values(surveyData?.[questionnaireId] || {}) + .find(e => (e?.section?.["jcr:uuid"] == section?.["jcr:uuid"])); + return answerSection && !answerSection.statusFlags?.includes("INCOMPLETE"); + } + + const displayFlag = flag => ( + + ) + + const displayFlags = q => ( + (surveyData?.[q]?.statusFlags ?? []) + .filter(f => ["INCOMPLETE", "SUBMITTED", "LOCKED"].includes(f)) + .map(displayFlag) + ); + + // ----------------------------------------------------------------------------------------------------------------_ + // Prepare the display of the forms for this visit + + const clinicQIds = questionnaireIds.filter(q => surveyData[q].targetUserType == "clinician"); + const patientQIds = questionnaireIds.filter(q => surveyData[q].targetUserType != "clinician"); + + const listForms = (qIds, title, withAction) => (qIds.length > 0 && + <> + {title} + + { qIds.map((q, i) => ( + } + > + navigate(`/content.html${surveyData?.[q]?.["@path"]}`)}> + + { isFormLocked(q) ? lockedIndicator : ( + isFormComplete(q) ? doneIndicator : ( + isFormSubmitted(q) ? incompleteIndicator : surveyIndicator + ) + ) + } + + + { displayFlags(q) } + { !isFormComplete(q) && isFormNavigable(q) && listPages(q) } + } + slotProps={{'secondary': {'component': 'div'}}} + /> + + + ))} + + + ); + + // For navigable forms, list pages with their completion status + const listPages = (qId) => ( + + { Object.values(surveyData?.[qId]?.questionnaire || {}) + .filter(c => c?.["jcr:primaryType"] == "cards:Section") + .map(s => { + let isComplete = isPageComplete(qId, s); + return ( + }> + {s.label || s["@name"]}} + /> + + ); + }) + } + + ) + + // ----------------------------------------------------------------------------------------------------------------- + // Render the visit: + // * Resource header (sticky to the top) with a simplified menu + // * Visit information + // * List of forms for this visit, as specified by the associated QuestionnaireSet + + return ( + + + { !isLocked && <> + + } + + + } + tags={visit?.statusFlags?.map(displayFlag)} + /> + { displayVisitInfo() } + + { listForms(clinicQIds, "Clinical examination", true) } + { listForms(patientQIds, "Patient surveys") } + + + ); +} + +export default withStyles(Visit, QuestionnaireStyle); diff --git a/modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/Views/Form.json b/modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/Views/Form.json new file mode 100644 index 0000000000..e9fa9aa136 --- /dev/null +++ b/modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/Views/Form.json @@ -0,0 +1,8 @@ +{ + "jcr:primaryType": "cards:Extension", + "cards:extensionPointId": "cards/coreUI/view", + "cards:extensionName": "Forms", + "cards:targetURL": "/content.html/Forms/:formId", + "cards:defaultOrder": -100, + "cards:extensionRenderURL": "asset:clinician-portal.Form.js" +} diff --git a/modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/Views/Patient.json b/modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/Views/Patient.json new file mode 100644 index 0000000000..0a43930a97 --- /dev/null +++ b/modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/Views/Patient.json @@ -0,0 +1,8 @@ +{ + "jcr:primaryType": "cards:Extension", + "cards:extensionPointId": "cards/coreUI/view", + "cards:extensionName": "Forms", + "cards:targetURL": "/content.html/Subjects/:patientId", + "cards:defaultOrder": -100, + "cards:extensionRenderURL": "asset:clinician-portal.Patient.js" +} diff --git a/modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/Views/Visit.json b/modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/Views/Visit.json new file mode 100644 index 0000000000..d14219bb14 --- /dev/null +++ b/modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/Views/Visit.json @@ -0,0 +1,8 @@ +{ + "jcr:primaryType": "cards:Extension", + "cards:extensionPointId": "cards/coreUI/view", + "cards:extensionName": "Forms", + "cards:targetURL": "/content.html/Subjects/:patientId/:visitId", + "cards:defaultOrder": -200, + "cards:extensionRenderURL": "asset:clinician-portal.Visit.js" +} diff --git a/modules/clinician-portal/src/main/resources/SLING-INF/content/libs/cards/resources/assetDependencies.json b/modules/clinician-portal/src/main/resources/SLING-INF/content/libs/cards/resources/assetDependencies.json index bb3e38192f..a25cd9d76c 100644 --- a/modules/clinician-portal/src/main/resources/SLING-INF/content/libs/cards/resources/assetDependencies.json +++ b/modules/clinician-portal/src/main/resources/SLING-INF/content/libs/cards/resources/assetDependencies.json @@ -2,5 +2,8 @@ "clinician-portal.ClinicDashboard.js": ["asset:cards-dataentry.Questionnaires.js", "asset:clinician-portal.ClinicForms.js", "asset:clinician-portal.ClinicVisits.js"], "clinician-portal.ClinicForms.js": ["asset:cards-dataentry.LiveTable.js"], "clinician-portal.ClinicVisits.js": ["asset:cards-dataentry.LiveTable.js"], + "clinician-portal.Form.js": ["asset:cards-login.ReLoginDialog.js", "asset:cards-dataentry.Forms.js", "asset:cards-dataentry.FormView.js", "asset:cards-pedigree.Pedigree.js"], + "clinician-portal.Visit.js": ["asset:cards-login.ReLoginDialog.js"], + "clinician-portal.Patient.js": ["asset:cards-login.ReLoginDialog.js"], "clinician-portal.DashboardSettingsConfiguration.js": ["asset:cards-login.ReLoginDialog.js"] } diff --git a/modules/patient-portal/pom.xml b/modules/patient-portal/pom.xml index a520d55ef6..def9c0063f 100644 --- a/modules/patient-portal/pom.xml +++ b/modules/patient-portal/pom.xml @@ -54,6 +54,7 @@ SLING-INF/content/libs/cards/resources/assetDependencies.json;path:=/libs/cards/resources/assetDependencies;overwriteProperties:=true, SLING-INF/content/apps/cards/ExtensionPoints/;path:=/apps/cards/ExtensionPoints/;overwriteProperties:=true;uninstall:=true, SLING-INF/content/apps/cards/LinkDefinitions/;path:=/apps/cards/LinkDefinitions/;overwriteProperties:=true;uninstall:=true, + SLING-INF/content/apps/cards/config/CopyAnswers;path:=/apps/cards/config/CopyAnswers;overwriteProperties:=true;uninstall:=true, SLING-INF/content/Extensions/;path:=/Extensions/;overwriteProperties:=true;uninstall:=true, SLING-INF/content/Extensions/AdminDashboard/;path:=/Extensions/AdminDashboard/;overwriteProperties:=true;uninstall:=true, SLING-INF/content/Extensions/Views/;path:=/Extensions/Views/;overwriteProperties:=true;uninstall:=true, diff --git a/modules/patient-portal/src/main/resources/SLING-INF/content/apps/cards/config/CopyAnswers/Questionnaires/Patient information.json b/modules/patient-portal/src/main/resources/SLING-INF/content/apps/cards/config/CopyAnswers/Questionnaires/Patient information.json new file mode 100644 index 0000000000..3c7d3d3c3a --- /dev/null +++ b/modules/patient-portal/src/main/resources/SLING-INF/content/apps/cards/config/CopyAnswers/Questionnaires/Patient information.json @@ -0,0 +1,8 @@ +{ + "jcr:primaryType": "nt:unstructured", + "jcr:reference:date_of_birth": "/Questionnaires/Patient information/date_of_birth", + "jcr:reference:sex": "/Questionnaires/Patient information/sex", + "jcr:reference:mrn": "/Questionnaires/Patient information/mrn", + "jcr:reference:first_name": "/Questionnaires/Patient information/first_name", + "jcr:reference:last_name": "/Questionnaires/Patient information/last_name" +} diff --git a/modules/patient-portal/src/main/resources/SLING-INF/content/apps/cards/config/CopyAnswers/Questionnaires/Visit information.json b/modules/patient-portal/src/main/resources/SLING-INF/content/apps/cards/config/CopyAnswers/Questionnaires/Visit information.json new file mode 100644 index 0000000000..cc3e1ec2ff --- /dev/null +++ b/modules/patient-portal/src/main/resources/SLING-INF/content/apps/cards/config/CopyAnswers/Questionnaires/Visit information.json @@ -0,0 +1,17 @@ +{ + "jcr:primaryType": "nt:unstructured", + "jcr:reference:time": "/Questionnaires/Visit information/time", + "jcr:reference:location": "/Questionnaires/Visit information/location", + "jcr:reference:provider": "/Questionnaires/Visit information/provider", + "jcr:reference:surveys_complete": "/Questionnaires/Visit information/surveys_complete", + "jcr:reference:surveys_submitted": "/Questionnaires/Visit information/surveys_submitted", + "jcr:reference:has_surveys": "/Questionnaires/Visit information/has_surveys", + "jcr:reference:invitation_sent": "/Questionnaires/Survey events/invitation_sent", + "jcr:reference:reminder1_sent": "/Questionnaires/Survey events/reminder1_sent", + "jcr:reference:email_unsubscribed": "/Questionnaires/Patient information/email_unsubscribed", + "jcr:reference:date_of_birth": "/Questionnaires/Patient information/date_of_birth", + "jcr:reference:sex": "/Questionnaires/Patient information/sex", + "jcr:reference:mrn": "/Questionnaires/Patient information/mrn", + "jcr:reference:first_name": "/Questionnaires/Patient information/first_name", + "jcr:reference:last_name": "/Questionnaires/Patient information/last_name" +} diff --git a/modules/patient-portal/src/main/resources/SLING-INF/content/apps/cards/config/CopyAnswers/SubjectTypes/Patient/ROOT.json b/modules/patient-portal/src/main/resources/SLING-INF/content/apps/cards/config/CopyAnswers/SubjectTypes/Patient/ROOT.json new file mode 100644 index 0000000000..3c7d3d3c3a --- /dev/null +++ b/modules/patient-portal/src/main/resources/SLING-INF/content/apps/cards/config/CopyAnswers/SubjectTypes/Patient/ROOT.json @@ -0,0 +1,8 @@ +{ + "jcr:primaryType": "nt:unstructured", + "jcr:reference:date_of_birth": "/Questionnaires/Patient information/date_of_birth", + "jcr:reference:sex": "/Questionnaires/Patient information/sex", + "jcr:reference:mrn": "/Questionnaires/Patient information/mrn", + "jcr:reference:first_name": "/Questionnaires/Patient information/first_name", + "jcr:reference:last_name": "/Questionnaires/Patient information/last_name" +} diff --git a/modules/patient-portal/src/main/resources/SLING-INF/content/apps/cards/config/CopyAnswers/SubjectTypes/Patient/Visit.json b/modules/patient-portal/src/main/resources/SLING-INF/content/apps/cards/config/CopyAnswers/SubjectTypes/Patient/Visit.json new file mode 100644 index 0000000000..f0ee743570 --- /dev/null +++ b/modules/patient-portal/src/main/resources/SLING-INF/content/apps/cards/config/CopyAnswers/SubjectTypes/Patient/Visit.json @@ -0,0 +1,13 @@ +{ + "jcr:primaryType": "nt:unstructured", + "jcr:reference:time": "/Questionnaires/Visit information/time", + "jcr:reference:location": "/Questionnaires/Visit information/location", + "jcr:reference:provider": "/Questionnaires/Visit information/provider", + "jcr:reference:surveys_complete": "/Questionnaires/Visit information/surveys_complete", + "jcr:reference:surveys_submitted": "/Questionnaires/Visit information/surveys_submitted", + "jcr:reference:date_of_birth": "/Questionnaires/Patient information/date_of_birth", + "jcr:reference:sex": "/Questionnaires/Patient information/sex", + "jcr:reference:mrn": "/Questionnaires/Patient information/mrn", + "jcr:reference:first_name": "/Questionnaires/Patient information/first_name", + "jcr:reference:last_name": "/Questionnaires/Patient information/last_name" +} From 0c0abc4a09b47601b3928c7eb66792be1925b36d Mon Sep 17 00:00:00 2001 From: Marta Girdea Date: Tue, 23 Jul 2024 10:02:19 -0400 Subject: [PATCH 04/11] CARDS-2558 / CARDS-2559: Visit page view for clinicians Visit page - added link to patient survey --- .../src/clinician-portal/SurveyLinkButton.jsx | 95 +++++++++++++++++++ .../frontend/src/clinician-portal/Visit.jsx | 1 + 2 files changed, 96 insertions(+) create mode 100644 modules/clinician-portal/src/main/frontend/src/clinician-portal/SurveyLinkButton.jsx diff --git a/modules/clinician-portal/src/main/frontend/src/clinician-portal/SurveyLinkButton.jsx b/modules/clinician-portal/src/main/frontend/src/clinician-portal/SurveyLinkButton.jsx new file mode 100644 index 0000000000..2910396941 --- /dev/null +++ b/modules/clinician-portal/src/main/frontend/src/clinician-portal/SurveyLinkButton.jsx @@ -0,0 +1,95 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// +import React, { useState, useEffect, useContext } from "react"; +import PropTypes from "prop-types"; + +import { + CircularProgress, + IconButton, + Tooltip, +} from "@mui/material"; + +import ErrorIcon from '@mui/icons-material/Error'; +import DoneIcon from '@mui/icons-material/Done'; +import ShareIcon from '@mui/icons-material/Share'; + +import { fetchWithReLogin, GlobalLoginContext } from "../login/ReLoginDialog.js"; + +function SurveyLinkButton(props) { + const { visitURL, size } = props; + + const [ surveyLink, setSurveyLink ] = useState(); + const [ fetchingLink, setFetchingLink ] = useState(); + const [ copied, setCopied ] = useState(); + const [ error, setError ] = useState(); + + const globalLoginDisplay = useContext(GlobalLoginContext); + + const copy = (text) => { + navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(function() { + setCopied(false); + }, 5_000); + }; + + const fetchToken = () => { + setFetchingLink(true); + fetchWithReLogin(globalLoginDisplay, `${visitURL}.token.html`) + .then((response) => response.ok ? response.text() : Promise.reject(response)) + .then((text) => { setSurveyLink(window.location.origin + "/Survey.html?auth_token=" + text.trim()); copy(window.location.origin + "/Survey.html?auth_token=" + text.trim()); }) + .catch(() => setError("Could not generate survey link")) + .finally(() => setFetchingLink(false)); + }; + + const onClick = () => { + surveyLink ? copy(surveyLink) : fetchToken(); + }; + + if (error) { + return ( + + + + ); + } + if (fetchingLink) { + return ( + + + + ); + } + return ( + + { copied ? : } + + ); +} + +SurveyLinkButton.propTypes = { + visitURL: PropTypes.string.isRequired, + size: PropTypes.oneOf(["small", "medium", "large"]), +} + +SurveyLinkButton.defaultProps = { + size: "large", +} + +export default SurveyLinkButton; diff --git a/modules/clinician-portal/src/main/frontend/src/clinician-portal/Visit.jsx b/modules/clinician-portal/src/main/frontend/src/clinician-portal/Visit.jsx index ea4aed2a4b..bb19193f4b 100644 --- a/modules/clinician-portal/src/main/frontend/src/clinician-portal/Visit.jsx +++ b/modules/clinician-portal/src/main/frontend/src/clinician-portal/Visit.jsx @@ -389,6 +389,7 @@ function Visit(props) { action={
{ !isLocked && <> + } Date: Mon, 7 Jul 2025 13:54:18 -0400 Subject: [PATCH 05/11] CARDS-2558: New "clinician-dashboard" module Rename clinician-portal -> clinician-dashboard --- .../{clinician-portal => clinician-dashboard}/pom.xml | 4 ++-- .../src/main/features/feature.json | 0 .../src/main/frontend/assets.config | 11 +++++++++++ .../src/clinician-dashboard}/ClinicDashboard.jsx | 0 .../src/clinician-dashboard}/ClinicFormList.jsx | 0 .../frontend/src/clinician-dashboard}/ClinicForms.jsx | 0 .../frontend/src/clinician-dashboard}/ClinicStyle.jsx | 0 .../src/clinician-dashboard}/ClinicVisits.jsx | 0 .../DashboardSettingsConfiguration.jsx | 0 .../main/frontend/src/clinician-dashboard}/Form.jsx | 0 .../frontend/src/clinician-dashboard}/Patient.jsx | 0 .../frontend/src/clinician-dashboard}/PrintHeader.jsx | 0 .../src/clinician-dashboard}/SurveyLinkButton.jsx | 0 .../main/frontend/src/clinician-dashboard}/Visit.jsx | 0 .../DashboardSettingsConfiguration.json | 2 +- .../content/Extensions/PrintHeader/PatientInfo.json | 2 +- .../content/Extensions/Views/ClinicDashboard.json | 2 +- .../Views/DashboardSettingsConfiguration.json | 2 +- .../SLING-INF/content/Extensions/Views/Form.json | 2 +- .../SLING-INF/content/Extensions/Views/Patient.json | 2 +- .../SLING-INF/content/Extensions/Views/Visit.json | 2 +- .../libs/cards/resources/assetDependencies.json | 9 +++++++++ .../clinician-portal/src/main/frontend/assets.config | 11 ----------- .../libs/cards/resources/assetDependencies.json | 9 --------- modules/pom.xml | 2 +- 25 files changed, 30 insertions(+), 30 deletions(-) rename modules/{clinician-portal => clinician-dashboard}/pom.xml (96%) rename modules/{clinician-portal => clinician-dashboard}/src/main/features/feature.json (100%) create mode 100644 modules/clinician-dashboard/src/main/frontend/assets.config rename modules/{clinician-portal/src/main/frontend/src/clinician-portal => clinician-dashboard/src/main/frontend/src/clinician-dashboard}/ClinicDashboard.jsx (100%) rename modules/{clinician-portal/src/main/frontend/src/clinician-portal => clinician-dashboard/src/main/frontend/src/clinician-dashboard}/ClinicFormList.jsx (100%) rename modules/{clinician-portal/src/main/frontend/src/clinician-portal => clinician-dashboard/src/main/frontend/src/clinician-dashboard}/ClinicForms.jsx (100%) rename modules/{clinician-portal/src/main/frontend/src/clinician-portal => clinician-dashboard/src/main/frontend/src/clinician-dashboard}/ClinicStyle.jsx (100%) rename modules/{clinician-portal/src/main/frontend/src/clinician-portal => clinician-dashboard/src/main/frontend/src/clinician-dashboard}/ClinicVisits.jsx (100%) rename modules/{clinician-portal/src/main/frontend/src/clinician-portal => clinician-dashboard/src/main/frontend/src/clinician-dashboard}/DashboardSettingsConfiguration.jsx (100%) rename modules/{clinician-portal/src/main/frontend/src/clinician-portal => clinician-dashboard/src/main/frontend/src/clinician-dashboard}/Form.jsx (100%) rename modules/{clinician-portal/src/main/frontend/src/clinician-portal => clinician-dashboard/src/main/frontend/src/clinician-dashboard}/Patient.jsx (100%) rename modules/{clinician-portal/src/main/frontend/src/clinician-portal => clinician-dashboard/src/main/frontend/src/clinician-dashboard}/PrintHeader.jsx (100%) rename modules/{clinician-portal/src/main/frontend/src/clinician-portal => clinician-dashboard/src/main/frontend/src/clinician-dashboard}/SurveyLinkButton.jsx (100%) rename modules/{clinician-portal/src/main/frontend/src/clinician-portal => clinician-dashboard/src/main/frontend/src/clinician-dashboard}/Visit.jsx (100%) rename modules/{clinician-portal => clinician-dashboard}/src/main/resources/SLING-INF/content/Extensions/AdminDashboard/DashboardSettingsConfiguration.json (80%) rename modules/{clinician-portal => clinician-dashboard}/src/main/resources/SLING-INF/content/Extensions/PrintHeader/PatientInfo.json (72%) rename modules/{clinician-portal => clinician-dashboard}/src/main/resources/SLING-INF/content/Extensions/Views/ClinicDashboard.json (73%) rename modules/{clinician-portal => clinician-dashboard}/src/main/resources/SLING-INF/content/Extensions/Views/DashboardSettingsConfiguration.json (69%) rename modules/{clinician-portal => clinician-dashboard}/src/main/resources/SLING-INF/content/Extensions/Views/Form.json (76%) rename modules/{clinician-portal => clinician-dashboard}/src/main/resources/SLING-INF/content/Extensions/Views/Patient.json (75%) rename modules/{clinician-portal => clinician-dashboard}/src/main/resources/SLING-INF/content/Extensions/Views/Visit.json (76%) create mode 100644 modules/clinician-dashboard/src/main/resources/SLING-INF/content/libs/cards/resources/assetDependencies.json delete mode 100644 modules/clinician-portal/src/main/frontend/assets.config delete mode 100644 modules/clinician-portal/src/main/resources/SLING-INF/content/libs/cards/resources/assetDependencies.json diff --git a/modules/clinician-portal/pom.xml b/modules/clinician-dashboard/pom.xml similarity index 96% rename from modules/clinician-portal/pom.xml rename to modules/clinician-dashboard/pom.xml index 183ad76783..1fef4712a8 100644 --- a/modules/clinician-portal/pom.xml +++ b/modules/clinician-dashboard/pom.xml @@ -26,9 +26,9 @@ 0.9.32-SNAPSHOT - cards-clinician-portal + cards-clinician-dashboard bundle - CARDS - Clinician Portal + CARDS - Clinician Dashboard An alternative view of patients, visits and forms, more suited for clinicians diff --git a/modules/clinician-portal/src/main/features/feature.json b/modules/clinician-dashboard/src/main/features/feature.json similarity index 100% rename from modules/clinician-portal/src/main/features/feature.json rename to modules/clinician-dashboard/src/main/features/feature.json diff --git a/modules/clinician-dashboard/src/main/frontend/assets.config b/modules/clinician-dashboard/src/main/frontend/assets.config new file mode 100644 index 0000000000..3a1a859108 --- /dev/null +++ b/modules/clinician-dashboard/src/main/frontend/assets.config @@ -0,0 +1,11 @@ +// These lines are aggregated as module.exports entries in webpack.config.js file by the webpack_script.py in aggregated-frontend module + +['clinician-dashboard.Visit']: { 'dependOn': ['cards-login.ReLoginDialog'], 'import': './src/clinician-dashboard/Visit.jsx' } +['clinician-dashboard.Patient']: { 'dependOn': ['cards-login.ReLoginDialog'], 'import': './src/clinician-dashboard/Patient.jsx' } +['clinician-dashboard.Form']: { 'dependOn': ['cards-login.ReLoginDialog', 'cards-dataentry.Forms', 'cards-dataentry.FormView', 'cards-pedigree.Pedigree'], 'import': './src/clinician-dashboard/Form.jsx' } +['clinician-dashboard.ClinicForms']: { 'dependOn': ['cards-dataentry.LiveTable'], 'import': './src/clinician-dashboard/ClinicForms.jsx' } +['clinician-dashboard.ClinicVisits']: { 'dependOn': ['cards-dataentry.LiveTable'], 'import': './src/clinician-dashboard/ClinicVisits.jsx' } +['clinician-dashboard.ClinicDashboard']: { 'dependOn': ['cards-dataentry.Questionnaires', 'clinician-dashboard.ClinicForms', 'clinician-dashboard.ClinicVisits'], 'import': './src/clinician-dashboard/ClinicDashboard.jsx' } +['clinician-dashboard.PrintHeader']: './src/clinician-dashboard/PrintHeader.jsx' +['clinician-dashboard.DashboardSettingsConfiguration']: { 'dependOn': ['cards-login.ReLoginDialog'], 'import': './src/clinician-dashboard/DashboardSettingsConfiguration.jsx' } +['clinician-dashboard.DashboardSettingsConfigurationIcon']: '@mui/icons-material/Dashboard' diff --git a/modules/clinician-portal/src/main/frontend/src/clinician-portal/ClinicDashboard.jsx b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/ClinicDashboard.jsx similarity index 100% rename from modules/clinician-portal/src/main/frontend/src/clinician-portal/ClinicDashboard.jsx rename to modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/ClinicDashboard.jsx diff --git a/modules/clinician-portal/src/main/frontend/src/clinician-portal/ClinicFormList.jsx b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/ClinicFormList.jsx similarity index 100% rename from modules/clinician-portal/src/main/frontend/src/clinician-portal/ClinicFormList.jsx rename to modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/ClinicFormList.jsx diff --git a/modules/clinician-portal/src/main/frontend/src/clinician-portal/ClinicForms.jsx b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/ClinicForms.jsx similarity index 100% rename from modules/clinician-portal/src/main/frontend/src/clinician-portal/ClinicForms.jsx rename to modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/ClinicForms.jsx diff --git a/modules/clinician-portal/src/main/frontend/src/clinician-portal/ClinicStyle.jsx b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/ClinicStyle.jsx similarity index 100% rename from modules/clinician-portal/src/main/frontend/src/clinician-portal/ClinicStyle.jsx rename to modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/ClinicStyle.jsx diff --git a/modules/clinician-portal/src/main/frontend/src/clinician-portal/ClinicVisits.jsx b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/ClinicVisits.jsx similarity index 100% rename from modules/clinician-portal/src/main/frontend/src/clinician-portal/ClinicVisits.jsx rename to modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/ClinicVisits.jsx diff --git a/modules/clinician-portal/src/main/frontend/src/clinician-portal/DashboardSettingsConfiguration.jsx b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/DashboardSettingsConfiguration.jsx similarity index 100% rename from modules/clinician-portal/src/main/frontend/src/clinician-portal/DashboardSettingsConfiguration.jsx rename to modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/DashboardSettingsConfiguration.jsx diff --git a/modules/clinician-portal/src/main/frontend/src/clinician-portal/Form.jsx b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/Form.jsx similarity index 100% rename from modules/clinician-portal/src/main/frontend/src/clinician-portal/Form.jsx rename to modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/Form.jsx diff --git a/modules/clinician-portal/src/main/frontend/src/clinician-portal/Patient.jsx b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/Patient.jsx similarity index 100% rename from modules/clinician-portal/src/main/frontend/src/clinician-portal/Patient.jsx rename to modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/Patient.jsx diff --git a/modules/clinician-portal/src/main/frontend/src/clinician-portal/PrintHeader.jsx b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/PrintHeader.jsx similarity index 100% rename from modules/clinician-portal/src/main/frontend/src/clinician-portal/PrintHeader.jsx rename to modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/PrintHeader.jsx diff --git a/modules/clinician-portal/src/main/frontend/src/clinician-portal/SurveyLinkButton.jsx b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/SurveyLinkButton.jsx similarity index 100% rename from modules/clinician-portal/src/main/frontend/src/clinician-portal/SurveyLinkButton.jsx rename to modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/SurveyLinkButton.jsx diff --git a/modules/clinician-portal/src/main/frontend/src/clinician-portal/Visit.jsx b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/Visit.jsx similarity index 100% rename from modules/clinician-portal/src/main/frontend/src/clinician-portal/Visit.jsx rename to modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/Visit.jsx diff --git a/modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/AdminDashboard/DashboardSettingsConfiguration.json b/modules/clinician-dashboard/src/main/resources/SLING-INF/content/Extensions/AdminDashboard/DashboardSettingsConfiguration.json similarity index 80% rename from modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/AdminDashboard/DashboardSettingsConfiguration.json rename to modules/clinician-dashboard/src/main/resources/SLING-INF/content/Extensions/AdminDashboard/DashboardSettingsConfiguration.json index 06ac8d1161..482e739044 100644 --- a/modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/AdminDashboard/DashboardSettingsConfiguration.json +++ b/modules/clinician-dashboard/src/main/resources/SLING-INF/content/Extensions/AdminDashboard/DashboardSettingsConfiguration.json @@ -3,7 +3,7 @@ "cards:extensionPointId": "cards/coreUI/admindashboard/entry", "cards:extensionName": "Clinic dashboard", "cards:targetURL": "/content.html/admin/DashboardSettingsConfiguration", - "cards:icon": "asset:clinician-portal.DashboardSettingsConfigurationIcon.js", + "cards:icon": "asset:clinician-dashboard.DashboardSettingsConfigurationIcon.js", "cards:hint": "Configure how data is displayed on the clinic dashboard", "cards:defaultOrder": 420 } diff --git a/modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/PrintHeader/PatientInfo.json b/modules/clinician-dashboard/src/main/resources/SLING-INF/content/Extensions/PrintHeader/PatientInfo.json similarity index 72% rename from modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/PrintHeader/PatientInfo.json rename to modules/clinician-dashboard/src/main/resources/SLING-INF/content/Extensions/PrintHeader/PatientInfo.json index 5cdc3b1521..2c6bc89792 100644 --- a/modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/PrintHeader/PatientInfo.json +++ b/modules/clinician-dashboard/src/main/resources/SLING-INF/content/Extensions/PrintHeader/PatientInfo.json @@ -2,6 +2,6 @@ "jcr:primaryType": "cards:Extension", "cards:extensionPointId": "cards/coreUI/print/header", "cards:extensionName": "Print header with patient information", - "cards:extensionRenderURL": "asset:clinician-portal.PrintHeader.js", + "cards:extensionRenderURL": "asset:clinician-dashboard.PrintHeader.js", "cards:defaultOrder": 10 } diff --git a/modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/Views/ClinicDashboard.json b/modules/clinician-dashboard/src/main/resources/SLING-INF/content/Extensions/Views/ClinicDashboard.json similarity index 73% rename from modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/Views/ClinicDashboard.json rename to modules/clinician-dashboard/src/main/resources/SLING-INF/content/Extensions/Views/ClinicDashboard.json index 48614232bd..61315abac6 100644 --- a/modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/Views/ClinicDashboard.json +++ b/modules/clinician-dashboard/src/main/resources/SLING-INF/content/Extensions/Views/ClinicDashboard.json @@ -2,7 +2,7 @@ "jcr:primaryType": "cards:Extension", "cards:extensionPointId": "cards/coreUI/view", "cards:extensionName": "Dashboard", - "cards:extensionRenderURL": "asset:clinician-portal.ClinicDashboard.js", + "cards:extensionRenderURL": "asset:clinician-dashboard.ClinicDashboard.js", "cards:targetURL": "/content.html/Dashboard/*", "cards:exactURLMatch": false } diff --git a/modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/Views/DashboardSettingsConfiguration.json b/modules/clinician-dashboard/src/main/resources/SLING-INF/content/Extensions/Views/DashboardSettingsConfiguration.json similarity index 69% rename from modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/Views/DashboardSettingsConfiguration.json rename to modules/clinician-dashboard/src/main/resources/SLING-INF/content/Extensions/Views/DashboardSettingsConfiguration.json index 126fe222ec..a507c4f91c 100644 --- a/modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/Views/DashboardSettingsConfiguration.json +++ b/modules/clinician-dashboard/src/main/resources/SLING-INF/content/Extensions/Views/DashboardSettingsConfiguration.json @@ -3,5 +3,5 @@ "cards:extensionPointId": "cards/coreUI/view", "cards:extensionName": "Clinic dashboard", "cards:targetURL": "/content.html/admin/DashboardSettingsConfiguration", - "cards:extensionRenderURL": "asset:clinician-portal.DashboardSettingsConfiguration.js" + "cards:extensionRenderURL": "asset:clinician-dashboard.DashboardSettingsConfiguration.js" } diff --git a/modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/Views/Form.json b/modules/clinician-dashboard/src/main/resources/SLING-INF/content/Extensions/Views/Form.json similarity index 76% rename from modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/Views/Form.json rename to modules/clinician-dashboard/src/main/resources/SLING-INF/content/Extensions/Views/Form.json index e9fa9aa136..0ed3087bf2 100644 --- a/modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/Views/Form.json +++ b/modules/clinician-dashboard/src/main/resources/SLING-INF/content/Extensions/Views/Form.json @@ -4,5 +4,5 @@ "cards:extensionName": "Forms", "cards:targetURL": "/content.html/Forms/:formId", "cards:defaultOrder": -100, - "cards:extensionRenderURL": "asset:clinician-portal.Form.js" + "cards:extensionRenderURL": "asset:clinician-dashboard.Form.js" } diff --git a/modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/Views/Patient.json b/modules/clinician-dashboard/src/main/resources/SLING-INF/content/Extensions/Views/Patient.json similarity index 75% rename from modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/Views/Patient.json rename to modules/clinician-dashboard/src/main/resources/SLING-INF/content/Extensions/Views/Patient.json index 0a43930a97..b00cf7fdd5 100644 --- a/modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/Views/Patient.json +++ b/modules/clinician-dashboard/src/main/resources/SLING-INF/content/Extensions/Views/Patient.json @@ -4,5 +4,5 @@ "cards:extensionName": "Forms", "cards:targetURL": "/content.html/Subjects/:patientId", "cards:defaultOrder": -100, - "cards:extensionRenderURL": "asset:clinician-portal.Patient.js" + "cards:extensionRenderURL": "asset:clinician-dashboard.Patient.js" } diff --git a/modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/Views/Visit.json b/modules/clinician-dashboard/src/main/resources/SLING-INF/content/Extensions/Views/Visit.json similarity index 76% rename from modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/Views/Visit.json rename to modules/clinician-dashboard/src/main/resources/SLING-INF/content/Extensions/Views/Visit.json index d14219bb14..ee6eae69e2 100644 --- a/modules/clinician-portal/src/main/resources/SLING-INF/content/Extensions/Views/Visit.json +++ b/modules/clinician-dashboard/src/main/resources/SLING-INF/content/Extensions/Views/Visit.json @@ -4,5 +4,5 @@ "cards:extensionName": "Forms", "cards:targetURL": "/content.html/Subjects/:patientId/:visitId", "cards:defaultOrder": -200, - "cards:extensionRenderURL": "asset:clinician-portal.Visit.js" + "cards:extensionRenderURL": "asset:clinician-dashboard.Visit.js" } diff --git a/modules/clinician-dashboard/src/main/resources/SLING-INF/content/libs/cards/resources/assetDependencies.json b/modules/clinician-dashboard/src/main/resources/SLING-INF/content/libs/cards/resources/assetDependencies.json new file mode 100644 index 0000000000..dd61e32e56 --- /dev/null +++ b/modules/clinician-dashboard/src/main/resources/SLING-INF/content/libs/cards/resources/assetDependencies.json @@ -0,0 +1,9 @@ +{ + "clinician-dashboard.ClinicDashboard.js": ["asset:cards-dataentry.Questionnaires.js", "asset:clinician-dashboard.ClinicForms.js", "asset:clinician-dashboard.ClinicVisits.js"], + "clinician-dashboard.ClinicForms.js": ["asset:cards-dataentry.LiveTable.js"], + "clinician-dashboard.ClinicVisits.js": ["asset:cards-dataentry.LiveTable.js"], + "clinician-dashboard.Form.js": ["asset:cards-login.ReLoginDialog.js", "asset:cards-dataentry.Forms.js", "asset:cards-dataentry.FormView.js", "asset:cards-pedigree.Pedigree.js"], + "clinician-dashboard.Visit.js": ["asset:cards-login.ReLoginDialog.js"], + "clinician-dashboard.Patient.js": ["asset:cards-login.ReLoginDialog.js"], + "clinician-dashboard.DashboardSettingsConfiguration.js": ["asset:cards-login.ReLoginDialog.js"] +} diff --git a/modules/clinician-portal/src/main/frontend/assets.config b/modules/clinician-portal/src/main/frontend/assets.config deleted file mode 100644 index 03df735030..0000000000 --- a/modules/clinician-portal/src/main/frontend/assets.config +++ /dev/null @@ -1,11 +0,0 @@ -// These lines are aggregated as module.exports entries in webpack.config.js file by the webpack_script.py in aggregated-frontend module - -['clinician-portal.Visit']: { 'dependOn': ['cards-login.ReLoginDialog'], 'import': './src/clinician-portal/Visit.jsx' } -['clinician-portal.Patient']: { 'dependOn': ['cards-login.ReLoginDialog'], 'import': './src/clinician-portal/Patient.jsx' } -['clinician-portal.Form']: { 'dependOn': ['cards-login.ReLoginDialog', 'cards-dataentry.Forms', 'cards-dataentry.FormView', 'cards-pedigree.Pedigree'], 'import': './src/clinician-portal/Form.jsx' } -['clinician-portal.ClinicForms']: { 'dependOn': ['cards-dataentry.LiveTable'], 'import': './src/clinician-portal/ClinicForms.jsx' } -['clinician-portal.ClinicVisits']: { 'dependOn': ['cards-dataentry.LiveTable'], 'import': './src/clinician-portal/ClinicVisits.jsx' } -['clinician-portal.ClinicDashboard']: { 'dependOn': ['cards-dataentry.Questionnaires', 'clinician-portal.ClinicForms', 'clinician-portal.ClinicVisits'], 'import': './src/clinician-portal/ClinicDashboard.jsx' } -['clinician-portal.PrintHeader']: './src/clinician-portal/PrintHeader.jsx' -['clinician-portal.DashboardSettingsConfiguration']: { 'dependOn': ['cards-login.ReLoginDialog'], 'import': './src/clinician-portal/DashboardSettingsConfiguration.jsx' } -['clinician-portal.DashboardSettingsConfigurationIcon']: '@mui/icons-material/Dashboard' diff --git a/modules/clinician-portal/src/main/resources/SLING-INF/content/libs/cards/resources/assetDependencies.json b/modules/clinician-portal/src/main/resources/SLING-INF/content/libs/cards/resources/assetDependencies.json deleted file mode 100644 index a25cd9d76c..0000000000 --- a/modules/clinician-portal/src/main/resources/SLING-INF/content/libs/cards/resources/assetDependencies.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "clinician-portal.ClinicDashboard.js": ["asset:cards-dataentry.Questionnaires.js", "asset:clinician-portal.ClinicForms.js", "asset:clinician-portal.ClinicVisits.js"], - "clinician-portal.ClinicForms.js": ["asset:cards-dataentry.LiveTable.js"], - "clinician-portal.ClinicVisits.js": ["asset:cards-dataentry.LiveTable.js"], - "clinician-portal.Form.js": ["asset:cards-login.ReLoginDialog.js", "asset:cards-dataentry.Forms.js", "asset:cards-dataentry.FormView.js", "asset:cards-pedigree.Pedigree.js"], - "clinician-portal.Visit.js": ["asset:cards-login.ReLoginDialog.js"], - "clinician-portal.Patient.js": ["asset:cards-login.ReLoginDialog.js"], - "clinician-portal.DashboardSettingsConfiguration.js": ["asset:cards-login.ReLoginDialog.js"] -} diff --git a/modules/pom.xml b/modules/pom.xml index a61b4e1441..39624d558f 100644 --- a/modules/pom.xml +++ b/modules/pom.xml @@ -43,7 +43,7 @@ data-entry data-model patient-portal - clinician-portal + clinician-dashboard ldap-support saml-support utils From bacecfc1216fb1b184e2e8bd163a159990435f7b Mon Sep 17 00:00:00 2001 From: Andrew Crowther Date: Wed, 5 Feb 2025 21:20:05 -0500 Subject: [PATCH 06/11] CARDS-2609: Configurable actions on user dashboard and related views Implement new clinician views --- .../clinician-dashboard/ClinicianFormView.jsx | 145 +++++++++++++++++ .../clinician-dashboard/ClinicianForms.jsx | 95 +++++++++++ .../ClinicianSubjectView.jsx | 150 ++++++++++++++++++ 3 files changed, 390 insertions(+) create mode 100644 modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/ClinicianFormView.jsx create mode 100644 modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/ClinicianForms.jsx create mode 100644 modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/ClinicianSubjectView.jsx diff --git a/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/ClinicianFormView.jsx b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/ClinicianFormView.jsx new file mode 100644 index 0000000000..88b37c93c9 --- /dev/null +++ b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/ClinicianFormView.jsx @@ -0,0 +1,145 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// +import React, { useState, useEffect } from "react"; +import LiveTable from "../dataHomepage/LiveTable.jsx"; + +import QuestionnaireStyle from "../questionnaire/QuestionnaireStyle.jsx"; + +import { + Avatar, + Card, + CardContent, + CardHeader, + Divider, + LinearProgress, + Tab, + Tabs, + Typography, +} from "@mui/material"; +import withStyles from '@mui/styles/withStyles'; +import DescriptionIcon from '@mui/icons-material/Description'; +import { getEntityIdentifier } from "../themePage/EntityIdentifier.jsx"; + +function ClinicianFormView(props) { + const { extension, questionnaire, expanded, disableHeader, disableAvatar, topPagination, classes } = props; + + const [ title, setTitle ] = useState(props.title); + const [ subtitle, setSubtitle ] = useState(props.subtitle); + const [ qFilter, setQFilter ] = useState(); + const [ filtersJsonString, setFiltersJsonString ] = useState(new URLSearchParams(window.location.hash.substring(1)).get("forms:filters")); + + const admin = extension?.["cards:admin"] || props.admin + + // Column configuration for the LiveTables + const columns = [ + { + "key": "@name", + "label": "Identifier", + "format": getEntityIdentifier, + "link": "dashboard+path", + }, + { + "key": "jcr:created", + "label": "Created on", + "format": "date:yyyy-MM-dd HH:mm", + }, + { + "key": "jcr:createdBy", + "label": "Created by", + "format": "string", + }, + ] + + const tabFilter = { + "Questionnaires" : '&includeallstatus=true', + "Completed" : '', + "Drafts" : '&fieldname=statusFlags&fieldvalue=INCOMPLETE', + }; + const tabs = Object.keys(tabFilter); + + const activeTabParam = new URLSearchParams(window.location.hash.substring(1)).get("forms:activeTab"); + let activeTabIndex = Math.max(tabs.indexOf(activeTabParam), 0); + const [ activeTab, setActiveTab ] = useState(activeTabIndex); + + useEffect (() => { + // If a questionnaire parameter is specified: + if (questionnaire) { + // Fetch the questionnaire info and update the title, subtitle and query filter + fetch(`${questionnaire}.json`) + .then((response) => response.ok ? response.json() : Promise.reject(response)) + .then(qData => { + setTitle(qData["title"]); + setSubtitle(qData["description"]); + setQFilter('&fieldname=questionnaire&fieldvalue=' + encodeURIComponent(qData["jcr:uuid"])); + }) + .catch(err => setQFilter('')); + } else { + // No filtering by questionnaire + setQFilter(''); + } + }, [questionnaire]); + + return ( + + {title && + + {title && {title}} + {subtitle && {subtitle}} + + } + /> + } + {(!expanded || !disableHeader && !disableAvatar) && + } + title={ + <> + setActiveTab(value)} indicatorColor="primary" textColor="inherit" > + { tabs.map((value, index) => { + return {value}} key={"form-" + index} />; + })} + + + } + /> + } + + + { typeof(qFilter) == "undefined" ? : + { setFiltersJsonString(str); }} + filtersJsonString={filtersJsonString} + admin={admin} + /> + } + + + ); +} + +export default withStyles(QuestionnaireStyle)(ClinicianFormView); diff --git a/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/ClinicianForms.jsx b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/ClinicianForms.jsx new file mode 100644 index 0000000000..efb900fe95 --- /dev/null +++ b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/ClinicianForms.jsx @@ -0,0 +1,95 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// +import React, { useEffect, useState } from "react"; +import Form from "../questionnaire/Form.jsx"; +import { getHierarchy } from "../questionnaire/SubjectIdentifier.jsx"; + +import { Grid } from "@mui/material"; +import withStyles from '@mui/styles/withStyles'; +import questionnaireStyle from "../questionnaire/QuestionnaireStyle.jsx"; +import FormView from "./ClinicianFormView.jsx"; +import { getEntityIdentifier } from "../themePage/EntityIdentifier.jsx"; +import { usePageNameWriterContext } from "../themePage/Page.jsx"; + +function Forms(props) { + const { location, classes } = props; + const questionnaire = /questionnaire=([^&]+)/.exec(location.search)?.[1]; + const pageNameWriter = usePageNameWriterContext(); + + const entry = /Forms\/([^.\/]+)/.exec(location.pathname); + + // When moving from a specific form to the "Forms" page, ensure that the title properly changes + useEffect(() => { + if (!entry) { + pageNameWriter(""); + } + }, [entry]); + + if (entry) { + return
; + } + + const columns = [ + { + "key": "@name", + "label": "Identifier", + "format": getEntityIdentifier, + "link": "dashboard+path", + }, + { + "key": "", + "label": "Subject", + "format": (row) => (row.subject ? getHierarchy(row.subject, undefined, undefined) : ''), + }, + { + "key": "questionnaire/title", + "label": "Questionnaire", + "format": "string", + }, + { + "key": "jcr:created", + "label": "Created on", + "format": "date:yyyy-MM-dd HH:mm", + }, + { + "key": "jcr:createdBy", + "label": "Created by", + "format": "string", + }, + ] + + return ( + + + + + + ); +} + +export default withStyles(questionnaireStyle)(Forms); diff --git a/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/ClinicianSubjectView.jsx b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/ClinicianSubjectView.jsx new file mode 100644 index 0000000000..254a956f88 --- /dev/null +++ b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/ClinicianSubjectView.jsx @@ -0,0 +1,150 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// +import React, { useState, useContext } from "react"; +import LiveTable from "../dataHomepage/LiveTable.jsx"; + +import QuestionnaireStyle from "../questionnaire/QuestionnaireStyle.jsx"; + +import { + Avatar, + Card, + CardContent, + CardHeader, + CircularProgress, + Divider, + Tab, + Tabs, + Typography, +} from "@mui/material"; +import withStyles from '@mui/styles/withStyles'; +import AssignmentIndIcon from '@mui/icons-material/AssignmentInd'; +import { getEntityIdentifier } from "../themePage/EntityIdentifier.jsx"; +import { fetchWithReLogin, GlobalLoginContext } from "../login/loginDialogue.js"; + +function ClinicianSubjectView(props) { + const { disableHeader, disableAvatar, topPagination, extension, classes } = props; + const [ activeTab, setActiveTab ] = useState(0); + const [ subjectTypes, setSubjectTypes] = useState([]) + const [ tabsLoading, setTabsLoading ] = useState(null); + const [ columns, setColumns ] = React.useState(props.columns || null); + const [ filtersJsonString, setFiltersJsonString ] = useState(new URLSearchParams(window.location.hash.substring(1)).get("subjects:filters")); + const hasSubjects = tabsLoading === false && subjectTypes.length > 0; + const admin = extension?.["cards:admin"] || props.admin + + const activeTabParam = new URLSearchParams(window.location.hash.substring(1)).get("subjects:activeTab"); + + const globalLoginDisplay = useContext(GlobalLoginContext); + + // Default column configuration for the LiveTables to be used from User Dashboard + const defaultColumns = [ + { + "key": "@name", + "label": "Identifier", + "format": getEntityIdentifier, + "link": "dashboard+path", + }, + { + "key": "jcr:created", + "label": "Created on", + "format": "date:yyyy-MM-dd HH:mm", + }, + { + "key": "jcr:createdBy", + "label": "Created by", + "format": "string", + }, + ] + + let fetchSubjectTypes = () => { + let url = new URL("/query", window.location.origin); + url.searchParams.set("query", `SELECT * FROM [cards:SubjectType] as n order by n.'cards:defaultOrder' option (index tag cards)`); + return fetchWithReLogin(globalLoginDisplay, url) + .then(response => response.json()) + .then(result => { + let optionTypes = Array.from(result.rows); + if (columns && optionTypes.length <= 1) { + let result = columns.slice(); + result.splice(1, 2); + setColumns(result); + } + setSubjectTypes(optionTypes); + setTabsLoading(false); + + if (activeTabParam && result.rows.length > 0) { + let activeTabIndex = result.rows.indexOf(result.rows.find(element => element["@name"] === activeTabParam)); + activeTabIndex > 0 && setActiveTab(activeTabIndex); + } + }) + } + + if (tabsLoading === null) { + fetchSubjectTypes(); + setTabsLoading(true); + } else if (tabsLoading === false && !subjectTypes?.length) { + return null; + } + + return ( + + {!disableHeader && + } + title={ + tabsLoading + ? + : subjectTypes.length < 1 ? + <> + : setActiveTab(value)} indicatorColor="primary" textColor="inherit" > + {subjectTypes.map((subject, index) => { + return + {subject['subjectListLabel'] || subject['label'] || subject['@name']} + + } + key={"subject-" + index} + />; + })} + + } + /> + } + + + { + hasSubjects + ? setFiltersJsonString(str)} + filtersJsonString={filtersJsonString} + admin={admin} + /> + : No results + } + + + ); +} + +export default withStyles(QuestionnaireStyle)(ClinicianSubjectView); From 987e41b142a6c17efe7b71b1600b02c8cc114aa4 Mon Sep 17 00:00:00 2001 From: Andrew Crowther Date: Thu, 6 Feb 2025 22:23:41 -0500 Subject: [PATCH 07/11] CARDS-2609: Configurable actions on user dashboard and related views Finish new clinician views --- .../clinician-dashboard/ClinicianFormView.jsx | 145 ----------------- .../clinician-dashboard/ClinicianForms.jsx | 58 ++----- .../ClinicianSubjectView.jsx | 150 ------------------ .../src/clinician-dashboard/FormView.jsx | 56 +++++++ .../src/clinician-dashboard/Forms.jsx | 67 ++++++++ .../src/clinician-dashboard/SubjectView.jsx | 56 +++++++ .../src/clinician-dashboard/Subjects.jsx | 62 ++++++++ 7 files changed, 256 insertions(+), 338 deletions(-) delete mode 100644 modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/ClinicianFormView.jsx delete mode 100644 modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/ClinicianSubjectView.jsx create mode 100644 modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/FormView.jsx create mode 100644 modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/Forms.jsx create mode 100644 modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/SubjectView.jsx create mode 100644 modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/Subjects.jsx diff --git a/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/ClinicianFormView.jsx b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/ClinicianFormView.jsx deleted file mode 100644 index 88b37c93c9..0000000000 --- a/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/ClinicianFormView.jsx +++ /dev/null @@ -1,145 +0,0 @@ -// -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. -// -import React, { useState, useEffect } from "react"; -import LiveTable from "../dataHomepage/LiveTable.jsx"; - -import QuestionnaireStyle from "../questionnaire/QuestionnaireStyle.jsx"; - -import { - Avatar, - Card, - CardContent, - CardHeader, - Divider, - LinearProgress, - Tab, - Tabs, - Typography, -} from "@mui/material"; -import withStyles from '@mui/styles/withStyles'; -import DescriptionIcon from '@mui/icons-material/Description'; -import { getEntityIdentifier } from "../themePage/EntityIdentifier.jsx"; - -function ClinicianFormView(props) { - const { extension, questionnaire, expanded, disableHeader, disableAvatar, topPagination, classes } = props; - - const [ title, setTitle ] = useState(props.title); - const [ subtitle, setSubtitle ] = useState(props.subtitle); - const [ qFilter, setQFilter ] = useState(); - const [ filtersJsonString, setFiltersJsonString ] = useState(new URLSearchParams(window.location.hash.substring(1)).get("forms:filters")); - - const admin = extension?.["cards:admin"] || props.admin - - // Column configuration for the LiveTables - const columns = [ - { - "key": "@name", - "label": "Identifier", - "format": getEntityIdentifier, - "link": "dashboard+path", - }, - { - "key": "jcr:created", - "label": "Created on", - "format": "date:yyyy-MM-dd HH:mm", - }, - { - "key": "jcr:createdBy", - "label": "Created by", - "format": "string", - }, - ] - - const tabFilter = { - "Questionnaires" : '&includeallstatus=true', - "Completed" : '', - "Drafts" : '&fieldname=statusFlags&fieldvalue=INCOMPLETE', - }; - const tabs = Object.keys(tabFilter); - - const activeTabParam = new URLSearchParams(window.location.hash.substring(1)).get("forms:activeTab"); - let activeTabIndex = Math.max(tabs.indexOf(activeTabParam), 0); - const [ activeTab, setActiveTab ] = useState(activeTabIndex); - - useEffect (() => { - // If a questionnaire parameter is specified: - if (questionnaire) { - // Fetch the questionnaire info and update the title, subtitle and query filter - fetch(`${questionnaire}.json`) - .then((response) => response.ok ? response.json() : Promise.reject(response)) - .then(qData => { - setTitle(qData["title"]); - setSubtitle(qData["description"]); - setQFilter('&fieldname=questionnaire&fieldvalue=' + encodeURIComponent(qData["jcr:uuid"])); - }) - .catch(err => setQFilter('')); - } else { - // No filtering by questionnaire - setQFilter(''); - } - }, [questionnaire]); - - return ( - - {title && - - {title && {title}} - {subtitle && {subtitle}} - - } - /> - } - {(!expanded || !disableHeader && !disableAvatar) && - } - title={ - <> - setActiveTab(value)} indicatorColor="primary" textColor="inherit" > - { tabs.map((value, index) => { - return {value}} key={"form-" + index} />; - })} - - - } - /> - } - - - { typeof(qFilter) == "undefined" ? : - { setFiltersJsonString(str); }} - filtersJsonString={filtersJsonString} - admin={admin} - /> - } - - - ); -} - -export default withStyles(QuestionnaireStyle)(ClinicianFormView); diff --git a/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/ClinicianForms.jsx b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/ClinicianForms.jsx index efb900fe95..4d5e41f815 100644 --- a/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/ClinicianForms.jsx +++ b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/ClinicianForms.jsx @@ -16,38 +16,19 @@ // specific language governing permissions and limitations // under the License. // -import React, { useEffect, useState } from "react"; -import Form from "../questionnaire/Form.jsx"; -import { getHierarchy } from "../questionnaire/SubjectIdentifier.jsx"; +import React from "react"; -import { Grid } from "@mui/material"; -import withStyles from '@mui/styles/withStyles'; -import questionnaireStyle from "../questionnaire/QuestionnaireStyle.jsx"; -import FormView from "./ClinicianFormView.jsx"; +import { getHierarchy } from "../questionnaire/SubjectIdentifier.jsx"; import { getEntityIdentifier } from "../themePage/EntityIdentifier.jsx"; -import { usePageNameWriterContext } from "../themePage/Page.jsx"; +import DefaultForms from "../dataHomepage/Forms.jsx"; function Forms(props) { - const { location, classes } = props; - const questionnaire = /questionnaire=([^&]+)/.exec(location.search)?.[1]; - const pageNameWriter = usePageNameWriterContext(); - - const entry = /Forms\/([^.\/]+)/.exec(location.pathname); - - // When moving from a specific form to the "Forms" page, ensure that the title properly changes - useEffect(() => { - if (!entry) { - pageNameWriter(""); - } - }, [entry]); - if (entry) { - return ; + const actionSwitches = { + edit: () => false, + delete: () => false, + create: () => false, + expand: () => false, } const columns = [ @@ -60,7 +41,7 @@ function Forms(props) { { "key": "", "label": "Subject", - "format": (row) => (row.subject ? getHierarchy(row.subject, undefined, undefined) : ''), + "format": (row) => (row.subject ? getHierarchy(row.subject, undefined, undefined, props.extensionURL) : ''), }, { "key": "questionnaire/title", @@ -72,24 +53,15 @@ function Forms(props) { "label": "Created on", "format": "date:yyyy-MM-dd HH:mm", }, - { - "key": "jcr:createdBy", - "label": "Created by", - "format": "string", - }, ] return ( - - - - - + ); } -export default withStyles(questionnaireStyle)(Forms); +export default Forms; diff --git a/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/ClinicianSubjectView.jsx b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/ClinicianSubjectView.jsx deleted file mode 100644 index 254a956f88..0000000000 --- a/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/ClinicianSubjectView.jsx +++ /dev/null @@ -1,150 +0,0 @@ -// -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. -// -import React, { useState, useContext } from "react"; -import LiveTable from "../dataHomepage/LiveTable.jsx"; - -import QuestionnaireStyle from "../questionnaire/QuestionnaireStyle.jsx"; - -import { - Avatar, - Card, - CardContent, - CardHeader, - CircularProgress, - Divider, - Tab, - Tabs, - Typography, -} from "@mui/material"; -import withStyles from '@mui/styles/withStyles'; -import AssignmentIndIcon from '@mui/icons-material/AssignmentInd'; -import { getEntityIdentifier } from "../themePage/EntityIdentifier.jsx"; -import { fetchWithReLogin, GlobalLoginContext } from "../login/loginDialogue.js"; - -function ClinicianSubjectView(props) { - const { disableHeader, disableAvatar, topPagination, extension, classes } = props; - const [ activeTab, setActiveTab ] = useState(0); - const [ subjectTypes, setSubjectTypes] = useState([]) - const [ tabsLoading, setTabsLoading ] = useState(null); - const [ columns, setColumns ] = React.useState(props.columns || null); - const [ filtersJsonString, setFiltersJsonString ] = useState(new URLSearchParams(window.location.hash.substring(1)).get("subjects:filters")); - const hasSubjects = tabsLoading === false && subjectTypes.length > 0; - const admin = extension?.["cards:admin"] || props.admin - - const activeTabParam = new URLSearchParams(window.location.hash.substring(1)).get("subjects:activeTab"); - - const globalLoginDisplay = useContext(GlobalLoginContext); - - // Default column configuration for the LiveTables to be used from User Dashboard - const defaultColumns = [ - { - "key": "@name", - "label": "Identifier", - "format": getEntityIdentifier, - "link": "dashboard+path", - }, - { - "key": "jcr:created", - "label": "Created on", - "format": "date:yyyy-MM-dd HH:mm", - }, - { - "key": "jcr:createdBy", - "label": "Created by", - "format": "string", - }, - ] - - let fetchSubjectTypes = () => { - let url = new URL("/query", window.location.origin); - url.searchParams.set("query", `SELECT * FROM [cards:SubjectType] as n order by n.'cards:defaultOrder' option (index tag cards)`); - return fetchWithReLogin(globalLoginDisplay, url) - .then(response => response.json()) - .then(result => { - let optionTypes = Array.from(result.rows); - if (columns && optionTypes.length <= 1) { - let result = columns.slice(); - result.splice(1, 2); - setColumns(result); - } - setSubjectTypes(optionTypes); - setTabsLoading(false); - - if (activeTabParam && result.rows.length > 0) { - let activeTabIndex = result.rows.indexOf(result.rows.find(element => element["@name"] === activeTabParam)); - activeTabIndex > 0 && setActiveTab(activeTabIndex); - } - }) - } - - if (tabsLoading === null) { - fetchSubjectTypes(); - setTabsLoading(true); - } else if (tabsLoading === false && !subjectTypes?.length) { - return null; - } - - return ( - - {!disableHeader && - } - title={ - tabsLoading - ? - : subjectTypes.length < 1 ? - <> - : setActiveTab(value)} indicatorColor="primary" textColor="inherit" > - {subjectTypes.map((subject, index) => { - return - {subject['subjectListLabel'] || subject['label'] || subject['@name']} - - } - key={"subject-" + index} - />; - })} - - } - /> - } - - - { - hasSubjects - ? setFiltersJsonString(str)} - filtersJsonString={filtersJsonString} - admin={admin} - /> - : No results - } - - - ); -} - -export default withStyles(QuestionnaireStyle)(ClinicianSubjectView); diff --git a/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/FormView.jsx b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/FormView.jsx new file mode 100644 index 0000000000..c712d0e5c7 --- /dev/null +++ b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/FormView.jsx @@ -0,0 +1,56 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// +import React, { useState, useEffect } from "react"; + +import { getEntityIdentifier } from "../themePage/EntityIdentifier.jsx"; +import DefaultFormView from "../dataHomepage/FormView.jsx"; + +function FormView(props) { + + const actionSwitches = { + edit: () => false, + delete: () => false, + create: () => false, + expand: () => false, + } + + const columns = [ + { + "key": "@name", + "label": "Identifier", + "format": getEntityIdentifier, + "link": "dashboard+path", + }, + { + "key": "jcr:created", + "label": "Created on", + "format": "date:yyyy-MM-dd HH:mm", + }, + ] + + return ( + + ); +} + +export default FormView; diff --git a/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/Forms.jsx b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/Forms.jsx new file mode 100644 index 0000000000..4d5e41f815 --- /dev/null +++ b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/Forms.jsx @@ -0,0 +1,67 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// +import React from "react"; + +import { getHierarchy } from "../questionnaire/SubjectIdentifier.jsx"; +import { getEntityIdentifier } from "../themePage/EntityIdentifier.jsx"; +import DefaultForms from "../dataHomepage/Forms.jsx"; + +function Forms(props) { + + const actionSwitches = { + edit: () => false, + delete: () => false, + create: () => false, + expand: () => false, + } + + const columns = [ + { + "key": "@name", + "label": "Identifier", + "format": getEntityIdentifier, + "link": "dashboard+path", + }, + { + "key": "", + "label": "Subject", + "format": (row) => (row.subject ? getHierarchy(row.subject, undefined, undefined, props.extensionURL) : ''), + }, + { + "key": "questionnaire/title", + "label": "Questionnaire", + "format": "string", + }, + { + "key": "jcr:created", + "label": "Created on", + "format": "date:yyyy-MM-dd HH:mm", + }, + ] + + return ( + + ); +} + +export default Forms; diff --git a/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/SubjectView.jsx b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/SubjectView.jsx new file mode 100644 index 0000000000..9f6096ef7d --- /dev/null +++ b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/SubjectView.jsx @@ -0,0 +1,56 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// +import React from "react"; + +import { getEntityIdentifier } from "../themePage/EntityIdentifier.jsx"; +import DefaultSubjectView from "../dataHomepage/SubjectView.jsx"; + +function SubjectView(props) { + + const actionSwitches = { + edit: () => false, + delete: () => false, + create: () => false, + expand: () => false, + } + + const columns = [ + { + "key": "@name", + "label": "Identifier", + "format": getEntityIdentifier, + "link": "dashboard+path", + }, + { + "key": "jcr:created", + "label": "Created on", + "format": "date:yyyy-MM-dd HH:mm", + }, + ] + + return ( + + ); +} + +export default SubjectView; diff --git a/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/Subjects.jsx b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/Subjects.jsx new file mode 100644 index 0000000000..27f97850c3 --- /dev/null +++ b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/Subjects.jsx @@ -0,0 +1,62 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// +import React from "react"; + +import { getHierarchy } from "../questionnaire/SubjectIdentifier.jsx"; +import { getEntityIdentifier } from "../themePage/EntityIdentifier.jsx"; +import DefaultSubjects from "../dataHomepage/Subjects.jsx"; + +function Subjects(props) { + + const actionSwitches = { + edit: () => false, + delete: () => false, + create: () => false, + expand: () => false, + } + + const columns = [ + { + "key": "identifier", + "label": "Identifier", + "format": getEntityIdentifier, + "link": "dashboard+path", + }, + { + "key": "", + "label": "Parents", + "format": (row) => (row['parents'] ? getHierarchy(row['parents'], undefined, undefined, props.extensionURL) : ''), + }, + { + "key": "jcr:created", + "label": "Created on", + "format": "date:yyyy-MM-dd HH:mm", + }, + ] + + return ( + + ); +} + +export default Subjects; From 684252c9d1ce45df33f4b51513f6f6d6d87fa1d9 Mon Sep 17 00:00:00 2001 From: Andrew Crowther Date: Thu, 22 Jan 2026 13:55:20 -0500 Subject: [PATCH 08/11] CARDS-2609: Configurable actions on user dashboard and related views Fix merge and linting errors --- modules/clinician-dashboard/pom.xml | 2 +- .../src/main/frontend/assets.config | 4 + .../frontend/src/clinician-dashboard/Form.jsx | 2 - .../src/clinician-dashboard/FormView.jsx | 4 +- .../src/clinician-dashboard/Forms.jsx | 4 +- .../src/clinician-dashboard/Patient.jsx | 61 ++++----- .../src/clinician-dashboard/SubjectView.jsx | 4 +- .../src/clinician-dashboard/Subjects.jsx | 4 +- .../clinician-dashboard/SurveyLinkButton.jsx | 11 +- .../src/clinician-dashboard/Visit.jsx | 126 +++++++++--------- .../cards/resources/assetDependencies.json | 6 +- .../main/frontend/src/questionnaire/Form.jsx | 12 +- .../frontend/src/questionnaire/Subject.jsx | 8 +- .../src/main/frontend/assets.config | 3 - 14 files changed, 121 insertions(+), 130 deletions(-) diff --git a/modules/clinician-dashboard/pom.xml b/modules/clinician-dashboard/pom.xml index 1fef4712a8..031c2022eb 100644 --- a/modules/clinician-dashboard/pom.xml +++ b/modules/clinician-dashboard/pom.xml @@ -23,7 +23,7 @@ io.uhndata.cards cards-modules - 0.9.32-SNAPSHOT + 0.9.36-SNAPSHOT cards-clinician-dashboard diff --git a/modules/clinician-dashboard/src/main/frontend/assets.config b/modules/clinician-dashboard/src/main/frontend/assets.config index 3a1a859108..a7eea7997d 100644 --- a/modules/clinician-dashboard/src/main/frontend/assets.config +++ b/modules/clinician-dashboard/src/main/frontend/assets.config @@ -9,3 +9,7 @@ ['clinician-dashboard.PrintHeader']: './src/clinician-dashboard/PrintHeader.jsx' ['clinician-dashboard.DashboardSettingsConfiguration']: { 'dependOn': ['cards-login.ReLoginDialog'], 'import': './src/clinician-dashboard/DashboardSettingsConfiguration.jsx' } ['clinician-dashboard.DashboardSettingsConfigurationIcon']: '@mui/icons-material/Dashboard' +['clinician-dashboard.FormView']: { 'dependOn': ['cards-dataentry.LiveTable'], 'import': './src/clinician-dashboard/FormView.jsx' } +['clinician-dashboard.SubjectView']: { 'dependOn': ['cards-dataentry.LiveTable'], 'import': './src/clinician-dashboard/SubjectView.jsx' } +['clinician-dashboard.Forms']: { 'dependOn': ['cards-login.ReLoginDialog'], 'import': './src/clinician-dashboard/Forms.jsx' } +['clinician-dashboard.Subjects']: { 'dependOn': ['cards-login.ReLoginDialog'], 'import': './src/clinician-dashboard/Subjects.jsx' } diff --git a/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/Form.jsx b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/Form.jsx index 78d23a35c0..d31865e13e 100644 --- a/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/Form.jsx +++ b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/Form.jsx @@ -16,8 +16,6 @@ // specific language governing permissions and limitations // under the License. // -import React from "react"; - import DefaultForm from "../questionnaire/FormView.jsx"; function Form(props) { diff --git a/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/FormView.jsx b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/FormView.jsx index c712d0e5c7..a2ac9eb0e6 100644 --- a/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/FormView.jsx +++ b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/FormView.jsx @@ -16,10 +16,8 @@ // specific language governing permissions and limitations // under the License. // -import React, { useState, useEffect } from "react"; - -import { getEntityIdentifier } from "../themePage/EntityIdentifier.jsx"; import DefaultFormView from "../dataHomepage/FormView.jsx"; +import { getEntityIdentifier } from "../themePage/EntityIdentifier.jsx"; function FormView(props) { diff --git a/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/Forms.jsx b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/Forms.jsx index 4d5e41f815..2a122dec8c 100644 --- a/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/Forms.jsx +++ b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/Forms.jsx @@ -16,11 +16,9 @@ // specific language governing permissions and limitations // under the License. // -import React from "react"; - +import DefaultForms from "../dataHomepage/Forms.jsx"; import { getHierarchy } from "../questionnaire/SubjectIdentifier.jsx"; import { getEntityIdentifier } from "../themePage/EntityIdentifier.jsx"; -import DefaultForms from "../dataHomepage/Forms.jsx"; function Forms(props) { diff --git a/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/Patient.jsx b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/Patient.jsx index 0a9cc9fdd6..e67e5d1776 100644 --- a/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/Patient.jsx +++ b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/Patient.jsx @@ -16,10 +16,11 @@ // specific language governing permissions and limitations // under the License. // -import React, { useState, useEffect, useContext } from "react"; - -import { useNavigate } from "react-router"; +import { useState, useEffect, useContext } from "react"; +import ContactPageIcon from '@mui/icons-material/ContactPage'; +import EventNoteIcon from '@mui/icons-material/EventNote'; +import LockIcon from '@mui/icons-material/Lock'; import { Alert, AlertTitle, @@ -27,27 +28,19 @@ import { CircularProgress, Grid, Tooltip, - Typography, } from "@mui/material"; - -import ContactPageIcon from '@mui/icons-material/ContactPage'; -import EventNoteIcon from '@mui/icons-material/EventNote'; -import LockIcon from '@mui/icons-material/Lock'; - import { DataGrid } from '@mui/x-data-grid'; - import { DateTime } from "luxon"; +import { useNavigate } from "react-router"; +import { fetchWithReLogin, GlobalLoginContext } from "../login/ReLoginDialog.js"; +import { FORM_ENTRY_CONTAINER_PROPS } from "../questionnaire/QuestionnaireStyle.jsx"; import ResourceHeader from "../questionnaire/ResourceHeader.jsx"; -import DateQuestionUtilities from "../questionnaire/DateQuestionUtilities"; import { getSubjectIdFromPath, getHierarchyAsList, getHomepageLink } from "../questionnaire/SubjectIdentifier"; -import { FORM_ENTRY_CONTAINER_PROPS } from "../questionnaire/QuestionnaireStyle.jsx"; - -import { fetchWithReLogin, GlobalLoginContext } from "../login/ReLoginDialog.js"; const statusIcons = { - "LOCKED" : , - "Default" : , + "LOCKED": , + "Default": , } const visitGridColumns = [ @@ -57,8 +50,8 @@ const visitGridColumns = [ width: 50, renderCell: ({ value }) => ( value && statusIcons[value] - ? {statusIcons[value]} - : statusIcons["Default"] + ? {statusIcons[value]} + : statusIcons["Default"] ) }, { @@ -76,8 +69,7 @@ const visitGridColumns = [ if (value instanceof Date) { value = value.toISOString(); } - let dateTime = DateQuestionUtilities.toPrecision(DateQuestionUtilities.stripTimeZone(value)); - let dateTimeString = !dateTime?.isValid ? "" : dateTime.toLocaleString(DateTime.DATETIME_MED_WITH_WEEKDAY); + let dateTimeString = DateTime.fromISO(value).toLocaleString(DateTime.DATETIME_MED_WITH_WEEKDAY); return dateTimeString; } }, @@ -97,13 +89,13 @@ function Patient(props) { const patientUuid = getSubjectIdFromPath(location.pathname); // Data already associated with the subject - const [ patientData, setPatientData ] = useState(); + const [patientData, setPatientData] = useState(); // List of visits on record for this patient - const [ visits, setVisits ] = useState(); + const [visits, setVisits] = useState(); // Visit data formatted for display in a DataGrid - const [ visitGridRows, setVisitGridRows ] = useState(); + const [visitGridRows, setVisitGridRows] = useState(); // When something goes wrong: - const [ error, setError ] = useState(); + const [error, setError] = useState(); const navigate = useNavigate(); @@ -183,8 +175,7 @@ function Patient(props) { let name = [lName, fName].filter(n => n).join(", "); let dobAnswer = patientData?.date_of_birth; - let dob = DateQuestionUtilities.toPrecision(DateQuestionUtilities.stripTimeZone(dobAnswer)); - let dobString = !dob?.isValid ? "" : dob.toLocaleString(DateTime.DATE_FULL); + let dobString = DateTime.fromISO(dobAnswer).toLocaleString(DateTime.DATE_FULL); let sex = patientData?.sex; let birthInfo = [dobString, sex].filter(i => i).join(", "); @@ -193,7 +184,7 @@ function Patient(props) { {name ? {name} : null} {birthInfo ? <> {birthInfo} : null} - : null + : null }; const patientInfo = displayPatientInfo(); @@ -208,19 +199,21 @@ function Patient(props) { - { patientInfo && + {patientInfo && - }> - { patientInfo } + }> + {patientInfo} } { - event?.preventDefault(); - navigate(`/content.html${params.row.path}`); + event?.preventDefault(); + navigate(`/content.html${params.row.path}`); }} pageSizeOptions={[5]} /> diff --git a/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/SubjectView.jsx b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/SubjectView.jsx index 9f6096ef7d..a1f410c633 100644 --- a/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/SubjectView.jsx +++ b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/SubjectView.jsx @@ -16,10 +16,8 @@ // specific language governing permissions and limitations // under the License. // -import React from "react"; - -import { getEntityIdentifier } from "../themePage/EntityIdentifier.jsx"; import DefaultSubjectView from "../dataHomepage/SubjectView.jsx"; +import { getEntityIdentifier } from "../themePage/EntityIdentifier.jsx"; function SubjectView(props) { diff --git a/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/Subjects.jsx b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/Subjects.jsx index 27f97850c3..3f41108aa7 100644 --- a/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/Subjects.jsx +++ b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/Subjects.jsx @@ -16,11 +16,9 @@ // specific language governing permissions and limitations // under the License. // -import React from "react"; - +import DefaultSubjects from "../dataHomepage/Subjects.jsx"; import { getHierarchy } from "../questionnaire/SubjectIdentifier.jsx"; import { getEntityIdentifier } from "../themePage/EntityIdentifier.jsx"; -import DefaultSubjects from "../dataHomepage/Subjects.jsx"; function Subjects(props) { diff --git a/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/SurveyLinkButton.jsx b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/SurveyLinkButton.jsx index 2910396941..25a5ee3b8b 100644 --- a/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/SurveyLinkButton.jsx +++ b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/SurveyLinkButton.jsx @@ -16,18 +16,17 @@ // specific language governing permissions and limitations // under the License. // -import React, { useState, useEffect, useContext } from "react"; -import PropTypes from "prop-types"; +import { useState, useContext } from "react"; +import DoneIcon from '@mui/icons-material/Done'; +import ErrorIcon from '@mui/icons-material/Error'; +import ShareIcon from '@mui/icons-material/Share'; import { CircularProgress, IconButton, Tooltip, } from "@mui/material"; - -import ErrorIcon from '@mui/icons-material/Error'; -import DoneIcon from '@mui/icons-material/Done'; -import ShareIcon from '@mui/icons-material/Share'; +import PropTypes from "prop-types"; import { fetchWithReLogin, GlobalLoginContext } from "../login/ReLoginDialog.js"; diff --git a/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/Visit.jsx b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/Visit.jsx index bb19193f4b..bf24a4e914 100644 --- a/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/Visit.jsx +++ b/modules/clinician-dashboard/src/main/frontend/src/clinician-dashboard/Visit.jsx @@ -16,10 +16,13 @@ // specific language governing permissions and limitations // under the License. // -import React, { useState, useEffect, useContext } from "react"; - -import { useNavigate } from "react-router"; +import { useState, useEffect, useContext } from "react"; +import SurveyIcon from '@mui/icons-material/Assignment'; +import DoneIcon from '@mui/icons-material/Done'; +import EventNoteIcon from '@mui/icons-material/EventNote'; +import LockIcon from '@mui/icons-material/Lock'; +import WarningIcon from '@mui/icons-material/Warning'; import { Alert, AlertTitle, @@ -35,26 +38,19 @@ import { ListItemText, Typography, } from "@mui/material"; - -import DoneIcon from '@mui/icons-material/Done'; -import WarningIcon from '@mui/icons-material/Warning'; -import SurveyIcon from '@mui/icons-material/Assignment'; -import LockIcon from '@mui/icons-material/Lock'; -import EventNoteIcon from '@mui/icons-material/EventNote'; - +import { DateTime } from "luxon"; +import { useNavigate } from "react-router"; import { makeStyles, withStyles } from 'tss-react/mui'; -import { DateTime } from "luxon"; import SurveyLinkButton from "./SurveyLinkButton"; +import FormattedText from "../components/FormattedText"; import EditButton from "../dataHomepage/EditButton"; import PrintButton from "../dataHomepage/PrintButton"; import SubjectLockAction from "../locking/SubjectLockAction"; -import FormattedText from "../components/FormattedText"; +import { fetchWithReLogin, GlobalLoginContext } from "../login/ReLoginDialog.js"; +import QuestionnaireStyle, { FORM_ENTRY_CONTAINER_PROPS } from "../questionnaire/QuestionnaireStyle"; import ResourceHeader from "../questionnaire/ResourceHeader"; import { getSubjectIdFromPath, getHierarchyAsList, getTextHierarchy } from "../questionnaire/SubjectIdentifier"; -import DateQuestionUtilities from "../questionnaire/DateQuestionUtilities"; -import QuestionnaireStyle, { FORM_ENTRY_CONTAINER_PROPS } from "../questionnaire/QuestionnaireStyle"; -import { fetchWithReLogin, GlobalLoginContext } from "../login/ReLoginDialog.js"; const useStyles = makeStyles()(theme => ({ formItem: { @@ -84,7 +80,7 @@ const useStyles = makeStyles()(theme => ({ function Visit(props) { const id = getSubjectIdFromPath(location.pathname); - const [ , patientUuid, visitUuid ] = /^([^\/]+)\/([^\/]+)$/.exec(id); + const [ , patientUuid, visitUuid ] = /^([^/]+)\/([^/]+)$/.exec(id); // Identifier of the questionnaire set used for the visit const [ questionnaireSetId, setQuestionnaireSetId ] = useState(); @@ -174,7 +170,7 @@ function Visit(props) { 'targetUserType': value.targetUserType, '@path': value.questionnaire?.['@path'], } - }); + }); setQuestionnaires(data); let qids = Object.values(json || {}) @@ -191,7 +187,7 @@ function Visit(props) { let data = {}; questionnaireSetIds.forEach(q => { if (visit[questionnaireSet?.[q]?.title]?.[0]?.['jcr:primaryType'] == "cards:Form") { - data[q] = {...visit[questionnaireSet?.[q]?.title][0], targetUserType: questionnaireSet?.[q]?.targetUserType}; + data[q] = { ...visit[questionnaireSet?.[q]?.title][0], targetUserType: questionnaireSet?.[q]?.targetUserType }; ids.push(q); } }); @@ -247,8 +243,7 @@ function Visit(props) { const displayVisitDateTime = () => { let dateTimeAnswer = getVisitField("time"); - let dateTime = DateQuestionUtilities.toPrecision(DateQuestionUtilities.stripTimeZone(dateTimeAnswer)); - return !dateTime?.isValid ? "" : dateTime.toLocaleString(DateTime.DATETIME_MED_WITH_WEEKDAY); + return DateTime.fromISO(dateTimeAnswer).toLocaleString(DateTime.DATETIME_MED_WITH_WEEKDAY); } const displayVisitInfo = () => { @@ -257,11 +252,11 @@ function Visit(props) { let provider = getVisitField("provider"); provider = provider && provider.length > 1 ? provider.join(", ") : provider; return (dateTime || location || provider) ? - } sx={{marginTop: -2}}> + } sx={{ marginTop: -2 }}> - {dateTime ? <> {dateTime} : null} - {location ? <> at {location} : null} - {provider ? <> with {provider} : null} + {dateTime ? <> {dateTime} : null} + {location ? <> at {location} : null} + {provider ? <> with {provider} : null} : null @@ -293,7 +288,7 @@ function Visit(props) { const isPageComplete = (questionnaireId, section) => { let answerSection = Object.values(surveyData?.[questionnaireId] || {}) - .find(e => (e?.section?.["jcr:uuid"] == section?.["jcr:uuid"])); + .find(e => (e?.section?.["jcr:uuid"] == section?.["jcr:uuid"])); return answerSection && !answerSection.statusFlags?.includes("INCOMPLETE"); } @@ -303,7 +298,7 @@ function Visit(props) { variant="outlined" size="small" className={`${classes[flag + "Flag"] || classes.DefaultFlag}`} - sx={{mr: 1}} + sx={{ mr: 1 }} key={flag} /> ) @@ -324,53 +319,52 @@ function Visit(props) { <> {title} - { qIds.map((q, i) => ( - } - > - navigate(`/content.html${surveyData?.[q]?.["@path"]}`)}> - - { isFormLocked(q) ? lockedIndicator : ( - isFormComplete(q) ? doneIndicator : ( - isFormSubmitted(q) ? incompleteIndicator : surveyIndicator - ) - ) - } - - - { displayFlags(q) } - { !isFormComplete(q) && isFormNavigable(q) && listPages(q) } - } - slotProps={{'secondary': {'component': 'div'}}} - /> - - - ))} + { qIds.map((q, i) => ( + } + > + navigate(`/content.html${surveyData?.[q]?.["@path"]}`)}> + + { isFormLocked(q) ? lockedIndicator : ( + isFormComplete(q) ? doneIndicator : ( + isFormSubmitted(q) ? incompleteIndicator : surveyIndicator + ) + )} + + + { displayFlags(q) } + { !isFormComplete(q) && isFormNavigable(q) && listPages(q) } + } + slotProps={{ 'secondary': { 'component': 'div' } }} + /> + + + ))} ); // For navigable forms, list pages with their completion status const listPages = (qId) => ( - + { Object.values(surveyData?.[qId]?.questionnaire || {}) - .filter(c => c?.["jcr:primaryType"] == "cards:Section") - .map(s => { - let isComplete = isPageComplete(qId, s); - return ( - }> - {s.label || s["@name"]}} - /> - - ); - }) + .filter(c => c?.["jcr:primaryType"] == "cards:Section") + .map(s => { + let isComplete = isPageComplete(qId, s); + return ( + } key={ s["@name"] } > + {s.label || s["@name"]}} + /> + + ); + }) } ) diff --git a/modules/clinician-dashboard/src/main/resources/SLING-INF/content/libs/cards/resources/assetDependencies.json b/modules/clinician-dashboard/src/main/resources/SLING-INF/content/libs/cards/resources/assetDependencies.json index dd61e32e56..77d9910334 100644 --- a/modules/clinician-dashboard/src/main/resources/SLING-INF/content/libs/cards/resources/assetDependencies.json +++ b/modules/clinician-dashboard/src/main/resources/SLING-INF/content/libs/cards/resources/assetDependencies.json @@ -5,5 +5,9 @@ "clinician-dashboard.Form.js": ["asset:cards-login.ReLoginDialog.js", "asset:cards-dataentry.Forms.js", "asset:cards-dataentry.FormView.js", "asset:cards-pedigree.Pedigree.js"], "clinician-dashboard.Visit.js": ["asset:cards-login.ReLoginDialog.js"], "clinician-dashboard.Patient.js": ["asset:cards-login.ReLoginDialog.js"], - "clinician-dashboard.DashboardSettingsConfiguration.js": ["asset:cards-login.ReLoginDialog.js"] + "clinician-dashboard.DashboardSettingsConfiguration.js": ["asset:cards-login.ReLoginDialog.js"], + "clinician-dashboard.FormView": ["cards-dataentry.LiveTable"], + "clinician-dashboard.SubjectView": ["cards-dataentry.LiveTable"], + "clinician-dashboard.Forms": ["cards-login.ReLoginDialog"], + "clinician-dashboard.Subjects": ["cards-login.ReLoginDialog"] } diff --git a/modules/data-entry/src/main/frontend/src/questionnaire/Form.jsx b/modules/data-entry/src/main/frontend/src/questionnaire/Form.jsx index 99ff538cdf..40f1c329be 100644 --- a/modules/data-entry/src/main/frontend/src/questionnaire/Form.jsx +++ b/modules/data-entry/src/main/frontend/src/questionnaire/Form.jsx @@ -73,8 +73,9 @@ import { usePageNameWriterContext } from "../themePage/Page.jsx"; * */ function Form (props) { - let { classes, contentOffset, extensionURL } = props; let { + classes, + contentOffset, mode, className, disableHeader, @@ -84,7 +85,9 @@ function Form (props) { doneLabel, onDone, questionnaireAddons, - paginationProps + paginationProps, + actionSwitches, + extension } = props; // Record if the form was already checked out before opening it, which may indicate that another user is editing, or it is being edited in a different tab let [ wasCheckedOut, setWasCheckedOut ] = useState(false); @@ -125,6 +128,7 @@ function Form (props) { let id = props.id || /Forms\/([^./]+)/.exec(location.pathname)[1]; let isEdit = window.location.pathname.endsWith(".edit") || mode == "edit"; let isSummary = window.location.pathname.endsWith(".summary") || mode == "summary"; + const extensionURL = extension?.["cards:extensionURL"] || props.extensionURL || ""; // Whether we reached the of the form (as opposed to a page that is not the last on a paginated form) let [ endReached, setEndReached ] = useState(); @@ -499,7 +503,7 @@ function Form (props) { onClose={() => setActionsMenu(null)} /> - ( isActionEnabled("text") && + { isActionEnabled("text") &&