diff --git a/ehr/api-src/org/labkey/api/ehr/buttons/DiscardEmptyTasksButton.java b/ehr/api-src/org/labkey/api/ehr/buttons/DiscardEmptyTasksButton.java new file mode 100644 index 000000000..9387dff36 --- /dev/null +++ b/ehr/api-src/org/labkey/api/ehr/buttons/DiscardEmptyTasksButton.java @@ -0,0 +1,35 @@ +/* + * 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.api.ehr.buttons; + +import org.labkey.api.data.TableInfo; +import org.labkey.api.ehr.security.EHRDataAdminPermission; +import org.labkey.api.ldk.table.SimpleButtonConfigFactory; +import org.labkey.api.module.Module; + +public class DiscardEmptyTasksButton extends SimpleButtonConfigFactory +{ + public DiscardEmptyTasksButton(Module owner) + { + super(owner, "Delete Empty Tasks", "EHR.DatasetButtons.discardEmptyTasks(dataRegionName);"); + } + + @Override + public boolean isAvailable(TableInfo ti) + { + return super.isAvailable(ti) && ti.getUserSchema().getContainer().hasPermission(ti.getUserSchema().getUser(), EHRDataAdminPermission.class); + } +} diff --git a/ehr/resources/web/ehr/studyButtons.js b/ehr/resources/web/ehr/studyButtons.js index fc99b1ae8..adfc6fb37 100644 --- a/ehr/resources/web/ehr/studyButtons.js +++ b/ehr/resources/web/ehr/studyButtons.js @@ -266,6 +266,34 @@ EHR.DatasetButtons = new function () { }, this); }, + discardEmptyTasks: function (dataRegionName) { + var dataRegion = LABKEY.DataRegions[dataRegionName]; + var checked = dataRegion.getChecked(); + if (!checked || !checked.length) { + Ext4.Msg.alert('Error', 'No records selected'); + return; + } + + Ext4.Msg.confirm('Delete Empty Tasks', + 'Permanently delete the selected task(s)? This will fail if any selected task has related data in a study dataset. Note: Task related data in other schemas is not checked.', + function (val) { + if (val !== 'yes') return; + Ext4.Msg.wait('Deleting...'); + LABKEY.Ajax.request({ + url: LABKEY.ActionURL.buildURL('ehr', 'discardEmptyTasks', null, {taskIds: checked}), + method: 'POST', + success: function (response) { + Ext4.Msg.hide(); + var json = LABKEY.Utils.decode(response.responseText) || {}; + Ext4.Msg.alert('Success', 'Deleted ' + (json.deletedCount || 0) + ' task(s).'); + dataRegion.refresh(); + }, + failure: LDK.Utils.getErrorCallback(), + scope: this + }); + }, this); + }, + limitKinshipSelection: function (dataRegionName) { var dataRegion = LABKEY.DataRegions[dataRegionName]; diff --git a/ehr/src/org/labkey/ehr/EHRController.java b/ehr/src/org/labkey/ehr/EHRController.java index 7bf750c76..c02738ba3 100644 --- a/ehr/src/org/labkey/ehr/EHRController.java +++ b/ehr/src/org/labkey/ehr/EHRController.java @@ -49,6 +49,7 @@ import org.labkey.api.ehr.dataentry.DataEntryForm; import org.labkey.api.ehr.demographics.AnimalRecord; import org.labkey.api.ehr.history.HistoryRow; +import org.labkey.api.ehr.security.EHRDataAdminPermission; import org.labkey.api.ehr.security.EHRDataEntryPermission; import org.labkey.api.exp.api.ExperimentService; import org.labkey.api.gwt.client.AuditBehaviorType; @@ -60,6 +61,7 @@ import org.labkey.api.query.BatchValidationException; import org.labkey.api.query.DetailsURL; import org.labkey.api.query.FieldKey; +import org.labkey.api.query.InvalidKeyException; import org.labkey.api.query.QueryAction; import org.labkey.api.query.QueryForm; import org.labkey.api.query.QueryParseException; @@ -331,6 +333,137 @@ public ApiResponse execute(DiscardFormForm form, BindException errors) } } + public static class DiscardEmptyTasksForm + { + private String[] taskIds; + + public String[] getTaskIds() + { + return taskIds; + } + + public void setTaskIds(String[] taskIds) + { + this.taskIds = taskIds; + } + } + + @RequiresPermission(EHRDataAdminPermission.class) + public static class DiscardEmptyTasksAction extends MutatingApiAction + { + private List _selectedTaskIds; + private TableInfo _tasksTable; + + @Override + public void validateForm(DiscardEmptyTasksForm form, Errors errors) + { + super.validateForm(form, errors); + + if (form.getTaskIds() == null || form.getTaskIds().length == 0) + { + errors.reject(ERROR_MSG, "No tasks selected."); + return; + } + _selectedTaskIds = Arrays.asList(form.getTaskIds()); + + UserSchema ehrSchema = QueryService.get().getUserSchema(getUser(), getContainer(), EHRSchema.EHR_SCHEMANAME); + if (ehrSchema == null) + { + errors.reject(ERROR_MSG, "EHR schema is not available in this container."); + return; + } + + _tasksTable = ehrSchema.getTable(EHRSchema.TABLE_TASKS); + if (_tasksTable == null) + { + errors.reject(ERROR_MSG, "ehr.tasks table is not available in this container."); + return; + } + + Set nonEmpty = new LinkedHashSet<>(); + UserSchema studySchema = QueryService.get().getUserSchema(getUser(), getContainer(), "study"); + if (studySchema != null) + { + TableInfo studyData = studySchema.getTable("StudyData"); + if (studyData != null && studyData.getColumn("taskid") != null) + { + SimpleFilter filter = new SimpleFilter(FieldKey.fromString("taskid"), _selectedTaskIds, CompareType.IN); + String[] ids = new TableSelector(studyData, Collections.singleton("taskid"), filter, null).getArray(String.class); + if (ids != null) + { + for (String id : ids) + { + if (id != null) + nonEmpty.add(id); + } + } + } + } + + if (!nonEmpty.isEmpty()) + { + errors.reject(ERROR_MSG, "Cannot delete: " + nonEmpty.size() + " of the selected task(s) have associated dataset records. Task ID(s): " + String.join(", ", nonEmpty)); + } + } + + @Override + public ApiResponse execute(DiscardEmptyTasksForm form, BindException errors) + { + int deleted; + try (DbScope.Transaction transaction = ExperimentService.get().ensureTransaction()) + { + deleted = deleteRowsByTaskIds(_tasksTable, _selectedTaskIds); + transaction.commit(); + } + catch (SQLException e) + { + throw new RuntimeSQLException(e); + } + + Map resultProperties = new HashMap<>(); + resultProperties.put("success", true); + resultProperties.put("deletedCount", deleted); + return new ApiSimpleResponse(resultProperties); + } + + private int deleteRowsByTaskIds(TableInfo ti, List taskIds) throws SQLException + { + QueryUpdateService qus = ti.getUpdateService(); + if (qus == null) + return 0; + + int total = 0; + final int chunkSize = 1000; + for (int start = 0; start < taskIds.size(); start += chunkSize) + { + List chunk = taskIds.subList(start, Math.min(start + chunkSize, taskIds.size())); + SimpleFilter filter = new SimpleFilter(FieldKey.fromString("taskid"), chunk, CompareType.IN); + String[] pkColumns = ti.getPkColumnNames().toArray(new String[0]); + Set selectCols = new LinkedHashSet<>(Arrays.asList(pkColumns)); + selectCols.add("taskid"); + + Map[] rows = new TableSelector(ti, selectCols, filter, null).getMapArray(); + if (rows == null || rows.length == 0) + continue; + + List> keys = new ArrayList<>(rows.length); + for (Map row : rows) + keys.add(new HashMap<>(row)); + + try + { + qus.deleteRows(getUser(), getContainer(), keys, null, new HashMap<>()); + } + catch (InvalidKeyException | QueryUpdateServiceException | BatchValidationException e) + { + throw new RuntimeException(e); + } + total += rows.length; + } + return total; + } + } + public static class EHRQueryForm extends QueryForm { private boolean _showImport = false; diff --git a/ehr/src/org/labkey/ehr/EHRModule.java b/ehr/src/org/labkey/ehr/EHRModule.java index 953be0a11..5ee3f873d 100644 --- a/ehr/src/org/labkey/ehr/EHRModule.java +++ b/ehr/src/org/labkey/ehr/EHRModule.java @@ -25,6 +25,7 @@ import org.labkey.api.data.UpgradeCode; import org.labkey.api.ehr.EHRDemographicsService; import org.labkey.api.ehr.EHRService; +import org.labkey.api.ehr.buttons.DiscardEmptyTasksButton; import org.labkey.api.ehr.buttons.EHRShowEditUIButton; import org.labkey.api.ehr.buttons.MarkCompletedButton; import org.labkey.api.ehr.demographics.ActiveAssignmentsDemographicsProvider; @@ -249,6 +250,7 @@ public void moduleStartupComplete(ServletContext servletContext) EHRService.get().registerMoreActionsButton(new CompareWeightsButton(this), "study", "weight"); EHRService.get().registerMoreActionsButton(new TaskAssignButton(this), "ehr", "my_tasks"); EHRService.get().registerMoreActionsButton(new TaskAssignButton(this), "ehr", "tasks"); + EHRService.get().registerMoreActionsButton(new DiscardEmptyTasksButton(this), "ehr", "tasks"); EHRService.get().registerMoreActionsButton(new MarkCompletedButton(this, "study", "treatment_order", "Set End Date"), "study", "treatment_order"); EHRService.get().registerMoreActionsButton(new MarkCompletedButton(this, "study", "problem", "End Problem(s)", true), "study", "problem"); EHRService.get().registerMoreActionsButton(new MarkCompletedButton(this, "study", "feeding"), "study", "feeding");