Feature/request type access all requests (#22)
Tests / frontend-test (push) Successful in 1m24s

Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz>
Reviewed-on: http://git.tnsu.kz/timur.gorodetskiy/request-portal/pulls/22
This commit was merged in pull request #22.
This commit is contained in:
2026-09-25 11:48:09 +00:00
parent ebc74dc46d
commit 98be3980cb
15 changed files with 590 additions and 56 deletions
@@ -147,6 +147,28 @@ export async function getRequestTypeIdByPageUrl(context: WebPartContext, pageUrl
return found.id;
}
// GET /api/request-type-access/{requestTypeId}/emails — email'ы, кому в
// админке approval-route выдан доступ видеть ВСЕ заявки этого типа (не только
// свои). Тот же эндпоинт дёргает и Power Automate — здесь просто ещё один
// потребитель.
export async function getRequestTypeAccessEmails(
context: WebPartContext,
requestTypeId: number
): Promise<string[]> {
return withNetworkRetry(async () => {
const c = await client(context);
const response: HttpClientResponse = await c.get(
`${getApprovalApiBaseUrl()}/api/request-type-access/${requestTypeId}/emails`,
AadHttpClient.configurations.v1
);
if (!response.ok) {
throw new Error(`request-type-access/${requestTypeId}/emails: ${response.status} ${response.statusText}`);
}
const body: IApiResponse<string[]> = await response.json();
return body.data ?? [];
});
}
// POST /api/template-routes/resolve — планируемый маршрут для типа заявки и
// пользователя. Бэкенд сам подставляет исполнителей шага (executorEmail/position)
// по ролям исходя из userEmail. Параметр status выбирает ФАЗУ маршрута:
@@ -31,8 +31,11 @@ export {
getRequestTypeIdByPageUrl,
getListPartitions,
getCurrentListPartition,
getRequestTypeAccessEmails,
resolveRoute
} from './approvalRoute.service';
export { hasRequestTypeAccess, accessibleAllRequestsTypes } from './requestTypeAccess';
export { useAccessibleAllRequestsFormKeys } from './useAccessibleAllRequestsFormKeys';
export {
initListPartitions,
listPartitionsLoaded,
@@ -0,0 +1,48 @@
import { WebPartContext } from '@microsoft/sp-webpart-base';
import { getRequestTypeAccessEmails } from './approvalRoute.service';
import { IRequestType } from './models/IApprovalRouteApi';
import { effectiveUserEmail } from '../impersonation';
// Кеш на уровне МОДУЛЯ, по requestTypeId — тот же приём, что и у
// requestTypesPromise в approvalRoute.service.ts: реестр доступа меняется
// только из админки approval-route, а не в течение сеанса портала. Ошибка НЕ
// кешируется, чтобы временный сбой сети не «залип» на весь сеанс.
const cache = new Map<number, Promise<string[]>>();
function emailsForType(context: WebPartContext, requestTypeId: number): Promise<string[]> {
let pending = cache.get(requestTypeId);
if (!pending) {
pending = getRequestTypeAccessEmails(context, requestTypeId).catch((e) => {
cache.delete(requestTypeId);
throw e;
});
cache.set(requestTypeId, pending);
}
return pending;
}
/**
* Есть ли у ТЕКУЩЕГО пользователя (effectiveUserEmail — учитывает отладочную
* подмену личности) доступ видеть все заявки этого типа. Ошибка запроса — не
* доступ, а не исключение наверх: как и hasReportsAccess, тихий сбой сети не
* должен ронять раздел, который его вызывает.
*/
export async function hasRequestTypeAccess(context: WebPartContext, requestTypeId: number): Promise<boolean> {
const email = effectiveUserEmail(context).trim().toLowerCase();
if (!email) return false;
try {
const emails = await emailsForType(context, requestTypeId);
return emails.some((e) => e.trim().toLowerCase() === email);
} catch {
return false;
}
}
/** Из переданных типов — те, чьи заявки текущему пользователю разрешено видеть целиком. */
export async function accessibleAllRequestsTypes(
context: WebPartContext,
types: IRequestType[]
): Promise<IRequestType[]> {
const flags = await Promise.all(types.map((t) => hasRequestTypeAccess(context, t.id)));
return types.filter((_, i) => flags[i]);
}
@@ -0,0 +1,35 @@
import { useEffect, useState } from 'react';
import type { WebPartContext } from '@microsoft/sp-webpart-base';
import { getRequestTypesCached } from './approvalRoute.service';
import { accessibleAllRequestsTypes } from './requestTypeAccess';
/**
* pageUrl (formKey формы) → числовой RequestTypeId approval-route, только для
* типов, чьи заявки текущему пользователю разрешено видеть целиком (см.
* requestTypeAccess.ts). Один поход по всем типам разом — плитки и кнопка на
* открытой форме (SubmitFeature) решают по нему, показывать ли переход в
* «Все заявки», не дожидаясь резолва конкретного типа.
*
* undefined — ответ ещё не пришёл (плитки без кнопки, а не с ошибочно пустой).
*/
export function useAccessibleAllRequestsFormKeys(context: WebPartContext): Map<string, number> | undefined {
const [map, setMap] = useState<Map<string, number> | undefined>(undefined);
useEffect(() => {
let cancelled = false;
getRequestTypesCached(context)
.then((types) => accessibleAllRequestsTypes(context, types))
.then((accessible) => {
if (cancelled) return;
setMap(new Map(accessible.map((t) => [t.pageUrl, t.id])));
})
.catch(() => {
if (!cancelled) setMap(new Map());
});
return () => {
cancelled = true;
};
}, [context]);
return map;
}