153c966f0b
При доработке и повторной отправке writeRoute раньше удалял все строки маршрута и создавал заново — кто вернул или отклонил заявку и с каким комментарием, терялось. Теперь маршрут пересохраняется патчем (planRoutePatch + ApprovalListService.patchSteps): строки, что остаются в маршруте, обновляются на месте, и их прошлые решения лежат в истории версий; недостающие шаги создаются, выпавшие и строки делегатов уходят в корзину. Всё одним пакетом. Проверено пилотами на увольнении и рабочем месте (обе фазы, уход в корзину). Аварийный откат для всех форм — ROUTE_PATCH_ENABLED = false. Из версий строк маршрута и заявки собирается лента «История согласования» (shared/approval/history): отправки, решения с комментариями и временем на шаге, делегирование, замена исполнителя, у кого заявка сейчас. Свёрнутый раздел в карточке и в форме доработки всех форм, грузится только при раскрытии. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
143 lines
7.9 KiB
TypeScript
143 lines
7.9 KiB
TypeScript
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<IMatrixRow[]> {
|
|
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<void> {
|
|
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<void> {
|
|
// При ОТПРАВКЕ все шаги пишем «Ждёт очереди», затем активируем первый —
|
|
// так активен ровно текущий шаг, остальные ждут. В черновике — статус заявки
|
|
// (Черновик), без активации.
|
|
//
|
|
// Именно предикат, а не `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);
|
|
}
|
|
}
|