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
6 changes: 0 additions & 6 deletions nbri_ehr/resources/data/death_reason.tsv

This file was deleted.

18 changes: 9 additions & 9 deletions nbri_ehr/resources/data/death_type.tsv
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
value title sort_order
A Experimental 1
value title sort_order date_disabled
A Experimental 1 2026-09-03
D Spontaneous/Normal 2
F Fetal 3
FD Fetal Death 4
FL Fetal Live 5
FN Fetal found at necropsy 6
FX Fetal experimental 7
F Fetal 3 2026-09-03
FD Fetal Death 4 2026-09-03
FL Fetal Live 5 2026-09-03
FN Fetal found at necropsy 6 2026-09-03
FX Fetal experimental 7 2026-09-03
K Cull (scheduled) 8
M Medical cull (non-scheduled) 9
ND Non-vaginal (C-section) dead 10
NT Not pregnant at assessment 11
ND Non-vaginal (C-section) dead 10 2026-09-03
NT Not pregnant at assessment 11 2026-09-03
S Cull 12
X Experimental 13
1 change: 0 additions & 1 deletion nbri_ehr/resources/data/editable_lookups.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ ehr_lookups country Colony Management Country
ehr_lookups country_category Colony Management Country Category
ehr_lookups daily_enrich_codes Behavior Daily enrichment codes.
ehr_lookups data_category Clinical Data Categories Used in datasets.
ehr_lookups death_reason Colony Management Death Reason
ehr_lookups death_type Colony Management Death Type Death type codes.
ehr_lookups delivery_mode Colony Management Delivery Mode
ehr_lookups delivery_state Colony Management Delivery State
Expand Down
1 change: 0 additions & 1 deletion nbri_ehr/resources/data/lookup_sets.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ country Country value title
country_category Country Category value title
daily_enrich_codes Daily Enrichment Codes value
data_category Data Category Field Values value
death_reason Death Reason value
death_type Death Type value title
delivery_mode Delivery Mode value title
delivery_state Delivery State value title
Expand Down
1 change: 0 additions & 1 deletion nbri_ehr/resources/data/lookupsManifest.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ country
country_category
daily_enrich_codes
data_category
death_reason
death_type
delivery_mode
delivery_state
Expand Down
1 change: 0 additions & 1 deletion nbri_ehr/resources/data/lookupsManifestTest.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ country
country_category
daily_enrich_codes
data_category
death_reason
death_type
delivery_mode
delivery_state
Expand Down
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
<column columnName="Id"/>
<column columnName="date">
<columnTitle>Date Added</columnTitle>
<formatString>DateTime</formatString>
</column>
<column columnName="enddate">
<columnTitle>Date Removed</columnTitle>
<formatString>DateTime</formatString>
<isHidden>false</isHidden>
</column>
<column columnName="groupId">
Expand Down
17 changes: 17 additions & 0 deletions nbri_ehr/resources/queries/study/arrival.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ require("ehr/triggers").initScript(this);
var triggerHelper = new org.labkey.nbri_ehr.query.NBRI_EHRTriggerHelper(LABKEY.Security.currentUser.id, LABKEY.Security.currentContainer.id);
var idsToSync = [];

// generation 0 is a real value, so emptiness cannot be tested by truthiness the way the other demographics fields test it
function isBlankGeneration(value) {
return value === null || value === undefined || value === '';
}

// opens one assignment record against the animal being entered; each dataset carries the assignment under its own field
function createAssignment(scriptErrors, dataset, fieldName, value, row) {
if (!value)
Expand Down Expand Up @@ -46,6 +51,11 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even
EHR.Server.Utils.addError(scriptErrors, 'Id', 'Animal Id ' + row.Id + ' is already in use. Please use a different Id.', 'ERROR');
}

// only a form entry can be held to a generation: the form seeds it to 0, while a study import has no such column to carry
if (!row.rearrival && !helper.isETL() && helper.isEHRDataEntry() && helper.getEvent() == 'insert' && isBlankGeneration(row['Id/demographics/generation'])) {
EHR.Server.Utils.addError(scriptErrors, 'Id/demographics/generation', 'Generation is required', 'ERROR');
}

if (row.eventDate) {
row.date = row.eventDate;
}
Expand All @@ -69,6 +79,7 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even
row.gender = row['Id/demographics/gender'] || null;
row.geographic_origin = row['Id/demographics/geographic_origin'] || null;
row.socialCode = row['Id/demographics/socialCode'] || null;
row.generation = isBlankGeneration(row['Id/demographics/generation']) ? null : parseInt(row['Id/demographics/generation'], 10);

if (row.QCStateLabel) {
row.qcstate = helper.getJavaHelper().getQCStateForLabel(row.QCStateLabel).getRowId();
Expand Down Expand Up @@ -152,6 +163,12 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even
hasUpdates = true;
}

if (!isBlankGeneration(row.generation) && row.generation !== data.generation)
{
obj.generation = row.generation;
hasUpdates = true;
}

if (row.sire && row.sire !== data.sire)
{
obj.sire = row.sire;
Expand Down
1 change: 1 addition & 0 deletions nbri_ehr/resources/queries/study/arrival.query.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<column columnName="Id"/>
<column columnName="date">
<columnTitle>Arrival Date</columnTitle>
<formatString>DateTime</formatString>
</column>
<column columnName="arrivalType">
<columnTitle>Arrival Type</columnTitle>
Expand Down
4 changes: 3 additions & 1 deletion nbri_ehr/resources/queries/study/assignment.query.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
<column columnName="date">
<formatString>DateTime</formatString>
</column>
<column columnName="endDate"/>
<column columnName="endDate">
<formatString>DateTime</formatString>
</column>
<column columnName="project">
<fk>
<fkDbSchema>ehr</fkDbSchema>
Expand Down
58 changes: 57 additions & 1 deletion nbri_ehr/resources/queries/study/birth.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,19 @@ 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 = [];

// generation 0 is a real value, so emptiness cannot be tested by truthiness the way the other demographics fields test it
function isBlankGeneration(value) {
return value === null || value === undefined || value === '';
}

// opens one assignment record against the animal being entered; each dataset carries the assignment under its own field
function createAssignment(scriptErrors, dataset, fieldName, value, row) {
if (!value)
Expand All @@ -34,6 +43,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 @@ -43,13 +63,13 @@ function onInit(event, helper){
skipHousingCheck: true,
announceAllModifiedParticipants: true,
allowDatesInDistantPast: true,
removeTimeFromDate: true,
skipAssignmentCheck: true,
});

// 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 +85,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 +126,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 Expand Up @@ -146,6 +183,7 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even
birth: row.date || null,
gender: row['Id/demographics/gender'] || null,
socialCode: row['Id/demographics/socialCode'] || null,
generation: isBlankGeneration(row['Id/demographics/generation']) ? null : parseInt(row['Id/demographics/generation'], 10),
taskid: row.taskid,
remark: row.remark,
QCStateLabel: row.QCStateLabel,
Expand All @@ -161,6 +199,19 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even
obj.species = helper.getJavaHelper().getSpecies(obj.dam);
}

// the conception window fills this in, so a blank one means a save that bypassed the form
if (isBlankGeneration(obj.generation)) {
var damGeneration = obj.dam ? triggerHelper.getGeneration(obj.dam) : null;
if (damGeneration === null) {
var generationWarning = obj.dam
? 'No generation is recorded for dam ' + obj.dam + ', so this birth was recorded as generation 1'
: 'This birth record has no dam, so it was recorded as generation 1';
EHR.Server.Utils.addError(scriptErrors, 'Id/demographics/generation', generationWarning, 'WARN');
}

obj.generation = (damGeneration === null ? 0 : damGeneration) + 1;
}

if (!oldRow) {
//if not already present, we insert into demographics
helper.getJavaHelper().createDemographicsRecord(row.Id, obj, extraDemographicsFieldMappings);
Expand Down Expand Up @@ -198,6 +249,11 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even
hasUpdates = true;
}

if (!isBlankGeneration(obj.generation) && obj.generation !== data.generation) {
record.generation = obj.generation;
hasUpdates = true;
}

if (obj.performedby && obj.performedby !== data.performedby) {
record.performedby = obj.performedby;
hasUpdates = true;
Expand Down
1 change: 1 addition & 0 deletions nbri_ehr/resources/queries/study/birth.query.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<column columnName="Id" />
<column columnName="date">
<columnTitle>Birth Date</columnTitle>
<formatString>DateTime</formatString>
</column>
<column columnName="conceptId">
<columnTitle>Conception Id</columnTitle>
Expand Down
7 changes: 7 additions & 0 deletions nbri_ehr/resources/queries/study/clinical_observations.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ function onUpsert(helper, scriptErrors, row, oldRow) {
EHR.Server.Utils.addError(scriptErrors, 'remark', "You selected 'Yes' for " + row.category + ", please explain in the Remark", "WARN");
}

// Always derive the type from the observation type's category rather than trusting the incoming value.
// The Observations form leaves it blank because it offers every type; the other forms set it explicitly,
// but their type pickers are filtered to the categories that agree with the value they set, so deriving
// here gives them the same answer. Deriving unconditionally also re-derives when a re-opened draft or a
// saved template carries a type left over from a different category.
row.type = triggerHelper.getObservationTypeCategory(row.category) === 'Behavior' ? 'Behavior' : 'Clinical';

// Handle scheduled observations
if (!helper.isValidateOnly() && row.scheduledDate) {
var qc;
Expand Down
3 changes: 1 addition & 2 deletions nbri_ehr/resources/queries/study/deathNotification.sql
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,5 @@ SELECT
Id,
date,
taskid,
performedBy.DisplayName AS performedBy,
reason.title AS reason
performedBy.DisplayName AS performedBy
FROM study.deaths
Loading
Loading