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