feat: в чате заявки можно отметить тех, кому открыты «Все заявки» этого типа
Tests / frontend-test (pull_request) Successful in 2m4s

Они открывают любую заявку типа и пишут в её чат — теперь попадают и в
подсказку «@». Список берётся из админки approval-route
(request-type-access), ФИО — из кадрового справочника.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
gorbenko.pavel
2026-10-14 14:48:36 +05:00
parent 2bcfb9f6f4
commit fedb3b4c6b
4 changed files with 43 additions and 8 deletions
@@ -34,7 +34,7 @@ export {
getRequestTypeAccessEmails,
resolveRoute
} from './approvalRoute.service';
export { hasRequestTypeAccess, accessibleAllRequestsTypes } from './requestTypeAccess';
export { hasRequestTypeAccess, accessibleAllRequestsTypes, emailsForType } from './requestTypeAccess';
export { useAccessibleAllRequestsFormKeys } from './useAccessibleAllRequestsFormKeys';
export {
initListPartitions,
@@ -9,7 +9,12 @@ import { effectiveUserEmail } from '../impersonation';
// кешируется, чтобы временный сбой сети не «залип» на весь сеанс.
const cache = new Map<number, Promise<string[]>>();
function emailsForType(context: WebPartContext, requestTypeId: number): Promise<string[]> {
/**
* Кому выдан доступ видеть ВСЕ заявки этого типа (раздел «Все заявки»). Кешируется
* на сеанс. Нужен не только для проверки доступа: эти люди могут писать в чат
* любой заявки типа, значит, их можно и отмечать в нём (см. shared/chat).
*/
export function emailsForType(context: WebPartContext, requestTypeId: number): Promise<string[]> {
let pending = cache.get(requestTypeId);
if (!pending) {
pending = getRequestTypeAccessEmails(context, requestTypeId).catch((e) => {
@@ -7,7 +7,11 @@ import { IChatMessage } from './models/IChatMessage';
import { buildCommentText, hasAttachments, toChatMessages } from './chatMessages';
import { chatFilesFolderUrl, uploadChatFiles } from './chatFiles.service';
import { IChatParticipant } from './chatMentions';
import type { WebPartContext } from '@microsoft/sp-webpart-base';
import { approvalServiceFor } from '../approval/approvalServiceFactory';
import { getRequestTypeIdByPageUrl } from '../approval/approvalRoute.service';
import { emailsForType } from '../approval/requestTypeAccess';
import { getEmployeeOrganizations } from '../dictionary';
/**
* Чат по заявке ПОВЕРХ штатных комментариев к строке списка.
@@ -84,10 +88,34 @@ export interface IChatRequestInfo {
code: string;
title: string;
initiator: IChatParticipant;
/** Инициатор и все из маршрута (согласующие и исполнители), как есть — фильтрует buildParticipants. */
/**
* Инициатор, все из маршрута (согласующие и исполнители) и те, кому открыты
* «Все заявки» этого типа, — как есть, повторы убирает buildParticipants.
*/
people: IChatParticipant[];
}
/**
* Люди с доступом ко «Всем заявкам» этого типа (админка approval-route): они
* открывают любую заявку типа и пишут в её чат, значит, их можно и отметить.
* ФИО — из кадрового справочника, не нашлось — показываем почту. Любой сбой —
* просто без них: отметки участников маршрута от этого не страдают.
*/
async function allRequestsViewers(context: WebPartContext, formKey: RequestTypeId): Promise<IChatParticipant[]> {
try {
const typeId = await getRequestTypeIdByPageUrl(context, formKey);
const emails = await emailsForType(context, typeId);
return await Promise.all(
emails.map(async (email) => {
const records = await getEmployeeOrganizations(context, email).catch(() => []);
return { name: records[0]?.fio || email, email };
})
);
} catch {
return [];
}
}
/**
* Инициатор — автор строки списка данных: заявку создаёт он сам (подмена
* личности на автора записи не влияет, см. RequestChat). Участники — строки
@@ -95,18 +123,20 @@ export interface IChatRequestInfo {
* просто отметить можно только инициатора.
*/
export async function getChatRequestInfo(
context: WebPartContext,
formKey: RequestTypeId,
itemId: number,
listId: string | undefined
): Promise<IChatRequestInfo> {
const [item, matrix] = await Promise.all([
const [item, matrix, viewers] = await Promise.all([
resolveDataList(formKey, listId)
.items.getById(itemId)
.select('Title', 'RequestUniqueCode', 'Author/Title', 'Author/EMail')
.expand('Author')() as Promise<Record<string, unknown>>,
approvalServiceFor(formKey, listId)
.getMatrixForRequest(itemId)
.catch(() => [])
.catch(() => []),
allRequestsViewers(context, formKey)
]);
const author = item.Author as { Title?: string; EMail?: string } | undefined;
const initiator = { name: author?.Title || '', email: author?.EMail || '' };
@@ -114,6 +144,6 @@ export async function getChatRequestInfo(
code: (item.RequestUniqueCode as string) || '',
title: (item.Title as string) || '',
initiator,
people: [initiator, ...matrix.map((r) => ({ name: r.who, email: r.email }))]
people: [initiator, ...matrix.map((r) => ({ name: r.who, email: r.email })), ...viewers]
};
}
@@ -69,7 +69,7 @@ export function useRequestChat(
React.useEffect(() => {
let alive = true;
setInfo(undefined);
getChatRequestInfo(formKey, itemId, listId)
getChatRequestInfo(context, formKey, itemId, listId)
.then((loaded) => {
if (alive) setInfo(loaded);
})
@@ -77,7 +77,7 @@ export function useRequestChat(
return () => {
alive = false;
};
}, [formKey, itemId, listId]);
}, [context, formKey, itemId, listId]);
// В режиме проверки себя тоже можно отметить — отсекаем «себя» по пустому адресу.
const participants = React.useMemo(