From 4ace1369caec88f4b98f3e6f558126a8d9622378 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D1=8C=D1=8F=D1=81=20=D0=A1=D1=83=D0=BB=D1=82?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2?= Date: Mon, 3 Aug 2026 11:18:14 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20=D1=81=D0=BE=D0=B3=D0=BB=D0=B0=D1=81?= =?UTF-8?q?=D1=83=D1=8E=D1=89=D0=B8=D0=B5=20=D0=BF=D0=B5=D1=80=D0=B5=D1=85?= =?UTF-8?q?=D0=BE=D0=B4=D0=B0=20=E2=80=94=20=D1=82=D0=BE=D0=BB=D1=8C=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=B0=D0=B2=D1=82=D0=BE=D1=80=D0=B8=D0=B7=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8F,=20=D0=B1=D0=B5=D0=B7=20=D0=B0=D0=B2=D1=82=D0=BE?= =?UTF-8?q?=D0=BD=D0=B0=D0=B7=D0=BD=D0=B0=D1=87=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit assigneeUserId/assigneeConditions перехода раньше и давали право нажать кнопку, и молча переназначали задачу на согласующего; кликнувший также всегда добавлялся в task_assignees (список «Ответственный»). Теперь ответственный при переходе всегда сохраняется, кликнувший в ответственные не добавляется (доступ трекается через recordTransitionAccess). В FormEditor добавлена подсказка о семантике блока «Согласующие». --- client/src/pages/FormEditor.tsx | 3 ++ server/services/task-transition.service.ts | 36 +++++----------------- 2 files changed, 10 insertions(+), 29 deletions(-) diff --git a/client/src/pages/FormEditor.tsx b/client/src/pages/FormEditor.tsx index 6291545..b948ab1 100644 --- a/client/src/pages/FormEditor.tsx +++ b/client/src/pages/FormEditor.tsx @@ -3869,6 +3869,9 @@ return function Layout({ ctx }) { +

+ Указанные пользователи/роли получают право выполнить этот переход. Ответственный задачи при переходе не меняется. +

{assigneeBlocks.map((block, bi) => { return ( diff --git a/server/services/task-transition.service.ts b/server/services/task-transition.service.ts index 572bd3e..34b987e 100644 --- a/server/services/task-transition.service.ts +++ b/server/services/task-transition.service.ts @@ -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 = { @@ -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); });