feat: удалять согласующих при составлении заявки во всех формах — для редакторов маршрута

Редактор маршрута (canEditRoute) теперь может удалить любого согласующего при подаче или
правке заявки во всех формах с маршрутом согласования, а не только в трёх рекрутинговых.
В список редакторов добавлен Павел Горбенко. Реестр особых прав обновлён.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
gorbenko.pavel
2026-10-01 17:08:06 +05:00
parent bed190835e
commit 80caa59301
14 changed files with 72 additions and 10 deletions
+2 -1
View File
@@ -16,7 +16,8 @@
| Право | Кто | Как определяется | Где действует | Файл с проверкой |
|---|---|---|---|---|
| Удалять согласующих, которые ещё ждут очереди | Алтынай Бултурукова | email в коде | Подбор, Приём, Реферальная рекомендация: и при составлении, и в поданной заявке | `shared/approval/routeEditRights.ts` → `canEditRoute` |
| Удалять согласующих, которые ещё ждут очереди | Алтынай Бултурукова, Павел Горбенко | email в коде | Подбор, Приём, Реферальная рекомендация: и при составлении, и в поданной заявке | `shared/approval/routeEditRights.ts` → `canEditRoute` |
| Удалять любых согласующих **при составлении** заявки | те же (`canEditRoute`) | email в коде | Все остальные формы с маршрутом согласования (отпуск, увольнение, 1С и т. д.); в поданной заявке — нет | `shared/approval/routeEditRights.ts` → `composeRemoveRowRule` |
| Заменять и удалять исполнителей, **в том числе активного** («На исполнении») | Алтынай Бултурукова | тот же email (`canEditRoute`) | **Только** Заявка на подбор, матрица «Исполнение» | `canEditRoute` + `forms/RecruitingSelection/RecruitingSelectionView.tsx` |
| Править **все поля** заявки на своём активном шаге согласования (без вложений) | Алтынай Бултурукова | email в коде | Подбор, Приём (Реферальная рекомендация — нет) | `shared/approval/fullEditRights.ts` → `canEditAllFields` |
| Править оклад на ИС, оклад после ИС, «Отдел/Участок» на своём шаге согласования | Начальник расчётной группы (сейчас Ким Анна) | должность шага: «начальник/руководитель» + «расчет» | Заявка на приём | `forms/RecruitingHire/approverRights.ts` → `canEditPayrollFields` |
@@ -13,6 +13,7 @@ import {
useRequestSubmit
} from '../../shared/ui';
import {
composeRemoveRowRule,
ApprovalMatrix,
ApprovalHistory,
StatusBadge,
@@ -27,6 +28,7 @@ import {
APPROVAL_PHASE,
unassignedStepMessage
} from '../../shared/approval';
import { effectiveUserEmail } from '../../shared/impersonation';
import { IRequestFormProps } from '../../models/types';
import { getEmployeeOrganizations, formatEmployeeOrganizations } from '../../shared/dictionary';
import { friendlyErrorMessage } from '../../shared/diagnostics';
@@ -326,7 +328,11 @@ const ClearanceForm: React.FC<IRequestFormProps> = ({
<RequestSection {...sectionProps} />
</FormModeProvider>
<ApprovalMatrix editor={matrix} search={(q) => approval.searchUsers(q)} />
<ApprovalMatrix
editor={matrix}
search={(q) => approval.searchUsers(q)}
canRemoveRow={composeRemoveRowRule(effectiveUserEmail(context))}
/>
{/* Блок исполнителя показываем, только когда по маршруту реально есть
исполнители (условие «IsManager» и т.п. могло не назначить ни одного). */}
@@ -13,6 +13,7 @@ import {
useRequestSubmit
} from '../../shared/ui';
import {
composeRemoveRowRule,
ApprovalMatrix,
ApprovalHistory,
StatusBadge,
@@ -26,6 +27,7 @@ import {
IApproverToWrite,
unassignedStepMessage
} from '../../shared/approval';
import { effectiveUserEmail } from '../../shared/impersonation';
import { IRequestFormProps } from '../../models/types';
import { getEmployeeOrganizations, formatEmployeeOrganizations } from '../../shared/dictionary';
import { friendlyErrorMessage } from '../../shared/diagnostics';
@@ -323,6 +325,7 @@ const ConcreteRequestForm: React.FC<IRequestFormProps> = ({
<ApprovalMatrix
editor={matrix}
canRemoveRow={composeRemoveRowRule(effectiveUserEmail(context))}
search={(q) => approval.searchUsers(q)}
waiting={!matrixReady}
waitingText="Укажите начальника участка и проект, чтобы построить маршрут согласования."
@@ -13,6 +13,7 @@ import {
useRequestSubmit
} from '../../shared/ui';
import {
composeRemoveRowRule,
ApprovalMatrix,
ApprovalHistory,
StatusBadge,
@@ -26,6 +27,7 @@ import {
unassignedStepMessage,
resolveWriteListId
} from '../../shared/approval';
import { effectiveUserEmail } from '../../shared/impersonation';
import { IRequestFormProps } from '../../models/types';
import { getEmployeeOrganizations, getCompanyDirectorEmail, IEmployeeOrganization } from '../../shared/dictionary';
import { friendlyErrorMessage } from '../../shared/diagnostics';
@@ -340,7 +342,11 @@ const DismissalForm: React.FC<IRequestFormProps> = ({
<RequestSection {...sectionProps} />
</FormModeProvider>
<ApprovalMatrix editor={matrix} search={(q) => approval.searchUsers(q)} />
<ApprovalMatrix
editor={matrix}
search={(q) => approval.searchUsers(q)}
canRemoveRow={composeRemoveRowRule(effectiveUserEmail(context))}
/>
{/* Автору на доработке важнее всего видеть, почему вернули или
отклонили, — поэтому история и в форме, а не только в просмотре.
@@ -13,6 +13,7 @@ import {
useRequestSubmit
} from '../../shared/ui';
import {
composeRemoveRowRule,
ApprovalMatrix,
ApprovalHistory,
StatusBadge,
@@ -26,6 +27,7 @@ import {
IApproverToWrite,
unassignedStepMessage
} from '../../shared/approval';
import { effectiveUserEmail } from '../../shared/impersonation';
import { IRequestFormProps } from '../../models/types';
import { getEmployeeOrganizations, formatEmployeeOrganizations } from '../../shared/dictionary';
import { friendlyErrorMessage } from '../../shared/diagnostics';
@@ -302,7 +304,11 @@ const EmployeeTransferForm: React.FC<IRequestFormProps> = ({
<RequestSection {...sectionProps} />
</FormModeProvider>
<ApprovalMatrix editor={matrix} search={(q) => approval.searchUsers(q)} />
<ApprovalMatrix
editor={matrix}
search={(q) => approval.searchUsers(q)}
canRemoveRow={composeRemoveRowRule(effectiveUserEmail(context))}
/>
{/* Автору на доработке важнее всего видеть, почему вернули или
отклонили. Только у уже существующей заявки: у новой истории нет. */}
@@ -13,6 +13,7 @@ import {
useRequestSubmit
} from '../../shared/ui';
import {
composeRemoveRowRule,
ApprovalMatrix,
ApprovalHistory,
StatusBadge,
@@ -383,6 +384,7 @@ const ServiceRequestForm: React.FC<IRequestFormProps> = ({
<ApprovalMatrix
editor={matrix}
canRemoveRow={composeRemoveRowRule(effectiveUserEmail(context))}
search={(q) => approval.searchUsers(q)}
waiting={!matrixReady}
waitingText="Укажите заявителя и проект, чтобы построить маршрут согласования."
@@ -13,6 +13,7 @@ import {
useRequestSubmit
} from '../../shared/ui';
import {
composeRemoveRowRule,
ApprovalMatrix,
ApprovalHistory,
StatusBadge,
@@ -25,6 +26,7 @@ import {
unassignedStepMessage,
resolveWriteListId
} from '../../shared/approval';
import { effectiveUserEmail } from '../../shared/impersonation';
import { IRequestFormProps } from '../../models/types';
import { getEmployeeOrganizations, formatEmployeeOrganizations } from '../../shared/dictionary';
import { friendlyErrorMessage } from '../../shared/diagnostics';
@@ -268,7 +270,11 @@ const SkudExplanationForm: React.FC<IRequestFormProps> = ({
<ExplanationSection {...sectionProps} />
</FormModeProvider>
<ApprovalMatrix editor={matrix} search={(q) => approval.searchUsers(q)} />
<ApprovalMatrix
editor={matrix}
search={(q) => approval.searchUsers(q)}
canRemoveRow={composeRemoveRowRule(effectiveUserEmail(context))}
/>
{!executors.loading && executors.rows.length > 0 && (
<ApprovalMatrix
@@ -17,6 +17,7 @@ import {
useRequestSubmit
} from '../../shared/ui';
import {
composeRemoveRowRule,
ApprovalMatrix,
ApprovalHistory,
StatusBadge,
@@ -415,6 +416,7 @@ const Ticket1CForm: React.FC<IRequestFormProps> = ({
<ApprovalMatrix
editor={matrixEditor}
canRemoveRow={composeRemoveRowRule(effectiveUserEmail(context))}
search={(q) => approval.searchUsers(q)}
// Пустой маршрут — штатная ситуация: для части видов работ согласование
// не нужно, заявка сразу уходит в Planner. Сбой загрузки маршрута
@@ -17,6 +17,7 @@ import {
useRequestSubmit
} from '../../shared/ui';
import {
composeRemoveRowRule,
ApprovalMatrix,
ApprovalHistory,
StatusBadge,
@@ -498,6 +499,7 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({
<ApprovalMatrix
editor={matrix}
canRemoveRow={composeRemoveRowRule(effectiveUserEmail(context))}
search={(q) => approval.searchUsers(q)}
waiting={!matrixReady}
waitingText="Укажите заявителя и проект-источник («Откуда»), чтобы построить маршрут согласования."
@@ -13,6 +13,7 @@ import {
useRequestSubmit
} from '../../shared/ui';
import {
composeRemoveRowRule,
ApprovalMatrix,
ApprovalHistory,
StatusBadge,
@@ -26,6 +27,7 @@ import {
unassignedStepMessage,
resolveWriteListId
} from '../../shared/approval';
import { effectiveUserEmail } from '../../shared/impersonation';
import { IRequestFormProps } from '../../models/types';
import { getEmployeeOrganizations, formatEmployeeOrganizations } from '../../shared/dictionary';
import { friendlyErrorMessage } from '../../shared/diagnostics';
@@ -314,7 +316,11 @@ const UnpaidLeaveForm: React.FC<IRequestFormProps> = ({
<RequestSection {...sectionProps} />
</FormModeProvider>
<ApprovalMatrix editor={matrix} search={(q) => approval.searchUsers(q)} />
<ApprovalMatrix
editor={matrix}
search={(q) => approval.searchUsers(q)}
canRemoveRow={composeRemoveRowRule(effectiveUserEmail(context))}
/>
{/* Автору на доработке важнее всего видеть, почему вернули или
отклонили. Только у уже существующей заявки: у новой истории нет. */}
@@ -13,6 +13,7 @@ import {
useRequestSubmit
} from '../../shared/ui';
import {
composeRemoveRowRule,
ApprovalMatrix,
ApprovalHistory,
StatusBadge,
@@ -26,6 +27,7 @@ import {
IApproverToWrite,
unassignedStepMessage
} from '../../shared/approval';
import { effectiveUserEmail } from '../../shared/impersonation';
import { IRequestFormProps } from '../../models/types';
import { getEmployeeOrganizations, formatEmployeeOrganizations } from '../../shared/dictionary';
import { friendlyErrorMessage } from '../../shared/diagnostics';
@@ -313,7 +315,11 @@ const VacationForm: React.FC<IRequestFormProps> = ({
<RequestSection {...sectionProps} />
</FormModeProvider>
<ApprovalMatrix editor={matrix} search={(q) => approval.searchUsers(q)} />
<ApprovalMatrix
editor={matrix}
search={(q) => approval.searchUsers(q)}
canRemoveRow={composeRemoveRowRule(effectiveUserEmail(context))}
/>
{/* Автору на доработке важнее всего видеть, почему вернули или
отклонили. Только у уже существующей заявки: у новой истории нет. */}
@@ -17,6 +17,7 @@ import {
useRequestSubmit
} from '../../shared/ui';
import {
composeRemoveRowRule,
ApprovalMatrix,
ApprovalHistory,
StatusBadge,
@@ -391,7 +392,11 @@ const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({
</CollapseSection>
)}
<ApprovalMatrix editor={matrix} search={(q) => approval.searchUsers(q)} />
<ApprovalMatrix
editor={matrix}
search={(q) => approval.searchUsers(q)}
canRemoveRow={composeRemoveRowRule(effectiveUserEmail(context))}
/>
{!executors.loading && executors.rows.length > 0 && (
<ApprovalMatrix
@@ -65,7 +65,7 @@ export { useMatrixEditor, findUnassignedStep, unassignedStepMessage } from './us
export type { IUseMatrixEditor } from './useMatrixEditor';
export { resolveApproverOrgInfo } from './approverOrgInfo';
export type { IApproverOrgInfo } from './approverOrgInfo';
export { canEditRoute } from './routeEditRights';
export { canEditRoute, composeRemoveRowRule } from './routeEditRights';
export { canEditAllFields } from './fullEditRights';
export { ApprovalMatrix } from './ui/ApprovalMatrix';
export { ApprovalHistory } from './history/ApprovalHistory';
@@ -12,10 +12,21 @@
* canRemoveRow в ApprovalMatrix; здесь только сама проверка личности.
*/
const ROUTE_EDITOR_EMAILS: ReadonlySet<string> = new Set([
'a.bulturukova@tnsu.kz' // Алтынай Бултурукова
'a.bulturukova@tnsu.kz', // Алтынай Бултурукова
'gorbenko.pavel@tnsu.kz' // Павел Горбенко — составляет свой маршрут при подаче (обкатка форм)
]);
/** Может ли этот email удалять согласующих из маршрута (см. описание выше). */
export function canEditRoute(email: string | undefined): boolean {
return !!email && ROUTE_EDITOR_EMAILS.has(email.trim().toLowerCase());
}
/**
* Правило для ApprovalMatrix.canRemoveRow при СОСТАВЛЕНИИ заявки: редактор маршрута удаляет
* любого согласующего, остальным кнопки удаления нет. Удаление тут чисто локальное — при
* отправке маршрут целиком пишется из строк матрицы, поэтому записи на сервер не нужно.
* В поданной заявке так нельзя: там своя реализация (см. RecruitingHireView).
*/
export function composeRemoveRowRule(email: string | undefined): (() => boolean) | undefined {
return canEditRoute(email) ? () => true : undefined;
}