feat: добавление исполнителей при правке мебели/ПО согласующим

Маршрут планируется один раз при подаче: если мебель или ПО отметил
руководитель на своём шаге, исполнитель по ним не появлялся. Теперь при
сохранении правок (и перед решением) строятся два плана исполнения — со
старыми и новыми значениями — и разница встаёт в конец исполнения «Ждёт
очереди» с SLA из маршрута. Никого не удаляем, уже стоящих не дублируем;
ошибка добавления не теряется и блокирует решение.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Тимур Городецкий
2026-10-29 17:30:03 +05:00
parent 45fc06a6ac
commit 36432c75e6
8 changed files with 295 additions and 22 deletions
@@ -3,13 +3,13 @@ import '@pnp/sp/webs';
import '@pnp/sp/lists';
import { IList } from '@pnp/sp/lists';
import '@pnp/sp/items';
import { ApprovalListService } from './approvalList.service';
import { ApprovalListService, IApproverToWrite } from './approvalList.service';
import { REQUEST_STATUS, APPROVAL_PHASE } from './models/statuses';
import { IApprovalRow } from './models/IApprovalRow';
import { ISiteUser } from './models/ISiteUser';
import { emitRequestsChanged } from '../requestEvents';
import { planQueuedStepMove, StepMoveDirection } from './queuedStepMove';
import { planStepAdd } from './routeStepAdd';
import { IStepAddRow, planStepAdd } from './routeStepAdd';
function isActiveStatus(status: string): boolean {
return status === REQUEST_STATUS.onApproval || status === REQUEST_STATUS.onExecution;
@@ -209,6 +209,40 @@ export class ApproverService {
return rowsAfter.filter((r) => isActiveStatus(r.status) && !activeBefore.has(r.id)).map((r) => r.id);
}
/**
* Добавить в поданную заявку исполнителей, которых привела правка полей
* маршрута согласующим (КОГО — решает planMissingExecutors). Каждый встаёт в
* КОНЕЦ исполнения «Ждёт очереди», по тем же правилам, что addStep
* (planStepAdd), но без права редактора маршрута: людей назначает маршрут из
* админки, а не пользователь. SLA шага переносится из маршрута.
*
* Возвращает Id строк, ставших активными (обычно пусто); им нужно оповещение.
*/
public async addMissingExecutors(requestId: number, executors: IApproverToWrite[]): Promise<number[]> {
if (executors.length === 0) return [];
const rows = await this.approval.getForRequest(requestId);
const activeBefore = new Set(rows.filter((r) => isActiveStatus(r.status)).map((r) => r.id));
// По одному и с локальным дописыванием rows: иначе второй исполнитель
// получил бы тот же order, что первый, и стал бы параллельным шагом.
const planned: IStepAddRow[] = rows.slice();
for (const executor of executors) {
const approverId = await this.approval.getUserIdByEmail(executor.email);
const plan = planStepAdd(planned, APPROVAL_PHASE.execution, approverId);
if (!plan.ok) throw new Error(plan.error);
await this.approval.addApproverRow(requestId, executor, plan.order, REQUEST_STATUS.queued, APPROVAL_PHASE.execution);
planned.push({ approverId, order: plan.order, status: REQUEST_STATUS.queued, phase: APPROVAL_PHASE.execution });
}
await this.approval.activateNextQueued(requestId);
const rowsAfter = await this.approval.getForRequest(requestId);
// Новым исполнителям — права на элемент данных (см. addStep).
await this.touchRequest(requestId);
emitRequestsChanged();
return rowsAfter.filter((r) => isActiveStatus(r.status) && !activeBefore.has(r.id)).map((r) => r.id);
}
/**
* Шаг фазы «Исполнение», который ещё можно заменить или удалить: ждёт очереди
* или уже активен («На исполнении»). Выполненный шаг — решение человека, его