Files
request-portal/src/webparts/requestPortal/shared/approval/notify.service.ts
T
Тимур Городецкий 9cacb6b271 feat: письмо инициатору при отклонении и возврате заявки
Поток RequestsNotification умеет ветки «Отклонено» и «Возврат на доработку»,
но портал их не вызывал: notifyActiveStep ищет только активные строки, а после
такого решения их нет — инициатор узнавал о решении, только открыв заявку.

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

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-28 16:50:31 +05:00

168 lines
8.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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';
// Данные формы для тела запроса к потоку (постоянны для формы).
export interface INotifyTarget {
requestTypeId: number; // RequestTypeId (id типа заявки во внешнем API маршрутов)
dataListName: string; // ListName (список данных заявки)
approvalListName: string; // ApproveListName (список согласования)
}
// POST в поток Power Automate (Entra-триггер) через AadHttpClient — обычный fetch → 401,
// AadHttpClient даёт токен Flow Service (и снимает вопрос CORS). Общий для всех
// потоков оповещения портала (согласование, чат заявки — shared/chat/chatNotify.service.ts).
export async function postToFlow(context: WebPartContext, flowUrl: string, body: object): Promise<void> {
if (!flowUrl) {
return;
}
const client = await context.aadHttpClientFactory.getClient(FLOW_RESOURCE);
const res = await client.post(flowUrl, AadHttpClient.configurations.v1, {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!res.ok) {
throw new Error(`Поток оповещения: ${res.status} ${res.statusText}`);
}
}
// Разрешена ли отправка на согласующего активной строки — по свойствам веб-части
// notificationsEnabled / notifyOnlyTo (см. setNotificationSettings в config.ts).
//
// Ограничение стоит ЗДЕСЬ, на стороне портала, а не в потоке Power Automate,
// потому что адресата поток определяет САМ по RowID — тело запроса на выбор
// получателя не влияет. Значит, единственный способ не отправить письмо, не
// трогая поток, — не дёргать его.
//
// При заданном notifyOnlyTo неизвестный адрес (пользователь не найден, поле
// пустое, запрос упал) — НЕ отправляем: сомнение трактуется в пользу «письмо
// живому человеку лучше не слать».
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;
if (email.toLowerCase() === onlyTo.toLowerCase()) return true;
console.warn(
`[Оповещение] Рассылка ограничена одним адресом: письмо для «${email || 'адрес неизвестен'}» не отправлено ` +
`(разрешён только ${onlyTo})`
);
return false;
}
// Одна строка маршрута → один вызов потока. Ошибку глотаем здесь, по строке:
// сбой письма одному участнику параллельного шага не должен лишать письма остальных.
async function notifyRow(
context: WebPartContext,
approval: ApprovalListService,
requestId: number,
target: INotifyTarget,
row: IApprovalRow
): Promise<void> {
try {
if (!(await isNotifyAllowed(approval, row.approverId))) {
return;
}
await postToFlow(context, FLOW_NOTIFY_URL, {
RequestTypeId: target.requestTypeId,
RowID: row.id,
RequestID: requestId,
ListName: target.dataListName,
ApproveListName: target.approvalListName,
});
} catch {
// не критично — просто не отправили уведомление
}
}
// Находит АКТИВНЫЙ шаг маршрута (На согласовании / На исполнении) и уведомляет его.
// Вызывать после отправки заявки и после каждого положительного решения. Поток по
// переданным полям сам находит согласующего/исполнителя и шлёт уведомление.
// Уведомление некритично — ошибки глотаем, чтобы не ронять основной сценарий.
//
// Параллельный шаг (несколько активных строк с одним StepOrder) оповещается
// целиком — каждому участнику свой вызов потока (см. pickRowsToNotify).
//
// rowId — уведомить именно эту активную строку, а не весь шаг: при
// замене/удалении исполнителя на параллельном шаге активных строк несколько, и
// письмо должно уйти новому человеку, а не тому, кто уже его получил.
export async function notifyActiveStep(
context: WebPartContext,
approval: ApprovalListService,
requestId: number,
target: INotifyTarget,
rowId?: number
): Promise<void> {
let rows: IApprovalRow[];
try {
rows = await approval.getForRequest(requestId);
} catch {
return; // не критично — просто не отправили уведомление
}
await Promise.all(
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 {
// не критично — просто не отправили уведомление
}
}