This commit is contained in:
@@ -28,7 +28,8 @@ import {
|
|||||||
IApproverToWrite,
|
IApproverToWrite,
|
||||||
unassignedStepMessage,
|
unassignedStepMessage,
|
||||||
resolveApproverOrgInfo,
|
resolveApproverOrgInfo,
|
||||||
canEditRoute
|
canEditRoute,
|
||||||
|
APPROVAL_PHASE
|
||||||
} from '../../shared/approval';
|
} from '../../shared/approval';
|
||||||
import {
|
import {
|
||||||
uploadAttachments,
|
uploadAttachments,
|
||||||
@@ -200,7 +201,9 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({
|
|||||||
const loadMatrix = useCallback(
|
const loadMatrix = useCallback(
|
||||||
() =>
|
() =>
|
||||||
editId
|
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),
|
: getPlannedMatrix(context, requestTypeId as number, userEmail, approval, 'Approval', null, fieldValues, projectId),
|
||||||
[context, userEmail, approval, editId, requestTypeId, fieldValues, projectId]
|
[context, userEmail, approval, editId, requestTypeId, fieldValues, projectId]
|
||||||
);
|
);
|
||||||
@@ -214,6 +217,25 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({
|
|||||||
resolveApproverOrgInfo(context)
|
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): маршрут
|
// ObjectProjectId обязателен (а не просто текст ObjectProject): маршрут
|
||||||
// согласования резолвится ПО ПРОЕКТУ (ByProject, см. projectId выше), и без
|
// согласования резолвится ПО ПРОЕКТУ (ByProject, см. projectId выше), и без
|
||||||
// выбранного из списка варианта (id) approval-route вернёт 404 — форма
|
// выбранного из списка варианта (id) approval-route вернёт 404 — форма
|
||||||
@@ -284,6 +306,7 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({
|
|||||||
rtId,
|
rtId,
|
||||||
data,
|
data,
|
||||||
toApprovers(matrix.rows),
|
toApprovers(matrix.rows),
|
||||||
|
toApprovers(executors.rows),
|
||||||
status,
|
status,
|
||||||
writeListId
|
writeListId
|
||||||
);
|
);
|
||||||
@@ -338,6 +361,11 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({
|
|||||||
(r) => `Шаг ${r.order} маршрута согласования («${r.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`
|
(r) => `Шаг ${r.order} маршрута согласования («${r.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`
|
||||||
);
|
);
|
||||||
if (unassignedError) return unassignedError;
|
if (unassignedError) return unassignedError;
|
||||||
|
const unassignedExecutorError = unassignedStepMessage(
|
||||||
|
executors.rows,
|
||||||
|
(r) => `Шаг ${r.order} исполнения («${r.position || 'без должности'}») без назначенного человека — выберите исполнителя или удалите шаг.`
|
||||||
|
);
|
||||||
|
if (unassignedExecutorError) return unassignedExecutorError;
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -407,6 +435,16 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({
|
|||||||
canRemoveRow={canRemoveApprover ? () => true : undefined}
|
canRemoveRow={canRemoveApprover ? () => true : undefined}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{!executors.loading && executors.rows.length > 0 && (
|
||||||
|
<ApprovalMatrix
|
||||||
|
editor={executors}
|
||||||
|
search={(q) => approval.searchUsers(q)}
|
||||||
|
title="Исполнение (после согласования)"
|
||||||
|
roleLabel="Исполнитель"
|
||||||
|
emptyText="Исполнители не заданы по маршруту."
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<SubmitActions
|
<SubmitActions
|
||||||
submit={submit}
|
submit={submit}
|
||||||
isValid={isValid}
|
isValid={isValid}
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import {
|
|||||||
resolveApproverOrgInfo,
|
resolveApproverOrgInfo,
|
||||||
canEditRoute,
|
canEditRoute,
|
||||||
REQUEST_STATUS,
|
REQUEST_STATUS,
|
||||||
IMatrixRow
|
IMatrixRow,
|
||||||
|
APPROVAL_PHASE
|
||||||
} from '../../shared/approval';
|
} from '../../shared/approval';
|
||||||
import { getAttachments, IAttachmentInfo } from '../../shared/attachments/attachments.service';
|
import { getAttachments, IAttachmentInfo } from '../../shared/attachments/attachments.service';
|
||||||
import { effectiveUserEmail } from '../../shared/impersonation';
|
import { effectiveUserEmail } from '../../shared/impersonation';
|
||||||
@@ -79,13 +80,30 @@ const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ cont
|
|||||||
const dataListRef = listId || dataListTitle(FORM_ID);
|
const dataListRef = listId || dataListTitle(FORM_ID);
|
||||||
const approverService = useMemo(() => new ApproverService(approval, dataListRef), [approval, dataListRef]);
|
const approverService = useMemo(() => new ApproverService(approval, dataListRef), [approval, dataListRef]);
|
||||||
|
|
||||||
// Матрица маршрута из фактических строк заявки (read-only). decisionSeq в
|
// Матрица маршрута из фактических строк заявки (read-only), разбитая по
|
||||||
// reloadKey: после решения строки на сервере меняются — перечитываем матрицу.
|
// фазам. decisionSeq в reloadKey: после решения строки на сервере меняются —
|
||||||
|
// перечитываем матрицу.
|
||||||
const [decisionSeq, setDecisionSeq] = useState<number>(0);
|
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) =>
|
const approvers = useMatrixEditor(loadApprovers, `a-${itemId}-${decisionSeq}`, false, true, (l) =>
|
||||||
approval.getJobTitle(l), resolveApproverOrgInfo(context)
|
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).
|
// Удалить ещё не решённый шаг маршрута (см. ApproverService.deleteQueuedStep).
|
||||||
const handleRemoveApprover = (row: IMatrixRow): void => {
|
const handleRemoveApprover = (row: IMatrixRow): void => {
|
||||||
@@ -187,6 +205,16 @@ const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ cont
|
|||||||
canRemoveRow={canRemoveApprover ? (row) => row.status === REQUEST_STATUS.queued : undefined}
|
canRemoveRow={canRemoveApprover ? (row) => row.status === REQUEST_STATUS.queued : undefined}
|
||||||
onRemoveRow={handleRemoveApprover}
|
onRemoveRow={handleRemoveApprover}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{!executors.loading && executors.rows.length > 0 && (
|
||||||
|
<ApprovalMatrix
|
||||||
|
editor={executors}
|
||||||
|
search={(q) => approval.searchUsers(q)}
|
||||||
|
title="Исполнение"
|
||||||
|
roleLabel="Исполнитель"
|
||||||
|
emptyText="Исполнители не назначены."
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Панель решения — ВНЕ печати и FormModeProvider (комментарий редактируемый). */}
|
{/* Панель решения — ВНЕ печати и FormModeProvider (комментарий редактируемый). */}
|
||||||
|
|||||||
@@ -86,10 +86,11 @@ async function applyPeople(item: Record<string, unknown>, data: IFormData): Prom
|
|||||||
export type ISaveResult = ISaveItemResult;
|
export type ISaveResult = ISaveItemResult;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Сохранить заявку «на подбор»: содержимое + маршрут согласования.
|
* Сохранить заявку «на подбор»: содержимое + маршрут (согласующие + исполнители).
|
||||||
* existingId задан → обновляем ту же запись (черновик/доработка), иначе создаём новую.
|
* existingId задан → обновляем ту же запись (черновик/доработка), иначе создаём новую.
|
||||||
* status = «Черновик» (сохранение) или «На согласовании» (отправка). Фазы исполнения
|
* status = «Черновик» (сохранение) или «На согласовании» (отправка).
|
||||||
* нет — writeRoute вызывается с пустым списком исполнителей. Возвращает Id (+ код при создании).
|
* executors может быть пустым — тогда после согласования заявка станет «Согласовано»
|
||||||
|
* (см. computeRequestStatus в approverDecision.service.ts). Возвращает Id (+ код при создании).
|
||||||
*
|
*
|
||||||
* listId — партиция, в которую фактически пишем: при редактировании — партиция
|
* listId — партиция, в которую фактически пишем: при редактировании — партиция
|
||||||
* ОБНОВЛЯЕМОЙ заявки (см. loadSelectionRequest), при создании — партиция,
|
* ОБНОВЛЯЕМОЙ заявки (см. loadSelectionRequest), при создании — партиция,
|
||||||
@@ -104,6 +105,7 @@ export async function saveSelectionRequest(
|
|||||||
requestTypeId: number,
|
requestTypeId: number,
|
||||||
data: IFormData,
|
data: IFormData,
|
||||||
approvers: IApproverToWrite[],
|
approvers: IApproverToWrite[],
|
||||||
|
executors: IApproverToWrite[],
|
||||||
status: string,
|
status: string,
|
||||||
listId?: string
|
listId?: string
|
||||||
): Promise<ISaveResult> {
|
): Promise<ISaveResult> {
|
||||||
@@ -112,6 +114,6 @@ export async function saveSelectionRequest(
|
|||||||
await applyPeople(item, data);
|
await applyPeople(item, data);
|
||||||
|
|
||||||
const result = await saveRequestItem(FORM_ID, existingId, listId, requestTypeId, item);
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user