feat: рейтинговые автоматизации, мультивыбор user/role-полей, график history-number

Инфраструктура автоматизаций:
- триггер task.status_changed оживлён: вызов из executeTaskTransition (fire-and-forget) + фильтр triggerConfig.statusId
- ctx.users.setFieldValue(userId, fieldCodeOrId, value): запись user_profile_field_values с аудитом; для history-number — append в user_field_history (миграция 0078)
- документация sandbox обновлена (Automations.tsx, mcp.ts, AGENTS.md) по критическому правилу 2

Мультивыбор пользователей и ролей в user-поле:
- чекбокс «Множественный выбор (пользователи и роли)» в настройках поля (form_fields.allow_multiple)
- значение — массив ['user:ID','role:ID'], обратная совместимость с одиночными/legacy значениями
- UserRoleMultiSelect в карточке задачи, реестре и диалоге создания (роли добавлены и в одиночный сценарий создания)
- синк в task_assignees итерирует массив; role:ID раскрывается в членов роли; PATCH single — diff старого/нового значений
- отображение массивов: реестр, title-шаблоны, условия полей, MCP update_task_fields
- список орг-ролей (/api/roles) загружается всем пользователям (сервер и так был открыт) — роли доступны не только админам

График history-number в профиле:
- GET /api/users/:id/field-history/:fieldId?period=week|month|quarter|year (агрегация AVG по бакетам)
- диалог с LineChart (recharts) и переключателем периодов по клику на иконку у поля
This commit is contained in:
2026-09-04 19:39:10 +03:00
parent 013e97c8d3
commit 4d65049e02
27 changed files with 983 additions and 112 deletions

View File

@@ -7,6 +7,7 @@ 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";
import { runAutomationsByTrigger } from "../routes/automation.routes";
export interface ExecuteTaskTransitionOptions {
taskId: number;
@@ -528,6 +529,18 @@ export async function executeTaskTransition(options: ExecuteTaskTransitionOption
indexTaskAsync(taskId, organizationId).catch(() => {});
// Автоматизации по триггеру task.status_changed — fire-and-forget,
// чтобы ошибки/таймауты скриптов не ломали сам переход
if (targetStatusId !== transition.fromStatusId) {
runAutomationsByTrigger(organizationId, 'task.status_changed', {
formId: existingTask.formId,
taskId,
oldStatusId: transition.fromStatusId,
newStatusId: targetStatusId,
task: updatedTask,
}).catch(err => console.error('[automation] task.status_changed error:', err));
}
return {
success: true,
message: 'Переход выполнен успешно',