feat: исполнители заявки 1С сами заменяют и удаляют друг друга
Tests / frontend-test (push) Successful in 2m0s
Tests / frontend-test (push) Successful in 2m0s
Отдел 1С передаёт заявки внутри себя: исполнитель с невыполненным шагом может в поданной заявке заменить или убрать невыполненного исполнителя, в т.ч. активного и себя. Выполненные шаги не трогаются, последнего невыполненного — только заменить. Механизм тот же, что в подборе, но право по роли в маршруте, а не по списку email. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+2
-1
@@ -20,6 +20,7 @@
|
||||
| Удалять любых согласующих **при составлении** заявки | те же (`canEditRoute`) | email в коде | Все остальные формы с маршрутом согласования (отпуск, увольнение, 1С и т. д.); в поданной заявке — нет | `shared/approval/routeEditRights.ts` → `composeRemoveRowRule` |
|
||||
| Добавлять и удалять исполнителей **при составлении** заявки | те же (`canEditRoute`) | email в коде | Все формы, где в маршруте из админки есть фаза исполнения; в поданной заявке — нет | `shared/approval/routeEditRights.ts` → `canEditExecutors` |
|
||||
| Заменять и удалять исполнителей, **в том числе активного** («На исполнении») | Алтынай Бултурукова | тот же email (`canEditRoute`) | **Только** Заявка на подбор, матрица «Исполнение» | `canEditRoute` + `forms/RecruitingSelection/RecruitingSelectionView.tsx` |
|
||||
| То же в поданной заявке: заменять и удалять невыполненных исполнителей, **в том числе активного и себя** | Исполнители заявки, у которых свой шаг исполнения ещё не выполнен | роль в маршруте (фаза «Исполнение»), без списка email | **Только** Заявка в отдел 1С | `forms/Ticket1C/useExecutorEditing.ts` |
|
||||
| Править **все поля и вложения** заявки (добавлять и удалять файлы) на своём активном шаге согласования | Алтынай Бултурукова | email в коде (список по каждой форме) | Подбор, Приём (Реферальная рекомендация — нет) | `shared/approval/fullEditRights.ts` → `canEditAllFields` |
|
||||
| То же: **все поля и вложения**, но **не только на своём шаге**: достаточно быть в маршруте (согласование или исполнение), пока заявка «На согласовании» или «Выполнение» | Ербол Шаймерденов | email в коде (список по каждой форме) + участие в маршруте | **Только** Организация рабочего места | `shared/approval/fullEditRights.ts` → `canEditAllFields` + `forms/WorkplaceSetup/useApproverRights.ts` |
|
||||
| Заменять человека на шаге согласования (не удалять шаг) **до отправки**: новая заявка, черновик, доработка | Любой инициатор | право по статусу заявки (форма редактируема) | **Только** Организация рабочего места | `forms/WorkplaceSetup/WorkplaceSetupForm.tsx` (`canReplaceRow`) + `useMatrixEditor.replace` |
|
||||
@@ -57,7 +58,7 @@
|
||||
|
||||
## Известные ограничения
|
||||
|
||||
- **Замена активного исполнителя (подбор).** Если прежний исполнитель держит
|
||||
- **Замена активного исполнителя (подбор, 1С).** Если прежний исполнитель держит
|
||||
заявку открытой и, не обновив страницу, нажмёт «Выполнено», решение
|
||||
запишется в шаг, который уже принадлежит новому исполнителю. При решении
|
||||
пока не перепроверяется, что шаг всё ещё за этим человеком
|
||||
|
||||
@@ -37,6 +37,12 @@
|
||||
шаг. Работает только для НОВОЙ заявки (`positionCode` есть только у планируемого
|
||||
маршрута, не у уже записанных строк списка согласования).
|
||||
|
||||
- **Исполнители сами заменяют и удаляют друг друга** в поданной заявке
|
||||
(`useExecutorEditing.ts`): любой исполнитель с невыполненным шагом может заменить
|
||||
или убрать невыполненного исполнителя, в т.ч. активного и себя. Выполненные шаги
|
||||
не трогаются, последнего невыполненного — только заменить. Карточка Planner на
|
||||
людей не назначается, поэтому замена её не затрагивает.
|
||||
|
||||
## Особое поведение
|
||||
- **Контакты заявителя предзаполняются через Microsoft Graph** (`/me`): ФИО, email,
|
||||
телефон, отдел, должность — единственная форма портала, которая это делает
|
||||
|
||||
@@ -42,6 +42,7 @@ import { handoffToPlanner, isPlannerHandoffStatus } from './plannerHandoff';
|
||||
import { RequestSection } from './sections/RequestSection';
|
||||
import { ContactSection } from './sections/ContactSection';
|
||||
import { useStableCallback } from '../../shared/useStableCallback';
|
||||
import { useExecutorEditing } from './useExecutorEditing';
|
||||
|
||||
interface ITicket1CViewProps {
|
||||
context: WebPartContext;
|
||||
@@ -101,6 +102,32 @@ const Ticket1CView: React.FC<ITicket1CViewProps> = ({ context, itemId, formKey,
|
||||
approval.getJobTitle(l)
|
||||
);
|
||||
|
||||
// rowId — оповестить конкретную активную строку (см. notifyActiveStep).
|
||||
const notify = (rowId?: number): 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)
|
||||
},
|
||||
rowId
|
||||
).catch(() => undefined);
|
||||
};
|
||||
|
||||
const executorEditing = useExecutorEditing({
|
||||
approval,
|
||||
approverService,
|
||||
itemId,
|
||||
executorRows: executors.rows,
|
||||
reloadKey: decisionSeq,
|
||||
onChanged: () => setDecisionSeq((n) => n + 1),
|
||||
notifyRow: notify
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
setLoading(true);
|
||||
@@ -140,15 +167,6 @@ const Ticket1CView: React.FC<ITicket1CViewProps> = ({ context, itemId, formKey,
|
||||
|
||||
const sectionProps = { data, set, context };
|
||||
|
||||
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`}>
|
||||
@@ -196,6 +214,7 @@ const Ticket1CView: React.FC<ITicket1CViewProps> = ({ context, itemId, formKey,
|
||||
emptyText="Согласование не требовалось для этого вида работ."
|
||||
/>
|
||||
|
||||
{executorEditing.error && <Alert variant="err">{executorEditing.error}</Alert>}
|
||||
{!executors.loading && executors.rows.length > 0 && (
|
||||
<ApprovalMatrix
|
||||
editor={executors}
|
||||
@@ -203,6 +222,10 @@ const Ticket1CView: React.FC<ITicket1CViewProps> = ({ context, itemId, formKey,
|
||||
title="Исполнение"
|
||||
roleLabel="Исполнитель"
|
||||
emptyText="Исполнители не назначены."
|
||||
canReplaceRow={executorEditing.canReplaceRow}
|
||||
onReplaceRow={executorEditing.onReplaceRow}
|
||||
canRemoveRow={executorEditing.canRemoveRow}
|
||||
onRemoveRow={executorEditing.onRemoveRow}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
ApprovalListService,
|
||||
ApproverService,
|
||||
APPROVAL_PHASE,
|
||||
REQUEST_STATUS,
|
||||
IMatrixRow,
|
||||
ISiteUser
|
||||
} from '../../shared/approval';
|
||||
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
||||
|
||||
/** Шаг исполнения ещё не выполнен — его можно заменить или удалить. */
|
||||
function isPendingExecutor(row: { status: string }): boolean {
|
||||
return row.status === REQUEST_STATUS.queued || row.status === REQUEST_STATUS.onExecution;
|
||||
}
|
||||
|
||||
export interface IExecutorEditing {
|
||||
canReplaceRow?: (row: IMatrixRow) => boolean;
|
||||
canRemoveRow?: (row: IMatrixRow) => boolean;
|
||||
onReplaceRow: (row: IMatrixRow, user: ISiteUser) => void;
|
||||
onRemoveRow: (row: IMatrixRow) => void;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface IExecutorEditingParams {
|
||||
approval: ApprovalListService;
|
||||
approverService: ApproverService;
|
||||
itemId: number;
|
||||
executorRows: IMatrixRow[];
|
||||
reloadKey: number;
|
||||
/** Маршрут изменился на сервере — перечитать матрицы. */
|
||||
onChanged: () => void;
|
||||
/** Оповестить строку, ставшую активной (заявка теперь у этого исполнителя). */
|
||||
notifyRow: (rowId: number) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Замена/удаление исполнителей в поданной заявке 1С. Право — у самих
|
||||
* исполнителей отдела 1С: кто стоит в фазе «Исполнение» с невыполненным шагом,
|
||||
* может заменить или убрать любого невыполненного исполнителя (в т.ч.
|
||||
* активного и себя) — так отдел передаёт заявку внутри себя. Выполненные шаги
|
||||
* не трогаются, последнего невыполненного исполнителя можно только заменить
|
||||
* (см. ApproverService.deleteExecutionStep).
|
||||
*/
|
||||
export function useExecutorEditing(params: IExecutorEditingParams): IExecutorEditing {
|
||||
const { approval, approverService, itemId, executorRows, reloadKey, onChanged, notifyRow } = params;
|
||||
const [isExecutor, setIsExecutor] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string | undefined>(undefined);
|
||||
|
||||
// В загруженных строках матрицы нет email, поэтому «я ли исполнитель»
|
||||
// проверяем по Id пользователя в строках списка согласования.
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
Promise.all([approval.getForRequest(itemId), approval.getCurrentUserId()])
|
||||
.then(([rows, userId]) => {
|
||||
if (!alive) return;
|
||||
setIsExecutor(
|
||||
rows.some((r) => r.phase === APPROVAL_PHASE.execution && r.approverId === userId && isPendingExecutor(r))
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
if (alive) setIsExecutor(false);
|
||||
});
|
||||
return () => {
|
||||
alive = false;
|
||||
};
|
||||
}, [approval, itemId, reloadKey]);
|
||||
|
||||
const pendingCount = executorRows.filter(isPendingExecutor).length;
|
||||
|
||||
const onReplaceRow = (row: IMatrixRow, user: ISiteUser): void => {
|
||||
if (!row.id) return;
|
||||
const rowId = row.id;
|
||||
setError(undefined);
|
||||
approverService
|
||||
.replaceExecutionStep(itemId, rowId, user)
|
||||
.then((wasActive) => {
|
||||
onChanged();
|
||||
// Шаг уже был в работе — заявка сразу у нового исполнителя, сообщаем ему.
|
||||
if (wasActive) notifyRow(rowId);
|
||||
})
|
||||
.catch((e) => setError(friendlyErrorMessage(e)));
|
||||
};
|
||||
|
||||
const onRemoveRow = (row: IMatrixRow): void => {
|
||||
if (!row.id) return;
|
||||
setError(undefined);
|
||||
approverService
|
||||
.deleteExecutionStep(itemId, row.id)
|
||||
.then((activatedRowIds) => {
|
||||
onChanged();
|
||||
activatedRowIds.forEach(notifyRow);
|
||||
})
|
||||
.catch((e) => setError(friendlyErrorMessage(e)));
|
||||
};
|
||||
|
||||
return {
|
||||
canReplaceRow: isExecutor ? isPendingExecutor : undefined,
|
||||
canRemoveRow: isExecutor ? (row) => isPendingExecutor(row) && pendingCount > 1 : undefined,
|
||||
onReplaceRow,
|
||||
onRemoveRow,
|
||||
error
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user