fix: добавил блок исполнения в заявке на подбор
Tests / frontend-test (push) Successful in 1m11s

This commit is contained in:
Тимур Городецкий
2026-09-22 13:07:04 +05:00
parent 11e05c1786
commit f6dcf349e9
3 changed files with 78 additions and 10 deletions
@@ -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<IRequestFormProps> = ({
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<IRequestFormProps> = ({
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<IRequestFormProps> = ({
rtId,
data,
toApprovers(matrix.rows),
toApprovers(executors.rows),
status,
writeListId
);
@@ -338,6 +361,11 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({
(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<IRequestFormProps> = ({
canRemoveRow={canRemoveApprover ? () => true : undefined}
/>
{!executors.loading && executors.rows.length > 0 && (
<ApprovalMatrix
editor={executors}
search={(q) => approval.searchUsers(q)}
title="Исполнение (после согласования)"
roleLabel="Исполнитель"
emptyText="Исполнители не заданы по маршруту."
/>
)}
<SubmitActions
submit={submit}
isValid={isValid}
@@ -15,7 +15,8 @@ import {
resolveApproverOrgInfo,
canEditRoute,
REQUEST_STATUS,
IMatrixRow
IMatrixRow,
APPROVAL_PHASE
} from '../../shared/approval';
import { getAttachments, IAttachmentInfo } from '../../shared/attachments/attachments.service';
import { effectiveUserEmail } from '../../shared/impersonation';
@@ -79,13 +80,30 @@ const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ 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<number>(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<IRecruitingSelectionViewProps> = ({ cont
canRemoveRow={canRemoveApprover ? (row) => row.status === REQUEST_STATUS.queued : undefined}
onRemoveRow={handleRemoveApprover}
/>
{!executors.loading && executors.rows.length > 0 && (
<ApprovalMatrix
editor={executors}
search={(q) => approval.searchUsers(q)}
title="Исполнение"
roleLabel="Исполнитель"
emptyText="Исполнители не назначены."
/>
)}
</div>
{/* Панель решения — ВНЕ печати и FormModeProvider (комментарий редактируемый). */}
@@ -86,10 +86,11 @@ async function applyPeople(item: Record<string, unknown>, 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<ISaveResult> {
@@ -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;
}