feat(users): выбор колонок в реестре пользователей + кликабельная Оценка и доработанный график истории

Реестр /users:
- кнопка «Колонки» — чекбоксы базовых колонок и кастомных полей профиля
  (Оценка, Средняя оценка и др.), скрытые ключи в localStorage;
- /api/users/list?includeFieldValues=1 — значения полей текущей страницы;
- поля типа history-number (Оценка) кликабельны — открывают график истории.

График истории (FieldHistoryDialog):
- точки красные и крупные (на тёмном фоне дефолтные не видны), линия primary;
- клик по точке — под графиком список оценок за бакет со ссылками на задачи
  (название задачи → /forms/<formId>/tasks/<id>);
- API field-history возвращает entries (value, taskId, taskTitle, formId, bucket).
This commit is contained in:
2026-09-29 15:09:22 +03:00
parent c56ec26b49
commit c0011b49a3
5 changed files with 330 additions and 44 deletions

View File

@@ -565,6 +565,49 @@ export class UserProfileStorage extends UsersStorage {
};
}
// Сырые записи истории с привязкой к задачам — для клика по точке графика
// (список задач за бакет: название + ссылка). bucket считается тем же
// date_trunc, что и у точек графика, чтобы клиент мог их сопоставить.
async getUserFieldHistoryEntries(
userId: number,
fieldId: number,
organizationId: number,
days: number,
bucket: 'day' | 'week' | 'month'
): Promise<Array<{
value: number;
taskId: number | null;
taskTitle: string | null;
formId: number | null;
bucket: string;
createdAt: string;
}>> {
const result = await db.execute(sql`
SELECT ufh.value::float8 AS value,
ufh.task_id AS "taskId",
t.title AS "taskTitle",
t.form_id AS "formId",
to_char(date_trunc(${bucket}, ufh.created_at), 'YYYY-MM-DD') AS bucket,
ufh.created_at AS "createdAt"
FROM user_field_history ufh
LEFT JOIN tasks t ON ufh.task_id = t.id
WHERE ufh.user_id = ${userId}
AND ufh.field_id = ${fieldId}
AND ufh.organization_id = ${organizationId}
AND ufh.created_at >= now() - make_interval(days => ${days})
ORDER BY ufh.created_at DESC
LIMIT 500
`);
return result.rows as Array<{
value: number;
taskId: number | null;
taskTitle: string | null;
formId: number | null;
bucket: string;
createdAt: string;
}>;
}
// ========== DETAIL ==========
async getUserProfileDetail(
userId: number,