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:
@@ -56,6 +56,46 @@ export function isFieldValueEmpty(value: unknown, fieldType?: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Отображаемый текст одного элемента user-поля (`user:<id>` / `role:<id>` / legacy-число).
|
||||
*/
|
||||
function resolveUserEntryText(entry: string, context: FieldConditionContext): string {
|
||||
if (entry.startsWith("user:")) {
|
||||
const uid = Number(entry.replace("user:", ""));
|
||||
const user = context.users?.find((u) => u.id === uid);
|
||||
if (user) {
|
||||
return (
|
||||
user.fullName ||
|
||||
`${user.firstName || ""} ${user.middleName || ""} ${user.lastName || ""}`.trim() ||
|
||||
user.email ||
|
||||
entry
|
||||
);
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
if (entry.startsWith("role:")) {
|
||||
const rid = Number(entry.replace("role:", ""));
|
||||
const role = context.roles?.find((r) => r.id === rid);
|
||||
if (role) {
|
||||
return `Роль: ${role.name || entry}`;
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
const uid = Number(entry);
|
||||
if (!isNaN(uid)) {
|
||||
const user = context.users?.find((u) => u.id === uid);
|
||||
if (user) {
|
||||
return (
|
||||
user.fullName ||
|
||||
`${user.firstName || ""} ${user.middleName || ""} ${user.lastName || ""}`.trim() ||
|
||||
user.email ||
|
||||
entry
|
||||
);
|
||||
}
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Преобразует хранимое значение поля в строку для поиска (отображаемый текст).
|
||||
*/
|
||||
@@ -75,40 +115,23 @@ export function resolveFieldDisplayText(
|
||||
|
||||
switch (fieldType) {
|
||||
case "user": {
|
||||
if (rawStr.startsWith("user:")) {
|
||||
const uid = Number(rawStr.replace("user:", ""));
|
||||
const user = context.users?.find((u) => u.id === uid);
|
||||
if (user) {
|
||||
return (
|
||||
user.fullName ||
|
||||
`${user.firstName || ""} ${user.middleName || ""} ${user.lastName || ""}`.trim() ||
|
||||
user.email ||
|
||||
rawStr
|
||||
);
|
||||
}
|
||||
return rawStr;
|
||||
}
|
||||
if (rawStr.startsWith("role:")) {
|
||||
const rid = Number(rawStr.replace("role:", ""));
|
||||
const role = context.roles?.find((r) => r.id === rid);
|
||||
if (role) {
|
||||
return `Роль: ${role.name || rawStr}`;
|
||||
}
|
||||
return rawStr;
|
||||
}
|
||||
const uid = Number(rawStr);
|
||||
if (!isNaN(uid)) {
|
||||
const user = context.users?.find((u) => u.id === uid);
|
||||
if (user) {
|
||||
return (
|
||||
user.fullName ||
|
||||
`${user.firstName || ""} ${user.middleName || ""} ${user.lastName || ""}`.trim() ||
|
||||
user.email ||
|
||||
rawStr
|
||||
);
|
||||
// Мультивыбор (allow_multiple): массив ["user:5", "role:2"] — имена через запятую
|
||||
let parsedVal: unknown = value;
|
||||
if (typeof value === "string" && value.startsWith("[")) {
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
if (Array.isArray(parsed)) parsedVal = parsed;
|
||||
} catch {
|
||||
parsedVal = value;
|
||||
}
|
||||
}
|
||||
return rawStr;
|
||||
if (Array.isArray(parsedVal)) {
|
||||
return parsedVal
|
||||
.map((entry) => resolveUserEntryText(safeString(entry), context))
|
||||
.filter(Boolean)
|
||||
.join(", ");
|
||||
}
|
||||
return resolveUserEntryText(rawStr, context);
|
||||
}
|
||||
|
||||
case "task": {
|
||||
|
||||
Reference in New Issue
Block a user