fix: оповещения параллельного шага — всем участникам и без дублей

Письмо уходило только первому из параллельных согласующих, а после решения
одного из них остальные получали повторное. Теперь шаг оповещается целиком
при старте и не оповещается снова, если кто-то из его участников уже решил.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Тимур Городецкий
2026-10-08 16:07:59 +05:00
parent 8fc9776895
commit 474a663da6
3 changed files with 152 additions and 22 deletions
@@ -2,7 +2,8 @@ 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 { REQUEST_STATUS } from './models/statuses';
import { IApprovalRow } from './models/IApprovalRow';
import { pickRowsToNotify } from './notifyTargets';
// Данные формы для тела запроса к потоку (постоянны для формы).
export interface INotifyTarget {
@@ -54,12 +55,40 @@ async function isNotifyAllowed(approval: ApprovalListService, approverId: number
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 postNotify(context, {
RequestTypeId: target.requestTypeId,
RowID: row.id,
RequestID: requestId,
ListName: target.dataListName,
ApproveListName: target.approvalListName,
});
} catch {
// не критично — просто не отправили уведомление
}
}
// Находит АКТИВНЫЙ шаг маршрута (На согласовании / На исполнении) и уведомляет его.
// Вызывать после отправки заявки и после каждого положительного решения. Поток по
// переданным полям сам находит согласующего/исполнителя и шлёт уведомление.
// Уведомление некритично — ошибки глотаем, чтобы не ронять основной сценарий.
//
// rowId — уведомить именно эту активную строку, а не первую попавшуюся: при
// Параллельный шаг (несколько активных строк с одним StepOrder) оповещается
// целиком — каждому участнику свой вызов потока (см. pickRowsToNotify).
//
// rowId — уведомить именно эту активную строку, а не весь шаг: при
// замене/удалении исполнителя на параллельном шаге активных строк несколько, и
// письмо должно уйти новому человеку, а не тому, кто уже его получил.
export async function notifyActiveStep(
@@ -69,27 +98,13 @@ export async function notifyActiveStep(
target: INotifyTarget,
rowId?: number
): Promise<void> {
let rows: IApprovalRow[];
try {
const rows = await approval.getForRequest(requestId);
const active = rows.filter(
(r) =>
(r.status === REQUEST_STATUS.onApproval || r.status === REQUEST_STATUS.onExecution) &&
(rowId === undefined || r.id === rowId)
)[0];
if (!active) {
return;
}
if (!(await isNotifyAllowed(approval, active.approverId))) {
return;
}
await postNotify(context, {
RequestTypeId: target.requestTypeId,
RowID: active.id,
RequestID: requestId,
ListName: target.dataListName,
ApproveListName: target.approvalListName,
});
rows = await approval.getForRequest(requestId);
} catch {
// не критично — просто не отправили уведомление
return; // не критично — просто не отправили уведомление
}
await Promise.all(
pickRowsToNotify(rows, rowId).map((row) => notifyRow(context, approval, requestId, target, row))
);
}