Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion nbri_ehr/resources/queries/nbri_ehr/Conception.js
Original file line number Diff line number Diff line change
@@ -1 +1,45 @@
require("ehr/triggers").initScript(this);
/*
* Copyright (c) 2026 LabKey Corporation
*
* Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
*/
require("ehr/triggers").initScript(this);

var triggerHelper = new org.labkey.nbri_ehr.query.NBRI_EHRTriggerHelper(LABKEY.Security.currentUser.id, LABKEY.Security.currentContainer.id);

// the shared trigger collects modified participants from row.Id, which this table does not have, so announce the dams
// here or their cached activeConceptions keeps a stale Pregnant value
var damsModified = [];

function addDam(dam) {
if (dam && damsModified.indexOf(dam) === -1) {
damsModified.push(dam);
}
}

EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.INIT, 'nbri_ehr', 'Conception', function(event, helper){
// the script scope can outlive a single save, so never inherit dams from a prior one
damsModified = [];
});

EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.BEFORE_UPSERT, 'nbri_ehr', 'Conception', function(helper, scriptErrors, row, oldRow) {
if (helper.isValidateOnly())
return;

addDam(row.Dam);

// a re-pointed conception frees the dam it used to belong to
addDam(oldRow ? oldRow.Dam : null);
});

EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.BEFORE_DELETE, 'nbri_ehr', 'Conception', function(helper, scriptErrors, row) {
// the row LabKey passes for a delete can carry keys only, and the record is still readable at this point
addDam(row.Dam || triggerHelper.getConceptionDam(row.ConceptId));
});

EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.COMPLETE, 'nbri_ehr', 'Conception', function(event, errors, helper){
if (damsModified.length) {
triggerHelper.reportDataChange('nbri_ehr', 'Conception', damsModified);
damsModified = [];
}
});
1 change: 1 addition & 0 deletions nbri_ehr/resources/queries/nbri_ehr/Conception.query.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<metadata>
<tables xmlns="http://labkey.org/data/xml">
<table tableName="Conception" tableDbType="TABLE" useColumnOrder="true">
<javaCustomizer class="org.labkey.nbri_ehr.table.NBRI_EHRCustomizer"/>
<tableTitle>Conception Records</tableTitle>
<columns>
<column columnName="rowId">
Expand Down
12 changes: 7 additions & 5 deletions nbri_ehr/resources/queries/nbri_ehr/ConceptionsByDam.sql
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,19 @@ SELECT
c.ConceptDate,
c.Estimated,
c.Sire,
c.isActive,
CASE
WHEN c.isActive = true THEN 'Unknown'
WHEN b.conceptId IS NOT NULL THEN 'Live Birth'
WHEN po.conceptId IS NOT NULL THEN COALESCE(po.result, 'Unknown')
ELSE 'Unknown'
ELSE COALESCE(po.result, 'Unknown')
END AS conceptionOutcome,
b.offspring,
c.Remark,
c.QCState AS qcstate
FROM Conception c
-- a conception yields at most one birth; the aggregate only guards against duplicates the birth trigger warns about but does not block
LEFT JOIN (SELECT b.conceptId, MAX(b.Id) AS offspring FROM study.birth b WHERE b.conceptId IS NOT NULL GROUP BY b.conceptId) b
-- Both joins match isActive: a record claims its conception unless its QC state is explicitly non-public, so a null state counts as public.
-- The birth trigger blocks a duplicate conceptId, but ETL imports skip that check, so the aggregate guards against one.
LEFT JOIN (SELECT b.conceptId, MAX(b.Id) AS offspring FROM study.birth b WHERE b.conceptId IS NOT NULL AND (b.qcstate IS NULL OR b.qcstate.publicdata = true) GROUP BY b.conceptId) b
ON b.conceptId = c.ConceptId
LEFT JOIN (SELECT p.conceptId, MAX(p.result.title) AS result FROM study.pregnancy p WHERE p.conceptId IS NOT NULL GROUP BY p.conceptId) po
LEFT JOIN (SELECT p.conceptId, MAX(p.result.title) AS result FROM study.pregnancy p WHERE p.conceptId IS NOT NULL AND (p.qcstate IS NULL OR p.qcstate.publicdata = true) GROUP BY p.conceptId) po
ON po.conceptId = c.ConceptId
11 changes: 11 additions & 0 deletions nbri_ehr/resources/queries/study/activeConceptions.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* Copyright (c) 2026 LabKey Corporation
*
* Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
*/
SELECT
c.Dam AS Id,
c.ConceptId,
c.ConceptDate
FROM nbri_ehr.Conception c
WHERE c.isActive = true AND c.Dam IS NOT NULL
33 changes: 33 additions & 0 deletions nbri_ehr/resources/queries/study/birth.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ EHR.Server.Utils = require("ehr/utils").EHR.Server.Utils;
var triggerHelper = new org.labkey.nbri_ehr.query.NBRI_EHRTriggerHelper(LABKEY.Security.currentUser.id, LABKEY.Security.currentContainer.id);
var idsToSync = [];

// dams whose conception this save closes. The birth row announces the newborn, so the dam's cached Pregnant value
// would otherwise keep listing a conception that is no longer open.
var damsToSync = [];

// conception ids claimed by the rows of this save that have already been validated. Rows entered together are not in
// study.birth yet when each one is checked, so this is the only way the one-birth-per-conception rule can see them.
var conceptIdsInSave = [];
Expand All @@ -34,6 +38,17 @@ function createAssignment(scriptErrors, dataset, fieldName, value, row) {
}
}

// resolves the dam of a conception so the birth can announce her; the conception carries the dam, the birth row does not
function addConceptionDam(conceptId) {
if (!conceptId)
return;

var dam = triggerHelper.getConceptionDam(conceptId);
if (dam && damsToSync.indexOf(dam) === -1) {
damsToSync.push(dam);
}
}

function onInit(event, helper){
helper.setScriptOptions({
allowAnyId: true,
Expand All @@ -50,6 +65,7 @@ function onInit(event, helper){
// the script scope can outlive a single save, so never inherit ids from a prior one
idsToSync = [];
conceptIdsInSave = [];
damsToSync = [];

helper.decodeExtraContextProperty('birthsInTransaction');
}
Expand All @@ -65,6 +81,16 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even
}
idsToSync = [];
}

if (damsToSync.length) {
triggerHelper.reportDataChange('nbri_ehr', 'Conception', damsToSync);
damsToSync = [];
}
});

EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.BEFORE_DELETE, 'study', 'birth', function(helper, scriptErrors, row) {
// deleting the birth reopens its conception
addConceptionDam(row.conceptId);
});

EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.BEFORE_UPSERT, 'study', 'birth', function(helper, scriptErrors, row, oldRow) {
Expand Down Expand Up @@ -96,6 +122,13 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even
}
}

if (!helper.isETL() && !helper.isValidateOnly()) {
addConceptionDam(row.conceptId);

// a re-pointed birth reopens the conception it used to claim
addConceptionDam(oldRow ? oldRow.conceptId : null);
}

if (!helper.isETL()) {

if (row.QCStateLabel) {
Expand Down
42 changes: 41 additions & 1 deletion nbri_ehr/resources/queries/study/pregnancy.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,26 @@ EHR.Server.Utils = require("ehr/utils").EHR.Server.Utils;

var triggerHelper = new org.labkey.nbri_ehr.query.NBRI_EHRTriggerHelper(LABKEY.Security.currentUser.id, LABKEY.Security.currentContainer.id);

// dams whose conception this save claims or frees. The outcome announces its own Id, which is the dam only when the
// record was entered against her, so resolve the dam from the conception instead of trusting row.Id.
var damsToSync = [];

// resolves the dam of a conception so the outcome can announce her; the conception carries the dam, the outcome row does not
function addConceptionDam(conceptId) {
if (!conceptId)
return;

var dam = triggerHelper.getConceptionDam(conceptId);
if (dam && damsToSync.indexOf(dam) === -1) {
damsToSync.push(dam);
}
}

EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.INIT, 'study', 'pregnancy', function(event, helper){
// the script scope can outlive a single save, so never inherit dams from a prior one
damsToSync = [];
});

EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.BEFORE_UPSERT, 'study', 'pregnancy', function(helper, scriptErrors, row, oldRow) {
if (!helper.isETL() && row.conceptId) {
if (triggerHelper.totalRecords('nbri_ehr', 'Conception', 'ConceptId', row.conceptId) === 0) {
Expand All @@ -24,6 +44,26 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even
EHR.Server.Utils.addError(scriptErrors, 'conceptId', 'This conception Id is already used by a birth record', 'INFO');
}
}

// validation never reaches COMPLETE, so resolving dams during it is a wasted query per row
if (!helper.isETL() && !helper.isValidateOnly()) {
addConceptionDam(row.conceptId);

// a re-pointed or cleared outcome reopens the conception it used to claim
addConceptionDam(oldRow ? oldRow.conceptId : null);
}
});

EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.BEFORE_DELETE, 'study', 'pregnancy', function(helper, scriptErrors, row) {
// deleting the outcome reopens its conception
addConceptionDam(row.conceptId);
});

EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.COMPLETE, 'study', 'pregnancy', function(event, errors, helper){
if (damsToSync.length) {
triggerHelper.reportDataChange('nbri_ehr', 'Conception', damsToSync);
damsToSync = [];
}
});

EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.ON_BECOME_PUBLIC, 'study', 'pregnancy', function(scriptErrors, helper, row, oldRow) {
Expand All @@ -36,4 +76,4 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even
}
triggerHelper.sendPregnancyOutcomeNotification(row.Id, outcomeRec);
}
});
});
3 changes: 3 additions & 0 deletions nbri_ehr/resources/web/nbri_ehr/panel/AnimalDetailsPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,9 @@ Ext4.define('NBRI_EHR.panel.AnimalDetailsPanel', {
},{
fieldLabel: 'Cagemates',
name: 'cagemates'
},{
fieldLabel: 'Pregnant',
name: 'pregnant'
},{
fieldLabel: 'Weight',
name: 'weights'
Expand Down
36 changes: 32 additions & 4 deletions nbri_ehr/resources/web/nbri_ehr/panel/SnapshotPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,6 @@ Ext4.define('NBRI_EHR.panel.SnapshotPanel', {
xtype: 'displayfield',
fieldLabel: 'Source',
name: 'source'
},{
xtype: 'displayfield',
fieldLabel: 'Prev Id',
name: 'prev_id'
}]
},{
xtype: 'container',
Expand Down Expand Up @@ -133,6 +129,10 @@ Ext4.define('NBRI_EHR.panel.SnapshotPanel', {
xtype: 'displayfield',
fieldLabel: 'Last TB',
name: 'lastTB'
},{
xtype: 'displayfield',
fieldLabel: 'Pregnant',
name: 'pregnant'
},{
xtype: 'displayfield',
fieldLabel: 'Weights',
Expand Down Expand Up @@ -391,4 +391,32 @@ Ext4.define('NBRI_EHR.panel.SnapshotPanel', {
toSet['parents'] = 'No data';
}
},

appendDataResults: function(toSet, results, id){
this.callParent(arguments);
this.appendPregnancy(toSet, results);
},

appendPregnancy: function(toSet, results){
var records = results ? results.getData()['activeConceptions'] : null;
// getEHRContext returns null when the study container property is unset; fall back to the current container
var ctx = EHR.Utils.getEHRContext() || {};
var values = [];

if (Ext4.isArray(records)){
Ext4.each(records, function(record){
var conceptId = record['ConceptId'];
if (conceptId){
var url = LABKEY.ActionURL.buildURL('query', 'executeQuery', ctx['EHRStudyContainer'], {
schemaName: 'nbri_ehr',
'query.queryName': 'Conception',
'query.ConceptId~eq': conceptId
});
values.push('<a href="' + url + '" target="_blank">' + LABKEY.Utils.encodeHtml(conceptId) + '</a>');
}
}, this);
}

toSet['pregnant'] = values.length ? values.join('<br>') : 'No';
},
});
2 changes: 2 additions & 0 deletions nbri_ehr/src/org/labkey/nbri_ehr/NBRI_EHRModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
import org.labkey.nbri_ehr.dataentry.form.*;
import org.labkey.nbri_ehr.demographics.ActiveAssignmentsDemographicsProvider;
import org.labkey.nbri_ehr.demographics.ActiveCasesDemographicsProvider;
import org.labkey.nbri_ehr.demographics.ActiveConceptionsDemographicsProvider;
import org.labkey.nbri_ehr.demographics.ActiveFlagsDemographicsProvider;
import org.labkey.nbri_ehr.demographics.ActiveTreatmentsDemographicsProvider;
import org.labkey.nbri_ehr.demographics.CagematesDemographicsProvider;
Expand Down Expand Up @@ -135,6 +136,7 @@ protected void doStartupAfterSpringConfig(ModuleContext moduleContext)
ehrService.registerDemographicsProvider(new ActiveTreatmentsDemographicsProvider(this));
ehrService.registerDemographicsProvider(new SourceDemographicsProvider(this));
ehrService.registerDemographicsProvider(new NecropsyStatusDemographicsProvider(this));
ehrService.registerDemographicsProvider(new ActiveConceptionsDemographicsProvider(this));

EHRService.get().registerHistoryDataSource(new AnimalGroupsDataSource(this));
EHRService.get().registerHistoryDataSource(new AnimalGroupsEndDataSource(this));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright (c) 2026 LabKey Corporation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.labkey.nbri_ehr.demographics;

import org.labkey.api.data.Sort;
import org.labkey.api.ehr.demographics.AbstractListDemographicsProvider;
import org.labkey.api.module.Module;
import org.labkey.api.query.FieldKey;

import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

public class ActiveConceptionsDemographicsProvider extends AbstractListDemographicsProvider
{
public ActiveConceptionsDemographicsProvider(Module module)
{
super(module, "study", "activeConceptions", "activeConceptions");
// isActive already excludes non-public conceptions, and the query exposes no QCState column for the inherited filter to use
_supportsQCState = false;
}

@Override
public boolean requiresRecalc(String schema, String query)
{
return ("study".equalsIgnoreCase(schema) && ("birth".equalsIgnoreCase(query) || "pregnancy".equalsIgnoreCase(query))) ||
("nbri_ehr".equalsIgnoreCase(schema) && "Conception".equalsIgnoreCase(query));
}

@Override
protected Collection<FieldKey> getFieldKeys()
{
Set<FieldKey> keys = new HashSet<>();
keys.add(FieldKey.fromString("Id"));
keys.add(FieldKey.fromString("ConceptId"));
keys.add(FieldKey.fromString("ConceptDate"));

return keys;
}

@Override
protected Sort getSort()
{
return new Sort("-ConceptDate");
}
}
12 changes: 12 additions & 0 deletions nbri_ehr/src/org/labkey/nbri_ehr/query/NBRI_EHRTriggerHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -829,6 +829,18 @@ public long totalRecords(String schemaName, String queryName, String columnName,
return ts.getRowCount();
}

// The Conception table has no Id column, so its trigger cannot announce a modified participant on its own
public String getConceptionDam(String conceptId)
{
if (conceptId == null)
return null;

TableInfo ti = getTableInfo("nbri_ehr", "Conception");
SimpleFilter filter = new SimpleFilter(FieldKey.fromString("ConceptId"), conceptId);

return new TableSelector(ti, Collections.singleton("Dam"), filter, null).getObject(String.class);
}

public boolean canCloseCase()
{
return _container.hasPermission(_user, EHRVeterinarianPermission.class);
Expand Down
Loading