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

@@ -326,6 +326,39 @@ export const userProfileFieldValuesRelations = relations(userProfileFieldValues,
}),
}));
// User field history table (numeric history of user profile fields with type history-number)
export const userFieldHistory = pgTable("user_field_history", {
id: serial("id").primaryKey(),
organizationId: integer("organization_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
userId: integer("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
fieldId: integer("field_id").notNull().references(() => userProfileFields.id, { onDelete: "cascade" }),
value: numeric("value").notNull(),
taskId: integer("task_id").references(() => tasks.id, { onDelete: "set null" }), // задача, из которой выставлена оценка (может быть null)
createdAt: timestamp("created_at").defaultNow(),
}, (table) => ({
userFieldCreatedIdx: index("user_field_history_user_field_created_idx").on(table.userId, table.fieldId, table.createdAt),
orgIdx: index("user_field_history_org_idx").on(table.organizationId),
}));
export const userFieldHistoryRelations = relations(userFieldHistory, ({ one }) => ({
organization: one(organizations, {
fields: [userFieldHistory.organizationId],
references: [organizations.id],
}),
user: one(users, {
fields: [userFieldHistory.userId],
references: [users.id],
}),
field: one(userProfileFields, {
fields: [userFieldHistory.fieldId],
references: [userProfileFields.id],
}),
task: one(tasks, {
fields: [userFieldHistory.taskId],
references: [tasks.id],
}),
}));
// User profile audit log table (history of user profile changes)
export const userProfileAuditLog = pgTable("user_profile_audit_log", {
id: serial("id").primaryKey(),
@@ -1049,6 +1082,11 @@ export const insertUserProfileAuditLogSchema = createInsertSchema(userProfileAud
createdAt: true,
});
export const insertUserFieldHistorySchema = createInsertSchema(userFieldHistory).omit({
id: true,
createdAt: true,
});
export const insertFormSchema = createInsertSchema(forms).omit({
id: true,
createdAt: true,
@@ -1336,10 +1374,12 @@ export type UserProfileTab = typeof userProfileTabs.$inferSelect;
export type UserProfileField = typeof userProfileFields.$inferSelect;
export type UserProfileFieldValue = typeof userProfileFieldValues.$inferSelect;
export type UserProfileAuditLog = typeof userProfileAuditLog.$inferSelect;
export type UserFieldHistory = typeof userFieldHistory.$inferSelect;
export type InsertUserProfileTab = z.infer<typeof insertUserProfileTabSchema>;
export type InsertUserProfileField = z.infer<typeof insertUserProfileFieldSchema>;
export type InsertUserProfileFieldValue = z.infer<typeof insertUserProfileFieldValueSchema>;
export type InsertUserProfileAuditLog = z.infer<typeof insertUserProfileAuditLogSchema>;
export type InsertUserFieldHistory = z.infer<typeof insertUserFieldHistorySchema>;
export type InsertForm = z.infer<typeof insertFormSchema>;
export type InsertFormTab = z.infer<typeof insertFormTabSchema>;
export type InsertFormField = z.infer<typeof insertFormFieldSchema>;