163 lines
7.2 KiB
TypeScript
163 lines
7.2 KiB
TypeScript
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, useRoute } from '../../routing';
|
||
import { texts } from '../../texts';
|
||
import { DashboardScreen } from '../screens/DashboardScreen';
|
||
import { RequestTypesScreen } from '../screens/RequestTypesScreen';
|
||
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';
|
||
|
||
// Тот же боковой рейл (@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: 'manageRequests', title: texts.nav.manageRequests, icon: 'clipboard' },
|
||
{ id: 'manageByUser', title: texts.nav.manageByUser, icon: 'user-check' }
|
||
];
|
||
|
||
function sidebarActiveId(tab: ScreenKey): string {
|
||
return tab === 'routeSteps' ? 'routes' : tab;
|
||
}
|
||
|
||
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 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 === '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 === 'manageRequests' && (
|
||
<ManageRequestsScreen api={api} dictionary={dictionary} requestManage={requestManage} route={route.manage} />
|
||
)}
|
||
{route.tab === 'manageByUser' && (
|
||
<ManageByUserScreen api={api} dictionary={dictionary} requestManage={requestManage} />
|
||
)}
|
||
</div>
|
||
</main>
|
||
</div>
|
||
);
|
||
};
|