feat: добавление согласующих и исполнителей в поданные заявки на подбор и приём

Алтынай Бултурукова (право canEditRoute) просила добавлять участников в уже
поданный маршрут. Новый согласующий или исполнитель встаёт в конец своего
этапа со статусом «Ждёт очереди» (дальше его можно поднять стрелками);
согласующего — пока идёт согласование, исполнителя — пока не завершено
исполнение, без дублей среди ещё не решивших. Заявка «трогается», чтобы
бэкенд выдал новому участнику права на элемент данных.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Тимур Городецкий
2026-10-09 12:16:18 +05:00
parent 79d2a59229
commit c80972469f
10 changed files with 334 additions and 14 deletions
@@ -9,6 +9,11 @@ import { IApprovalRow } from './models/IApprovalRow';
import { ISiteUser } from './models/ISiteUser';
import { emitRequestsChanged } from '../requestEvents';
import { planQueuedStepMove, StepMoveDirection } from './queuedStepMove';
import { planStepAdd } from './routeStepAdd';
function isActiveStatus(status: string): boolean {
return status === REQUEST_STATUS.onApproval || status === REQUEST_STATUS.onExecution;
}
// approve/return/reject — согласующий; execute — исполнитель отметил «Выполнено».
export type ApproverDecision = 'approve' | 'return' | 'reject' | 'execute';
@@ -165,6 +170,45 @@ export class ApproverService {
emitRequestsChanged();
}
/**
* Добавить участника в поданную заявку: согласующего или исполнителя — в
* КОНЕЦ его этапа со статусом «Ждёт очереди» (право — routeEditRights.ts,
* правило — routeStepAdd.ts). Строки перечитываются перед записью.
*
* Заявку «трогаем» (touchRequest): новый участник должен получить права на
* элемент данных, иначе откроет её с «Элемент не найден».
*
* Возвращает Id строк, ставших активными (обычно пусто — активный шаг уже
* есть); им нужно оповещение (отправляет вызывающая сторона).
*/
public async addStep(requestId: number, phase: string, user: ISiteUser): Promise<number[]> {
const [rows, approverId] = await Promise.all([
this.approval.getForRequest(requestId),
this.approval.getUserIdByEmail(user.email),
]);
const plan = planStepAdd(rows, phase, approverId);
if (!plan.ok) throw new Error(plan.error);
const position = await this.approval.getJobTitle(user.loginName).catch(() => '');
await this.approval.addApproverRow(
requestId,
{ email: user.email, position, allowReassign: false },
plan.order,
REQUEST_STATUS.queued,
phase
);
// На случай, если активного шага почему-то не было: очередь сама подхватит
// нового участника. При обычном ходе заявки ничего не меняется.
const activeBefore = new Set(rows.filter((r) => isActiveStatus(r.status)).map((r) => r.id));
await this.approval.activateNextQueued(requestId);
const rowsAfter = await this.approval.getForRequest(requestId);
await this.touchRequest(requestId);
emitRequestsChanged();
return rowsAfter.filter((r) => isActiveStatus(r.status) && !activeBefore.has(r.id)).map((r) => r.id);
}
/**
* Шаг фазы «Исполнение», который ещё можно заменить или удалить: ждёт очереди
* или уже активен («На исполнении»). Выполненный шаг — решение человека, его