Files
request-portal/src/webparts/requestPortal/shared/approval/approvalRoute.service.ts
T
2026-08-21 10:01:33 +00:00

168 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { AadHttpClient, HttpClientResponse } from '@microsoft/sp-http';
import { WebPartContext } from '@microsoft/sp-webpart-base';
import { APPROVAL_API_CLIENT_ID, getApprovalApiBaseUrl } from '../config';
import {
ApprovalStatus,
ICurrentListPartition,
IApiResponse,
IRequestType,
IRequestTypeListSeries,
IResolvedRoute,
IResolveFieldValue
} from './models/IApprovalRouteApi';
// Все обращения к внешнему approval-route API — только здесь.
// Клиент авторизуется через AadHttpClient (SSO по текущему пользователю).
async function client(context: WebPartContext): Promise<AadHttpClient> {
return context.aadHttpClientFactory.getClient(APPROVAL_API_CLIENT_ID);
}
// GET /api/request-types — список типов заявок (id + имя). Пригодится, чтобы
// сопоставить форму с RequestTypeId, если он неизвестен заранее.
export async function getRequestTypes(context: WebPartContext): Promise<IRequestType[]> {
const c = await client(context);
const response: HttpClientResponse = await c.get(
`${getApprovalApiBaseUrl()}/api/request-types`,
AadHttpClient.configurations.v1
);
if (!response.ok) {
throw new Error(`request-types: ${response.status} ${response.statusText}`);
}
const body: IApiResponse<IRequestType[]> = await response.json();
return body.data ?? [];
}
/**
* GET /api/request-types/lists — серии партиций сразу по ВСЕМ типам заявок.
*
* Именно bulk-эндпоинт, а не по одному на тип: партиции нужны и для записи (куда
* писать), и для поиска (по каким спискам искать), причём поиск идёт сразу по
* всем типам — запрашивать серию отдельно на каждый тип означало бы N запросов
* на каждое открытие «Моих заявок».
*
* Типы без настроенного списка SharePoint в ответ не попадают.
*/
export async function getListPartitions(context: WebPartContext): Promise<IRequestTypeListSeries[]> {
const c = await client(context);
const response: HttpClientResponse = await c.get(
`${getApprovalApiBaseUrl()}/api/request-types/lists`,
AadHttpClient.configurations.v1
);
if (!response.ok) {
throw new Error(`request-types/lists: ${response.status} ${response.statusText}`);
}
const body: IApiResponse<IRequestTypeListSeries[]> = await response.json();
return body.data ?? [];
}
/**
* GET /api/request-types/{id}/lists/current — партиция, в которую нужно писать
* НОВУЮ заявку этого типа. Вызывать перед КАЖДЫМ созданием, не кешировать: этот
* запрос заодно проверяет порог элементов на бэкенде и ставит в очередь
* создание следующей партиции при достижении — без него архивация никогда не
* запустится, сколько бы заявок ни создали (bulk-эндпоинт lists, который
* портал и так грузит при onInit, только читает реестр и порог не проверяет).
*/
export async function getCurrentListPartition(
context: WebPartContext,
requestTypeId: number
): Promise<ICurrentListPartition> {
const c = await client(context);
const response: HttpClientResponse = await c.get(
`${getApprovalApiBaseUrl()}/api/request-types/${requestTypeId}/lists/current`,
AadHttpClient.configurations.v1
);
if (!response.ok) {
throw new Error(`request-types/${requestTypeId}/lists/current: ${response.status} ${response.statusText}`);
}
const body: IApiResponse<ICurrentListPartition> = await response.json();
return body.data;
}
// Кеш на уровне МОДУЛЯ (не useRef/useState) — список типов заявок админ меняет
// редко, а без кеша каждая форма и каждый переход в режим согласующего дёргали
// бы /api/request-types заново. Живёт до перезагрузки страницы — тот же принцип,
// что и у features/myRequests/pageCache.ts. Ошибку НЕ кешируем: если запрос
// упал (сеть, 401 на холодном старте), следующий вызов должен попробовать заново,
// а не молча повторять один и тот же провал до конца сессии.
let requestTypesPromise: Promise<IRequestType[]> | undefined;
export async function getRequestTypesCached(context: WebPartContext): Promise<IRequestType[]> {
if (!requestTypesPromise) {
requestTypesPromise = getRequestTypes(context).catch((e) => {
requestTypesPromise = undefined;
throw e;
});
}
return requestTypesPromise;
}
/**
* Числовой RequestTypeId по pageUrl (rptype) формы. Id типа заявки выдаётся
* сервером при регистрации типа в админке approval-route и НЕПРЕДСКАЗУЕМ
* заранее (см. README форм — «в старой pto-tracker было 1, при заведении типа
* в админке получил новый id 11»), а pageUrl задаём мы сами в data/requests.ts
* и он стабилен. Поэтому формы резолвят числовой id отсюда (через
* useRequestTypeId), а не хардкодят его.
*/
export async function getRequestTypeIdByPageUrl(context: WebPartContext, pageUrl: string): Promise<number> {
const types = await getRequestTypesCached(context);
const found = types.filter((t) => t.pageUrl === pageUrl)[0];
if (!found) {
throw new Error(
`Тип заявки с pageUrl «${pageUrl}» не найден в approval-route API — зарегистрируйте его в админке маршрутов (Типы заявок).`
);
}
return found.id;
}
// POST /api/template-routes/resolve — планируемый маршрут для типа заявки и
// пользователя. Бэкенд сам подставляет исполнителей шага (executorEmail/position)
// по ролям исходя из userEmail. Параметр status выбирает ФАЗУ маршрута:
// 'Approval' — согласующие (кто одобряет),
// 'Execution' — исполнители (кто выполняет после согласования).
// Один и тот же маршрут, разные наборы шагов.
//
// fieldValues — условие видимости шагов (см. IResolveFieldValue): для форм с
// шагами, зависящими от значения колонки списка (напр. WorkType), передать
// текущее значение поля здесь. Формы без условных шагов его не передают —
// бэкенд получает пустой массив, как раньше.
export async function resolveRoute(
context: WebPartContext,
requestTypeId: number,
userEmail: string,
status: ApprovalStatus = 'Approval',
// eslint-disable-next-line @rushstack/no-new-null -- null отправляется в теле запроса как «не задано», undefined в JSON.stringify пропал бы вовсе
departmentId: string | null = null,
fieldValues: IResolveFieldValue[] = [],
// eslint-disable-next-line @rushstack/no-new-null -- см. departmentId
projectId: string | null = null
): Promise<IResolvedRoute> {
const c = await client(context);
const url = `${getApprovalApiBaseUrl()}/api/template-routes/resolve`;
// Эндпоинт сменился с GET (query) на POST (JSON-тело).
// projectId (uuid проекта) — для форм, где согласующий зависит от проекта
// (напр. «руководитель проекта-источника» в СЗ на перемещение ТМЦ): бэкенд по
// нему подставляет исполнителя роли. Формы без привязки к проекту шлют null.
const response: HttpClientResponse = await c.post(url, AadHttpClient.configurations.v1, {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json'
},
body: JSON.stringify({
requestTypeId,
userEmail,
projectId: projectId || null,
status,
fieldValues,
departmentId: departmentId || null
})
});
if (!response.ok) {
throw new Error(`resolve: ${response.status} ${response.statusText}`);
}
const body: IApiResponse<IResolvedRoute> = await response.json();
return body.data;
}