183 lines
9.7 KiB
TypeScript
183 lines
9.7 KiB
TypeScript
import * as React from 'react';
|
|
|
|
// Маршрутизация портала по параметрам URL.
|
|
//
|
|
// Раздел бокового меню — rpnav: 'new' (подать заявку, по умолчанию),
|
|
// 'approvals' (ждут моего решения), 'own' (мои заявки), 'search' (поиск
|
|
// сразу по обеим категориям — и по своим заявкам, и по тем, где участвовал).
|
|
// Открытая заявка — переход по ссылке из потока Power Automate или клик по
|
|
// строке таблицы:
|
|
// …/СтраницаПортала.aspx?rpmode=approve&rptype=<форма>&rpid=<RowID>
|
|
// где rptype = ключ формы в реестре (forms/registry), rpid = RowID заявки в
|
|
// списке данных. Заявка показывается в выезжающей панели ПОВЕРХ текущего
|
|
// раздела, поэтому rpnav сохраняется вместе с rpmode.
|
|
//
|
|
// Префикс rp (request-portal): имена id/mode/type зарезервированы SharePoint
|
|
// (напр. ?id= он трактует как навигацию к элементу → «Элемент не существует»),
|
|
// поэтому используем собственные, неконфликтные имена параметров.
|
|
//
|
|
// Навигация внутри портала идёт через navigate() — это history.pushState без
|
|
// перезагрузки страницы. useRoute() подписан и на popstate (кнопка «назад»),
|
|
// и на её собственное событие, поэтому реагирует на оба способа смены URL.
|
|
|
|
// 'edit' — открыть СВОЮ незавершённую заявку (черновик / возвращённую /
|
|
// отклонённую) на редактирование в той же выезжающей панели, что и просмотр.
|
|
export type PortalMode = 'submit' | 'approve' | 'create' | 'edit';
|
|
|
|
/**
|
|
* Раздел бокового меню.
|
|
*
|
|
* Три раздела — очереди действий текущего пользователя, и они НЕ пересекаются
|
|
* (см. ApprovalQueueKind в myRequests.service.ts):
|
|
* 'approvals' — ждут его согласования (статус строки «На согласовании»),
|
|
* 'execution' — ждут его исполнения (статус строки «На исполнении»),
|
|
* 'rework' — ЕГО заявки, которые вернули ему на доработку (статус ЗАЯВКИ
|
|
* «Возвращена на доработку»); в отличие от первых двух смотрит
|
|
* на список данных по автору, а не на строки маршрута.
|
|
*/
|
|
export type NavSection = 'new' | 'approvals' | 'execution' | 'rework' | 'own' | 'search';
|
|
|
|
/**
|
|
* Подвкладка раздела «Мои заявки». 'created' — заявки, где пользователь автор
|
|
* (единственная, где есть фильтр по статусу); остальные — история его решений
|
|
* как участника чужих маршрутов.
|
|
*/
|
|
export type OwnTab = 'created' | 'approved' | 'rejected' | 'executed';
|
|
|
|
export interface IRoute {
|
|
mode: PortalMode;
|
|
/** Раздел бокового меню; сохраняется и когда поверх открыта заявка. */
|
|
nav: NavSection;
|
|
/** Ключ формы (тип заявки) — для 'approve', 'edit' и 'create'. */
|
|
formKey?: string;
|
|
/** Id заявки в списке данных — для 'approve' и 'edit'. */
|
|
itemId?: number;
|
|
/** Id исходной заявки для предзаполнения — для 'create' (напр. орг. места на основе прием). */
|
|
sourceId?: number;
|
|
/** Подвкладка «Моих заявок»; задана только при nav='own'. */
|
|
ownTab?: OwnTab;
|
|
/** Фильтр по статусу заявки; задан только при nav='own' и ownTab='created'. */
|
|
ownStatus?: string;
|
|
}
|
|
|
|
const ROUTE_CHANGE_EVENT = 'rp-route-change';
|
|
|
|
function parseNav(raw: string | undefined): NavSection {
|
|
if (
|
|
raw === 'approvals' ||
|
|
raw === 'execution' ||
|
|
raw === 'rework' ||
|
|
raw === 'own' ||
|
|
raw === 'search'
|
|
) {
|
|
return raw;
|
|
}
|
|
return 'new';
|
|
}
|
|
|
|
function parseOwnTab(raw: string | undefined): OwnTab {
|
|
if (raw === 'approved' || raw === 'rejected' || raw === 'executed') return raw;
|
|
return 'created';
|
|
}
|
|
|
|
/** Разобрать строку запроса (по умолчанию — из адреса страницы) в маршрут. */
|
|
export function parseRoute(search: string = window.location.search): IRoute {
|
|
const params = new URLSearchParams(search);
|
|
const mode = params.get('rpmode');
|
|
const nav = parseNav(params.get('rpnav') ?? undefined);
|
|
const formKey = params.get('rptype') ?? undefined;
|
|
const idRaw = params.get('rpid');
|
|
const itemId = idRaw ? Number(idRaw) : undefined;
|
|
const sourceRaw = params.get('rpsource');
|
|
const sourceId = sourceRaw ? Number(sourceRaw) : undefined;
|
|
|
|
// Подвкладка и фильтр — состояние раздела «Мои заявки», поэтому вне его не
|
|
// читаются вовсе (иначе чужой rptab из ссылки «протёк» бы в другой раздел).
|
|
// Фильтр по статусу живёт только на вкладке «Инициированные»: у вкладок с
|
|
// решениями отбор задаёт сама вкладка.
|
|
const ownTab = nav === 'own' ? parseOwnTab(params.get('rptab') ?? undefined) : undefined;
|
|
const ownStatus = ownTab === 'created' ? params.get('rpstatus') ?? undefined : undefined;
|
|
|
|
// Открытая заявка (панель поверх раздела) — при полном наборе параметров.
|
|
// Раздел под панелью сохраняется целиком, вместе с подвкладкой и фильтром:
|
|
// закрыв панель, пользователь должен вернуться ровно в тот же список.
|
|
if (mode === 'approve' && formKey && itemId && !isNaN(itemId)) {
|
|
return { mode: 'approve', nav, formKey, itemId, ownTab, ownStatus };
|
|
}
|
|
// Редактирование собственной незавершённой заявки — та же панель, но с формой.
|
|
if (mode === 'edit' && formKey && itemId && !isNaN(itemId)) {
|
|
return { mode: 'edit', nav, formKey, itemId, ownTab, ownStatus };
|
|
}
|
|
// Создание производной заявки с предзаполнением из исходной.
|
|
if (formKey && sourceId && !isNaN(sourceId)) {
|
|
return { mode: 'create', nav: 'new', formKey, sourceId };
|
|
}
|
|
return { mode: 'submit', nav, ownTab, ownStatus };
|
|
}
|
|
|
|
// Собрать query-строку маршрута, сохранив посторонние параметры страницы.
|
|
function buildQuery(route: IRoute): string {
|
|
const params = new URLSearchParams(window.location.search);
|
|
params.delete('rpmode');
|
|
params.delete('rpnav');
|
|
params.delete('rptype');
|
|
params.delete('rpid');
|
|
params.delete('rpsource');
|
|
params.delete('rptab');
|
|
params.delete('rpstatus');
|
|
|
|
if (route.nav !== 'new') {
|
|
params.set('rpnav', route.nav);
|
|
}
|
|
// Значения по умолчанию в адрес не пишем — чтобы обычный переход на «Мои
|
|
// заявки» давал чистую ссылку, а не ?rpnav=own&rptab=created.
|
|
if (route.nav === 'own') {
|
|
const ownTab = route.ownTab ?? 'created';
|
|
if (ownTab !== 'created') params.set('rptab', ownTab);
|
|
else if (route.ownStatus) params.set('rpstatus', route.ownStatus);
|
|
}
|
|
if ((route.mode === 'approve' || route.mode === 'edit') && route.formKey && route.itemId) {
|
|
params.set('rpmode', route.mode);
|
|
params.set('rptype', route.formKey);
|
|
params.set('rpid', String(route.itemId));
|
|
} else if (route.mode === 'create' && route.formKey && route.sourceId) {
|
|
params.set('rptype', route.formKey);
|
|
params.set('rpsource', String(route.sourceId));
|
|
}
|
|
|
|
const query = params.toString();
|
|
return `${window.location.pathname}${query ? `?${query}` : ''}${window.location.hash}`;
|
|
}
|
|
|
|
/**
|
|
* Перейти на другой маршрут без перезагрузки страницы (SPA): обновляет
|
|
* query-параметры адресной строки через history.pushState и оповещает
|
|
* useRoute() своим событием (popstate на pushState сам не срабатывает).
|
|
*/
|
|
export function navigate(route: IRoute): void {
|
|
window.history.pushState({}, '', buildQuery(route));
|
|
window.dispatchEvent(new Event(ROUTE_CHANGE_EVENT));
|
|
}
|
|
|
|
/** Ссылка на маршрут (для href — открытие в новой вкладке, копирование и т.п.). */
|
|
export function routeUrl(route: IRoute): string {
|
|
return buildQuery(route);
|
|
}
|
|
|
|
/** Текущий маршрут, реактивно обновляется по navigate() и кнопке «назад». */
|
|
export function useRoute(): IRoute {
|
|
const [route, setRoute] = React.useState<IRoute>(() => parseRoute());
|
|
|
|
React.useEffect(() => {
|
|
const onChange = (): void => setRoute(parseRoute());
|
|
window.addEventListener('popstate', onChange);
|
|
window.addEventListener(ROUTE_CHANGE_EVENT, onChange);
|
|
return () => {
|
|
window.removeEventListener('popstate', onChange);
|
|
window.removeEventListener(ROUTE_CHANGE_EVENT, onChange);
|
|
};
|
|
}, []);
|
|
|
|
return route;
|
|
}
|