Рейтинг сотрудников: history-number поле = последняя оценка, среднее за месяц — в поле options.avgFieldCode

- setUserProfileFieldValueFromAutomation: для history-number значением поля становится переданное число (последняя оценка), а не среднее
- recomputeUserFieldMonthlyAverage: среднее за текущий месяц пишется в поле, указанное в options.avgFieldCode history-number поля (если не задано — в само поле, обратная совместимость)
- Автоматизация «Оценка сотрудникам» (форма 11): сумма трёх числовых полей «Оценка 1/2/3» вместо одного select-поля
- Документация sandbox обновлена во всех четырёх местах (UI, MCP, Obsidian, AGENTS.md)
This commit is contained in:
2026-09-06 11:55:15 +03:00
parent a7a14d947a
commit ec86fa4c11
3 changed files with 42 additions and 17 deletions

View File

@@ -151,7 +151,8 @@ export class UserProfileStorage extends UsersStorage {
throw new Error(`Поле профиля "${fieldCodeOrId}" не найдено`);
}
// Для history-number: переданное число идёт в историю, значением поля становится среднее за текущий месяц
// Для history-number: переданное число идёт в историю и становится значением поля (последняя оценка);
// среднее за месяц пересчитывается отдельно — в поле, указанном в options.avgFieldCode (или в само поле)
let storedValue = value;
if (field.type === 'history-number') {
const num = typeof value === 'number' ? value : parseFloat(String(value).replace(',', '.').trim());
@@ -165,7 +166,8 @@ export class UserProfileStorage extends UsersStorage {
value: String(num),
taskId: taskId ?? null,
});
storedValue = await this.recomputeUserFieldMonthlyAverage(userId, field.id, organizationId);
storedValue = num;
await this.recomputeUserFieldMonthlyAverage(userId, field.id, organizationId);
}
const existing = await this.getUserProfileFieldValue(userId, field.id);
@@ -190,10 +192,20 @@ export class UserProfileStorage extends UsersStorage {
}
/**
* Пересчитывает значение history-number поля как среднее за текущий календарный месяц.
* Если в месяце нет записей — поле очищается (null). Возвращает новое значение.
* Пересчитывает среднее за текущий календарный месяц по истории history-number поля.
* Среднее пишется в поле, указанное в options.avgFieldCode самого history-number поля
* (например «Оценка» → «Средняя оценка»); если avgFieldCode не задан — в само history-number поле.
* Если в месяце нет записей — целевое поле очищается (null). Возвращает новое среднее.
*/
async recomputeUserFieldMonthlyAverage(userId: number, fieldId: number, organizationId: number) {
const [field] = await db.select().from(userProfileFields).where(and(
eq(userProfileFields.id, fieldId),
eq(userProfileFields.organizationId, organizationId),
));
if (!field) {
throw new Error(`Поле профиля ${fieldId} не найдено`);
}
const [row] = await db
.select({ avg: sql<number | null>`ROUND(AVG(${userFieldHistory.value})::numeric, 2)::float8` })
.from(userFieldHistory)
@@ -205,24 +217,35 @@ export class UserProfileStorage extends UsersStorage {
));
const avg = row?.avg != null ? Number(row.avg) : null;
const existing = await this.getUserProfileFieldValue(userId, fieldId);
// Целевое поле для среднего: из options.avgFieldCode, иначе — само history-number поле
let targetFieldId = fieldId;
const avgFieldCode = (field.options as { avgFieldCode?: string } | null)?.avgFieldCode;
if (avgFieldCode) {
const target = await this.getUserProfileFieldByCode(avgFieldCode, organizationId);
if (!target) {
throw new Error(`Поле для среднего "${avgFieldCode}" (options.avgFieldCode поля "${field.name}") не найдено`);
}
targetFieldId = target.id;
}
const existing = await this.getUserProfileFieldValue(userId, targetFieldId);
if (existing) {
if (existing.value !== avg) {
await this.updateUserProfileFieldValue(userId, fieldId, { value: avg as UserProfileFieldValue['value'] });
await this.updateUserProfileFieldValue(userId, targetFieldId, { value: avg as UserProfileFieldValue['value'] });
await this.createUserProfileAuditLog({
userId,
organizationId,
action: 'field.changed',
fieldId,
fieldId: targetFieldId,
oldValue: existing.value ?? null,
newValue: avg != null ? String(avg) : null,
changedBy: null,
changedByName: 'Автоматизация',
metadata: { source: 'automation', reason: 'monthly-average-recalc' },
metadata: { source: 'automation', reason: 'monthly-average-recalc', historyFieldId: fieldId },
});
}
} else if (avg != null) {
await this.createUserProfileFieldValue({ userId, fieldId, value: avg as InsertUserProfileFieldValue['value'] });
await this.createUserProfileFieldValue({ userId, fieldId: targetFieldId, value: avg as InsertUserProfileFieldValue['value'] });
}
return avg;
}