This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user