276 lines
7.7 KiB
TypeScript
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),
|
|
};
|
|
}
|