diff --git a/src/webparts/requestPortal/shared/approval/approvalSubmission.service.ts b/src/webparts/requestPortal/shared/approval/approvalSubmission.service.ts index 3e38182..f09b1ab 100644 --- a/src/webparts/requestPortal/shared/approval/approvalSubmission.service.ts +++ b/src/webparts/requestPortal/shared/approval/approvalSubmission.service.ts @@ -3,7 +3,7 @@ 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 } from './models/statuses'; +import { APPROVAL_PHASE, REQUEST_STATUS, isSubmittedStatus } from './models/statuses'; // Планируемый маршрут для НОВОЙ заявки: resolve по email инициатора → строки матрицы. // status выбирает фазу: 'Approval' (согласующие, по умолчанию) или 'Execution' @@ -82,7 +82,11 @@ export async function writeRoute( // При ОТПРАВКЕ все шаги пишем «Ждёт очереди», затем активируем первый — // так активен ровно текущий шаг, остальные ждут. В черновике — статус заявки // (Черновик), без активации. - const submitted = status === REQUEST_STATUS.onApproval; + // + // Именно предикат, а не `status === onApproval`: отправка маршрута без + // согласующих даёт «Выполнение» (см. initialSubmitStatus), и при строгом + // сравнении такая заявка не активировала ни одного шага исполнения. + const submitted = isSubmittedStatus(status); const rowStatus = submitted ? REQUEST_STATUS.queued : status; // Смещение фазы исполнения — по МАКСИМАЛЬНОМУ номеру шага согласующих, а не по diff --git a/src/webparts/requestPortal/shared/approval/index.ts b/src/webparts/requestPortal/shared/approval/index.ts index 9f96dcc..9441042 100644 --- a/src/webparts/requestPortal/shared/approval/index.ts +++ b/src/webparts/requestPortal/shared/approval/index.ts @@ -8,6 +8,7 @@ export { EDITABLE_STATUSES, isEditableStatus, initialSubmitStatus, + isSubmittedStatus, } from './models/statuses'; export type { ISiteUser } from './models/ISiteUser'; export type { IMatrixRow } from './models/IMatrixRow'; diff --git a/src/webparts/requestPortal/shared/approval/models/statuses.test.ts b/src/webparts/requestPortal/shared/approval/models/statuses.test.ts index 30f8d72..d9f049f 100644 --- a/src/webparts/requestPortal/shared/approval/models/statuses.test.ts +++ b/src/webparts/requestPortal/shared/approval/models/statuses.test.ts @@ -5,7 +5,9 @@ import { EDITABLE_STATUSES, REQUEST_STATUS, REQUEST_STATUS_CHOICES, + initialSubmitStatus, isEditableStatus, + isSubmittedStatus, } from './statuses'; describe('isEditableStatus', () => { @@ -32,6 +34,43 @@ describe('isEditableStatus', () => { }); }); +describe('initialSubmitStatus', () => { + it('есть согласующие — заявка идёт на согласование', () => { + expect(initialSubmitStatus(2, 0)).toBe(REQUEST_STATUS.onApproval); + expect(initialSubmitStatus(2, 3)).toBe(REQUEST_STATUS.onApproval); + }); + + it('согласующих нет, но есть исполнители — сразу в исполнение', () => { + expect(initialSubmitStatus(0, 1)).toBe(REQUEST_STATUS.execution); + }); + + it('маршрута нет вовсе — заявка сразу согласована', () => { + expect(initialSubmitStatus(0, 0)).toBe(REQUEST_STATUS.approved); + }); +}); + +// writeRoute по этому предикату решает, писать строки как «Ждёт очереди» и +// активировать первую, или оставить как есть (черновик). Каждый статус, который +// возвращает initialSubmitStatus, ОБЯЗАН считаться отправкой: иначе строки +// маршрута получают недопустимый статус, а активации не происходит вовсе — +// именно так заявка без согласующих перестала попадать в очередь исполнения. +describe('isSubmittedStatus', () => { + it('любой результат initialSubmitStatus считается отправкой', () => { + [initialSubmitStatus(1, 0), initialSubmitStatus(0, 1), initialSubmitStatus(0, 0)].forEach((s) => + expect(isSubmittedStatus(s)).toBe(true) + ); + }); + + it('черновик отправкой не считается', () => { + expect(isSubmittedStatus(REQUEST_STATUS.draft)).toBe(false); + }); + + it('возврат и отклонение отправкой не считаются (маршрут не переактивируется)', () => { + expect(isSubmittedStatus(REQUEST_STATUS.returned)).toBe(false); + expect(isSubmittedStatus(REQUEST_STATUS.rejected)).toBe(false); + }); +}); + // Наборы Choice-значений держатся в синхроне с provisioning.json списков — // расхождение ломает запись (SharePoint отвергает значение вне Choice). describe('наборы Choice-значений', () => { diff --git a/src/webparts/requestPortal/shared/approval/models/statuses.ts b/src/webparts/requestPortal/shared/approval/models/statuses.ts index da186a7..1618583 100644 --- a/src/webparts/requestPortal/shared/approval/models/statuses.ts +++ b/src/webparts/requestPortal/shared/approval/models/statuses.ts @@ -61,6 +61,27 @@ export function initialSubmitStatus(approversCount: number, executorsCount: numb return REQUEST_STATUS.approved; } +/** + * Статус заявки означает «отправлена» (в отличие от черновика)? + * + * Нужен writeRoute: он по этому признаку решает, писать строки маршрута как + * «Ждёт очереди» + активировать первую, или сохранить их «как есть» (черновик). + * Раньше там стояло строгое `status === onApproval` — единственный статус, + * которым формы отправляли заявку. С появлением initialSubmitStatus отправка + * может дать «Выполнение» (маршрут без согласующих), и строгое сравнение молча + * ломалось: строки получали ApprovalStatus «Выполнение» (недопустимое значение + * для строки, см. APPROVAL_ROW_STATUS_CHOICES), а activateNextQueued не + * вызывался — ни одна строка не становилась «На исполнении», и заявка не + * попадала в очередь исполнителя. + */ +export function isSubmittedStatus(status: string): boolean { + return ( + status === REQUEST_STATUS.onApproval || + status === REQUEST_STATUS.execution || + status === REQUEST_STATUS.approved + ); +} + // Наборов значений ДВА, и они не совпадают — это разные сущности: // // 1) статус ЗАЯВКИ — колонка RequestStatus в списке «… — данные». Пишется