From 3f8eb870a1f351f53b5201764b0bc490b249884a Mon Sep 17 00:00:00 2001 From: Chad Sebranek Date: Fri, 3 Jan 2025 13:15:15 -0600 Subject: [PATCH 1/3] sort current blood by room and cage first (#723) --- WNPRC_EHR/resources/queries/study/currentBloodDraws/.qview.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WNPRC_EHR/resources/queries/study/currentBloodDraws/.qview.xml b/WNPRC_EHR/resources/queries/study/currentBloodDraws/.qview.xml index 22719345e..4e3ff6275 100644 --- a/WNPRC_EHR/resources/queries/study/currentBloodDraws/.qview.xml +++ b/WNPRC_EHR/resources/queries/study/currentBloodDraws/.qview.xml @@ -32,9 +32,9 @@ - + From c7188f89673f0db371d22298e106573670d006c8 Mon Sep 17 00:00:00 2001 From: aschmidt34 <124093649+aschmidt34@users.noreply.github.com> Date: Tue, 7 Jan 2025 10:46:17 -0600 Subject: [PATCH 2/3] 24.3 fb notification revamps (#720) * - In Blood Draws Today (All, Animal Care, and Vet Staff), I changed 'Assignment Status' column header to 'Unassigned' and 'Completion Status' to 'Incomplete'. I also sorted all results so Incomplete draws show up first. - In BloodDrawReviewTriggerNotification, I made it so warnings only send when the draw has a date of today. - In BloodDrawReviewDailyNotification, I added a check to verify that there are no upcoming blood draws that will overdraw. - TODO: After testing this on test servers, make sure to remove test dates in BloodDrawsTodayAll and BloodDrawReviewDailyNotification. * Migrated automated test changes from 23.11 to 24.3. 23.11_notificationRevamps is now ready to be deleted. * Added blood overdraw trigger notification. * Registered new notification. * Updated code so message sends when testing/running in browser. Updated log for wnprc_triggers.js for when function is run to help debug until issue with webpack generation is fixed. * Updated overdraw notification. Notification is now created everytime blood is updated. Logic is run inside notification to determine if current draw is an overdraw. If it is an overdraw, the message is sent. * Fixed issue where 'Animal Replacement Fee' showed incorrectly in the revamped Death Notification. This occurred when the Type of Death resulted in a fee in the table 'ehr_lookups' > 'death_cause', but the 'prepaid' field was empty in the table 'study' > 'demographics'. This was due to labkeys table lookup resulting in a string 'null' instead of a real null when using the TableSelector. My new notifications use a new query function i wrote, but this Death Notification used the old TableSelector method. * Reordered messages in BloodDrawReviewDailyNotification.java so overdraws are listed first. * -NotificationToolkit.java: Updated getWeightFromAnimalID() and getSexFromAnimalID() so they use new query function getTableMultiRowMultiColumnWithFieldKeys() instead of old function getTableRowAsList(). Also moved DeathNecropsyObject and DeathDemographicObject to DeathNotificationRevamp.java. - DeathNotificationRevamp.java: Moved DeathNecropsyObject and DeathDemographicObject here from NotificationToolkit.java. Also cleaned up these objects and updated these them to use the new query getTableMultiRowMultiColumnWithFieldKeys. * Removed automated tests that don't aren't finished. * Added 2 new functions to NotificationToolkit for creating URL's. This is to avoid hardcoding URL's per new LabKey policy. Implimented these 2 new functions in DeathNotificationRevamp.java for getting the necropsy and animal abstract URL's. * Adding automated tests for notifications. * - WNPRC_EHRTest.java: Added notification setup function, multiple notification check functions, and a toolkit object containing reusable test functions that are commonly used when writing different tests. - BloodOverdrawTriggerNotification.java & BloodDrawReviewTriggerNotification.java: Added a reset function to clear the data after notification is triggered. This is because there are null checks that don't work when artifacts are left over from previous instances. - NotificationToolkit.java: Added null check to checkIfBloodDrawIsOverdraw() because empty data was crashing the function. * - DeathNotificationRevamp.java: Added a null check to make sure class doesn't cause a failure if there's no taskID in the row returned from the query. - WNPRC_EHRTest.java: Added a check for death notification and prenatal death notification. * Added null check before sending blood overdraw trigger manually. The built in labkey notification setup just doesn't send a notification if there's a null body, but for my trigger notifications where I send it manually, I should check for null. * Moved the automated tests to the end of the list and removed the @Test flag. * Disabled site-wide notifications. Also disable individual notifications. * Re-added 'test' tag before function. Originally removed this because Marty said it was not necessary if I called the function in doSetup(), but it's not showing up in the console log anymore. Trying to re-add this. * Added try/catch for insertValueIntoBloodBilledByDataset due to duplicate data being uploaded. Looks like labkey's loadBloodBilledByLookup() already uploads the same values. * Added try/catch for remaining functions that insert into datasets. * Changed a query function in checkIfAnimalIsAlive() so it no longer references my functions that use QviewObject. Need to evenutally phase out all these functions as my new query functions work much better. * Removing migrated alerts from ehrcron to Java based notifications * Added comment to leave commented-out code block alone. This will be used for future notifications, and it's very tough coding this correctly in the right order. Please do not delete. * Removed function call for notification tests in doSetup(). This was recommended by Binal via a LabKey ticket; it caused my test to run twice since the @test annotation already exists. * - TriggerScriptHelper.java: Added trigger call for new AnimalRequestUpdateNotificationRevamp notification. Also moved call for old notification inside if/else statement so 'on/off' status is checked before sending. Will delete the old call after vefifying new version works well. - WNPRC_EHRModule.java: Registered new EmptyNotificationRevamp and AnimalRequestUpdateNotificationRevamp notifications. - AdminAlertsNotificationRevamp.java: Updated this notification so the days of the week are displayed in the correct order. Also fixed an error with the wrong results being queried (needed to update the filter). - AnimalRequestNotificationRevamp.java: Added test data to be set when notification is triggered from 'Run Report in Browser'. Also added a resetClass function. - AnimalRequestUpdateNotificationRevamp.java: Created this new revamped notification. - ColonyAlertsLiteNotificationRevamp.java: Updated this notification to use the new EmptyNotificationRevamp notification. This is sent instead of the original notification if there is no data to be sent. - EmptyNotificationRevamp.java: Created this new revamped notification. This is sent when certain notifications have no data to send. This is to prevent users from receiving empty emails, but allows Daniel and I to see that the notifications are still being sent. - NotificationToolkit.java: Added function to send the new EmptyNotificationRevamp.java notification. * - TriggerScriptHelper.java: Added 'sendManually' function call that I had forgotten. - BloodDrawReviewDailyNotification.java: Added 2 extra checks requested by blood draw team. - BloodDrawsTodayAnimalCare.java: Updated this to use the new dummy notification when there is no data to be sent. * - animal_requests.js: Added fix for fatal issue with qcstatus being different in dataset and form. - ColonyAlertsNotificationRevamp.java: Added extra query requested by Kim. This query checks all animals in the 'Assignments' dataset and returns any where the project has expired or the protocol has deactivated. * - WNPRC_EHRModule.java: Registered new notification. - TreatmentAlertsNotificationRevamp.java: Created new notification. - BloodDrawsTodayAll.java: Added 'incomplete count' to the notification (as requested by blood draw team). - BloodDrawsTodayVetStaff.java: Added functionality so message does not send when there is no data (as requested by blood draw team). * Removed joda time import statement because it was unused. * ClinpathRefRange.sql - Added units column to table for data retrieval in ClinpathAbnormalResultsAlertsRevamp.java. WNPRC_EHRModule.java - Registered the 2 new notifications. ClinpathAbnormalResultsAlertsRevamp.java - Created new alert. ClinpathAlertsRevamp.java - Created new alert. * ClinpathResultAlertsRevamp.java: Created new revamp notification. WNPRC_EHRModule.java: Registered new notification. * LargeInfantAlertsRevamp.java: Created new revamped notification. WNPRC_EHRModule.java: Registered new LargeInfantAlertwsRevamp notification. * ClinpathAbnormalResultsAlertsRevamp.java: Fixed text display, typos, and filter. ClinpathAlertsRevamp.java: Fixed text display. ClinpathResultAlertsRevamp.java: Fixed filter. ColonyAlertsNotificationRevamp.java: Added Kim's fix (there was an issue where the query retrieved inactive projects. LargeInfantAlertsRevamp.java: Fixed text display. * TreatmentAlertsNotificationRevamp.java: Fixed line spacing before & after areas & rooms. Also removed unnecessary hyperlink tags. ClinpathAbnormalResultsAlertsRevamp.java: Fixed table result to only show results with an alert status set to true (i.e. high, low, or blank status in the table). Fixed line spacing before & after areas & rooms. Also removed unnecessary hyperlink tags. Fixed issue with DATE_GTE stripping time from query parameter. ClinpathResultsAlertsRevamp.java: Fixed line spacing before & after areas & rooms. ClinpathAlertsRevamp.java: Fixed issue with DATE_GTE stripping time from query parameter. --------- Co-authored-by: F. Daniel Nicolalde --- .../ClinpathAbnormalResultsAlertsRevamp.java | 74 ++++++++++--------- .../notification/ClinpathAlertsRevamp.java | 2 +- .../ClinpathResultAlertsRevamp.java | 4 +- .../TreatmentAlertsNotificationRevamp.java | 14 ++-- 4 files changed, 48 insertions(+), 46 deletions(-) diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/ClinpathAbnormalResultsAlertsRevamp.java b/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/ClinpathAbnormalResultsAlertsRevamp.java index 907663c8b..e9d00279e 100644 --- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/ClinpathAbnormalResultsAlertsRevamp.java +++ b/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/ClinpathAbnormalResultsAlertsRevamp.java @@ -88,9 +88,9 @@ public String getMessageBodyHTML(Container c, User u) { // Creates filter. SimpleFilter myFilter = new SimpleFilter("qcstate/PublicData", true, CompareType.EQUAL); - myFilter.addCondition("taskid/datecompleted", lastRunDate, CompareType.DATE_GTE); + myFilter.addCondition("taskid/datecompleted", lastRunDate, CompareType.GTE); myFilter.addCondition("taskid/datecompleted", "", CompareType.NONBLANK); - myFilter.addCondition("date", lastRunMinusWeek, CompareType.DATE_GTE); + myFilter.addCondition("date", lastRunMinusWeek, CompareType.GTE); // Creates columns to retrieve. String[] targetColumns = new String[]{"Id", "date", "Id/curLocation/area", "Id/curLocation/room", "Id/curLocation/cage", "alertStatus", "taskid/datecompleted", "testid", "result", "units", "status", "ref_range_min", "ref_range_max", "ageAtTime"}; // Runs query. @@ -102,52 +102,54 @@ public String getMessageBodyHTML(Container c, User u) { for (HashMap result : returnArray) { // Verifies 'alert status' exists before adding results. if (!result.get("alertStatus").isEmpty()) { - // Updates current location. - if (result.get("Id/curLocation/area").isEmpty()) { - result.put("Id/curLocation/area", "No Active Housing"); - } - // Updates current room. - if (result.get("Id/curLocation/room").isEmpty()) { - result.put("Id/curLocation/room", "No Room"); - } + if (result.get("alertStatus").equals("t")) { + // Updates current location. + if (result.get("Id/curLocation/area").isEmpty()) { + result.put("Id/curLocation/area", "No Active Housing"); + } + // Updates current room. + if (result.get("Id/curLocation/room").isEmpty()) { + result.put("Id/curLocation/room", "No Room"); + } - // Adds to list if area does not exist yet. - if (!filteredResults.containsKey(result.get("Id/curLocation/area"))) { - // Creates new room results list. - ArrayList> newRoomList = new ArrayList<>(); - newRoomList.add(result); - // Creates new room map. - HashMap>> newRoom = new HashMap<>(); - newRoom.put(result.get("Id/curLocation/room"), newRoomList); - // Creates new area map and adds to the filtered results. - filteredResults.put(result.get("Id/curLocation/area"), newRoom); - } - // Adds to list if room does not exist yet. - else if (!filteredResults.get(result.get("Id/curLocation/area")).containsKey(result.get("Id/curLocation/room"))) { - // Creates new room results list. - ArrayList> newRoomList = new ArrayList<>(); - newRoomList.add(result); - // Creates new room map and adds to the areas list. - filteredResults.get(result.get("Id/curLocation/area")).put(result.get("Id/curLocation/room"), newRoomList); - } - // Adds to list if area and room exist already. - else { - filteredResults.get(result.get("Id/curLocation/area")).get(result.get("Id/curLocation/room")).add(result); + // Adds to list if area does not exist yet. + if (!filteredResults.containsKey(result.get("Id/curLocation/area"))) { + // Creates new room results list. + ArrayList> newRoomList = new ArrayList<>(); + newRoomList.add(result); + // Creates new room map. + HashMap>> newRoom = new HashMap<>(); + newRoom.put(result.get("Id/curLocation/room"), newRoomList); + // Creates new area map and adds to the filtered results. + filteredResults.put(result.get("Id/curLocation/area"), newRoom); + } + // Adds to list if room does not exist yet. + else if (!filteredResults.get(result.get("Id/curLocation/area")).containsKey(result.get("Id/curLocation/room"))) { + // Creates new room results list. + ArrayList> newRoomList = new ArrayList<>(); + newRoomList.add(result); + // Creates new room map and adds to the areas list. + filteredResults.get(result.get("Id/curLocation/area")).put(result.get("Id/curLocation/room"), newRoomList); + } + // Adds to list if area and room exist already. + else { + filteredResults.get(result.get("Id/curLocation/area")).get(result.get("Id/curLocation/room")).add(result); + } } } } // Prints text. messageBody.append("There have been " + returnArray.size() + " clinpath tasks completed since " + lastRunDate + "
"); - messageBody.append(notificationToolkit.createHyperlink("Click here to view them", clinpathTasksUrlView) + "

\n"); - messageBody.append("

Listed below are the abnormal records.

\n"); + messageBody.append(notificationToolkit.createHyperlink("Click here to view them", clinpathTasksUrlView) + "

\n"); + messageBody.append("

Listed below are the abnormal records.
\n"); // Prints table with all records. String[] tableColumns = new String[]{"Id", "Collect Date", "Date Completed", "Test ID", "Result", "Units", "Status", "Ref Range Min", "Ref Range Max", "Age At Time"}; for (String currentArea : notificationToolkit.sortSetWithNulls(filteredResults.keySet())) { - messageBody.append("" + currentArea + ":
\n"); + messageBody.append("
\n" + currentArea + ":
\n"); for (String currentRoom : notificationToolkit.sortSetWithNulls(filteredResults.get(currentArea).keySet())) { - messageBody.append(currentRoom + ":
\n"); + messageBody.append("
\n" + currentRoom + ":\n"); // Reformats the hashmap into a String[] List (to be compatible with the table creation function). ArrayList currentTableData = new ArrayList<>(); ArrayList rowColorsList = new ArrayList<>(); diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/ClinpathAlertsRevamp.java b/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/ClinpathAlertsRevamp.java index 04326efc5..a1927bf85 100644 --- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/ClinpathAlertsRevamp.java +++ b/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/ClinpathAlertsRevamp.java @@ -131,7 +131,7 @@ public ClinpathAlertsObject(Container currentContainer, User currentUser, Date l private void getRecordsRequestedSinceLastEmail() { // Creates filter. SimpleFilter myFilter = new SimpleFilter("qcstate/label", "Request: Pending", CompareType.EQUAL); - myFilter.addCondition("created", dateLastRun, CompareType.DATE_GTE); + myFilter.addCondition("created", dateLastRun, CompareType.GTE); // Creates columns to retrieve. String[] targetColumns = new String[]{"created"}; // Runs query. diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/ClinpathResultAlertsRevamp.java b/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/ClinpathResultAlertsRevamp.java index 97dea6e8f..8bee3df8d 100644 --- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/ClinpathResultAlertsRevamp.java +++ b/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/ClinpathResultAlertsRevamp.java @@ -149,9 +149,9 @@ else if (!filteredResults.get(result.get("Id/curLocation/area")).containsKey(res // Creates a table from the data. String[] tableColumns = new String[]{"Id", "Collect Date", "Service Requested", "Requestor", "Date Reviewed", "Reviewed By"}; for (String currentArea : notificationToolkit.sortSetWithNulls(filteredResults.keySet())) { - messageBody.append("" + currentArea + ":
\n"); + messageBody.append("
\n" + currentArea + ":
\n"); for (String currentRoom : notificationToolkit.sortSetWithNulls(filteredResults.get(currentArea).keySet())) { - messageBody.append(currentRoom + ":
\n"); + messageBody.append("
\n" + currentRoom + ":\n"); // Reformats the hashmap into a String[] List (to be compatible with the table creation function). ArrayList currentTableData = new ArrayList<>(); ArrayList rowColorsList = new ArrayList<>(); diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/TreatmentAlertsNotificationRevamp.java b/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/TreatmentAlertsNotificationRevamp.java index 13412570a..a177fde7d 100644 --- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/TreatmentAlertsNotificationRevamp.java +++ b/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/TreatmentAlertsNotificationRevamp.java @@ -85,7 +85,7 @@ public String getMessageBodyHTML(Container c, User u) { // 1. Shows all rooms lacking observations today. if (!myTreatmentAlertsObject.roomsLackingObservationsToday.isEmpty()) { messageBody.append("WARNING: The following rooms do not have any obs for today as of: " + dateToolkit.getCurrentTime() + "."); - messageBody.append("" + notificationToolkit.createHyperlink("Click here to view them.

\n", myTreatmentAlertsObject.roomsLackingObservationsTodayUrlView)); + messageBody.append("" + notificationToolkit.createHyperlink(" Click here to view them.

\n", myTreatmentAlertsObject.roomsLackingObservationsTodayUrlView)); for (HashMap result : myTreatmentAlertsObject.roomsLackingObservationsToday) { messageBody.append(result.get("room") + "
"); } @@ -94,7 +94,7 @@ public String getMessageBodyHTML(Container c, User u) { // 2. Shows all treatments where the animal is not assigned to that project. if (!myTreatmentAlertsObject.treatmentsWithAnimalNotAssignedToProject.isEmpty()) { messageBody.append("WARNING: There are " + myTreatmentAlertsObject.treatmentsWithAnimalNotAssignedToProject.size() + " scheduled treatments where the animal is not assigned to the project.

"); - messageBody.append("" + notificationToolkit.createHyperlink("Click here to view them.
\n", myTreatmentAlertsObject.treatmentsWithAnimalNotAssignedToProjectUrlView)); + messageBody.append("" + notificationToolkit.createHyperlink(" Click here to view them.
\n", myTreatmentAlertsObject.treatmentsWithAnimalNotAssignedToProjectUrlView)); messageBody.append("


\n"); } // 3. Shows treatments for each time of day. @@ -106,7 +106,7 @@ public String getMessageBodyHTML(Container c, User u) { Integer totalTreatments = myTreatmentAlertsObject.incompleteTreatmentsForEachTimeOfDay.get(timeOfDay).size() + myTreatmentAlertsObject.completedTreatmentCountsForEachTimeOfDay.get(timeOfDay); if (totalTreatments > 0) { messageBody.append("There are " + totalTreatments + " scheduled " + timeOfDay + " treatments. " + myTreatmentAlertsObject.completedTreatmentCountsForEachTimeOfDay.get(timeOfDay) + " have been completed. "); - messageBody.append("" + notificationToolkit.createHyperlink("Click here to view them.

\n", myTreatmentAlertsObject.treatmentsForEachTimeOfDayUrlView.get(timeOfDay))); + messageBody.append("" + notificationToolkit.createHyperlink("Click here to view them.
\n", myTreatmentAlertsObject.treatmentsForEachTimeOfDayUrlView.get(timeOfDay))); // Creates the current timeOfDay results sorted by (area --> room --> result). HashMap>>> resultsByArea = new HashMap<>(); @@ -141,10 +141,10 @@ else if (!resultsByArea.get(currentArea).containsKey(currentRoom)) { // Iterates through each area (sorted alphabetically). for (String currentArea : notificationToolkit.sortSetWithNulls(resultsByArea.keySet())) { - messageBody.append("" + currentArea + ":
\n"); + messageBody.append("
\n" + currentArea + ":
\n"); // Iterates through each room (sorted alphabetically) for (String currentRoom : notificationToolkit.sortSetWithNulls(resultsByArea.get(currentArea).keySet())) { - messageBody.append(currentRoom + ": " + resultsByArea.get(currentArea).get(currentRoom).size() + "
\n"); + messageBody.append("
\n" + currentRoom + ": " + resultsByArea.get(currentArea).get(currentRoom).size() + "\n"); // Reformats the treatment hashmap into a String[] List (to be compatible with the table creation function). ArrayList formattedResults = new ArrayList<>(); for (HashMap currentTreatment : resultsByArea.get(currentArea).get(currentRoom)) { @@ -188,13 +188,13 @@ else if (!resultsByArea.get(currentArea).containsKey(currentRoom)) { // 5. Shows any treatments where the animal is not alive. if (!myTreatmentAlertsObject.treatmentsWhereAnimalIsNotAlive.isEmpty()) { messageBody.append("WARNING: There are " + myTreatmentAlertsObject.treatmentsWhereAnimalIsNotAlive.size() + " active treatments for animals not currently at WNPRC."); - messageBody.append("" + notificationToolkit.createHyperlink("Click here to view and update them.
\n", myTreatmentAlertsObject.treatmentsWhereAnimalIsNotAliveURLView)); + messageBody.append("" + notificationToolkit.createHyperlink(" Click here to view and update them.
\n", myTreatmentAlertsObject.treatmentsWhereAnimalIsNotAliveURLView)); messageBody.append("
\n"); } // 6. Find any problems where the animal is not alive. if (!myTreatmentAlertsObject.problemsWhereAnimalIsNotAlive.isEmpty()) { messageBody.append("WARNING: There are " + myTreatmentAlertsObject.problemsWhereAnimalIsNotAlive.size() + " unresolved problems for animals not currently at WNPRC."); - messageBody.append("" + notificationToolkit.createHyperlink("Click here to view and update them.
\n", myTreatmentAlertsObject.problemsWhereAnimalIsNotAliveUrlView)); + messageBody.append("" + notificationToolkit.createHyperlink(" Click here to view and update them.
\n", myTreatmentAlertsObject.problemsWhereAnimalIsNotAliveUrlView)); messageBody.append("
\n"); } // 7. Checks for missing In Rooms after 2:30pm, as specified in the SOP. From f965e786f935fa1510975a591db1eccff1ded10e Mon Sep 17 00:00:00 2001 From: Chad Sebranek Date: Thu, 9 Jan 2025 13:57:14 -0600 Subject: [PATCH 3/3] Blood draw scheduling improvement (#724) * working with selected records * Check DB for scheduled/approved blood draws * half working test * finish test * remove unused import * add error details * change wording * add timestamp * auto adjust height to fit message * more appropriate naming --- .../resources/web/wnprc_ehr/datasetButtons.js | 68 +++++- .../labkey/wnprc_ehr/WNPRC_EHRController.java | 200 ++++++++++++++++++ .../test/tests/wnprc_ehr/WNPRC_EHRTest.java | 99 +++++++++ 3 files changed, 364 insertions(+), 3 deletions(-) diff --git a/WNPRC_EHR/resources/web/wnprc_ehr/datasetButtons.js b/WNPRC_EHR/resources/web/wnprc_ehr/datasetButtons.js index 51ea4fd69..a02a93d60 100644 --- a/WNPRC_EHR/resources/web/wnprc_ehr/datasetButtons.js +++ b/WNPRC_EHR/resources/web/wnprc_ehr/datasetButtons.js @@ -673,6 +673,25 @@ WNPRC_EHR.DatasetButtons = new function(){ } } }, + + checkBloodSchedule: function(records) { + + return new Promise((resolve, reject) => { + LABKEY.Ajax.request({ + url: LABKEY.ActionURL.buildURL('WNPRC_EHR', 'CompareBloodSchedules'), + jsonData: { records: records }, + callback: function (config, success, xhr) { + if (success) { + resolve(xhr.responseText); + } else { + reject('Couldn\'t compare blood schedule, internal error.'); + } + } + }) + }); + + }, + /** * This add a handler to a dataset that allows the user to change the QCState of the records, designed to approve or deny blood requests. * It also captures values for 'billedBy' and 'instructions'. @@ -710,14 +729,26 @@ WNPRC_EHR.DatasetButtons = new function(){ title: 'Change Request Status', width: 430, autoHeight: true, + id: 'change-request-window', items: [{ xtype: 'form', + height: '100%', ref: 'theForm', + id: 'change-request-form', + autoHeight: true, bodyStyle: 'padding: 5px;', defaults: { border: false }, - items: [{ + items: [ + + { + id:'bloodCompareResponseWrapper', + height: '100%', + html: '
loading..
', + tag: 'div' + }, + { html: 'Total Records: '+checked.length+'

', tag: 'div' },{ @@ -767,10 +798,11 @@ WNPRC_EHR.DatasetButtons = new function(){ }], buttons: [{ text:'Submit', - disabled:false, + disabled:true, formBind: true, ref: '../submit', scope: this, + id: 'submitButton', handler: function(o){ var win = o.up('window'); var form = win.down('form'); @@ -848,7 +880,37 @@ WNPRC_EHR.DatasetButtons = new function(){ handler: function(o){ o.ownerCt.ownerCt.close(); } - }] + }], + listeners: { + afterrender: () => { + this.checkBloodSchedule(records).then(response => { + let resp = document.getElementById('bloodCompareResponse'); + let rsp = JSON.parse(response).message; + let txt = ''; + if (rsp) { + for (let item of rsp) { + txt += '
  • ' + item.message + ' (Project(s): ' + item.projects + '; Contacts: ' + item.emails + '' + ')
  • '; + } + } + if (txt.length > 0){ + txt = '
      ' + txt + '
    ' + resp.innerHTML = ' Warning: ' + txt; + } else { + resp.innerHTML = ''; + } + Ext4.getCmp('submitButton').enable() + // Have to reset the height since the form has a set height after initial rendering, + // but when text is dynamically added the height does not change. + Ext4.getCmp('change-request-form').setHeight('100%') + }).catch(error => { + Ext4.getCmp('submitButton').enable() + console.error(error); + let resp = document.getElementById('bloodCompareResponse'); + resp.innerHTML = '

    Error checking blood schedule, you are still able to submit. Please contact EHR admins with details: ' + new Date() + ' ' + JSON.parse(error) + '

    ' + Ext4.getCmp('change-request-form').setHeight('100%') + }); + } + } }).show(); } }, diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java b/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java index e6e06d899..4742774b7 100644 --- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java +++ b/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java @@ -37,6 +37,7 @@ import org.labkey.api.action.SimpleRedirectAction; import org.labkey.api.action.SpringActionController; import org.labkey.api.data.ColumnInfo; +import org.labkey.api.data.CompareType; import org.labkey.api.data.Container; import org.labkey.api.data.CoreSchema; import org.labkey.api.data.DbSchema; @@ -76,11 +77,13 @@ import org.labkey.api.security.RequiresPermission; import org.labkey.api.security.RequiresSiteAdmin; import org.labkey.api.security.User; +import org.labkey.api.security.UserManager; import org.labkey.api.security.permissions.AdminPermission; import org.labkey.api.security.permissions.ReadPermission; import org.labkey.api.study.Dataset; import org.labkey.api.study.StudyService; import org.labkey.api.util.ExceptionUtil; +import org.labkey.api.util.PageFlowUtil; import org.labkey.api.util.Path; import org.labkey.api.util.ResultSetUtil; import org.labkey.api.util.URLHelper; @@ -122,6 +125,8 @@ import java.io.InputStreamReader; import java.nio.file.Paths; import java.sql.SQLException; +import java.sql.Timestamp; +import java.text.ParseException; import java.text.SimpleDateFormat; import java.time.ZoneId; import java.time.format.DateTimeFormatter; @@ -129,6 +134,7 @@ import java.util.Date; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -2105,4 +2111,198 @@ public Object execute(Object o, BindException errors) throws Exception return response; } } + + public static class CompareBloodSchedulesForm + { + private List> _records; + public List> getRecords() + { + return _records; + } + + public void setRecords(List> records) + { + _records = records; + } + } + + public static String convertSetToString(Set set, String delimiter) { + StringBuilder sb = new StringBuilder(); + Iterator iterator = set.iterator(); + + while (iterator.hasNext()) { + Object element = iterator.next(); + sb.append(element); + if (iterator.hasNext()) { + sb.append(delimiter + " "); + } + } + + return sb.toString(); + } + + @ActionNames("CompareBloodSchedules") + @RequiresNoPermission() + public class comparebloodSchedulesAction extends ReadOnlyApiAction + { + + @Override + public ApiResponse execute(CompareBloodSchedulesForm form, BindException errors) throws IOException, InvalidFormatException + { + + ApiSimpleResponse response = new ApiSimpleResponse(); + Map>> groupedById = new HashMap<>(); + Map>> groupedByDate = new HashMap<>(); + List lsids = new ArrayList<>(); + + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); + Date parsedDate; + + for (Map mp : form.getRecords()) + { + String id = (String) mp.get("Id"); + try + { + //convert the date to Timestamp since this is what comes from the db later + parsedDate = dateFormat.parse((String) mp.get("date")); + Timestamp date = new Timestamp(parsedDate.getTime()); + mp.put("date", date); + groupedById.computeIfAbsent(id, k -> new ArrayList<>()).add(mp); + groupedByDate.computeIfAbsent(date, k -> new ArrayList<>()).add(mp); + } + catch (ParseException e) + { + throw new RuntimeException(e); + } + } + + + Set ids = groupedById.keySet(); + Set dates = groupedByDate.keySet(); + + //get min date to query DB later + java.time.LocalDate minDate= java.time.LocalDate.now().plusYears(100); + for (Timestamp dateTime : dates) + { + java.time.LocalDate date1 = dateTime.toLocalDateTime().toLocalDate(); + minDate = date1.isBefore(minDate) ? date1 : minDate; + } + + //get max date to query DB later + java.time.LocalDate maxDate = java.time.LocalDate.now().minusYears(100); + for (Timestamp dateTime : dates) + { + java.time.LocalDate date1 = dateTime.toLocalDateTime().toLocalDate(); + maxDate = date1.isAfter(maxDate) ? date1 : maxDate; + } + + //get any blood draws that have potentially the same date & id + Set qcStates = new HashSet<>(); + qcStates.add(EHRService.get().getQCStates(getContainer()).get(EHRService.QCSTATES.RequestApproved.getLabel()).getRowId()); + qcStates.add(EHRService.get().getQCStates(getContainer()).get(EHRService.QCSTATES.Scheduled.getLabel()).getRowId()); + + SimpleFilter bloodFilter = new SimpleFilter(FieldKey.fromString("lsid"), String.join("; ",ids), CompareType.CONTAINS_ONE_OF); + bloodFilter.addCondition(FieldKey.fromString("date"), minDate, CompareType.DATE_GTE); + bloodFilter.addCondition(FieldKey.fromString("date"), maxDate, CompareType.DATE_LTE); + bloodFilter.addCondition(FieldKey.fromString("qcstate"), qcStates, CompareType.IN); + //Runs query with updated info. + TableInfo bloodTi = QueryService.get().getUserSchema(getUser(), getContainer(), "study").getTable("Blood Draws"); + TableSelector bloodTable = new TableSelector(bloodTi, PageFlowUtil.set("lsid", "date", "project", "Id", "requestor", "createdby"), bloodFilter, null); + Map[] bloodRows = bloodTable.getMapArray(); + + //add the records from the db to our groupedById array + for (Map row : bloodRows) + { + for (Map.Entry>> entry: groupedById.entrySet()) + { + if (entry.getKey().equals(row.get("Id"))) + { + entry.getValue().add(row); + } + } + } + + + List> messageList = new ArrayList<>(); + + for (Map.Entry>> entry : groupedById.entrySet()) + { + Map theMessage = new HashMap<>(); + + List> records = entry.getValue(); + + Set uniqueTimes = new HashSet<>(); + Set uniqueRequestors = new HashSet<>(); + Set uniqueProjects = new HashSet<>(); + Set uniqueCreatedBy = new HashSet<>(); + + + // Create a Set to track unique dates + for (int i = 0; i < records.size() - 1; i++) + { + Map record1 = records.get(i); + Timestamp dateTime1 = (Timestamp) record1.get("date"); + + lsids.add((String) record1.get("lsid")); + + for (int j = i + 1; j < records.size(); j++) + { + Map record2 = records.get(j); + Timestamp dateTime2 = (Timestamp) record2.get("date"); + lsids.add((String) record1.get("lsid")); + + + java.time.LocalDate date1 = dateTime1.toLocalDateTime().toLocalDate(); + java.time.LocalTime time1 = dateTime1.toLocalDateTime().toLocalTime(); + + java.time.LocalDate date2 = dateTime2.toLocalDateTime().toLocalDate(); + java.time.LocalTime time2 = dateTime2.toLocalDateTime().toLocalTime(); + + if (date1.isEqual(date2) && !time1.equals(time2)) + { + uniqueTimes.add(time1); + uniqueTimes.add(time2); + } + } + } + if (uniqueTimes.size() > 0) + { + //get requestor and project information + + SimpleFilter myFilter = new SimpleFilter(FieldKey.fromString("lsid"), String.join("; ", lsids), CompareType.CONTAINS_ONE_OF); + //Runs query with updated info. + TableInfo ti = QueryService.get().getUserSchema(getUser(), getContainer(), "study").getTable("Blood Draws"); + TableSelector myTable = new TableSelector(ti, PageFlowUtil.set("lsid", "project", "Id", "requestor", "createdby"), myFilter, null); + Map[] rows = myTable.getMapArray(); + for (Map row : rows) + { + uniqueRequestors.add((String) row.get("requestor")); + uniqueProjects.add((Integer) row.get("project")); + uniqueCreatedBy.add((Integer) row.get("createdBy")); + } + + List emails = new ArrayList<>(); + for (var userId : uniqueCreatedBy) + { + var user = UserManager.getUser(userId); + + if (user != null) + emails.add(user.getEmail()); + } + + //pull out requestors email/userid + theMessage.put("emails", String.join(",", emails)); + theMessage.put("projects", convertSetToString(uniqueProjects, ",")); + theMessage.put("message",entry.getKey() + " has "+ uniqueTimes.size() + " draws on the same day but at different times."); + + messageList.add(theMessage); + } + } + response.put("message", messageList); + + return response; + + } + } + } diff --git a/WNPRC_EHR/test/src/org/labkey/test/tests/wnprc_ehr/WNPRC_EHRTest.java b/WNPRC_EHR/test/src/org/labkey/test/tests/wnprc_ehr/WNPRC_EHRTest.java index da85269de..118bf65e5 100644 --- a/WNPRC_EHR/test/src/org/labkey/test/tests/wnprc_ehr/WNPRC_EHRTest.java +++ b/WNPRC_EHR/test/src/org/labkey/test/tests/wnprc_ehr/WNPRC_EHRTest.java @@ -3701,6 +3701,105 @@ public void testBloodDrawAPI() throws Exception */ } + + // Note: there is one flaw with this test below w.r.t the dates. + // the idea is to test two dates within the same day under a couple constraints, + // one is there can be no blood requests in the past, and another is it can only go up to 60 days into the future, + // so setting a date in the distant past or future is not possible, so we have to do a dynamic date using + // new Date() and offsetting the hours. If the first date happens to run within an hour before midnight, this test will fail. + @Test + public void testBloodDrawDoubleScheduleWarning() throws Exception + { + goToProjectHome(); + ReusableTestFunctions myReusableFunctions = new ReusableTestFunctions(); + + Integer numTubes = 1; + String tubeType = "EDTA"; + Integer project = 640991; + String account = "acct102"; + Double tubeVolOK = 1.0; + Double quantityOK = tubeVolOK * numTubes; + InsertRowsCommand bloodCmd = new InsertRowsCommand("study", "blood"); + Date dt = prepareDate(new Date(), 10, 0); + Integer requestPending = myReusableFunctions.getQCStateRowID("Request: Pending"); + Integer scheduled = myReusableFunctions.getQCStateRowID("Scheduled"); + + + bloodCmd.addRow(new HashMap() + { + { + put("Id", TEST_SUBJECTS[0]); + put("date", dt); + put("project", project); + put("account", account); + put("tube_type", tubeType); + put("tube_vol", tubeVolOK); + put("num_tubes", numTubes); + put("quantity", quantityOK); + put("additionalServices", null); + put("restraint", "Chemical"); + put("restraintDuration", "< 30 min"); + put("instructions", "test special instruction"); + put("remark", "test remark"); + put("performedby", "autotest"); + put("QCState", requestPending); + + } + }); + Date d2 = prepareDate(new Date(), 10, 1); + bloodCmd.addRow(new HashMap() + { + { + put("Id", TEST_SUBJECTS[0]); + put("date", d2); + put("project", project); + put("account", account); + put("tube_type", tubeType); + put("tube_vol", tubeVolOK); + put("num_tubes", numTubes); + put("quantity", quantityOK); + put("additionalServices", null); + put("restraint", "Chemical"); + put("restraintDuration", "< 30 min"); + put("instructions", "test special instruction"); + put("remark", "test remark"); + put("performedby", "autotest"); + put("QCState", scheduled); + + } + }); + bloodCmd.execute(getApiHelper().getConnection(), getContainerPath()); + + SelectRowsCommand sr = new SelectRowsCommand("study","blood"); + sr.addFilter("Id", TEST_SUBJECTS[0], Filter.Operator.EQUAL); + sr.addFilter("date", dt, Filter.Operator.EQUAL); + SelectRowsResponse resp2 = sr.execute(getApiHelper().getConnection(), EHR_FOLDER_PATH); + Assert.assertEquals(1, resp2.getRowCount()); + Assert.assertEquals(requestPending, resp2.getRows().get(0).get("QCState")); + + + //approve some draws + goToEHRFolder(); + waitAndClickAndWait(Locator.linkWithText("Enter Data")); + waitAndClick(Locator.linkWithText("Blood Draw Requests")); + //clickBootstrapTab("Blood Draw Requests"); + waitForText(TEST_SUBJECTS[0]); + + WebElement parentDiv = getDriver().findElement(By.cssSelector("div[id*='lk-gen'][class='tab-pane active']")); + + // Locate the form element within the parent div + WebElement formElement = parentDiv.findElement(By.xpath(".//form[contains(@id, 'lk-region-')]")); + + // Get the value of the "lk-region-form" attribute + String formAttribute = formElement.getAttribute("lk-region-form"); + DataRegionTable dataRegionTable = new DataRegionTable.DataRegionFinder(getDriver()).withName(formAttribute).find(); + + dataRegionTable.checkCheckbox(0); + + dataRegionTable.clickHeaderMenu("More Actions", false, "Change Request Status"); + waitForText(TEST_SUBJECTS[0] + " has 2 draws on the same day but at different times"); + assertTextPresent(TEST_SUBJECTS[0] + " has 2 draws on the same day but at different times"); + } protected String generateGUID() { return (String)executeScript("return LABKEY.Utils.generateUUID().toUpperCase()");