diff --git a/docs/permissions.md b/docs/permissions.md index d049d6a..24ef29f 100644 --- a/docs/permissions.md +++ b/docs/permissions.md @@ -19,6 +19,7 @@ | Удалять согласующих, которые ещё ждут очереди | Алтынай Бултурукова, Павел Горбенко | email в коде | Подбор, Приём, Реферальная рекомендация: и при составлении, и в поданной заявке | `shared/approval/routeEditRights.ts` → `canEditRoute` | | Удалять любых согласующих **при составлении** заявки | те же (`canEditRoute`) | email в коде | Все остальные формы с маршрутом согласования (отпуск, увольнение, 1С и т. д.); в поданной заявке — нет | `shared/approval/routeEditRights.ts` → `composeRemoveRowRule` | | Добавлять и удалять исполнителей **при составлении** заявки | те же (`canEditRoute`) | email в коде | Все формы, где в маршруте из админки есть фаза исполнения; в поданной заявке — нет | `shared/approval/routeEditRights.ts` → `canEditExecutors` | +| Добавлять участников **в поданную заявку**: согласующего — пока идёт согласование, исполнителя — пока не завершено исполнение; встаёт в конец своего этапа со статусом «Ждёт очереди» | те же (`canEditRoute`) | email в коде | **Только** Подбор и Приём | `canEditRoute` + `shared/approval/routeStepAdd.ts` (правило) + `RecruitingSelectionView.tsx` / `RecruitingHireView.tsx` | | Менять местами шаги **в поданной заявке** (и согласующих, и исполнителей): только соседние шаги одной фазы, оба «Ждут очереди»; активный и пройденные шаги не двигаются | те же (`canEditRoute`) | email в коде | **Только** Подбор и Приём | `canEditRoute` + `shared/approval/queuedStepMove.ts` (правило) + `RecruitingSelectionView.tsx` / `RecruitingHireView.tsx` | | Заменять и удалять исполнителей, **в том числе активного** («На исполнении») | Алтынай Бултурукова | тот же email (`canEditRoute`) | **Только** Заявка на подбор, матрица «Исполнение» | `canEditRoute` + `forms/RecruitingSelection/RecruitingSelectionView.tsx` | | То же в поданной заявке: заменять и удалять невыполненных исполнителей, **в том числе активного и себя** | Исполнители заявки, у которых свой шаг исполнения ещё не выполнен | роль в маршруте (фаза «Исполнение»), без списка email | **Только** Заявка в отдел 1С | `forms/Ticket1C/useExecutorEditing.ts` | diff --git a/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireView.tsx b/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireView.tsx index 7e9a6d6..fe7528b 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireView.tsx +++ b/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireView.tsx @@ -36,6 +36,7 @@ import { queuedStepMoveRule, useQueuedStepMove } from '../../shared/approval'; +import { useRouteStepAdd } from '../../shared/approval/useRouteStepAdd'; import { navigate } from '../../shared/routing'; import { effectiveUserEmail } from '../../shared/impersonation'; import { getAttachments } from '../../shared/attachments/attachments.service'; @@ -280,6 +281,43 @@ const RecruitingHireView: React.FC = ({ context, itemI setRouteError ); + // Выше ранних return (загрузка/ошибка): зовётся и из колбэков хуков ниже. + const notify = (): void => { + if (!requestTypeId) return; + notifyActiveStep(context, approval, itemId, { + requestTypeId, + dataListName: listId ? dataListTitleForListId(FORM_ID, listId) : dataListTitle(FORM_ID), + approvalListName: listId ? approvalListTitleForListId(FORM_ID, listId) : approvalListTitle(FORM_ID), + }).catch(() => undefined); + }; + + // Добавить согласующего/исполнителя в поданную заявку — то же право + // routeEditRights.ts; правило «в конец этапа, пока он не завершён» — routeStepAdd.ts. + // Новый участник обычно ждёт очереди; если стал активным — сообщаем ему. + const handleStepAdded = (activatedRowIds: number[]): void => { + setDecisionSeq((n) => n + 1); + if (activatedRowIds.length > 0) notify(); + }; + const handleAddApprover = useRouteStepAdd( + approverService, + itemId, + APPROVAL_PHASE.approval, + handleStepAdded, + setRouteError + ); + const handleAddExecutor = useRouteStepAdd( + approverService, + itemId, + APPROVAL_PHASE.execution, + handleStepAdded, + setRouteError + ); + // По статусу заявки: согласующих — пока идёт согласование, исполнителей — + // пока не завершено исполнение. Сервер перепроверит по строкам (routeStepAdd.ts). + const canAddApprover = canRemoveApprover && status === REQUEST_STATUS.onApproval; + const canAddExecutor = + canRemoveApprover && (status === REQUEST_STATUS.onApproval || status === REQUEST_STATUS.execution); + useEffect(() => { let alive = true; setLoading(true); @@ -321,15 +359,6 @@ const RecruitingHireView: React.FC = ({ context, itemI const sectionProps = { data, set, context, payrollEditable, hireDateEditable }; - const notify = (): void => { - if (!requestTypeId) return; - notifyActiveStep(context, approval, itemId, { - requestTypeId, - dataListName: listId ? dataListTitleForListId(FORM_ID, listId) : dataListTitle(FORM_ID), - approvalListName: listId ? approvalListTitleForListId(FORM_ID, listId) : approvalListTitle(FORM_ID), - }).catch(() => undefined); - }; - return (
@@ -410,17 +439,21 @@ const RecruitingHireView: React.FC = ({ context, itemI onRemoveRow={handleRemoveApprover} canMoveRow={canRemoveApprover ? queuedStepMoveRule(approvers.rows) : undefined} onMoveRow={handleMoveStep} + onAddRow={canAddApprover ? handleAddApprover : undefined} /> - {!executors.loading && executors.rows.length > 0 && ( + {/* Без исполнителей блок показываем, только если их можно добавить. */} + {!executors.loading && (executors.rows.length > 0 || canAddExecutor) && ( approval.searchUsers(q)} title="Исполнение" roleLabel="Исполнитель" emptyText="Исполнители не назначены." + pickerPlaceholder="Добавить исполнителя: начните вводить ФИО…" canMoveRow={canRemoveApprover ? queuedStepMoveRule(executors.rows) : undefined} onMoveRow={handleMoveStep} + onAddRow={canAddExecutor ? handleAddExecutor : undefined} /> )}
diff --git a/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionView.tsx b/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionView.tsx index b151f62..114218e 100644 --- a/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionView.tsx +++ b/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionView.tsx @@ -35,6 +35,7 @@ import { queuedStepMoveRule, useQueuedStepMove } from '../../shared/approval'; +import { useRouteStepAdd } from '../../shared/approval/useRouteStepAdd'; import { getAttachments } from '../../shared/attachments/attachments.service'; import { useAttachmentEditor } from '../../shared/attachments/useAttachmentEditor'; import { EditableAttachments } from '../../shared/attachments/EditableAttachments'; @@ -267,6 +268,33 @@ const RecruitingSelectionView: React.FC = ({ cont setRouteError ); + // Добавить согласующего/исполнителя в поданную заявку — то же право + // routeEditRights.ts; правило «в конец этапа, пока он не завершён» — routeStepAdd.ts. + // Новый участник обычно ждёт очереди; если стал активным — сообщаем ему. + const handleStepAdded = (activatedRowIds: number[]): void => { + setDecisionSeq((n) => n + 1); + activatedRowIds.forEach((id) => notify(id)); + }; + const handleAddApprover = useRouteStepAdd( + approverService, + itemId, + APPROVAL_PHASE.approval, + handleStepAdded, + setRouteError + ); + const handleAddExecutor = useRouteStepAdd( + approverService, + itemId, + APPROVAL_PHASE.execution, + handleStepAdded, + setRouteError + ); + // По статусу заявки: согласующих — пока идёт согласование, исполнителей — + // пока не завершено исполнение. Сервер перепроверит по строкам (routeStepAdd.ts). + const canAddApprover = canRemoveApprover && status === REQUEST_STATUS.onApproval; + const canAddExecutor = + canRemoveApprover && (status === REQUEST_STATUS.onApproval || status === REQUEST_STATUS.execution); + useEffect(() => { let alive = true; setLoading(true); @@ -371,15 +399,19 @@ const RecruitingSelectionView: React.FC = ({ cont onRemoveRow={handleRemoveApprover} canMoveRow={canRemoveApprover ? queuedStepMoveRule(approvers.rows) : undefined} onMoveRow={handleMoveStep} + onAddRow={canAddApprover ? handleAddApprover : undefined} /> - {!executors.loading && executors.rows.length > 0 && ( + {/* Без исполнителей блок показываем, только если их можно добавить. */} + {!executors.loading && (executors.rows.length > 0 || canAddExecutor) && ( approval.searchUsers(q)} title="Исполнение" roleLabel="Исполнитель" emptyText="Исполнители не назначены." + pickerPlaceholder="Добавить исполнителя: начните вводить ФИО…" + onAddRow={canAddExecutor ? handleAddExecutor : undefined} canReplaceRow={canRemoveApprover ? isPendingExecutor : undefined} onReplaceRow={handleReplaceExecutor} canRemoveRow={ diff --git a/src/webparts/requestPortal/shared/approval/approvalList.service.ts b/src/webparts/requestPortal/shared/approval/approvalList.service.ts index ace1745..b2df7b4 100644 --- a/src/webparts/requestPortal/shared/approval/approvalList.service.ts +++ b/src/webparts/requestPortal/shared/approval/approvalList.service.ts @@ -315,6 +315,12 @@ export class ApprovalListService { await execute(); } + // Id пользователя сайта по email (ensureUser) — чтобы сравнить нового участника + // с уже стоящими в маршруте (см. ApproverService.addStep). + public async getUserIdByEmail(email: string): Promise { + return this.resolveUserId(email); + } + // Добавляет одну строку согласования с заданным Order (для делегата). // delegatedById — Id пользователя, который выполнил переназначение (не строки // из маршрута — у них делегата нет). Он сохраняет право переназначить эту diff --git a/src/webparts/requestPortal/shared/approval/approverDecision.service.ts b/src/webparts/requestPortal/shared/approval/approverDecision.service.ts index f8d5d94..2a19b3f 100644 --- a/src/webparts/requestPortal/shared/approval/approverDecision.service.ts +++ b/src/webparts/requestPortal/shared/approval/approverDecision.service.ts @@ -9,6 +9,11 @@ import { IApprovalRow } from './models/IApprovalRow'; import { ISiteUser } from './models/ISiteUser'; import { emitRequestsChanged } from '../requestEvents'; import { planQueuedStepMove, StepMoveDirection } from './queuedStepMove'; +import { planStepAdd } from './routeStepAdd'; + +function isActiveStatus(status: string): boolean { + return status === REQUEST_STATUS.onApproval || status === REQUEST_STATUS.onExecution; +} // approve/return/reject — согласующий; execute — исполнитель отметил «Выполнено». export type ApproverDecision = 'approve' | 'return' | 'reject' | 'execute'; @@ -165,6 +170,45 @@ export class ApproverService { emitRequestsChanged(); } + /** + * Добавить участника в поданную заявку: согласующего или исполнителя — в + * КОНЕЦ его этапа со статусом «Ждёт очереди» (право — routeEditRights.ts, + * правило — routeStepAdd.ts). Строки перечитываются перед записью. + * + * Заявку «трогаем» (touchRequest): новый участник должен получить права на + * элемент данных, иначе откроет её с «Элемент не найден». + * + * Возвращает Id строк, ставших активными (обычно пусто — активный шаг уже + * есть); им нужно оповещение (отправляет вызывающая сторона). + */ + public async addStep(requestId: number, phase: string, user: ISiteUser): Promise { + const [rows, approverId] = await Promise.all([ + this.approval.getForRequest(requestId), + this.approval.getUserIdByEmail(user.email), + ]); + const plan = planStepAdd(rows, phase, approverId); + if (!plan.ok) throw new Error(plan.error); + + const position = await this.approval.getJobTitle(user.loginName).catch(() => ''); + await this.approval.addApproverRow( + requestId, + { email: user.email, position, allowReassign: false }, + plan.order, + REQUEST_STATUS.queued, + phase + ); + + // На случай, если активного шага почему-то не было: очередь сама подхватит + // нового участника. При обычном ходе заявки ничего не меняется. + const activeBefore = new Set(rows.filter((r) => isActiveStatus(r.status)).map((r) => r.id)); + await this.approval.activateNextQueued(requestId); + const rowsAfter = await this.approval.getForRequest(requestId); + + await this.touchRequest(requestId); + emitRequestsChanged(); + return rowsAfter.filter((r) => isActiveStatus(r.status) && !activeBefore.has(r.id)).map((r) => r.id); + } + /** * Шаг фазы «Исполнение», который ещё можно заменить или удалить: ждёт очереди * или уже активен («На исполнении»). Выполненный шаг — решение человека, его diff --git a/src/webparts/requestPortal/shared/approval/routeEditRights.ts b/src/webparts/requestPortal/shared/approval/routeEditRights.ts index 5ffb5bf..9dad845 100644 --- a/src/webparts/requestPortal/shared/approval/routeEditRights.ts +++ b/src/webparts/requestPortal/shared/approval/routeEditRights.ts @@ -14,7 +14,8 @@ import { effectiveUserEmail } from '../impersonation'; * Реферальная рекомендация) — каждая форма сама решает, передавать ли * canRemoveRow в ApprovalMatrix; здесь только сама проверка личности. * В поданных подборе и приёме это же право даёт менять местами ждущие шаги - * (canMoveRow, правило — queuedStepMove.ts). + * (canMoveRow, правило — queuedStepMove.ts) и добавлять согласующих/исполнителей + * в конец их этапа (onAddRow, правило — routeStepAdd.ts). */ const ROUTE_EDITOR_EMAILS: ReadonlySet = new Set([ 'a.bulturukova@tnsu.kz', // Алтынай Бултурукова diff --git a/src/webparts/requestPortal/shared/approval/routeStepAdd.test.ts b/src/webparts/requestPortal/shared/approval/routeStepAdd.test.ts new file mode 100644 index 0000000..a7b1d87 --- /dev/null +++ b/src/webparts/requestPortal/shared/approval/routeStepAdd.test.ts @@ -0,0 +1,71 @@ +import { IStepAddRow, planStepAdd, STEP_ADD_ERRORS } from './routeStepAdd'; + +const APPROVAL = 'Согласование'; +const EXECUTION = 'Исполнение'; +const QUEUED = 'Ждёт очереди'; +const ON_APPROVAL = 'На согласовании'; +const ON_EXECUTION = 'На исполнении'; +const APPROVED = 'Согласовано'; +const EXECUTED = 'Выполнено'; +const RETURNED = 'Возвращена на доработку'; + +function row(approverId: number, order: number, status: string, phase = APPROVAL): IStepAddRow { + return { approverId, order, status, phase }; +} + +describe('planStepAdd — согласующий', () => { + it('встаёт после последнего согласующего, перед исполнителями', () => { + const rows = [row(1, 1, APPROVED), row(2, 2, ON_APPROVAL), row(3, 3, QUEUED), row(9, 4, QUEUED, EXECUTION)]; + expect(planStepAdd(rows, APPROVAL, 5)).toEqual({ ok: true, order: 3.5 }); + }); + + it('без исполнителей — следующий номер', () => { + expect(planStepAdd([row(1, 1, ON_APPROVAL)], APPROVAL, 5)).toEqual({ ok: true, order: 2 }); + }); + + it('нельзя, если согласование пройдено', () => { + const rows = [row(1, 1, APPROVED), row(9, 2, ON_EXECUTION, EXECUTION)]; + expect(planStepAdd(rows, APPROVAL, 5)).toEqual({ ok: false, error: STEP_ADD_ERRORS.approvalDone }); + }); + + it('нельзя, если заявка возвращена на доработку', () => { + const rows = [row(1, 1, RETURNED), row(2, 2, QUEUED)]; + expect(planStepAdd(rows, APPROVAL, 5)).toEqual({ ok: false, error: STEP_ADD_ERRORS.stopped }); + }); + + it('дубль — только среди ещё не решивших; уже согласовавшего можно добавить повторно', () => { + const rows = [row(1, 1, APPROVED), row(2, 2, ON_APPROVAL)]; + expect(planStepAdd(rows, APPROVAL, 2)).toEqual({ ok: false, error: STEP_ADD_ERRORS.duplicateApprover }); + expect(planStepAdd(rows, APPROVAL, 1)).toEqual({ ok: true, order: 3 }); + }); +}); + +describe('planStepAdd — исполнитель', () => { + it('в конец маршрута, пока идёт согласование', () => { + const rows = [row(1, 1, ON_APPROVAL), row(9, 2, QUEUED, EXECUTION)]; + expect(planStepAdd(rows, EXECUTION, 5)).toEqual({ ok: true, order: 3 }); + }); + + it('можно и без исполнителей в маршруте, пока идёт согласование', () => { + expect(planStepAdd([row(1, 1, ON_APPROVAL)], EXECUTION, 5)).toEqual({ ok: true, order: 2 }); + }); + + it('можно во время исполнения', () => { + const rows = [row(1, 1, APPROVED), row(9, 2, ON_EXECUTION, EXECUTION)]; + expect(planStepAdd(rows, EXECUTION, 5)).toEqual({ ok: true, order: 3 }); + }); + + it('нельзя, если исполнение завершено или исполнителей не было и заявка согласована', () => { + const closed = [row(1, 1, APPROVED), row(9, 2, EXECUTED, EXECUTION)]; + expect(planStepAdd(closed, EXECUTION, 5)).toEqual({ ok: false, error: STEP_ADD_ERRORS.executionDone }); + expect(planStepAdd([row(1, 1, APPROVED)], EXECUTION, 5)).toEqual({ + ok: false, + error: STEP_ADD_ERRORS.executionDone, + }); + }); + + it('дубль среди ещё не выполнивших', () => { + const rows = [row(1, 1, ON_APPROVAL), row(9, 2, QUEUED, EXECUTION)]; + expect(planStepAdd(rows, EXECUTION, 9)).toEqual({ ok: false, error: STEP_ADD_ERRORS.duplicateExecutor }); + }); +}); diff --git a/src/webparts/requestPortal/shared/approval/routeStepAdd.ts b/src/webparts/requestPortal/shared/approval/routeStepAdd.ts new file mode 100644 index 0000000..8618093 --- /dev/null +++ b/src/webparts/requestPortal/shared/approval/routeStepAdd.ts @@ -0,0 +1,86 @@ +import { APPROVAL_PHASE, REQUEST_STATUS } from './models/statuses'; + +/** + * Добавление участника в маршрут УЖЕ ПОДАННОЙ заявки (право — routeEditRights.ts). + * Чистая функция без SharePoint: решает, можно ли добавить, и с каким StepOrder. + * Выполняет ApproverService.addStep. + * + * Правило (согласовано с владельцем): новый участник встаёт в КОНЕЦ своего этапа + * со статусом «Ждёт очереди», дальше его можно поднять стрелками + * (queuedStepMove.ts). Активный шаг не трогается. + */ + +/** То, что нужно о строке маршрута; подходит IApprovalRow. */ +export interface IStepAddRow { + status: string; + order: number; + phase?: string; + approverId: number; +} + +export type StepAddPlan = { ok: true; order: number } | { ok: false; error: string }; + +export const STEP_ADD_ERRORS = { + stopped: 'Заявка отклонена или возвращена на доработку — маршрут сейчас не меняется.', + approvalDone: 'Согласование уже пройдено — добавить согласующего нельзя.', + executionDone: 'Исполнение уже завершено — добавить исполнителя нельзя.', + duplicateApprover: 'Этот сотрудник уже есть среди согласующих, которые ещё не решили.', + duplicateExecutor: 'Этот сотрудник уже есть среди исполнителей, которые ещё не выполнили.', +} as const; + +// Пустая фаза у старых строк — «Согласование» (см. getForRequest). +function isExecution(row: IStepAddRow): boolean { + return row.phase === APPROVAL_PHASE.execution; +} + +function maxOrder(rows: IStepAddRow[]): number { + return rows.reduce((max, r) => Math.max(max, r.order), 0); +} + +/** Можно ли добавить человека approverId в этап phase, и с каким StepOrder. */ +export function planStepAdd(rows: IStepAddRow[], phase: string, approverId: number): StepAddPlan { + const approvalRows = rows.filter((r) => !isExecution(r)); + const execRows = rows.filter(isExecution); + + const stopped = approvalRows.some( + (r) => r.status === REQUEST_STATUS.rejected || r.status === REQUEST_STATUS.returned + ); + if (stopped) return { ok: false, error: STEP_ADD_ERRORS.stopped }; + + const approvalDone = approvalRows.length === 0 || approvalRows.every((r) => r.status === REQUEST_STATUS.approved); + + if (phase === APPROVAL_PHASE.execution) { + // Согласование пройдено и исполнять уже нечего — заявка в финальном статусе + // («Согласовано» без исполнителей или «Закрыта»): переоткрывать её так нельзя. + const executionDone = execRows.every((r) => r.status === REQUEST_STATUS.executed); + if (approvalDone && executionDone) return { ok: false, error: STEP_ADD_ERRORS.executionDone }; + + const duplicate = execRows.some( + (r) => + r.approverId === approverId && + (r.status === REQUEST_STATUS.queued || r.status === REQUEST_STATUS.onExecution) + ); + if (duplicate) return { ok: false, error: STEP_ADD_ERRORS.duplicateExecutor }; + + // Исполнители идут после всех строк маршрута. + return { ok: true, order: maxOrder(rows) + 1 }; + } + + if (approvalDone) return { ok: false, error: STEP_ADD_ERRORS.approvalDone }; + + // Тот, кто уже согласовал, может понадобиться повторно — дублем считаем только ещё не решивших. + const duplicate = approvalRows.some( + (r) => + r.approverId === approverId && + (r.status === REQUEST_STATUS.queued || r.status === REQUEST_STATUS.onApproval) + ); + if (duplicate) return { ok: false, error: STEP_ADD_ERRORS.duplicateApprover }; + + // После последнего согласующего, но перед первым исполнителем — дробным + // order, как у делегатов, чтобы не перенумеровывать исполнителей. Очередь + // активирует шаги в пределах фазы, так что это лишь порядок показа. + const lastApproval = maxOrder(approvalRows); + const firstExec = execRows.length > 0 ? Math.min(...execRows.map((r) => r.order)) : undefined; + const order = firstExec !== undefined && firstExec > lastApproval ? (lastApproval + firstExec) / 2 : lastApproval + 1; + return { ok: true, order }; +} diff --git a/src/webparts/requestPortal/shared/approval/ui/ApprovalMatrix.tsx b/src/webparts/requestPortal/shared/approval/ui/ApprovalMatrix.tsx index 7bc1510..2ad38ca 100644 --- a/src/webparts/requestPortal/shared/approval/ui/ApprovalMatrix.tsx +++ b/src/webparts/requestPortal/shared/approval/ui/ApprovalMatrix.tsx @@ -63,6 +63,12 @@ export interface IApprovalMatrixProps { canMoveRow?: (row: IMatrixRow, direction: StepMoveDirection) => boolean; /** Сдвинуть шаг на сервере (задаётся вместе с canMoveRow, см. useQueuedStepMove). */ onMoveRow?: (row: IMatrixRow, direction: StepMoveDirection) => void; + /** + * Добавить участника в УЖЕ ПОДАННУЮ заявку (с записью на сервер, см. + * useRouteStepAdd). Задан — под таблицей пикер, как в черновике, даже вне + * editable. В черновике не нужен: там пикер добавляет строку локально. + */ + onAddRow?: (user: ISiteUser) => void; } /** Таблица маршрута (согласование или исполнение) + (если editable) правка состава. */ @@ -81,6 +87,7 @@ export const ApprovalMatrix: React.FC = ({ onReplaceRow, canMoveRow, onMoveRow, + onAddRow, }) => { const { rows, loading, error, editable, blocks, retry } = editor; // Строка, для которой открыт выбор замены (ReplaceRowPanel); undefined — закрыт. @@ -368,9 +375,13 @@ export const ApprovalMatrix: React.FC = ({ onCancel={() => setReplacingKey(undefined)} /> )} - {editable && !waiting && !loading && ( + {(editable || onAddRow) && !waiting && !loading && (
- +
)}
diff --git a/src/webparts/requestPortal/shared/approval/useRouteStepAdd.ts b/src/webparts/requestPortal/shared/approval/useRouteStepAdd.ts new file mode 100644 index 0000000..7abc54d --- /dev/null +++ b/src/webparts/requestPortal/shared/approval/useRouteStepAdd.ts @@ -0,0 +1,35 @@ +import { useRef } from 'react'; +import { friendlyErrorMessage } from '../diagnostics'; +import { ApproverService } from './approverDecision.service'; +import { ISiteUser } from './models/ISiteUser'; + +/** + * Добавление участника в поданную заявку из ApprovalMatrix (onAddRow) — общая + * обвязка форм просмотра (RecruitingHireView, RecruitingSelectionView), как + * useQueuedStepMove. Пока одно добавление пишется, следующие выборы + * игнорируются: двойной выбор в пикере добавил бы человека дважды. + * + * onAdded получает Id строк, ставших активными, — им нужно оповещение. + */ +export function useRouteStepAdd( + service: ApproverService, + requestId: number, + phase: string, + onAdded: (activatedRowIds: number[]) => void, + onError: (message: string | undefined) => void +): (user: ISiteUser) => void { + const isAddingRef = useRef(false); + + return (user) => { + if (isAddingRef.current) return; + isAddingRef.current = true; + onError(undefined); + service + .addStep(requestId, phase, user) + .then(onAdded) + .catch((e) => onError(friendlyErrorMessage(e))) + .finally(() => { + isAddingRef.current = false; + }); + }; +}