From 4825443a9d91cc3353ea7c2a3e14b15ea38a8515 Mon Sep 17 00:00:00 2001 From: "gorbenko.pavel" Date: Wed, 19 Aug 2026 10:11:36 +0500 Subject: [PATCH] =?UTF-8?q?=D0=B2=D0=BE=D1=80=D0=B8=D0=BD=D0=B3=D0=B8=20?= =?UTF-8?q?=D1=82=D0=B5=D0=BF=D0=B5=D1=80=D1=8C=20=D0=BA=D0=BD=D0=BE=D0=BF?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=BE=D1=82=D1=80=D1=8C=D1=80=D0=B0=D0=B4=D0=B0?= =?UTF-8?q?=D0=B5=D1=82=D0=B2=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../forms/Clearance/ClearanceForm.tsx | 20 ++++++++++--------- .../RecruitingHire/RecruitingHireForm.tsx | 20 ++++++++++--------- .../RecruitingSelectionForm.tsx | 11 +++++----- .../forms/Ticket1C/Ticket1CForm.tsx | 11 +++++----- .../forms/TmcTransfer/TmcTransferForm.tsx | 20 ++++++++++--------- .../forms/UnpaidLeave/UnpaidLeaveForm.tsx | 11 +++++----- .../forms/Vacation/VacationForm.tsx | 11 +++++----- .../WorkplaceSetup/WorkplaceSetupForm.tsx | 20 ++++++++++--------- .../approval/approvalSubmission.service.ts | 1 + .../requestPortal/shared/approval/index.ts | 2 +- .../shared/approval/models/IMatrixRow.ts | 12 +++++++++++ .../shared/approval/useMatrixEditor.ts | 19 ++++++++++++++++++ 12 files changed, 101 insertions(+), 57 deletions(-) diff --git a/src/webparts/requestPortal/forms/Clearance/ClearanceForm.tsx b/src/webparts/requestPortal/forms/Clearance/ClearanceForm.tsx index 80c2a6d..427739e 100644 --- a/src/webparts/requestPortal/forms/Clearance/ClearanceForm.tsx +++ b/src/webparts/requestPortal/forms/Clearance/ClearanceForm.tsx @@ -23,7 +23,7 @@ import { isEditableStatus, IApproverToWrite, APPROVAL_PHASE, - findUnassignedStep + unassignedStepMessage } from '../../shared/approval'; import { IRequestFormProps } from '../../models/types'; import { @@ -236,14 +236,16 @@ const ClearanceForm: React.FC = ({ if (matrix.error) { return `Не удалось определить маршрут согласования: ${matrix.error}. Попробуйте ещё раз или обратитесь к администратору.`; } - const unassigned = findUnassignedStep(matrix.rows); - if (unassigned) { - return `Шаг ${unassigned.order} маршрута согласования («${unassigned.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`; - } - const unassignedExecutor = findUnassignedStep(executors.rows); - if (unassignedExecutor) { - return `Шаг ${unassignedExecutor.order} исполнения («${unassignedExecutor.position || 'без должности'}») без назначенного человека — выберите исполнителя или удалите шаг.`; - } + const unassignedApproverError = unassignedStepMessage( + matrix.rows, + (r) => `Шаг ${r.order} маршрута согласования («${r.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.` + ); + if (unassignedApproverError) return unassignedApproverError; + const unassignedExecutorError = unassignedStepMessage( + executors.rows, + (r) => `Шаг ${r.order} исполнения («${r.position || 'без должности'}») без назначенного человека — выберите исполнителя или удалите шаг.` + ); + if (unassignedExecutorError) return unassignedExecutorError; return undefined; }; diff --git a/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireForm.tsx b/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireForm.tsx index c0e6695..aa7cbb6 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireForm.tsx +++ b/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireForm.tsx @@ -27,7 +27,7 @@ import { isEditableStatus, IApproverToWrite, APPROVAL_PHASE, - findUnassignedStep + unassignedStepMessage } from '../../shared/approval'; import { uploadAttachments, @@ -237,14 +237,16 @@ const RecruitingHireForm: React.FC = ({ const validateSubmit = (): string | undefined => { const formError = validate(); if (formError) return formError; - const unassigned = findUnassignedStep(matrix.rows); - if (unassigned) { - return `Шаг ${unassigned.order} маршрута согласования («${unassigned.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`; - } - const unassignedExecutor = findUnassignedStep(executors.rows); - if (unassignedExecutor) { - return `Шаг ${unassignedExecutor.order} исполнения («${unassignedExecutor.position || 'без должности'}») без назначенного человека — выберите исполнителя или удалите шаг.`; - } + const unassignedApproverError = unassignedStepMessage( + matrix.rows, + (r) => `Шаг ${r.order} маршрута согласования («${r.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.` + ); + if (unassignedApproverError) return unassignedApproverError; + const unassignedExecutorError = unassignedStepMessage( + executors.rows, + (r) => `Шаг ${r.order} исполнения («${r.position || 'без должности'}») без назначенного человека — выберите исполнителя или удалите шаг.` + ); + if (unassignedExecutorError) return unassignedExecutorError; return undefined; }; diff --git a/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx b/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx index 0ef7eb0..b26b474 100644 --- a/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx +++ b/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx @@ -26,7 +26,7 @@ import { REQUEST_STATUS, isEditableStatus, IApproverToWrite, - findUnassignedStep + unassignedStepMessage } from '../../shared/approval'; import { uploadAttachments, @@ -211,10 +211,11 @@ const RecruitingSelectionForm: React.FC = ({ if (matrix.error) { return `Не удалось определить маршрут согласования: ${matrix.error}. Попробуйте ещё раз или обратитесь к администратору.`; } - const unassigned = findUnassignedStep(matrix.rows); - if (unassigned) { - return `Шаг ${unassigned.order} маршрута согласования («${unassigned.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`; - } + const unassignedError = unassignedStepMessage( + matrix.rows, + (r) => `Шаг ${r.order} маршрута согласования («${r.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.` + ); + if (unassignedError) return unassignedError; return undefined; }; diff --git a/src/webparts/requestPortal/forms/Ticket1C/Ticket1CForm.tsx b/src/webparts/requestPortal/forms/Ticket1C/Ticket1CForm.tsx index 6445df8..8850622 100644 --- a/src/webparts/requestPortal/forms/Ticket1C/Ticket1CForm.tsx +++ b/src/webparts/requestPortal/forms/Ticket1C/Ticket1CForm.tsx @@ -26,7 +26,7 @@ import { REQUEST_STATUS, isEditableStatus, IApproverToWrite, - findUnassignedStep + unassignedStepMessage } from '../../shared/approval'; import { uploadAttachments, @@ -296,10 +296,11 @@ const Ticket1CForm: React.FC = ({ // Матрица нередактируема (маршрут задаётся видом работ, а не заявителем) — // незаполненный шаг заявитель исправить сам не может, только администратор // маршрутов (роль в шаге не разрешилась в конкретного человека). - const unassigned = findUnassignedStep(matrix.rows); - if (unassigned) { - return `Шаг ${unassigned.order} маршрута согласования («${unassigned.position || 'без должности'}») не назначен на конкретного человека. Обратитесь к администратору маршрутов согласования.`; - } + const unassignedError = unassignedStepMessage( + matrix.rows, + (r) => `Шаг ${r.order} маршрута согласования («${r.position || 'без должности'}») не назначен на конкретного человека. Обратитесь к администратору маршрутов согласования.` + ); + if (unassignedError) return unassignedError; return undefined; }; diff --git a/src/webparts/requestPortal/forms/TmcTransfer/TmcTransferForm.tsx b/src/webparts/requestPortal/forms/TmcTransfer/TmcTransferForm.tsx index 6eb0d39..0d90a43 100644 --- a/src/webparts/requestPortal/forms/TmcTransfer/TmcTransferForm.tsx +++ b/src/webparts/requestPortal/forms/TmcTransfer/TmcTransferForm.tsx @@ -26,7 +26,7 @@ import { isEditableStatus, IApproverToWrite, APPROVAL_PHASE, - findUnassignedStep, + unassignedStepMessage, resolveWriteListId } from '../../shared/approval'; import { @@ -245,14 +245,16 @@ const TmcTransferForm: React.FC = ({ const validateSubmit = (): string | undefined => { const formError = validate(); if (formError) return formError; - const unassigned = findUnassignedStep(matrix.rows); - if (unassigned) { - return `Шаг ${unassigned.order} маршрута согласования («${unassigned.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`; - } - const unassignedExecutor = findUnassignedStep(executors.rows); - if (unassignedExecutor) { - return `Шаг ${unassignedExecutor.order} исполнения («${unassignedExecutor.position || 'без должности'}») без назначенного человека — выберите исполнителя или удалите шаг.`; - } + const unassignedApproverError = unassignedStepMessage( + matrix.rows, + (r) => `Шаг ${r.order} маршрута согласования («${r.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.` + ); + if (unassignedApproverError) return unassignedApproverError; + const unassignedExecutorError = unassignedStepMessage( + executors.rows, + (r) => `Шаг ${r.order} исполнения («${r.position || 'без должности'}») без назначенного человека — выберите исполнителя или удалите шаг.` + ); + if (unassignedExecutorError) return unassignedExecutorError; return undefined; }; diff --git a/src/webparts/requestPortal/forms/UnpaidLeave/UnpaidLeaveForm.tsx b/src/webparts/requestPortal/forms/UnpaidLeave/UnpaidLeaveForm.tsx index 783255d..ee15ae6 100644 --- a/src/webparts/requestPortal/forms/UnpaidLeave/UnpaidLeaveForm.tsx +++ b/src/webparts/requestPortal/forms/UnpaidLeave/UnpaidLeaveForm.tsx @@ -21,7 +21,7 @@ import { REQUEST_STATUS, isEditableStatus, IApproverToWrite, - findUnassignedStep, + unassignedStepMessage, resolveWriteListId } from '../../shared/approval'; import { IRequestFormProps } from '../../models/types'; @@ -217,10 +217,11 @@ const UnpaidLeaveForm: React.FC = ({ if (matrix.error) { return `Не удалось определить маршрут согласования: ${matrix.error}. Попробуйте ещё раз или обратитесь к администратору.`; } - const unassigned = findUnassignedStep(matrix.rows); - if (unassigned) { - return `Шаг ${unassigned.order} маршрута согласования («${unassigned.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`; - } + const unassignedError = unassignedStepMessage( + matrix.rows, + (r) => `Шаг ${r.order} маршрута согласования («${r.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.` + ); + if (unassignedError) return unassignedError; return undefined; }; diff --git a/src/webparts/requestPortal/forms/Vacation/VacationForm.tsx b/src/webparts/requestPortal/forms/Vacation/VacationForm.tsx index c680c32..738f0ad 100644 --- a/src/webparts/requestPortal/forms/Vacation/VacationForm.tsx +++ b/src/webparts/requestPortal/forms/Vacation/VacationForm.tsx @@ -22,7 +22,7 @@ import { REQUEST_STATUS, isEditableStatus, IApproverToWrite, - findUnassignedStep + unassignedStepMessage } from '../../shared/approval'; import { IRequestFormProps } from '../../models/types'; import { @@ -215,10 +215,11 @@ const VacationForm: React.FC = ({ if (matrix.error) { return `Не удалось определить маршрут согласования: ${matrix.error}. Попробуйте ещё раз или обратитесь к администратору.`; } - const unassigned = findUnassignedStep(matrix.rows); - if (unassigned) { - return `Шаг ${unassigned.order} маршрута согласования («${unassigned.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`; - } + const unassignedError = unassignedStepMessage( + matrix.rows, + (r) => `Шаг ${r.order} маршрута согласования («${r.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.` + ); + if (unassignedError) return unassignedError; return undefined; }; diff --git a/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupForm.tsx b/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupForm.tsx index f4b77c6..9c84956 100644 --- a/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupForm.tsx +++ b/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupForm.tsx @@ -27,7 +27,7 @@ import { isEditableStatus, IApproverToWrite, APPROVAL_PHASE, - findUnassignedStep + unassignedStepMessage } from '../../shared/approval'; import { uploadAttachments, @@ -262,14 +262,16 @@ const WorkplaceSetupForm: React.FC = ({ const validateSubmit = (): string | undefined => { const formError = validate(); if (formError) return formError; - const unassigned = findUnassignedStep(matrix.rows); - if (unassigned) { - return `Шаг ${unassigned.order} маршрута согласования («${unassigned.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`; - } - const unassignedExecutor = findUnassignedStep(executors.rows); - if (unassignedExecutor) { - return `Шаг ${unassignedExecutor.order} исполнения («${unassignedExecutor.position || 'без должности'}») без назначенного человека — выберите исполнителя или удалите шаг.`; - } + const unassignedApproverError = unassignedStepMessage( + matrix.rows, + (r) => `Шаг ${r.order} маршрута согласования («${r.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.` + ); + if (unassignedApproverError) return unassignedApproverError; + const unassignedExecutorError = unassignedStepMessage( + executors.rows, + (r) => `Шаг ${r.order} исполнения («${r.position || 'без должности'}») без назначенного человека — выберите исполнителя или удалите шаг.` + ); + if (unassignedExecutorError) return unassignedExecutorError; return undefined; }; diff --git a/src/webparts/requestPortal/shared/approval/approvalSubmission.service.ts b/src/webparts/requestPortal/shared/approval/approvalSubmission.service.ts index 3850c63..3e38182 100644 --- a/src/webparts/requestPortal/shared/approval/approvalSubmission.service.ts +++ b/src/webparts/requestPortal/shared/approval/approvalSubmission.service.ts @@ -38,6 +38,7 @@ export async function getPlannedMatrix( phase, slaHours: s.slaHours, askOverdueReason: s.askOverdueReason, + warning: s.warning, })) ); } diff --git a/src/webparts/requestPortal/shared/approval/index.ts b/src/webparts/requestPortal/shared/approval/index.ts index fcaf057..ebb26d8 100644 --- a/src/webparts/requestPortal/shared/approval/index.ts +++ b/src/webparts/requestPortal/shared/approval/index.ts @@ -50,7 +50,7 @@ export type { ApproverDecision, IPendingRow } from './approverDecision.service'; export { getPlannedMatrix, writeApprovers, writeRoute } from './approvalSubmission.service'; export { notifyActiveStep } from './notify.service'; export type { INotifyTarget } from './notify.service'; -export { useMatrixEditor, findUnassignedStep } from './useMatrixEditor'; +export { useMatrixEditor, findUnassignedStep, unassignedStepMessage } from './useMatrixEditor'; export type { IUseMatrixEditor } from './useMatrixEditor'; export { ApprovalMatrix } from './ui/ApprovalMatrix'; export { UserPicker } from './ui/UserPicker'; diff --git a/src/webparts/requestPortal/shared/approval/models/IMatrixRow.ts b/src/webparts/requestPortal/shared/approval/models/IMatrixRow.ts index 993fe5a..29b5810 100644 --- a/src/webparts/requestPortal/shared/approval/models/IMatrixRow.ts +++ b/src/webparts/requestPortal/shared/approval/models/IMatrixRow.ts @@ -31,4 +31,16 @@ export interface IMatrixRow { slaHours?: number | null; // eslint-disable-next-line @rushstack/no-new-null -- см. slaHours askOverdueReason?: boolean | null; + /** + * Предупреждение approval-route по ЭТОМУ шагу (см. IResolvedRouteStep.warning) — + * например, роль не разрешилась в конкретного человека, и почему именно. + * Только у ПЛАНИРУЕМОГО маршрута (см. getPlannedMatrix): у уже записанных строк + * (loadXxxRequest) неоткуда взяться — резолв запускался один раз при подаче. + * + * findUnassignedStep показывает его вместо своего общего текста, когда он есть — + * бэкенд знает причину точнее («роль не назначена в справочнике отдела» и т.п.), + * а не просто «нет назначенного человека». + */ + // eslint-disable-next-line @rushstack/no-new-null -- зеркалит nullable-контракт бэкенда (IResolvedRouteStep.warning) + warning?: string | null; } diff --git a/src/webparts/requestPortal/shared/approval/useMatrixEditor.ts b/src/webparts/requestPortal/shared/approval/useMatrixEditor.ts index 81f70ab..a125909 100644 --- a/src/webparts/requestPortal/shared/approval/useMatrixEditor.ts +++ b/src/webparts/requestPortal/shared/approval/useMatrixEditor.ts @@ -118,6 +118,25 @@ export function findUnassignedStep(rows: IMatrixRow[]): IMatrixRow | undefined { return rows.filter((r) => !r.email).sort((a, b) => a.order - b.order)[0]; } +/** + * Текст ошибки под первый незанятый шаг — то, что раньше было скопировано в + * validateSubmit каждой формы (findUnassignedStep + `row.warning || <шаблон>`). + * fallback принимает саму строку и решает, что писать, если approval-route не + * прислал свою причину (warning) — у большинства форм это общий шаблон «без + * назначенного человека — выберите …», но у некоторых (см. Ticket1CForm) текст + * другой, поэтому шаблон не зашит сюда, а передаётся вызывающей стороной. + * + * undefined — назначенных не хватает не для этих rows (или все шаги заняты). + */ +export function unassignedStepMessage( + rows: IMatrixRow[], + fallback: (row: IMatrixRow) => string +): string | undefined { + const row = findUnassignedStep(rows); + if (!row) return undefined; + return row.warning || fallback(row); +} + /** * Загружает матрицу согласования и (если editable) позволяет её редактировать. * @param load загрузчик строк (планируемый маршрут из resolve или строки заявки).