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
@@ -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 {
// не критично — просто не отправили уведомление
}
}