fix(number-fields): избегаем потери точности длинных чисел

- number-поля теперь рендерятся как text + inputMode=numeric,
  чтобы браузер не округлял значения через input type=number
- пробелы при вставке в number-поля удаляются
- бэкенд нормализует значения number-полей в строку перед сохранением
- добавлен хелпер normalizeFieldValueForStorage

Closes: искажение расчётного счёта и других длинных числовых полей
This commit is contained in:
2026-07-07 21:03:40 +03:00
commit 1f5ecb6da4
1089 changed files with 237546 additions and 0 deletions

View File

@@ -0,0 +1,71 @@
import { notificationService, EVENT_TYPES } from "../services/notification.service";
import { eventBus } from "../routes/shared";
/**
* Отправляет уведомление о назначении ответственного по задаче.
* Централизованная функция — используется из всех endpoint'ов,
* где меняется assignedTo (assignees, transitions, crud-write, auto-transitions).
*
* Возвращает массив userId пользователей, которым было отправлено уведомление.
* Отправляет SSE notification events для real-time обновления UI.
*
* Если у назначенного пользователя нет подписки на task.assigned,
* создаёт уведомление принудительно (через targetUserIds), чтобы
* назначение задачи никогда не пропадало мимо пользователя.
*/
export async function notifyTaskAssigned(
task: { id: number; title: string; formId: number },
assigneeUserId: number,
triggeredBy: number | null,
organizationId: number
): Promise<number[]> {
const eventBase = {
type: EVENT_TYPES.TASK_ASSIGNED,
organizationId,
triggeredBy: triggeredBy ?? null,
taskId: task.id,
formId: task.formId,
payload: {
taskTitle: task.title,
assignedTo: assigneeUserId,
authorName: triggeredBy ? undefined : "Система",
message: `Вас назначили ответственным по задаче "${task.title}"`,
},
timestamp: new Date(),
};
// 1. Primary: normal subscriber-based flow
let notifiedUserIds = await notificationService.processEvent(eventBase);
if (!Array.isArray(notifiedUserIds)) {
console.warn(`[notifyTaskAssigned] processEvent returned non-array (${typeof notifiedUserIds}), treating as empty`);
notifiedUserIds = [];
}
// 2. Fallback: if subscription-based flow didn't notify the assignee,
// force-create the notification via targetUserIds (bypasses subscription check).
// This ensures task assignment is NEVER silently lost.
if (!notifiedUserIds.includes(assigneeUserId)) {
console.warn(
`[notifyTaskAssigned] User ${assigneeUserId} has no subscription for TASK_ASSIGNED. ` +
`Forcing notification via targetUserIds (taskId=${task.id}, formId=${task.formId})`
);
const forced = await notificationService.processEvent({
...eventBase,
targetUserIds: [assigneeUserId],
});
notifiedUserIds = Array.isArray(forced) ? forced : [];
}
// 3. Send SSE notification events for real-time UI update (badge + inbox count)
for (const userId of notifiedUserIds) {
console.log(`[SSE] Publishing notification event for user ${userId}, taskId=${task.id}`);
eventBus.publishEvent({
type: "notification",
data: { type: EVENT_TYPES.TASK_ASSIGNED, taskId: task.id },
organizationId,
userId,
});
}
return notifiedUserIds;
}