From f6dcf349e9ba5b2d1002b59dc0a545041224e306 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: Tue, 22 Sep 2026 13:07:04 +0500 Subject: [PATCH] =?UTF-8?q?fix:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=B1=D0=BB=D0=BE=D0=BA=20=D0=B8=D1=81=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B2=20=D0=B7=D0=B0?= =?UTF-8?q?=D1=8F=D0=B2=D0=BA=D0=B5=20=D0=BD=D0=B0=20=D0=BF=D0=BE=D0=B4?= =?UTF-8?q?=D0=B1=D0=BE=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RecruitingSelectionForm.tsx | 42 ++++++++++++++++++- .../RecruitingSelectionView.tsx | 36 ++++++++++++++-- .../forms/RecruitingSelection/submit.ts | 10 +++-- 3 files changed, 78 insertions(+), 10 deletions(-) diff --git a/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx b/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx index 327f616..15dea85 100644 --- a/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx +++ b/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx @@ -28,7 +28,8 @@ import { IApproverToWrite, unassignedStepMessage, resolveApproverOrgInfo, - canEditRoute + canEditRoute, + APPROVAL_PHASE } from '../../shared/approval'; import { uploadAttachments, @@ -200,7 +201,9 @@ const RecruitingSelectionForm: React.FC = ({ const loadMatrix = useCallback( () => editId - ? approval.getMatrixForRequest(editId) + ? approval + .getMatrixForRequest(editId) + .then((rows) => rows.filter((r) => r.phase !== APPROVAL_PHASE.execution)) : getPlannedMatrix(context, requestTypeId as number, userEmail, approval, 'Approval', null, fieldValues, projectId), [context, userEmail, approval, editId, requestTypeId, fieldValues, projectId] ); @@ -214,6 +217,25 @@ const RecruitingSelectionForm: React.FC = ({ resolveApproverOrgInfo(context) ); + // Фаза исполнения: исполнители из маршрута (status='Execution'), только для чтения. + const loadExecutors = useCallback( + () => + editId + ? approval + .getMatrixForRequest(editId) + .then((rows) => rows.filter((r) => r.phase === APPROVAL_PHASE.execution)) + : getPlannedMatrix(context, requestTypeId as number, userEmail, approval, 'Execution', null, fieldValues, projectId), + [context, userEmail, approval, editId, requestTypeId, fieldValues, projectId] + ); + const executors = useMatrixEditor( + loadExecutors, + `${userEmail}-${editId ?? 'new'}-${projectId}-${companiesFieldValue}`, + false, + matrixReady, + (login) => approval.getJobTitle(login), + resolveApproverOrgInfo(context) + ); + // ObjectProjectId обязателен (а не просто текст ObjectProject): маршрут // согласования резолвится ПО ПРОЕКТУ (ByProject, см. projectId выше), и без // выбранного из списка варианта (id) approval-route вернёт 404 — форма @@ -284,6 +306,7 @@ const RecruitingSelectionForm: React.FC = ({ rtId, data, toApprovers(matrix.rows), + toApprovers(executors.rows), status, writeListId ); @@ -338,6 +361,11 @@ const RecruitingSelectionForm: React.FC = ({ (r) => `Шаг ${r.order} маршрута согласования («${r.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.` ); if (unassignedError) return unassignedError; + const unassignedExecutorError = unassignedStepMessage( + executors.rows, + (r) => `Шаг ${r.order} исполнения («${r.position || 'без должности'}») без назначенного человека — выберите исполнителя или удалите шаг.` + ); + if (unassignedExecutorError) return unassignedExecutorError; return undefined; }; @@ -407,6 +435,16 @@ const RecruitingSelectionForm: React.FC = ({ canRemoveRow={canRemoveApprover ? () => true : undefined} /> + {!executors.loading && executors.rows.length > 0 && ( + approval.searchUsers(q)} + title="Исполнение (после согласования)" + roleLabel="Исполнитель" + emptyText="Исполнители не заданы по маршруту." + /> + )} + = ({ cont const dataListRef = listId || dataListTitle(FORM_ID); const approverService = useMemo(() => new ApproverService(approval, dataListRef), [approval, dataListRef]); - // Матрица маршрута из фактических строк заявки (read-only). decisionSeq в - // reloadKey: после решения строки на сервере меняются — перечитываем матрицу. + // Матрица маршрута из фактических строк заявки (read-only), разбитая по + // фазам. decisionSeq в reloadKey: после решения строки на сервере меняются — + // перечитываем матрицу. const [decisionSeq, setDecisionSeq] = useState(0); - const loadApprovers = useCallback(() => approval.getMatrixForRequest(itemId), [approval, itemId]); + const loadApprovers = useCallback( + () => + approval + .getMatrixForRequest(itemId) + .then((rows) => rows.filter((r) => r.phase !== APPROVAL_PHASE.execution)), + [approval, itemId] + ); const approvers = useMatrixEditor(loadApprovers, `a-${itemId}-${decisionSeq}`, false, true, (l) => approval.getJobTitle(l), resolveApproverOrgInfo(context) ); + const loadExecutors = useCallback( + () => + approval + .getMatrixForRequest(itemId) + .then((rows) => rows.filter((r) => r.phase === APPROVAL_PHASE.execution)), + [approval, itemId] + ); + const executors = useMatrixEditor(loadExecutors, `e-${itemId}-${decisionSeq}`, false, true, (l) => + approval.getJobTitle(l), resolveApproverOrgInfo(context) + ); // Удалить ещё не решённый шаг маршрута (см. ApproverService.deleteQueuedStep). const handleRemoveApprover = (row: IMatrixRow): void => { @@ -187,6 +205,16 @@ const RecruitingSelectionView: React.FC = ({ cont canRemoveRow={canRemoveApprover ? (row) => row.status === REQUEST_STATUS.queued : undefined} onRemoveRow={handleRemoveApprover} /> + + {!executors.loading && executors.rows.length > 0 && ( + approval.searchUsers(q)} + title="Исполнение" + roleLabel="Исполнитель" + emptyText="Исполнители не назначены." + /> + )} {/* Панель решения — ВНЕ печати и FormModeProvider (комментарий редактируемый). */} diff --git a/src/webparts/requestPortal/forms/RecruitingSelection/submit.ts b/src/webparts/requestPortal/forms/RecruitingSelection/submit.ts index 4e4db9d..7b7c659 100644 --- a/src/webparts/requestPortal/forms/RecruitingSelection/submit.ts +++ b/src/webparts/requestPortal/forms/RecruitingSelection/submit.ts @@ -86,10 +86,11 @@ async function applyPeople(item: Record, data: IFormData): Prom export type ISaveResult = ISaveItemResult; /** - * Сохранить заявку «на подбор»: содержимое + маршрут согласования. + * Сохранить заявку «на подбор»: содержимое + маршрут (согласующие + исполнители). * existingId задан → обновляем ту же запись (черновик/доработка), иначе создаём новую. - * status = «Черновик» (сохранение) или «На согласовании» (отправка). Фазы исполнения - * нет — writeRoute вызывается с пустым списком исполнителей. Возвращает Id (+ код при создании). + * status = «Черновик» (сохранение) или «На согласовании» (отправка). + * executors может быть пустым — тогда после согласования заявка станет «Согласовано» + * (см. computeRequestStatus в approverDecision.service.ts). Возвращает Id (+ код при создании). * * listId — партиция, в которую фактически пишем: при редактировании — партиция * ОБНОВЛЯЕМОЙ заявки (см. loadSelectionRequest), при создании — партиция, @@ -104,6 +105,7 @@ export async function saveSelectionRequest( requestTypeId: number, data: IFormData, approvers: IApproverToWrite[], + executors: IApproverToWrite[], status: string, listId?: string ): Promise { @@ -112,6 +114,6 @@ export async function saveSelectionRequest( await applyPeople(item, data); const result = await saveRequestItem(FORM_ID, existingId, listId, requestTypeId, item); - await writeRoute(selectionApprovalService(listId), result.id, approvers, [], status); + await writeRoute(selectionApprovalService(listId), result.id, approvers, executors, status); return result; }