diff --git a/nbri_ehr/resources/data/pairing_formation_types.tsv b/nbri_ehr/resources/data/pairing_formation_types.tsv
index 64101aa..ed90dde 100644
--- a/nbri_ehr/resources/data/pairing_formation_types.tsv
+++ b/nbri_ehr/resources/data/pairing_formation_types.tsv
@@ -1,7 +1,11 @@
-value
-Full Contact Pair
-Group Formation
-Introduction
-Limited Contact Pair
-Pair Formation
-Other
\ No newline at end of file
+value title date_disabled
+C Compatible 2026-08-06
+CF CF
+CP Continuous pair
+F Fought 2026-08-06
+FC Full Contact
+IC Intermittent Contact
+IG Indoor Group Housing
+PC Protected Contact
+S Single Housing
+U Unsuccessful
\ No newline at end of file
diff --git a/nbri_ehr/resources/domain-templates/ehr.template.xml b/nbri_ehr/resources/domain-templates/ehr.template.xml
index 281f4bd..a29a278 100644
--- a/nbri_ehr/resources/domain-templates/ehr.template.xml
+++ b/nbri_ehr/resources/domain-templates/ehr.template.xml
@@ -6,118 +6,18 @@
-
- string
-
-
- int
-
-
- string
-
-
- string
-
-
- string
-
-
- dateTime
-
-
- dateTime
-
-
- dateTime
-
dateTime
-
- dateTime
-
-
- dateTime
-
-
- dateTime
-
-
- dateTime
-
-
- string
-
-
- string
-
-
- int
-
-
- string
-
-
- boolean
-
-
+
boolean
-
+
boolean
-
- string
-
-
- string
-
-
- string
-
-
- string
-
-
- string
-
-
- string
-
-
- string
-
-
- boolean
-
-
- boolean
-
-
- boolean
-
-
+
boolean
-
- boolean
-
-
- boolean
-
-
- string
-
-
-
-
-
-
-
diff --git a/nbri_ehr/resources/queries/ehr/investigators.query.xml b/nbri_ehr/resources/queries/ehr/investigators.query.xml
new file mode 100644
index 0000000..13d4973
--- /dev/null
+++ b/nbri_ehr/resources/queries/ehr/investigators.query.xml
@@ -0,0 +1,26 @@
+
+
+
+
+ Investigators
+
+
+
+
+
+
+ Last Name
+ true
+
+
+ First Name
+
+
+ User
+ http://www.labkey.org/types#userId
+
+
+
+
+
+
diff --git a/nbri_ehr/resources/queries/ehr/project.query.xml b/nbri_ehr/resources/queries/ehr/project.query.xml
index 5b72f21..8ce9715 100644
--- a/nbri_ehr/resources/queries/ehr/project.query.xml
+++ b/nbri_ehr/resources/queries/ehr/project.query.xml
@@ -49,10 +49,10 @@
true
- core
- Users
- UserId
- DisplayName
+ ehr
+ investigators
+ rowid
+ lastName
diff --git a/nbri_ehr/resources/queries/ehr/protocol.query.xml b/nbri_ehr/resources/queries/ehr/protocol.query.xml
index 82667e7..edf95db 100644
--- a/nbri_ehr/resources/queries/ehr/protocol.query.xml
+++ b/nbri_ehr/resources/queries/ehr/protocol.query.xml
@@ -9,78 +9,15 @@
PI
false
- core
- Users
- UserId
- DisplayName
+ ehr
+ investigators
+ rowid
+ lastName
true
-
- Author
-
- core
- Users
- UserId
- DisplayName
-
-
-
- Owner
-
- core
- Users
- UserId
- DisplayName
-
-
-
- Protocol Type
-
- ehr_lookups
- protocol_type
- value
- title
-
-
-
- Protocol Category
-
- ehr_lookups
- protocol_category
- value
- title
-
-
-
- Current State
-
- ehr_lookups
- protocol_state
- value
- title
-
-
-
- Parent Protocol
-
- ehr
- protocol
- protocol
- displayName
-
-
-
- Questionnaire
-
- ehr_lookups
- questionnaire
- value
- title
-
-
Approval Date
@@ -88,4 +25,4 @@
-
\ No newline at end of file
+
diff --git a/nbri_ehr/resources/queries/ehr/protocol/.qview.xml b/nbri_ehr/resources/queries/ehr/protocol/.qview.xml
index e2e125f..62dfd8e 100644
--- a/nbri_ehr/resources/queries/ehr/protocol/.qview.xml
+++ b/nbri_ehr/resources/queries/ehr/protocol/.qview.xml
@@ -2,40 +2,14 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-
\ No newline at end of file
+
diff --git a/nbri_ehr/resources/queries/ehr/protocol_counts.query.xml b/nbri_ehr/resources/queries/ehr/protocol_counts.query.xml
index 6586f91..82b9060 100644
--- a/nbri_ehr/resources/queries/ehr/protocol_counts.query.xml
+++ b/nbri_ehr/resources/queries/ehr/protocol_counts.query.xml
@@ -14,15 +14,6 @@
scientific_name
-
- Euthanasia
-
- ehr_lookups
- euthanasia_type
- value
- title
-
-
diff --git a/nbri_ehr/resources/queries/study/activeAssignments.sql b/nbri_ehr/resources/queries/study/activeAssignments.sql
index 990598c..9cd67a4 100644
--- a/nbri_ehr/resources/queries/study/activeAssignments.sql
+++ b/nbri_ehr/resources/queries/study/activeAssignments.sql
@@ -6,7 +6,7 @@
SELECT pa.Id,
pa.protocol.title AS protocolTitle,
pa.protocol.InvestigatorId AS investigatorId,
- pa.protocol.InvestigatorId.DisplayName AS investigatorName,
+ initcap(pa.protocol.InvestigatorId.FirstName) || ' ' || initcap(pa.protocol.InvestigatorId.LastName) AS investigatorName,
pa.protocol.InvestigatorId.LastName AS investigatorLastName,
a.project.name AS project,
a.isActive AS isActiveAssignment,
diff --git a/nbri_ehr/resources/queries/study/animalGroupHousingSummary.query.xml b/nbri_ehr/resources/queries/study/animalGroupHousingSummary.query.xml
new file mode 100644
index 0000000..95ce7a9
--- /dev/null
+++ b/nbri_ehr/resources/queries/study/animalGroupHousingSummary.query.xml
@@ -0,0 +1,15 @@
+
+
+
+
+ Animal Groups Housing Summary
+
+
+ Total Animals
+ /query/executeQuery.view?schemaName=study&query.queryName=demographics&query.id/curLocation/room~eq=${room}&query.id/activeAnimalGroups/groups~contains=${groupId/title}
+
+
+
+
+
+
diff --git a/nbri_ehr/resources/queries/study/animalGroupHousingSummary.sql b/nbri_ehr/resources/queries/study/animalGroupHousingSummary.sql
new file mode 100644
index 0000000..5ca3a32
--- /dev/null
+++ b/nbri_ehr/resources/queries/study/animalGroupHousingSummary.sql
@@ -0,0 +1,13 @@
+/*
+ * Copyright (c) 2026 LabKey Corporation
+ *
+ * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
+ */
+SELECT
+ m.groupId,
+ m.id.curLocation.room,
+ count(distinct m.id) as totalAnimals
+
+FROM study.animal_group_members m
+WHERE m.isActive = true
+GROUP BY m.groupId, m.id.curLocation.room
diff --git a/nbri_ehr/resources/queries/study/animalGroupOverlapSummary.query.xml b/nbri_ehr/resources/queries/study/animalGroupOverlapSummary.query.xml
new file mode 100644
index 0000000..0db6a6c
--- /dev/null
+++ b/nbri_ehr/resources/queries/study/animalGroupOverlapSummary.query.xml
@@ -0,0 +1,22 @@
+
+
+
+
+ Animal Group Member Summary
+ This query identifies the total distinct animals that were part of a group over the provided date range
+
+
+ /query/executeQuery.view?schemaName=study&query.queryName=animalGroupOverlaps&query.param.StartDate=${StartDate}&query.param.EndDate=${EndDate}&query.groupId/title~eq=${groupId/title}
+ Total Animals
+
+
+ true
+
+
+ true
+
+
+
+
+
+
diff --git a/nbri_ehr/resources/queries/study/animalGroupOverlapSummary.sql b/nbri_ehr/resources/queries/study/animalGroupOverlapSummary.sql
new file mode 100644
index 0000000..c855540
--- /dev/null
+++ b/nbri_ehr/resources/queries/study/animalGroupOverlapSummary.sql
@@ -0,0 +1,15 @@
+/*
+ * Copyright (c) 2026 LabKey Corporation
+ *
+ * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
+ */
+SELECT
+ o.groupId,
+ count(distinct o.id) as totalAnimals,
+
+ max(o.StartDate) as StartDate,
+ max(o.EndDate) as EndDate
+
+FROM study.animalGroupOverlaps o
+
+GROUP BY o.groupId
diff --git a/nbri_ehr/resources/queries/study/animalGroupOverlaps.query.xml b/nbri_ehr/resources/queries/study/animalGroupOverlaps.query.xml
new file mode 100644
index 0000000..92106fe
--- /dev/null
+++ b/nbri_ehr/resources/queries/study/animalGroupOverlaps.query.xml
@@ -0,0 +1,18 @@
+
+
+
+
+ Animal Group Overlaps
+ This query identifies distinct animals that were part of a group over the provided date range
+
+
+ true
+
+
+ true
+
+
+
+
+
+
diff --git a/nbri_ehr/resources/queries/study/animalGroupOverlaps.sql b/nbri_ehr/resources/queries/study/animalGroupOverlaps.sql
new file mode 100644
index 0000000..c5a2e62
--- /dev/null
+++ b/nbri_ehr/resources/queries/study/animalGroupOverlaps.sql
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2026 LabKey Corporation
+ *
+ * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+/**
+ * This query is designed to find distinct animals that were part of a group at a given point in time
+ */
+PARAMETERS(StartDate TIMESTAMP, EndDate TIMESTAMP)
+
+SELECT
+ m.Id,
+ m.groupId,
+
+ max(StartDate) as StartDate,
+ max(EndDate) as EndDate
+FROM study.animal_group_members m
+
+WHERE (
+ /* entered startdate must be <= entered enddate */
+ coalesce( StartDate , cast('1900-01-01 00:00:00.0' as timestamp)) <= coalesce(EndDate, now())
+ AND
+
+ /* entered startdate must be less than record's enddate */
+ cast(coalesce( StartDate , cast('1900-01-01 00:00:00.0' as DATE)) AS DATE) <= m.enddateCoalesced
+
+ and
+
+ /* entered enddate must be greater than record's startdate */
+ cast(coalesce(EndDate, curdate()) AS DATE) >= m.dateOnly
+ )
+
+GROUP BY m.groupId, m.id
diff --git a/nbri_ehr/resources/queries/study/animalGroupsPivoted.query.xml b/nbri_ehr/resources/queries/study/animalGroupsPivoted.query.xml
new file mode 100644
index 0000000..3ff39fc
--- /dev/null
+++ b/nbri_ehr/resources/queries/study/animalGroupsPivoted.query.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
diff --git a/nbri_ehr/resources/queries/study/animalGroupsPivoted.sql b/nbri_ehr/resources/queries/study/animalGroupsPivoted.sql
new file mode 100644
index 0000000..5c5c53b
--- /dev/null
+++ b/nbri_ehr/resources/queries/study/animalGroupsPivoted.sql
@@ -0,0 +1,17 @@
+/*
+ * Copyright (c) 2026 LabKey Corporation
+ *
+ * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
+ */
+SELECT
+g.id,
+g.groupId.title as name,
+cast('yes' as varchar) as valueField
+
+FROM study.animal_group_members g
+
+WHERE (g.enddate IS NULL OR COALESCE(g.enddate, curdate()) >= curdate())
+
+GROUP BY g.id, g.groupId.title
+
+PIVOT valueField by name IN (select title FROM ehr_lookups.breeding_type)
diff --git a/nbri_ehr/resources/queries/study/animal_group_members.query.xml b/nbri_ehr/resources/queries/study/animal_group_members.query.xml
new file mode 100644
index 0000000..4cf726e
--- /dev/null
+++ b/nbri_ehr/resources/queries/study/animal_group_members.query.xml
@@ -0,0 +1,42 @@
+
+
+
+
+ Animal Group Members
+
+
+
+ Date Added
+
+
+ Date Removed
+ false
+
+
+ Group
+
+ ehr_lookups
+ breeding_type
+ value
+ title
+
+
+
+
+ core
+ qcstate
+ rowid
+
+
+
+ true
+
+
+ false
+ false
+
+
+
+
+
+
diff --git a/nbri_ehr/resources/queries/study/demographicsActiveAnimalGroups.query.xml b/nbri_ehr/resources/queries/study/demographicsActiveAnimalGroups.query.xml
new file mode 100644
index 0000000..f12d8e0
--- /dev/null
+++ b/nbri_ehr/resources/queries/study/demographicsActiveAnimalGroups.query.xml
@@ -0,0 +1,19 @@
+
+
+
+
+ Active Animal Groups
+
+
+
+ ALWAYS_OFF
+ Total Groups
+
+
+ Groups
+
+
+
+
+
+
diff --git a/nbri_ehr/resources/queries/study/demographicsActiveAnimalGroups.sql b/nbri_ehr/resources/queries/study/demographicsActiveAnimalGroups.sql
new file mode 100644
index 0000000..435a4b4
--- /dev/null
+++ b/nbri_ehr/resources/queries/study/demographicsActiveAnimalGroups.sql
@@ -0,0 +1,16 @@
+/*
+ * Copyright (c) 2026 LabKey Corporation
+ *
+ * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
+ */
+SELECT
+ m.Id,
+ count(distinct m.objectid) as totalGroups,
+ group_concat(distinct m.name, chr(10)) as groups
+
+FROM (SELECT Id,
+ objectid,
+ groupId.title as name
+ FROM study.animal_group_members
+ WHERE enddate is NULL AND qcstate.publicdata = true) m
+GROUP BY m.Id
diff --git a/nbri_ehr/resources/queries/study/pairings.query.xml b/nbri_ehr/resources/queries/study/pairings.query.xml
index 727a724..d75ab6a 100644
--- a/nbri_ehr/resources/queries/study/pairings.query.xml
+++ b/nbri_ehr/resources/queries/study/pairings.query.xml
@@ -45,6 +45,7 @@
ehr_lookups
pairing_formation_types
value
+ title
diff --git a/nbri_ehr/resources/referenceStudy/study/datasets/datasets_manifest.xml b/nbri_ehr/resources/referenceStudy/study/datasets/datasets_manifest.xml
index a4cd2a4..fbc9052 100644
--- a/nbri_ehr/resources/referenceStudy/study/datasets/datasets_manifest.xml
+++ b/nbri_ehr/resources/referenceStudy/study/datasets/datasets_manifest.xml
@@ -2,6 +2,7 @@
+
diff --git a/nbri_ehr/resources/referenceStudy/study/datasets/datasets_metadata.xml b/nbri_ehr/resources/referenceStudy/study/datasets/datasets_metadata.xml
index 2ae212d..aa2bd6a 100644
--- a/nbri_ehr/resources/referenceStudy/study/datasets/datasets_metadata.xml
+++ b/nbri_ehr/resources/referenceStudy/study/datasets/datasets_metadata.xml
@@ -58,6 +58,32 @@
+
+ Animal Group Members
+ Records membership of individual primates in named animal groups, with the date the animal joined and the date it was removed.
+
+
+ varchar
+ http://cpas.labkey.com/Study#ParticipantId
+
+ ptid
+
+
+
+ timestamp
+ http://cpas.labkey.com/Study#VisitDate
+ http://cpas.labkey.com/Study#VisitDate
+
+
+ timestamp
+ urn:ehr.labkey.org/#EndDate
+
+
+ Group
+ varchar
+
+
+
Arrival
Tracks the arrival of primates into the research facility, including source, acquisition type, and associated documentation.
diff --git a/nbri_ehr/resources/scripts/nbri_triggers.js b/nbri_ehr/resources/scripts/nbri_triggers.js
index 16b2ddc..320c63f 100644
--- a/nbri_ehr/resources/scripts/nbri_triggers.js
+++ b/nbri_ehr/resources/scripts/nbri_triggers.js
@@ -27,7 +27,7 @@ exports.init = function (EHR) {
EHR.Server.TriggerManager.unregisterAllHandlersForQueryNameAndEvent('study', 'cases', EHR.Server.TriggerManager.Events.AFTER_DELETE);
helper.setScriptOptions({
- datasetsToClose: ['assignment', 'protocolAssignment' , 'housing', 'treatment_order', 'observation_order', 'cases', 'pairings', 'exemptions', 'flags']
+ datasetsToClose: ['assignment', 'protocolAssignment' , 'housing', 'treatment_order', 'observation_order', 'cases', 'pairings', 'exemptions', 'flags', 'animal_group_members']
});
});
@@ -54,6 +54,14 @@ exports.init = function (EHR) {
});
});
+ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.INIT, 'study', 'animal_group_members', function(event, helper) {
+ // group memberships are routinely backdated, so historical dates must not raise a warning
+ helper.setScriptOptions({
+ requiresStatusRecalc: false,
+ allowDatesInDistantPast: true
+ });
+ });
+
EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.INIT, 'study', 'assignment', function(event, helper) {
helper.setScriptOptions({
allowAnyId: isAllowAnyIdRequested(helper),
@@ -136,7 +144,7 @@ exports.init = function (EHR) {
EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.INIT, 'study', 'deaths', function(event, helper) {
helper.setScriptOptions({
- datasetsToClose: ['assignment', 'protocolAssignment' , 'housing', 'treatment_order', 'observation_order', 'cases', 'pairings', 'exemptions', 'flags'],
+ datasetsToClose: ['assignment', 'protocolAssignment' , 'housing', 'treatment_order', 'observation_order', 'cases', 'pairings', 'exemptions', 'flags', 'animal_group_members'],
allowShippedIds: false,
allowDeadIds: false,
requiresStatusRecalc: true,
diff --git a/nbri_ehr/resources/web/nbri_ehr/data/AllowAnyIdClientStore.js b/nbri_ehr/resources/web/nbri_ehr/data/AllowAnyIdClientStore.js
new file mode 100644
index 0000000..49ecfdb
--- /dev/null
+++ b/nbri_ehr/resources/web/nbri_ehr/data/AllowAnyIdClientStore.js
@@ -0,0 +1,16 @@
+/*
+ * Copyright (c) 2026 LabKey Corporation
+ *
+ * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
+ */
+Ext4.define('NBRI_EHR.data.AllowAnyIdClientStore', {
+ extend: 'EHR.data.DataEntryClientStore',
+
+ getExtraContext: function () {
+ var ret = this.callParent(arguments) || {};
+
+ // Tell the trigger scripts to allow any Id. Requires handling in trigger script to fully enable.
+ ret['allowAnyId'] = true;
+ return ret;
+ }
+});
diff --git a/nbri_ehr/resources/web/nbri_ehr/model/sources/AnimalGroupMembers.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/AnimalGroupMembers.js
new file mode 100644
index 0000000..3678d54
--- /dev/null
+++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/AnimalGroupMembers.js
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2026 LabKey Corporation
+ *
+ * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
+ */
+EHR.model.DataModelManager.registerMetadata('AnimalGroupMembers', {
+ byQuery: {
+ 'study.animal_group_members': {
+ Id: {
+ xtype: 'ehr-animalIdUpperField',
+ dataIndex: 'Id',
+ nullable: false,
+ allowBlank: false,
+ lookups: false,
+ noSaveInTemplateByDefault: true,
+ columnConfig: {
+ width: 95,
+ showLink: false
+ },
+ editorConfig: {
+ allowAnyId: true
+ }
+ },
+ date: {
+ allowBlank: false,
+ nullable: false,
+ noSaveInTemplateByDefault: true,
+ hidden: false,
+ getInitialValue: function(v, rec){
+ if (v)
+ return v;
+
+ let curDate = new Date();
+ curDate.setHours(0, 0, 0, 0);
+ return curDate;
+ }
+ },
+ groupId: {
+ allowBlank: false,
+ nullable: false,
+ lookup: {
+ // the shared default filters on a date column that the breeding type lookup does not have
+ filterArray: []
+ }
+ },
+ performedBy: {
+ shownInGrid: false,
+ hidden: true
+ }
+ }
+ }
+});
diff --git a/nbri_ehr/resources/web/nbri_ehr/model/sources/Assignment.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/Assignment.js
index 4d38d3c..b055afb 100644
--- a/nbri_ehr/resources/web/nbri_ehr/model/sources/Assignment.js
+++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/Assignment.js
@@ -5,13 +5,12 @@
*/
EHR.model.DataModelManager.registerMetadata('Assignment', {
- allQueries: {
- endDate: {
- hidden: true
- }
- },
byQuery: {
'study.assignment': {
+ // the dataset column is hidden by default; project assignments are ended by entering an end date
+ 'enddate': {
+ hidden: false
+ },
'project': {
xtype: 'combo',
nullable: false,
@@ -27,6 +26,9 @@ EHR.model.DataModelManager.registerMetadata('Assignment', {
}
},
'study.protocolAssignment': {
+ 'enddate': {
+ hidden: true
+ },
'project': {
hidden: true
},
diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/NBRI_EHRModule.java b/nbri_ehr/src/org/labkey/nbri_ehr/NBRI_EHRModule.java
index aa76225..b76fdbb 100644
--- a/nbri_ehr/src/org/labkey/nbri_ehr/NBRI_EHRModule.java
+++ b/nbri_ehr/src/org/labkey/nbri_ehr/NBRI_EHRModule.java
@@ -136,6 +136,8 @@ protected void doStartupAfterSpringConfig(ModuleContext moduleContext)
ehrService.registerDemographicsProvider(new SourceDemographicsProvider(this));
ehrService.registerDemographicsProvider(new NecropsyStatusDemographicsProvider(this));
+ EHRService.get().registerHistoryDataSource(new AnimalGroupsDataSource(this));
+ EHRService.get().registerHistoryDataSource(new AnimalGroupsEndDataSource(this));
EHRService.get().registerHistoryDataSource(new ArrivalDataSource(this));
EHRService.get().registerHistoryDataSource(new BiopsyDataSource(this));
EHRService.get().registerHistoryDataSource(new BirthDataSource(this));
@@ -177,7 +179,7 @@ protected void doStartupAfterSpringConfig(ModuleContext moduleContext)
ehrService.registerActionOverride("participantView", this, "views/participantView.html");
ehrService.registerActionOverride("enterData", this, "views/enterData.html");
- ehrService.registerTriggerScriptOption("datasetsToCloseOnNewEntry", List.of("assignment", "protocolAssignment"));
+ ehrService.registerTriggerScriptOption("datasetsToCloseOnNewEntry", List.of("assignment", "protocolAssignment", "animal_group_members"));
RoleManager.registerRole(new NBRIEHRVetTechRole());
EHRService.get().registerMoreActionsButton(new ShowEditUIButton(this, "ehr", "observation_types", EHRDataAdminPermission.class), "ehr", "observation_types");
@@ -207,6 +209,7 @@ private void registerDataEntry()
{
EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIAliasFormType.class, this));
EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIAssignmentFormType.class, this));
+ EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIGroupAssignmentFormType.class, this));
EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIArrivalFormType.class, this));
EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIBirthFormType.class, this));
EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIBulkClinicalFormType.class, this));
@@ -217,6 +220,7 @@ private void registerDataEntry()
EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIMedicationTreatmentFormType.class, this));
EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIProjectFormType.class, this));
EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIProtocolFormType.class, this));
+ EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIInvestigatorsFormType.class, this));
EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIPregnancyFormType.class, this));
EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIWeightFormType.class, this));
EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIFlagsFormType.class, this));
diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIGroupAssignmentFormType.java b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIGroupAssignmentFormType.java
new file mode 100644
index 0000000..5ebea19
--- /dev/null
+++ b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIGroupAssignmentFormType.java
@@ -0,0 +1,48 @@
+/*
+ * 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.dataentry.DataEntryFormContext;
+import org.labkey.api.ehr.dataentry.FormSection;
+import org.labkey.api.module.Module;
+import org.labkey.api.view.template.ClientDependency;
+import org.labkey.nbri_ehr.dataentry.section.NBRIAnimalDetailsFormSection;
+import org.labkey.nbri_ehr.dataentry.section.NBRIGroupAssignmentFormSection;
+import org.labkey.nbri_ehr.dataentry.section.NBRITaskFormSection;
+
+import java.util.List;
+
+public class NBRIGroupAssignmentFormType extends NBRIBaseTaskFormType
+{
+ public static final String NAME = "animalGroupAssignment";
+ public static final String LABEL = "Animal Group Assignment";
+
+ public NBRIGroupAssignmentFormType(DataEntryFormContext ctx, Module owner)
+ {
+ super(ctx, owner, NAME, LABEL, "Colony Management", List.of(
+ new NBRITaskFormSection(),
+ new NBRIAnimalDetailsFormSection(),
+ new NBRIGroupAssignmentFormSection(false, true, false)
+ ));
+
+ addClientDependency(ClientDependency.supplierFromPath("nbri_ehr/model/sources/AnimalGroupMembers.js"));
+
+ for (FormSection s : getFormSections())
+ {
+ s.addConfigSource("AnimalGroupMembers");
+ }
+ }
+}
diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIInvestigatorsFormType.java b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIInvestigatorsFormType.java
new file mode 100644
index 0000000..14055b2
--- /dev/null
+++ b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIInvestigatorsFormType.java
@@ -0,0 +1,48 @@
+/*
+ * 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.dataentry.DataEntryFormContext;
+import org.labkey.api.ehr.dataentry.forms.AdminLinksFormType;
+import org.labkey.api.ehr.security.EHRDataAdminPermission;
+import org.labkey.api.module.Module;
+import org.labkey.api.view.ActionURL;
+
+import java.util.ArrayList;
+
+public class NBRIInvestigatorsFormType extends AdminLinksFormType
+{
+ public NBRIInvestigatorsFormType(DataEntryFormContext ctx, Module owner)
+ {
+ super(ctx, owner, "Investigators", "Investigators", "Admin", new ArrayList<>());
+ }
+
+ @Override
+ protected ActionURL dataEntryLink()
+ {
+ ActionURL url = new ActionURL("ldk", "updateQuery", getCtx().getContainer());
+ url.addParameter("schemaName", "ehr");
+ url.addParameter("query.queryName", "investigators");
+ url.addParameter("showImport", "true");
+ return url;
+ }
+
+ @Override
+ public boolean isAvailable()
+ {
+ return (super.isAvailable() || getCtx().getContainer().hasPermission(getCtx().getUser(), EHRDataAdminPermission.class));
+ }
+}
diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIGroupAssignmentFormSection.java b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIGroupAssignmentFormSection.java
new file mode 100644
index 0000000..3ef5297
--- /dev/null
+++ b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIGroupAssignmentFormSection.java
@@ -0,0 +1,32 @@
+/*
+ * 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.section;
+
+import org.labkey.api.view.template.ClientDependency;
+
+public class NBRIGroupAssignmentFormSection extends BaseFormSection
+{
+ public NBRIGroupAssignmentFormSection(boolean allowAnyId, boolean collapsible, boolean initCollapsed)
+ {
+ super("study", "animal_group_members", "Group Assignments", "ehr-gridpanel", collapsible, initCollapsed, true);
+
+ if (allowAnyId)
+ {
+ setClientStoreClass("NBRI_EHR.data.AllowAnyIdClientStore");
+ addClientDependency(ClientDependency.supplierFromPath("nbri_ehr/data/AllowAnyIdClientStore.js"));
+ }
+ }
+}
diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/history/AnimalGroupsDataSource.java b/nbri_ehr/src/org/labkey/nbri_ehr/history/AnimalGroupsDataSource.java
new file mode 100644
index 0000000..69f1ace
--- /dev/null
+++ b/nbri_ehr/src/org/labkey/nbri_ehr/history/AnimalGroupsDataSource.java
@@ -0,0 +1,52 @@
+/*
+ * 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.history;
+
+import org.labkey.api.data.Container;
+import org.labkey.api.data.Results;
+import org.labkey.api.ehr.history.AbstractDataSource;
+import org.labkey.api.module.Module;
+import org.labkey.api.query.FieldKey;
+import org.labkey.api.util.PageFlowUtil;
+
+import java.sql.SQLException;
+import java.util.Set;
+
+public class AnimalGroupsDataSource extends AbstractDataSource
+{
+ public AnimalGroupsDataSource(Module module)
+ {
+ super("study", "animal_group_members", "Added To Group", "Animal Groups", module);
+ }
+
+ @Override
+ protected Set getColumnNames()
+ {
+ return PageFlowUtil.set("Id", "date", "enddate", "groupId", "groupId/title");
+ }
+
+ @Override
+ protected String getHtml(Container c, Results rs, boolean redacted) throws SQLException
+ {
+ StringBuilder sb = new StringBuilder();
+
+ FieldKey title = FieldKey.fromString("groupId/title");
+ if (rs.hasColumn(title) && rs.getObject(title) != null)
+ sb.append("Added to group: ").append(rs.getString(title)).append("\n");
+
+ return sb.toString();
+ }
+}
diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/history/AnimalGroupsEndDataSource.java b/nbri_ehr/src/org/labkey/nbri_ehr/history/AnimalGroupsEndDataSource.java
new file mode 100644
index 0000000..dfdf187
--- /dev/null
+++ b/nbri_ehr/src/org/labkey/nbri_ehr/history/AnimalGroupsEndDataSource.java
@@ -0,0 +1,71 @@
+/*
+ * 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.history;
+
+import org.jetbrains.annotations.NotNull;
+import org.labkey.api.data.CompareType;
+import org.labkey.api.data.Container;
+import org.labkey.api.data.Results;
+import org.labkey.api.data.SimpleFilter;
+import org.labkey.api.ehr.history.AbstractDataSource;
+import org.labkey.api.ehr.history.HistoryRow;
+import org.labkey.api.module.Module;
+import org.labkey.api.query.FieldKey;
+import org.labkey.api.security.User;
+import org.labkey.api.util.PageFlowUtil;
+
+import java.sql.SQLException;
+import java.util.List;
+import java.util.Set;
+
+public class AnimalGroupsEndDataSource extends AbstractDataSource
+{
+ public AnimalGroupsEndDataSource(Module module)
+ {
+ super("study", "animal_group_members", "Removed From Group", "Animal Groups", module);
+ }
+
+ @Override
+ protected String getDateField()
+ {
+ return "enddate";
+ }
+
+ @Override
+ protected @NotNull List getRows(Container c, User u, SimpleFilter filter, boolean redacted)
+ {
+ filter.addCondition(FieldKey.fromString(getDateField()), null, CompareType.NONBLANK);
+ return super.getRows(c, u, filter, redacted);
+ }
+
+ @Override
+ protected Set getColumnNames()
+ {
+ return PageFlowUtil.set("Id", "date", "enddate", "groupId", "groupId/title");
+ }
+
+ @Override
+ protected String getHtml(Container c, Results rs, boolean redacted) throws SQLException
+ {
+ StringBuilder sb = new StringBuilder();
+
+ FieldKey title = FieldKey.fromString("groupId/title");
+ if (rs.hasColumn(title) && rs.getObject(title) != null)
+ sb.append("Removed from group: ").append(rs.getString(title)).append("\n");
+
+ return sb.toString();
+ }
+}
diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/table/NBRI_EHRCustomizer.java b/nbri_ehr/src/org/labkey/nbri_ehr/table/NBRI_EHRCustomizer.java
index b365680..7f4406b 100644
--- a/nbri_ehr/src/org/labkey/nbri_ehr/table/NBRI_EHRCustomizer.java
+++ b/nbri_ehr/src/org/labkey/nbri_ehr/table/NBRI_EHRCustomizer.java
@@ -36,6 +36,8 @@
import org.labkey.api.ehr.security.EHRDataEntryPermission;
import org.labkey.api.ehr.security.EHRVeterinarianPermission;
import org.labkey.api.ehr.table.FixedWidthDisplayColumn;
+import org.labkey.api.ehr.table.TreatmentLinkConfig;
+import org.labkey.api.ehr.table.TreatmentLinkDisplayColumnFactory;
import org.labkey.api.exp.api.StorageProvisioner;
import org.labkey.api.exp.property.Domain;
import org.labkey.api.gwt.client.FacetingBehaviorType;
@@ -67,6 +69,10 @@
public class NBRI_EHRCustomizer extends AbstractTableCustomizer
{
+ private static final TreatmentLinkConfig RECORD_TREATMENT = TreatmentLinkConfig.builder()
+ .formTypes("Behavior", "Behavioral Rounds", "Bulk Behavior Entry")
+ .build();
+
public UserSchema getEHRUserSchema(AbstractTableInfo ds, String name)
{
Container ehrContainer = EHRService.get().getEHRStudyContainer(ds.getUserSchema().getContainer());
@@ -926,6 +932,21 @@ private void customizeAnimalTable(AbstractTableInfo ds)
prjAssignment.setDescription("Shows all project to which the animal is actively assigned on the current date");
ds.addColumn(prjAssignment);
}
+ if (ds.getColumn("activeAnimalGroups") == null)
+ {
+ var activeGroups = getWrappedCol(us, ds, "activeAnimalGroups", "demographicsActiveAnimalGroups", "Id", "Id");
+ activeGroups.setLabel("Animal Groups - Active");
+ activeGroups.setDescription("Displays the animal groups to which this animal currently belongs");
+ ds.addColumn(activeGroups);
+ }
+ if (ds.getColumn("animalGroupsPivoted") == null)
+ {
+ var groupsPivoted = getWrappedCol(us, ds, "animalGroupsPivoted", "animalGroupsPivoted", "Id", "Id");
+ groupsPivoted.setLabel("Active Group Summary");
+ groupsPivoted.setHidden(true);
+ groupsPivoted.setDescription("Displays the active groups for each animal");
+ ds.addColumn(groupsPivoted);
+ }
if (ds.getColumn("alias") == null)
{
var col = getWrappedCol(us, ds, "alias", "demographicsAliases", "Id", "Id");
@@ -1008,7 +1029,7 @@ private void customizeTreatmentOrder(AbstractTableInfo ti)
{
WrappedColumn col = new WrappedColumn(ti.getColumn("objectid"), "treatmentRecord");
col.setLabel("Record Treatment");
- col.setDisplayColumnFactory(new TreatmentDisplayColumnFactory(false));
+ col.setDisplayColumnFactory(TreatmentLinkDisplayColumnFactory.forOrder(RECORD_TREATMENT));
ti.addColumn(col);
}
}
@@ -1019,7 +1040,7 @@ private void customizeTreatmentSchedule(AbstractTableInfo ti)
{
WrappedColumn col = new WrappedColumn(ti.getColumn("objectid"), "treatmentRecord");
col.setLabel("Record Treatment");
- col.setDisplayColumnFactory(new TreatmentDisplayColumnFactory(true));
+ col.setDisplayColumnFactory(TreatmentLinkDisplayColumnFactory.forSchedule(RECORD_TREATMENT));
ti.addColumn(col);
}
}
diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/table/TreatmentDisplayColumnFactory.java b/nbri_ehr/src/org/labkey/nbri_ehr/table/TreatmentDisplayColumnFactory.java
deleted file mode 100644
index 83db906..0000000
--- a/nbri_ehr/src/org/labkey/nbri_ehr/table/TreatmentDisplayColumnFactory.java
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- * 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.table;
-
-import org.labkey.api.data.ColumnInfo;
-import org.labkey.api.data.DataColumn;
-import org.labkey.api.data.DisplayColumn;
-import org.labkey.api.data.DisplayColumnFactory;
-import org.labkey.api.data.RenderContext;
-import org.labkey.api.ehr.security.EHRClinicalEntryPermission;
-import org.labkey.api.query.FieldKey;
-import org.labkey.api.util.DateUtil;
-import org.labkey.api.util.LinkBuilder;
-import org.labkey.api.view.ActionURL;
-import org.labkey.api.writer.HtmlWriter;
-
-import java.util.Date;
-import java.util.Set;
-
-/**
- * Display column factory for creating Record Treatment links. When includeScheduledDate is set, the row's date is
- * passed as the scheduledDate URL parameter, so it should only be set on tables whose date column is the scheduled
- * slot being recorded (e.g. treatmentSchedule), not the treatment order's start date.
- */
-public class TreatmentDisplayColumnFactory implements DisplayColumnFactory
-{
- private final boolean _includeScheduledDate;
-
- public TreatmentDisplayColumnFactory(boolean includeScheduledDate)
- {
- _includeScheduledDate = includeScheduledDate;
- }
-
- @Override
- public DisplayColumn createRenderer(final ColumnInfo colInfo)
- {
- return new DataColumn(colInfo){
-
- @Override
- public void renderGridCellContents(RenderContext ctx, HtmlWriter out)
- {
- String objectid = (String)getBoundColumn().getValue(ctx);
- Date date = (Date)ctx.get("date");
- String caseid = (String)ctx.get("caseid");
- String category = (String)ctx.get("category");
- ActionURL url = new ActionURL("ehr", "dataEntryForm", colInfo.getParentTable().getUserSchema().getContainer());
- if (!colInfo.getParentTable().getUserSchema().getContainer().hasPermission(colInfo.getParentTable().getUserSchema().getUser(), EHRClinicalEntryPermission.class))
- return;
-
- if (category == null)
- return;
-
- if (category.equals("Behavior"))
- {
- if (caseid != null)
- {
- url.addParameter("formType", "Behavioral Rounds");
- url.addParameter("caseid", caseid);
- }
- else
- {
- url.addParameter("formType", "Bulk Behavior Entry");
- }
- }
- else
- {
- if (caseid != null)
- {
- url.addParameter("formType", "Clinical Rounds");
- url.addParameter("caseid", caseid);
- }
- else
- {
- url.addParameter("formType", "medicationTreatment");
- }
- }
-
- url.addParameter("treatmentid", objectid);
- if (_includeScheduledDate && date != null)
- url.addParameter("scheduledDate", DateUtil.formatIsoDateShortTime(date));
-
- String returnUrl = new ActionURL("ehr", "animalHistory", colInfo.getParentTable().getUserSchema().getContainer()) + "#inputType:none&showReport:0&activeReport:clinMedicationSchedule";
- url.addParameter("returnUrl", returnUrl);
-
- out.write(LinkBuilder.labkeyLink("Record Treatment", url).target("_blank"));
- }
-
- @Override
- public void addQueryFieldKeys(Set keys)
- {
- super.addQueryFieldKeys(keys);
- keys.add(getBoundColumn().getFieldKey());
- keys.add(FieldKey.fromString("date"));
- keys.add(FieldKey.fromString("caseid"));
- keys.add(FieldKey.fromString("category"));
- }
-
- @Override
- public boolean isSortable()
- {
- return false;
- }
-
- @Override
- public boolean isFilterable()
- {
- return false;
- }
-
- @Override
- public boolean isEditable()
- {
- return false;
- }
- };
- }
-}