diff --git a/src/webparts/requestPortal/forms/BusinessTrip/BusinessTripView.tsx b/src/webparts/requestPortal/forms/BusinessTrip/BusinessTripView.tsx index 64c4acd..701b500 100644 --- a/src/webparts/requestPortal/forms/BusinessTrip/BusinessTripView.tsx +++ b/src/webparts/requestPortal/forms/BusinessTrip/BusinessTripView.tsx @@ -10,6 +10,7 @@ import { StatusBadge, ApprovalHistory, notifyActiveStep, + notifyInitiatorOfReturn, useRequestTypeId, usePhasedRoute } from '../../shared/approval'; @@ -108,14 +109,17 @@ const BusinessTripView: React.FC = ({ 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 = ({ context, itemId, f onDecided={(newStatus) => { setStatus(newStatus); setDecisionSeq((n) => n + 1); - notify(); + notify(newStatus); }} onDelegated={() => { setDecisionSeq((n) => n + 1); diff --git a/src/webparts/requestPortal/forms/Clearance/ClearanceView.tsx b/src/webparts/requestPortal/forms/Clearance/ClearanceView.tsx index 72dd8ee..bfbb9c0 100644 --- a/src/webparts/requestPortal/forms/Clearance/ClearanceView.tsx +++ b/src/webparts/requestPortal/forms/Clearance/ClearanceView.tsx @@ -11,6 +11,7 @@ import { ApprovalHistory, useMatrixEditor, notifyActiveStep, + notifyInitiatorOfReturn, useRequestTypeId, APPROVAL_PHASE } from '../../shared/approval'; @@ -255,16 +256,19 @@ const ClearanceView: React.FC = ({ 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 = ({ context, itemId, formKey onDecided={(newStatus) => { setStatus(newStatus); setDecisionSeq((n) => n + 1); - notify(); + notify(newStatus); }} onDelegated={() => { setDecisionSeq((n) => n + 1); diff --git a/src/webparts/requestPortal/forms/ConcreteRequest/ConcreteRequestView.tsx b/src/webparts/requestPortal/forms/ConcreteRequest/ConcreteRequestView.tsx index f6d8c91..d690bf7 100644 --- a/src/webparts/requestPortal/forms/ConcreteRequest/ConcreteRequestView.tsx +++ b/src/webparts/requestPortal/forms/ConcreteRequest/ConcreteRequestView.tsx @@ -12,6 +12,7 @@ import { ApprovalHistory, useMatrixEditor, notifyActiveStep, + notifyInitiatorOfReturn, useRequestTypeId, useViewExecutors, approvalPhaseRows @@ -118,16 +119,19 @@ const ConcreteRequestView: React.FC = ({ 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 = ({ context, ite onDecided={(newStatus) => { setStatus(newStatus); setDecisionSeq((n) => n + 1); - notify(); + notify(newStatus); }} onDelegated={() => { setDecisionSeq((n) => n + 1); diff --git a/src/webparts/requestPortal/forms/Dismissal/DismissalView.tsx b/src/webparts/requestPortal/forms/Dismissal/DismissalView.tsx index 4bfe0b6..079ae6e 100644 --- a/src/webparts/requestPortal/forms/Dismissal/DismissalView.tsx +++ b/src/webparts/requestPortal/forms/Dismissal/DismissalView.tsx @@ -12,6 +12,7 @@ import { ApprovalHistory, useMatrixEditor, notifyActiveStep, + notifyInitiatorOfReturn, useRequestTypeId, REQUEST_STATUS, approvalPhaseRows, @@ -218,16 +219,19 @@ const DismissalView: React.FC = ({ 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 = ({ context, itemId, formKey onDecided={(newStatus) => { setStatus(newStatus); setDecisionSeq((n) => n + 1); - notify(); + notify(newStatus); // «Согласовано» = маршрут заявления пройден целиком (фазы исполнения у // него нет, см. README формы) — значит увольнение подтверждено и пора // заводить обходной лист. Промис ВОЗВРАЩАЕМ: пока он не разрешится, diff --git a/src/webparts/requestPortal/forms/EmployeeTransfer/EmployeeTransferView.tsx b/src/webparts/requestPortal/forms/EmployeeTransfer/EmployeeTransferView.tsx index e47881f..c8b5a4d 100644 --- a/src/webparts/requestPortal/forms/EmployeeTransfer/EmployeeTransferView.tsx +++ b/src/webparts/requestPortal/forms/EmployeeTransfer/EmployeeTransferView.tsx @@ -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 = ({ 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 = ({ context, i onDecided={(newStatus) => { setStatus(newStatus); setDecisionSeq((n) => n + 1); - notify(); + notify(newStatus); }} onDelegated={() => { setDecisionSeq((n) => n + 1); diff --git a/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireView.tsx b/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireView.tsx index 98721ba..d735734 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireView.tsx +++ b/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireView.tsx @@ -24,6 +24,7 @@ import { ApprovalHistory, useMatrixEditor, notifyActiveStep, + notifyInitiatorOfReturn, useRequestTypeId, APPROVAL_PHASE, isApprovedStatus, @@ -283,13 +284,16 @@ const RecruitingHireView: React.FC = ({ 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 = ({ context, itemI setStatus(newStatus); setDecisionSeq((n) => n + 1); // Очередь сдвинулась — уведомляем следующий активный шаг. - notify(); + notify(newStatus); }} onDelegated={() => { // Шаг переназначен: активной стала строка делегата — обновляем diff --git a/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionView.tsx b/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionView.tsx index 3048641..d8c0ba6 100644 --- a/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionView.tsx +++ b/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionView.tsx @@ -22,6 +22,7 @@ import { ApprovalHistory, useMatrixEditor, notifyActiveStep, + notifyInitiatorOfReturn, useRequestTypeId, isApprovedStatus, resolveApproverOrgInfo, @@ -204,20 +205,25 @@ const RecruitingSelectionView: React.FC = ({ 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 = ({ cont setStatus(newStatus); setDecisionSeq((n) => n + 1); notify(); + notifyInitiator(newStatus); }} onDelegated={() => { setDecisionSeq((n) => n + 1); diff --git a/src/webparts/requestPortal/forms/Referral/ReferralView.tsx b/src/webparts/requestPortal/forms/Referral/ReferralView.tsx index 3665cf1..124c649 100644 --- a/src/webparts/requestPortal/forms/Referral/ReferralView.tsx +++ b/src/webparts/requestPortal/forms/Referral/ReferralView.tsx @@ -21,6 +21,7 @@ import { ApprovalHistory, useMatrixEditor, notifyActiveStep, + notifyInitiatorOfReturn, useRequestTypeId, APPROVAL_PHASE, isApprovedStatus, @@ -169,16 +170,19 @@ const ReferralView: React.FC = ({ 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 = ({ context, itemId, formKey, onDecided={(newStatus) => { setStatus(newStatus); setDecisionSeq((n) => n + 1); - notify(); + notify(newStatus); }} onDelegated={() => { setDecisionSeq((n) => n + 1); diff --git a/src/webparts/requestPortal/forms/ServiceRequest/ServiceRequestView.tsx b/src/webparts/requestPortal/forms/ServiceRequest/ServiceRequestView.tsx index 120d91e..2aac9bd 100644 --- a/src/webparts/requestPortal/forms/ServiceRequest/ServiceRequestView.tsx +++ b/src/webparts/requestPortal/forms/ServiceRequest/ServiceRequestView.tsx @@ -24,6 +24,7 @@ import { ExecutorsMatrix, approvalPhaseRows, notifyActiveStep, + notifyInitiatorOfReturn, useRequestTypeId, isEditableStatus } from '../../shared/approval'; @@ -141,13 +142,16 @@ const ServiceRequestView: React.FC = ({ 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 = ({ context, itemI onDecided={(newStatus) => { setStatus(newStatus); setDecisionSeq((n) => n + 1); - notify(); + notify(newStatus); }} onDelegated={() => { setDecisionSeq((n) => n + 1); diff --git a/src/webparts/requestPortal/forms/SkudExplanation/SkudExplanationView.tsx b/src/webparts/requestPortal/forms/SkudExplanation/SkudExplanationView.tsx index d903c44..81be874 100644 --- a/src/webparts/requestPortal/forms/SkudExplanation/SkudExplanationView.tsx +++ b/src/webparts/requestPortal/forms/SkudExplanation/SkudExplanationView.tsx @@ -9,6 +9,7 @@ import { StatusBadge, ApprovalHistory, notifyActiveStep, + notifyInitiatorOfReturn, useRequestTypeId, usePhasedRoute } from '../../shared/approval'; @@ -110,14 +111,17 @@ const SkudExplanationView: React.FC = ({ 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 = ({ context, ite onDecided={(newStatus) => { setStatus(newStatus); setDecisionSeq((n) => n + 1); - notify(); + notify(newStatus); }} onDelegated={() => { setDecisionSeq((n) => n + 1); diff --git a/src/webparts/requestPortal/forms/Ticket1C/Ticket1CView.tsx b/src/webparts/requestPortal/forms/Ticket1C/Ticket1CView.tsx index 57ce9fa..57cdcb9 100644 --- a/src/webparts/requestPortal/forms/Ticket1C/Ticket1CView.tsx +++ b/src/webparts/requestPortal/forms/Ticket1C/Ticket1CView.tsx @@ -23,6 +23,7 @@ import { ApprovalHistory, useMatrixEditor, notifyActiveStep, + notifyInitiatorOfReturn, useRequestTypeId, isEditableStatus, APPROVAL_PHASE @@ -102,20 +103,25 @@ const Ticket1CView: React.FC = ({ 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 = ({ context, itemId, formKey, setDecisionSeq((n) => n + 1); // Очередь сдвинулась — уведомляем следующий активный шаг. notify(); + notifyInitiator(newStatus); // Дошла до исполнения / завершилась / отклонена — карточка в Planner // (повторно не создаётся, см. plannerHandoff.ts). diff --git a/src/webparts/requestPortal/forms/TmcTransfer/TmcTransferView.tsx b/src/webparts/requestPortal/forms/TmcTransfer/TmcTransferView.tsx index 56b2ba4..5c79a2e 100644 --- a/src/webparts/requestPortal/forms/TmcTransfer/TmcTransferView.tsx +++ b/src/webparts/requestPortal/forms/TmcTransfer/TmcTransferView.tsx @@ -24,6 +24,7 @@ import { ApprovalHistory, useMatrixEditor, notifyActiveStep, + notifyInitiatorOfReturn, useRequestTypeId, isEditableStatus, APPROVAL_PHASE @@ -144,13 +145,16 @@ const TmcTransferView: React.FC = ({ 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 = ({ context, itemId, for onDecided={(newStatus) => { setStatus(newStatus); setDecisionSeq((n) => n + 1); - notify(); + notify(newStatus); }} onDelegated={() => { setDecisionSeq((n) => n + 1); diff --git a/src/webparts/requestPortal/forms/UnpaidLeave/UnpaidLeaveView.tsx b/src/webparts/requestPortal/forms/UnpaidLeave/UnpaidLeaveView.tsx index 27ca2b4..0ff119b 100644 --- a/src/webparts/requestPortal/forms/UnpaidLeave/UnpaidLeaveView.tsx +++ b/src/webparts/requestPortal/forms/UnpaidLeave/UnpaidLeaveView.tsx @@ -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 = ({ 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 = ({ context, itemId, for onDecided={(newStatus) => { setStatus(newStatus); setDecisionSeq((n) => n + 1); - notify(); + notify(newStatus); }} onDelegated={() => { setDecisionSeq((n) => n + 1); diff --git a/src/webparts/requestPortal/forms/Vacation/VacationView.tsx b/src/webparts/requestPortal/forms/Vacation/VacationView.tsx index e8f377f..244d8dc 100644 --- a/src/webparts/requestPortal/forms/Vacation/VacationView.tsx +++ b/src/webparts/requestPortal/forms/Vacation/VacationView.tsx @@ -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 = ({ 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 = ({ context, itemId, formKey, onDecided={(newStatus) => { setStatus(newStatus); setDecisionSeq((n) => n + 1); - notify(); + notify(newStatus); return VACATION_EDO_SIGNING_ENABLED && sentBeforeDecision.current ?'Документы отправлены на подписание в ЭДО.' : undefined; }} onDelegated={() => { diff --git a/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupView.tsx b/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupView.tsx index 97e56ef..db6ed75 100644 --- a/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupView.tsx +++ b/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupView.tsx @@ -23,6 +23,7 @@ import { ApprovalHistory, useMatrixEditor, notifyActiveStep, + notifyInitiatorOfReturn, useRequestTypeId, isEditableStatus, APPROVAL_PHASE, @@ -224,13 +225,16 @@ const WorkplaceSetupView: React.FC = ({ 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 = ({ context, itemI setStatus(newStatus); setDecisionSeq((n) => n + 1); // Очередь сдвинулась — уведомляем следующий активный шаг. - notify(); + notify(newStatus); }} onDelegated={() => { // Шаг переназначен: активной стала строка делегата — обновляем diff --git a/src/webparts/requestPortal/shared/approval/index.ts b/src/webparts/requestPortal/shared/approval/index.ts index 57e6731..d582573 100644 --- a/src/webparts/requestPortal/shared/approval/index.ts +++ b/src/webparts/requestPortal/shared/approval/index.ts @@ -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'; diff --git a/src/webparts/requestPortal/shared/approval/notify.service.ts b/src/webparts/requestPortal/shared/approval/notify.service.ts index 0955e2e..c0fc8d4 100644 --- a/src/webparts/requestPortal/shared/approval/notify.service.ts +++ b/src/webparts/requestPortal/shared/approval/notify.service.ts @@ -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 { 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 { + 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 { + // не критично — просто не отправили уведомление + } +}