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
14 changes: 14 additions & 0 deletions nbri_ehr/resources/data/death_type.tsv
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
value title sort_order
A Experimental 1
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
K Cull (scheduled) 8
M Medical cull (non-scheduled) 9
ND Non-vaginal (C-section) dead 10
NT Not pregnant at assessment 11
S Cull 12
X Experimental 13
5 changes: 3 additions & 2 deletions nbri_ehr/resources/data/editable_lookups.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,10 @@ 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_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
ehr_lookups delivery_state Colony Management Delivery State
ehr_lookups dental_obs Clinical Dental Observation Types Clinical observation values.
ehr_lookups derm_obs Clinical Dermatologic Observation Types Clinical observation values.
ehr_lookups digit_amputation Clinical Digit Amputation Clinical observation fixed values.
Expand Down
1 change: 1 addition & 0 deletions nbri_ehr/resources/data/lookup_sets.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ 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
dental_obs Dental Observations value
Expand Down
1 change: 1 addition & 0 deletions nbri_ehr/resources/data/lookupsManifest.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ country_category
daily_enrich_codes
data_category
death_reason
death_type
delivery_mode
delivery_state
dental_obs
Expand Down
1 change: 1 addition & 0 deletions nbri_ehr/resources/data/lookupsManifestTest.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ country_category
daily_enrich_codes
data_category
death_reason
death_type
delivery_mode
delivery_state
dental_obs
Expand Down
3 changes: 1 addition & 2 deletions nbri_ehr/resources/queries/study/aliases.sql
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,4 @@ FROM nbri_ehr.IdHistory
UNION
SELECT Id,
Alias as alias
FROM study.alias where Id.demographics.calculated_status != 'Alive - In Progress'
'
FROM study.alias where Id.demographics.calculated_status != 'Alive - In Progress'
91 changes: 57 additions & 34 deletions nbri_ehr/resources/queries/study/deaths.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
require("ehr/triggers").initScript(this);

var triggerHelper = new org.labkey.nbri_ehr.query.NBRI_EHRTriggerHelper(LABKEY.Security.currentUser.id, LABKEY.Security.currentContainer.id);
var validIds = [];
var idMap = {};
var deathIdMap = {};

Expand All @@ -26,9 +25,7 @@ function onInit(event, helper){
return;

for(var i=0; i < results.rows.length; i++) {
validIds.push(results.rows[i]["Id"]["value"])
idMap[results.rows[i]["Id"]["value"]] = {calculated_status: results.rows[i]["calculated_status"]["value"], QCStateLabel: results.rows[i]["QCState/Label"]["value"]};
// console.log(idMap[results.rows[i]["Id"]["value"]]);
}
},
failure: function (error) {
Expand Down Expand Up @@ -84,37 +81,56 @@ function onUpsert(helper, scriptErrors, row, oldRow) {
//only allow death record to be created if the animal is in the demographics table
if (idMap[row.Id]) {

// deathIdMap has no entry for the animal on initial import, and any of these values can be null
var status = idMap[row.Id].calculated_status ? idMap[row.Id].calculated_status.toUpperCase() : null;
var priorDeathQCState = deathIdMap[row.Id] && deathIdMap[row.Id].QCStateLabel ? deathIdMap[row.Id].QCStateLabel.toUpperCase() : null;
var rowQCState = row.QCStateLabel ? row.QCStateLabel.toUpperCase() : null;

// deathIdMap is a snapshot taken before any row was processed, so it cannot see earlier rows of this same
// save. Track them separately: study.deaths is demographic, so a second row for one animal cannot be saved.
var deathsInTransaction = helper.getProperty('deathsInTransaction') || {};

var errorMsg = null;

// check if a death record already exists for this animal
if (idMap[row.Id].calculated_status.toUpperCase() === 'DEAD' && deathIdMap[row.Id].QCStateLabel.toUpperCase() === 'COMPLETED') {
EHR.Server.Utils.addError(scriptErrors, 'Id', 'Death record already exists for this animal.', 'ERROR');
if (status === 'DEAD' && priorDeathQCState === 'COMPLETED') {
errorMsg = 'Death record already exists for this animal.';
}
// check if the animal is at the center
else if (idMap[row.Id].calculated_status.toUpperCase() === 'SHIPPED') {
EHR.Server.Utils.addError(scriptErrors, 'Id', 'Animal is not at the center.', 'ERROR');
else if (status === 'SHIPPED') {
errorMsg = 'Animal is not at the center.';
}
else if (deathsInTransaction[row.Id]) {
errorMsg = 'This animal is entered more than once. Only one death record per animal can be saved.';
}
// Check if an animal that's being entered is pending any request/review.
// Note 1: When trying to enter a new record for an animal, the QCState = 'IN PROGRESS'.
// Note 2: Upon 'Submit Death', the QCState will get set to 'REQUEST: PENDING', and upon 'Submit Necropsy for Review',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this comment needs to be updated to match the client workflow.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will update this comment in my next PR

// the QCState will get set to 'Review Required' - this way we can distinguish between the two states in the Death/Necropsy workflow.
// If a user tries to submit a new Death record (identified by QCState = 'IN PROGRESS') for an animal that
// already has a pending request/review status in study.deaths, then below error message will be displayed.
else if (row.QCStateLabel.toUpperCase() === 'IN PROGRESS' &&
deathIdMap[row.Id] && deathIdMap[row.Id].QCStateLabel &&
(deathIdMap[row.Id].QCStateLabel.toUpperCase() === 'REQUEST: PENDING' ||
deathIdMap[row.Id].QCStateLabel.toUpperCase() === 'REVIEW REQUIRED')) {
EHR.Server.Utils.addError(scriptErrors, 'Id', 'Death record is pending review for this animal', 'ERROR');
else if (rowQCState === 'IN PROGRESS' &&
(priorDeathQCState === 'REQUEST: PENDING' || priorDeathQCState === 'REVIEW REQUIRED')) {
errorMsg = 'Death record is pending review for this animal';
}
// if 'Save Draft' record already exists, it doesn't allow to 'Save Draft' or 'Submit Death'
// on the same animal again - throws an error "duplicate key value violates unique constraint"
// So, added this check to allow 'Save Draft' record to be saved only once.
else if (oldRow === undefined && row.QCStateLabel.toUpperCase() === 'IN PROGRESS' &&
deathIdMap[row.Id] && deathIdMap[row.Id].QCStateLabel &&
deathIdMap[row.Id].QCStateLabel.toUpperCase() === 'IN PROGRESS') {
EHR.Server.Utils.addError(scriptErrors, 'Id', 'Death/Necropsy data entry is in progress for this animal', 'ERROR');
else if (oldRow === undefined && rowQCState === 'IN PROGRESS' && priorDeathQCState === 'IN PROGRESS') {
errorMsg = 'Death/Necropsy data entry is in progress for this animal';
}
// study.deaths is demographic (one row per animal), so any other new row for an animal with an existing
// record would fail on the unique constraint; report it as a validation error instead. Test record
// existence, not QC state: ETL/import-sourced rows can carry a null QCState.
else if (oldRow === undefined && deathIdMap[row.Id]) {
errorMsg = 'A death record already exists for this animal (' + (deathIdMap[row.Id].QCStateLabel || 'unknown state') + ').';
}
else if (!helper.isValidateOnly() && row.Id && row.date && row.QCStateLabel.toUpperCase() === 'COMPLETED') {

if (validIds.indexOf(row.id) !== -1) {
if (errorMsg) {
EHR.Server.Utils.addError(scriptErrors, 'Id', errorMsg, 'ERROR');
}
else {
if (!helper.isValidateOnly() && row.Id && row.date && rowQCState === 'COMPLETED') {

// update demographics
demographicsUpdates.push({
Expand All @@ -128,26 +144,33 @@ function onUpsert(helper, scriptErrors, row, oldRow) {
helper.getJavaHelper().updateDemographicsRecord(demographicsUpdates);
console.log('updated demographics death date for animal: ' + row.Id);
}
else {
console.log(row.id + " is not a valid animal id");

if (!helper.isValidateOnly() && row.date && row.QCStateLabel && EHR.Server.Security.getQCStateByLabel(row.QCStateLabel).PublicData) {
var qcstate = helper.getJavaHelper().getQCStateForLabel(row.QCStateLabel).getRowId();

//add/update weight record
var weightRecord = {
Id: row.Id,
date: row.date,
weight: row.deathWeight,
taskid: row.taskid,
qcstate: qcstate,
performedby: row.performedby
};
if (triggerHelper.upsertWeightRecord(weightRecord, false)) {
helper.addTableModified('study', 'weight');
}
}
}

if(row.QCStateLabel && EHR.Server.Security.getQCStateByLabel(row.QCStateLabel).PublicData) {
var qcstate = helper.getJavaHelper().getQCStateForLabel(row.QCStateLabel).getRowId();

//add/update weight record
var weightRecord = {
Id: row.Id,
date: row.date,
weight: row.deathWeight,
taskid: row.taskid,
qcstate: qcstate,
performedby: row.performedby
};
triggerHelper.upsertWeightRecord(weightRecord);
// mark only rows that passed, so a duplicate of a failed row reports that row's underlying error
deathsInTransaction[row.Id] = true;
helper.setProperty('deathsInTransaction', deathsInTransaction);
}
}
// insert-only: updates of existing death records keep their prior behavior
else if (oldRow === undefined) {
EHR.Server.Utils.addError(scriptErrors, 'Id', 'Id not found in the demographics table.', 'ERROR');
}
}
}

Expand Down
9 changes: 9 additions & 0 deletions nbri_ehr/resources/queries/study/deaths.query.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@
<column columnName="date">
<columnTitle>Death Date</columnTitle>
</column>
<column columnName="type">
<columnTitle>Death Type</columnTitle>
<fk>
<fkDbSchema>ehr_lookups</fkDbSchema>
<fkTable>death_type</fkTable>
<fkColumnName>value</fkColumnName>
<fkDisplayColumnName>title</fkDisplayColumnName>
</fk>
</column>
<column columnName="reason">
<columnTitle>Disposition</columnTitle>
<fk>
Expand Down
1 change: 1 addition & 0 deletions nbri_ehr/resources/queries/study/deaths/.qview.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<column name="Id"/>
<column name="Id/Demographics/species"/>
<column name="date"/>
<column name="type"/>
<column name="reason"/>
<column name="deathWeight"/>
<column name="remark"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,9 @@
<column columnName="deathWeight">
<datatype>double</datatype>
</column>
<column columnName="type">
<datatype>varchar</datatype>
</column>
</columns>
</table>
<table tableName="exemptions" tableDbType="TABLE">
Expand Down
54 changes: 54 additions & 0 deletions nbri_ehr/resources/web/nbri_ehr/model/sources/Death.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright (c) 2026 LabKey Corporation
*
* Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
*/
/**
* Metadata for the grid-based Bulk Deaths form. The columnConfig widths only take effect in the grid; they are ignored
* when the same fields render in a form panel.
*/
EHR.model.DataModelManager.registerMetadata('Death', {
allQueries: {
},
byQuery: {
'study.deaths': {
qcstate: {
hidden: true
},
date: {
xtype: 'xdatetime',
editorConfig: {
dateFormat: 'Y-m-d',
timeFormat: 'H:i'
},
columnConfig: {
width: 160
}
},
deathWeight: {
label: 'Weight (kg)',
columnConfig: {
width: 150
}
},
type: {
allowBlank: false,
nullable: false,
columnConfig: {
width: 160
}
},
reason: {
columnConfig: {
width: 160
}
},
remark: {
xtype: 'ehr-remarkfield',
columnConfig: {
width: 200
}
}
}
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,11 @@ EHR.model.DataModelManager.registerMetadata('DeathNecropsy', {
},
},
deathWeight: {
label: 'Weight (kg)',
allowBlank: false,
nullable: false,
label: 'Weight (kg)'
},
reason: {
type: {
allowBlank: false,
nullable: false,
nullable: false
}
},
'study.necropsy': {
Expand Down
1 change: 1 addition & 0 deletions nbri_ehr/src/org/labkey/nbri_ehr/NBRI_EHRModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ private void registerDataEntry()
EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIBulkClinicalFormType.class, this));
EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIDepartureFormType.class, this));
EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIDeathNecropsyFormType.class, this));
EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIBulkDeathFormType.class, this));
EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIHousingFormType.class, this));
EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIMedicationTreatmentFormType.class, this));
EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIProjectFormType.class, this));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* 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.dataentry.form;

import org.labkey.api.ehr.EHRService;
import org.labkey.api.ehr.dataentry.DataEntryFormContext;
import org.labkey.api.ehr.dataentry.FormSection;
import org.labkey.api.ehr.security.EHRCompletedInsertPermission;
import org.labkey.api.module.Module;
import org.labkey.api.security.permissions.AdminPermission;
import org.labkey.api.view.template.ClientDependency;
import org.labkey.nbri_ehr.dataentry.section.BaseFormSection;
import org.labkey.nbri_ehr.dataentry.section.NBRIAnimalDetailsFormSection;
import org.labkey.nbri_ehr.dataentry.section.NBRITaskFormSection;

import java.util.Arrays;

/**
* Admin-only form that records deaths only, as a grid so several animals can be entered at once.
*/
public class NBRIBulkDeathFormType extends NBRIBaseTaskFormType
{
public static final String NAME = "BulkDeaths";
public static final String LABEL = "Bulk Deaths";

public NBRIBulkDeathFormType(DataEntryFormContext ctx, Module owner)
{
super(ctx, owner, NAME, LABEL, "Colony Management", Arrays.asList(
new NBRITaskFormSection(),
new NBRIAnimalDetailsFormSection(),
new BaseFormSection("study", "deaths", "Deaths", EHRService.FORM_SECTION_LOCATION.Body, true, false)
));

addClientDependency(ClientDependency.supplierFromPath("nbri_ehr/model/sources/Death.js"));

for (FormSection s : getFormSections())
{
s.addConfigSource("Death");
}
}

@Override
public boolean isAvailable()
{
return super.isAvailable() && getCtx().getContainer().hasPermission(getCtx().getUser(), AdminPermission.class);
}

@Override
protected boolean canInsert()
{
return EHRService.get().hasPermission("study", "deaths", getCtx().getContainer(), getCtx().getUser(), EHRCompletedInsertPermission.class);
}
}
Loading