Заявление реферальная программа #13

Merged
gorbenko.pavel merged 9 commits from referal-request into main 2026-08-21 12:27:34 +00:00
5 changed files with 89 additions and 12 deletions
Showing only changes of commit a0d2907cc0 - Show all commits
+1 -1
View File
@@ -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;
}