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 в коде | Все остальные формы с маршрутом согласования (отпуск, увольнение, 1С и т. д.); в поданной заявке — нет | `shared/approval/routeEditRights.ts` → `composeRemoveRowRule` |
|
||||||
| Добавлять и удалять исполнителей **при составлении** заявки | те же (`canEditRoute`) | email в коде | Все формы, где в маршруте из админки есть фаза исполнения; в поданной заявке — нет | `shared/approval/routeEditRights.ts` → `canEditExecutors` |
|
| Добавлять и удалять исполнителей **при составлении** заявки | те же (`canEditRoute`) | email в коде | Все формы, где в маршруте из админки есть фаза исполнения; в поданной заявке — нет | `shared/approval/routeEditRights.ts` → `canEditExecutors` |
|
||||||
| Заменять и удалять исполнителей, **в том числе активного** («На исполнении») | Алтынай Бултурукова | тот же email (`canEditRoute`) | **Только** Заявка на подбор, матрица «Исполнение» | `canEditRoute` + `forms/RecruitingSelection/RecruitingSelectionView.tsx` |
|
| Заменять и удалять исполнителей, **в том числе активного** («На исполнении») | Алтынай Бултурукова | тот же 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` |
|
||||||
| То же: **все поля и вложения**, но **не только на своём шаге**: достаточно быть в маршруте (согласование или исполнение), пока заявка «На согласовании» или «Выполнение» | Ербол Шаймерденов | email в коде (список по каждой форме) + участие в маршруте | **Только** Организация рабочего места | `shared/approval/fullEditRights.ts` → `canEditAllFields` + `forms/WorkplaceSetup/useApproverRights.ts` |
|
| То же: **все поля и вложения**, но **не только на своём шаге**: достаточно быть в маршруте (согласование или исполнение), пока заявка «На согласовании» или «Выполнение» | Ербол Шаймерденов | email в коде (список по каждой форме) + участие в маршруте | **Только** Организация рабочего места | `shared/approval/fullEditRights.ts` → `canEditAllFields` + `forms/WorkplaceSetup/useApproverRights.ts` |
|
||||||
| Заменять человека на шаге согласования (не удалять шаг) **до отправки**: новая заявка, черновик, доработка | Любой инициатор | право по статусу заявки (форма редактируема) | **Только** Организация рабочего места | `forms/WorkplaceSetup/WorkplaceSetupForm.tsx` (`canReplaceRow`) + `useMatrixEditor.replace` |
|
| Заменять человека на шаге согласования (не удалять шаг) **до отправки**: новая заявка, черновик, доработка | Любой инициатор | право по статусу заявки (форма редактируема) | **Только** Организация рабочего места | `forms/WorkplaceSetup/WorkplaceSetupForm.tsx` (`canReplaceRow`) + `useMatrixEditor.replace` |
|
||||||
@@ -57,7 +58,7 @@
|
|||||||
|
|
||||||
## Известные ограничения
|
## Известные ограничения
|
||||||
|
|
||||||
- **Замена активного исполнителя (подбор).** Если прежний исполнитель держит
|
- **Замена активного исполнителя (подбор, 1С).** Если прежний исполнитель держит
|
||||||
заявку открытой и, не обновив страницу, нажмёт «Выполнено», решение
|
заявку открытой и, не обновив страницу, нажмёт «Выполнено», решение
|
||||||
запишется в шаг, который уже принадлежит новому исполнителю. При решении
|
запишется в шаг, который уже принадлежит новому исполнителю. При решении
|
||||||
пока не перепроверяется, что шаг всё ещё за этим человеком
|
пока не перепроверяется, что шаг всё ещё за этим человеком
|
||||||
|
|||||||
@@ -37,6 +37,12 @@
|
|||||||
шаг. Работает только для НОВОЙ заявки (`positionCode` есть только у планируемого
|
шаг. Работает только для НОВОЙ заявки (`positionCode` есть только у планируемого
|
||||||
маршрута, не у уже записанных строк списка согласования).
|
маршрута, не у уже записанных строк списка согласования).
|
||||||
|
|
||||||
|
- **Исполнители сами заменяют и удаляют друг друга** в поданной заявке
|
||||||
|
(`useExecutorEditing.ts`): любой исполнитель с невыполненным шагом может заменить
|
||||||
|
или убрать невыполненного исполнителя, в т.ч. активного и себя. Выполненные шаги
|
||||||
|
не трогаются, последнего невыполненного — только заменить. Карточка Planner на
|
||||||
|
людей не назначается, поэтому замена её не затрагивает.
|
||||||
|
|
||||||
## Особое поведение
|
## Особое поведение
|
||||||
- **Контакты заявителя предзаполняются через Microsoft Graph** (`/me`): ФИО, email,
|
- **Контакты заявителя предзаполняются через Microsoft Graph** (`/me`): ФИО, email,
|
||||||
телефон, отдел, должность — единственная форма портала, которая это делает
|
телефон, отдел, должность — единственная форма портала, которая это делает
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import { handoffToPlanner, isPlannerHandoffStatus } from './plannerHandoff';
|
|||||||
import { RequestSection } from './sections/RequestSection';
|
import { RequestSection } from './sections/RequestSection';
|
||||||
import { ContactSection } from './sections/ContactSection';
|
import { ContactSection } from './sections/ContactSection';
|
||||||
import { useStableCallback } from '../../shared/useStableCallback';
|
import { useStableCallback } from '../../shared/useStableCallback';
|
||||||
|
import { useExecutorEditing } from './useExecutorEditing';
|
||||||
|
|
||||||
interface ITicket1CViewProps {
|
interface ITicket1CViewProps {
|
||||||
context: WebPartContext;
|
context: WebPartContext;
|
||||||
@@ -101,6 +102,32 @@ const Ticket1CView: React.FC<ITicket1CViewProps> = ({ context, itemId, formKey,
|
|||||||
approval.getJobTitle(l)
|
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(() => {
|
useEffect(() => {
|
||||||
let alive = true;
|
let alive = true;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -140,15 +167,6 @@ const Ticket1CView: React.FC<ITicket1CViewProps> = ({ context, itemId, formKey,
|
|||||||
|
|
||||||
const sectionProps = { data, set, context };
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={`${styles.toolbar} rp-no-print`}>
|
<div className={`${styles.toolbar} rp-no-print`}>
|
||||||
@@ -196,6 +214,7 @@ const Ticket1CView: React.FC<ITicket1CViewProps> = ({ context, itemId, formKey,
|
|||||||
emptyText="Согласование не требовалось для этого вида работ."
|
emptyText="Согласование не требовалось для этого вида работ."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{executorEditing.error && <Alert variant="err">{executorEditing.error}</Alert>}
|
||||||
{!executors.loading && executors.rows.length > 0 && (
|
{!executors.loading && executors.rows.length > 0 && (
|
||||||
<ApprovalMatrix
|
<ApprovalMatrix
|
||||||
editor={executors}
|
editor={executors}
|
||||||
@@ -203,6 +222,10 @@ const Ticket1CView: React.FC<ITicket1CViewProps> = ({ context, itemId, formKey,
|
|||||||
title="Исполнение"
|
title="Исполнение"
|
||||||
roleLabel="Исполнитель"
|
roleLabel="Исполнитель"
|
||||||
emptyText="Исполнители не назначены."
|
emptyText="Исполнители не назначены."
|
||||||
|
canReplaceRow={executorEditing.canReplaceRow}
|
||||||
|
onReplaceRow={executorEditing.onReplaceRow}
|
||||||
|
canRemoveRow={executorEditing.canRemoveRow}
|
||||||
|
onRemoveRow={executorEditing.onRemoveRow}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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