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:
2026-08-05 10:44:55 +03:00
parent 17a8dc42f2
commit 667bddeac9
15 changed files with 1216 additions and 12 deletions

View File

@@ -6,6 +6,7 @@ import { evaluateAutoTransitions } from "../utils/auto-transitions";
import { tasksMinimalCache } from "../utils/cache";
import { eventBus } from "./shared";
import { notifyTaskAssigned } from "../utils/notifyAssignee";
import { acknowledgeDelegationReview } from "../services/delegation.service";
export function registerTaskRoleRoutes(router: ReturnType<typeof import("express").Router>): void {
// === TASK ROLES ===
@@ -242,6 +243,34 @@ export function registerTaskRoleRoutes(router: ReturnType<typeof import("express
}
);
// Подтверждение ознакомления по замещению: текущий пользователь (отсутствующий
// исполнитель, вернувшийся в задачу с pending_review) снимает свою строку.
router.post('/api/tasks/:id/delegation-ack',
authenticateToken,
tenantIsolation,
async (req: AuthenticatedRequest, res) => {
try {
const taskId = parseInt(req.params.id);
if (isNaN(taskId)) return res.status(400).json({ error: 'Неверный ID задачи' });
const task = await storage.getTask(taskId, req.organizationId!);
if (!task) return res.status(404).json({ error: 'Задача не найдена' });
const acknowledged = await acknowledgeDelegationReview(taskId, req.user!.id, req.organizationId!);
if (!acknowledged) {
return res.status(404).json({ error: 'Задача не ожидает вашего ознакомления' });
}
eventBus.publishEvent({
type: 'task_updated',
organizationId: req.organizationId!,
data: { taskId, formId: task.formId, task }
});
res.json({ success: true });
} catch (err) {
console.error('Delegation ack error:', err);
res.status(500).json({ error: 'Ошибка при подтверждении ознакомления' });
}
}
);
// === TASK USER ACCESS ===
router.get('/api/tasks/:id/access',