214 lines
8.4 KiB
TypeScript
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,
|
|
};
|
|
}
|