feat: инициатор заменяет согласующих в заявке на рабочее место до отправки

Если согласующий из маршрута недоступен (отпуск и т.п.), инициатор может
поставить на его шаг другого человека, пока заявка не отправлена. Шаг
целиком (порядок, роль, SLA) сохраняется, удалить его по-прежнему нельзя —
состав маршрута из админки остаётся гарантированным. Замена — общая операция
useMatrixEditor.replace, кнопка подключена только в этой форме.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Тимур Городецкий
2026-10-07 14:37:09 +05:00
parent bce2283bf3
commit 053ca9d8cd
3 changed files with 20 additions and 0 deletions
+1
View File
@@ -22,6 +22,7 @@
| Заменять и удалять исполнителей, **в том числе активного** («На исполнении») | Алтынай Бултурукова | тот же email (`canEditRoute`) | **Только** Заявка на подбор, матрица «Исполнение» | `canEditRoute` + `forms/RecruitingSelection/RecruitingSelectionView.tsx` | | Заменять и удалять исполнителей, **в том числе активного** («На исполнении») | Алтынай Бултурукова | тот же email (`canEditRoute`) | **Только** Заявка на подбор, матрица «Исполнение» | `canEditRoute` + `forms/RecruitingSelection/RecruitingSelectionView.tsx` |
| Править **все поля и вложения** заявки (добавлять и удалять файлы) на своём активном шаге согласования | Алтынай Бултурукова | email в коде (список по каждой форме) | Подбор, Приём (Реферальная рекомендация — нет) | `shared/approval/fullEditRights.ts` → `canEditAllFields` | | Править **все поля и вложения** заявки (добавлять и удалять файлы) на своём активном шаге согласования | Алтынай Бултурукова | email в коде (список по каждой форме) | Подбор, Приём (Реферальная рекомендация — нет) | `shared/approval/fullEditRights.ts` → `canEditAllFields` |
| То же: **все поля и вложения**, но **не только на своём шаге**: достаточно быть в маршруте (согласование или исполнение), пока заявка «На согласовании» или «Выполнение» | Ербол Шаймерденов | email в коде (список по каждой форме) + участие в маршруте | **Только** Организация рабочего места | `shared/approval/fullEditRights.ts` → `canEditAllFields` + `forms/WorkplaceSetup/useApproverRights.ts` | | То же: **все поля и вложения**, но **не только на своём шаге**: достаточно быть в маршруте (согласование или исполнение), пока заявка «На согласовании» или «Выполнение» | Ербол Шаймерденов | email в коде (список по каждой форме) + участие в маршруте | **Только** Организация рабочего места | `shared/approval/fullEditRights.ts` → `canEditAllFields` + `forms/WorkplaceSetup/useApproverRights.ts` |
| Заменять человека на шаге согласования (не удалять шаг) **до отправки**: новая заявка, черновик, доработка | Любой инициатор | право по статусу заявки (форма редактируема) | **Только** Организация рабочего места | `forms/WorkplaceSetup/WorkplaceSetupForm.tsx` (`canReplaceRow`) + `useMatrixEditor.replace` |
| Заполнять «Необходимое ПО» и «Доступы» на своём шаге согласования | Первый шаг маршрута согласования (по маршруту — непосредственный руководитель) | порядок шага в маршруте | Организация рабочего места | `forms/WorkplaceSetup/useApproverRights.ts` (`canEditReviewFields`) | | Заполнять «Необходимое ПО» и «Доступы» на своём шаге согласования | Первый шаг маршрута согласования (по маршруту — непосредственный руководитель) | порядок шага в маршруте | Организация рабочего места | `forms/WorkplaceSetup/useApproverRights.ts` (`canEditReviewFields`) |
| Править оклад на ИС, оклад после ИС, «Отдел/Участок» на своём шаге согласования | Начальник расчётной группы (сейчас Ким Анна) | должность шага: «начальник/руководитель» + «расчет» | Заявка на приём | `forms/RecruitingHire/approverRights.ts` → `canEditPayrollFields` | | Править оклад на ИС, оклад после ИС, «Отдел/Участок» на своём шаге согласования | Начальник расчётной группы (сейчас Ким Анна) | должность шага: «начальник/руководитель» + «расчет» | Заявка на приём | `forms/RecruitingHire/approverRights.ts` → `canEditPayrollFields` |
| Ставить дату приёма (2.2) | Активный исполнитель | роль в маршруте (шаг «Исполнение»), без проверки должности | Заявка на приём | `forms/RecruitingHire/RecruitingHireView.tsx` (`hireDateEditable`) | | Ставить дату приёма (2.2) | Активный исполнитель | роль в маршруте (шаг «Исполнение»), без проверки должности | Заявка на приём | `forms/RecruitingHire/RecruitingHireView.tsx` (`hireDateEditable`) |
@@ -414,6 +414,11 @@ const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({
editor={matrix} editor={matrix}
search={(q) => approval.searchUsers(q)} search={(q) => approval.searchUsers(q)}
canRemoveRow={composeRemoveRowRule(effectiveUserEmail(context))} canRemoveRow={composeRemoveRowRule(effectiveUserEmail(context))}
// Пока заявка не отправлена, любой инициатор может поставить на шаг
// другого человека (напр. согласующий в отпуске), но не убрать шаг:
// состав маршрута из админки остаётся гарантированным.
canReplaceRow={readOnly ? undefined : () => true}
onReplaceRow={matrix.replace}
/> />
{!executors.loading && (executors.editable || executors.rows.length > 0) && ( {!executors.loading && (executors.editable || executors.rows.length > 0) && (
@@ -17,6 +17,12 @@ export interface IUseMatrixEditor {
moveBlockUp: (index: number) => void; moveBlockUp: (index: number) => void;
moveBlockDown: (index: number) => void; moveBlockDown: (index: number) => void;
remove: (index: number) => void; remove: (index: number) => void;
/**
* Поставить в строку другого человека. Шаг (порядок, роль, SLA и прочие
* настройки из админки) остаётся прежним — меняется только исполнитель шага.
* Человек, уже стоящий в маршруте, не подставляется (дубль шага).
*/
replace: (row: IMatrixRow, user: ISiteUser) => void;
/** Переключить «идёт параллельно с предыдущим шагом». Для первой строки — no-op. */ /** Переключить «идёт параллельно с предыдущим шагом». Для первой строки — no-op. */
toggleParallel: (index: number) => void; toggleParallel: (index: number) => void;
/** Границы шагов: индексы строк, объединённых общим order. */ /** Границы шагов: индексы строк, объединённых общим order. */
@@ -285,6 +291,13 @@ export function useMatrixEditor(
const remove = (i: number): void => const remove = (i: number): void =>
setRows((prev) => withOrders(prev.filter((_, idx) => idx !== i))); 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 => const toggleParallel = (i: number): void =>
setRows((prev) => setRows((prev) =>
i <= 0 i <= 0
@@ -302,6 +315,7 @@ export function useMatrixEditor(
moveBlockUp, moveBlockUp,
moveBlockDown, moveBlockDown,
remove, remove,
replace,
toggleParallel, toggleParallel,
blocks: blockRanges(rows), blocks: blockRanges(rows),
}; };