From fedb3b4c6bc701a30fb585815fcca4ccb737957e Mon Sep 17 00:00:00 2001 From: "gorbenko.pavel" Date: Wed, 14 Oct 2026 14:48:36 +0500 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=B2=20=D1=87=D0=B0=D1=82=D0=B5=20?= =?UTF-8?q?=D0=B7=D0=B0=D1=8F=D0=B2=D0=BA=D0=B8=20=D0=BC=D0=BE=D0=B6=D0=BD?= =?UTF-8?q?=D0=BE=20=D0=BE=D1=82=D0=BC=D0=B5=D1=82=D0=B8=D1=82=D1=8C=20?= =?UTF-8?q?=D1=82=D0=B5=D1=85,=20=D0=BA=D0=BE=D0=BC=D1=83=20=D0=BE=D1=82?= =?UTF-8?q?=D0=BA=D1=80=D1=8B=D1=82=D1=8B=20=C2=AB=D0=92=D1=81=D0=B5=20?= =?UTF-8?q?=D0=B7=D0=B0=D1=8F=D0=B2=D0=BA=D0=B8=C2=BB=20=D1=8D=D1=82=D0=BE?= =?UTF-8?q?=D0=B3=D0=BE=20=D1=82=D0=B8=D0=BF=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Они открывают любую заявку типа и пишут в её чат — теперь попадают и в подсказку «@». Список берётся из админки approval-route (request-type-access), ФИО — из кадрового справочника. Co-Authored-By: Claude Opus 5.5 --- .../requestPortal/shared/approval/index.ts | 2 +- .../shared/approval/requestTypeAccess.ts | 7 +++- .../shared/chat/requestChat.service.ts | 38 +++++++++++++++++-- .../shared/chat/useRequestChat.ts | 4 +- 4 files changed, 43 insertions(+), 8 deletions(-) diff --git a/src/webparts/requestPortal/shared/approval/index.ts b/src/webparts/requestPortal/shared/approval/index.ts index 142ead0..57e6731 100644 --- a/src/webparts/requestPortal/shared/approval/index.ts +++ b/src/webparts/requestPortal/shared/approval/index.ts @@ -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, diff --git a/src/webparts/requestPortal/shared/approval/requestTypeAccess.ts b/src/webparts/requestPortal/shared/approval/requestTypeAccess.ts index 6719428..1a7a35d 100644 --- a/src/webparts/requestPortal/shared/approval/requestTypeAccess.ts +++ b/src/webparts/requestPortal/shared/approval/requestTypeAccess.ts @@ -9,7 +9,12 @@ import { effectiveUserEmail } from '../impersonation'; // кешируется, чтобы временный сбой сети не «залип» на весь сеанс. const cache = new Map>(); -function emailsForType(context: WebPartContext, requestTypeId: number): Promise { +/** + * Кому выдан доступ видеть ВСЕ заявки этого типа (раздел «Все заявки»). Кешируется + * на сеанс. Нужен не только для проверки доступа: эти люди могут писать в чат + * любой заявки типа, значит, их можно и отмечать в нём (см. shared/chat). + */ +export function emailsForType(context: WebPartContext, requestTypeId: number): Promise { let pending = cache.get(requestTypeId); if (!pending) { pending = getRequestTypeAccessEmails(context, requestTypeId).catch((e) => { diff --git a/src/webparts/requestPortal/shared/chat/requestChat.service.ts b/src/webparts/requestPortal/shared/chat/requestChat.service.ts index f330760..a67d6e9 100644 --- a/src/webparts/requestPortal/shared/chat/requestChat.service.ts +++ b/src/webparts/requestPortal/shared/chat/requestChat.service.ts @@ -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 { + 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 { - 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>, 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] }; } diff --git a/src/webparts/requestPortal/shared/chat/useRequestChat.ts b/src/webparts/requestPortal/shared/chat/useRequestChat.ts index 08a3f0f..03f4d87 100644 --- a/src/webparts/requestPortal/shared/chat/useRequestChat.ts +++ b/src/webparts/requestPortal/shared/chat/useRequestChat.ts @@ -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(