feat: добавление согласующих и исполнителей в поданные заявки на подбор и приём
Алтынай Бултурукова (право canEditRoute) просила добавлять участников в уже поданный маршрут. Новый согласующий или исполнитель встаёт в конец своего этапа со статусом «Ждёт очереди» (дальше его можно поднять стрелками); согласующего — пока идёт согласование, исполнителя — пока не завершено исполнение, без дублей среди ещё не решивших. Заявка «трогается», чтобы бэкенд выдал новому участнику права на элемент данных. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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` |
|
||||
|
||||
@@ -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<IRecruitingHireViewProps> = ({ 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<IRecruitingHireViewProps> = ({ 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 (
|
||||
<div>
|
||||
<div className={`${styles.toolbar} rp-no-print`}>
|
||||
@@ -410,17 +439,21 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ 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) && (
|
||||
<ApprovalMatrix
|
||||
editor={executors}
|
||||
search={(q) => approval.searchUsers(q)}
|
||||
title="Исполнение"
|
||||
roleLabel="Исполнитель"
|
||||
emptyText="Исполнители не назначены."
|
||||
pickerPlaceholder="Добавить исполнителя: начните вводить ФИО…"
|
||||
canMoveRow={canRemoveApprover ? queuedStepMoveRule(executors.rows) : undefined}
|
||||
onMoveRow={handleMoveStep}
|
||||
onAddRow={canAddExecutor ? handleAddExecutor : undefined}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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<IRecruitingSelectionViewProps> = ({ 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<IRecruitingSelectionViewProps> = ({ 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) && (
|
||||
<ApprovalMatrix
|
||||
editor={executors}
|
||||
search={(q) => approval.searchUsers(q)}
|
||||
title="Исполнение"
|
||||
roleLabel="Исполнитель"
|
||||
emptyText="Исполнители не назначены."
|
||||
pickerPlaceholder="Добавить исполнителя: начните вводить ФИО…"
|
||||
onAddRow={canAddExecutor ? handleAddExecutor : undefined}
|
||||
canReplaceRow={canRemoveApprover ? isPendingExecutor : undefined}
|
||||
onReplaceRow={handleReplaceExecutor}
|
||||
canRemoveRow={
|
||||
|
||||
@@ -315,6 +315,12 @@ export class ApprovalListService {
|
||||
await execute();
|
||||
}
|
||||
|
||||
// Id пользователя сайта по email (ensureUser) — чтобы сравнить нового участника
|
||||
// с уже стоящими в маршруте (см. ApproverService.addStep).
|
||||
public async getUserIdByEmail(email: string): Promise<number> {
|
||||
return this.resolveUserId(email);
|
||||
}
|
||||
|
||||
// Добавляет одну строку согласования с заданным Order (для делегата).
|
||||
// delegatedById — Id пользователя, который выполнил переназначение (не строки
|
||||
// из маршрута — у них делегата нет). Он сохраняет право переназначить эту
|
||||
|
||||
@@ -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<number[]> {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Шаг фазы «Исполнение», который ещё можно заменить или удалить: ждёт очереди
|
||||
* или уже активен («На исполнении»). Выполненный шаг — решение человека, его
|
||||
|
||||
@@ -14,7 +14,8 @@ import { effectiveUserEmail } from '../impersonation';
|
||||
* Реферальная рекомендация) — каждая форма сама решает, передавать ли
|
||||
* canRemoveRow в ApprovalMatrix; здесь только сама проверка личности.
|
||||
* В поданных подборе и приёме это же право даёт менять местами ждущие шаги
|
||||
* (canMoveRow, правило — queuedStepMove.ts).
|
||||
* (canMoveRow, правило — queuedStepMove.ts) и добавлять согласующих/исполнителей
|
||||
* в конец их этапа (onAddRow, правило — routeStepAdd.ts).
|
||||
*/
|
||||
const ROUTE_EDITOR_EMAILS: ReadonlySet<string> = new Set([
|
||||
'a.bulturukova@tnsu.kz', // Алтынай Бултурукова
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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<IApprovalMatrixProps> = ({
|
||||
onReplaceRow,
|
||||
canMoveRow,
|
||||
onMoveRow,
|
||||
onAddRow,
|
||||
}) => {
|
||||
const { rows, loading, error, editable, blocks, retry } = editor;
|
||||
// Строка, для которой открыт выбор замены (ReplaceRowPanel); undefined — закрыт.
|
||||
@@ -368,9 +375,13 @@ export const ApprovalMatrix: React.FC<IApprovalMatrixProps> = ({
|
||||
onCancel={() => setReplacingKey(undefined)}
|
||||
/>
|
||||
)}
|
||||
{editable && !waiting && !loading && (
|
||||
{(editable || onAddRow) && !waiting && !loading && (
|
||||
<div className={styles.matrixPicker}>
|
||||
<UserPicker search={search} onPick={addApprover} placeholder={pickerPlaceholder} />
|
||||
<UserPicker
|
||||
search={search}
|
||||
onPick={editable || !onAddRow ? addApprover : onAddRow}
|
||||
placeholder={pickerPlaceholder}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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<boolean>(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;
|
||||
});
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user