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, getRequestTypeAccessEmails,
resolveRoute resolveRoute
} from './approvalRoute.service'; } from './approvalRoute.service';
export { hasRequestTypeAccess, accessibleAllRequestsTypes } from './requestTypeAccess'; export { hasRequestTypeAccess, accessibleAllRequestsTypes, emailsForType } from './requestTypeAccess';
export { useAccessibleAllRequestsFormKeys } from './useAccessibleAllRequestsFormKeys'; export { useAccessibleAllRequestsFormKeys } from './useAccessibleAllRequestsFormKeys';
export { export {
initListPartitions, initListPartitions,
@@ -9,7 +9,12 @@ import { effectiveUserEmail } from '../impersonation';
// кешируется, чтобы временный сбой сети не «залип» на весь сеанс. // кешируется, чтобы временный сбой сети не «залип» на весь сеанс.
const cache = new Map<number, Promise<string[]>>(); 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); let pending = cache.get(requestTypeId);
if (!pending) { if (!pending) {
pending = getRequestTypeAccessEmails(context, requestTypeId).catch((e) => { pending = getRequestTypeAccessEmails(context, requestTypeId).catch((e) => {
@@ -7,7 +7,11 @@ import { IChatMessage } from './models/IChatMessage';
import { buildCommentText, hasAttachments, toChatMessages } from './chatMessages'; import { buildCommentText, hasAttachments, toChatMessages } from './chatMessages';
import { chatFilesFolderUrl, uploadChatFiles } from './chatFiles.service'; import { chatFilesFolderUrl, uploadChatFiles } from './chatFiles.service';
import { IChatParticipant } from './chatMentions'; import { IChatParticipant } from './chatMentions';
import type { WebPartContext } from '@microsoft/sp-webpart-base';
import { approvalServiceFor } from '../approval/approvalServiceFactory'; 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; code: string;
title: string; title: string;
initiator: IChatParticipant; initiator: IChatParticipant;
/** Инициатор и все из маршрута (согласующие и исполнители), как есть — фильтрует buildParticipants. */ /**
* Инициатор, все из маршрута (согласующие и исполнители) и те, кому открыты
* «Все заявки» этого типа, — как есть, повторы убирает buildParticipants.
*/
people: IChatParticipant[]; 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). Участники — строки * личности на автора записи не влияет, см. RequestChat). Участники — строки
@@ -95,18 +123,20 @@ export interface IChatRequestInfo {
* просто отметить можно только инициатора. * просто отметить можно только инициатора.
*/ */
export async function getChatRequestInfo( export async function getChatRequestInfo(
context: WebPartContext,
formKey: RequestTypeId, formKey: RequestTypeId,
itemId: number, itemId: number,
listId: string | undefined listId: string | undefined
): Promise<IChatRequestInfo> { ): Promise<IChatRequestInfo> {
const [item, matrix] = await Promise.all([ const [item, matrix, viewers] = await Promise.all([
resolveDataList(formKey, listId) resolveDataList(formKey, listId)
.items.getById(itemId) .items.getById(itemId)
.select('Title', 'RequestUniqueCode', 'Author/Title', 'Author/EMail') .select('Title', 'RequestUniqueCode', 'Author/Title', 'Author/EMail')
.expand('Author')() as Promise<Record<string, unknown>>, .expand('Author')() as Promise<Record<string, unknown>>,
approvalServiceFor(formKey, listId) approvalServiceFor(formKey, listId)
.getMatrixForRequest(itemId) .getMatrixForRequest(itemId)
.catch(() => []) .catch(() => []),
allRequestsViewers(context, formKey)
]); ]);
const author = item.Author as { Title?: string; EMail?: string } | undefined; const author = item.Author as { Title?: string; EMail?: string } | undefined;
const initiator = { name: author?.Title || '', email: author?.EMail || '' }; const initiator = { name: author?.Title || '', email: author?.EMail || '' };
@@ -114,6 +144,6 @@ export async function getChatRequestInfo(
code: (item.RequestUniqueCode as string) || '', code: (item.RequestUniqueCode as string) || '',
title: (item.Title as string) || '', title: (item.Title as string) || '',
initiator, 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(() => { React.useEffect(() => {
let alive = true; let alive = true;
setInfo(undefined); setInfo(undefined);
getChatRequestInfo(formKey, itemId, listId) getChatRequestInfo(context, formKey, itemId, listId)
.then((loaded) => { .then((loaded) => {
if (alive) setInfo(loaded); if (alive) setInfo(loaded);
}) })
@@ -77,7 +77,7 @@ export function useRequestChat(
return () => { return () => {
alive = false; alive = false;
}; };
}, [formKey, itemId, listId]); }, [context, formKey, itemId, listId]);
// В режиме проверки себя тоже можно отметить — отсекаем «себя» по пустому адресу. // В режиме проверки себя тоже можно отметить — отсекаем «себя» по пустому адресу.
const participants = React.useMemo( const participants = React.useMemo(