diff --git a/onprc_ehr/resources/queries/study/Demographics_NotInMMA.query.xml b/onprc_ehr/resources/queries/study/Demographics_NotInMMA.query.xml new file mode 100644 index 000000000..48f834dca --- /dev/null +++ b/onprc_ehr/resources/queries/study/Demographics_NotInMMA.query.xml @@ -0,0 +1,9 @@ + + + + + Demographics (Excluding animals in Weight MMA regimen) +
+
+
+
diff --git a/onprc_ehr/resources/queries/study/Demographics_NotInMMA.sql b/onprc_ehr/resources/queries/study/Demographics_NotInMMA.sql new file mode 100644 index 000000000..76ec5bed2 --- /dev/null +++ b/onprc_ehr/resources/queries/study/Demographics_NotInMMA.sql @@ -0,0 +1,54 @@ +/* + Created by Kollil in Dec 2025 + Tkt # 13461 + Added two filters to the Demographics dataset: + 1. Filter out any animal with the following SNOMED Codes: + Begin active weight management regimen (P-YY961) + However, we would need to include animals that have this additional SNOMED Code if it's entered AFTER the one above + Release from active weight management regimen (P-YY960) + 2. Remove Shelters, Corral and Hospital locations from the lists + */ + +SELECT + d.Id.curlocation.area AS Area, + d.Id.curlocation.room AS Room, + d.Id.curlocation.cage AS Cage, + d.Id, + d.Id.utilization.use AS ProjectsAndGroups, + d.species, + d.geographic_origin, + d.gender AS Sex, + d.calculated_status, + d.birth, + d.Id.Age.YearAndDays, + d.Id.MostRecentWeight.MostRecentWeight, + d.Id.MostRecentWeight.MostRecentWeightDate, + d.Id.viral_status.viralStatus, + d.history +FROM Demographics d +WHERE d.Id.curlocation.area NOT IN ('Shelters', 'Corral', 'Hospital', 'Catch Area')-- Exclude animals from these locations + AND NOT (-- Exclude females under 5yrs, males under 7yrs + (d.gender.code = 'f' AND d.Id.age.ageInYears < 5) + OR (d.gender.code = 'm' AND d.Id.age.ageInYears < 7) + ) + AND NOT EXISTS ( + -- -- Find animals whose latest 'Weight MMA BEGIN' has no later 'Weight MMA RELEASE' + SELECT 1 + FROM study.WeightManagementMMAData b + WHERE b.Id = d.Id + AND b.code = 'P-YY961' + AND b.date = ( + SELECT MAX(b2.date) + FROM study.WeightManagementMMAData b2 + WHERE b2.Id = d.Id + AND b2.code = 'P-YY961' + ) + AND NOT EXISTS ( + SELECT 1 + FROM study.WeightManagementMMAData r + WHERE r.Id = d.Id + AND r.code = 'P-YY960' + AND r.date > b.date + ) +) + diff --git a/onprc_ehr/resources/queries/study/weightRelChange_NotInMMA.query.xml b/onprc_ehr/resources/queries/study/weightRelChange_NotInMMA.query.xml new file mode 100644 index 000000000..68bcb330c --- /dev/null +++ b/onprc_ehr/resources/queries/study/weightRelChange_NotInMMA.query.xml @@ -0,0 +1,37 @@ + + + + + Weight Change, Relative to Current Weight (Excluding the animals enrolled in MMA) + This query shows the percent change of each weight, relative to the current weight + + + true + true + + + false + + study + animal + id + + + + false + Prev Weight Date + + + Prev Weight (kg) + + + % Change Relative To Current + + + Abs Pct Change + + +
+
+
+
diff --git a/onprc_ehr/resources/queries/study/weightRelChange_NotInMMA.sql b/onprc_ehr/resources/queries/study/weightRelChange_NotInMMA.sql new file mode 100644 index 000000000..8afd087e5 --- /dev/null +++ b/onprc_ehr/resources/queries/study/weightRelChange_NotInMMA.sql @@ -0,0 +1,85 @@ +/* + Created by Kollil in Dec 2025 + Tkt # 13461 + Added two filters to the Demographics dataset: + 1. Filter out any animal with the following SNOMED Codes: + Begin active weight management regimen (P-YY961) + However, we would need to include animals that have this additional SNOMED Code if it's entered AFTER the one above + Release from active weight management regimen (P-YY960) + 2. Remove Shelters, Corral and Hospital locations from the lists + */ + +SELECT DISTINCT + l.lsid, + l.Id, + + l.date AS LatestWeightDate, + l.weight AS LatestWeight, + + p.date, -- AS PrevWeightDate + p.weight, -- AS PrevWeight + + timestampdiff('SQL_TSI_DAY', p.date, l.date) AS IntervalInDays, + age_in_months(p.date, l.date) AS IntervalInMonths, + + CASE + WHEN p.weight IS NOT NULL AND p.weight > 0 THEN + ROUND(((l.weight - p.weight) * 100 / p.weight), 1) + ELSE NULL + END AS PctChange, + + CASE + WHEN p.weight IS NOT NULL AND p.weight > 0 THEN + ABS(ROUND(((l.weight - p.weight) * 100 / p.weight), 1)) + ELSE NULL + END AS AbsPctChange, + + l.qcstate AS LatestQcState, + p.qcstate AS PrevQcState + +FROM + (SELECT Id, MAX(date) AS LatestDate + FROM study.weight + GROUP BY Id) lw + + JOIN study.weight l + ON l.Id = lw.Id + AND l.date = lw.LatestDate + + LEFT JOIN study.weight p + ON p.Id = lw.Id + AND p.date = ( + SELECT MAX(w2.date) + FROM study.weight w2 + WHERE w2.Id = lw.Id + AND w2.date <= timestampadd('SQL_TSI_DAY', -30, lw.LatestDate) + AND w2.date >= timestampadd('SQL_TSI_DAY', -100, lw.LatestDate) + ) + +WHERE l.Id.curlocation.area NOT IN ('Shelters', 'Corral', 'Hospital', 'Catch Area')-- Exclude animals from these locations + AND NOT (-- Exclude females under 5yrs, males under 7yrs + (l.Id.demographics.gender.code = 'f' AND l.Id.age.ageInYears < 5) + OR (l.Id.demographics.gender.code = 'm' AND l.Id.age.ageInYears < 7) + ) + AND l.qcstate.publicdata = true + AND NOT EXISTS ( + -- -- Find animals whose latest 'Weight MMA BEGIN' has no later 'Weight MMA RELEASE' + SELECT 1 + FROM study.WeightManagementMMAData b + WHERE b.Id = l.Id + AND b.code = 'P-YY961' + AND b.date = ( + SELECT MAX(b2.date) + FROM study.WeightManagementMMAData b2 + WHERE b2.Id = l.Id + AND b2.code = 'P-YY961' + ) + AND NOT EXISTS ( + SELECT 1 + FROM study.WeightManagementMMAData r + WHERE r.Id = l.Id + AND r.code = 'P-YY960' + AND r.date > b.date + ) +) + diff --git a/onprc_ehr/src/org/labkey/onprc_ehr/notification/WeightAlertsNotification.java b/onprc_ehr/src/org/labkey/onprc_ehr/notification/WeightAlertsNotification.java index b661a3a64..af03f795b 100644 --- a/onprc_ehr/src/org/labkey/onprc_ehr/notification/WeightAlertsNotification.java +++ b/onprc_ehr/src/org/labkey/onprc_ehr/notification/WeightAlertsNotification.java @@ -20,6 +20,7 @@ import org.labkey.api.data.ColumnInfo; import org.labkey.api.data.CompareType; import org.labkey.api.data.Container; +import org.labkey.api.data.ContainerFilter; import org.labkey.api.data.Results; import org.labkey.api.data.ResultsImpl; import org.labkey.api.data.Selector; @@ -33,6 +34,7 @@ import org.labkey.api.security.User; import org.labkey.api.settings.AppProps; import org.labkey.api.ehr.notification.AbstractEHRNotification; +import org.labkey.api.util.PageFlowUtil; import java.sql.ResultSet; import java.sql.SQLException; @@ -50,6 +52,10 @@ * User: bbimber * Date: 7/23/12 * Time: 7:41 PM + * + * Modified by Kollil as per ticket # 13461 + * Date: Jan 2026 + * */ public class WeightAlertsNotification extends AbstractEHRNotification { @@ -79,13 +85,13 @@ public String getEmailSubject(Container c) @Override public String getCronString() { - return "0 15 9 ? * MON"; - } + return "0 0 12 ? * THU"; + } //Made changes to the alert by Kollil, Refer to tkt # 13461 @Override public String getScheduleDescription() { - return "every Monday, at 9:15 AM"; + return "every Thursday, at 12pm"; } @Override @@ -97,10 +103,13 @@ public String getMessageBodyHTML(Container c, User u) Date now = new Date(); msg.append("This email contains alerts of significant weight changes. It was run on: " + getDateFormat(c).format(now) + " at " + _timeFormat.format(now) + ".

"); - getLivingWithoutWeight(c, u, msg); + /* + Changed by Kollil: 1/2026, tkt #13461 + 1. Delete the "animals do not have a weight" since that's captured on the colony management emails already. + */ + //getLivingWithoutWeight(c, u, msg); generateCombinedWeightTable(c, u, msg); - return msg.toString(); } @@ -110,68 +119,35 @@ private void generateCombinedWeightTable(final Container c, User u, final String //first weight drops Set dropDistinctIds = new HashSet<>(); - processWeights(c, u, sb, 0, 30, CompareType.LTE, -10, dropDistinctIds); - consecutiveWeightDrops(c, u, sb, dropDistinctIds); + processWeights(c, u, sb, 0, 100, CompareType.LTE, -10, dropDistinctIds); + /* + Changed by Kollil: 1/2026 + Disabling this section as per ticket #13461 + */ +// consecutiveWeightDrops(c, u, sb, dropDistinctIds); if (!dropDistinctIds.isEmpty()) { - String url = getExecuteQueryUrl(c, "study", "Demographics", "By Location") + "&query.calculated_status~eq=Alive&query.Id~in=" + (StringUtils.join(new ArrayList(dropDistinctIds), ";")); + String url = getExecuteQueryUrl(c, "study", "Demographics_NotInMMA", null ) + "&query.calculated_status~eq=Alive&query.Id~in=" + (StringUtils.join(new ArrayList(dropDistinctIds), ";")); sb.insert(0, "WARNING: There are " + dropDistinctIds.size() + " animals that experienced either a large weight loss, or 3 consecutive weight drops. Click here to view this list, or view the data below.


"); } //also weight gains Set gainDistinctIds = new HashSet<>(); - processWeights(c, u, sb, 0, 30, CompareType.GTE, 10, gainDistinctIds); + processWeights(c, u, sb, 0, 100, CompareType.GTE, 10, gainDistinctIds); if (!gainDistinctIds.isEmpty()) { - String url = getExecuteQueryUrl(c, "study", "Demographics", "By Location") + "&query.calculated_status~eq=Alive&query.Id~in=" + (StringUtils.join(new ArrayList(gainDistinctIds), ";")); + String url = getExecuteQueryUrl(c, "study", "Demographics_NotInMMA", null) + "&query.calculated_status~eq=Alive&query.Id~in=" + (StringUtils.join(new ArrayList(gainDistinctIds), ";")); sb.insert(0, "WARNING: There are " + gainDistinctIds.size() + " animals that experienced large weight gain (>10%). Click here to view this list, or view the data below.


"); } msg.append(sb); } - private void getLivingWithoutWeight(final Container c, User u, final StringBuilder msg) - { - SimpleFilter filter = new SimpleFilter(FieldKey.fromString("calculated_status"), "Alive"); - filter.addCondition(FieldKey.fromString("Id/MostRecentWeight/MostRecentWeightDate"), null, CompareType.ISBLANK); - Sort sort = new Sort(getStudy(c).getSubjectColumnName()); - - TableInfo ti = getStudySchema(c, u).getTable("Demographics"); - List colKeys = new ArrayList<>(); - colKeys.add(FieldKey.fromString(getStudy(c).getSubjectColumnName())); - colKeys.add(FieldKey.fromString("Id/age/AgeFriendly")); - final Map columns = QueryService.get().getColumns(ti, colKeys); - - TableSelector ts = new TableSelector(ti, columns.values(), filter, sort); - if (ts.exists()) - { - msg.append("WARNING: The animals listed below do not have a weight.\n"); - msg.append(" Click here to view these animals

\n"); - - ts.forEach(new TableSelector.ForEachBlock<>() - { - @Override - public void exec(ResultSet rs) throws SQLException - { - Results results = new ResultsImpl(rs, columns); - msg.append(rs.getString(getStudy(c).getSubjectColumnName())); - String age = results.getString(FieldKey.fromString("Id/age/AgeFriendly")); - if (age != null) - msg.append(" (Age: " + age + ")"); - - msg.append("
\n"); - } - }); - - msg.append("
\n"); - } - } - private void processWeights(Container c, User u, final StringBuilder msg, int min, int max, CompareType ct, double pct, @Nullable Set distinctIds) { - TableInfo ti = getStudySchema(c, u).getTable("weightRelChange"); + TableInfo ti = QueryService.get().getUserSchema(u, c, "study").getTable("weightRelChange_NotInMMA", ContainerFilter.Type.AllFolders.create(c, u)); assert ti != null; final FieldKey areaKey = FieldKey.fromString("Id/curLocation/Area"); @@ -180,7 +156,7 @@ private void processWeights(Container c, User u, final StringBuilder msg, int mi final FieldKey ageKey = FieldKey.fromString("Id/age/AgeFriendly"); final FieldKey problemKey = FieldKey.fromString("Id/openProblems/problems"); final FieldKey investKey = FieldKey.fromString("Id/activeAssignments/investigators"); - final FieldKey vetsKey = FieldKey.fromString("Id/activeAssignments/vets"); + final FieldKey vetsKey = FieldKey.fromString("Id/assignedVet/AssignedVet"); final FieldKey peKey = FieldKey.fromString("Id/physicalExamHistory/daysSinceExam"); List colKeys = new ArrayList<>(); @@ -206,21 +182,37 @@ private void processWeights(Container c, User u, final StringBuilder msg, int mi filter.addCondition(FieldKey.fromString("IntervalInDays"), max, CompareType.LTE); Calendar date = Calendar.getInstance(); - date.add(Calendar.DATE, -30); + date.add(Calendar.DATE, -100); /// change to last 100 days filter.addCondition(FieldKey.fromString("LatestWeightDate"), getDateFormat(c).format(date.getTime()), CompareType.DATE_GTE); - TableSelector ts = new TableSelector(ti, columns.values(), filter, null); +// TableSelector ts = new TableSelector(ti, columns.values(), filter, null); + //Added by Kollill to avoid the duplicate rows + Sort sort = new Sort(); + sort.appendSortColumn(FieldKey.fromString("Id"), Sort.SortDirection.ASC, false); + sort.appendSortColumn(FieldKey.fromString("LatestWeightDate"), Sort.SortDirection.DESC, false); + sort.appendSortColumn(FieldKey.fromString("date"), Sort.SortDirection.DESC, false); + + TableSelector ts = new TableSelector(ti, columns.values(), filter, sort); msg.append("Weights since " + getDateFormat(c).format(date.getTime()) + " representing changes of " + (pct > 0 ? "+" : "") + pct + "% in the past " + max + " days:

"); final Set distinctAnimals = new HashSet<>(); final Map>>> summary = new TreeMap<>(); + final Set seenIds = new HashSet<>(); ///Added by Kollil to avoid duplicates ts.forEach(new Selector.ForEachBlock<>() { @Override public void exec(ResultSet object) throws SQLException { Results rs = new ResultsImpl(object, columns); + String id = rs.getString("Id"); //Added by Kollil + if (id == null) + return; + + // keep only the first (most recent) row per Id because of the sort above + if (!seenIds.add(id)) + return; + String area = rs.getString(areaKey) == null ? "" : rs.getString(areaKey); Map>> areaMap = summary.get(area); if (areaMap == null) @@ -256,20 +248,20 @@ public void exec(ResultSet object) throws SQLException roomList.add(rowMap); - distinctAnimals.add(rs.getString("Id")); + //distinctAnimals.add(rs.getString("Id")); //deleted by Kollil + distinctAnimals.add(id); } }); if (!summary.isEmpty()) { - msg.append("

Click here to view these " + distinctAnimals.size() + " animals

\n"); + msg.append("

Click here to view these " + distinctAnimals.size() + " animals

\n"); msg.append(""); for (String area : summary.keySet()) { Map>> areaValue = summary.get(area); - for (String room : areaValue.keySet()) - { + for (String room : areaValue.keySet()) { List> roomValue = areaValue.get(room); for (Map map : roomValue) { @@ -297,7 +289,14 @@ public void exec(ResultSet object) throws SQLException msg.append(""); - msg.append(""); + double pct_ch = ((Number) map.get("PctChange")).doubleValue(); + if (Math.abs(pct_ch) >= 15.0) { // highlight in yellow if <= -15 OR >= +15 + msg.append(""); + } + else { + msg.append(""); + } msg.append(""); } } @@ -305,8 +304,7 @@ public void exec(ResultSet object) throws SQLException msg.append("
AreaRoomCageIdInvestigatorsResponsible VetOpen ProblemsDays Since Last PEWeight DatesDays BetweenWeight (kg)Percent Change
").append(map.get("LatestWeight")).append("
"); msg.append(map.get("weight")).append("
").append(map.get("PctChange")).append("") + .append(map.get("PctChange")).append("").append(map.get("PctChange")).append("

\n"); msg.append("


"); } - else - { + else { msg.append("There are no changes during this period.
"); } @@ -314,6 +312,7 @@ public void exec(ResultSet object) throws SQLException distinctIds.addAll(distinctAnimals); } + protected void consecutiveWeightDrops(final Container c, User u, final StringBuilder msg, @Nullable Set distinctIds) { SimpleFilter filter = new SimpleFilter(FieldKey.fromString("Id/dataset/demographics/calculated_status"), "Alive"); @@ -326,7 +325,7 @@ protected void consecutiveWeightDrops(final Container c, User u, final StringBui sort.appendSortColumn(new Sort.SortField(FieldKey.fromString("Id/curLocation/room"), Sort.SortDirection.ASC)); sort.appendSortColumn(new Sort.SortField(FieldKey.fromString("Id/curLocation/cage"), Sort.SortDirection.ASC)); - TableInfo ti = getStudySchema(c, u).getTable("weightConsecutiveDrops"); + TableInfo ti = getStudySchema(c, u).getTable("weightConsecutiveDrops_NotInMMA"); assert ti != null; List colKeys = new ArrayList<>(); @@ -404,7 +403,7 @@ public void exec(ResultSet rs) throws SQLException tableMsg.append("\n"); - msg.append("

Click here to view these " + animalIds.size() + " animals

\n"); + msg.append("

Click here to view these " + animalIds.size() + " animals

\n"); msg.append(tableMsg); msg.append("
\n"); @@ -413,6 +412,43 @@ public void exec(ResultSet rs) throws SQLException } } + // private void getLivingWithoutWeight(final Container c, User u, final StringBuilder msg) +// { +// SimpleFilter filter = new SimpleFilter(FieldKey.fromString("calculated_status"), "Alive"); +// filter.addCondition(FieldKey.fromString("Id/MostRecentWeight/MostRecentWeightDate"), null, CompareType.ISBLANK); +// Sort sort = new Sort(getStudy(c).getSubjectColumnName()); +// +// TableInfo ti = getStudySchema(c, u).getTable("Demographics"); +// List colKeys = new ArrayList<>(); +// colKeys.add(FieldKey.fromString(getStudy(c).getSubjectColumnName())); +// colKeys.add(FieldKey.fromString("Id/age/AgeFriendly")); +// final Map columns = QueryService.get().getColumns(ti, colKeys); +// +// TableSelector ts = new TableSelector(ti, columns.values(), filter, sort); +// if (ts.exists()) +// { +// msg.append("WARNING: The animals listed below do not have a weight.\n"); +// msg.append(" Click here to view these animals

\n"); +// +// ts.forEach(new TableSelector.ForEachBlock<>() +// { +// @Override +// public void exec(ResultSet rs) throws SQLException +// { +// Results results = new ResultsImpl(rs, columns); +// msg.append(rs.getString(getStudy(c).getSubjectColumnName())); +// String age = results.getString(FieldKey.fromString("Id/age/AgeFriendly")); +// if (age != null) +// msg.append(" (Age: " + age + ")"); +// +// msg.append("
\n"); +// } +// }); +// +// msg.append("
\n"); +// } +// } + private String getValue(Results rs, String prop) throws SQLException { String val = rs.getString(FieldKey.fromString(prop));