Добавить фазу исполнения в форму «Реферальная рекомендация»
Форма запрашивала маршрут только для фазы '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-selection`](../src/webparts/requestPortal/forms/RecruitingSelection/README.md) | Заявка на подбор | Рекрутинг | нет | нет | нет | много полей (~42), поля-люди (в т.ч. мульти) | ✅ |
|
||||||
| [`recruiting-hire`](../src/webparts/requestPortal/forms/RecruitingHire/README.md) | Заявка на прием | Рекрутинг | да | да | да | маска телефона, деньги, условное «взамен кого», источник для орг. места | ✅ |
|
| [`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-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 (согласовано/сразу без согласования — рабочий бакет, отклонено — отдельный бакет) | ✅ |
|
| [`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» резолвится по должности из самой заявки; да/нет-поля (парковка, СКУД), сумма обучения | ✅ |
|
| [`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».
|
- Тип заявки в approval-route API (`/api/request-types`): `pageUrl` «recruiting-referral».
|
||||||
- Фазы: только согласование. Фазы исполнения нет — после согласования заявка
|
- Фазы: согласование + исполнение. Исполнитель в маршруте — фиксированная
|
||||||
получает статус «Согласовано».
|
роль (не выбирается заявителем), которая назначает **реального** исполнителя
|
||||||
- **Матрица редактируема заявителем** при новой заявке / черновике / возврате:
|
через переназначение (флаг `AllowReassignToOtherApprovers` на шаге в
|
||||||
`useMatrixEditor(editable = !readOnly)`.
|
админке approval-route) — см. обсуждение в истории репо. Обе фазы
|
||||||
|
подгружаются отдельными запросами `getPlannedMatrix(..., 'Approval' | 'Execution')`
|
||||||
|
и рендерятся как два отдельных блока `<ApprovalMatrix>` (как в RecruitingHire).
|
||||||
|
- **Матрица согласования редактируема заявителем** при новой заявке / черновике
|
||||||
|
/ возврате: `useMatrixEditor(editable = !readOnly)`. **Матрица исполнения —
|
||||||
|
только чтение** (фиксируется маршрутом, не заявителем).
|
||||||
|
|
||||||
## Особое поведение
|
## Особое поведение
|
||||||
- **Кандидат — не сотрудник компании.** Его Ф.И.О., телефон, email и город —
|
- **Кандидат — не сотрудник компании.** Его Ф.И.О., телефон, email и город —
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ import {
|
|||||||
isEditableStatus,
|
isEditableStatus,
|
||||||
IApproverToWrite,
|
IApproverToWrite,
|
||||||
unassignedStepMessage,
|
unassignedStepMessage,
|
||||||
resolveWriteListId
|
resolveWriteListId,
|
||||||
|
APPROVAL_PHASE
|
||||||
} from '../../shared/approval';
|
} from '../../shared/approval';
|
||||||
import { IRequestFormProps } from '../../models/types';
|
import { IRequestFormProps } from '../../models/types';
|
||||||
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
||||||
@@ -138,7 +139,9 @@ const ReferralForm: 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, data.Referrer, approval, 'Approval'),
|
: getPlannedMatrix(context, requestTypeId as number, data.Referrer, approval, 'Approval'),
|
||||||
[context, approval, editId, data.Referrer, requestTypeId]
|
[context, approval, editId, data.Referrer, requestTypeId]
|
||||||
);
|
);
|
||||||
@@ -150,6 +153,27 @@ const ReferralForm: React.FC<IRequestFormProps> = ({
|
|||||||
(login) => approval.getJobTitle(login)
|
(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 = !!(
|
const sourceValid = !!(
|
||||||
data.AcquaintanceSource &&
|
data.AcquaintanceSource &&
|
||||||
@@ -217,6 +241,7 @@ const ReferralForm: React.FC<IRequestFormProps> = ({
|
|||||||
rtId,
|
rtId,
|
||||||
data,
|
data,
|
||||||
toApprovers(matrix.rows),
|
toApprovers(matrix.rows),
|
||||||
|
toApprovers(executors.rows),
|
||||||
status,
|
status,
|
||||||
writeListId
|
writeListId
|
||||||
);
|
);
|
||||||
@@ -253,6 +278,11 @@ const ReferralForm: 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;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -303,6 +333,16 @@ const ReferralForm: React.FC<IRequestFormProps> = ({
|
|||||||
|
|
||||||
<ApprovalMatrix editor={matrix} search={(q) => approval.searchUsers(q)} />
|
<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
|
<SubmitActions
|
||||||
submit={submit}
|
submit={submit}
|
||||||
isValid={isValid}
|
isValid={isValid}
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import {
|
|||||||
ApprovalMatrix,
|
ApprovalMatrix,
|
||||||
useMatrixEditor,
|
useMatrixEditor,
|
||||||
notifyActiveStep,
|
notifyActiveStep,
|
||||||
useRequestTypeId
|
useRequestTypeId,
|
||||||
|
APPROVAL_PHASE
|
||||||
} from '../../shared/approval';
|
} from '../../shared/approval';
|
||||||
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
||||||
import {
|
import {
|
||||||
@@ -59,13 +60,31 @@ const ReferralView: React.FC<IReferralViewProps> = ({ context, itemId, formKey,
|
|||||||
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), разбитая по
|
||||||
|
// фазам — иначе шаг исполнения (реальный исполнитель, назначаемый по
|
||||||
|
// маршруту) смешивался бы с согласующими в одной таблице. decisionSeq в
|
||||||
// reloadKey: после решения строки на сервере меняются — перечитываем матрицу.
|
// 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)
|
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(() => {
|
useEffect(() => {
|
||||||
let alive = true;
|
let alive = true;
|
||||||
@@ -144,6 +163,16 @@ const ReferralView: React.FC<IReferralViewProps> = ({ context, itemId, formKey,
|
|||||||
</FormCard>
|
</FormCard>
|
||||||
|
|
||||||
<ApprovalMatrix editor={approvers} search={(q) => approval.searchUsers(q)} />
|
<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>
|
</div>
|
||||||
|
|
||||||
{/* Панель решения — ВНЕ печати и FormModeProvider (комментарий редактируемый). */}
|
{/* Панель решения — ВНЕ печати и FormModeProvider (комментарий редактируемый). */}
|
||||||
|
|||||||
@@ -60,7 +60,9 @@ export type ISaveResult = ISaveItemResult;
|
|||||||
/**
|
/**
|
||||||
* Сохранить реферальную рекомендацию: содержимое + маршрут согласования.
|
* Сохранить реферальную рекомендацию: содержимое + маршрут согласования.
|
||||||
* existingId задан → обновляем ту же запись (черновик/доработка), иначе создаём новую.
|
* existingId задан → обновляем ту же запись (черновик/доработка), иначе создаём новую.
|
||||||
* Фазы исполнения нет — writeRoute вызывается с пустым списком исполнителей.
|
* После согласования — фаза исполнения: реальный исполнитель, назначаемый по
|
||||||
|
* маршруту (не заявителем). executors может быть пустым — тогда после
|
||||||
|
* согласования заявка сразу станет «Согласовано» (как у форм без исполнения).
|
||||||
* Возвращает Id заявки (+ код при создании).
|
* Возвращает Id заявки (+ код при создании).
|
||||||
*
|
*
|
||||||
* listId — партиция ОБНОВЛЯЕМОЙ заявки (см. loadReferralRequest). Имеет смысл
|
* listId — партиция ОБНОВЛЯЕМОЙ заявки (см. loadReferralRequest). Имеет смысл
|
||||||
@@ -73,6 +75,7 @@ export async function saveReferralRequest(
|
|||||||
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> {
|
||||||
@@ -80,6 +83,6 @@ export async function saveReferralRequest(
|
|||||||
const item = buildItem(data, status, referrer.Id);
|
const item = buildItem(data, status, referrer.Id);
|
||||||
|
|
||||||
const result = await saveRequestItem(FORM_ID, existingId, listId, requestTypeId, item);
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user