From f965e786f935fa1510975a591db1eccff1ded10e Mon Sep 17 00:00:00 2001 From: Chad Sebranek Date: Thu, 9 Jan 2025 13:57:14 -0600 Subject: [PATCH] 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 = '' + 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()");