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 + фильтр «Замещение» на главной - блок «Замещение» в профиле пользователя, бейдж «за <имя>» и баннер ознакомления в карточке задачи
This commit is contained in:
@@ -48,12 +48,41 @@ export class SystemStorage extends RolesStorage {
|
||||
));
|
||||
}
|
||||
|
||||
async addTaskAssignee(taskId: number, userId: number, organizationId: number): Promise<TaskAssignee> {
|
||||
async addTaskAssignee(
|
||||
taskId: number,
|
||||
userId: number,
|
||||
organizationId: number,
|
||||
options?: { delegatedFromUserId?: number | null; pendingReview?: boolean }
|
||||
): Promise<TaskAssignee> {
|
||||
const existing = await db.select().from(taskAssignees)
|
||||
.where(and(eq(taskAssignees.taskId, taskId), eq(taskAssignees.userId, userId)))
|
||||
.limit(1);
|
||||
if (existing.length > 0) return existing[0];
|
||||
const [row] = await db.insert(taskAssignees).values({ taskId, userId, organizationId }).returning();
|
||||
if (existing.length > 0) {
|
||||
// Явный запрос «на ознакомление» — проставляем флаг на существующей строке
|
||||
if (options?.pendingReview && !existing[0].pendingReview) {
|
||||
const [updated] = await db.update(taskAssignees)
|
||||
.set({ pendingReview: true })
|
||||
.where(eq(taskAssignees.id, existing[0].id))
|
||||
.returning();
|
||||
return updated;
|
||||
}
|
||||
return existing[0];
|
||||
}
|
||||
const [row] = await db.insert(taskAssignees).values({
|
||||
taskId,
|
||||
userId,
|
||||
organizationId,
|
||||
delegatedFromUserId: options?.delegatedFromUserId ?? null,
|
||||
pendingReview: options?.pendingReview ?? false,
|
||||
}).returning();
|
||||
// Хук замещения: прямое назначение резолвит активного заместителя.
|
||||
// Пропускаем строки заместителей/ознакомления — иначе получили бы рекурсию.
|
||||
// Динамический import разрывает цикл storage → delegation.service → storage.
|
||||
if (!options?.delegatedFromUserId && !options?.pendingReview) {
|
||||
import("../services/delegation.service")
|
||||
.then((m) => m.applyDelegationsOnAssign(taskId, userId, organizationId))
|
||||
.catch((err) => console.error("[Delegation] applyDelegationsOnAssign hook error:", err));
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user