From 053ca9d8cdb53b16574ec075d4a884d6923ea689 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A2=D0=B8=D0=BC=D1=83=D1=80=20=D0=93=D0=BE=D1=80=D0=BE?= =?UTF-8?q?=D0=B4=D0=B5=D1=86=D0=BA=D0=B8=D0=B9?= Date: Wed, 7 Oct 2026 14:37:09 +0500 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=B8=D0=BD=D0=B8=D1=86=D0=B8=D0=B0?= =?UTF-8?q?=D1=82=D0=BE=D1=80=20=D0=B7=D0=B0=D0=BC=D0=B5=D0=BD=D1=8F=D0=B5?= =?UTF-8?q?=D1=82=20=D1=81=D0=BE=D0=B3=D0=BB=D0=B0=D1=81=D1=83=D1=8E=D1=89?= =?UTF-8?q?=D0=B8=D1=85=20=D0=B2=20=D0=B7=D0=B0=D1=8F=D0=B2=D0=BA=D0=B5=20?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=80=D0=B0=D0=B1=D0=BE=D1=87=D0=B5=D0=B5=20?= =?UTF-8?q?=D0=BC=D0=B5=D1=81=D1=82=D0=BE=20=D0=B4=D0=BE=20=D0=BE=D1=82?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Если согласующий из маршрута недоступен (отпуск и т.п.), инициатор может поставить на его шаг другого человека, пока заявка не отправлена. Шаг целиком (порядок, роль, SLA) сохраняется, удалить его по-прежнему нельзя — состав маршрута из админки остаётся гарантированным. Замена — общая операция useMatrixEditor.replace, кнопка подключена только в этой форме. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/permissions.md | 1 + .../forms/WorkplaceSetup/WorkplaceSetupForm.tsx | 5 +++++ .../shared/approval/useMatrixEditor.ts | 14 ++++++++++++++ 3 files changed, 20 insertions(+) diff --git a/docs/permissions.md b/docs/permissions.md index f0b6fe3..1f3e8e3 100644 --- a/docs/permissions.md +++ b/docs/permissions.md @@ -22,6 +22,7 @@ | Заменять и удалять исполнителей, **в том числе активного** («На исполнении») | Алтынай Бултурукова | тот же email (`canEditRoute`) | **Только** Заявка на подбор, матрица «Исполнение» | `canEditRoute` + `forms/RecruitingSelection/RecruitingSelectionView.tsx` | | Править **все поля и вложения** заявки (добавлять и удалять файлы) на своём активном шаге согласования | Алтынай Бултурукова | email в коде (список по каждой форме) | Подбор, Приём (Реферальная рекомендация — нет) | `shared/approval/fullEditRights.ts` → `canEditAllFields` | | То же: **все поля и вложения**, но **не только на своём шаге**: достаточно быть в маршруте (согласование или исполнение), пока заявка «На согласовании» или «Выполнение» | Ербол Шаймерденов | email в коде (список по каждой форме) + участие в маршруте | **Только** Организация рабочего места | `shared/approval/fullEditRights.ts` → `canEditAllFields` + `forms/WorkplaceSetup/useApproverRights.ts` | +| Заменять человека на шаге согласования (не удалять шаг) **до отправки**: новая заявка, черновик, доработка | Любой инициатор | право по статусу заявки (форма редактируема) | **Только** Организация рабочего места | `forms/WorkplaceSetup/WorkplaceSetupForm.tsx` (`canReplaceRow`) + `useMatrixEditor.replace` | | Заполнять «Необходимое ПО» и «Доступы» на своём шаге согласования | Первый шаг маршрута согласования (по маршруту — непосредственный руководитель) | порядок шага в маршруте | Организация рабочего места | `forms/WorkplaceSetup/useApproverRights.ts` (`canEditReviewFields`) | | Править оклад на ИС, оклад после ИС, «Отдел/Участок» на своём шаге согласования | Начальник расчётной группы (сейчас Ким Анна) | должность шага: «начальник/руководитель» + «расчет» | Заявка на приём | `forms/RecruitingHire/approverRights.ts` → `canEditPayrollFields` | | Ставить дату приёма (2.2) | Активный исполнитель | роль в маршруте (шаг «Исполнение»), без проверки должности | Заявка на приём | `forms/RecruitingHire/RecruitingHireView.tsx` (`hireDateEditable`) | diff --git a/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupForm.tsx b/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupForm.tsx index 3fcced6..b46aec6 100644 --- a/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupForm.tsx +++ b/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupForm.tsx @@ -414,6 +414,11 @@ const WorkplaceSetupForm: React.FC = ({ editor={matrix} search={(q) => approval.searchUsers(q)} canRemoveRow={composeRemoveRowRule(effectiveUserEmail(context))} + // Пока заявка не отправлена, любой инициатор может поставить на шаг + // другого человека (напр. согласующий в отпуске), но не убрать шаг: + // состав маршрута из админки остаётся гарантированным. + canReplaceRow={readOnly ? undefined : () => true} + onReplaceRow={matrix.replace} /> {!executors.loading && (executors.editable || executors.rows.length > 0) && ( diff --git a/src/webparts/requestPortal/shared/approval/useMatrixEditor.ts b/src/webparts/requestPortal/shared/approval/useMatrixEditor.ts index beb882e..8ebf25c 100644 --- a/src/webparts/requestPortal/shared/approval/useMatrixEditor.ts +++ b/src/webparts/requestPortal/shared/approval/useMatrixEditor.ts @@ -17,6 +17,12 @@ export interface IUseMatrixEditor { moveBlockUp: (index: number) => void; moveBlockDown: (index: number) => void; remove: (index: number) => void; + /** + * Поставить в строку другого человека. Шаг (порядок, роль, SLA и прочие + * настройки из админки) остаётся прежним — меняется только исполнитель шага. + * Человек, уже стоящий в маршруте, не подставляется (дубль шага). + */ + replace: (row: IMatrixRow, user: ISiteUser) => void; /** Переключить «идёт параллельно с предыдущим шагом». Для первой строки — no-op. */ toggleParallel: (index: number) => void; /** Границы шагов: индексы строк, объединённых общим order. */ @@ -285,6 +291,13 @@ export function useMatrixEditor( const remove = (i: number): void => setRows((prev) => withOrders(prev.filter((_, idx) => idx !== i))); + const replace = (row: IMatrixRow, user: ISiteUser): void => + setRows((prev) => { + if (user.email && prev.some((r) => r !== row && r.email === user.email)) return prev; + // warning убираем: он был про незаполненную роль шага, а теперь человек выбран вручную. + return prev.map((r) => (r === row ? { ...r, who: user.name, email: user.email, warning: null } : r)); + }); + const toggleParallel = (i: number): void => setRows((prev) => i <= 0 @@ -302,6 +315,7 @@ export function useMatrixEditor( moveBlockUp, moveBlockDown, remove, + replace, toggleParallel, blocks: blockRanges(rows), };