refactor: общий хук маршрута по фазам (usePhasedRoute)

Хук, который грузит маршрут согласования отдельно по фазам «Согласование»
и «Исполнение», был в папке «Объяснительной по СКУД». Он понадобился и
заявке на командировку, поэтому перенесён в shared/approval под общим
именем. Поведение СКУД не меняется — поменялись только импорты.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Тимур Городецкий
2026-10-05 10:19:57 +05:00
parent 4c8410bdcc
commit e85c01fca3
5 changed files with 26 additions and 22 deletions
@@ -10,7 +10,7 @@
## Маршрут ## Маршрут
- Фазы: согласование (напр. непосредственный руководитель) + исполнение (напр. кадры) — - Фазы: согласование (напр. непосредственный руководитель) + исполнение (напр. кадры) —
обе фазы грузятся отдельными запросами `getPlannedMatrix(..., 'Approval' | 'Execution')` обе фазы грузятся отдельными запросами `getPlannedMatrix(..., 'Approval' | 'Execution')`
в `useSkudRoute` (паттерн Referral). Без запроса 'Execution' шаг исполнения пропадает. в общем `usePhasedRoute` (shared/approval, паттерн Referral). Без запроса 'Execution' шаг исполнения пропадает.
- Планируемый маршрут резолвится по e-mail сотрудника; согласующих заявитель может - Планируемый маршрут резолвится по e-mail сотрудника; согласующих заявитель может
править (новая / черновик / возврат), исполнителей — нет (только чтение). править (новая / черновик / возврат), исполнителей — нет (только чтение).
- Нет согласующих вовсе — `initialSubmitStatus` сразу ставит «Выполнение». - Нет согласующих вовсе — `initialSubmitStatus` сразу ставит «Выполнение».
@@ -23,7 +23,8 @@ import {
initialSubmitStatus, initialSubmitStatus,
IApproverToWrite, IApproverToWrite,
unassignedStepMessage, unassignedStepMessage,
resolveWriteListId resolveWriteListId,
usePhasedRoute
} from '../../shared/approval'; } from '../../shared/approval';
import { IRequestFormProps } from '../../models/types'; import { IRequestFormProps } from '../../models/types';
import { getEmployeeOrganizations, formatEmployeeOrganizations } from '../../shared/dictionary'; import { getEmployeeOrganizations, formatEmployeeOrganizations } from '../../shared/dictionary';
@@ -43,7 +44,6 @@ import { loadSkudExplanationRequest } from './load';
import { submittedConflictMessage } from './submittedDays'; import { submittedConflictMessage } from './submittedDays';
import { getSubmittedDays } from './submittedDays.service'; import { getSubmittedDays } from './submittedDays.service';
import { useSubmittedDays } from './useSubmittedDays'; import { useSubmittedDays } from './useSubmittedDays';
import { useSkudRoute } from './useSkudRoute';
import { todayDateKey } from './skud/skudDays'; import { todayDateKey } from './skud/skudDays';
import { EmployeeSection } from './sections/EmployeeSection'; import { EmployeeSection } from './sections/EmployeeSection';
import { DaysSection } from './sections/DaysSection'; import { DaysSection } from './sections/DaysSection';
@@ -126,7 +126,7 @@ const SkudExplanationForm: React.FC<IRequestFormProps> = ({
}, [editId, context]); }, [editId, context]);
// Маршрут по сотруднику; у открытой заявки — из уже записанных строк, чтобы не затереть правки. // Маршрут по сотруднику; у открытой заявки — из уже записанных строк, чтобы не затереть правки.
const { approvers: matrix, executors } = useSkudRoute({ const { approvers: matrix, executors } = usePhasedRoute({
context, context,
approval, approval,
requestId: editId, requestId: editId,
@@ -9,7 +9,8 @@ import {
StatusBadge, StatusBadge,
ApprovalHistory, ApprovalHistory,
notifyActiveStep, notifyActiveStep,
useRequestTypeId useRequestTypeId,
usePhasedRoute
} from '../../shared/approval'; } from '../../shared/approval';
import type { ISiteUser } from '../../shared/approval'; import type { ISiteUser } from '../../shared/approval';
import { friendlyErrorMessage } from '../../shared/diagnostics'; import { friendlyErrorMessage } from '../../shared/diagnostics';
@@ -23,7 +24,6 @@ import { useStableCallback } from '../../shared/useStableCallback';
import { FORM_ID, FORM_TITLE, IFormData, EMPTY_DATA, SetField } from './model'; import { FORM_ID, FORM_TITLE, IFormData, EMPTY_DATA, SetField } from './model';
import { loadSkudExplanationRequest } from './load'; import { loadSkudExplanationRequest } from './load';
import { skudExplanationApprovalService } from './submit'; import { skudExplanationApprovalService } from './submit';
import { useSkudRoute } from './useSkudRoute';
import { EmployeeSection } from './sections/EmployeeSection'; import { EmployeeSection } from './sections/EmployeeSection';
import { DaysSection } from './sections/DaysSection'; import { DaysSection } from './sections/DaysSection';
import { ExplanationSection } from './sections/ExplanationSection'; import { ExplanationSection } from './sections/ExplanationSection';
@@ -65,7 +65,7 @@ const SkudExplanationView: React.FC<ISkudExplanationViewProps> = ({ context, ite
// decisionSeq: после решения строки маршрута меняются — перечитываем обе фазы. // decisionSeq: после решения строки маршрута меняются — перечитываем обе фазы.
const [decisionSeq, setDecisionSeq] = useState<number>(0); const [decisionSeq, setDecisionSeq] = useState<number>(0);
const { approvers, executors } = useSkudRoute({ const { approvers, executors } = usePhasedRoute({
context, context,
approval, approval,
requestId: itemId, requestId: itemId,
@@ -63,6 +63,8 @@ export { notifyActiveStep } from './notify.service';
export type { INotifyTarget } from './notify.service'; export type { INotifyTarget } from './notify.service';
export { useMatrixEditor, findUnassignedStep, unassignedStepMessage } from './useMatrixEditor'; export { useMatrixEditor, findUnassignedStep, unassignedStepMessage } from './useMatrixEditor';
export type { IUseMatrixEditor } from './useMatrixEditor'; export type { IUseMatrixEditor } from './useMatrixEditor';
export { usePhasedRoute } from './usePhasedRoute';
export type { IPhasedRoute } from './usePhasedRoute';
export { resolveApproverOrgInfo } from './approverOrgInfo'; export { resolveApproverOrgInfo } from './approverOrgInfo';
export type { IApproverOrgInfo } from './approverOrgInfo'; export type { IApproverOrgInfo } from './approverOrgInfo';
export { canEditRoute } from './routeEditRights'; export { canEditRoute } from './routeEditRights';
@@ -1,17 +1,15 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { WebPartContext } from '@microsoft/sp-webpart-base'; import { WebPartContext } from '@microsoft/sp-webpart-base';
import { import { APPROVAL_PHASE } from './models/statuses';
APPROVAL_PHASE, import { IMatrixRow } from './models/IMatrixRow';
ApprovalListService, import { ApprovalListService } from './approvalList.service';
IMatrixRow, import { getPlannedMatrix } from './approvalSubmission.service';
getPlannedMatrix, import { resolveApproverOrgInfo } from './approverOrgInfo';
resolveApproverOrgInfo, import { useMatrixEditor } from './useMatrixEditor';
useMatrixEditor
} from '../../shared/approval';
type MatrixEditor = ReturnType<typeof useMatrixEditor>; type MatrixEditor = ReturnType<typeof useMatrixEditor>;
export interface ISkudRoute { export interface IPhasedRoute {
/** Согласующие — редактируемы заявителем в новой заявке / черновике / доработке. */ /** Согласующие — редактируемы заявителем в новой заявке / черновике / доработке. */
approvers: MatrixEditor; approvers: MatrixEditor;
/** Исполнители по маршруту (фаза «Исполнение») — только чтение. */ /** Исполнители по маршруту (фаза «Исполнение») — только чтение. */
@@ -23,6 +21,7 @@ interface IRouteSource {
approval: ApprovalListService; approval: ApprovalListService;
/** Заявка уже записана — маршрут читаем из её строк, а не планируем заново. */ /** Заявка уже записана — маршрут читаем из её строк, а не планируем заново. */
requestId: number | undefined; requestId: number | undefined;
/** E-mail сотрудника, по которому резолвится планируемый маршрут. */
employeeEmail: string; employeeEmail: string;
requestTypeId: number | undefined; requestTypeId: number | undefined;
/** Можно ли править согласующих. */ /** Можно ли править согласующих. */
@@ -31,15 +30,18 @@ interface IRouteSource {
reloadSeq?: number; reloadSeq?: number;
} }
const byPhase = (execution: boolean) => (rows: IMatrixRow[]): IMatrixRow[] => const byPhase =
rows.filter((r) => (r.phase === APPROVAL_PHASE.execution) === execution); (execution: boolean): ((rows: IMatrixRow[]) => IMatrixRow[]) =>
(rows: IMatrixRow[]): IMatrixRow[] =>
rows.filter((r) => (r.phase === APPROVAL_PHASE.execution) === execution);
/** /**
* Маршрут объяснительной по двум фазам. Без отдельного запроса фазы * Маршрут заявки по двум фазам: согласование + исполнение. Без отдельного
* 'Execution' getPlannedMatrix исполнителей не резолвит — шаг кадров из * запроса фазы 'Execution' getPlannedMatrix исполнителей не резолвит — шаг
* маршрута «пропадал» бы. Паттерн тот же, что в Referral. * исполнения из маршрута «пропадал» бы. Маршрут настраивают в админке
* approval-route, форма его только получает.
*/ */
export function useSkudRoute(source: IRouteSource): ISkudRoute { export function usePhasedRoute(source: IRouteSource): IPhasedRoute {
const { context, approval, requestId, employeeEmail, requestTypeId, editable, reloadSeq = 0 } = source; const { context, approval, requestId, employeeEmail, requestTypeId, editable, reloadSeq = 0 } = source;
const canLoad = !!requestId || (!!employeeEmail && !!requestTypeId); const canLoad = !!requestId || (!!employeeEmail && !!requestTypeId);
const reloadKey = `${employeeEmail}-${requestId ?? 'new'}-${reloadSeq}`; const reloadKey = `${employeeEmail}-${requestId ?? 'new'}-${reloadSeq}`;