fix: согласующие перехода — только авторизация, без автоназначения

assigneeUserId/assigneeConditions перехода раньше и давали право нажать
кнопку, и молча переназначали задачу на согласующего; кликнувший также
всегда добавлялся в task_assignees (список «Ответственный»).
Теперь ответственный при переходе всегда сохраняется, кликнувший в
ответственные не добавляется (доступ трекается через recordTransitionAccess).
В FormEditor добавлена подсказка о семантике блока «Согласующие».
This commit is contained in:
2026-08-03 11:18:14 +03:00
parent a701b06394
commit 4ace1369ca
2 changed files with 10 additions and 29 deletions

View File

@@ -3,7 +3,6 @@ import { webhookService } from "./webhook.service";
import { eventBus, resolveTaskFieldTitles } from "../routes/shared";
import { pushTaskUpdated } from "../utils/pushTaskUpdated";
import { tasksMinimalCache } from "../utils/cache";
import { notifyTaskAssigned } from "../utils/notifyAssignee";
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";
@@ -181,7 +180,6 @@ export async function executeTaskTransition(options: ExecuteTaskTransitionOption
let isAuthorized = false;
let resolvedRoleId: number | null = null; // track role to assign after transition
let resolvedAssigneeId: number | null = null; // old-format resolved user
let resolvedUserId: number | null = null; // new-format resolved user
// Check direct assigneeUserId (Type A simple — backward compat)
if (!isAuthorized && transition.assigneeUserId !== null && transition.assigneeUserId !== undefined) {
@@ -203,7 +201,6 @@ export async function executeTaskTransition(options: ExecuteTaskTransitionOption
if (!rulesMatch) continue;
if (assignee.type === 'user' && assignee.id === user?.id) {
resolvedUserId = assignee.id;
isAuthorized = true;
break;
}
@@ -227,7 +224,6 @@ export async function executeTaskTransition(options: ExecuteTaskTransitionOption
userId = fieldValue?.value ? parseInt(String(fieldValue.value)) : undefined;
}
if (userId && userId > 0 && userId === user?.id) {
resolvedUserId = userId;
isAuthorized = true;
break;
}
@@ -243,7 +239,6 @@ export async function executeTaskTransition(options: ExecuteTaskTransitionOption
if (row) {
const userId = parseInt(String((row.values as unknown[])[assignee.columnIndex as number] ?? ''));
if (!isNaN(userId) && userId > 0 && userId === user?.id) {
resolvedUserId = userId;
isAuthorized = true;
break;
}
@@ -401,19 +396,10 @@ export async function executeTaskTransition(options: ExecuteTaskTransitionOption
// Check if target status is final
const isFinalStatus = toStatus?.isFinal || false;
// Determine assignee based on transition rules (priority: direct > old format > new format > preserve current)
let targetAssigneeId: number | null = null;
if (transition.assigneeUserId !== null && transition.assigneeUserId !== undefined) {
targetAssigneeId = transition.assigneeUserId;
} else if (resolvedAssigneeId !== null) {
targetAssigneeId = resolvedAssigneeId;
} else if (resolvedUserId !== null) {
targetAssigneeId = resolvedUserId;
} else {
// If the transition does not explicitly specify an assignee, keep the existing one
// instead of defaulting to the user who clicked the button.
targetAssigneeId = existingTask.assignedTo ?? null;
}
// «Согласующие» перехода (assigneeUserId / assigneeConditions) — это ТОЛЬКО
// авторизация (кто может выполнить переход). Они НЕ меняют ответственного задачи:
// ответственный всегда сохраняется, автоназначения при переходе нет.
const targetAssigneeId: number | null = existingTask.assignedTo ?? null;
// Update task status
const updateData: Record<string, unknown> = {
@@ -440,12 +426,6 @@ export async function executeTaskTransition(options: ExecuteTaskTransitionOption
});
}
// Уведомление новому ответственному, если он изменился
if (targetAssigneeId && targetAssigneeId !== existingTask.assignedTo) {
notifyTaskAssigned(updatedTask, targetAssigneeId, user?.id ?? null, organizationId)
.catch((err) => console.error('[Transition] notifyTaskAssigned error:', err));
}
// If a role was resolved as assignee — assign it to the task
if (resolvedRoleId !== null) {
await storage.addTaskRole(taskId, resolvedRoleId, organizationId).catch(err => {
@@ -453,12 +433,10 @@ export async function executeTaskTransition(options: ExecuteTaskTransitionOption
});
}
// Add current user as task assignee (upsert — no duplicates)
// Доступ по переходу (grantedVia='transition', со statusId и roleId).
// ВНИМАНИЕ: кликнувшего НЕ добавляем в task_assignees — это список ответственных,
// а не участников; иначе любой выполнивший переход «становился ответственным».
if (user?.id) {
storage.addTaskAssignee(taskId, user.id, organizationId).catch(err => {
console.error('addTaskAssignee (transition) error:', err);
});
// Record transition access (grantedVia='transition', with statusId and roleId)
storage.recordTransitionAccess(taskId, user.id, organizationId, resolvedRoleId, targetStatusId).catch(err => {
console.error('recordTransitionAccess error:', err);
});