feat: замещение пользователей (substitution) — заместители по умолчанию и по формам, контур ознакомления, inbox reasons

- таблица user_delegations (миграция 0067) + колонки delegated_from_user_id/pending_review в task_assignees
- delegation.service: резолвер с цепочкой и защитой от циклов, bulk-до-назначение чанками, чистка истёкших замещений
- центральный хук в addTaskAssignee: заместитель добавляется при любом назначении (ручном, автопереходе, user-поле, MCP)
- контур ознакомления: после перехода отсутствующий остаётся в исполнителях с pending_review, снимается кнопкой «Ознакомлен»
- inbox reasons delegation/delegation_review + фильтр «Замещение» на главной
- блок «Замещение» в профиле пользователя, бейдж «за <имя>» и баннер ознакомления в карточке задачи
This commit is contained in:
2026-08-05 10:44:55 +03:00
parent 17a8dc42f2
commit 667bddeac9
15 changed files with 1216 additions and 12 deletions

View File

@@ -6,6 +6,7 @@ import { tasksMinimalCache } from "../utils/cache";
import { indexTaskAsync } from "../routes/task-helpers";
import { fieldConditionsMatch, resolveFieldDisplayText, isFieldValueEmpty, SYSTEM_TASK_FIELDS, buildSystemFieldValuesForConditions } from "../utils/field-conditions";
import { validateRequiredFieldsDetailed } from "../utils/validate-required-fields";
import { ensureReviewAssignees, cleanupStaleDelegates } from "./delegation.service";
export interface ExecuteTaskTransitionOptions {
taskId: number;
@@ -426,6 +427,11 @@ export async function executeTaskTransition(options: ExecuteTaskTransitionOption
});
}
// Замещение: вернуть отсутствующих исполнителей «на ознакомление»
// и снять заместителей с истёкшим замещением
await ensureReviewAssignees(taskId, organizationId);
await cleanupStaleDelegates(taskId, organizationId);
// If a role was resolved as assignee — assign it to the task
if (resolvedRoleId !== null) {
await storage.addTaskRole(taskId, resolvedRoleId, organizationId).catch(err => {