Files
request-portal/src/webparts/requestPortal/shared/approval/notify.service.ts
T
gorbenko.pavel 5d2abaf430
Tests / frontend-test (push) Successful in 2m1s
Чат заявки: отметки «@» и оповещения через поток (#26)
## Что сделано

**Чат заявки: отметки и оповещения**
- В поле ввода «@» открывает подсказку с участниками заявки: инициатор, согласующие и исполнители из маршрута, а также те, кому открыты «Все заявки» этого типа (список из request-type-access).
- Отметка — обычный текст «@ФИО», без штатного упоминания SharePoint, чтобы не уходило его письмо.
- После отправки сообщения портал вызывает поток Power Automate (`FLOW_CHAT_NOTIFY_URL`): отмеченным — «Вас упомянули», инициатору — «Новое сообщение по вашей заявке». Сбой потока не мешает отправке.
- В письмо добавляется строка «Прикреплены файлы: …», чтобы сообщение из одного файла не приходило пустым.
- Свойство веб-части «Чат: уведомлять о своих сообщениях (для теста)», по умолчанию выключено: письма и о своих сообщениях, можно отметить себя.
- Когда пишет сам инициатор с отметками, в `initiatorEmail` уходит первый отмеченный — поток шлёт инициатору всегда, если его нет среди отмеченных, и иначе он получил бы письмо о своём же сообщении.

**Чат включён для:** Обходной лист, заявка на подбор, заявка на приём, заявка в отдел 1С.

**Исправление:** редактор маршрута может удалять исполнителей в Обходном листе, Командировке и заявке в 1С (не передавалось `canRemoveRow`).

**Прочее:** убраны 4 предупреждения линтера (`no-new-null`, `no-script-url`).

## Не проверено на тенанте
- Ответ потока 200 через AadHttpClient и доставка писем.
- Права согласующих на комментарии и на запись в библиотеку «Файлы чата».
- Папки в «Файлы чата» наследуют права библиотеки, а не закрыты на заявку — для подбора и приёма это важно решить до выкладки.
- Если поток вставляет `messageText` в HTML-письмо, перенос строки перед «Прикреплены файлы» может не отобразиться.

---------

Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz>
Reviewed-on: http://git.tnsu.kz/timur.gorodetskiy/request-portal/pulls/26
2026-10-14 10:05:45 +00:00

112 lines
5.8 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 { ApprovalListService } from './approvalList.service';
import { IApprovalRow } from './models/IApprovalRow';
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;
const onlyTo = getNotifyOnlyTo();
if (!onlyTo) return true;
const email = await approval.resolveUserEmail(approverId);
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))
);
}