From 31ddb635e4ca8fb795350be6fdda1623837762bc Mon Sep 17 00:00:00 2001 From: Siva Reddy Date: Fri, 3 Nov 2023 10:33:53 +0530 Subject: [PATCH] BS-258 | SNOMED Changes (#768) * BS-160 | Patrick | Filter and display only current medication alert (#662) * BS-160 | Patrick | Close alerts when medication input is cleared (#670) * BS 216 | BS-Drug Drug - Enhance FHIR CDSS Service based on Standard Format(Only Salt scope) (#673) * BS-216 | Mani, Siva | updated filtering cdss alert logic * BS-216 | Mani, Siva | fixed ts lint * BS-207 | Placement of previous alerts near Drugs (#683) * BS-207 | Patrick | add CDSS alert icons and tooltips for previous drug alerts * BS-207 | Patrick | Fix tooltip placement * BS-207 | Patrick | add cdss alert details and link * BS-207 | Patrick | make CDSS alert icon a button * BS-207 | Patrick | fix alert icon button size * BS-207 | Patrick | Refactor previous alert filtering logic * BS-207 | Patrick | Refactor new drug order alert filtering logic * BS-207 | Patrick | remove font size from global icon class * BS-207 | Patrick | Allow multiple cdss alerts in one tooltip * BS-207 | Patrick | move tooltip trigger to button * BS-207 | Patrick | Fix tooltip padding * BS-207 | Patrick | Add directive for CDSS alerts * BS-207 | Patrick | Add watchers for the alerts and treatment changes * BS-207 | Patrick | Add translations for "Dismissed" * BS-207 | Patrick | Add unit tests * BS-207 | Patrick | remove translations for other languages * BS-208 | Placement of Alert near Diagnosis (#687) * BS-208 | Patrick | Add alerts for past and saved diagnoses * BS-208 | Patrick | Update system information in FHIR bundle payload * BS-208 | Patrick | update drug uuid when getting drug concept system * BS-208 | Patrick | Refactor alert filtering * BS-208 | Patrick | fix circular dependency bug and styling * BS-208 | Patrick | Refactor to include unsaved medication * BS-208 | Patrick | refactor bundle generation for draft drugs * BS-208 | Patrick | create CDSS service * BS-208 | Patrick | Remove unused cdss methods for diagnosis and treatment * BS-208 | Patrick | Add Unit tests * BS 247 - FHIR Export Admin UI (#757) * BS-247 | Vijay,Siva | Admin UI * BS-247 | Vijay,Siva | Admin UI Unit Test * BS-247 | Vijay,Mani,Siva | restore missing code * BS-247 | Vijay,Siva | restore missing code * BS-247 | Vijay | add mising file * BS-247 | vijay | add username and fix date format * BS-247 | vijay | i18n * BS-247 | vijay | cleanup * BS-58: Trigger and show CDSS Active alert alert at the diagnosis/conditions and Medications tab (#728) * BS-58 | Patrick | Add active and dismissed CDSS alert row form medications tab * BS-58 | Patrick | trigger cdss also when updating medication * BS-58 | Patrick | Fix cdss alert audit * BS-58 | Patrick | Fix alerts status update on diagnosis tab * BS-58 | Patrick | Remove close button from diagnosis alert * BS-58 | Patrick | align source link and watcher for new medication alerts * BS-270 Added a tooltip to procedures buttons (#723) * BS-270 | Patrick | Add tooltip to procedures buttons * BS-270 | Patrick | Show tooltip only if text overflows * BS-270 | Patrick | fix formatting issues * BS-270 | Vijay, Mani | remove timeout discrepancy * BS-270 | Vijay, Mani | resolve timeout discrepancy * BS-270 | Patrick | Add comment for tooltip positioning --------- Co-authored-by: vijayanandtwks * BS-282 | Adding Drug Reference Maps to Order set member (#758) * BS-281 | CDSS Observations for all the interactions (#761) * BS-281 | Patrick | fix diagnosis alert message layout * BS=281 | Patrick | Show alerts on conditions change * BS-281 | Patrick | Fix cdss popover button padding for conditions * BS-281 | Patrick | Update cdss alerts filter to keep previous alert statuses * BS-281 | patrick | Get alerts on drug refills * BS-281 | Patrick | Add high dosage prefix for high dosage alerts * BS-281 | Patrick | Fix medication duration error * BS-281 | Patrick | Show alerts on dosage recalculation and activated condition status * BS-281 | Patrick | Label saved medications and condition alerts as dismissed * BS-281 | Patrick | Update diagnoses alerts if medication is removed * BS-281 | Patrick | Change diagnosis alert link color to black * BS-281 | Patrick | Align dismissed condition alert icon * BS-281 | Patrick | Fix alert expand toggle in medications tab * BS-281 | Patrick | Fix to Avoid calling CDSS methods if CDSS is disabled * BS-281 | Patrick | Remove alerts from inactive diagnosis rows * BS-283 | BS-285 | Mani | fixed issue when removal of last draft medications/conditions by adding patient resource to get patient id (#763) * BS-283 | BS-285 | Mani | fixed issue when removal of last draft medications/conditions by adding patient resource to get patient id * BS-283 | BS-285 | Mani | moved patient resource creation inside the guard condition * BS-14 | Trigger CDSS on the Drug Order Sets (#765) * BS-14 | Patrick | Add CDSS alerts for drug order sets * BS-14 | Patrick | Fix button group styling * BS281 | CDSS Observations for all the interactions (#766) * BS-281 | Patrick | Fix closing alert issue * BS-281 | Patrick | Clear alerts if diagnosis is cleared * BS-281 | Patrick | Remove alerts for cleared conditions * Adding missing Keys --------- Co-authored-by: Patrick Nyatindo Co-authored-by: manimaarans <110804183+manimaarans@users.noreply.github.com> Co-authored-by: vijayanandtwks --- ui/app/admin/app.js | 7 + .../admin/controllers/fhirExportController.js | 101 ++++++ .../controllers/orderTemplateController.js | 3 +- ui/app/admin/index.html | 2 + ui/app/admin/services/fhirExportService.js | 48 +++ ui/app/admin/views/fhirExport.html | 65 ++++ .../common/models/drugOrderViewModel.js | 1 - .../clinical/common/services/cdssService.js | 342 ++++++++++++++++++ .../controllers/addTreatmentController.js | 200 ++-------- .../controllers/consultationController.js | 17 + .../controllers/diagnosisController.js | 176 ++++++++- .../controllers/drugOrderHistoryController.js | 8 +- .../controllers/treatmentController.js | 32 +- .../consultation/directives/cdssAlertRow.js | 94 +++++ .../consultation/directives/cdssPopover.js | 13 + .../consultation/directives/newDrugOrders.js | 6 + .../consultation/directives/tooltip.js | 42 +++ .../consultation/views/cdssAlertRow.html | 87 +++++ .../consultation/views/cdssPopover.html | 16 + .../consultation/views/conditionRow.html | 15 +- .../consultation/views/conditions.html | 15 +- .../consultation/views/diagnosis.html | 20 +- .../consultation/views/diagnosisAlertRow.html | 16 + .../consultation/views/diagnosisRow.html | 14 +- .../consultation/views/newDrugOrders.html | 13 +- .../consultation/views/orderSelector.html | 4 +- .../views/treatmentSections/addTreatment.html | 46 +-- .../treatmentSections/drugOrderHistory.html | 17 +- ui/app/clinical/index.html | 7 + ui/app/common/constants.js | 4 + .../directives/drugOrdersSection.js | 53 +++ .../views/drugOrdersSection.html | 46 ++- ui/app/common/ui-helper/directives/popOver.js | 1 + ui/app/common/util/httpErrorInterceptor.js | 6 +- ui/app/i18n/admin/locale_en.json | 21 +- ui/app/i18n/clinical/locale_en.json | 5 +- ui/app/styles/admin/_auditLog.scss | 17 + ui/app/styles/bahmni-components/_tooltip.scss | 152 +++++++- ui/app/styles/clinical/_diagnosis.scss | 96 ++++- .../treatment/_customDrugOrderHistory.scss | 240 +++++++----- .../styles/clinical/treatment/_treatment.scss | 142 +++++++- .../treatment/_treatmentOrderSet.scss | 28 +- ui/app/styles/common/_bahmniGlobal.scss | 198 +++++----- .../controllers/fhirExportController.spec.js | 134 +++++++ .../controllers/diagnosisController.spec.js | 154 +++++++- .../consultation/services/cdssService.spec.js | 143 ++++++++ .../addTreatmentController.spec.js | 162 ++++++++- .../drugOrderHistoryController.spec.js | 24 +- .../clinical/directives/cdssAlertRow.spec.js | 186 ++++++++++ .../clinical/directives/cdssPopover.spec.js | 45 +++ 50 files changed, 2763 insertions(+), 521 deletions(-) create mode 100644 ui/app/admin/controllers/fhirExportController.js create mode 100644 ui/app/admin/services/fhirExportService.js create mode 100644 ui/app/admin/views/fhirExport.html create mode 100644 ui/app/clinical/common/services/cdssService.js create mode 100644 ui/app/clinical/consultation/directives/cdssAlertRow.js create mode 100644 ui/app/clinical/consultation/directives/cdssPopover.js create mode 100644 ui/app/clinical/consultation/directives/tooltip.js create mode 100644 ui/app/clinical/consultation/views/cdssAlertRow.html create mode 100644 ui/app/clinical/consultation/views/cdssPopover.html create mode 100644 ui/app/clinical/consultation/views/diagnosisAlertRow.html create mode 100644 ui/test/unit/admin/controllers/fhirExportController.spec.js create mode 100644 ui/test/unit/clinical/consultation/services/cdssService.spec.js create mode 100644 ui/test/unit/clinical/directives/cdssAlertRow.spec.js create mode 100644 ui/test/unit/clinical/directives/cdssPopover.spec.js diff --git a/ui/app/admin/app.js b/ui/app/admin/app.js index f9f1f91f59..bab682f62f 100644 --- a/ui/app/admin/app.js +++ b/ui/app/admin/app.js @@ -55,6 +55,13 @@ angular.module('admin') data: { backLinks: [{label: "Home", state: "admin.dashboard", icon: "fa-home"}] } + }).state('admin.fhirExport', { + url: '/fhirExport', + templateUrl: 'views/fhirExport.html', + controller: 'FHIRExportController', + data: { + backLinks: [{label: "Home", state: "admin.dashboard", icon: "fa-home"}] + } }); $httpProvider.defaults.headers.common['Disable-WWW-Authenticate'] = true; $bahmniTranslateProvider.init({app: 'admin', shouldMerge: true}); diff --git a/ui/app/admin/controllers/fhirExportController.js b/ui/app/admin/controllers/fhirExportController.js new file mode 100644 index 0000000000..ae6bd037c1 --- /dev/null +++ b/ui/app/admin/controllers/fhirExportController.js @@ -0,0 +1,101 @@ +'use strict'; + +angular.module('bahmni.admin') + .controller('FHIRExportController', ['$rootScope', '$scope', '$q', '$http', '$translate', 'messagingService', 'fhirExportService', function ($rootScope, $scope, $q, $http, $translate, messagingService, fhirExportService) { + var DateUtil = Bahmni.Common.Util.DateUtil; + + var convertToLocalDate = function (date) { + var localDate = DateUtil.parseLongDateToServerFormat(date); + return DateUtil.getDateTimeInSpecifiedFormat(localDate, 'MMMM Do, YYYY [at] h:mm:ss A'); + }; + + var subtractDaysFromToday = function (minusDays) { + const currentDate = new Date(); + currentDate.setDate(currentDate.getDate() - minusDays); + return currentDate; + }; + + $scope.startDate = subtractDaysFromToday(30); + $scope.endDate = subtractDaysFromToday(0); + + $scope.anonymise = true; + + var isLoggedInUserPrivileged = function (expectedPrivileges) { + var currentPrivileges = _.map($rootScope.currentUser.privileges, function (privilege) { + return privilege.name; + }); + var hasPrivilege = expectedPrivileges.some(function (privilege) { + return currentPrivileges.indexOf(privilege) !== -1; + }); + return hasPrivilege; + }; + + var hasInsufficientPrivilegeForPlainExport = function () { + var plainExportPrivileges = [Bahmni.Common.Constants.plainFhirExportPrivilege]; + var hasPlainExportPrivilege = isLoggedInUserPrivileged(plainExportPrivileges); + return !hasPlainExportPrivilege; + }; + $scope.isCheckboxDisabled = hasInsufficientPrivilegeForPlainExport(); + + var isUserPrivilegedForFhirExport = function () { + var defaultExportPrivileges = [Bahmni.Common.Constants.fhirExportPrivilege, Bahmni.Common.Constants.plainFhirExportPrivilege]; + return isLoggedInUserPrivileged(defaultExportPrivileges); + }; + $scope.hasExportPrivileges = isUserPrivilegedForFhirExport(); + + $scope.loadFhirTasksForPrivilegedUsers = function () { + var deferred = $q.defer(); + $scope.tasks = []; + if (isUserPrivilegedForFhirExport()) { + fhirExportService.loadFhirTasks().then(function (response) { + if (response.data && response.data.entry) { + response.data.entry.map(function (task) { + task.resource.authoredOn = convertToLocalDate(task.resource.authoredOn); + return task; + }); + $scope.tasks = response.data.entry; + deferred.resolve(); + } + }).catch(function (error) { + deferred.reject(error); + }); + } + return deferred.promise; + }; + + $scope.loadFhirTasksForPrivilegedUsers(); + + $scope.exportFhirData = function () { + var deferred = $q.defer(); + var startDate = DateUtil.getDateWithoutTime($scope.startDate); + var endDate = DateUtil.getDateWithoutTime($scope.endDate); + var anonymise = $scope.anonymise; + var username = $rootScope.currentUser.username; + + fhirExportService.export(username, startDate, endDate, anonymise).success(function () { + fhirExportService.submitAudit(username, startDate, endDate, anonymise).success(function () { + messagingService.showMessage("info", $translate.instant("EXPORT_PATIENT_REQUEST_SUBMITTED")); + $scope.loadFhirTasksForPrivilegedUsers(); + deferred.resolve(); + }); + }).catch(function (error) { + messagingService.showMessage("error", $translate.instant("EXPORT_PATIENT_REQUEST_SUBMIT_ERROR")); + console.error("FHIR Export request failed"); + deferred.reject(error); + }); + return deferred.promise; + }; + + $scope.extractAttribute = function (array, searchValue, attributeToExtract) { + var foundElement = array && array.find(function (inputElement) { return inputElement.type.text === searchValue; }); + if (foundElement && foundElement.hasOwnProperty(attributeToExtract)) { + return foundElement[attributeToExtract]; + } + return null; + }; + + $scope.extractBoolean = function (array, searchValue, attributeToExtract) { + var booleanStr = $scope.extractAttribute(array, searchValue, attributeToExtract); + return booleanStr && booleanStr.toLowerCase() === "true"; + }; + }]); diff --git a/ui/app/admin/controllers/orderTemplateController.js b/ui/app/admin/controllers/orderTemplateController.js index 98b082b460..b6a412b493 100644 --- a/ui/app/admin/controllers/orderTemplateController.js +++ b/ui/app/admin/controllers/orderTemplateController.js @@ -6,7 +6,8 @@ 'drug': { 'name': drug.name, 'uuid': drug.uuid, - 'form': drug.dosageForm.display + 'form': drug.dosageForm.display, + 'drugReferenceMaps': drug.drugReferenceMaps || [] }, 'value': drug.name }; diff --git a/ui/app/admin/index.html b/ui/app/admin/index.html index 194347201a..af5d4cbfd4 100644 --- a/ui/app/admin/index.html +++ b/ui/app/admin/index.html @@ -104,12 +104,14 @@ + + diff --git a/ui/app/admin/services/fhirExportService.js b/ui/app/admin/services/fhirExportService.js new file mode 100644 index 0000000000..5b38cabd52 --- /dev/null +++ b/ui/app/admin/services/fhirExportService.js @@ -0,0 +1,48 @@ +'use strict'; + +angular.module('bahmni.admin') +.service('fhirExportService', ['$http', '$translate', 'messagingService', function ($http, $translate, messagingService) { + var DateUtil = Bahmni.Common.Util.DateUtil; + var convertToLocalDate = function (date) { + var localDate = DateUtil.parseLongDateToServerFormat(date); + return DateUtil.getDateTimeInSpecifiedFormat(localDate, 'MMMM Do, YYYY [at] h:mm:ss A'); + }; + + this.loadFhirTasks = function () { + const params = { + "_sort:desc": "_lastUpdated", + _count: 50 + }; + return $http.get(Bahmni.Common.Constants.fhirTasks, {params: params}); + }; + + this.submitAudit = function (username, startDate, endDate, anonymise) { + var eventType = "PATIENT_DATA_BULK_EXPORT"; + var exportMode = anonymise ? "Anonymized" : "Non-Anonymized"; + var message = "User " + username + " performed a bulk patient data export for: Start Date " + convertToLocalDate(startDate) + " and End Date " + convertToLocalDate(endDate) + " in " + exportMode + " mode"; + var module = "Export"; + var auditData = { + username: username, + eventType: eventType, + message: message, + module: module + }; + return $http.post(Bahmni.Common.Constants.auditLogUrl, auditData, { + withCredentials: true + }); + }; + + this.export = function (username, startDate, endDate, anonymise) { + var url = Bahmni.Common.Constants.fhirExportUrl + "?anonymise=" + anonymise; + if (startDate) { + url = url + "&startDate=" + startDate; + } + if (endDate) { + url = url + "&endDate=" + endDate; + } + return $http.post(url, { + withCredentials: true, + headers: {"Accept": "application/json", "Content-Type": "application/json"} + }); + }; +}]); diff --git a/ui/app/admin/views/fhirExport.html b/ui/app/admin/views/fhirExport.html new file mode 100644 index 0000000000..34a8d40b93 --- /dev/null +++ b/ui/app/admin/views/fhirExport.html @@ -0,0 +1,65 @@ +
+
+

{{ 'INSUFFICIENT_PRIVILEGE_TO_EXPORT'|translate }}

+
+
+ {{ 'EXPORT_PATIENT_DATA_WARNING'|translate }} +
+
+
+

{{ 'PATIENT_DATA_BULK_FHIR_EXPORT_LABEL'|translate }}

+
+
+
+

+ + +

+

+ + +

+

+

+ + +
+

+ +
+
+
+
+
+

{{ 'FHIR_EXPORT_TABLE_HEADER_LABEL'|translate }}

+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
{{ 'EXPORT_USER_NAME'|translate }}{{ 'EXPORT_TRIGGERED_DATE'|translate }}{{ 'EXPORT_START_DATE'|translate }}{{ 'EXPORT_END_DATE'|translate }}{{ 'EXPORT_ANONYMISED'|translate }}{{ 'EXPORT_TASK_STATUS'|translate }}{{ 'EXPORT_DOWNLOAD_LINK'|translate }}
{{extractAttribute(task.resource.input,'FHIR Export User Name','valueString')}}{{task.resource.authoredOn}}{{extractAttribute(task.resource.input,'FHIR Export Start Date','valueString') | bahmniDate}}{{extractAttribute(task.resource.input,'FHIR Export End Date','valueString') | bahmniDate}}{{task.resource.status}}Download
+
+ +
\ No newline at end of file diff --git a/ui/app/clinical/common/models/drugOrderViewModel.js b/ui/app/clinical/common/models/drugOrderViewModel.js index 496ef8b1bf..8b10fdb50b 100644 --- a/ui/app/clinical/common/models/drugOrderViewModel.js +++ b/ui/app/clinical/common/models/drugOrderViewModel.js @@ -707,7 +707,6 @@ Bahmni.Clinical.DrugOrderViewModel.createFromContract = function (drugOrderRespo }; } viewModel.instructions = administrationInstructions.instructions; - viewModel.audit = drugOrderResponse.audit; viewModel.additionalInstructions = administrationInstructions.additionalInstructions; viewModel.quantity = drugOrderResponse.dosingInstructions.quantity; viewModel.quantityUnit = drugOrderResponse.dosingInstructions.quantityUnits; diff --git a/ui/app/clinical/common/services/cdssService.js b/ui/app/clinical/common/services/cdssService.js new file mode 100644 index 0000000000..8e0de3f3a5 --- /dev/null +++ b/ui/app/clinical/common/services/cdssService.js @@ -0,0 +1,342 @@ +'use strict'; + +angular.module('bahmni.clinical') + .service('cdssService', ['drugService', '$rootScope', function (drugService, $rootScope) { + var createMedicationRequest = function (medication, patientUuid, conceptSource) { + return extractCodeInfo(medication, conceptSource).then(function (coding) { + var medicationRequest = { + resourceType: 'MedicationRequest', + id: medication.uuid, + status: 'active', + intent: 'order', + subject: { + reference: 'Patient/' + patientUuid + }, + medicationCodeableConcept: { + id: medication.drug.uuid, + coding: coding, + text: medication.drugNameDisplay + }, + "dosageInstruction": [ + { + "text": angular.toJson({ "instructions": medication.instructions }), + "timing": { + "event": [medication.effectiveStartDate], + "repeat": { + "duration": medication.durationInDays, + "durationUnit": 'd' + }, + "code": { + "coding": [ + { + "code": medication.drug.uuid, + "display": medication.uniformDosingType.frequency + } + ], + "text": medication.uniformDosingType.frequency + } + }, + "asNeededBoolean": medication.asNeeded, + "doseAndRate": [ + { + "doseQuantity": { + "value": medication.uniformDosingType.dose, + "unit": medication.doseUnits, + "code": medication.drug.uuid + } + } + ] + } + ] + + }; + return { + resource: medicationRequest + }; + }); + }; + + var extractConditionInfo = function (condition) { + var uuid = condition.concept.uuid.split('/'); + var code = uuid[uuid.length - 1]; + uuid.pop(); + var system = uuid.join('/'); + return { + code: code, + system: system, + display: condition.concept.name + }; + }; + + var extractCodeInfo = function (medication, conceptSource) { + if (!(medication.drug.drugReferenceMaps && medication.drug.drugReferenceMaps.length > 0)) { + return Promise.resolve([{ + code: medication.drug.uuid, + display: medication.drug.name, + system: 'https://fhir.openmrs.org' + }]); + } else { + var drugReferenceMap = medication.drug.drugReferenceMaps[0]; + if (!conceptSource) { + return drugService.getDrugConceptSourceMapping(medication.drug.uuid).then(function (response) { + var bundle = response.data; + var code = bundle.entry && bundle.entry.length > 0 && bundle.entry[0].resource.code; + var conceptCode = code.coding.find(function (coding) { + return coding.system; + }); + if (conceptCode) { + localStorage.setItem("conceptSource", conceptCode.system); + conceptSource = conceptCode.system; + return [{ + system: conceptSource, + code: drugReferenceMap.conceptReferenceTerm && drugReferenceMap.conceptReferenceTerm.display && drugReferenceMap.conceptReferenceTerm.display.split(':')[1].trim(), + display: medication.drug.name + }, { + code: medication.drug.uuid, + system: 'https://fhir.openmrs.org', + display: medication.drug.name + }]; + } else { + return [{ + code: medication.drug.uuid, + display: medication.drug.name, + system: 'https://fhir.openmrs.org' + }]; + } + }); + } else { + return Promise.resolve([{ + system: conceptSource, + code: drugReferenceMap.conceptReferenceTerm && drugReferenceMap.conceptReferenceTerm.display && drugReferenceMap.conceptReferenceTerm.display.split(':')[1].trim(), + display: medication.drug.name + }, { + code: medication.drug.uuid, + system: 'https://fhir.openmrs.org', + display: medication.drug.name + }]); + } + } + }; + + var createConditionResource = function (condition, patientUuid, isDiagnosis) { + var conceptLimitIndex = isDiagnosis ? -1 : condition.concept.uuid.lastIndexOf('/'); + var conditionStatus = condition.status || condition.diagnosisStatus || condition.certainty; + var activeConditions = ['CONFIRMED', 'PRESUMED', 'ACTIVE']; + var status = (!conditionStatus || activeConditions.indexOf(conditionStatus) > -1) ? 'active' : 'inactive'; + var conditionCoding = condition.concept ? extractConditionInfo(condition) : { + system: isDiagnosis ? condition.codedAnswer.conceptSystem : (conceptLimitIndex > -1 ? (condition.concept.uuid.substring(0, conceptLimitIndex) || '') : ''), + code: isDiagnosis ? condition.codedAnswer.uuid : (conceptLimitIndex > -1 ? condition.concept.uuid.substring(conceptLimitIndex + 1) : condition.concept.uuid), + display: isDiagnosis ? condition.codedAnswer.name : condition.concept.name + }; + + var conditionResource = { + resourceType: 'Condition', + id: condition.uuid, + clinicalStatus: { + coding: [ + { + code: status, + display: status, + system: 'http://terminology.hl7.org/CodeSystem/condition-clinical' + } + ] + }, + code: { + coding: [ conditionCoding ], + text: isDiagnosis ? condition.codedAnswer.name : condition.concept.name + }, + subject: { + reference: 'Patient/' + patientUuid + } + }; + if (angular.isNumber(condition.onSetDate) === 'number') { + conditionResource.onsetDateTime = new Date(condition.onSetDate).toLocaleDateString('en-CA'); + } + if (!conditionResource.onsetDateTime) { + delete conditionResource.onsetDateTime; + } + return { + resource: conditionResource + }; + }; + + function createPatientResource (patient) { + var patientResource = { + resourceType: 'Patient', + id: patient.uuid + }; + return { + resource: patientResource + }; + } + + var createFhirBundle = function (patient, conditions, medications, diagnosis, conceptSource) { + var encounterResource = conditions.filter(function (condition) { + return !condition.uuid; + }).map(function (condition) { + return createConditionResource(condition, patient.uuid, false); + }); + encounterResource = encounterResource.concat(diagnosis.map(function (condition) { + return createConditionResource(condition, patient.uuid, true); + })); + + medications = medications.filter(function (medication) { + return angular.isDefined(medication.include) && medication.include || medication.include === undefined; + }); + + return Promise.all(medications.map(function (medication) { + return createMedicationRequest(medication, patient.uuid, conceptSource).then(function (medicationResource) { + return medicationResource; + }); + })).then(function (medicationResources) { + var bundleResource = { + resourceType: 'Bundle', + type: 'collection', + entry: [] + }; + if (medicationResources.length === 0 && encounterResource.length === 0) { + var patientResource = createPatientResource(patient); + bundleResource.entry = bundleResource.entry.concat(patientResource); + return bundleResource; + } + bundleResource.entry = bundleResource.entry.concat(encounterResource, medicationResources); + return bundleResource; + }); + }; + + var getAlerts = function (cdssEnabled, consultation, patient) { + if (cdssEnabled) { + var consultationData = angular.copy(consultation); + consultationData.patient = patient; + + var orderSetTreatments = consultationData.newlyAddedTabTreatments ? consultationData.newlyAddedTabTreatments.allMedicationTabConfig.orderSetTreatments : []; + var drafts = consultationData.newlyAddedTabTreatments ? consultationData.newlyAddedTabTreatments.allMedicationTabConfig.treatments : []; + consultationData.draftDrug = drafts.concat(orderSetTreatments); + var params = createParams(consultationData); + createFhirBundle(params.patient, params.conditions, params.medications, params.diagnosis) + .then(function (bundle) { + var cdssAlerts = drugService.sendDiagnosisDrugBundle(bundle); + cdssAlerts.then(function (response) { + var alerts = response.data; + var existingAlerts = $rootScope.cdssAlerts || []; + $rootScope.cdssAlerts = addNewAlerts(alerts, existingAlerts, bundle); + }); + }); + } + }; + + var createParams = function (consultationData) { + var patient = consultationData.patient; + var conditions = consultationData.condition && consultationData.condition.concept.uuid ? consultationData.conditions.concat(consultationData.condition) : consultationData.conditions; + var diagnosis = consultationData.newlyAddedDiagnoses && consultationData.newlyAddedDiagnoses.filter(function (diagnosis) { + return diagnosis.codedAnswer && diagnosis.codedAnswer.name; + }) || []; + var medications = consultationData.draftDrug; + return { + patient: patient, + conditions: conditions, + diagnosis: diagnosis, + medications: medications + }; + }; + + var getAlertMedicationCodes = function (alert) { + if (alert.referenceMedications) { + return alert.referenceMedications.map(function (med) { + return med.coding[0].code; + }); + } + return []; + }; + + var getAlertConditionCodes = function (alert) { + if (alert.referenceCondition) { + return alert.referenceCondition.coding.map(function (cond) { + return cond.code; + }); + } + return []; + }; + + var getMedicationCodesFromEntry = function (entry) { + return entry.resource.medicationCodeableConcept.coding[0].code; + }; + + var getConditionCodesFromEntry = function (entry) { + return entry.resource.code.coding[0].code; + }; + + var isMedicationRequest = function (entry) { + return entry.resource.resourceType === 'MedicationRequest'; + }; + + var isCondition = function (entry) { + return entry.resource.resourceType === 'Condition'; + }; + + var checkAlertBundleMatch = function (alert, bundle) { + var alertMedicationCodes = getAlertMedicationCodes(alert); + var alertConditionCodes = getAlertConditionCodes(alert); + + var bundleMedicationCodes = bundle.entry + .filter(isMedicationRequest) + .map(getMedicationCodesFromEntry); + + var bundleConditionCodes = bundle.entry + .filter(isCondition) + .map(getConditionCodesFromEntry); + + return ( + alertMedicationCodes.some(function (code) { + return bundleMedicationCodes.includes(code); + }) || + alertConditionCodes.some(function (code) { + return bundleConditionCodes.includes(code); + }) + ); + }; + + var addNewAlerts = function (newAlerts, currentAlerts, bundle) { + var activeAlerts = newAlerts.map(function (item) { + var isAlertInBundle = checkAlertBundleMatch(item, bundle); + if (isAlertInBundle) { + item.isActive = true; + } + item.detail = item.detail.indexOf('\n') > -1 ? marked.parse(item.detail) : item.detail; + return item; + }); + if (!currentAlerts || (currentAlerts && currentAlerts.length === 0)) { + return activeAlerts; + } + var alerts = activeAlerts.map(function (alert) { + var getAlert = currentAlerts.find(function (currentAlert) { + return currentAlert.uuid === alert.uuid; + }); + if (getAlert) { + if (alert.indicator !== getAlert.indicator || alert.summary !== getAlert.summary) { + alert.isActive = true; + } else { + alert.isActive = getAlert.isActive; + } + } + return alert; + }); + + return alerts; + }; + + var sortInteractionsByStatus = function (alerts) { + var order = { "critical": 0, "warning": 1, "info": 2 }; + return alerts.sort(function (a, b) { + return order[a.indicator] - order[b.indicator]; + }); + }; + + return { + createFhirBundle: createFhirBundle, + createParams: createParams, + addNewAlerts: addNewAlerts, + sortInteractionsByStatus: sortInteractionsByStatus, + getAlerts: getAlerts + }; + }]); diff --git a/ui/app/clinical/consultation/controllers/addTreatmentController.js b/ui/app/clinical/consultation/controllers/addTreatmentController.js index 5740d5508e..8830119650 100644 --- a/ui/app/clinical/consultation/controllers/addTreatmentController.js +++ b/ui/app/clinical/consultation/controllers/addTreatmentController.js @@ -3,10 +3,10 @@ angular.module('bahmni.clinical') .controller('AddTreatmentController', ['$scope', '$rootScope', 'contextChangeHandler', 'treatmentConfig', 'drugService', '$timeout', 'clinicalAppConfigService', 'ngDialog', '$window', 'messagingService', 'appService', 'activeDrugOrders', - 'orderSetService', '$q', 'locationService', 'spinner', '$translate', '$state', + 'orderSetService', '$q', 'locationService', 'spinner', '$translate', '$state', 'cdssService', function ($scope, $rootScope, contextChangeHandler, treatmentConfig, drugService, $timeout, clinicalAppConfigService, ngDialog, $window, messagingService, appService, activeDrugOrders, - orderSetService, $q, locationService, spinner, $translate, $state) { + orderSetService, $q, locationService, spinner, $translate, $state, cdssService) { var DateUtil = Bahmni.Common.Util.DateUtil; var DrugOrderViewModel = Bahmni.Clinical.DrugOrderViewModel; var scrollTop = _.partial($window.scrollTo, 0, 0); @@ -79,9 +79,9 @@ angular.module('bahmni.clinical') $scope.submitAudit = function (index) { var patientUuid = $scope.patient.uuid; - var message = $scope.cdssaAlerts[index].summary.replace(/"/g, ''); + var message = $scope.newAlerts[index].summary.replace(/"/g, ''); var eventType = 'Dismissed: ' + $scope.treatment.audit; - $scope.cdssaAlerts.splice(index, 1); + $scope.newAlerts.splice(index, 1); return drugService .cdssAudit(patientUuid, eventType, message, 'CDSS') .then(function () { @@ -158,12 +158,12 @@ angular.module('bahmni.clinical') durationUnit: treatment.durationUnit }; }; - var getCdssEnabled = function () { + + (function () { drugService.getCdssEnabled().then(function (response) { $scope.cdssEnabled = response.data; }); - }; - getCdssEnabled(); + })(); var isSameDrugBeingDiscontinuedAndOrdered = function () { var existingTreatment = false; @@ -272,6 +272,7 @@ angular.module('bahmni.clinical') } */ $scope.refillDrug(drugOrder, alreadyActiveSimilarOrder); + getAlerts(); }); var refillDrugOrders = function (drugOrders) { @@ -293,6 +294,7 @@ angular.module('bahmni.clinical') $scope.$on("event:refillDrugOrders", function (event, drugOrders) { $scope.bulkSelectCheckbox = false; refillDrugOrders(drugOrders); + getAlerts(); }); $scope.$on("event:discontinueDrugOrder", function (event, drugOrder) { @@ -380,12 +382,18 @@ angular.module('bahmni.clinical') if ($scope.treatment.isBeingEdited) { treatments.splice($scope.treatment.currentIndex, 1, $scope.treatment); $scope.treatment.isBeingEdited = false; + getAlerts(); } else { treatments.push($scope.treatment); + getAlerts(); } $scope.clearForm(); }; + var getAlerts = function () { + return cdssService.getAlerts($scope.cdssEnabled, $scope.consultation, $scope.patient); + }; + var getConflictingDrugOrder = function (newDrugOrder) { var allDrugOrders = $scope.treatments.concat($scope.orderSetTreatments); allDrugOrders = _.reject(allDrugOrders, newDrugOrder); @@ -491,6 +499,7 @@ angular.module('bahmni.clinical') $scope.$on("event:removeDrugOrder", function (event, index) { $scope.treatments.splice(index, 1); + getAlerts(); }); $scope.incompleteDrugOrders = function () { @@ -527,14 +536,14 @@ angular.module('bahmni.clinical') }; $scope.closeAlert = function (index) { - $scope.cdssaAlerts = $scope.cdssaAlerts.filter(function (alert, alertIndex) { + $scope.newAlerts = $scope.newAlerts.filter(function (_alert, alertIndex) { return alertIndex !== index; }); }; $scope.toggleAlertDetails = function (index) { - $scope.cdssaAlerts[index].showDetails = - !$scope.cdssaAlerts[index].showDetails; + $scope.newAlerts[index].showDetails = + !$scope.newAlerts[index].showDetails; }; $scope.getDataResults = function (drugs) { @@ -544,175 +553,12 @@ angular.module('bahmni.clinical') }); return _.flatten(listOfDrugSynonyms); }; - var createMedicationRequest = function (medication, patientUuid) { - return extractCodeInfo(medication).then(function (coding) { - var medicationRequest = { - resourceType: 'MedicationRequest', - id: medication.uuid, - status: 'active', - intent: 'order', - subject: { - reference: 'Patient/' + patientUuid - }, - medicationCodeableConcept: { - id: medication.drug.uuid, - coding: [ - coding - ], - text: medication.drugNameDisplay - } - }; - return { - resource: medicationRequest - }; - }); - }; - var extractCodeInfo = function (medication) { - if (!(medication.drug.drugReferenceMaps && medication.drug.drugReferenceMaps.length > 0)) { - return Promise.resolve({ - code: medication.drug.uuid, - display: medication.drug.name - }); - } else { - var drugReferenceMap = medication.drug.drugReferenceMaps[0]; - if (!$scope.conceptSource) { - return drugService.getDrugConceptSourceMapping(medication.drug.uuid).then(function (response) { - var bundle = response.data; - var code = bundle.entry && bundle.entry.length > 0 && bundle.entry[0].resource.code; - var conceptCode = code.coding.find(function (coding) { - return coding.system; - }); - if (conceptCode) { - localStorage.setItem("conceptSource", conceptCode.system); - $scope.conceptSource = conceptCode.system; - return { - system: $scope.conceptSource, - code: drugReferenceMap.conceptReferenceTerm && drugReferenceMap.conceptReferenceTerm.display && drugReferenceMap.conceptReferenceTerm.display.split(':')[1].trim(), - display: medication.drug.name - }; - } else { - return { - code: medication.drug.uuid, - display: medication.drug.name - }; - } - }); - } else { - return Promise.resolve({ - system: $scope.conceptSource, - code: drugReferenceMap.conceptReferenceTerm && drugReferenceMap.conceptReferenceTerm.display && drugReferenceMap.conceptReferenceTerm.display.split(':')[1].trim(), - display: medication.drug.name - }); - } - } - }; - - var createConditionResource = function (condition, patientUuid, isDiagnosis) { - var conceptLimitIndex = isDiagnosis ? -1 : condition.concept.uuid.lastIndexOf('/'); - var conditionStatus = condition.status || condition.certainty; - var activeConditions = ['CONFIRMED', 'PRESUMED', 'ACTIVE']; - var status = activeConditions.indexOf(conditionStatus) > -1 ? 'active' : 'inactive'; - var conditionResource = { - resourceType: 'Condition', - id: condition.uuid, - clinicalStatus: { - coding: [ - { - code: status, - display: status.charAt(0).toUpperCase() + status.slice(1), - system: 'http://terminology.hl7.org/CodeSystem/condition-clinical' - } - ] - }, - code: { - coding: [ - { - system: isDiagnosis ? condition.codedAnswer.conceptSystem : (conceptLimitIndex > -1 ? (condition.concept.uuid.substring(0, conceptLimitIndex) || '') : ''), - code: isDiagnosis ? condition.codedAnswer.uuid : (conceptLimitIndex > -1 ? condition.concept.uuid.substring(conceptLimitIndex + 1) : condition.concept.uuid), - display: isDiagnosis ? condition.codedAnswer.name : condition.concept.name - } - ], - text: isDiagnosis ? condition.codedAnswer.name : condition.concept.name - }, - subject: { - reference: 'Patient/' + patientUuid - } - }; - if (angular.isNumber(condition.onSetDate) === 'number') { - conditionResource.onsetDateTime = new Date(condition.onSetDate).toLocaleDateString('en-CA'); - } - if (!conditionResource.onsetDateTime) { - delete conditionResource.onsetDateTime; - } - return { - resource: conditionResource - }; - }; - - $scope.createFhirBundle = function (patient, conditions, medications, diagnosis) { - var encounterResource = conditions.filter(function (condition) { - return !condition.uuid; - }).map(function (condition) { - return createConditionResource(condition, patient.uuid, false); - }); - encounterResource = encounterResource.concat(diagnosis.map(function (condition) { - return createConditionResource(condition, patient.uuid, true); - })); - - return Promise.all(medications.map(function (medication) { - return createMedicationRequest(medication, patient.uuid).then(function (medicationResource) { - return medicationResource; - }); - })).then(function (medicationResources) { - return { - resourceType: 'Bundle', - type: 'collection', - entry: [].concat(encounterResource, medicationResources) - }; - }); - }; - - var createParams = function (consultationData) { - var patient = consultationData.patient; - var conditions = consultationData.conditions; - var diagnosis = consultationData.newlyAddedDiagnoses; - var medications = consultationData.draftDrug; - return { - patient: patient, - conditions: conditions, - diagnosis: diagnosis, - medications: medications - }; - }; - - function sortInteractionsByStatus (arr) { - var order = { "critical": 0, "warning": 1, "info": 2 }; - return arr.sort(function (a, b) { - return order[a.indicator] - order[b.indicator]; - }); - } (function () { var selectedItem; $scope.onSelect = function (item) { selectedItem = item; $scope.onChange(); - if ($scope.cdssEnabled) { - var consultationData = angular.copy($scope.consultation); - consultationData.patient = $scope.patient; - - consultationData.draftDrug = [$scope.treatment].concat( - consultationData.newlyAddedTabTreatments ? consultationData.newlyAddedTabTreatments.allMedicationTabConfig.treatments : [] - ); - var params = createParams(consultationData); - $scope.createFhirBundle(params.patient, params.conditions, params.medications, params.diagnosis) - .then(function (bundle) { - var cdssaAlerts = drugService.sendDiagnosisDrugBundle(bundle); - cdssaAlerts.then(function (response) { - $scope.cdssaAlerts = sortInteractionsByStatus(response.data); - }); - }); - } }; $scope.onAccept = function () { $scope.treatment.acceptedItem = $scope.treatment.drugNameDisplay; @@ -740,14 +586,14 @@ angular.module('bahmni.clinical') return; } delete $scope.treatment.drug; - $scope.cdssaAlerts = []; + $scope.newAlerts = []; }; })(); $scope.clearForm = function () { $scope.treatment = newTreatment(); $scope.formInvalid = false; - $scope.cdssaAlerts = []; + $scope.newAlerts = []; clearHighlights(); markVariable("startNewDrugEntry"); }; @@ -937,7 +783,8 @@ angular.module('bahmni.clinical') calculateDoseForTemplatesIn(orderSet) .then(createDrugOrdersAndGetConflicts) .then(showConflictMessageIfAny) - .then(setUpNewOrderSet); + .then(setUpNewOrderSet) + .then(getAlerts); }; $scope.removeOrderSet = function () { @@ -957,6 +804,7 @@ angular.module('bahmni.clinical') }); $scope.popupActive = true; } + getAlerts(); }); $scope.consultation.preSaveHandler.register("drugOrderSaveHandlerKey", saveTreatment); diff --git a/ui/app/clinical/consultation/controllers/consultationController.js b/ui/app/clinical/consultation/controllers/consultationController.js index eb9761d35d..26c44b0960 100644 --- a/ui/app/clinical/consultation/controllers/consultationController.js +++ b/ui/app/clinical/consultation/controllers/consultationController.js @@ -515,6 +515,23 @@ angular.module('bahmni.clinical').controller('ConsultationController', return $q.when({}); } try { + var alerts = $rootScope.cdssAlerts || []; + var activeAlerts = alerts.filter(function (alert) { + return alert.indicator === 'critical' && alert.isActive; + }); + + if (activeAlerts && activeAlerts.length > 0) { + messagingService.showMessage("error", "{{ 'CDSS_ALERT_SAVE_ERROR' | translate }}"); + return $q.when({}); + } + + var cdssAlerts = $rootScope.cdssAlerts; + cdssAlerts && cdssAlerts.forEach( + function (cdssAlert) { + cdssAlert.isActive = false; + } + ); + $rootScope.cdssAlerts = cdssAlerts; preSaveEvents(); return spinner.forPromise($q.all([preSavePromise(), encounterService.getEncounterType($state.params.programUuid, sessionService.getLoginLocationUuid())]).then(function (results) { diff --git a/ui/app/clinical/consultation/controllers/diagnosisController.js b/ui/app/clinical/consultation/controllers/diagnosisController.js index 6f65cebdb4..1d3f96a39d 100644 --- a/ui/app/clinical/consultation/controllers/diagnosisController.js +++ b/ui/app/clinical/consultation/controllers/diagnosisController.js @@ -1,8 +1,8 @@ 'use strict'; angular.module('bahmni.clinical') - .controller('DiagnosisController', ['$scope', '$rootScope', 'diagnosisService', 'messagingService', 'contextChangeHandler', 'spinner', 'appService', '$translate', 'retrospectiveEntryService', '$state', - function ($scope, $rootScope, diagnosisService, messagingService, contextChangeHandler, spinner, appService, $translate, retrospectiveEntryService, $state) { + .controller('DiagnosisController', ['$scope', '$rootScope', 'diagnosisService', 'messagingService', 'contextChangeHandler', 'spinner', 'appService', '$translate', 'retrospectiveEntryService', '$state', 'drugService', 'cdssService', + function ($scope, $rootScope, diagnosisService, messagingService, contextChangeHandler, spinner, appService, $translate, retrospectiveEntryService, $state, drugService, cdssService) { var DateUtil = Bahmni.Common.Util.DateUtil; $scope.todayWithoutTime = DateUtil.getDateWithoutTime(DateUtil.today()); $scope.toggles = { @@ -23,6 +23,9 @@ angular.module('bahmni.clinical') $scope.placeholder = "Add Diagnosis"; $scope.hasAnswers = false; + $scope.cdssEnabled = false; + $scope.conceptSource = localStorage.getItem('conceptSource') || ''; + $scope.orderOptions = { 'CLINICAL_DIAGNOSIS_ORDER_PRIMARY': 'PRIMARY', 'CLINICAL_DIAGNOSIS_ORDER_SECONDARY': 'SECONDARY' @@ -66,6 +69,12 @@ angular.module('bahmni.clinical') $scope.diagnosisForm.$dirty = false; }); + (function () { + drugService.getCdssEnabled().then(function (response) { + $scope.cdssEnabled = response.data; + }); + })(); + $scope.getAddNewDiagnosisMethod = function (diagnosisAtIndex) { return function (item) { var concept = item.lookup; @@ -78,10 +87,137 @@ angular.module('bahmni.clinical') change to say array[index]=newObj instead array.splice(index,1,newObj); */ $scope.consultation.newlyAddedDiagnoses.splice(index, 1, diagnosis); + getAlerts(); } }; }; + function getAlerts () { + return cdssService.getAlerts($scope.cdssEnabled, $scope.consultation, $scope.patient); + } + + $scope.hasActiveAlerts = function (alerts) { + return alerts.some(function (alert) { + return alert.isActive; + }); + }; + + var isPastDiagnosisFlagged = function () { + var pastDiagnoses = $scope.consultation.pastDiagnoses; + var alerts = $rootScope.cdssAlerts; + var flaggedDiagnoses = []; + if (pastDiagnoses && pastDiagnoses.length > 0) { + pastDiagnoses.forEach(function (diagnosis) { + diagnosis.alerts = alerts.filter(function (cdssAlert) { + return cdssAlert.referenceCondition && cdssAlert.referenceCondition.coding.some(function (coding) { + var findMapping = diagnosis.codedAnswer.mappings.find(function (mapping) { + return mapping.code === coding.code; + }); + return findMapping !== undefined; + }); + }); + if (diagnosis.alerts) { + diagnosis.alerts = cdssService.sortInteractionsByStatus(diagnosis.alerts); + flaggedDiagnoses.push(diagnosis); + } + }); + } + $scope.pastDiagnosesAlerts = flaggedDiagnoses.length > 0; + }; + + var getFlaggedSavedDiagnosisAlert = function () { + var alerts = $rootScope.cdssAlerts; + var diagnoses = $scope.consultation.savedDiagnosesFromCurrentEncounter; + if (diagnoses && diagnoses.length > 0 && alerts) { + diagnoses.forEach(function (diagnosis) { + diagnosis.alerts = alerts.filter(function (cdssAlert) { + return cdssAlert.referenceCondition && cdssAlert.referenceCondition.coding.some(function (coding) { + var findMapping = diagnosis.codedAnswer.mappings.find(function (mapping) { + return mapping.code === coding.code; + }); + return findMapping !== undefined; + }); + }); + if (diagnosis.alerts) { + diagnosis.alerts = cdssService.sortInteractionsByStatus(diagnosis.alerts); + } + }); + } + }; + + var getAlertForCurrentDiagnosis = function () { + var alerts = $rootScope.cdssAlerts; + var diagnoses = $scope.consultation.newlyAddedDiagnoses; + var flaggedDiagnoses = []; + if (diagnoses && diagnoses.length > 0 && alerts) { + diagnoses.forEach(function (diagnosis) { + diagnosis.alerts = alerts.filter(function (cdssAlert) { + return cdssAlert.referenceCondition && cdssAlert.referenceCondition.coding.some(function (coding) { + return diagnosis.codedAnswer.uuid === coding.code; + }); + }); + if (diagnosis.alerts) { + diagnosis.alerts = cdssService.sortInteractionsByStatus(diagnosis.alerts); + flaggedDiagnoses.push(diagnosis); + } + }); + } + return flaggedDiagnoses; + }; + + var getConditionAlerts = function () { + var alerts = $rootScope.cdssAlerts; + var condition = $scope.consultation.condition; + var flaggedConditions = []; + if (condition && condition.concept && condition.concept.name === '' && alerts) { + condition.alerts = []; + } else if (condition && condition.concept && condition.concept.uuid && alerts) { + condition.alerts = alerts.filter(function (cdssAlert) { + return cdssAlert.referenceCondition && cdssAlert.referenceCondition.coding.some(function (coding) { + return condition.concept.uuid.includes(coding.code); + }); + }); + if (condition.alerts) { + condition.alerts = cdssService.sortInteractionsByStatus(condition.alerts); + flaggedConditions.push(condition); + } + } + return flaggedConditions; + }; + + var getConditionsAlerts = function () { + var alerts = $rootScope.cdssAlerts; + var conditions = $scope.consultation.conditions; + var flaggedConditions = []; + if (conditions && conditions.length > 0 && alerts) { + conditions.forEach(function (condition) { + condition.alerts = alerts.filter(function (cdssAlert) { + return cdssAlert.referenceCondition && cdssAlert.referenceCondition.coding.some(function (coding) { + return condition.concept.uuid.includes(coding.code); + }); + }); + if (condition.alerts) { + condition.alerts = cdssService.sortInteractionsByStatus(condition.alerts); + flaggedConditions.push(condition); + } + }); + } + return flaggedConditions; + }; + + var alertsWatch = $rootScope.$watch('cdssAlerts', function () { + if (!$rootScope.cdssAlerts) return; + isPastDiagnosisFlagged(); + getFlaggedSavedDiagnosisAlert(); + getAlertForCurrentDiagnosis(); + getConditionAlerts(); + getConditionsAlerts(); + }, true); + + $scope.$on('$destroy', function () { + alertsWatch(); + }); + var addPlaceHolderDiagnosis = function () { var diagnosis = new Bahmni.Common.Domain.Diagnosis(''); $scope.consultation.newlyAddedDiagnoses.push(diagnosis); @@ -140,13 +276,31 @@ angular.module('bahmni.clinical') }); var isValidConditionForm = ($scope.consultation.condition.isEmpty() || $scope.consultation.condition.isValid()); return { - allow: invalidnewlyAddedDiagnoses.length === 0 && invalidPastDiagnoses.length === 0 - && invalidSavedDiagnosesFromCurrentEncounter.length === 0 && isValidConditionForm, + allow: invalidnewlyAddedDiagnoses.length === 0 && invalidPastDiagnoses.length === 0 && + invalidSavedDiagnosesFromCurrentEncounter.length === 0 && isValidConditionForm, errorMessage: $scope.errorMessage }; }; contextChangeHandler.add(contextChange); + $scope.$watch('consultation.condition.concept.name', function () { + if ($scope.consultation.condition.concept.name === '') { + getAlerts(); + } + }); + + $scope.$watch('consultation.condition.status', getAlerts); + + $scope.$watch('consultation.conditions', getAlerts); + + $scope.$watch('consultation.newlyAddedDiagnoses', function (present, previous) { + present.forEach(function (diagnosis, index) { + if (previous[index] && previous[index].diagnosisStatus !== diagnosis.diagnosisStatus) { + getAlerts(); + } + }); + }, true); + var mapConcept = function (result) { return _.map(result.data, function (concept) { var response = { @@ -179,6 +333,7 @@ angular.module('bahmni.clinical') item.lookup.uuid = conceptSystem + item.lookup.uuid; $scope.consultation.condition.concept.uuid = item.lookup.uuid; item.value = $scope.consultation.condition.concept.name = item.lookup.name; + getAlerts(); }; }; @@ -253,6 +408,7 @@ angular.module('bahmni.clinical') condition.status = status; condition.onSetDate = DateUtil.today(); expandInactiveOnNewInactive(condition); + getAlerts(); }; var clearCondition = function () { $scope.consultation.condition = new Bahmni.Common.Domain.Condition(); @@ -302,6 +458,7 @@ angular.module('bahmni.clinical') $scope.removeObservation = function (index) { if (index >= 0) { $scope.consultation.newlyAddedDiagnoses.splice(index, 1); + getAlerts(); } }; @@ -323,12 +480,11 @@ angular.module('bahmni.clinical') spinner.forPromise( diagnosisService.deleteDiagnosis(obsUUid).then(function () { messagingService.showMessage('info', 'DELETED_MESSAGE'); - var currentUuid = $scope.consultation.savedDiagnosesFromCurrentEncounter.length > 0 ? - $scope.consultation.savedDiagnosesFromCurrentEncounter[0].encounterUuid : ""; + var currentUuid = $scope.consultation.savedDiagnosesFromCurrentEncounter.length > 0 + ? $scope.consultation.savedDiagnosesFromCurrentEncounter[0].encounterUuid : ""; + getAlerts(); return reloadDiagnosesSection(currentUuid); - })) - .then(function () { - }); + })); }; var clearBlankDiagnosis = true; var removeBlankDiagnosis = function () { @@ -372,6 +528,8 @@ angular.module('bahmni.clinical') }); if (emptyRows.length === 0) { addPlaceHolderDiagnosis(); + } else { + getAlerts(); } clearBlankDiagnosis = true; }; diff --git a/ui/app/clinical/consultation/controllers/drugOrderHistoryController.js b/ui/app/clinical/consultation/controllers/drugOrderHistoryController.js index 9a782efc9c..69ea010778 100644 --- a/ui/app/clinical/consultation/controllers/drugOrderHistoryController.js +++ b/ui/app/clinical/consultation/controllers/drugOrderHistoryController.js @@ -187,7 +187,7 @@ angular.module('bahmni.clinical') $scope.updateAllOrderAttributesByName = function (orderAttribute, drugOrderGroup) { drugOrderGroup[orderAttribute.name] = drugOrderGroup[orderAttribute.name] || {}; - drugOrderGroup[orderAttribute.name].selected = drugOrderGroup[orderAttribute.name].selected ? false : true; + drugOrderGroup[orderAttribute.name].selected = !drugOrderGroup[orderAttribute.name].selected; drugOrderGroup.drugOrders.forEach(function (drugOrder) { var selectedOrderAttribute = getAttribute(drugOrder, orderAttribute.name); @@ -230,5 +230,11 @@ angular.module('bahmni.clinical') return _.find(drugOrder.orderAttributes, {name: attributeName}); }; + $scope.hasActiveAlerts = function (alerts) { + return alerts.some(function (alert) { + return alert.isActive; + }); + }; + init(); }]); diff --git a/ui/app/clinical/consultation/controllers/treatmentController.js b/ui/app/clinical/consultation/controllers/treatmentController.js index 8094d85de1..8de4bb61f7 100644 --- a/ui/app/clinical/consultation/controllers/treatmentController.js +++ b/ui/app/clinical/consultation/controllers/treatmentController.js @@ -1,8 +1,8 @@ 'use strict'; angular.module('bahmni.clinical') - .controller('TreatmentController', ['$scope', 'clinicalAppConfigService', 'treatmentConfig', '$stateParams', - function ($scope, clinicalAppConfigService, treatmentConfig, $stateParams) { + .controller('TreatmentController', ['$scope', 'clinicalAppConfigService', 'treatmentConfig', '$stateParams', '$rootScope', 'cdssService', + function ($scope, clinicalAppConfigService, treatmentConfig, $stateParams, $rootScope, cdssService) { var init = function () { var drugOrderHistoryConfig = treatmentConfig.drugOrderHistoryConfig || {}; $scope.drugOrderHistoryView = drugOrderHistoryConfig.view || 'default'; @@ -16,7 +16,35 @@ angular.module('bahmni.clinical') $scope.newOrderSet = $scope.consultation.newlyAddedTabTreatments[$scope.tabConfigName].newOrderSet; }; + var getPreviousDrugAlerts = function () { + var treatments = $scope.treatments; + treatments && treatments.forEach(function (drugOrder) { + var drug = drugOrder.drug; + var cdssAlerts = angular.copy($rootScope.cdssAlerts); + if (!cdssAlerts) return; + drugOrder.alerts = cdssAlerts.filter(function (cdssAlert) { + return cdssAlert.referenceMedications.some(function ( + referenceMedication + ) { + return referenceMedication.coding.some(function ( + coding + ) { + return ( + drug.uuid === coding.code || + drug.name === coding.display + ); + }); + }); + }); + drugOrder.alerts = cdssService.sortInteractionsByStatus(drugOrder.alerts); + }); + }; + $scope.$watch('consultation.newlyAddedTabTreatments', initializeTreatments); + $rootScope.$watch('cdssAlerts', function () { + if (!$rootScope.cdssAlerts) return; + getPreviousDrugAlerts(); + }, true); $scope.enrollment = $stateParams.enrollment; $scope.treatmentConfig = treatmentConfig; diff --git a/ui/app/clinical/consultation/directives/cdssAlertRow.js b/ui/app/clinical/consultation/directives/cdssAlertRow.js new file mode 100644 index 0000000000..2d80a27afe --- /dev/null +++ b/ui/app/clinical/consultation/directives/cdssAlertRow.js @@ -0,0 +1,94 @@ +'use strict'; + +angular.module('bahmni.clinical') +.controller('cdssAlertRowController', ['$scope', '$rootScope', '$stateParams', 'appService', 'drugService', function ($scope, $rootScope, $stateParams, appService, drugService) { + var sortInteractionsByStatus = function (arr) { + var order = { "critical": 0, "warning": 1, "info": 2 }; + return arr.sort(function (a, b) { + return order[a.indicator] - order[b.indicator]; + }); + }; + + var getPreviousDrugAlerts = function () { + var drugOrderGroups = $scope.consultation ? $scope.consultation.drugOrderGroups : []; + if (!drugOrderGroups || (drugOrderGroups && !drugOrderGroups.length > 0)) return; + + drugOrderGroups.forEach(function (order) { + var drugOrders = order.drugOrders; + drugOrders && drugOrders.forEach(function (drugOrder) { + var drug = drugOrder.drug; + var cdssAlerts = angular.copy($rootScope.cdssAlerts); + if (cdssAlerts) { + drugOrder.alerts = cdssAlerts.filter(function (cdssAlert) { + return cdssAlert.referenceMedications.some(function (referenceMedication) { + return referenceMedication.coding.some(function (coding) { + return ( + drug.uuid === coding.code || drug.name === coding.display + ); + } + ); + } + ); + }); + + drugOrder.alerts = sortInteractionsByStatus(drugOrder.alerts); + } + }); + }); + }; + + $scope.hasActiveAlerts = function (alerts) { + return alerts.some(function (alert) { + return alert.isActive; + }); + }; + + $scope.closeAlert = function (alert) { + var alertItem = $rootScope.cdssAlerts.find(function (item) { + return item.uuid === alert.uuid; + }); + + if (alertItem) { + alertItem.isActive = false; + } + }; + + $scope.toggleDetails = function (alert) { + alert.showDetails = !alert.showDetails; + }; + + $scope.auditOptions = function () { + return appService + .getAppDescriptor() + .getConfigValue('cdssDismissalOptionsToDisplay'); + }; + + $scope.auditForm = {}; + + $scope.submitAudit = function (alert) { + var patientUuid = $stateParams.patientUuid; + var message = alert.summary.replace(/"/g, ''); + var eventType = 'Dismissed: ' + $scope.auditForm.audit; + + $scope.closeAlert(alert); + return drugService.cdssAudit(patientUuid, eventType, message, 'CDSS'); + }; + + var cdssAlertsWatcher = $rootScope.$watch('cdssAlerts', function () { + if (!$rootScope.cdssAlerts) return; + getPreviousDrugAlerts(); + }, true); + + $scope.$on('$destroy', cdssAlertsWatcher); +}]) +.directive('cdssAlertRow', function () { + return { + restrict: 'E', + controller: 'cdssAlertRowController', + templateUrl: './consultation/views/cdssAlertRow.html', + scope: { + alerts: '=', + consultation: '=' + } + }; +}); diff --git a/ui/app/clinical/consultation/directives/cdssPopover.js b/ui/app/clinical/consultation/directives/cdssPopover.js new file mode 100644 index 0000000000..9010dcaeb0 --- /dev/null +++ b/ui/app/clinical/consultation/directives/cdssPopover.js @@ -0,0 +1,13 @@ +"use strict"; + +angular.module('bahmni.clinical') + .directive('cdssPopover', [function () { + return { + restrict: 'E', + templateUrl: './consultation/views/cdssPopover.html', + scope: { + alerts: '=' + } + }; + }] +); diff --git a/ui/app/clinical/consultation/directives/newDrugOrders.js b/ui/app/clinical/consultation/directives/newDrugOrders.js index b3a32d8ac3..7ca8c54141 100644 --- a/ui/app/clinical/consultation/directives/newDrugOrders.js +++ b/ui/app/clinical/consultation/directives/newDrugOrders.js @@ -85,6 +85,12 @@ angular.module('bahmni.clinical') } $scope.bulkDurationData.bulkDuration += stepperValue; }; + + $scope.hasActiveAlerts = function (alerts) { + return alerts.some(function (alert) { + return alert.isActive; + }); + }; }; return { templateUrl: 'consultation/views/newDrugOrders.html', diff --git a/ui/app/clinical/consultation/directives/tooltip.js b/ui/app/clinical/consultation/directives/tooltip.js new file mode 100644 index 0000000000..cd61936171 --- /dev/null +++ b/ui/app/clinical/consultation/directives/tooltip.js @@ -0,0 +1,42 @@ +"use strict"; + +angular.module("bahmni.clinical").directive("tooltip", ['$timeout', function ($timeout) { + return { + restrict: "A", + link: function (scope, element, attrs) { + var tooltip = angular.element('
'); + tooltip.text(attrs.tooltip); + + // Tooltip element element positioning absolutely next to the element that the directive is applied to. + tooltip.css({ + position: "absolute", + top: element.offset().top + element.height(), + left: element.offset().left + element.width() / 2 - 70 + }); + + tooltip.hide(); + + element.on("mouseover", function () { + var isTextTruncated = element[0].scrollWidth > element[0].clientWidth; + + var isStillOnElement = true; + var timeoutId = $timeout(function () { + if (isStillOnElement && isTextTruncated) { + tooltip.show(); + } + }, 1000); + element.on("mouseout", function () { + isStillOnElement = false; + clearTimeout(timeoutId); + tooltip.hide(); + }); + }); + + angular.element(document.body).append(tooltip); + + element.on("$destroy", function () { + tooltip.remove(); + }); + } + }; +}]); diff --git a/ui/app/clinical/consultation/views/cdssAlertRow.html b/ui/app/clinical/consultation/views/cdssAlertRow.html new file mode 100644 index 0000000000..7eaec33be9 --- /dev/null +++ b/ui/app/clinical/consultation/views/cdssAlertRow.html @@ -0,0 +1,87 @@ +
+
+
+
+ +
+
+
+

+ {{alert.alertType}}: + {{alert.summary}}

+
+
+
+ + + + +
+
+
+ +
+ +
+ +
+
+
+
+
+ + +
+
+
+
diff --git a/ui/app/clinical/consultation/views/cdssPopover.html b/ui/app/clinical/consultation/views/cdssPopover.html new file mode 100644 index 0000000000..f94963edcb --- /dev/null +++ b/ui/app/clinical/consultation/views/cdssPopover.html @@ -0,0 +1,16 @@ +
+
+ {{ "DISMISSED_KEY" | translate}} {{alert.indicator}} +
+

+ {{alert.alertType}}: + {{alert.summary}} +

+
+ + +
+
diff --git a/ui/app/clinical/consultation/views/conditionRow.html b/ui/app/clinical/consultation/views/conditionRow.html index 165fcccc29..55516741fd 100644 --- a/ui/app/clinical/consultation/views/conditionRow.html +++ b/ui/app/clinical/consultation/views/conditionRow.html @@ -1,5 +1,11 @@
-
+
+ + +
{{ condition.displayString() }}
{{'CONDITION_LIST_ACTIVE_SINCE'|translate}} {{condition.activeSince | bahmniDate}} @@ -7,6 +13,9 @@
{{'CONDITION_LIST_CREATED_BY'|translate}} {{condition.creator}}
+
+ +
{{'CONDITION_LIST_NOTES'|translate}}
@@ -27,3 +36,7 @@
+ +
+ +
diff --git a/ui/app/clinical/consultation/views/conditions.html b/ui/app/clinical/consultation/views/conditions.html index 408c4be45a..7bc6c9a458 100644 --- a/ui/app/clinical/consultation/views/conditions.html +++ b/ui/app/clinical/consultation/views/conditions.html @@ -6,9 +6,15 @@
-
+
{{'CONDITION_LIST_CONDITION'|translate}}
+ + + {{'CLINICAL_ACCEPT'|translate}}
+
+ +
{{'CONDITION_LIST_STATUS'|translate}}
@@ -60,7 +69,9 @@
- +
+ +
diff --git a/ui/app/clinical/consultation/views/diagnosis.html b/ui/app/clinical/consultation/views/diagnosis.html index ab4b03403d..9abc02e806 100644 --- a/ui/app/clinical/consultation/views/diagnosis.html +++ b/ui/app/clinical/consultation/views/diagnosis.html @@ -6,11 +6,17 @@
-
+
{{ ::'CLINICAL_DIAGNOSIS' | translate }}
+ + +
- +
+ +
@@ -83,6 +91,9 @@
+
+ +
@@ -116,6 +127,11 @@

{{ ::'CLINICAL_CURRENT' | translate }}

{{ ::'CLINICAL_INITIAL' | translate }}
{{ ::'CLINICAL_CURRENT' | translate }}
+
+
+ +
+
diff --git a/ui/app/clinical/consultation/views/diagnosisAlertRow.html b/ui/app/clinical/consultation/views/diagnosisAlertRow.html new file mode 100644 index 0000000000..1e457fcbdd --- /dev/null +++ b/ui/app/clinical/consultation/views/diagnosisAlertRow.html @@ -0,0 +1,16 @@ +
+
+ +
+

+ {{alert.summary}} +

+
+ + +
+
+
+
\ No newline at end of file diff --git a/ui/app/clinical/consultation/views/diagnosisRow.html b/ui/app/clinical/consultation/views/diagnosisRow.html index cd76863210..78091efcf8 100644 --- a/ui/app/clinical/consultation/views/diagnosisRow.html +++ b/ui/app/clinical/consultation/views/diagnosisRow.html @@ -1,9 +1,18 @@
-
+
+ + + {{diagnosis.getDisplayName()}} {{diagnosis.freeTextAnswer}} +
+ +
@@ -100,4 +109,7 @@ dirty-check-flag='diagnosis.isDirty' />
+
+
+
\ No newline at end of file diff --git a/ui/app/clinical/consultation/views/newDrugOrders.html b/ui/app/clinical/consultation/views/newDrugOrders.html index f0dd2fbf01..29c6d487ad 100644 --- a/ui/app/clinical/consultation/views/newDrugOrders.html +++ b/ui/app/clinical/consultation/views/newDrugOrders.html @@ -29,8 +29,16 @@

{{ ::'MEDICATION_NEW_PRESCRIPTION' | translate}} -
- {{newTreatment.getDisplayName()}} +
+ + + {{newTreatment.getDisplayName()}} + +
+ +
{{newTreatment.getDoseAndUnits()}} | {{newTreatment.getFrequency()}} | @@ -62,6 +70,7 @@

{{ ::'MEDICATION_NEW_PRESCRIPTION' | translate}}
{{newTreatment.getQuantityWithUnit()}}

+ diff --git a/ui/app/clinical/consultation/views/orderSelector.html b/ui/app/clinical/consultation/views/orderSelector.html index dfaf5959ed..30f41ad177 100644 --- a/ui/app/clinical/consultation/views/orderSelector.html +++ b/ui/app/clinical/consultation/views/orderSelector.html @@ -7,7 +7,9 @@ + ng-disabled="isTestIndirectlyPresent(test)" + tooltip="{{getName(test)}}" + > {{getName(test)}} diff --git a/ui/app/clinical/consultation/views/treatmentSections/addTreatment.html b/ui/app/clinical/consultation/views/treatmentSections/addTreatment.html index a70aa97636..a8f5acad50 100644 --- a/ui/app/clinical/consultation/views/treatmentSections/addTreatment.html +++ b/ui/app/clinical/consultation/views/treatmentSections/addTreatment.html @@ -259,54 +259,10 @@

0" class="cdss-alerts"> - {{"CDSS_" + cdssaAlerts[0].indicator.toUpperCase() | translate}} -

- -
- - -
-
- {{cdssAlert.summary}} - -
-
- {{cdssAlert.detail}} - - - -
-
- -
- -
- -
-
- -
- -
-
-
diff --git a/ui/app/clinical/consultation/views/treatmentSections/drugOrderHistory.html b/ui/app/clinical/consultation/views/treatmentSections/drugOrderHistory.html index 7d24bb47e1..4f93b07836 100755 --- a/ui/app/clinical/consultation/views/treatmentSections/drugOrderHistory.html +++ b/ui/app/clinical/consultation/views/treatmentSections/drugOrderHistory.html @@ -19,10 +19,20 @@

{{ ::'MEDICATION_NO_RECENT_TREATMENT' | translate }}

    -
  • +
  • -
    - {{drugOrder.getDisplayName()}} +
    + + + {{drugOrder.getDisplayName()}} + +
    + +
    +
    {{drugOrder.getDoseAndUnits()}} | {{drugOrder.getFrequency()}} | @@ -99,6 +109,7 @@
    +
diff --git a/ui/app/clinical/index.html b/ui/app/clinical/index.html index fd2508a8e7..9883d7273b 100644 --- a/ui/app/clinical/index.html +++ b/ui/app/clinical/index.html @@ -405,9 +405,12 @@

+ + + @@ -529,6 +532,7 @@

+ @@ -554,5 +558,8 @@

+ + + diff --git a/ui/app/common/constants.js b/ui/app/common/constants.js index e3ab952161..e7c117c69d 100644 --- a/ui/app/common/constants.js +++ b/ui/app/common/constants.js @@ -83,6 +83,10 @@ Bahmni.Common = Bahmni.Common || {}; emrEncounterUrl: EMRAPI + "/encounter", encounterUrl: RESTWS_V1 + "/encounter", cdssUrl: RESTWS_V1 + "/cdss", + fhirExportPrivilege: "Export Patient Data", + plainFhirExportPrivilege: "Export Non Anonymised Patient Data", + fhirExportUrl: RESTWS_V1 + "/fhirexport", + fhirTasks: FHIR_BASE_URL + "/Task", fhirMedicationsUrl: FHIR_BASE_URL + "/Medication", locationUrl: RESTWS_V1 + "/location", bahmniVisitLocationUrl: BAHMNI_CORE + "/visitLocation", diff --git a/ui/app/common/displaycontrols/drugOrdersSection/directives/drugOrdersSection.js b/ui/app/common/displaycontrols/drugOrdersSection/directives/drugOrdersSection.js index 35dc79e173..e8ec34264d 100644 --- a/ui/app/common/displaycontrols/drugOrdersSection/directives/drugOrdersSection.js +++ b/ui/app/common/displaycontrols/drugOrdersSection/directives/drugOrdersSection.js @@ -164,6 +164,59 @@ angular.module('bahmni.common.displaycontrol.drugOrdersSection') if (promise) { spinner.forPromise(promise); } + + var sortInteractionsByStatus = function (arr) { + var order = { "critical": 0, "warning": 1, "info": 2 }; + return arr.sort(function (a, b) { + return order[a.indicator] - order[b.indicator]; + }); + }; + + $scope.hasActiveAlerts = function (alerts) { + return alerts.some(function (alert) { + return alert.isActive; + }); + }; + + var alertsPresent = function (orders) { + return orders.some(function (drugOrder) { + return drugOrder.alerts && drugOrder.alerts.length > 0 && !$scope.hasActiveAlerts(drugOrder.alerts); + }); + }; + + var filterDrugAlerts = function () { + var drugOrders = $scope.drugOrders; + if (!drugOrders || (drugOrders && !drugOrders.length > 0)) return; + + drugOrders.forEach(function (drugOrder) { + var drug = drugOrder.drug; + var cdssAlerts = angular.copy($rootScope.cdssAlerts); + if (cdssAlerts) { + drugOrder.alerts = cdssAlerts.filter(function (cdssAlert) { + return cdssAlert.referenceMedications.some(function (referenceMedication) { + return referenceMedication.coding.some(function (coding) { + return ( + drug.uuid === coding.code || drug.name === coding.display + ); + } + ); + } + ); + }); + + drugOrder.alerts = sortInteractionsByStatus(drugOrder.alerts); + } + }); + + $scope.alertsPresent = alertsPresent(drugOrders); + }; + + var watchAlerts = $rootScope.$watch('cdssAlerts', function () { + if (!$rootScope.cdssAlerts) return; + filterDrugAlerts(); + }, true); + + $scope.$on('$destroy', function () { watchAlerts(); }); }; return { restrict: 'E', diff --git a/ui/app/common/displaycontrols/drugOrdersSection/views/drugOrdersSection.html b/ui/app/common/displaycontrols/drugOrdersSection/views/drugOrdersSection.html index bd312561a8..581baadcfd 100644 --- a/ui/app/common/displaycontrols/drugOrdersSection/views/drugOrdersSection.html +++ b/ui/app/common/displaycontrols/drugOrdersSection/views/drugOrdersSection.html @@ -12,6 +12,7 @@

+ {{columnHeaders[columnName] | translate}} {{ 'MEDICATIONS_ACTIONS_LABEL'|translate }} @@ -19,31 +20,45 @@

- + + +
+ +
+ + {{::drugOrder.getDoseAndUnits()}} {{::drugOrder.getDrugOrderName(false)}} - - - {{::drugOrder.drugNameDisplay}} + + + + + {{::drugOrder.drugNameDisplay}} + {{::drugOrder.route}} {{::drugOrder.getDurationAndDurationUnits()}} - - {{::drugOrder.getFrequency()}} - - , {{::'MEDICATION_AS_NEEDED' | translate}} + + {{::drugOrder.getFrequency()}} + + , {{::'MEDICATION_AS_NEEDED' | translate}} + - {{::drugOrder.effectiveStartDate | bahmniDate}} {{::drugOrder.effectiveStopDate | bahmniDate}} - - {{::drugOrder.orderReasonConcept.display || drugOrder.orderReasonConcept.name}} - - {{::drugOrder.orderReasonText}} - + + {{::drugOrder.orderReasonConcept.display || drugOrder.orderReasonConcept.name}} + - {{::drugOrder.orderReasonText}} + {{::drugOrder.instructions}} {{::drugOrder.getQuantityWithUnit()}} + 0" class="medicationDrugNotes order-alerts"> + + + +
diff --git a/ui/app/common/ui-helper/directives/popOver.js b/ui/app/common/ui-helper/directives/popOver.js index 104c2dd3ed..5e26a4697c 100644 --- a/ui/app/common/ui-helper/directives/popOver.js +++ b/ui/app/common/ui-helper/directives/popOver.js @@ -31,6 +31,7 @@ angular.module('bahmni.common.uiHelper') hideTargetElements(0); $(document).off('click', docClickHandler); } else { + $('.tooltip').hide(); $scope.isTargetOpen = true; showTargetElements(); $(document).on('click', docClickHandler); diff --git a/ui/app/common/util/httpErrorInterceptor.js b/ui/app/common/util/httpErrorInterceptor.js index e0511644e3..05a7d689d5 100644 --- a/ui/app/common/util/httpErrorInterceptor.js +++ b/ui/app/common/util/httpErrorInterceptor.js @@ -48,7 +48,7 @@ angular.module('httpErrorInterceptor', []) } else if (response.status === 405) { showError(unexpectedError); } else if (response.status === 400) { - var errorMessage = data.error && data.error.message ? data.error.message : (data.localizedMessage || "Could not connect to the server. Please check your connection and try again"); + var errorMessage = data.error && data.error.message ? data.error.message : data.error ? data.error : (data.localizedMessage || "Could not connect to the server. Please check your connection and try again"); showError(errorMessage); } else if (response.status === 403) { var errorMessage = data.error && data.error.message ? data.error.message : unexpectedError; @@ -58,8 +58,8 @@ angular.module('httpErrorInterceptor', []) showError(errorMessage); } } else if (response.status === 404) { - if (!_.includes(response.config.url, "implementation_config") && !_.includes(response.config.url, "locale_") - && !_.includes(response.config.url, "offlineMetadata")) { + if (!_.includes(response.config.url, "implementation_config") && !_.includes(response.config.url, "locale_") && + !_.includes(response.config.url, "offlineMetadata")) { showError("The requested information does not exist"); } } diff --git a/ui/app/i18n/admin/locale_en.json b/ui/app/i18n/admin/locale_en.json index 82e3f0ade6..b4e300069c 100644 --- a/ui/app/i18n/admin/locale_en.json +++ b/ui/app/i18n/admin/locale_en.json @@ -17,5 +17,22 @@ "FILTER_BUTTON_LABEL": "Filter", "AUDIT_LOG_TABLE_HEADER_LABEL": "Audit Log Events", "NO_NAVIGATION_LINKS_AVAILABLE_MESSAGE": "No navigation links available.", - "SELECT_VALUE_FROM_AUTOCOMPLETE_DEFAULT_MESSAGE": "Please select a value from auto complete" -} + "SELECT_VALUE_FROM_AUTOCOMPLETE_DEFAULT_MESSAGE": "Please select a value from auto complete", + "PATIENT_DATA_BULK_FHIR_EXPORT_LABEL": "Patient Data Bulk FHIR Export", + "FHIR_EXPORT_START_DATE_LABEL":"Start Date", + "FHIR_EXPORT_END_DATE_LABEL": "End Date", + "ANONYMISE_FHIR_EXPORT_LABEL": "Anonymise Export", + "FHIR_EXPORT_BUTTON_LABEL": "Export", + "FHIR_EXPORT_TABLE_HEADER_LABEL": "Export Details", + "EXPORT_USER_NAME": "User Name", + "EXPORT_TRIGGERED_DATE": "Export Triggered Date", + "EXPORT_START_DATE": "Start Date", + "EXPORT_END_DATE": "End Date", + "EXPORT_ANONYMISED": "Anonymised?", + "EXPORT_TASK_STATUS": "Status", + "EXPORT_DOWNLOAD_LINK": "Download link", + "INSUFFICIENT_PRIVILEGE_TO_EXPORT": "You do not have sufficient privilege to export data", + "EXPORT_PATIENT_DATA_WARNING" : "Note: Patient Data is confidential and should be handled carefully! It is recommended to export data in Anonymise mode, and ensure that the exported file is handled carefully by authorised parties only.", + "EXPORT_PATIENT_REQUEST_SUBMITTED": "Request submitted. Click 'Refresh' to check status of export.", + "EXPORT_PATIENT_REQUEST_SUBMIT_ERROR": "FHIR export request failed. Please contact administrator." +} \ No newline at end of file diff --git a/ui/app/i18n/clinical/locale_en.json b/ui/app/i18n/clinical/locale_en.json index 23caaa259a..99fb1f042b 100644 --- a/ui/app/i18n/clinical/locale_en.json +++ b/ui/app/i18n/clinical/locale_en.json @@ -388,5 +388,8 @@ "CDSS_CRITICAL" :"Critical", "CDSS_ALERT_DISMISSAL_REASON" :"Select reason for dismissal", "CDSS_INFO" :"Info", - "CDSS_ALERT_DISMISS": "Dismiss" + "CDSS_ALERT_DISMISS": "Dismiss", + "DISMISSED_KEY": "Dismissed", + "CDSS_ALERT_SAVE_ERROR": "A CDSS Critical Alert needs to be addressed in Medications Tab before continuing with Save operation", + "CDSS_ACTION": "Please go to Medications Tab to take further action" } diff --git a/ui/app/styles/admin/_auditLog.scss b/ui/app/styles/admin/_auditLog.scss index 83bab1acac..6607bf39de 100644 --- a/ui/app/styles/admin/_auditLog.scss +++ b/ui/app/styles/admin/_auditLog.scss @@ -58,6 +58,23 @@ padding: 0; } } + form { + display: flex; + align-items: center; + button[type="submit"] { + margin-left: auto; + } + > div { + display: flex; + align-items: center; + label { + margin-top: 4px !important; + } + input { + margin-top: 8px; + } + } + } } } diff --git a/ui/app/styles/bahmni-components/_tooltip.scss b/ui/app/styles/bahmni-components/_tooltip.scss index 7e121b6642..620ac43ed7 100644 --- a/ui/app/styles/bahmni-components/_tooltip.scss +++ b/ui/app/styles/bahmni-components/_tooltip.scss @@ -1,25 +1,137 @@ .ipd-test-results { - max-width: 70px; - overflow: hidden; - white-space: nowrap; - display: inline-block; - text-overflow: ellipsis; + max-width: 70px; + overflow: hidden; + white-space: nowrap; + display: inline-block; + text-overflow: ellipsis; cursor: pointer; } .tooltip { - position: absolute; - padding: 5px; - margin-left: 70px; - margin-top: -28px; - background: $clinicalInfoMessageBg; - border-radius: 5px; - box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.2); - i { - color: $clinicalInfoMessageBg; - font-size: 20px; - position: absolute; - left: -10px; - top: 0px; - } -} \ No newline at end of file + position: absolute; + padding: 5px; + margin-left: 70px; + margin-top: -28px; + background: $clinicalInfoMessageBg; + border-radius: 5px; + box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.2); + i { + color: $clinicalInfoMessageBg; + font-size: 20px; + position: absolute; + left: -10px; + top: 0px; + } +} + +.cdss-tooltip { + background-color: #393939; + border-radius: 2px; + color: #fff; + font-size: 12px; + padding: 10px; + position: absolute; + z-index: 1; + top: calc(100% + 10px); + left: -70px; + max-height: 500px; + overflow-y: auto; + &:before { + content: ''; + position: absolute; + bottom: 100%; + left: 10px; + margin-left: -5px; + width: 0; + height: 0; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-bottom: 5px solid #393939; + } + > div:not(:nth-child(1)) { + margin-bottom: 10px; + margin-top: 10px; + } + > div:not(:last-child){ + border-bottom: 1px solid #ccc; + } + + .cdss-tooltip__status { + display: flex; + align-items: center; + justify-content: center; + border-radius: 30px; + padding: 0px 5px; + margin-bottom: 10px; + width: fit-content; + color: black; + } + a { + margin-left: 5px; + } + .status-critical { + background-color: #ffd6d8; + border: 1px solid #ff0000; + } + + .status-warning { + background-color: #feead6; + border: 1px solid #ffa500; + } + + .status-info { + background-color: #d5f5ff; + border: 1px solid #00a3e0; + } + + + .drug-link { + position: relative !important; + } +} + +.alert-detail { + display: flex !important; + align-items: baseline !important; + text-align: left !important; + > span { + display: flex !important; + flex-direction: column !important; + justify-content: flex-end !important; + } + ul { + list-style: initial !important; + padding-left: 20px; + + i { + position: relative !important; + } + + li { + + border-bottom: none !important; + &::after, &::before { + content: none !important; + } + } + + ul { + list-style: circle !important; + } + + } + .cdss_alert_source { + margin-top: auto !important; + } +} + +.tooltip-list { + text-align: left !important; + i { + position: relative !important; + } + &:not(:first-child) { + padding-bottom: 10px; + padding-top: 10px; + } +} diff --git a/ui/app/styles/clinical/_diagnosis.scss b/ui/app/styles/clinical/_diagnosis.scss index 5db60bc6aa..1808d2ed29 100644 --- a/ui/app/styles/clinical/_diagnosis.scss +++ b/ui/app/styles/clinical/_diagnosis.scss @@ -1,6 +1,6 @@ .diagnosis { .table { - overflow: hidden; + // overflow: hidden; .row:not(.diagnosis-row) { border-bottom: 1px solid $lightGray; @@ -84,6 +84,7 @@ .col1 { width: 100%; + position: relative; input[type="text"] { min-width: 160px; @@ -262,6 +263,19 @@ padding: 8px 7px; font-size: 11px; } + .header-alert-icon { + height: auto !important; + padding: 0px; + } + .button-alert { + padding: 8px 7px !important; + height: auto !important; + } + + .button-alert-condition { + padding: 8px 7px !important; + height: auto !important; + } .btn-group, .small-btn { @@ -343,8 +357,73 @@ } } } - } + .alert-item { + float: left; + .alert-button { + padding: 8px 7px !important; + height: 100% !important; + .icon { + color: #8e8e8e; + } + } + } + + .new-diagnosis-alert-row { + width: 100%; + } + .new-diagnosis-alert { + padding: 10px !important; + + a { + text-decoration: none; + color: black !important; + } + .cdss-alert-close { + background: none; + border: none; + position: absolute; + right: 0; + top: 0; + font-size: 12px; + } + } + + .active-diagnosis-alert { + display: grid; + grid-template-columns: 2rem auto; + } + + .bg-critical { + background-color: #ffd6d8; + border: 1px solid #ff0000; + position: relative; + } + + .bg-warning { + background-color: #feead6; + border: 1px solid #ffa500; + position: relative; + } + + .bg-info { + background-color: #d5f5ff; + border: 1px solid #00a3e0; + position: relative; + } + + i.critical { + color: #ff0000; + } + + i.warning { + color: #ffa500; + } + + i.info { + color: #00a3e0; + } + .view-past, .edit-diagnosis { @@ -545,6 +624,12 @@ } } + .condition-alert-name { + display: flex; + align-items: center !important; + justify-content: flex-start !important; + } + .active-conditions, .history-conditions, .inactive-conditions { @@ -684,11 +769,9 @@ .current-diagnosis, .history-diagnosis { .diagnosis-row { - overflow: auto; clear: both; .view-past { - overflow: auto; clear: both; .col { @@ -705,7 +788,10 @@ text-decoration: line-through; } } - + + .history-alert { + left: -50px !important; + } .col2, .col3 { padding-left: 10px; diff --git a/ui/app/styles/clinical/treatment/_customDrugOrderHistory.scss b/ui/app/styles/clinical/treatment/_customDrugOrderHistory.scss index bb1dfc530f..c99ec0f3ca 100644 --- a/ui/app/styles/clinical/treatment/_customDrugOrderHistory.scss +++ b/ui/app/styles/clinical/treatment/_customDrugOrderHistory.scss @@ -87,42 +87,42 @@ If each of a table rows have multiple possible actions associated with them, use */ -.drugOrderHistorySectionContainer{ - .drugOrderTable{ - width:100%; +.drugOrderHistorySectionContainer { + .drugOrderTable { + width: 100%; position: relative !important; - clear:both; - overflow: hidden; + clear: both; + // overflow: hidden; } - .drugOrderTableScroll{ + .drugOrderTableScroll { width: auto; overflow-x: auto; overflow-y: hidden; - margin-left:240px; - &.noOfActions-4-width{ + margin-left: 240px; + &.noOfActions-4-width { margin-right: 200px; } - &.noOfActions-3-width{ + &.noOfActions-3-width { margin-right: 165px; } - &.noOfActions-2-width{ + &.noOfActions-2-width { margin-right: 130px; } - &.noOfActions-1-width{ + &.noOfActions-1-width { margin-right: 90px; } - &.noOfActions-0-width{ + &.noOfActions-0-width { margin-right: 75px; } - &.noOfActions--width{ + &.noOfActions--width { margin-right: 65px; } - @media (max-width : 1024px){ + @media (max-width: 1024px) { margin-left: 170px; } } - table.treatments-details-table{ - u{ + table.treatments-details-table { + u { border: none !important; font-family: openSans !important; } @@ -140,23 +140,23 @@ If each of a table rows have multiple possible actions associated with them, use white-space: nowrap; } //table-layout: fixed; - tr{ - &.edited{ + tr { + &.edited { border-bottom: none; } - &.medicationDrugNotes{ + &.medicationDrugNotes { border-top: 2px solid $clinicalNotesColor; background: $clinicalNotesColor; - td{ + td { text-align: left; - padding-left:10px; - .additionalInstructions{ - padding-right:10px; + padding-left: 10px; + .additionalInstructions { + padding-right: 10px; } } } - &.formStopReason{ - td{ + &.formStopReason { + td { padding: 4px 5px 7px; } .form-field { @@ -164,209 +164,251 @@ If each of a table rows have multiple possible actions associated with them, use border-bottom: none; padding-bottom: 0px; } - .form-field .field-attribute{ - font-size: 12px; width: auto; + .form-field .field-attribute { + font-size: 12px; + width: auto; } - .form-field .field-attribute label{ + .form-field .field-attribute label { margin-top: 3px !important; @media (max-width: 1024px) { margin-top: 7px !important; } } .form-field .field-value { - input[type="date"], select, textarea{ + input[type="date"], + select, + textarea { margin-top: 0 !important; } - input[type="date"]{ + input[type="date"] { width: 120px; padding: 2px 10px !important; } - select{ + select { width: 140px; min-width: 140px; color: #363463; padding: 2px 10px; margin: 5px 0 0; - background-color: #FFF; - border: 1px solid #DDD; + background-color: #fff; + border: 1px solid #ddd; } - textarea{ + textarea { width: 131px !important; padding: 2px 10px; - border: 1px solid #DDD; + border: 1px solid #ddd; } @media (max-width: 1024px) { - textarea{ + textarea { width: 400px !important; height: 32px !important; } - select{ + select { width: 197px !important; } } } } - } - th{ + th { background: $lightestGray; background-image: none; color: $darkGray; - width:113px; + width: 113px; font-family: openSansSemiBold; - &.active-drugs{ - width:25%; + &.active-drugs { + width: 25%; } - &.drugName{ + &.drugName { width: 240px; text-align: left; position: absolute; - top:auto; - left:0; + top: auto; + left: 0; box-sizing: border-box; border-right: none; - height:100%; + height: 100%; padding-left: 10px; - @media (max-width : 1024px){ + @media (max-width: 1024px) { width: 170px; } - span{ + span { z-index: 999; position: static; width: 100%; } } - &.stopReason{ - width:160px; + &.stopReason { + width: 160px; } - &.instructions{ - width:168px; + &.instructions { + width: 168px; } - &.empty-header{ + &.empty-header { position: absolute; right: 0; top: auto; box-sizing: border-box; - height:100%; - &.noOfActions-4-width{ + height: 100%; + &.noOfActions-4-width { width: 200px; } - &.noOfActions-3-width{ + &.noOfActions-3-width { width: 165px; } - &.noOfActions-2-width{ + &.noOfActions-2-width { width: 130px; } - &.noOfActions-1-width{ + &.noOfActions-1-width { width: 90px; } - &.noOfActions-0-width{ + &.noOfActions-0-width { width: 75px; } - &.noOfActions--width{ + &.noOfActions--width { width: 65px; } } } - td{ + td { text-align: center; - padding: 6px 5px 7px; + padding: 10px; vertical-align: middle; - &.drugName{ + &.drugName { text-align: left; padding-left: 10px; - width: 240px; + // width: 240px; text-align: left; position: absolute; - top:auto; - left:0; + top: auto; + left: 0; box-sizing: border-box; - background: #EFEFF2; + background: #efeff2; border-bottom: 1px solid $lighterGray; white-space: nowrap; - overflow: hidden; + width: 240px; + // overflow: hidden; text-overflow: ellipsis; - @media (max-width : 1024px){ + @media (max-width: 1024px) { width: 170px; } - span{ - z-index: 999; + > span { + // z-index: 999; position: static; + display: flex; + align-items: center; + width: 100%; + } + span, > div { + overflow: hidden; + text-overflow: ellipsis; + } + + .medication-name { + display: flex; + align-items: center; width: 100%; + button { + min-width: auto !important; + } } - .orderSetStagingAreaCheckbox{ + .orderSetStagingAreaCheckbox { margin: 1px 7px 0 0; - transform:scale(1.2); + transform: scale(1.2); } } - &.drugNameStopForm{ + &.drugNameStopForm { padding: 6px 10px 43px; } - &.drugNameAddNote{ + &.drugNameAddNote { padding: 6px 10px 31px; } - &.drugNameStopFormAddNote{ + &.drugNameStopFormAddNote { padding: 6px 10px 85px; } - &.buttonColumn{ + &.buttonColumn { position: absolute; - right:0; - top:auto; + right: 0; + top: auto; padding: 2px 5px; border-bottom: 1px solid $lighterGray; - background: #EFEFF2; + background: #efeff2; box-sizing: border-box; } - &.buttonColumnStopForm{ + &.buttonColumnStopForm { padding: 2px 5px 38px; } - &.buttonColumnAddNote{ + &.buttonColumnAddNote { padding: 2px 5px 26px; } - &.buttonColumnStopFormAddNote{ + &.buttonColumnStopFormAddNote { padding: 2px 5px 82px; } - &.noActionColumn{ + &.noActionColumn { padding: 3px 5px; } - &.noOfActions-4-width{ + &.noOfActions-4-width { width: 200px; } - &.noOfActions-3-width{ + &.noOfActions-3-width { width: 165px; } - &.noOfActions-2-width{ + &.noOfActions-2-width { width: 130px; } - &.noOfActions-1-width{ + &.noOfActions-1-width { width: 90px; } - &.noOfActions-0-width{ + &.noOfActions-0-width { width: 75px; } - &.noOfActions--width{ + &.noOfActions--width { width: 65px; } - &.noActionColumnAddNote{ + &.noActionColumnAddNote { padding: 3px 5px 27px; } - &.drug-stop-container{ + &.drug-stop-container { padding: 0; } - .button-group-wrapper{ + .button-group-wrapper { width: auto; - padding:0 !important; + padding: 0 !important; position: static; - button,button:hover{ - i{ + overflow: hidden; + float: left !important; + button, + button:hover { + i { font-size: 1.8em; - padding:0 !important; + padding: 0 !important; } } - .button-group{ + .button-group { float: left; } } } } -} \ No newline at end of file + + .order-set-name { + .cdss-alert-icon { + margin-right: 5px !important; + } + } + + .order-alerts { + td { + padding-left: 0px !important; + overflow: hidden !important; + white-space: normal !important; + p, a { + font-size: 12px !important; + } + .active-drug-alert { + padding: 5px 3px !important; + width: auto !important; + } + } + } +} diff --git a/ui/app/styles/clinical/treatment/_treatment.scss b/ui/app/styles/clinical/treatment/_treatment.scss index b8fe95d527..612dab409c 100644 --- a/ui/app/styles/clinical/treatment/_treatment.scss +++ b/ui/app/styles/clinical/treatment/_treatment.scss @@ -39,6 +39,7 @@ flex-direction: column; flex-wrap: wrap; align-items: flex-start; + position: relative; @media screen and (min-width: 540px) and (max-width: 768px) { flex-direction: row; @@ -74,6 +75,7 @@ padding-left: 10px; line-height: 1.1; flex-wrap: wrap; + padding-bottom: 10px; .footer-note { flex-grow: 1; @@ -117,7 +119,7 @@ font-size: 12px; li { - padding-bottom: 10px; + padding-bottom: 0px; } } @@ -680,6 +682,144 @@ clear: both; } + .cdss-icon-medication { + color: #8e8e8e; + padding: 0px !important; + margin: 0px 2px; + } + + .active-drug-alert { + width: calc(100% - 22px); + padding: 10px; + font-size: 14px; + cursor: pointer; + display: grid; + grid-template-columns: 2rem auto 4rem; + > div { + display: flex; + &:first-child { + align-items: flex-start !important; + } + &:not(:last-child) { + flex-direction: column; + justify-content: flex-start !important; + margin-top: 5px !important; + } + + } + a { + text-decoration: none; + color: #000; + } + .cdss-alert-close { + background: none; + border: none; + position: absolute; + right: 0; + top: 0; + font-size: 12px; + z-index: 1; + } + .summary { + position: relative; + .expand-details { + position: absolute; + right: 0; + top: calc(100% - 5px); + font-size: 12px; + font-weight: bold; + line-height: 1; + color: #000; + text-shadow: 0 1px 0 #fff; + background: transparent; + width: 1.5rem; + height: 1.5rem; + text-align: center; + border-radius: 50%; + border: none; + padding: 0; + cursor: pointer; + transition: opacity .15s ease-in-out; + } + } + .cdss-alert-details { + margin-top: 10px; + .operations, .field-value { + margin-top: 10px; + } + + .form-field { + padding: 0px !important; + } + } + + + + .alert-controls { + justify-content: space-between; + align-items: flex-start; + button, i { + padding: 5px !important; + position: relative !important; + font-size: 16px; + font-weight: 600; + } + } + } + + .cdss-alert-audit-form { + display: flex; + align-items: center; + > div { + // width: calc(100% - 20px); + select { + width: 100%; + } + &:first-child { + margin-right: 10px; + } + + } + } + + .bg-critical { + background-color: #ffd6d8; + border: 1px solid #ff0000; + position: relative; + } + + .bg-warning { + background-color: #feead6; + border: 1px solid #ffa500; + position: relative; + } + + .bg-info { + background-color: #d5f5ff; + border: 1px solid #00a3e0; + position: relative; + } + + .bg-inactive { + background-color: #f5f5f5; + border: 1px solid #ccc; + position: relative; + } + + i.critical { + color: #ff0000; + } + + i.warning { + color: #ffa500; + } + + i.info { + color: #00a3e0; + } + i.inactive { + color: #8e8e8e !important; + } } @import "customDrugOrderHistory"; diff --git a/ui/app/styles/clinical/treatment/_treatmentOrderSet.scss b/ui/app/styles/clinical/treatment/_treatmentOrderSet.scss index fbf5ae8995..3cdbc57055 100644 --- a/ui/app/styles/clinical/treatment/_treatmentOrderSet.scss +++ b/ui/app/styles/clinical/treatment/_treatmentOrderSet.scss @@ -32,6 +32,7 @@ table.orderSet-table{ th.drugName ,td.drugName{ width: 300px; text-align: left; + overflow: auto !important; span{ display: block; width:240px; @@ -42,8 +43,29 @@ table.orderSet-table{ } td{ padding: 4px 7px !important; - .button-group-wrapper{ - width:auto; - } } } + +.drug-order-table { + .button-group-wrapper{ + width: 100% !important; + } +} + +.header-alerts { + left: 0 !important; +} + +.order-alert-row { + position: absolute !important; + left: 0 !important; + padding: 7px !important; +} + +.drugName-alert { + left: 2rem !important; + width: 208px !important; + .medication-name { + padding-left: 2px !important; + } +} \ No newline at end of file diff --git a/ui/app/styles/common/_bahmniGlobal.scss b/ui/app/styles/common/_bahmniGlobal.scss index ae0819997a..3db5ce504d 100644 --- a/ui/app/styles/common/_bahmniGlobal.scss +++ b/ui/app/styles/common/_bahmniGlobal.scss @@ -1,15 +1,15 @@ $total-width: 100%; -p.caution{ - color: red; - padding: 5px; +p.caution { + color: red; + padding: 5px; } .clearfix { @include clearfix(); } .asterick { - color:red; + color: red; display: inline-block; padding-left: 5px; } @@ -19,10 +19,10 @@ p.caution{ } input[file-upload] { - position:absolute; - z-index: -999; - min-width: 1px; - width: 1px; + position: absolute; + z-index: -999; + min-width: 1px; + width: 1px; } .form-horizontal { @@ -35,7 +35,7 @@ input[file-upload] { width: 170px; } @media screen and (max-width: 850px) { - width:156px; + width: 156px; } } &.hasLegend { @@ -43,18 +43,18 @@ input[file-upload] { margin-left: 10px; } .offset-label { - padding-left:0px; + padding-left: 0px; } } } } .is-abnormal { - color: red!important; + color: red !important; } -.is-abnormal-bold{ - font-family:$OpenSansBoldFont !important; - color: red!important; +.is-abnormal-bold { + font-family: $OpenSansBoldFont !important; + color: red !important; } .visible-print { @@ -77,7 +77,6 @@ input[file-upload] { } } - .row-remover { border: 1px solid $lightGray; border-radius: 5px; @@ -92,7 +91,10 @@ input[file-upload] { } } -.opd-wrapper .has-toggle {cursor: pointer;margin-bottom:0;} +.opd-wrapper .has-toggle { + cursor: pointer; + margin-bottom: 0; +} .section-toggle { background: none; border: none; @@ -124,7 +126,7 @@ input[file-upload] { margin: 0 5px; } } -.quick-links{ +.quick-links { float: right; } button.save-consultation { @@ -133,11 +135,11 @@ button.save-consultation { padding: 10px 15px 9px; } button.save-consultation { - &.hideSaveText{ + &.hideSaveText { .fa-save { display: none; } - .text{ + .text { display: block; } @media screen and (max-width: 1024px) { @@ -152,22 +154,21 @@ button.save-consultation { } } -a.quick-links, a.btn--success { +a.quick-links, +a.btn--success { @include button($success, darken($success, 8%), white); padding: 10px 20px; font-size: 16px; } - td .status { color: orange; float: right; } - .abnormal-vitals { - width: auto!important; - margin: 4px!important; + width: auto !important; + margin: 4px !important; } pre.chief-notes { @@ -180,38 +181,39 @@ pre.chief-notes { padding-top: 7px; span:before { font-family: FontAwesome; - content: "\f00c"; + content: '\f00c'; margin-right: 5px; } } .current-visit-icon { - color: $clinicalCurrentVist; + color: $clinicalCurrentVist; } .has-notes { - color: orange; - i{ - color: orange !important; - } + color: orange; + i { + color: orange !important; + } } /*home*/ .bahmni-home { - margin-bottom: 5px!important; - .fa-home { - color: $lightestGray; - margin-right: 5px; - font-size: 18px; - } + margin-bottom: 5px !important; + .fa-home { + color: $lightestGray; + margin-right: 5px; + font-size: 18px; + } } .highlight { - border-left: 5px solid orange; + border-left: 5px solid orange; } -u, .add-drug-btninput[value]:first-letter { - text-decoration: none; - border-bottom: 1px solid; - font-family: $OpenSansBoldFont; +u, +.add-drug-btninput[value]:first-letter { + text-decoration: none; + border-bottom: 1px solid; + font-family: $OpenSansBoldFont; } .drug-dispensed { @@ -228,46 +230,46 @@ u, .add-drug-btninput[value]:first-letter { @include clearfix(); } -.overflowAuto{ +.overflowAuto { overflow: auto; } .is-abnormal .select2-chosen { - color: red!important; + color: red !important; } -.placeholder-text{ +.placeholder-text { background: #fff; padding: 5px; color: #444 !important; - p{ + p { margin-bottom: 0; color: #444 !important; } } -.fa-pencil, .fa-eye{ +.fa-pencil, +.fa-eye { cursor: pointer; - &:hover{ - color: #5C8A93; + &:hover { + color: #5c8a93; } @media screen and (max-width: 1024px) { font-size: 20px; } } -.cursor-pointer{ +.cursor-pointer { cursor: pointer; } -.clear-both{ +.clear-both { clear: both; } -.select2-drop{ +.select2-drop { z-index: 99999 !important; } -.ng-dialog-all-details-page{ - - .select2-container .select2-choice{ +.ng-dialog-all-details-page { + .select2-container .select2-choice { height: 26px !important; } } @@ -275,33 +277,33 @@ u, .add-drug-btninput[value]:first-letter { .mylegend { background: $lightestGray; line-height: 16px; - margin-bottom:20px; + margin-bottom: 20px; padding: 3px 0; border-radius: 3px; - border:1px solid $lightGray; - span{ - display:inline-block; - padding:2px 10px; - font-weight:bold; + border: 1px solid $lightGray; + span { + display: inline-block; + padding: 2px 10px; + font-weight: bold; } } -.registraion_legend{ +.registraion_legend { margin: 15px 0 15px 25px; - width:auto; + width: auto; position: relative; - @media (max-width : 768px){ + @media (max-width: 768px) { margin: 20px 0; width: 100%; } - &:before{ - content: ""; - border-top:1px $lightGray solid; + &:before { + content: ''; + border-top: 1px $lightGray solid; position: absolute; top: 14px; width: 96%; } - span.mylegend{ + span.mylegend { display: inline-block; width: auto; margin-bottom: 0; @@ -309,61 +311,62 @@ u, .add-drug-btninput[value]:first-letter { margin-left: 25px; position: relative; } - &.first-registraion_legend{ + &.first-registraion_legend { margin-left: 0; margin-right: 0; - &:before{ + &:before { width: 100%; } } - &.removeLegend{ - &:before{ + &.removeLegend { + &:before { display: none; } - span.mylegend{ + span.mylegend { display: none; } } } -.toggle-container{ - &.active{ - .fa-caret-right{ +.toggle-container { + &.active { + .fa-caret-right { display: none; } - .fa-caret-down{ + .fa-caret-down { display: inline; } } - .fa-caret-down{ + .fa-caret-down { display: none; } - } -.patient-image{ +.patient-image { background-color: #ffffff; } -.video, .video-dialog{ - width:280px; +.video, +.video-dialog { + width: 280px; height: 190px; display: table; - margin:0 auto; - &.disable{ - opacity: .1; + margin: 0 auto; + &.disable { + opacity: 0.1; pointer-events: none; border: 3px solid black; } } -.dashboard-section-loader{ +.dashboard-section-loader { position: absolute; - bottom:0; - width:100%; + bottom: 0; + width: 100%; height: 100%; - z-index:10000; - background: rgba(223,223,223, 0.4) url('../images/spinner-small.gif') no-repeat center center; + z-index: 10000; + background: rgba(223, 223, 223, 0.4) url('../images/spinner-small.gif') + no-repeat center center; } .print { @@ -371,10 +374,21 @@ u, .add-drug-btninput[value]:first-letter { display: none; } } -.spinnable{ +.spinnable { position: relative; min-height: 2em; } .ngdialog.ngdialog-theme-default .ngdialog-close { - z-index: 999; - } \ No newline at end of file + z-index: 999; +} + +.cdss-alert-icon { + cursor: pointer; + width: fit-content; + padding: 3px; + height: 24px; + i { + color: #8e8e8e; + width: auto !important; + } +} diff --git a/ui/test/unit/admin/controllers/fhirExportController.spec.js b/ui/test/unit/admin/controllers/fhirExportController.spec.js new file mode 100644 index 0000000000..8eeb8cf7bd --- /dev/null +++ b/ui/test/unit/admin/controllers/fhirExportController.spec.js @@ -0,0 +1,134 @@ +'use strict'; + +describe('FHIRExportController', function () { + var scope, rootScope, controller, translate, fhirExportService, messagingService; + + var fhirTasksMockData = { + "resourceType": "Bundle", + "total": 1, + "entry": [ + { + "fullUrl": "http://localhost/openmrs/ws/fhir2/R4/Task/dummy", + "resource": { + "resourceType": "Task", + "id": "dummy", + "status": "completed", + "authoredOn": "2023-10-11T12:21:03+00:00", + "owner": { + "reference": "Practitioner/superman", + "type": "Practitioner" + }, + "input": [ + { + "type": { + "coding": [ + { + "code": "dummy", + "display": "FHIR Export Start Date" + } + ], + "text": "FHIR Export Start Date" + }, + "valueString": "2023-09-11" + }, + { + "type": { + "coding": [ + { + "code": "dummy", + "display": "FHIR Export End Date" + } + ], + "text": "FHIR Export End Date" + }, + "valueString": "2023-10-11" + }, + { + "type": { + "coding": [ + { + "code": "dummy", + "display": "FHIR Export Anonymise Flag" + } + ], + "text": "FHIR Export Anonymise Flag" + }, + "valueString": "true" + } + ], + "output": [ + { + "type": { + "coding": [ + { + "code": "dummy", + "display": "Download URL" + } + ], + "text": "Download URL" + }, + "valueString": "http://localhost/openmrs/ws/rest/v1/fhirExtension/export?file=dummy" + } + ] + } + } + ] + }; + + var translatedMessages = { + "INSUFFICIENT_PRIVILEGE_TO_EXPORT": "You do not have sufficient privilege to export data", + "EXPORT_PATIENT_DATA_WARNING": "Note: Patient Data is confidential and should be handled carefully! It is recommended to export data in Anonymise mode, and ensure that the exported file is handled carefully by authorised parties only." }; + + beforeEach(module('bahmni.admin')); + + beforeEach(inject(function ($rootScope, $controller) { + scope = $rootScope.$new(); + rootScope = $rootScope; + $rootScope.currentUser = {privileges: [{name: Bahmni.Common.Constants.fhirExportPrivilege}, {name: Bahmni.Common.Constants.plainFhirExportPrivilege}]}; + translate = jasmine.createSpyObj('$translate', ['instant']); + fhirExportService = jasmine.createSpyObj('fhirExportService', ['loadFhirTasks', 'export', 'submitAudit']); + messagingService = jasmine.createSpyObj('messagingService', ['showMessage']); + + fhirExportService.loadFhirTasks.and.returnValue(specUtil.respondWith(fhirTasksMockData)); + + fhirExportService.export.and.callFake(function () { + return { + success: function (callback) { + return callback({ status: 200 }); + } + }; + }); + fhirExportService.submitAudit.and.callFake(function () { + return { + success: function (callback) { + return callback({ status: 200 }); + } + }; + }); + messagingService.showMessage.and.callFake(function () { + return { + then: function (callback) { + return callback({ status: 200 }); + } + }; + }); + + controller = $controller('FHIRExportController', { + $scope: scope, + $rootScope: rootScope, + $translate: translate, + fhirExportService: fhirExportService, + messagingService: messagingService + }); + })); + + it('should load fhir tasks', function () { + expect(scope.tasks).toEqual([]); + scope.loadFhirTasksForPrivilegedUsers().then(function () { + expect(scope.tasks).toEqual(fhirTasksMockData); + }); + }); + + afterEach(function () { + }); +}); diff --git a/ui/test/unit/clinical/consultation/controllers/diagnosisController.spec.js b/ui/test/unit/clinical/consultation/controllers/diagnosisController.spec.js index f2988c1d59..228b671a8c 100644 --- a/ui/test/unit/clinical/consultation/controllers/diagnosisController.spec.js +++ b/ui/test/unit/clinical/consultation/controllers/diagnosisController.spec.js @@ -1,9 +1,143 @@ describe("Diagnosis Controller", function () { - var $scope, rootScope, contextChangeHandler,mockDiagnosisService, spinner, appService, mockAppDescriptor, q, deferred, mockDiagnosisData, translate, retrospectiveEntryService, $state; + var $scope, rootScope, contextChangeHandler,mockDiagnosisService, spinner, appService, mockAppDescriptor, q, deferred, mockDiagnosisData, translate, retrospectiveEntryService, $state, drugService, cdssService;; var DateUtil = Bahmni.Common.Util.DateUtil; beforeEach(module('bahmni.clinical')); + var cdssBundle = { + "resourceType": "Bundle", + "type": "collection", + "entry": [ + { + "resource": { + "resourceType": "Condition", + "clinicalStatus": { + "coding": [ + { + "code": "active", + "display": "Active", + "system": "http://terminology.hl7.org/CodeSystem/condition-clinical" + } + ] + }, + "code": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "123456789", + "display": "Placeholder Diagnosis" + } + ], + "text": "Placeholder Diagnosis" + }, + "subject": { + "reference": "Patient/dc9444c6-ad55-4200-b6e9-407e025eb948" + } + } + }, + { + "resource": { + "resourceType": "MedicationRequest", + "status": "active", + "intent": "order", + "subject": { + "reference": "Patient/dc9444c6-ad55-4200-b6e9-407e025eb948" + }, + "medicationCodeableConcept": { + "id": "987654321", + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "987654321", + "display": "Placeholder Medication" + }, + { + "code": "987654321", + "system": "https://example.com", + "display": "Placeholder Medication" + } + ], + "text": "Placeholder Medication" + }, + "dosageInstruction": [ + { + "text": "{\"instructions\":\"As directed\"}", + "timing": { + "event": [ + "2023-10-01T05:58:27.000Z" + ], + "repeat": { + "duration": 3, + "durationUnit": "d" + }, + "code": { + "coding": [ + { + "code": "987654321", + "display": "Twice a day" + } + ], + "text": "Twice a day" + } + }, + "asNeededBoolean": false, + "doseAndRate": [ + { + "doseQuantity": { + "value": 3, + "unit": "Tablet(s)", + "code": "987654321" + } + } + ] + } + ] + } + } + ] + }; + + var cdssResponse = [{ + "uuid": "7b544b67-fbc7-48af-af95-ddaee09e836b", + "indicator": "warning", + "summary": "Contraindication: \"Placeholder Medication\" and \"Placeholder Medication\" with patient condition \"Placeholder Condition\" and \"Placeholder Condition\".", + "detail": "The use of Placeholder Medication is contraindicated when the patient has Placeholder Condition.", + "source": { + "label": "Wikipedia", + "url": "https://en.wikipedia.org/wiki/Atorvastatin#Contraindications" + }, + "referenceMedications": [ + { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "987654321", + "display": "Placeholder Medication" + }, + { + "system": "https://example.com", + "code": "987654321", + "display": "Placeholder Medication" + } + ] + } + ], + "referenceCondition": { + "coding": [ + { + "system": "https://example.com", + "code": "123456789", + "display": "Placeholder Condition" + }, + { + "system": "http://snomed.info/sct", + "code": "123456789", + "display": "Placeholder Condition" + } + ] + } + }]; + beforeEach(inject(function ($controller, $rootScope, $q, diagnosisService) { $scope = $rootScope.$new(); rootScope = $rootScope; @@ -35,10 +169,22 @@ describe("Diagnosis Controller", function () { } } }); - translate = jasmine.createSpyObj('$translate',['instant']); + translate = jasmine.createSpyObj('$translate', ['instant']); retrospectiveEntryService = jasmine.createSpyObj('retrospectiveEntryService', ['isRetrospectiveMode']); + drugService = jasmine.createSpyObj('drugService', ['getCdssEnabled', 'sendDiagnosisDrugBundle']); + + drugService.getCdssEnabled.and.returnValue(specUtil.respondWith(true)); + + drugService.sendDiagnosisDrugBundle.and.returnValue(specUtil.respondWith({ + data: [] + })); + + cdssService = jasmine.createSpyObj('cdssService', ['sortInteractionsByStatus', 'getAlerts']); + cdssService.sortInteractionsByStatus.and.returnValue(specUtil.respondWith(cdssResponse)); + cdssService.getAlerts.and.returnValue(specUtil.respondWith(cdssResponse)); + $controller('DiagnosisController', { $scope: $scope, $state: $state, @@ -48,7 +194,9 @@ describe("Diagnosis Controller", function () { appService: appService, diagnosisService: mockDiagnosisService, $translate: translate, - retrospectiveEntryService: retrospectiveEntryService + retrospectiveEntryService: retrospectiveEntryService, + drugService: drugService, + cdssService: cdssService }); })); diff --git a/ui/test/unit/clinical/consultation/services/cdssService.spec.js b/ui/test/unit/clinical/consultation/services/cdssService.spec.js new file mode 100644 index 0000000000..1f6035f7d0 --- /dev/null +++ b/ui/test/unit/clinical/consultation/services/cdssService.spec.js @@ -0,0 +1,143 @@ +describe('cdssService', function () { + var consultationDataMock = { + patient: { + uuid: 'patient_uuid_here' + }, + conditions: [ + { + uuid: 'condition_uuid_1', + status: 'CONFIRMED', + concept: { + uuid: 'concept_uuid_1', + name: 'Condition Name 1' + } + } + ], + newlyAddedDiagnoses: [ + { + uuid: 'diagnosis_uuid_1', + certainty: 'CONFIRMED', + codedAnswer: { + uuid: 'coded_answer_uuid_1', + name: 'Diagnosis Name 1' + } + }, + { + uuid: 'diagnosis_uuid_2', + certainty: 'CONFIRMED', + codedAnswer: { + uuid: 'coded_answer_uuid_2', + name: 'Diagnosis Name 2' + } + } + ], + draftDrug: [ + { + uuid: 'medication_uuid_1', + drug: { + uuid: 'drug_uuid_1', + name: 'Drug Name 1' + }, + instructions: 'Take once a day', + effectiveStartDate: '2023-10-03T08:00:00Z', + durationInDays: 7, + durationUnit: 'DAYS', + asNeeded: false, + uniformDosingType: { + frequency: 'Once daily', + dose: 1 + }, + doseUnits: 'mg' + } + ] + }; + + var cdssService; + var drugService = jasmine.createSpyObj('drugService', ['getDrugConceptSourceMapping']); + drugService.getDrugConceptSourceMapping.and.callFake(function () { + return specUtil.respondWith({ + data: { + entry: [{ + resource: { + code: { + coding: [{ + system: 'http://example.com', + code: '12345', + display: 'Sample Drug' + }] + } + } + }] + } + }); + }); + + beforeEach(function () { + module('bahmni.clinical'); + + module(function ($provide) { + $provide.value('drugService', drugService); + }); + + inject(['cdssService', '$rootScope', function (cdssServiceInjected, $rootScopeInjected) { + cdssService = cdssServiceInjected; + rootScope = $rootScopeInjected; + }]); + }); + + it('Should feturn an object with parameters for cdss request', function () { + var params = cdssService.createParams(consultationDataMock); + expect(params.patient).toEqual(consultationDataMock.patient); + expect(params.conditions).toEqual(consultationDataMock.conditions); + expect(params.diagnosis).toEqual(consultationDataMock.newlyAddedDiagnoses); + expect(params.medications).toEqual(consultationDataMock.draftDrug); + }); + + it('Should return a bundle of resources', function () { + cdssService.createFhirBundle(consultationDataMock.patient, consultationDataMock.conditions, consultationDataMock.draftDrug, consultationDataMock.newlyAddedDiagnoses, 'http://example.com').then(function (bundle) { + + expect(bundle.resourceType).toEqual('Bundle'); + expect(bundle.type).toEqual('collection'); + expect(bundle.entry.length).toEqual(3); + expect(bundle.entry[0].resource.resourceType).toEqual('Condition'); + expect(bundle.entry[2].resource.resourceType).toEqual('MedicationRequest'); + }); + }); + + it('Should return an array of alerts sorted by status', function () { + var alerts = [ + { + uuid: 'alert_uuid_1', + indicator: 'warning', + isActive: false, + detail: 'Alert Detail 1', + source: { + url: 'http://example.com' + } + }, + { + uuid: 'alert_uuid_2', + indicator: 'critical', + isActive: true, + detail: 'Alert Detail 2', + source: { + url: 'http://example.com' + } + }, + { + uuid: 'alert_uuid_3', + indicator: 'info', + isActive: true, + detail: 'Alert Detail 3', + source: { + url: 'http://example.com' + } + } + ]; + var sortedAlerts = cdssService.sortInteractionsByStatus(alerts); + expect(sortedAlerts[0].indicator).toEqual('critical'); + expect(sortedAlerts[1].indicator).toEqual('warning'); + expect(sortedAlerts[2].indicator).toEqual('info'); + }); +}); + diff --git a/ui/test/unit/clinical/controllers/addTreatmentController.spec.js b/ui/test/unit/clinical/controllers/addTreatmentController.spec.js index 3d96836536..1d257c24ff 100644 --- a/ui/test/unit/clinical/controllers/addTreatmentController.spec.js +++ b/ui/test/unit/clinical/controllers/addTreatmentController.spec.js @@ -107,9 +107,143 @@ describe("AddTreatmentController", function () { "provider": {name: "superman"} }; + var cdssBundle = { + "resourceType": "Bundle", + "type": "collection", + "entry": [ + { + "resource": { + "resourceType": "Condition", + "clinicalStatus": { + "coding": [ + { + "code": "active", + "display": "Active", + "system": "http://terminology.hl7.org/CodeSystem/condition-clinical" + } + ] + }, + "code": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "123456789", + "display": "Placeholder Diagnosis" + } + ], + "text": "Placeholder Diagnosis" + }, + "subject": { + "reference": "Patient/dc9444c6-ad55-4200-b6e9-407e025eb948" + } + } + }, + { + "resource": { + "resourceType": "MedicationRequest", + "status": "active", + "intent": "order", + "subject": { + "reference": "Patient/dc9444c6-ad55-4200-b6e9-407e025eb948" + }, + "medicationCodeableConcept": { + "id": "987654321", + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "987654321", + "display": "Placeholder Medication" + }, + { + "code": "987654321", + "system": "https://example.com", + "display": "Placeholder Medication" + } + ], + "text": "Placeholder Medication" + }, + "dosageInstruction": [ + { + "text": "{\"instructions\":\"As directed\"}", + "timing": { + "event": [ + "2023-10-01T05:58:27.000Z" + ], + "repeat": { + "duration": 3, + "durationUnit": "d" + }, + "code": { + "coding": [ + { + "code": "987654321", + "display": "Twice a day" + } + ], + "text": "Twice a day" + } + }, + "asNeededBoolean": false, + "doseAndRate": [ + { + "doseQuantity": { + "value": 3, + "unit": "Tablet(s)", + "code": "987654321" + } + } + ] + } + ] + } + } + ] + }; + + var cdssResponse = [{ + "uuid": "7b544b67-fbc7-48af-af95-ddaee09e836b", + "indicator": "warning", + "summary": "Contraindication: \"Placeholder Medication\" and \"Placeholder Medication\" with patient condition \"Placeholder Condition\" and \"Placeholder Condition\".", + "detail": "The use of Placeholder Medication is contraindicated when the patient has Placeholder Condition.", + "source": { + "label": "Wikipedia", + "url": "https://en.wikipedia.org/wiki/Atorvastatin#Contraindications" + }, + "referenceMedications": [ + { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "987654321", + "display": "Placeholder Medication" + }, + { + "system": "https://example.com", + "code": "987654321", + "display": "Placeholder Medication" + } + ] + } + ], + "referenceCondition": { + "coding": [ + { + "system": "https://example.com", + "code": "123456789", + "display": "Placeholder Condition" + }, + { + "system": "http://snomed.info/sct", + "code": "123456789", + "display": "Placeholder Condition" + } + ] + } + }]; + var $q, scope, stateParams, rootScope, contextChangeHandler, newTreatment, editTreatment, clinicalAppConfigService, ngDialog, drugService, drugs, - encounterDateTime, appService, appConfig, defaultDrugsPromise, orderSetService, locationService, $state; + encounterDateTime, appService, appConfig, defaultDrugsPromise, orderSetService, locationService, $state, cdssService; stateParams = { tabConfigName: null @@ -180,6 +314,10 @@ describe("AddTreatmentController", function () { drugService.cdssAudit.and.returnValue(specUtil.respondWith(true)); drugService.getDrugConceptSourceMapping.and.returnValue(specUtil.respondWithPromise($q, {entry: []})); + cdssService = jasmine.createSpyObj('cdssService', ['createFhirBundle', 'sendDiagnosisDrugBundle', 'createParams', 'addNewAlerts', 'sortInteractionsByStatus', 'getAlerts']); + cdssService.getAlerts.and.returnValue(specUtil.respondWith(cdssResponse)); + cdssService.sortInteractionsByStatus.and.returnValue(specUtil.respondWith(cdssResponse)); + appService.getAppDescriptor.and.returnValue(appConfig); orderSets = [{ "orderSetId": 3, @@ -238,6 +376,7 @@ describe("AddTreatmentController", function () { treatmentConfig: treatmentConfig, orderSetService: orderSetService, $state: $state, + cdssService: cdssService }); scope.treatments = []; scope.orderSetTreatments = []; @@ -498,16 +637,6 @@ describe("AddTreatmentController", function () { } ] }; - - it("should create FHIR bundle with diagnosis and medication orders", function () { - scope.consultation.newlyAddedDiagnoses = data.diagnosis; - scope.treatments = data.medications; - scope.createFhirBundle(scope.patient, data.conditions, data.medications, data.diagnosis).then(function (bundle) { - expect(bundle.entry.length).toBe(2); - expect(bundle.entry[0].resource.resourceType).toBe('Condition'); - expect(bundle.entry[1].resource.resourceType).toBe('MedicationRequest'); - }); - }); }); describe("add free text drug order()", function () { @@ -594,26 +723,27 @@ describe("AddTreatmentController", function () { ]; beforeEach(function () { - scope.cdssaAlerts = alerts; + scope.cdssAlerts = alerts; + scope.newAlerts = alerts; }); it("should close alert on clicking close button", function () { scope.closeAlert(0); - expect(scope.cdssaAlerts.length).toBe(0); + expect(scope.newAlerts.length).toBe(0); }); it("should toggle alert details on clicking toggle button", function () { scope.toggleAlertDetails(0); - expect(scope.cdssaAlerts[0].showDetails).toBeTruthy(); + expect(scope.newAlerts[0].showDetails).toBeTruthy(); scope.toggleAlertDetails(0); - expect(scope.cdssaAlerts[0].showDetails).toBeFalsy(); + expect(scope.newAlerts[0].showDetails).toBeFalsy(); }); it("should dismiss critical alert on submitting an audit", function () { scope.patient = {uuid: 'some-user-uuid'}; scope.treatment = {audit: "some-audit"}; scope.submitAudit(0).then(function () { - expect(scope.cdssaAlerts.length).toBe(0); + expect(scope.newAlerts.length).toBe(0); }); }); }); diff --git a/ui/test/unit/clinical/controllers/drugOrderHistoryController.spec.js b/ui/test/unit/clinical/controllers/drugOrderHistoryController.spec.js index e2226a74bf..8a9c1d69f8 100644 --- a/ui/test/unit/clinical/controllers/drugOrderHistoryController.spec.js +++ b/ui/test/unit/clinical/controllers/drugOrderHistoryController.spec.js @@ -1,7 +1,6 @@ 'use strict'; describe("DrugOrderHistoryController", function () { - beforeEach(module('bahmni.clinical')); var scope, prescribedDrugOrders, activeDrugOrder, _treatmentService, @@ -11,7 +10,7 @@ describe("DrugOrderHistoryController", function () { drugOrderHistoryConfig: { numberOfVisits: 4 } - } + }; var translate; beforeEach(module(function ($provide) { @@ -34,7 +33,8 @@ describe("DrugOrderHistoryController", function () { $rootScope.visit = {startDate: 1410322624000}; scope = $rootScope.$new(); scope.consultation = { - preSaveHandler: new Bahmni.Clinical.Notifier(), discontinuedDrugs: [], + preSaveHandler: new Bahmni.Clinical.Notifier(), + discontinuedDrugs: [], activeAndScheduledDrugOrders: [Bahmni.Clinical.DrugOrderViewModel.createFromContract(scheduledOrder), Bahmni.Clinical.DrugOrderViewModel.createFromContract(activeDrugOrder)] }; scope.currentBoard = {extensionParams: {}}; @@ -89,7 +89,6 @@ describe("DrugOrderHistoryController", function () { }); describe("when conditionally enable or disable order reason text for drug stoppage", function () { - it("should enable reason text for all concepts when nothing is configured", function () { var drugOrder = Bahmni.Clinical.DrugOrderViewModel.createFromContract(prescribedDrugOrders[0]); @@ -97,7 +96,6 @@ describe("DrugOrderHistoryController", function () { scope.discontinue(drugOrder); expect(drugOrder.orderReasonNotesEnabled).toBe(true); - }); it("should enable reason text only for configured reason concepts", function () { @@ -107,9 +105,7 @@ describe("DrugOrderHistoryController", function () { if (conceptName == "Adverse event") { drugOrder.orderReasonNotesEnabled = true; return true; - } - else - return false; + } else { return false; } } }; drugOrder.orderReasonConcept = {name: {name: "Adverse event"}}; @@ -125,9 +121,7 @@ describe("DrugOrderHistoryController", function () { if (conceptName == "Adverse event") { drugOrder.orderReasonNotesEnabled = true; return true; - } - else - return false; + } else { return false; } } }; drugOrder.orderReasonConcept = {name: {name: "Adverse event"}}; @@ -137,9 +131,7 @@ describe("DrugOrderHistoryController", function () { drugOrder.orderReasonConcept = {name: {name: "other event"}}; scope.updateFormConditions(drugOrder); expect(drugOrder.orderReasonNotesEnabled).toBe(false); - }); - }); it('should broadcast refillDrugOrder event on refill', function () { @@ -150,7 +142,7 @@ describe("DrugOrderHistoryController", function () { it('should broadcast reviseDrugOrder event on revise', function () { var drugOrder = Bahmni.Clinical.DrugOrderViewModel.createFromContract(prescribedDrugOrders[0]); - scope.consultation.drugOrdersWithUpdatedOrderAttributes = {} + scope.consultation.drugOrdersWithUpdatedOrderAttributes = {}; scope.revise(drugOrder, prescribedDrugOrders); expect(rootScope.$broadcast).toHaveBeenCalledWith('event:reviseDrugOrder', drugOrder, prescribedDrugOrders); }); @@ -194,7 +186,7 @@ describe("DrugOrderHistoryController", function () { "strength": null, "name": "Methylprednisolone 2ml" }, - "concept": { + "concept": { "shortName": "Methylprednisolone 2ml" } }; @@ -213,7 +205,7 @@ describe("DrugOrderHistoryController", function () { initController(); expect(scope.consultation.drugOrderGroups[0].drugOrders.length).toBe(1); - }) + }); }); activeDrugOrder = { diff --git a/ui/test/unit/clinical/directives/cdssAlertRow.spec.js b/ui/test/unit/clinical/directives/cdssAlertRow.spec.js new file mode 100644 index 0000000000..40fe5fd2ad --- /dev/null +++ b/ui/test/unit/clinical/directives/cdssAlertRow.spec.js @@ -0,0 +1,186 @@ +'use strict'; + +describe("CDSS alerts", function () { + var scope, rootScope, controller, appService, drugService, stateParams, q, deferred; + + var activeDrugOrder = { + "uuid": "activeOrderUuid", + "action": "NEW", + "careSetting": "Outpatient", + "orderType": "Drug Order", + "orderNumber": "ORD-1234", + "autoExpireDate": null, + "scheduledDate": null, + "dateStopped": null, + "instructions": null, + "visit": { + "startDateTime": 1397028261000, + "uuid": "002efa33-4c4f-469f-968a-faedfe3a5e0c" + }, + "drug": { + "form": "Injection", + "uuid": "8d7e3dc0-f4ad-400c-9468-5a9e2b1f4230", + "strength": null, + "name": "Methylprednisolone 2ml" + }, + "dosingInstructions": { + "quantity": 100, + "route": "Intramuscular", + "frequency": "Twice a day", + "doseUnits": "Tablespoon", + "asNeeded": false, + "quantityUnits": "Tablet", + "dose": 5, + "administrationInstructions": "{\"instructions\":\"In the evening\",\"additionalInstructions\":\"helylo\"}", + "numberOfRefills": null + }, + "durationUnits": "Days", + "dateActivated": 1410322624000, + "commentToFulfiller": null, + "effectiveStartDate": 1410322624000, + "effectiveStopDate": null, + "orderReasonConcept": null, + "dosingInstructionType": "org.openmrs.module.bahmniemrapi.drugorder.dosinginstructions.FlexibleDosingInstructions", + "previousOrderUuid": null, + "orderReasonText": null, + "duration": 10, + "concept": { + "shortName": "Methylprednisolone 2ml" + }, + "provider": {name: "superman"} + }; + + var drugOrderGroups = [ + { + "drugOrders": [ + { + "uuid": "drugOrderUuid", + "action": "NEW", + "careSetting": "Outpatient", + "orderType": "Drug Order", + "orderNumber": "ORD-1234", + "autoExpireDate": null, + "scheduledDate": null, + "dateStopped": null, + "instructions": null, + "visit": { + "startDateTime": 1397028261000, + "uuid": "002efa33-4c4f-469f-968a-faedfe3a5e0c" + }, + "drug": { + "form": "Injection", + "uuid": "8d7e3dc0-f4ad-400c-9468-5a9e2b1f4230", + "strength": null, + "name": "Methylprednisolone 2ml" + }, + "dosingInstructions": { + "quantity": 100, + "route": "Intramuscular", + "frequency": "Twice a day", + "doseUnits": "Tablespoon", + "asNeeded": false, + "quantityUnits": "Tablet", + "dose": 5, + "administrationInstructions": "{\"instructions\":\"In the evening\",\"additionalInstructions\":\"helylo\"}", + "numberOfRefills": null + }, + "durationUnits": "Days", + "dateActivated": 1410322624000, + "commentToFulfiller": null, + "effectiveStartDate": 1410322624000, + "effectiveStopDate": null, + "orderReasonConcept": null, + "dosingInstructionType": "org.openmrs.module.bahmniemrapi.drugorder.dosinginstructions.FlexibleDosingInstructions", + "previousOrderUuid": null, + "orderReasonText": null, + "duration": 10, + "concept": { + "shortName": "Methylprednisolone 2ml" + }, + "provider": {name: "superman"} + } + ] + } + ]; + + beforeEach(module('bahmni.clinical')); + + beforeEach(module(function ($provide) { + $provide.value('appService', appService); + })); + + beforeEach(inject(function ($controller, $rootScope, $q) { + controller = $controller; + rootScope = $rootScope; + spyOn($rootScope, '$broadcast'); + $rootScope.visit = {startDate: 1410322624000}; + scope = $rootScope.$new(); + q = $q; + deferred = $q.defer(); + drugService = jasmine.createSpyObj('drugService', ['cdssAudit']); + drugService.cdssAudit.and.returnValue(deferred.promise); + scope.consultation = { + activeAndScheduledDrugOrders: [Bahmni.Clinical.DrugOrderViewModel.createFromContract(activeDrugOrder)], + drugOrderGroups: drugOrderGroups + }; + } + )); + + var initController = function () { + controller('cdssAlertRowController', { + $scope: scope, + $stateParams: {patientUuid: "patientUuid"}, + drugService: drugService, + activeDrugOrders: [activeDrugOrder] + }); + rootScope.$apply(); + }; + + beforeEach(initController); + + it("should add alerts property to drugs whose code matches CDSS alerts", function () { + rootScope.cdssAlerts = [ + { + "indicator": "critical", + "summary": "Critical Alert", + "detail": "This is a critical alert", + "referenceMedications": [ + { + "coding": [ + { + "code": "8d7e3dc0-f4ad-400c-9468-5a9e2b1f4230", + "display": "Methylprednisolone 2ml" + + } + ] + } + ] + } + ]; + initController(); + expect(scope.consultation.drugOrderGroups[0].drugOrders[0].alerts.length).toBe(1); + }); + + it("should not add alerts property to drugs whose code does not match CDSS alerts", function () { + rootScope.cdssAlerts = [ + { + "indicator": "critical", + "summary": "Critical Alert", + "detail": "This is a critical alert", + "referenceMedications": [ + { + "coding": [ + { + "code": "12345", + "display": "Methylprednisolone 4ml" + + } + ] + } + ] + } + ]; + initController(); + expect(scope.consultation.drugOrderGroups[0].drugOrders[0].alerts.length).toBe(0); + }); +}); diff --git a/ui/test/unit/clinical/directives/cdssPopover.spec.js b/ui/test/unit/clinical/directives/cdssPopover.spec.js new file mode 100644 index 0000000000..c8b4b28a44 --- /dev/null +++ b/ui/test/unit/clinical/directives/cdssPopover.spec.js @@ -0,0 +1,45 @@ +describe('cdssPopover Directive', function () { + var httpBackend, scope; + var html = ''; + + beforeEach(module('bahmni.clinical')); + + beforeEach(inject(function ($compile, $rootScope, $httpBackend, $q) { + compile = $compile; + rootScope = $rootScope; + httpBackend = $httpBackend; + q = $q; + + scope = $rootScope.$new(); + scope.alerts = [ + { + indicator: 'critical', + summary: 'Critical Alert', + detail: 'This is a critical alert', + source: { + url: 'https://example.com/critical' + } + } + ]; + httpBackend + .expectGET('./consultation/views/cdssPopover.html') + .respond('
dummy
'); + var compiledEle = $compile(html)(scope); + scope.$digest(); + httpBackend.flush(); + scope.$digest(); + scope = compiledEle.isolateScope(); + scope.$digest(); + })); + + it('should show the popover when alerts are present', function () { + expect(scope).not.toBeUndefined(); + expect(scope.alerts.length).toBe(1); + }); + + it('should not show the popover when alerts are not present', function () { + scope.alerts = []; + scope.$digest(); + expect(scope.alerts.length).toBe(0); + }); +});