Добавить фазу исполнения в форму «Реферальная рекомендация»
Форма запрашивала маршрут только для фазы 'Approval' — исполнитель (фиксированная роль, назначающая реального через переназначение) никогда не подгружался, и на форме создания был виден только пустой список согласования. Добавлены второй loadExecutors/useMatrixEditor (read-only, как у RecruitingHire) и второй блок ApprovalMatrix «Исполнение» — в форме подачи и в режиме согласующего. submit.ts теперь пишет executors в writeRoute вместо пустого массива. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -42,7 +42,7 @@
|
||||
| --- | --- | --- | :---: | :---: | :---: | --- | :---: |
|
||||
| [`recruiting-selection`](../src/webparts/requestPortal/forms/RecruitingSelection/README.md) | Заявка на подбор | Рекрутинг | нет | нет | нет | много полей (~42), поля-люди (в т.ч. мульти) | ✅ |
|
||||
| [`recruiting-hire`](../src/webparts/requestPortal/forms/RecruitingHire/README.md) | Заявка на прием | Рекрутинг | да | да | да | маска телефона, деньги, условное «взамен кого», источник для орг. места | ✅ |
|
||||
| [`recruiting-referral`](../src/webparts/requestPortal/forms/Referral/README.md) | Реферальная рекомендация | Рекрутинг | да | нет | да | кандидат — внешний (текстовые поля, не People); маршрут по рекомендателю; условное уточнение «откуда знаете» | ✅ |
|
||||
| [`recruiting-referral`](../src/webparts/requestPortal/forms/Referral/README.md) | Реферальная рекомендация | Рекрутинг | да | да | да | кандидат — внешний (текстовые поля, не People); маршрут по рекомендателю; исполнитель переназначает на реального; условное уточнение «откуда знаете» | ✅ |
|
||||
| [`it-workplace-setup`](../src/webparts/requestPortal/forms/WorkplaceSetup/README.md) | Организация рабочего места | IT | да | да | да | префилл из «прием», поля ПО/доступы правит руководитель | ✅ |
|
||||
| [`it-1c-ticket`](../src/webparts/requestPortal/forms/Ticket1C/README.md) | Заявка в отдел 1С | IT | да | нет | да | контакты предзаполняются из Graph (/me); вместо фазы исполнения — карточка в Microsoft Planner (согласовано/сразу без согласования — рабочий бакет, отклонено — отдельный бакет) | ✅ |
|
||||
| [`clearance-form`](../src/webparts/requestPortal/forms/Clearance/README.md) | Обходной лист | Отдел кадров | да | нет | да | подаётся HR за увольняемого (People-picker); условие «IsManager» резолвится по должности из самой заявки; да/нет-поля (парковка, СКУД), сумма обучения | ✅ |
|
||||
|
||||
@@ -10,10 +10,15 @@
|
||||
|
||||
## Маршрут
|
||||
- Тип заявки в approval-route API (`/api/request-types`): `pageUrl` «recruiting-referral».
|
||||
- Фазы: только согласование. Фазы исполнения нет — после согласования заявка
|
||||
получает статус «Согласовано».
|
||||
- **Матрица редактируема заявителем** при новой заявке / черновике / возврате:
|
||||
`useMatrixEditor(editable = !readOnly)`.
|
||||
- Фазы: согласование + исполнение. Исполнитель в маршруте — фиксированная
|
||||
роль (не выбирается заявителем), которая назначает **реального** исполнителя
|
||||
через переназначение (флаг `AllowReassignToOtherApprovers` на шаге в
|
||||
админке approval-route) — см. обсуждение в истории репо. Обе фазы
|
||||
подгружаются отдельными запросами `getPlannedMatrix(..., 'Approval' | 'Execution')`
|
||||
и рендерятся как два отдельных блока `<ApprovalMatrix>` (как в RecruitingHire).
|
||||
- **Матрица согласования редактируема заявителем** при новой заявке / черновике
|
||||
/ возврате: `useMatrixEditor(editable = !readOnly)`. **Матрица исполнения —
|
||||
только чтение** (фиксируется маршрутом, не заявителем).
|
||||
|
||||
## Особое поведение
|
||||
- **Кандидат — не сотрудник компании.** Его Ф.И.О., телефон, email и город —
|
||||
|
||||
@@ -22,7 +22,8 @@ import {
|
||||
isEditableStatus,
|
||||
IApproverToWrite,
|
||||
unassignedStepMessage,
|
||||
resolveWriteListId
|
||||
resolveWriteListId,
|
||||
APPROVAL_PHASE
|
||||
} from '../../shared/approval';
|
||||
import { IRequestFormProps } from '../../models/types';
|
||||
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
||||
@@ -138,7 +139,9 @@ const ReferralForm: 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, data.Referrer, approval, 'Approval'),
|
||||
[context, approval, editId, data.Referrer, requestTypeId]
|
||||
);
|
||||
@@ -150,6 +153,27 @@ const ReferralForm: React.FC<IRequestFormProps> = ({
|
||||
(login) => approval.getJobTitle(login)
|
||||
);
|
||||
|
||||
// Фаза исполнения: реальный исполнитель, назначаемый по маршруту (не
|
||||
// редактируется заявителем — только чтение, как в RecruitingHire/Clearance).
|
||||
// Именно этот шаг сейчас «пропадал» на форме: без явного запроса фазы
|
||||
// 'Execution' getPlannedMatrix её просто не резолвит.
|
||||
const loadExecutors = useCallback(
|
||||
() =>
|
||||
editId
|
||||
? approval
|
||||
.getMatrixForRequest(editId)
|
||||
.then((rows) => rows.filter((r) => r.phase === APPROVAL_PHASE.execution))
|
||||
: getPlannedMatrix(context, requestTypeId as number, data.Referrer, approval, 'Execution'),
|
||||
[context, approval, editId, data.Referrer, requestTypeId]
|
||||
);
|
||||
const executors = useMatrixEditor(
|
||||
loadExecutors,
|
||||
`${data.Referrer}-${editId ?? 'new'}`,
|
||||
false,
|
||||
!!editId || (!!data.Referrer && !!requestTypeId),
|
||||
(login) => approval.getJobTitle(login)
|
||||
);
|
||||
|
||||
// «Другое» в «откуда знаете» без уточнения — незаполненное поле по сути.
|
||||
const sourceValid = !!(
|
||||
data.AcquaintanceSource &&
|
||||
@@ -217,6 +241,7 @@ const ReferralForm: React.FC<IRequestFormProps> = ({
|
||||
rtId,
|
||||
data,
|
||||
toApprovers(matrix.rows),
|
||||
toApprovers(executors.rows),
|
||||
status,
|
||||
writeListId
|
||||
);
|
||||
@@ -253,6 +278,11 @@ const ReferralForm: 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;
|
||||
};
|
||||
|
||||
@@ -303,6 +333,16 @@ const ReferralForm: React.FC<IRequestFormProps> = ({
|
||||
|
||||
<ApprovalMatrix editor={matrix} search={(q) => approval.searchUsers(q)} />
|
||||
|
||||
{!executors.loading && executors.rows.length > 0 && (
|
||||
<ApprovalMatrix
|
||||
editor={executors}
|
||||
search={(q) => approval.searchUsers(q)}
|
||||
title="Исполнение (после согласования)"
|
||||
roleLabel="Исполнитель"
|
||||
emptyText="Исполнители не заданы по маршруту."
|
||||
/>
|
||||
)}
|
||||
|
||||
<SubmitActions
|
||||
submit={submit}
|
||||
isValid={isValid}
|
||||
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
ApprovalMatrix,
|
||||
useMatrixEditor,
|
||||
notifyActiveStep,
|
||||
useRequestTypeId
|
||||
useRequestTypeId,
|
||||
APPROVAL_PHASE
|
||||
} from '../../shared/approval';
|
||||
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
||||
import {
|
||||
@@ -59,13 +60,31 @@ const ReferralView: React.FC<IReferralViewProps> = ({ context, itemId, formKey,
|
||||
const dataListRef = listId || dataListTitle(FORM_ID);
|
||||
const approverService = useMemo(() => new ApproverService(approval, dataListRef), [approval, dataListRef]);
|
||||
|
||||
// Матрица маршрута из фактических строк заявки (read-only). decisionSeq в
|
||||
// Матрица маршрута из фактических строк заявки (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)
|
||||
);
|
||||
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;
|
||||
@@ -144,6 +163,16 @@ const ReferralView: React.FC<IReferralViewProps> = ({ context, itemId, formKey,
|
||||
</FormCard>
|
||||
|
||||
<ApprovalMatrix editor={approvers} search={(q) => approval.searchUsers(q)} />
|
||||
|
||||
{!executors.loading && executors.rows.length > 0 && (
|
||||
<ApprovalMatrix
|
||||
editor={executors}
|
||||
search={(q) => approval.searchUsers(q)}
|
||||
title="Исполнение"
|
||||
roleLabel="Исполнитель"
|
||||
emptyText="Исполнители не назначены."
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Панель решения — ВНЕ печати и FormModeProvider (комментарий редактируемый). */}
|
||||
|
||||
@@ -60,7 +60,9 @@ export type ISaveResult = ISaveItemResult;
|
||||
/**
|
||||
* Сохранить реферальную рекомендацию: содержимое + маршрут согласования.
|
||||
* existingId задан → обновляем ту же запись (черновик/доработка), иначе создаём новую.
|
||||
* Фазы исполнения нет — writeRoute вызывается с пустым списком исполнителей.
|
||||
* После согласования — фаза исполнения: реальный исполнитель, назначаемый по
|
||||
* маршруту (не заявителем). executors может быть пустым — тогда после
|
||||
* согласования заявка сразу станет «Согласовано» (как у форм без исполнения).
|
||||
* Возвращает Id заявки (+ код при создании).
|
||||
*
|
||||
* listId — партиция ОБНОВЛЯЕМОЙ заявки (см. loadReferralRequest). Имеет смысл
|
||||
@@ -73,6 +75,7 @@ export async function saveReferralRequest(
|
||||
requestTypeId: number,
|
||||
data: IFormData,
|
||||
approvers: IApproverToWrite[],
|
||||
executors: IApproverToWrite[],
|
||||
status: string,
|
||||
listId?: string
|
||||
): Promise<ISaveResult> {
|
||||
@@ -80,6 +83,6 @@ export async function saveReferralRequest(
|
||||
const item = buildItem(data, status, referrer.Id);
|
||||
|
||||
const result = await saveRequestItem(FORM_ID, existingId, listId, requestTypeId, item);
|
||||
await writeRoute(referralApprovalService(listId), result.id, approvers, [], status);
|
||||
await writeRoute(referralApprovalService(listId), result.id, approvers, executors, status);
|
||||
return result;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user