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/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/src/org/labkey/nbri_ehr/NBRI_EHRModule.java b/nbri_ehr/src/org/labkey/nbri_ehr/NBRI_EHRModule.java
index aa76225..8cbb3f0 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));
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/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 4c39ae6..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
@@ -932,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");