Files
tnsu-approval-route/frontend/src/webparts/approvalAdmin/components/app/AdminApp.tsx
T
gorbenko.pavel 0e6a7064d2
Tests / backend-test (push) Successful in 6m7s
Tests / frontend-test (push) Successful in 1m29s
Feature/routes export (#29)
Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz>
Reviewed-on: http://git.tnsu.kz/gorbenko.pavel/tnsu-approval-route/pulls/29
2026-10-14 05:06:58 +00:00

201 lines
9.6 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 * 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<IAdminAppProps> = ({ api, dictionary, sharePoint, requestManage, currentUserEmail }) => {
const route = useRoute();
const [selectedRoute, setSelectedRoute] = React.useState<TemplateRoute | undefined>();
const [selectedRequestType, setSelectedRequestType] = React.useState<RequestType | undefined>();
const [navCollapsed, setNavCollapsed] = React.useState(false);
const [mobileNavOpen, setMobileNavOpen] = React.useState(false);
const [storedTab, setStoredTab] = useLocalStorageState<ScreenKey>(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 (
<div className={styles.shell}>
<IconSprite />
<div className={styles.side}>
<Sidebar
items={NAV_ITEMS}
activeId={sidebarActiveId(route.tab)}
onChange={id => navigateTab(id as ScreenKey)}
brand={texts.nav.title}
collapsed={navCollapsed}
onToggleCollapsed={() => setNavCollapsed(v => !v)}
mobileOpen={mobileNavOpen}
onMobileClose={() => setMobileNavOpen(false)}
/>
</div>
<main className={styles.main}>
<div className={styles.mobileNav}>
<SidebarToggle onClick={() => setMobileNavOpen(true)} />
</div>
<div className={styles.content}>
{route.tab === 'dashboard' && <DashboardScreen api={api} onNavigate={navigateTab} />}
{route.tab === 'requestTypes' && <RequestTypesScreen api={api} sharePoint={sharePoint} />}
{route.tab === 'requestTypeAccess' && (
<RequestTypeAccessScreen
api={api}
dictionary={dictionary}
initialFilterRequestTypeId={route.accessFilterRequestTypeId}
initialFilterEmail={route.accessFilterEmail}
/>
)}
{route.tab === 'routes' && !route.routeId && (
<RoutesScreen
api={api}
dictionary={dictionary}
initialFilterRequestTypeId={route.routesFilter}
onOpenSteps={openSteps}
/>
)}
{route.tab === 'routes' && route.routeId && selectedRoute && (
<RouteStepsScreen
api={api}
route={selectedRoute}
requestType={selectedRequestType}
dictionary={dictionary}
sharePoint={sharePoint}
onBack={() => navigate({ tab: 'routes', routesFilter: route.routesFilter })}
/>
)}
{route.tab === 'resolve' && (
<ResolveScreen
api={api}
dictionary={dictionary}
sharePoint={sharePoint}
currentUserEmail={currentUserEmail}
route={route.resolve}
/>
)}
{route.tab === 'routesExport' && <RoutesExportScreen api={api} />}
{route.tab === 'manageRequests' && (
<ManageRequestsScreen api={api} dictionary={dictionary} requestManage={requestManage} route={route.manage} />
)}
{route.tab === 'manageByUser' && (
<ManageByUserScreen api={api} dictionary={dictionary} requestManage={requestManage} route={route.manageByUser} />
)}
</div>
</main>
</div>
);
};