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:
@@ -9,6 +9,7 @@ import { authService } from "./services/auth.service";
|
||||
import { FORM_FIELD_TYPES, GLOBAL_FIELD_TYPES, TABLE_FIELD_TYPES } from "../shared/field-types";
|
||||
import { normalizeFieldValueForStorage } from "./utils/normalize-field-value";
|
||||
import { normalizeFileUrl, normalizeFileFieldUrls } from "./utils/upload";
|
||||
import { parseUserFieldEntries } from "./utils/user-field-value";
|
||||
|
||||
|
||||
// In-process dedup for legacy-key admin notifications (reset on process restart)
|
||||
@@ -948,14 +949,27 @@ function buildMcpServer(organizationId: number, scopes: ApiKeyScopes, apiKeyReco
|
||||
}
|
||||
}
|
||||
|
||||
// Sync task_assignees из user-полей, чтобы задача попадала в inbox (как REST POST — только добавление)
|
||||
// Sync task_assignees из user-полей, чтобы задача попадала в inbox (как REST POST — только добавление).
|
||||
// Мультивыбор: массив ["user:5", "role:2"]; role:<id> → все члены роли.
|
||||
for (const fv of fieldValues) {
|
||||
const field = fieldMap.get(fv.fieldId)!;
|
||||
if (field.type !== 'user') continue;
|
||||
const raw = fv.value === null || fv.value === undefined || fv.value === '' ? '' : String(fv.value);
|
||||
const userId = raw ? Number(raw.replace(/^user:/, '')) : NaN;
|
||||
if (!isNaN(userId) && userId > 0) {
|
||||
await storage.addTaskAssignee(taskId, userId, organizationId).catch(() => {});
|
||||
for (const entry of parseUserFieldEntries(fv.value)) {
|
||||
if (entry.startsWith('role:')) {
|
||||
const roleId = Number(entry.replace('role:', ''));
|
||||
if (isNaN(roleId)) continue;
|
||||
const members = await storage.getRoleMembersByRole(roleId).catch(() => []);
|
||||
for (const m of members) {
|
||||
if (m.userId) {
|
||||
await storage.addTaskAssignee(taskId, m.userId, organizationId).catch(() => {});
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const userId = Number(entry.replace(/^user:/, ''));
|
||||
if (!isNaN(userId) && userId > 0) {
|
||||
await storage.addTaskAssignee(taskId, userId, organizationId).catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2409,9 +2423,10 @@ Users:
|
||||
- ctx.users.get(userId) — get one user
|
||||
- ctx.users.update(userId, updates) — update user fields (firstName, lastName, position, statusId, etc.)
|
||||
- ctx.users.updateStatus(userId, statusId) — change user status
|
||||
- ctx.users.setFieldValue(userId, fieldCodeOrId, value) — set a user profile custom field value by field id or code (with audit as 'Автоматизация'). For fields of type 'history-number' the numeric value is also appended to user_field_history (linked to the trigger task); a non-numeric value throws an error.
|
||||
|
||||
Trigger types: 'manual', 'task.before_create', 'task.created', 'task.status_changed', 'qr.scan', 'schedule'
|
||||
TriggerConfig for task.before_create / task.created / task.status_changed: { formId: number, statusId?: number }
|
||||
TriggerConfig for task.before_create / task.created / task.status_changed: { formId: number, statusId?: number } — statusId applies only to task.status_changed and limits firing to transitions INTO that status
|
||||
TriggerConfig for qr.scan: { templateKey: string }
|
||||
TriggerConfig for schedule: { time: "HH:MM" } — daily run at this Moscow time (MSK, UTC+3); reruns within the same day are prevented via last_scheduled_run_at
|
||||
|
||||
|
||||
Reference in New Issue
Block a user