Files
request-portal/src/webparts/requestPortal/shared/approval/approvalSubmission.service.ts
T
gorbenko.pavel cf57b40513
Tests / frontend-test (push) Failing after 38s
feat(автосогласование): колонка AutoApprove во всех списках согласования (#20)
Провижининг (Provision-Lists.ps1 читает эти манифесты) теперь заводит колонку
«Автосогласование» (AutoApprove, Да/Нет) в каждом из 12 списков согласования —
рядом с уже существующими SLAhours/AskOverdueReason.

Значение приходит из resolve (approval-route API, шаг маршрута настраивается
только в админке — портал само значение не выбирает, только переносит его в
строку списка при отправке заявки/сохранении черновика, тем же путём, что и
slaHours/askOverdueReason). Фактическое автосогласование по истечении SLA
делает фоновая задача бэкенда (Hangfire), не портал.

В ApprovalMatrix добавлена пометка «(авто)» рядом со значением SLA — чисто
информативно, чтобы участник видел, что решение по шагу может принять не
только человек.

Примечание: ServiceRequest и TmcTransfer не передают ни slaHours/
askOverdueReason, ни (теперь) autoApprove из resolve в строку — это
существовавший пробел (SLA на уровне шага у этих двух форм и раньше не
работал), автосогласование для них наследует только это же ограничение.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz>
Reviewed-on: http://git.tnsu.kz/timur.gorodetskiy/request-portal/pulls/20
2026-09-14 09:02:14 +00:00

117 lines
6.3 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';
// Планируемый маршрут для НОВОЙ заявки: 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,
}))
);
}
// Записать строки согласующих для заявки с указанным статусом (только фаза согласования).
// Матрица перезаписывается целиком (для повторной отправки/черновика).
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).
export async function writeRoute(
approval: ApprovalListService,
requestId: number,
approvers: IApproverToWrite[],
executors: IApproverToWrite[],
status: string
): Promise<void> {
await approval.deleteForRequest(requestId);
// При ОТПРАВКЕ все шаги пишем «Ждёт очереди», затем активируем первый —
// так активен ровно текущий шаг, остальные ждут. В черновике — статус заявки
// (Черновик), без активации.
//
// Именно предикат, а не `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
);
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);
}
}