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

Алтынай Бултурукова (право canEditRoute) просила добавлять участников в уже
поданный маршрут. Новый согласующий или исполнитель встаёт в конец своего
этапа со статусом «Ждёт очереди» (дальше его можно поднять стрелками);
согласующего — пока идёт согласование, исполнителя — пока не завершено
исполнение, без дублей среди ещё не решивших. Заявка «трогается», чтобы
бэкенд выдал новому участнику права на элемент данных.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Тимур Городецкий
2026-10-09 12:16:18 +05:00
parent 79d2a59229
commit c80972469f
10 changed files with 334 additions and 14 deletions
@@ -36,6 +36,7 @@ import {
queuedStepMoveRule,
useQueuedStepMove
} from '../../shared/approval';
import { useRouteStepAdd } from '../../shared/approval/useRouteStepAdd';
import { navigate } from '../../shared/routing';
import { effectiveUserEmail } from '../../shared/impersonation';
import { getAttachments } from '../../shared/attachments/attachments.service';
@@ -280,6 +281,43 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
setRouteError
);
// Выше ранних return (загрузка/ошибка): зовётся и из колбэков хуков ниже.
const notify = (): void => {
if (!requestTypeId) return;
notifyActiveStep(context, approval, itemId, {
requestTypeId,
dataListName: listId ? dataListTitleForListId(FORM_ID, listId) : dataListTitle(FORM_ID),
approvalListName: listId ? approvalListTitleForListId(FORM_ID, listId) : approvalListTitle(FORM_ID),
}).catch(() => undefined);
};
// Добавить согласующего/исполнителя в поданную заявку — то же право
// routeEditRights.ts; правило «в конец этапа, пока он не завершён» — routeStepAdd.ts.
// Новый участник обычно ждёт очереди; если стал активным — сообщаем ему.
const handleStepAdded = (activatedRowIds: number[]): void => {
setDecisionSeq((n) => n + 1);
if (activatedRowIds.length > 0) notify();
};
const handleAddApprover = useRouteStepAdd(
approverService,
itemId,
APPROVAL_PHASE.approval,
handleStepAdded,
setRouteError
);
const handleAddExecutor = useRouteStepAdd(
approverService,
itemId,
APPROVAL_PHASE.execution,
handleStepAdded,
setRouteError
);
// По статусу заявки: согласующих — пока идёт согласование, исполнителей —
// пока не завершено исполнение. Сервер перепроверит по строкам (routeStepAdd.ts).
const canAddApprover = canRemoveApprover && status === REQUEST_STATUS.onApproval;
const canAddExecutor =
canRemoveApprover && (status === REQUEST_STATUS.onApproval || status === REQUEST_STATUS.execution);
useEffect(() => {
let alive = true;
setLoading(true);
@@ -321,15 +359,6 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
const sectionProps = { data, set, context, payrollEditable, hireDateEditable };
const notify = (): void => {
if (!requestTypeId) return;
notifyActiveStep(context, approval, itemId, {
requestTypeId,
dataListName: listId ? dataListTitleForListId(FORM_ID, listId) : dataListTitle(FORM_ID),
approvalListName: listId ? approvalListTitleForListId(FORM_ID, listId) : approvalListTitle(FORM_ID),
}).catch(() => undefined);
};
return (
<div>
<div className={`${styles.toolbar} rp-no-print`}>
@@ -410,17 +439,21 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
onRemoveRow={handleRemoveApprover}
canMoveRow={canRemoveApprover ? queuedStepMoveRule(approvers.rows) : undefined}
onMoveRow={handleMoveStep}
onAddRow={canAddApprover ? handleAddApprover : undefined}
/>
{!executors.loading && executors.rows.length > 0 && (
{/* Без исполнителей блок показываем, только если их можно добавить. */}
{!executors.loading && (executors.rows.length > 0 || canAddExecutor) && (
<ApprovalMatrix
editor={executors}
search={(q) => approval.searchUsers(q)}
title="Исполнение"
roleLabel="Исполнитель"
emptyText="Исполнители не назначены."
pickerPlaceholder="Добавить исполнителя: начните вводить ФИО…"
canMoveRow={canRemoveApprover ? queuedStepMoveRule(executors.rows) : undefined}
onMoveRow={handleMoveStep}
onAddRow={canAddExecutor ? handleAddExecutor : undefined}
/>
)}
</div>
@@ -35,6 +35,7 @@ import {
queuedStepMoveRule,
useQueuedStepMove
} from '../../shared/approval';
import { useRouteStepAdd } from '../../shared/approval/useRouteStepAdd';
import { getAttachments } from '../../shared/attachments/attachments.service';
import { useAttachmentEditor } from '../../shared/attachments/useAttachmentEditor';
import { EditableAttachments } from '../../shared/attachments/EditableAttachments';
@@ -267,6 +268,33 @@ const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ cont
setRouteError
);
// Добавить согласующего/исполнителя в поданную заявку — то же право
// routeEditRights.ts; правило «в конец этапа, пока он не завершён» — routeStepAdd.ts.
// Новый участник обычно ждёт очереди; если стал активным — сообщаем ему.
const handleStepAdded = (activatedRowIds: number[]): void => {
setDecisionSeq((n) => n + 1);
activatedRowIds.forEach((id) => notify(id));
};
const handleAddApprover = useRouteStepAdd(
approverService,
itemId,
APPROVAL_PHASE.approval,
handleStepAdded,
setRouteError
);
const handleAddExecutor = useRouteStepAdd(
approverService,
itemId,
APPROVAL_PHASE.execution,
handleStepAdded,
setRouteError
);
// По статусу заявки: согласующих — пока идёт согласование, исполнителей —
// пока не завершено исполнение. Сервер перепроверит по строкам (routeStepAdd.ts).
const canAddApprover = canRemoveApprover && status === REQUEST_STATUS.onApproval;
const canAddExecutor =
canRemoveApprover && (status === REQUEST_STATUS.onApproval || status === REQUEST_STATUS.execution);
useEffect(() => {
let alive = true;
setLoading(true);
@@ -371,15 +399,19 @@ const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ cont
onRemoveRow={handleRemoveApprover}
canMoveRow={canRemoveApprover ? queuedStepMoveRule(approvers.rows) : undefined}
onMoveRow={handleMoveStep}
onAddRow={canAddApprover ? handleAddApprover : undefined}
/>
{!executors.loading && executors.rows.length > 0 && (
{/* Без исполнителей блок показываем, только если их можно добавить. */}
{!executors.loading && (executors.rows.length > 0 || canAddExecutor) && (
<ApprovalMatrix
editor={executors}
search={(q) => approval.searchUsers(q)}
title="Исполнение"
roleLabel="Исполнитель"
emptyText="Исполнители не назначены."
pickerPlaceholder="Добавить исполнителя: начните вводить ФИО…"
onAddRow={canAddExecutor ? handleAddExecutor : undefined}
canReplaceRow={canRemoveApprover ? isPendingExecutor : undefined}
onReplaceRow={handleReplaceExecutor}
canRemoveRow={