import * as React from 'react'; // Маршрутизация портала по параметрам URL. // // Раздел бокового меню — rpnav: 'new' (подать заявку, по умолчанию), // 'approvals' (ждут моего решения), 'own' (мои заявки), 'search' (поиск // сразу по обеим категориям — и по своим заявкам, и по тем, где участвовал). // Открытая заявка — переход по ссылке из потока Power Automate или клик по // строке таблицы: // …/СтраницаПортала.aspx?rpmode=approve&rptype=<форма>&rpid= // где 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(() => 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; }