214 lines
8.4 KiB
TypeScript

import { useMemo, useState } from 'react';
import {
buildTopBarNotifications,
countUnreadTopBarNotifications,
getTopBarCampusLabel,
getTopBarInitials,
getTopBarRoleLabel,
} from '@/business/top-bar/selectors';
import {
buildTopBarSearchResults,
type TopBarContentItem,
type TopBarSearchResult,
} from '@/business/top-bar/search';
import {
useCommunicationEvents,
} from '@/business/communications/hooks';
import { getScopedModules } from '@/business/app-shell/selectors';
import { useContentCatalogPayload } from '@/business/content-catalog/hooks';
import { usePolicies, usePolicyAcknowledgments } from '@/business/policies/hooks';
import { useCurrentPersonalityResult } from '@/business/personality/queryHooks';
import { useMySafetyQuizStatus } from '@/business/safety-quiz/hooks';
import { canPersistPersonalScopeResults } from '@/business/scope/selectors';
import { useSafetyProtocols } from '@/business/safety-protocols/hooks';
import { hasPermission } from '@/business/auth/permissions';
import { CONTENT_CATALOG_TYPES } from '@/shared/constants/contentCatalog';
import { MODULES } from '@/shared/constants/appData';
import { PERSONALITY_QUIZ_KINDS } from '@/shared/constants/personality';
import type {
ModuleId,
SignItem,
Strategy,
ZoneInfo,
} from '@/shared/types/app';
import type {
TopBarPage,
UseTopBarPageOptions,
} from '@/business/top-bar/types';
import { useTodayZoneCheckIn } from '@/business/zone-checkin/hooks';
import { canZoneCheckIn, shouldNudgeZoneCheckIn } from '@/business/zone-checkin/selectors';
import { useScopeContext } from '@/shared/app/scope-context';
import { getCurrentSafetyQuizWeek } from '@/business/safety-quiz/selectors';
const EMPTY_STRATEGIES: readonly Strategy[] = [];
const EMPTY_SIGNS: readonly SignItem[] = [];
const EMPTY_ZONES: readonly ZoneInfo[] = [];
export function useTopBarPage({
user,
userRole,
userName,
campusInfo,
toggleSidebar,
setCurrentModule,
profile,
signOut: signOutAction,
}: UseTopBarPageOptions): TopBarPage {
const [showProfileMenu, setShowProfileMenu] = useState(false);
const [showNotifications, setShowNotifications] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [signOutError, setSignOutError] = useState<string | null>(null);
const { tier, ownTenant, selectedTenant } = useScopeContext();
const canPersistPersonalResults = canPersistPersonalScopeResults(ownTenant, selectedTenant);
const effectiveTier = selectedTenant ? selectedTenant.level : tier;
const scopedModules = useMemo(
() => getScopedModules(MODULES, user, effectiveTier, selectedTenant !== null),
[effectiveTier, selectedTenant, user],
);
const accessibleModuleIds = useMemo(
() => new Set(scopedModules.map((module) => module.id)),
[scopedModules],
);
const canUseZoneCheckIn = canPersistPersonalResults && canZoneCheckIn(user);
const zoneCheckIn = useTodayZoneCheckIn({ enabled: canUseZoneCheckIn });
const needsZoneCheckIn = shouldNudgeZoneCheckIn(
canUseZoneCheckIn ? user : null,
zoneCheckIn.isLoading,
zoneCheckIn.isCheckedInToday,
);
const canReceiveSafetyQuizNotification = canPersistPersonalResults
&& hasPermission(user, 'TAKE_QUIZ')
&& accessibleModuleIds.has('qbs')
&& (effectiveTier === 'school' || effectiveTier === 'campus' || effectiveTier === 'class');
const safetyQuizWeek = getCurrentSafetyQuizWeek(new Date());
const safetyQuizStatus = useMySafetyQuizStatus(
safetyQuizWeek,
canReceiveSafetyQuizNotification,
);
const needsSafetyQuiz = canReceiveSafetyQuizNotification
&& !safetyQuizStatus.isLoading
&& safetyQuizStatus.data?.completed !== true;
const canReceiveEmotionalIntelligenceNotifications = canPersistPersonalResults
&& hasPermission(user, 'TAKE_QUIZ')
&& accessibleModuleIds.has('ei');
const selfAssessmentStatus = useCurrentPersonalityResult(
PERSONALITY_QUIZ_KINDS.selfAssessment,
canReceiveEmotionalIntelligenceNotifications,
);
const personalityQuizStatus = useCurrentPersonalityResult(
PERSONALITY_QUIZ_KINDS.personalityType,
canReceiveEmotionalIntelligenceNotifications,
);
const needsEiSelfAssessment = canReceiveEmotionalIntelligenceNotifications
&& !selfAssessmentStatus.isLoading
&& !selfAssessmentStatus.data;
const needsPersonalityQuiz = canReceiveEmotionalIntelligenceNotifications
&& !personalityQuizStatus.isLoading
&& !personalityQuizStatus.data;
const communicationEvents = useCommunicationEvents();
const acknowledgedCommunicationEventIds = useMemo(() => new Set<string>(), []);
const canReceivePolicyNotifications = canPersistPersonalResults && hasPermission(user, 'ACK_POLICY');
const canReadHandbook = canReceivePolicyNotifications && accessibleModuleIds.has('handbook');
const canReadSafetyProtocols = canReceivePolicyNotifications && accessibleModuleIds.has('safety');
const policyAcknowledgments = usePolicyAcknowledgments(canReceivePolicyNotifications && (
canReadHandbook || canReadSafetyProtocols
));
const handbookPolicies = usePolicies(canReadHandbook);
const safetyProtocols = useSafetyProtocols(canReadSafetyProtocols);
const notifications = buildTopBarNotifications({
needsZoneCheckIn,
needsSafetyQuiz,
needsEiSelfAssessment,
needsPersonalityQuiz,
communicationEvents: communicationEvents.data ?? [],
acknowledgedCommunicationEventIds,
handbookPolicies: handbookPolicies.data ?? [],
safetyProtocols: safetyProtocols.data ?? [],
policyAcknowledgments: policyAcknowledgments.data ?? [],
});
// Header search = accessible modules (local) + their product content from the
// content catalog. Content is fetched lazily — only once the user types, and
// only for modules the user can access.
const hasQuery = searchQuery.trim().length > 0;
const moduleNameById = useMemo(
() => new Map<ModuleId, string>(MODULES.map((module) => [module.id, module.name])),
[],
);
const strategiesQuery = useContentCatalogPayload<readonly Strategy[]>(
CONTENT_CATALOG_TYPES.classroomStrategies,
EMPTY_STRATEGIES,
{ enabled: hasQuery && accessibleModuleIds.has('classroom') },
);
const signsQuery = useContentCatalogPayload<readonly SignItem[]>(
CONTENT_CATALOG_TYPES.signLanguageItems,
EMPTY_SIGNS,
{ enabled: hasQuery && accessibleModuleIds.has('signs') },
);
const zonesQuery = useContentCatalogPayload<readonly ZoneInfo[]>(
CONTENT_CATALOG_TYPES.regulationZones,
EMPTY_ZONES,
{ enabled: hasQuery && accessibleModuleIds.has('zones') },
);
const contentItems = useMemo<readonly TopBarContentItem[]>(() => {
const items: TopBarContentItem[] = [];
const add = (moduleId: ModuleId, id: string, label: string) => {
items.push({ id, label, moduleId, moduleName: moduleNameById.get(moduleId) ?? '' });
};
strategiesQuery.payload.forEach((s) => add('classroom', `strategy-${s.id}`, s.title));
signsQuery.payload.forEach((s) => add('signs', `sign-${s.id}`, s.word));
zonesQuery.payload.forEach((z) => add('zones', `zone-${z.color}`, z.name));
return items;
}, [strategiesQuery.payload, signsQuery.payload, zonesQuery.payload, moduleNameById]);
const searchResults = useMemo(
() => buildTopBarSearchResults(scopedModules, user, searchQuery, contentItems),
[scopedModules, user, searchQuery, contentItems],
);
function selectSearchResult(result: TopBarSearchResult) {
setSearchQuery('');
setCurrentModule(result.moduleId);
}
async function signOut() {
setShowProfileMenu(false);
setSignOutError(null);
const result = await signOutAction();
if (result.error) {
setSignOutError(result.error);
}
}
return {
userRole,
userName,
avatar: user?.avatar ?? null,
campusInfo,
profileRoleLabel: profile ? getTopBarRoleLabel(profile.role) : getTopBarRoleLabel(userRole),
roleLabel: getTopBarRoleLabel(userRole),
initials: getTopBarInitials(userName),
campusLabel: getTopBarCampusLabel(campusInfo),
showProfileMenu,
showNotifications,
searchQuery,
searchResults,
signOutError,
notifications,
unreadCount: countUnreadTopBarNotifications(notifications),
toggleSidebar,
toggleProfileMenu: () => setShowProfileMenu((current) => !current),
closeProfileMenu: () => setShowProfileMenu(false),
toggleNotifications: () => setShowNotifications((current) => !current),
closeNotifications: () => setShowNotifications(false),
setSearchQuery,
selectSearchResult,
signOut,
};
}