Files
iistwin/server/utils/notifyAssignee.ts
Ильяс Султанов 667bddeac9 feat: замещение пользователей (substitution) — заместители по умолчанию и по формам, контур ознакомления, inbox reasons
- таблица user_delegations (миграция 0067) + колонки delegated_from_user_id/pending_review в task_assignees
- delegation.service: резолвер с цепочкой и защитой от циклов, bulk-до-назначение чанками, чистка истёкших замещений
- центральный хук в addTaskAssignee: заместитель добавляется при любом назначении (ручном, автопереходе, user-поле, MCP)
- контур ознакомления: после перехода отсутствующий остаётся в исполнителях с pending_review, снимается кнопкой «Ознакомлен»
- inbox reasons delegation/delegation_review + фильтр «Замещение» на главной
- блок «Замещение» в профиле пользователя, бейдж «за <имя>» и баннер ознакомления в карточке задачи
2026-08-05 10:44:55 +03:00

77 lines
3.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
options?: { delegatedFromName?: string }
): Promise<number[]> {
const delegationSuffix = options?.delegatedFromName ? ` (вы замещаете ${options.delegatedFromName})` : "";
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}"${delegationSuffix}`,
...(options?.delegatedFromName
? { delegation: true, delegatedFromName: options.delegatedFromName }
: {}),
},
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;
}