2026-06-17 21:45:57 +02:00

276 lines
7.7 KiB
TypeScript

import { useEffect, useRef, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { MODULES } from '@/shared/constants/appData';
import { APP_ROUTE_PATHS } from '@/shared/constants/routes';
import {
DEFAULT_CAMPUS_LABEL,
findCampusByNameOrCode,
} from '@/shared/constants/campusDisplay';
import {
getModuleIdByRoutePath,
getModuleRoutePath,
} from '@/shared/constants/moduleRoutes';
import { DEFAULT_PRODUCT_ROLE } from '@/shared/constants/roles';
import type { ModuleId, UserRole } from '@/shared/types/app';
import {
getAccessibleModuleId,
getScopedModules,
getScopedModuleRouteRedirectPath,
getSidebarCampusInitial,
getSidebarRoleLabel,
shouldShowMobileSidebarOverlay,
withRoleAwareModuleNames,
} from '@/business/app-shell/selectors';
import { useCampusCatalog } from '@/business/campuses/hooks';
import { useScopeContext } from '@/shared/app/scope-context';
import type {
AppShellState,
SidebarPage,
SidebarProps,
UseAppShellOptions,
} from '@/business/app-shell/types';
import type { ActiveTenant } from '@/shared/types/scope';
interface ScopeRouteState {
readonly __scope?: ActiveTenant | null;
readonly [key: string]: unknown;
}
function sameScopeTenant(a: ActiveTenant | null, b: ActiveTenant | null): boolean {
if (a === b) return true;
if (!a || !b) return a === b;
return a.id === b.id && a.level === b.level;
}
function getUserRole(options: UseAppShellOptions): UserRole {
return options.profile?.role ?? DEFAULT_PRODUCT_ROLE;
}
function getUserName(options: UseAppShellOptions): string {
return options.profile?.full_name ?? '';
}
function getUserCampus(options: UseAppShellOptions): string {
return options.profile?.campus ?? DEFAULT_CAMPUS_LABEL;
}
export function useAppShell(options: UseAppShellOptions): AppShellState {
const location = useLocation();
const navigate = useNavigate();
const { tier, selectedTenant, resetScope, setScopeTenant } = useScopeContext();
const campusCatalog = useCampusCatalog();
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
const [zoneCheckIn, setZoneCheckIn] = useState<string | null>(null);
const userRole = getUserRole(options);
const userName = getUserName(options);
const userCampus = getUserCampus(options);
const effectiveTier = selectedTenant ? selectedTenant.level : tier;
const isDrilled = selectedTenant !== null;
const effectiveScopeKey = selectedTenant
? `${selectedTenant.level}:${selectedTenant.id}`
: `own:${tier}`;
const previousScopeKey = useRef(effectiveScopeKey);
const previousPathname = useRef(location.pathname);
const scopedModules = getScopedModules(MODULES, options.user, effectiveTier, isDrilled);
const currentRouteModule = getModuleIdByRoutePath(location.pathname);
const currentModule = getAccessibleModuleId(scopedModules, currentRouteModule, options.user);
const activeModule = getAccessibleModuleId(scopedModules, currentModule, options.user);
const campusInfo = findCampusByNameOrCode(campusCatalog.campuses, userCampus);
const mobileOverlayVisible = shouldShowMobileSidebarOverlay(options.isMobile, mobileSidebarOpen);
const routeState = (location.state as ScopeRouteState | null) ?? null;
const routeScopeTenant = routeState?.__scope ?? null;
useEffect(() => {
const scopeChanged = previousScopeKey.current !== effectiveScopeKey;
const pathnameChanged = previousPathname.current !== location.pathname;
previousScopeKey.current = effectiveScopeKey;
previousPathname.current = location.pathname;
if (pathnameChanged && !scopeChanged && !sameScopeTenant(routeScopeTenant, selectedTenant)) {
if (routeScopeTenant) {
setScopeTenant(routeScopeTenant);
} else {
resetScope();
}
return;
}
if (location.pathname === APP_ROUTE_PATHS.platformDashboard && tier === 'global' && selectedTenant !== null) {
if (scopeChanged) {
const drilledRedirectPath = getScopedModuleRouteRedirectPath(
MODULES,
location.pathname,
options.user,
effectiveTier,
isDrilled,
);
if (drilledRedirectPath && drilledRedirectPath !== location.pathname) {
navigate(drilledRedirectPath, {
replace: true,
state: { ...routeState, __scope: selectedTenant },
});
}
return;
}
}
if (scopeChanged) {
const redirectPath = getScopedModuleRouteRedirectPath(
MODULES,
location.pathname,
options.user,
effectiveTier,
isDrilled,
);
if (redirectPath && redirectPath !== location.pathname) {
navigate(redirectPath, {
replace: true,
state: { ...routeState, __scope: selectedTenant },
});
return;
}
}
if (!sameScopeTenant(routeScopeTenant, selectedTenant)) {
navigate(
{
pathname: location.pathname,
search: location.search,
hash: location.hash,
},
{
replace: true,
state: { ...routeState, __scope: selectedTenant },
},
);
}
}, [
effectiveScopeKey,
location.hash,
effectiveTier,
isDrilled,
location.pathname,
location.search,
navigate,
options.user,
resetScope,
routeScopeTenant,
routeState,
selectedTenant,
setScopeTenant,
tier,
]);
const setCurrentModule = (id: ModuleId) => {
const targetModule = scopedModules.find((module) => module.id === id);
navigate(
targetModule?.routePath ?? scopedModules[0]?.routePath ?? getModuleRoutePath(id),
{
state: { ...routeState, __scope: selectedTenant },
},
);
if (options.isMobile) {
setMobileSidebarOpen(false);
}
};
const toggleSidebar = () => {
if (options.isMobile) {
setMobileSidebarOpen((current) => !current);
return;
}
setSidebarCollapsed((current) => !current);
};
const sidebarProps: SidebarProps = {
currentModule: activeModule,
setCurrentModule,
user: options.user,
userRole,
collapsed: options.isMobile ? false : sidebarCollapsed,
setCollapsed: setSidebarCollapsed,
campusInfo,
};
const topBarProps = {
user: options.user,
userRole,
userName,
campusInfo,
toggleSidebar,
setCurrentModule,
};
const shellOutletContext = {
user: options.user,
userRole,
userName,
userCampus,
zoneCheckIn,
setZoneCheckIn,
setCurrentModule,
};
const footerProps = {
userName,
userRole,
modules: scopedModules,
setCurrentModule,
};
return {
activeModule,
currentModule: activeModule,
userRole,
userName,
userCampus,
campusInfo,
sidebarCollapsed,
mobileSidebarOpen,
mobileOverlayVisible,
zoneCheckIn,
sidebarProps,
topBarProps,
shellOutletContext,
footerProps,
setSidebarCollapsed,
setMobileSidebarOpen,
setZoneCheckIn,
setCurrentModule,
};
}
export function useSidebarPage({
currentModule,
setCurrentModule,
user,
userRole,
collapsed,
setCollapsed,
campusInfo,
}: SidebarProps): SidebarPage {
const { tier, selectedTenant } = useScopeContext();
const effectiveTier = selectedTenant ? selectedTenant.level : tier;
const isDrilled = selectedTenant !== null;
return {
currentModule,
user,
userRole,
collapsed,
campusInfo,
modules: withRoleAwareModuleNames(
getScopedModules(MODULES, user, effectiveTier, isDrilled),
userRole,
),
roleLabel: getSidebarRoleLabel(userRole),
campusInitial: getSidebarCampusInitial(campusInfo),
setCurrentModule,
toggleCollapsed: () => setCollapsed(!collapsed),
};
}