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:
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user