import * as React from 'react'; import styles from './AdminApp.module.scss'; import '@tnsu/ui-kit/styles.css'; import { IconSprite, Sidebar, SidebarToggle } from '@tnsu/ui-kit'; import type { ISidebarItem } from '@tnsu/ui-kit'; import { ScreenKey } from './types'; import { ApprovalRouteApi } from '../../services/api'; import { PortalDictionaryClient } from '../../services/portalDictionary'; import { SharePointListsClient } from '../../services/sharePointLists'; import { RequestManageService } from '../../services/requestManageService'; import { RequestType, TemplateRoute } from '../../models'; import { navigate, replaceRoute, useRoute } from '../../routing'; import { texts } from '../../texts'; import { useLocalStorageState } from '../../hooks/useLocalStorageState'; import { DashboardScreen } from '../screens/DashboardScreen'; import { RequestTypesScreen } from '../screens/RequestTypesScreen'; import { RequestTypeAccessScreen } from '../screens/RequestTypeAccessScreen'; import { RoutesScreen } from '../screens/RoutesScreen'; import { RouteStepsScreen } from '../screens/RouteStepsScreen'; import { ResolveScreen } from '../screens/ResolveScreen'; import { ManageRequestsScreen } from '../screens/ManageRequestsScreen'; import { ManageByUserScreen } from '../screens/ManageByUserScreen'; import { RoutesExportScreen } from '../screens/RoutesExportScreen'; // Тот же боковой рейл (@tnsu/ui-kit Sidebar), что и в request-portal — единый // стиль навигации во всех веб-частях Tnsu. 'routeSteps' сюда не входит: это не // отдельная вкладка URL, а состояние вкладки 'routes' при открытых шагах // конкретного маршрута (см. routing.ts) — подсвечиваем в этом случае 'routes'. const NAV_ITEMS: ISidebarItem[] = [ { id: 'dashboard', title: texts.nav.dashboard, icon: 'chart' }, { id: 'requestTypes', title: texts.nav.requestTypes, icon: 'document' }, { id: 'routes', title: texts.nav.routes, icon: 'send' }, { id: 'resolve', title: texts.nav.resolve, icon: 'search' }, { id: 'routesExport', title: texts.nav.routesExport, icon: 'document' }, { id: 'manageRequests', title: texts.nav.manageRequests, icon: 'clipboard' }, { id: 'manageByUser', title: texts.nav.manageByUser, icon: 'user-check' }, { id: 'requestTypeAccess', title: texts.nav.requestTypeAccess, icon: 'people' } ]; function sidebarActiveId(tab: ScreenKey): string { return tab === 'routeSteps' ? 'routes' : tab; } // Последняя открытая вкладка. В адресе она и так живёт (aatab, см. routing.ts), // но веб-часть часто открывают по «чистой» ссылке на страницу SharePoint, без // query — и тогда пользователь каждый раз приземлялся на дашборд, даже если // работает всегда в одном разделе. const TAB_STORAGE_KEY = 'approvalAdmin.activeTab'; export interface IAdminAppProps { api: ApprovalRouteApi; /** Undefined, пока SPFx выдаёт AAD-клиент для api.tnsu.kz. */ dictionary?: PortalDictionaryClient; sharePoint: SharePointListsClient; requestManage: RequestManageService; currentUserEmail?: string; } export const AdminApp: React.FC = ({ api, dictionary, sharePoint, requestManage, currentUserEmail }) => { const route = useRoute(); const [selectedRoute, setSelectedRoute] = React.useState(); const [selectedRequestType, setSelectedRequestType] = React.useState(); const [navCollapsed, setNavCollapsed] = React.useState(false); const [mobileNavOpen, setMobileNavOpen] = React.useState(false); const [storedTab, setStoredTab] = useLocalStorageState(TAB_STORAGE_KEY, 'dashboard'); // Восстанавливаем последнюю вкладку ТОЛЬКО когда в адресе её нет: глубокая // ссылка (в т.ч. присланная коллегой) всегда важнее запомненного выбора. // Один раз на монтировании — дальше вкладку меняет только сам пользователь. React.useEffect(() => { const explicitTab = new URLSearchParams(window.location.search).get('aatab'); if (explicitTab || storedTab === 'dashboard') return; replaceRoute({ tab: storedTab }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // 'routeSteps' сюда не попадёт: useRoute отдаёт только вкладки из TABS, а шаги // маршрута — состояние вкладки 'routes' (см. routing.ts). React.useEffect(() => { setStoredTab(route.tab); }, [route.tab, setStoredTab]); const navigateTab = (key: ScreenKey): void => { setMobileNavOpen(false); navigate({ tab: key }); }; const openSteps = (r: TemplateRoute): void => { setSelectedRoute(r); setSelectedRequestType(undefined); api.requestTypes.get(r.requestTypeId) .then(setSelectedRequestType) .catch(() => setSelectedRequestType(undefined)); // routesFilter несём с собой: возврат «к маршрутам» должен показать тот же // отфильтрованный список, из которого ушли (см. onBack ниже). navigate({ tab: 'routes', routeId: r.id, routesFilter: route.routesFilter }); }; // Открытие по прямой ссылке / обновление страницы / кнопка «назад»: в URL // лежит только aaroute (число), а RouteStepsScreen ждёт полный TemplateRoute — // дочитываем его через API. Если сюда пришли через openSteps (клик в // RoutesScreen), selectedRoute уже тот самый — повторный запрос не нужен. React.useEffect(() => { if (route.tab !== 'routes' || !route.routeId) { setSelectedRoute(undefined); setSelectedRequestType(undefined); return; } if (selectedRoute && selectedRoute.id === route.routeId) return; let alive = true; api.templateRoutes.get(route.routeId) .then(r => { if (!alive) return; setSelectedRoute(r); return api.requestTypes.get(r.requestTypeId).then(rt => { if (alive) setSelectedRequestType(rt); }); }) .catch(() => { if (alive) setSelectedRoute(undefined); }); return () => { alive = false; }; // selectedRoute — намеренно не в зависимостях: иначе повторный fetch // запускался бы сразу после того, как этот же эффект его и выставил. // eslint-disable-next-line react-hooks/exhaustive-deps }, [api, route.tab, route.routeId]); return (
navigateTab(id as ScreenKey)} brand={texts.nav.title} collapsed={navCollapsed} onToggleCollapsed={() => setNavCollapsed(v => !v)} mobileOpen={mobileNavOpen} onMobileClose={() => setMobileNavOpen(false)} />
setMobileNavOpen(true)} />
{route.tab === 'dashboard' && } {route.tab === 'requestTypes' && } {route.tab === 'requestTypeAccess' && ( )} {route.tab === 'routes' && !route.routeId && ( )} {route.tab === 'routes' && route.routeId && selectedRoute && ( navigate({ tab: 'routes', routesFilter: route.routesFilter })} /> )} {route.tab === 'resolve' && ( )} {route.tab === 'routesExport' && } {route.tab === 'manageRequests' && ( )} {route.tab === 'manageByUser' && ( )}
); };