feat: автоматический пересчёт среднего для history-number в sandbox
- setFieldValue для history-number: переданное число пишется в историю, значением поля становится среднее за текущий месяц (автопересчёт) - ctx.users.recalcFieldAverage(userId, fieldCodeOrId): принудительный пересчёт среднего за месяц (для schedule-автоматизаций; нет записей — поле очищается) - документация обновлена (Automations.tsx, mcp.ts)
This commit is contained in:
@@ -30,6 +30,7 @@ const ALLOWED_STORAGE_METHODS = new Set([
|
|||||||
'getUsersByOrganization',
|
'getUsersByOrganization',
|
||||||
'updateUser',
|
'updateUser',
|
||||||
'setUserProfileFieldValueFromAutomation',
|
'setUserProfileFieldValueFromAutomation',
|
||||||
|
'recalcUserFieldAverageFromAutomation',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
interface WorkerStorageCall {
|
interface WorkerStorageCall {
|
||||||
|
|||||||
@@ -151,25 +151,8 @@ export class UserProfileStorage extends UsersStorage {
|
|||||||
throw new Error(`Поле профиля "${fieldCodeOrId}" не найдено`);
|
throw new Error(`Поле профиля "${fieldCodeOrId}" не найдено`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const existing = await this.getUserProfileFieldValue(userId, field.id);
|
// Для history-number: переданное число идёт в историю, значением поля становится среднее за текущий месяц
|
||||||
const saved = existing
|
let storedValue = value;
|
||||||
? await this.updateUserProfileFieldValue(userId, field.id, { value: value as UserProfileFieldValue['value'] })
|
|
||||||
: await this.createUserProfileFieldValue({ userId, fieldId: field.id, value: value as InsertUserProfileFieldValue['value'] });
|
|
||||||
|
|
||||||
await this.createUserProfileAuditLog({
|
|
||||||
userId,
|
|
||||||
organizationId,
|
|
||||||
action: 'field.changed',
|
|
||||||
fieldId: field.id,
|
|
||||||
fieldName: field.name,
|
|
||||||
oldValue: existing?.value ?? null,
|
|
||||||
newValue: typeof value === 'string' ? value : value != null ? JSON.stringify(value) : null,
|
|
||||||
changedBy: null,
|
|
||||||
changedByName: 'Автоматизация',
|
|
||||||
metadata: { source: 'automation', taskId: taskId ?? null },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Для полей с историей дополнительно пишем числовое значение в user_field_history
|
|
||||||
if (field.type === 'history-number') {
|
if (field.type === 'history-number') {
|
||||||
const num = typeof value === 'number' ? value : parseFloat(String(value).replace(',', '.').trim());
|
const num = typeof value === 'number' ? value : parseFloat(String(value).replace(',', '.').trim());
|
||||||
if (!Number.isFinite(num)) {
|
if (!Number.isFinite(num)) {
|
||||||
@@ -182,11 +165,88 @@ export class UserProfileStorage extends UsersStorage {
|
|||||||
value: String(num),
|
value: String(num),
|
||||||
taskId: taskId ?? null,
|
taskId: taskId ?? null,
|
||||||
});
|
});
|
||||||
|
storedValue = await this.recomputeUserFieldMonthlyAverage(userId, field.id, organizationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const existing = await this.getUserProfileFieldValue(userId, field.id);
|
||||||
|
const saved = existing
|
||||||
|
? await this.updateUserProfileFieldValue(userId, field.id, { value: storedValue as UserProfileFieldValue['value'] })
|
||||||
|
: await this.createUserProfileFieldValue({ userId, fieldId: field.id, value: storedValue as InsertUserProfileFieldValue['value'] });
|
||||||
|
|
||||||
|
await this.createUserProfileAuditLog({
|
||||||
|
userId,
|
||||||
|
organizationId,
|
||||||
|
action: 'field.changed',
|
||||||
|
fieldId: field.id,
|
||||||
|
fieldName: field.name,
|
||||||
|
oldValue: existing?.value ?? null,
|
||||||
|
newValue: typeof storedValue === 'string' ? storedValue : storedValue != null ? JSON.stringify(storedValue) : null,
|
||||||
|
changedBy: null,
|
||||||
|
changedByName: 'Автоматизация',
|
||||||
|
metadata: { source: 'automation', taskId: taskId ?? null },
|
||||||
|
});
|
||||||
|
|
||||||
return saved;
|
return saved;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Пересчитывает значение history-number поля как среднее за текущий календарный месяц.
|
||||||
|
* Если в месяце нет записей — поле очищается (null). Возвращает новое значение.
|
||||||
|
*/
|
||||||
|
async recomputeUserFieldMonthlyAverage(userId: number, fieldId: number, organizationId: number) {
|
||||||
|
const [row] = await db
|
||||||
|
.select({ avg: sql<number | null>`ROUND(AVG(${userFieldHistory.value})::numeric, 2)::float8` })
|
||||||
|
.from(userFieldHistory)
|
||||||
|
.where(and(
|
||||||
|
eq(userFieldHistory.userId, userId),
|
||||||
|
eq(userFieldHistory.fieldId, fieldId),
|
||||||
|
eq(userFieldHistory.organizationId, organizationId),
|
||||||
|
sql`${userFieldHistory.createdAt} >= date_trunc('month', now())`,
|
||||||
|
));
|
||||||
|
const avg = row?.avg != null ? Number(row.avg) : null;
|
||||||
|
|
||||||
|
const existing = await this.getUserProfileFieldValue(userId, fieldId);
|
||||||
|
if (existing) {
|
||||||
|
if (existing.value !== avg) {
|
||||||
|
await this.updateUserProfileFieldValue(userId, fieldId, { value: avg as UserProfileFieldValue['value'] });
|
||||||
|
await this.createUserProfileAuditLog({
|
||||||
|
userId,
|
||||||
|
organizationId,
|
||||||
|
action: 'field.changed',
|
||||||
|
fieldId,
|
||||||
|
oldValue: existing.value ?? null,
|
||||||
|
newValue: avg != null ? String(avg) : null,
|
||||||
|
changedBy: null,
|
||||||
|
changedByName: 'Автоматизация',
|
||||||
|
metadata: { source: 'automation', reason: 'monthly-average-recalc' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (avg != null) {
|
||||||
|
await this.createUserProfileFieldValue({ userId, fieldId, value: avg as InsertUserProfileFieldValue['value'] });
|
||||||
|
}
|
||||||
|
return avg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ctx.users.recalcFieldAverage: принудительный пересчёт среднего за месяц для history-number поля.
|
||||||
|
*/
|
||||||
|
async recalcUserFieldAverageFromAutomation(userId: number, fieldCodeOrId: string | number, organizationId: number) {
|
||||||
|
const field = typeof fieldCodeOrId === 'number'
|
||||||
|
? await this.getUserProfileFieldById(fieldCodeOrId, organizationId)
|
||||||
|
: await this.getUserProfileFieldByCode(String(fieldCodeOrId), organizationId);
|
||||||
|
if (!field) {
|
||||||
|
throw new Error(`Поле профиля "${fieldCodeOrId}" не найдено`);
|
||||||
|
}
|
||||||
|
if (field.type !== 'history-number') {
|
||||||
|
throw new Error(`Пересчёт среднего доступен только для полей типа history-number, у поля "${field.name}" тип ${field.type}`);
|
||||||
|
}
|
||||||
|
const user = await this.getUser(userId);
|
||||||
|
if (!user || user.organizationId !== organizationId) {
|
||||||
|
throw new Error(`Пользователь ${userId} не найден в организации`);
|
||||||
|
}
|
||||||
|
return this.recomputeUserFieldMonthlyAverage(userId, field.id, organizationId);
|
||||||
|
}
|
||||||
|
|
||||||
async bulkSaveUserProfileFields(
|
async bulkSaveUserProfileFields(
|
||||||
organizationId: number,
|
organizationId: number,
|
||||||
fieldIdsToDelete: number[],
|
fieldIdsToDelete: number[],
|
||||||
|
|||||||
Reference in New Issue
Block a user