import { WebPartContext } from '@microsoft/sp-webpart-base'; import { resolveRoute } from './approvalRoute.service'; import { ApprovalListService, IApproverToWrite } from './approvalList.service'; import { IMatrixRow } from './models/IMatrixRow'; import { ApprovalStatus, IResolveFieldValue } from './models/IApprovalRouteApi'; import { APPROVAL_PHASE, REQUEST_STATUS, isSubmittedStatus } from './models/statuses'; /** * Выключатель пересохранения маршрута патчем — ДЛЯ ВСЕХ форм сразу (см. * writeRoute). Проверено пилотами на «Заявлении на увольнение» и «Организации * рабочего места» 2026-09-29: обновление строк на месте, обе фазы, уход * выпавших шагов в корзину. false — аварийный откат к прежнему «удалить всё и * создать заново» (история прошлых отправок снова перестанет сохраняться). */ const ROUTE_PATCH_ENABLED = true; // Планируемый маршрут для НОВОЙ заявки: resolve по email инициатора → строки матрицы. // status выбирает фазу: 'Approval' (согласующие, по умолчанию) или 'Execution' // (исполнители). Имена подтягиваются по executorEmail (бэкенд отдаёт email + должность). // departmentId — Id отдела из справочника (влияет на маршрут, если он различается по отделу). export async function getPlannedMatrix( context: WebPartContext, requestTypeId: number, userEmail: string, approval: ApprovalListService, status: ApprovalStatus = 'Approval', // eslint-disable-next-line @rushstack/no-new-null -- транзитом в resolveRoute(), см. её departmentId departmentId?: string | null, fieldValues?: IResolveFieldValue[], // eslint-disable-next-line @rushstack/no-new-null -- см. departmentId projectId?: string | null ): Promise { if (!userEmail) return []; const route = await resolveRoute(context, requestTypeId, userEmail, status, departmentId, fieldValues, projectId); const phase = status === 'Execution' ? APPROVAL_PHASE.execution : APPROVAL_PHASE.approval; const ordered = route.steps.slice().sort((a, b) => a.orderNo - b.orderNo); return Promise.all( ordered.map(async (s) => ({ order: s.orderNo, who: s.executorEmail ? await approval.resolveUserName(s.executorEmail) : '', email: s.executorEmail || '', position: s.position || '', status: '—', date: '', comment: '', allowReassign: s.allowReassignToOtherApprovers, phase, slaHours: s.slaHours, askOverdueReason: s.askOverdueReason, autoApprove: s.autoApprove, warning: s.warning, positionCode: s.positionCode, })) ); } // Записать строки согласующих для заявки с указанным статусом (только фаза согласования). // Матрица перезаписывается целиком (для повторной отправки/черновика). export async function writeApprovers( approval: ApprovalListService, requestId: number, approvers: IApproverToWrite[], status: string ): Promise { await approval.deleteForRequest(requestId); const submitted = status === REQUEST_STATUS.onApproval; const rowStatus = submitted ? REQUEST_STATUS.queued : status; await approval.createSteps(requestId, approvers, rowStatus, APPROVAL_PHASE.approval, 1); if (submitted) { await approval.activateNextQueued(requestId); } } // Записать ОБЕ фазы маршрута заявки: согласующих (Order 1..N) и исполнителей // (Order N+1..N+M) в один список. Исполнители пишутся сразу при отправке — // стороне согласующего внешний API уже не нужен, переход в фазу исполнения // делает computeRequestStatus, когда все согласующие одобрили. // // Исполнители стартуют ВСЕ как «Ждёт очереди» (rowStatus, та же логика, что и // у согласующих) — до прохождения фазы согласования у исполнителей нет текущей // очереди. Первого исполнителя переключит на «На исполнении» // approval.activateNextQueued, когда фаза согласования будет пройдена целиком // (см. approverDecision.service.ts/decide). // // Пересохранение — ПАТЧЕМ (ApprovalListService.patchSteps, см. ROUTE_PATCH_ENABLED): // строки, что остаются в маршруте, обновляются на месте, и их прошлые решения // (кто вернул/отклонил и с каким комментарием) сохраняются в истории версий — // из неё строится «История согласования». Раньше маршрут удалялся и создавался // заново, и история прошлых отправок терялась. export async function writeRoute( approval: ApprovalListService, requestId: number, approvers: IApproverToWrite[], executors: IApproverToWrite[], status: string ): Promise { // При ОТПРАВКЕ все шаги пишем «Ждёт очереди», затем активируем первый — // так активен ровно текущий шаг, остальные ждут. В черновике — статус заявки // (Черновик), без активации. // // Именно предикат, а не `status === onApproval`: отправка маршрута без // согласующих даёт «Выполнение» (см. initialSubmitStatus), и при строгом // сравнении такая заявка не активировала ни одного шага исполнения. const submitted = isSubmittedStatus(status); const rowStatus = submitted ? REQUEST_STATUS.queued : status; // Смещение фазы исполнения — по МАКСИМАЛЬНОМУ номеру шага согласующих, а не по // их количеству: при параллельных шагах несколько согласующих делят один номер, // поэтому approvers.length больше последнего реального шага (три согласующих, // из которых двое параллельны, — это шаги 1 и 2, а не 1..3). На очередь это не // влияет (activateNextQueued берёт минимальный order среди ждущих, разрывы ему // безразличны) — но нумерация шагов в таблице «Исполнение» шла бы с пропуском. const lastApprovalStep = approvers.reduce( (max, a, i) => Math.max(max, a.order ?? i + 1), 0 ); if (ROUTE_PATCH_ENABLED) { await approval.patchSteps( requestId, [ { approvers, phase: APPROVAL_PHASE.approval, orderStart: 1 }, { approvers: executors, phase: APPROVAL_PHASE.execution, orderStart: lastApprovalStep + 1 }, ], rowStatus ); } else { await approval.deleteForRequest(requestId); await approval.createSteps(requestId, approvers, rowStatus, APPROVAL_PHASE.approval, 1); await approval.createSteps( requestId, executors, rowStatus, APPROVAL_PHASE.execution, lastApprovalStep + 1 ); } if (submitted) { await approval.activateNextQueued(requestId); } }