feat: письмо инициатору при отклонении и возврате заявки

Поток RequestsNotification умеет ветки «Отклонено» и «Возврат на доработку»,
но портал их не вызывал: notifyActiveStep ищет только активные строки, а после
такого решения их нет — инициатор узнавал о решении, только открыв заявку.

notifyInitiatorOfReturn шлёт в тот же поток RowID строки, по которой решил
текущий пользователь; подключён в onDecided всех форм, кроме «Изменения
условий ТД» (доработаем позже).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Тимур Городецкий
2026-10-28 16:50:31 +05:00
parent 72e330e659
commit 9cacb6b271
17 changed files with 198 additions and 76 deletions
@@ -10,6 +10,7 @@ import {
StatusBadge,
ApprovalHistory,
notifyActiveStep,
notifyInitiatorOfReturn,
useRequestTypeId,
usePhasedRoute
} from '../../shared/approval';
@@ -108,14 +109,17 @@ const BusinessTripView: React.FC<IBusinessTripViewProps> = ({ context, itemId, f
);
}
const notify = (): void => {
const notify = (newStatus?: string): void => {
// Решение уже применено; без requestTypeId просто некого уведомлять — UI не роняем.
if (!requestTypeId) return;
notifyActiveStep(context, approval, itemId, {
const target = {
requestTypeId,
dataListName: listId ? dataListTitleForListId(FORM_ID, listId) : dataListTitle(FORM_ID),
approvalListName: listId ? approvalListTitleForListId(FORM_ID, listId) : approvalListTitle(FORM_ID)
}).catch(() => undefined);
};
notifyActiveStep(context, approval, itemId, target).catch(() => undefined);
// Отклонили или вернули на доработку — письмо инициатору (при других статусах ничего не делает).
if (newStatus) notifyInitiatorOfReturn(context, approval, itemId, target, newStatus).catch(() => undefined);
};
const sectionProps = { data, set: NOOP_SET, context };
@@ -173,7 +177,7 @@ const BusinessTripView: React.FC<IBusinessTripViewProps> = ({ context, itemId, f
onDecided={(newStatus) => {
setStatus(newStatus);
setDecisionSeq((n) => n + 1);
notify();
notify(newStatus);
}}
onDelegated={() => {
setDecisionSeq((n) => n + 1);
@@ -11,6 +11,7 @@ import {
ApprovalHistory,
useMatrixEditor,
notifyActiveStep,
notifyInitiatorOfReturn,
useRequestTypeId,
APPROVAL_PHASE
} from '../../shared/approval';
@@ -255,16 +256,19 @@ const ClearanceView: React.FC<IClearanceViewProps> = ({ context, itemId, formKey
printOrgIndex
};
const notify = (): void => {
const notify = (newStatus?: string): void => {
// requestTypeId ещё не резолвился — решение уже применилось (строка списка
// обновлена), просто нечего уведомлять на следующем шаге; не проваливаем
// весь UI ради необязательного оповещения.
if (!requestTypeId) return;
notifyActiveStep(context, approval, itemId, {
const target = {
requestTypeId,
dataListName: listId ? dataListTitleForListId(FORM_ID, listId) : dataListTitle(FORM_ID),
approvalListName: listId ? approvalListTitleForListId(FORM_ID, listId) : approvalListTitle(FORM_ID)
}).catch(() => undefined);
};
notifyActiveStep(context, approval, itemId, target).catch(() => undefined);
// Отклонили или вернули на доработку — письмо инициатору (при других статусах ничего не делает).
if (newStatus) notifyInitiatorOfReturn(context, approval, itemId, target, newStatus).catch(() => undefined);
};
return (
@@ -348,7 +352,7 @@ const ClearanceView: React.FC<IClearanceViewProps> = ({ context, itemId, formKey
onDecided={(newStatus) => {
setStatus(newStatus);
setDecisionSeq((n) => n + 1);
notify();
notify(newStatus);
}}
onDelegated={() => {
setDecisionSeq((n) => n + 1);
@@ -12,6 +12,7 @@ import {
ApprovalHistory,
useMatrixEditor,
notifyActiveStep,
notifyInitiatorOfReturn,
useRequestTypeId,
useViewExecutors,
approvalPhaseRows
@@ -118,16 +119,19 @@ const ConcreteRequestView: React.FC<IConcreteRequestViewProps> = ({ context, ite
const sectionProps = { data, set: NOOP_SET, context, printOrgIndex };
const notify = (): void => {
const notify = (newStatus?: string): void => {
// requestTypeId ещё не резолвился — решение уже применилось (строка списка
// обновлена), просто нечего уведомлять на следующем шаге; не проваливаем
// весь UI ради необязательного оповещения.
if (!requestTypeId) return;
notifyActiveStep(context, approval, itemId, {
const target = {
requestTypeId,
dataListName: listId ? dataListTitleForListId(FORM_ID, listId) : dataListTitle(FORM_ID),
approvalListName: listId ? approvalListTitleForListId(FORM_ID, listId) : approvalListTitle(FORM_ID)
}).catch(() => undefined);
};
notifyActiveStep(context, approval, itemId, target).catch(() => undefined);
// Отклонили или вернули на доработку — письмо инициатору (при других статусах ничего не делает).
if (newStatus) notifyInitiatorOfReturn(context, approval, itemId, target, newStatus).catch(() => undefined);
};
return (
@@ -175,7 +179,7 @@ const ConcreteRequestView: React.FC<IConcreteRequestViewProps> = ({ context, ite
onDecided={(newStatus) => {
setStatus(newStatus);
setDecisionSeq((n) => n + 1);
notify();
notify(newStatus);
}}
onDelegated={() => {
setDecisionSeq((n) => n + 1);
@@ -12,6 +12,7 @@ import {
ApprovalHistory,
useMatrixEditor,
notifyActiveStep,
notifyInitiatorOfReturn,
useRequestTypeId,
REQUEST_STATUS,
approvalPhaseRows,
@@ -218,16 +219,19 @@ const DismissalView: React.FC<IDismissalViewProps> = ({ context, itemId, formKey
await updateAssetsAcceptor(itemId, data.AssetsAcceptor, listId);
};
const notify = (): void => {
const notify = (newStatus?: string): void => {
// requestTypeId ещё не резолвился — решение уже применилось (строка списка
// обновлена), просто нечего уведомлять на следующем шаге; не проваливаем
// весь UI ради необязательного оповещения.
if (!requestTypeId) return;
notifyActiveStep(context, approval, itemId, {
const target = {
requestTypeId,
dataListName: listId ? dataListTitleForListId(FORM_ID, listId) : dataListTitle(FORM_ID),
approvalListName: listId ? approvalListTitleForListId(FORM_ID, listId) : approvalListTitle(FORM_ID)
}).catch(() => undefined);
};
notifyActiveStep(context, approval, itemId, target).catch(() => undefined);
// Отклонили или вернули на доработку — письмо инициатору (при других статусах ничего не делает).
if (newStatus) notifyInitiatorOfReturn(context, approval, itemId, target, newStatus).catch(() => undefined);
};
// Заявление согласовано целиком → сразу заводим обходной лист по нему (данные
@@ -311,7 +315,7 @@ const DismissalView: React.FC<IDismissalViewProps> = ({ context, itemId, formKey
onDecided={(newStatus) => {
setStatus(newStatus);
setDecisionSeq((n) => n + 1);
notify();
notify(newStatus);
// «Согласовано» = маршрут заявления пройден целиком (фазы исполнения у
// него нет, см. README формы) — значит увольнение подтверждено и пора
// заводить обходной лист. Промис ВОЗВРАЩАЕМ: пока он не разрешится,
@@ -14,6 +14,7 @@ import {
ExecutorsMatrix,
approvalPhaseRows,
notifyActiveStep,
notifyInitiatorOfReturn,
useRequestTypeId
} from '../../shared/approval';
import { friendlyErrorMessage } from '../../shared/diagnostics';
@@ -117,16 +118,19 @@ const EmployeeTransferView: React.FC<IEmployeeTransferViewProps> = ({ context, i
const sectionProps = { data, set: NOOP_SET, context, printOrgIndex };
const notify = (): void => {
const notify = (newStatus?: string): void => {
// requestTypeId ещё не резолвился — решение уже применилось (строка списка
// обновлена), просто нечего уведомлять на следующем шаге; не проваливаем
// весь UI ради необязательного оповещения.
if (!requestTypeId) return;
notifyActiveStep(context, approval, itemId, {
const target = {
requestTypeId,
dataListName: listId ? dataListTitleForListId(FORM_ID, listId) : dataListTitle(FORM_ID),
approvalListName: listId ? approvalListTitleForListId(FORM_ID, listId) : approvalListTitle(FORM_ID)
}).catch(() => undefined);
};
notifyActiveStep(context, approval, itemId, target).catch(() => undefined);
// Отклонили или вернули на доработку — письмо инициатору (при других статусах ничего не делает).
if (newStatus) notifyInitiatorOfReturn(context, approval, itemId, target, newStatus).catch(() => undefined);
};
return (
@@ -172,7 +176,7 @@ const EmployeeTransferView: React.FC<IEmployeeTransferViewProps> = ({ context, i
onDecided={(newStatus) => {
setStatus(newStatus);
setDecisionSeq((n) => n + 1);
notify();
notify(newStatus);
}}
onDelegated={() => {
setDecisionSeq((n) => n + 1);
@@ -24,6 +24,7 @@ import {
ApprovalHistory,
useMatrixEditor,
notifyActiveStep,
notifyInitiatorOfReturn,
useRequestTypeId,
APPROVAL_PHASE,
isApprovedStatus,
@@ -283,13 +284,16 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
);
// Выше ранних return (загрузка/ошибка): зовётся и из колбэков хуков ниже.
const notify = (): void => {
const notify = (newStatus?: string): void => {
if (!requestTypeId) return;
notifyActiveStep(context, approval, itemId, {
const target = {
requestTypeId,
dataListName: listId ? dataListTitleForListId(FORM_ID, listId) : dataListTitle(FORM_ID),
approvalListName: listId ? approvalListTitleForListId(FORM_ID, listId) : approvalListTitle(FORM_ID),
}).catch(() => undefined);
};
notifyActiveStep(context, approval, itemId, target).catch(() => undefined);
// Отклонили или вернули на доработку — письмо инициатору (при других статусах ничего не делает).
if (newStatus) notifyInitiatorOfReturn(context, approval, itemId, target, newStatus).catch(() => undefined);
};
// Добавить согласующего/исполнителя в поданную заявку — то же право
@@ -479,7 +483,7 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
setStatus(newStatus);
setDecisionSeq((n) => n + 1);
// Очередь сдвинулась — уведомляем следующий активный шаг.
notify();
notify(newStatus);
}}
onDelegated={() => {
// Шаг переназначен: активной стала строка делегата — обновляем
@@ -22,6 +22,7 @@ import {
ApprovalHistory,
useMatrixEditor,
notifyActiveStep,
notifyInitiatorOfReturn,
useRequestTypeId,
isApprovedStatus,
resolveApproverOrgInfo,
@@ -204,20 +205,25 @@ const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ cont
await saveAllFields();
};
// rowId — оповестить конкретную активную строку (см. notifyActiveStep).
const notify = (rowId?: number): void => {
if (!requestTypeId) return;
notifyActiveStep(
context,
approval,
itemId,
{
// Куда смотрит поток оповещения; undefined — тип заявки ещё не резолвился.
const notifyTarget = requestTypeId
? {
requestTypeId,
dataListName: listId ? dataListTitleForListId(FORM_ID, listId) : dataListTitle(FORM_ID),
approvalListName: listId ? approvalListTitleForListId(FORM_ID, listId) : approvalListTitle(FORM_ID)
},
rowId
).catch(() => undefined);
}
: undefined;
// rowId — оповестить конкретную активную строку (см. notifyActiveStep).
const notify = (rowId?: number): void => {
if (!notifyTarget) return;
notifyActiveStep(context, approval, itemId, notifyTarget, rowId).catch(() => undefined);
};
// Отклонили или вернули на доработку — письмо инициатору (при других статусах ничего не делает).
const notifyInitiator = (newStatus: string): void => {
if (!notifyTarget) return;
notifyInitiatorOfReturn(context, approval, itemId, notifyTarget, newStatus).catch(() => undefined);
};
// Удалить ещё не решённый шаг маршрута (см. ApproverService.deleteQueuedStep).
@@ -467,6 +473,7 @@ const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ cont
setStatus(newStatus);
setDecisionSeq((n) => n + 1);
notify();
notifyInitiator(newStatus);
}}
onDelegated={() => {
setDecisionSeq((n) => n + 1);
@@ -21,6 +21,7 @@ import {
ApprovalHistory,
useMatrixEditor,
notifyActiveStep,
notifyInitiatorOfReturn,
useRequestTypeId,
APPROVAL_PHASE,
isApprovedStatus,
@@ -169,16 +170,19 @@ const ReferralView: React.FC<IReferralViewProps> = ({ context, itemId, formKey,
const sectionProps = { data, set: NOOP_SET, context, printOrgIndex };
const notify = (): void => {
const notify = (newStatus?: string): void => {
// requestTypeId ещё не резолвился — решение уже применилось (строка списка
// обновлена), просто нечего уведомлять на следующем шаге; не проваливаем
// весь UI ради необязательного оповещения.
if (!requestTypeId) return;
notifyActiveStep(context, approval, itemId, {
const target = {
requestTypeId,
dataListName: listId ? dataListTitleForListId(FORM_ID, listId) : dataListTitle(FORM_ID),
approvalListName: listId ? approvalListTitleForListId(FORM_ID, listId) : approvalListTitle(FORM_ID)
}).catch(() => undefined);
};
notifyActiveStep(context, approval, itemId, target).catch(() => undefined);
// Отклонили или вернули на доработку — письмо инициатору (при других статусах ничего не делает).
if (newStatus) notifyInitiatorOfReturn(context, approval, itemId, target, newStatus).catch(() => undefined);
};
return (
@@ -253,7 +257,7 @@ const ReferralView: React.FC<IReferralViewProps> = ({ context, itemId, formKey,
onDecided={(newStatus) => {
setStatus(newStatus);
setDecisionSeq((n) => n + 1);
notify();
notify(newStatus);
}}
onDelegated={() => {
setDecisionSeq((n) => n + 1);
@@ -24,6 +24,7 @@ import {
ExecutorsMatrix,
approvalPhaseRows,
notifyActiveStep,
notifyInitiatorOfReturn,
useRequestTypeId,
isEditableStatus
} from '../../shared/approval';
@@ -141,13 +142,16 @@ const ServiceRequestView: React.FC<IServiceRequestViewProps> = ({ context, itemI
const sectionProps = { data, set, context, printOrgIndex, attachmentUrls };
const notify = (): void => {
const notify = (newStatus?: string): void => {
if (!requestTypeId) return;
notifyActiveStep(context, approval, itemId, {
const target = {
requestTypeId,
dataListName: listId ? dataListTitleForListId(FORM_ID, listId) : dataListTitle(FORM_ID),
approvalListName: listId ? approvalListTitleForListId(FORM_ID, listId) : approvalListTitle(FORM_ID)
}).catch(() => undefined);
};
notifyActiveStep(context, approval, itemId, target).catch(() => undefined);
// Отклонили или вернули на доработку — письмо инициатору (при других статусах ничего не делает).
if (newStatus) notifyInitiatorOfReturn(context, approval, itemId, target, newStatus).catch(() => undefined);
};
return (
@@ -205,7 +209,7 @@ const ServiceRequestView: React.FC<IServiceRequestViewProps> = ({ context, itemI
onDecided={(newStatus) => {
setStatus(newStatus);
setDecisionSeq((n) => n + 1);
notify();
notify(newStatus);
}}
onDelegated={() => {
setDecisionSeq((n) => n + 1);
@@ -9,6 +9,7 @@ import {
StatusBadge,
ApprovalHistory,
notifyActiveStep,
notifyInitiatorOfReturn,
useRequestTypeId,
usePhasedRoute
} from '../../shared/approval';
@@ -110,14 +111,17 @@ const SkudExplanationView: React.FC<ISkudExplanationViewProps> = ({ context, ite
);
}
const notify = (): void => {
const notify = (newStatus?: string): void => {
// Решение уже применено; без requestTypeId просто некого уведомлять — UI не роняем.
if (!requestTypeId) return;
notifyActiveStep(context, approval, itemId, {
const target = {
requestTypeId,
dataListName: listId ? dataListTitleForListId(FORM_ID, listId) : dataListTitle(FORM_ID),
approvalListName: listId ? approvalListTitleForListId(FORM_ID, listId) : approvalListTitle(FORM_ID)
}).catch(() => undefined);
};
notifyActiveStep(context, approval, itemId, target).catch(() => undefined);
// Отклонили или вернули на доработку — письмо инициатору (при других статусах ничего не делает).
if (newStatus) notifyInitiatorOfReturn(context, approval, itemId, target, newStatus).catch(() => undefined);
};
const sectionProps = { data, set: NOOP_SET, context, printOrgIndex };
@@ -175,7 +179,7 @@ const SkudExplanationView: React.FC<ISkudExplanationViewProps> = ({ context, ite
onDecided={(newStatus) => {
setStatus(newStatus);
setDecisionSeq((n) => n + 1);
notify();
notify(newStatus);
}}
onDelegated={() => {
setDecisionSeq((n) => n + 1);
@@ -23,6 +23,7 @@ import {
ApprovalHistory,
useMatrixEditor,
notifyActiveStep,
notifyInitiatorOfReturn,
useRequestTypeId,
isEditableStatus,
APPROVAL_PHASE
@@ -102,20 +103,25 @@ 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,
{
// Куда смотрит поток оповещения; undefined — тип заявки ещё не резолвился.
const notifyTarget = requestTypeId
? {
requestTypeId,
dataListName: listId ? dataListTitleForListId(FORM_ID, listId) : dataListTitle(FORM_ID),
approvalListName: listId ? approvalListTitleForListId(FORM_ID, listId) : approvalListTitle(FORM_ID)
},
rowId
).catch(() => undefined);
}
: undefined;
// rowId — оповестить конкретную активную строку (см. notifyActiveStep).
const notify = (rowId?: number): void => {
if (!notifyTarget) return;
notifyActiveStep(context, approval, itemId, notifyTarget, rowId).catch(() => undefined);
};
// Отклонили или вернули на доработку — письмо инициатору (при других статусах ничего не делает).
const notifyInitiator = (newStatus: string): void => {
if (!notifyTarget) return;
notifyInitiatorOfReturn(context, approval, itemId, notifyTarget, newStatus).catch(() => undefined);
};
const executorEditing = useExecutorEditing({
@@ -246,6 +252,7 @@ const Ticket1CView: React.FC<ITicket1CViewProps> = ({ context, itemId, formKey,
setDecisionSeq((n) => n + 1);
// Очередь сдвинулась — уведомляем следующий активный шаг.
notify();
notifyInitiator(newStatus);
// Дошла до исполнения / завершилась / отклонена — карточка в Planner
// (повторно не создаётся, см. plannerHandoff.ts).
@@ -24,6 +24,7 @@ import {
ApprovalHistory,
useMatrixEditor,
notifyActiveStep,
notifyInitiatorOfReturn,
useRequestTypeId,
isEditableStatus,
APPROVAL_PHASE
@@ -144,13 +145,16 @@ const TmcTransferView: React.FC<ITmcTransferViewProps> = ({ context, itemId, for
const sectionProps = { data, set, context, printOrgIndex };
const notify = (): void => {
const notify = (newStatus?: string): void => {
if (!requestTypeId) return;
notifyActiveStep(context, approval, itemId, {
const target = {
requestTypeId,
dataListName: listId ? dataListTitleForListId(FORM_ID, listId) : dataListTitle(FORM_ID),
approvalListName: listId ? approvalListTitleForListId(FORM_ID, listId) : approvalListTitle(FORM_ID)
}).catch(() => undefined);
};
notifyActiveStep(context, approval, itemId, target).catch(() => undefined);
// Отклонили или вернули на доработку — письмо инициатору (при других статусах ничего не делает).
if (newStatus) notifyInitiatorOfReturn(context, approval, itemId, target, newStatus).catch(() => undefined);
};
return (
@@ -222,7 +226,7 @@ const TmcTransferView: React.FC<ITmcTransferViewProps> = ({ context, itemId, for
onDecided={(newStatus) => {
setStatus(newStatus);
setDecisionSeq((n) => n + 1);
notify();
notify(newStatus);
}}
onDelegated={() => {
setDecisionSeq((n) => n + 1);
@@ -22,6 +22,7 @@ import {
useViewExecutors,
approvalPhaseRows,
notifyActiveStep,
notifyInitiatorOfReturn,
useRequestTypeId
} from '../../shared/approval';
import { friendlyErrorMessage } from '../../shared/diagnostics';
@@ -125,16 +126,19 @@ const UnpaidLeaveView: React.FC<IUnpaidLeaveViewProps> = ({ context, itemId, for
const sectionProps = { data, set: NOOP_SET, context, printOrgIndex };
const notify = (): void => {
const notify = (newStatus?: string): void => {
// requestTypeId ещё не резолвился — решение уже применилось (строка списка
// обновлена), просто нечего уведомлять на следующем шаге; не проваливаем
// весь UI ради необязательного оповещения.
if (!requestTypeId) return;
notifyActiveStep(context, approval, itemId, {
const target = {
requestTypeId,
dataListName: listId ? dataListTitleForListId(FORM_ID, listId) : dataListTitle(FORM_ID),
approvalListName: listId ? approvalListTitleForListId(FORM_ID, listId) : approvalListTitle(FORM_ID)
}).catch(() => undefined);
};
notifyActiveStep(context, approval, itemId, target).catch(() => undefined);
// Отклонили или вернули на доработку — письмо инициатору (при других статусах ничего не делает).
if (newStatus) notifyInitiatorOfReturn(context, approval, itemId, target, newStatus).catch(() => undefined);
};
return (
@@ -182,7 +186,7 @@ const UnpaidLeaveView: React.FC<IUnpaidLeaveViewProps> = ({ context, itemId, for
onDecided={(newStatus) => {
setStatus(newStatus);
setDecisionSeq((n) => n + 1);
notify();
notify(newStatus);
}}
onDelegated={() => {
setDecisionSeq((n) => n + 1);
@@ -23,6 +23,7 @@ import {
ExecutorsMatrix,
approvalPhaseRows,
notifyActiveStep,
notifyInitiatorOfReturn,
useRequestTypeId
} from '../../shared/approval';
import { completesApprovalPhase } from '../../shared/approval/models/statuses';
@@ -143,16 +144,19 @@ const VacationView: React.FC<IVacationViewProps> = ({ context, itemId, formKey,
const sectionProps = { data, set: NOOP_SET, context, printOrgIndex };
const notify = (): void => {
const notify = (newStatus?: string): void => {
// requestTypeId ещё не резолвился — решение уже применилось (строка списка
// обновлена), просто нечего уведомлять на следующем шаге; не проваливаем
// весь UI ради необязательного оповещения.
if (!requestTypeId) return;
notifyActiveStep(context, approval, itemId, {
const target = {
requestTypeId,
dataListName: listId ? dataListTitleForListId(FORM_ID, listId) : dataListTitle(FORM_ID),
approvalListName: listId ? approvalListTitleForListId(FORM_ID, listId) : approvalListTitle(FORM_ID)
}).catch(() => undefined);
};
notifyActiveStep(context, approval, itemId, target).catch(() => undefined);
// Отклонили или вернули на доработку — письмо инициатору (при других статусах ничего не делает).
if (newStatus) notifyInitiatorOfReturn(context, approval, itemId, target, newStatus).catch(() => undefined);
};
// Заявление уходит на подпись в ЭДО (по документу на каждую компанию сотрудника, одним запросом).
@@ -263,7 +267,7 @@ const VacationView: React.FC<IVacationViewProps> = ({ context, itemId, formKey,
onDecided={(newStatus) => {
setStatus(newStatus);
setDecisionSeq((n) => n + 1);
notify();
notify(newStatus);
return VACATION_EDO_SIGNING_ENABLED && sentBeforeDecision.current ?'Документы отправлены на подписание в ЭДО.' : undefined;
}}
onDelegated={() => {
@@ -23,6 +23,7 @@ import {
ApprovalHistory,
useMatrixEditor,
notifyActiveStep,
notifyInitiatorOfReturn,
useRequestTypeId,
isEditableStatus,
APPROVAL_PHASE,
@@ -224,13 +225,16 @@ const WorkplaceSetupView: React.FC<IWorkplaceSetupViewProps> = ({ context, itemI
const sectionProps = { data, set, context, printOrgIndex };
const canEditAnything = canEditAll || canEditReviewFields;
const notify = (): void => {
const notify = (newStatus?: string): void => {
if (!requestTypeId) return;
notifyActiveStep(context, approval, itemId, {
const target = {
requestTypeId,
dataListName: listId ? dataListTitleForListId(FORM_ID, listId) : dataListTitle(FORM_ID),
approvalListName: listId ? approvalListTitleForListId(FORM_ID, listId) : approvalListTitle(FORM_ID),
}).catch(() => undefined);
};
notifyActiveStep(context, approval, itemId, target).catch(() => undefined);
// Отклонили или вернули на доработку — письмо инициатору (при других статусах ничего не делает).
if (newStatus) notifyInitiatorOfReturn(context, approval, itemId, target, newStatus).catch(() => undefined);
};
return (
@@ -316,7 +320,7 @@ const WorkplaceSetupView: React.FC<IWorkplaceSetupViewProps> = ({ context, itemI
setStatus(newStatus);
setDecisionSeq((n) => n + 1);
// Очередь сдвинулась — уведомляем следующий активный шаг.
notify();
notify(newStatus);
}}
onDelegated={() => {
// Шаг переназначен: активной стала строка делегата — обновляем
@@ -59,7 +59,7 @@ export { approvalServiceFor } from './approvalServiceFactory';
export { ApproverService } from './approverDecision.service';
export type { ApproverDecision, IPendingRow } from './approverDecision.service';
export { getPlannedMatrix, writeApprovers, writeRoute } from './approvalSubmission.service';
export { notifyActiveStep, postToFlow } from './notify.service';
export { notifyActiveStep, notifyInitiatorOfReturn, postToFlow } from './notify.service';
export type { INotifyTarget } from './notify.service';
export { useMatrixEditor, findUnassignedStep, unassignedStepMessage } from './useMatrixEditor';
export type { IUseMatrixEditor } from './useMatrixEditor';
@@ -1,8 +1,10 @@
import { AadHttpClient } from '@microsoft/sp-http';
import { WebPartContext } from '@microsoft/sp-webpart-base';
import { FLOW_RESOURCE, FLOW_NOTIFY_URL, getNotificationsEnabled, getNotifyOnlyTo } from '../config';
import { getCreatedBy } from '../createdBy';
import { ApprovalListService } from './approvalList.service';
import { IApprovalRow } from './models/IApprovalRow';
import { REQUEST_STATUS } from './models/statuses';
import { pickRowsToNotify } from './notifyTargets';
// Данные формы для тела запроса к потоку (постоянны для формы).
@@ -42,11 +44,17 @@ export async function postToFlow(context: WebPartContext, flowUrl: string, body:
// живому человеку лучше не слать».
async function isNotifyAllowed(approval: ApprovalListService, approverId: number): Promise<boolean> {
if (!getNotificationsEnabled()) return false;
if (!getNotifyOnlyTo()) return true;
return isRecipientAllowed(await approval.resolveUserEmail(approverId));
}
// То же ограничение notifyOnlyTo, когда адрес получателя уже известен.
function isRecipientAllowed(email: string): boolean {
if (!getNotificationsEnabled()) return false;
const onlyTo = getNotifyOnlyTo();
if (!onlyTo) return true;
const email = await approval.resolveUserEmail(approverId);
if (email.toLowerCase() === onlyTo.toLowerCase()) return true;
console.warn(
@@ -109,3 +117,51 @@ export async function notifyActiveStep(
pickRowsToNotify(rows, rowId).map((row) => notifyRow(context, approval, requestId, target, row))
);
}
// Решения, после которых заявка уходит обратно к инициатору и ему нужно письмо.
const RETURNED_TO_INITIATOR_STATUSES: string[] = [REQUEST_STATUS.rejected, REQUEST_STATUS.returned];
// Сообщить инициатору, что заявку отклонили или вернули на доработку. Вызывать
// в onDecided с новым статусом заявки — при любом другом статусе ничего не делает.
//
// Поток тот же, что у notifyActiveStep: у него есть ветки «Отклонено» и «Возврат
// на доработку», в которых он пишет не согласующему строки, а автору заявки.
// notifyActiveStep до них не доходит — после такого решения активных строк нет.
//
// Строка — та, по которой решил ТЕКУЩИЙ пользователь: он только что принял
// решение. Искать «любую отклонённую» нельзя — на параллельном шаге второй
// участник может решить по уже отклонённой заявке, и в письме оказался бы чужой
// комментарий.
//
// Некритично, как и notifyActiveStep: ошибки глотаем.
export async function notifyInitiatorOfReturn(
context: WebPartContext,
approval: ApprovalListService,
requestId: number,
target: INotifyTarget,
requestStatus: string
): Promise<void> {
if (RETURNED_TO_INITIATOR_STATUSES.indexOf(requestStatus) === -1) {
return;
}
try {
const [rows, userId, initiator] = await Promise.all([
approval.getForRequest(requestId),
approval.getCurrentUserId(),
getCreatedBy(target.dataListName, requestId),
]);
const decidedRow = rows.filter((r) => r.status === requestStatus && r.approverId === userId)[0];
if (!decidedRow || !isRecipientAllowed(initiator?.email || '')) {
return;
}
await postToFlow(context, FLOW_NOTIFY_URL, {
RequestTypeId: target.requestTypeId,
RowID: decidedRow.id,
RequestID: requestId,
ListName: target.dataListName,
ApproveListName: target.approvalListName,
});
} catch {
// не критично — просто не отправили уведомление
}
}