9cacb6b271
Поток RequestsNotification умеет ветки «Отклонено» и «Возврат на доработку», но портал их не вызывал: notifyActiveStep ищет только активные строки, а после такого решения их нет — инициатор узнавал о решении, только открыв заявку. notifyInitiatorOfReturn шлёт в тот же поток RowID строки, по которой решил текущий пользователь; подключён в onDecided всех форм, кроме «Изменения условий ТД» (доработаем позже). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
168 lines
8.6 KiB
TypeScript
168 lines
8.6 KiB
TypeScript
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 {
|
||
// не критично — просто не отправили уведомление
|
||
}
|
||
}
|