diff --git a/src/webparts/requestPortal/forms/Clearance/ClearanceForm.tsx b/src/webparts/requestPortal/forms/Clearance/ClearanceForm.tsx index 4417739..0249ab0 100644 --- a/src/webparts/requestPortal/forms/Clearance/ClearanceForm.tsx +++ b/src/webparts/requestPortal/forms/Clearance/ClearanceForm.tsx @@ -21,6 +21,7 @@ import { REQUEST_STATUS, isEditableStatus, IApproverToWrite, + APPROVAL_PHASE, findUnassignedStep } from '../../shared/approval'; import { IRequestFormProps } from '../../models/types'; @@ -106,17 +107,42 @@ const ClearanceForm: React.FC = ({ requestType, context, onCa 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, data.Employee, approval, 'Approval', null, [ { fieldName: 'Position', value: data.Position } ]), [context, approval, editId, data.Employee, data.Position, requestTypeId] ); + const matrixReady = !!editId || (!!data.Employee && !!requestTypeId); const matrix = useMatrixEditor( loadMatrix, `${data.Employee}-${editId ?? 'new'}-${data.Position}`, !readOnly, - !!editId || (!!data.Employee && !!requestTypeId), + matrixReady, + (login) => approval.getJobTitle(login) + ); + + // Фаза исполнения: тот же маршрут (условие «IsManager» по data.Position — см. + // комментарий у loadMatrix выше), только 'Execution' вместо 'Approval'. У + // открытой на редактирование заявки — строки уже записаны, читаем как есть. + const loadExecutors = useCallback( + () => + editId + ? approval + .getMatrixForRequest(editId) + .then((rows) => rows.filter((r) => r.phase === APPROVAL_PHASE.execution)) + : getPlannedMatrix(context, requestTypeId as number, data.Employee, approval, 'Execution', null, [ + { fieldName: 'Position', value: data.Position } + ]), + [context, approval, editId, data.Employee, data.Position, requestTypeId] + ); + const executors = useMatrixEditor( + loadExecutors, + `e-${data.Employee}-${editId ?? 'new'}-${data.Position}`, + !readOnly, + matrixReady, (login) => approval.getJobTitle(login) ); @@ -148,7 +174,14 @@ const ClearanceForm: React.FC = ({ requestType, context, onCa if (!rtId) { throw new Error('Тип заявки не определён — обновите страницу и повторите.'); } - const { id, code } = await saveClearanceRequest(requestId, rtId, data, toApprovers(matrix.rows), status); + const { id, code } = await saveClearanceRequest( + requestId, + rtId, + data, + toApprovers(matrix.rows), + toApprovers(executors.rows), + status + ); setRequestId(id); setRequestStatus(status); if (code) { @@ -179,6 +212,10 @@ const ClearanceForm: React.FC = ({ requestType, context, onCa if (unassigned) { return `Шаг ${unassigned.order} маршрута согласования («${unassigned.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`; } + const unassignedExecutor = findUnassignedStep(executors.rows); + if (unassignedExecutor) { + return `Шаг ${unassignedExecutor.order} исполнения («${unassignedExecutor.position || 'без должности'}») без назначенного человека — выберите исполнителя или удалите шаг.`; + } return undefined; }; @@ -220,6 +257,18 @@ const ClearanceForm: React.FC = ({ requestType, context, onCa approval.searchUsers(q)} /> + {/* Блок исполнителя показываем, только когда по маршруту реально есть + исполнители (условие «IsManager» и т.п. могло не назначить ни одного). */} + {matrixReady && !executors.loading && executors.rows.length > 0 && ( + approval.searchUsers(q)} + title="Исполнение (после согласования)" + roleLabel="Исполнитель" + emptyText="Исполнители не заданы по маршруту." + /> + )} + = ({ context, itemId, formKey [approval] ); - // Матрица маршрута из фактических строк заявки (read-only). decisionSeq в - // reloadKey: после решения строки на сервере меняются — перечитываем матрицу. + // Матрица маршрута из фактических строк заявки (read-only), раздельно по + // фазам: согласование и исполнение — два отдельных блока в UI (см. ниже). + // 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) ); + 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) + ); useEffect(() => { let alive = true; @@ -196,6 +214,16 @@ const ClearanceView: React.FC = ({ context, itemId, formKey approval.searchUsers(q)} /> + + {!executors.loading && executors.rows.length > 0 && ( + approval.searchUsers(q)} + title="Исполнение" + roleLabel="Исполнитель" + emptyText="Исполнители не назначены." + /> + )} {/* Панель решения — ВНЕ печати и FormModeProvider (комментарий редактируемый). */} diff --git a/src/webparts/requestPortal/forms/Clearance/sections/RequestSection.tsx b/src/webparts/requestPortal/forms/Clearance/sections/RequestSection.tsx index 28ae2d1..9e45ea6 100644 --- a/src/webparts/requestPortal/forms/Clearance/sections/RequestSection.tsx +++ b/src/webparts/requestPortal/forms/Clearance/sections/RequestSection.tsx @@ -2,9 +2,9 @@ import * as React from 'react'; import { CollapseSection, FormField, TextInput, MoneyInput, Toggle, PeopleField } from '../../../shared/ui'; import { ISectionProps, EMPLOYEE_PLACEHOLDER } from '../model'; -// Должность/подразделение подтягиваются автоматически при выборе сотрудника -// (см. ClearanceForm.handleEmployeeChange — SharePoint User Profile) — -// свободный ввод здесь намеренно убран, чтобы не расходиться с профилем. +// Подразделение подтягивается автоматически при выборе сотрудника (см. +// ClearanceForm.handleEmployeeChange — SharePoint User Profile) — свободный +// ввод здесь намеренно убран, чтобы не расходиться с профилем. const NOOP = (): void => undefined; interface IRequestSectionProps extends ISectionProps { @@ -32,12 +32,16 @@ export const RequestSection: React.FC = ({ placeholder={EMPLOYEE_PLACEHOLDER} /> + {/* Подтягивается из профиля при выборе сотрудника, но поле остаётся + редактируемым: если профиль не синхронизирован (SPS-JobTitle пуст), + HR должен суметь ввести должность вручную — иначе маршрут + согласования (условие «IsManager» резолвится именно по ней) не + соберётся, и список исполнителей ниже останется пустым. */} set('Position', v)} placeholder="Подставится автоматически по выбранному сотруднику" /> diff --git a/src/webparts/requestPortal/forms/Clearance/submit.ts b/src/webparts/requestPortal/forms/Clearance/submit.ts index fe5c512..5d786f2 100644 --- a/src/webparts/requestPortal/forms/Clearance/submit.ts +++ b/src/webparts/requestPortal/forms/Clearance/submit.ts @@ -76,9 +76,8 @@ export interface ISaveResult { } /** - * Сохранить обходной лист: содержимое + маршрут согласования. + * Сохранить обходной лист: содержимое + маршрут (согласование + исполнение). * existingId задан → обновляем ту же запись (черновик/доработка), иначе создаём новую. - * Фазы исполнения у обходного листа нет — writeRoute вызывается с пустым списком исполнителей. * Возвращает Id заявки (+ код при создании). */ export async function saveClearanceRequest( @@ -86,6 +85,7 @@ export async function saveClearanceRequest( requestTypeId: number, data: IFormData, approvers: IApproverToWrite[], + executors: IApproverToWrite[], status: string ): Promise { const list = getSP().web.lists.getByTitle(dataListTitle(FORM_ID)); @@ -105,6 +105,6 @@ export async function saveClearanceRequest( await list.items.getById(id).update({ RequestUniqueCode: code }); } - await writeRoute(clearanceApprovalService(), id, approvers, [], status); + await writeRoute(clearanceApprovalService(), id, approvers, executors, status); return { id, code }; }