feat: замена и удаление исполнителей в заявке на подбор
Начальник отдела подбора (routeEditRights) назначает исполнителей, но сама в исполнении не стоит. В матрице «Исполнение» заявки на подбор у шагов «Ждёт очереди» и «На исполнении» появились кнопки «Заменить» и «Удалить»: - замена меняет только человека в шаге, активный шаг сразу переходит к новому исполнителю, ему уходит оповещение; - при удалении активного очередь переходит к следующему исполнителю; - выполненные шаги не трогаются, последнего невыполненного исполнителя можно только заменить — иначе заявка закрылась бы без исполнения. Остальные формы не затронуты: кнопки включаются пропсами ApprovalMatrix (canReplaceRow/onReplaceRow) только в RecruitingSelectionView. notifyActiveStep получил необязательный rowId, чтобы на параллельном шаге письмо ушло именно новому исполнителю. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -140,6 +140,77 @@ export class ApproverService {
|
||||
emitRequestsChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Шаг фазы «Исполнение», который ещё можно заменить или удалить: ждёт очереди
|
||||
* или уже активен («На исполнении»). Выполненный шаг — решение человека, его
|
||||
* не трогаем (иначе можно «переоткрыть» закрытую заявку). Бросает с текстом
|
||||
* для пользователя, если шаг не подходит.
|
||||
*/
|
||||
private editableExecutionRow(rows: IApprovalRow[], rowId: number): IApprovalRow {
|
||||
const row = rows.filter((r) => r.id === rowId)[0];
|
||||
if (!row || row.phase !== APPROVAL_PHASE.execution) {
|
||||
throw new Error('Шаг не найден среди исполнителей — обновите заявку.');
|
||||
}
|
||||
if (row.status !== REQUEST_STATUS.queued && row.status !== REQUEST_STATUS.onExecution) {
|
||||
throw new Error('Шаг уже выполнен — заменить или удалить его нельзя.');
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
/**
|
||||
* Заменить исполнителя на шаге фазы «Исполнение» — в т.ч. активном. Шаг
|
||||
* остаётся тем же (порядок, статус), меняется только человек. Подключено
|
||||
* только в «Заявке на подбор» (см. RecruitingSelectionView), право — по
|
||||
* routeEditRights.ts.
|
||||
*
|
||||
* Возвращает, был ли шаг активным: тогда заявка сразу у нового исполнителя,
|
||||
* и ему нужно отправить оповещение (это делает вызывающая сторона).
|
||||
*/
|
||||
public async replaceExecutionStep(requestId: number, rowId: number, user: ISiteUser): Promise<boolean> {
|
||||
const rows = await this.approval.getForRequest(requestId);
|
||||
const row = this.editableExecutionRow(rows, rowId);
|
||||
const position = await this.approval.getJobTitle(user.loginName).catch(() => '');
|
||||
await this.approval.replaceApprover(rowId, { email: user.email, position });
|
||||
emitRequestsChanged();
|
||||
return row.status === REQUEST_STATUS.onExecution;
|
||||
}
|
||||
|
||||
/**
|
||||
* Удалить исполнителя с шага фазы «Исполнение» — в т.ч. активного. Если
|
||||
* удалён активный, очередь переходит к следующему исполнителю.
|
||||
*
|
||||
* Последнего невыполненного исполнителя удалить нельзя — только заменить: без
|
||||
* него все оставшиеся шаги окажутся выполненными (или их не останется вовсе),
|
||||
* и computeRequestStatus закрыл бы заявку, хотя её никто не исполнил.
|
||||
*
|
||||
* Возвращает Id строк, ставших активными после удаления, — им нужно
|
||||
* оповещение (отправляет вызывающая сторона).
|
||||
*/
|
||||
public async deleteExecutionStep(requestId: number, rowId: number): Promise<number[]> {
|
||||
const rows = await this.approval.getForRequest(requestId);
|
||||
this.editableExecutionRow(rows, rowId);
|
||||
|
||||
const othersPending = rows.filter(
|
||||
(r) =>
|
||||
r.id !== rowId &&
|
||||
r.phase === APPROVAL_PHASE.execution &&
|
||||
(r.status === REQUEST_STATUS.queued || r.status === REQUEST_STATUS.onExecution)
|
||||
);
|
||||
if (othersPending.length === 0) {
|
||||
throw new Error('Это последний исполнитель заявки — его можно только заменить.');
|
||||
}
|
||||
|
||||
const activeBefore = new Set(rows.filter((r) => r.status === REQUEST_STATUS.onExecution).map((r) => r.id));
|
||||
await this.approval.deleteRow(rowId);
|
||||
await this.approval.activateNextQueued(requestId);
|
||||
|
||||
const rowsAfter = await this.approval.getForRequest(requestId);
|
||||
emitRequestsChanged();
|
||||
return rowsAfter
|
||||
.filter((r) => r.status === REQUEST_STATUS.onExecution && !activeBefore.has(r.id))
|
||||
.map((r) => r.id);
|
||||
}
|
||||
|
||||
// Применяет решение: обновляет строку, продвигает очередь (на следующего
|
||||
// согласующего внутри фазы либо на первого исполнителя, если фаза согласования
|
||||
// только что завершилась целиком) и пересчитывает статус заявки.
|
||||
|
||||
Reference in New Issue
Block a user