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