'use client';
import React, { useEffect, useState, useCallback, useMemo } from 'react';
import { useTheme } from 'next-themes';
import { motion, AnimatePresence } from 'framer-motion';
import {
LayoutDashboard,
Users,
Grid3X3,
PenLine,
Flower2,
ClipboardCheck,
Calculator,
FileText,
BookOpen,
Sun,
Moon,
Globe,
LogOut,
ChevronRight,
Heart,
Bell,
GraduationCap,
Settings,
Search,
HelpCircle,
Command,
CornerDownLeft,
User as UserIcon,
FolderSearch,
X,
TrendingUp,
CalendarCheck,
CalendarDays,
Calendar as CalendarIconNav,
Mail,
Shield,
Target,
Ruler,
MessageSquareText,
Pencil,
Plus,
Palette,
Leaf,
Check,
CheckCircle2,
AlertTriangle,
Info,
BookMarked,
StickyNote,
Award,
UserCheck,
BarChart3,
Trash2,
BookCheck,
Megaphone,
Briefcase,
Clock as ClockIcon,
Library as LibraryIcon,
Trophy,
FolderOpen,
Building2,
MessageSquare,
Brain,
Sparkles,
Tablet,
CalendarClock,
Timer,
LayoutGrid as LayoutGridIcon,
BookOpen as BookOpenIconNav,
Database,
PartyPopper,
Newspaper,
Compass,
UsersRound,
Bus,
} from 'lucide-react';
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
SidebarInset,
SidebarTrigger,
SidebarRail,
} from '@/components/ui/sidebar';
import { Button } from '@/components/ui/button';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { Separator } from '@/components/ui/separator';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@/components/ui/breadcrumb';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { ScrollArea } from '@/components/ui/scroll-area';
import { useAppStore, type ViewName } from '@/lib/store';
import { canAccessView, filterSectionsForRole, getFallbackView } from '@/lib/role-access';
import { t } from '@/lib/i18n';
import { fetchSchoolYears, fetchStudents, fetchClasses, fetchDBNotifications, markAllNotificationsRead, markSingleNotificationRead, type SchoolYear, type Student, type ClassGroup, type DBNotification, type DBNotificationData } from '@/lib/api';
import { apiGet } from '@/lib/api';
import type { School as SchoolType } from '@/lib/api';
import OnboardingTour, { isOnboardingCompleted } from '@/components/onboarding-tour';
import KeyboardShortcutsDialog from '@/components/keyboard-shortcuts-dialog';
import {
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandSeparator,
CommandShortcut,
} from '@/components/ui/command';
import { toast } from 'sonner';
import { useWebSocket, usePushNotifications, playNotificationSound, getNotificationSoundPref } from '@/lib/websocket';
import { OfflineIndicator, PWAInstallPrompt, useServiceWorker, OfflineSyncManager } from '@/components/offline-indicator';
import dynamic from 'next/dynamic';
import { Skeleton } from '@/components/ui/skeleton';
// Static imports for lightweight views
import DashboardView from './dashboard-view';
import StudentPortalView from './student-portal-view';
import ClassesView from './classes-view';
import CompetencyGridView from './competency-grid-view';
import ProgressEntriesView from './progress-entries-view';
import CompetenceFlowerView from './competence-flower-view';
import AssessmentsView from './assessments-view';
import ReportsView from './reports-view';
import SettingsView from './settings-view';
import StudentDetailView from './student-detail-view';
import MasteryMatrixView from './mastery-matrix-view';
import AttendanceView from './attendance-view';
import LessonPlansView from './lesson-plans-view';
import ParentCommunicationView from './parent-communication-view';
import BehaviorTrackingView from './behavior-tracking-view';
import CurriculumCoverageView from './curriculum-coverage-view';
import RubricLibraryView from './rubric-library-view';
import CommentBankView from './comment-bank-view';
import HomeworkView from './homework-view';
import PortfolioView from './portfolio-view';
import TimetableView from './timetable-view';
import ResourceLibraryView from './resource-library-view';
import CompetitionsView from './competitions-view';
import DistrictManagementView from './district-management-view';
import IllnessReportingView from './illness-reporting-view';
import CommunicationView from './communication-view';
import SubjectsView from './subjects-view';
import CounselingView from './counseling-view';
import DisciplinaryView from './disciplinary-view';
import AITestsView from './ai-tests-view';
import AIChatWidget from './ai-chat-widget';
import VirtualCharacter from './virtual-character';
import ParentPortalView from './parent-portal-view';
import NotificationCenterView from './notification-center-view';
import SchoolAnnouncementsView from './school-announcements-view';
import { UnifiedGradingPanel } from './unified-grading-panel';
import { ProfessionalCalendar } from './professional-calendar';
import StudentAchievementsView from './student-achievements-view';
import StudentStudyPlannerView from './student-study-planner-view';
import ReportCardView from './report-card-view';
import SchoolLibraryView from './school-library-view';
import SeatingChartView from './seating-chart-view';
import StudentWellnessView from './student-wellness-view';
import DataImportExportView from './data-import-export-view';
import StudentCareerView from './student-career-view';
import SubstituteTeacherView from './substitute-teacher-view';
import SchoolNewsletterView from './school-newsletter-view';
import SchoolTransportView from './school-transport-view';
import PeerAssessmentView from './peer-assessment-view';
// Dynamic imports for heavy components with loading skeletons
const AnalyticsView = dynamic(() => import('./analytics-view'), {
ssr: false,
loading: () =>
,
});
const NotebooksView = dynamic(() => import('./notebooks-view'), {
loading: () => ,
});
const DrawingView = dynamic(() => import('./drawing-view'), {
ssr: false,
loading: () => ,
});
type NavItem = { key: ViewName; icon: React.ElementType; labelKey: string };
type NavSection = { id: string; labelKey: string; items: NavItem[] };
const navSections: NavSection[] = [
{
id: 'analysis',
labelKey: 'polish.nav_analysis',
items: [
{ key: 'dashboard', icon: LayoutDashboard, labelKey: 'nav.dashboard' },
{ key: 'calendar', icon: CalendarIconNav, labelKey: 'nav.calendar' },
{ key: 'analytics', icon: TrendingUp, labelKey: 'nav.analytics' },
{ key: 'grade-analytics', icon: BarChart3, labelKey: 'nav.grade-analytics' },
{ key: 'reports', icon: FileText, labelKey: 'nav.reports' },
{ key: 'coverage', icon: Target, labelKey: 'nav.coverage' },
],
},
{
id: 'teaching',
labelKey: 'polish.nav_teaching',
items: [
{ key: 'classes', icon: Users, labelKey: 'nav.classes' },
{ key: 'progress', icon: PenLine, labelKey: 'nav.progress' },
{ key: 'flower', icon: Flower2, labelKey: 'nav.flower' },
{ key: 'matrix', icon: Grid3X3, labelKey: 'nav.matrix' },
{ key: 'assessments', icon: ClipboardCheck, labelKey: 'nav.assessments' },
{ key: 'grading', icon: Calculator, labelKey: 'nav.grading' },
{ key: 'attendance', icon: CalendarCheck, labelKey: 'nav.attendance' },
{ key: 'lesson-plans', icon: CalendarDays, labelKey: 'nav.lesson_plans' },
{ key: 'parents', icon: Mail, labelKey: 'nav.parents' },
{ key: 'behavior', icon: Shield, labelKey: 'nav.behavior' },
{ key: 'homework', icon: BookCheck, labelKey: 'nav.homework' },
{ key: 'rubrics', icon: Ruler, labelKey: 'nav.rubrics' },
{ key: 'comments', icon: MessageSquareText, labelKey: 'nav.comments' },
{ key: 'notebooks', icon: BookOpen, labelKey: 'nav.notebooks' },
{ key: 'drawing', icon: Palette, labelKey: 'nav.drawing' },
{ key: 'portfolio', icon: Briefcase, labelKey: 'nav.portfolio' },
{ key: 'timetable', icon: ClockIcon, labelKey: 'nav.timetable' },
{ key: 'resources', icon: LibraryIcon, labelKey: 'nav.resources' },
{ key: 'competitions', icon: Trophy, labelKey: 'nav.competitions' },
{ key: 'subjects', icon: BookOpen, labelKey: 'nav.subjects' },
{ key: 'illness', icon: Heart, labelKey: 'nav.illness' },
{ key: 'communication', icon: MessageSquare, labelKey: 'nav.communication' },
{ key: 'counseling', icon: Heart, labelKey: 'counseling.title' },
{ key: 'disciplinary', icon: Shield, labelKey: 'disciplinary.title' },
{ key: 'ai-tests', icon: Brain, labelKey: 'ai_tests.title' },
{ key: 'tablet-grading', icon: Tablet, labelKey: 'nav.tablet_grading' },
{ key: 'exam-calendar', icon: CalendarClock, labelKey: 'nav.exam_calendar' },
{ key: 'peer-assessment', icon: UsersRound, labelKey: 'nav.peer-assessment' },
{ key: 'report-cards', icon: FileText, labelKey: 'nav.report_cards' },
{ key: 'seating-chart', icon: LayoutGridIcon, labelKey: 'nav.seating_chart' },
{ key: 'school-library', icon: LibraryIcon, labelKey: 'nav.school_library' },
{ key: 'student-wellness', icon: Heart, labelKey: 'nav.student-wellness' },
{ key: 'student-career', icon: Compass, labelKey: 'nav.student-career' },
{ key: 'notification-center', icon: Bell, labelKey: 'nav.notification_center' },
{ key: 'announcements', icon: Megaphone, labelKey: 'nav.announcements' },
{ key: 'school-events', icon: PartyPopper, labelKey: 'nav.school-events' },
{ key: 'school-newsletter', icon: Newspaper, labelKey: 'nav.school-newsletter' },
{ key: 'school-transport', icon: Bus, labelKey: 'nav.school-transport' },
{ key: 'substitute-teacher', icon: UserCheck, labelKey: 'nav.substitute-teacher' },
{ key: 'data-import-export', icon: Database, labelKey: 'nav.data-import-export' },
],
},
{
id: 'setup',
labelKey: 'polish.nav_setup',
items: [
{ key: 'competencies', icon: BookOpen, labelKey: 'nav.competencies' },
{ key: 'districts', icon: Building2, labelKey: 'nav.districts' },
{ key: 'settings', icon: Settings, labelKey: 'nav.settings' },
],
},
];
// Student-specific navigation (simplified)
const studentNavSections: NavSection[] = [
{
id: 'student-main',
labelKey: 'student.dashboard_title',
items: [
{ key: 'dashboard', icon: LayoutDashboard, labelKey: 'nav.dashboard' },
{ key: 'student-portal', icon: GraduationCap, labelKey: 'student_portal.title' },
{ key: 'notebooks', icon: BookOpen, labelKey: 'nav.student_notebooks' },
{ key: 'flower', icon: Flower2, labelKey: 'nav.student_competencies' },
{ key: 'grading', icon: Calculator, labelKey: 'nav.student_grades' },
{ key: 'grade-analytics', icon: BarChart3, labelKey: 'nav.grade-analytics' },
{ key: 'homework', icon: BookCheck, labelKey: 'nav.homework' },
{ key: 'attendance', icon: CalendarCheck, labelKey: 'nav.student_attendance' },
{ key: 'portfolio', icon: Briefcase, labelKey: 'nav.portfolio' },
{ key: 'calendar', icon: CalendarIconNav, labelKey: 'nav.calendar' },
{ key: 'competitions', icon: Trophy, labelKey: 'nav.competitions' },
{ key: 'subjects', icon: BookOpen, labelKey: 'nav.subjects' },
{ key: 'illness', icon: Heart, labelKey: 'nav.illness' },
{ key: 'communication', icon: MessageSquare, labelKey: 'nav.communication' },
{ key: 'counseling', icon: Heart, labelKey: 'counseling.title' },
{ key: 'ai-tests', icon: Brain, labelKey: 'ai_tests.title' },
{ key: 'tablet-grading', icon: Tablet, labelKey: 'nav.tablet_grading' },
{ key: 'exam-calendar', icon: CalendarClock, labelKey: 'nav.exam_calendar' },
{ key: 'peer-assessment', icon: UsersRound, labelKey: 'nav.peer-assessment' },
{ key: 'report-cards', icon: FileText, labelKey: 'nav.report_cards' },
{ key: 'seating-chart', icon: LayoutGridIcon, labelKey: 'nav.seating_chart' },
{ key: 'student-achievements', icon: Trophy, labelKey: 'achievements.title' },
{ key: 'student-study-planner', icon: Timer, labelKey: 'study_planner.title' },
{ key: 'student-wellness', icon: Heart, labelKey: 'nav.student-wellness' },
{ key: 'student-career', icon: Compass, labelKey: 'nav.student-career' },
{ key: 'resources', icon: FolderOpen, labelKey: 'nav.resources' },
{ key: 'school-library', icon: LibraryIcon, labelKey: 'nav.school_library' },
{ key: 'school-events', icon: PartyPopper, labelKey: 'nav.school-events' },
{ key: 'school-newsletter', icon: Newspaper, labelKey: 'nav.school-newsletter' },
{ key: 'school-transport', icon: Bus, labelKey: 'nav.school-transport' },
{ key: 'substitute-teacher', icon: UserCheck, labelKey: 'nav.substitute-teacher' },
{ key: 'notification-center', icon: Bell, labelKey: 'nav.notification_center' },
{ key: 'announcements', icon: Megaphone, labelKey: 'nav.announcements' },
{ key: 'settings', icon: Settings, labelKey: 'nav.settings' },
],
},
];
// Parent-specific navigation (simplified)
const parentNavSections: NavSection[] = [
{
id: 'parent-main',
labelKey: 'parent.dashboard_title',
items: [
{ key: 'dashboard', icon: LayoutDashboard, labelKey: 'nav.dashboard' },
{ key: 'parent-portal', icon: Heart, labelKey: 'parent_portal.title' },
{ key: 'parents', icon: Mail, labelKey: 'parent.messages' },
{ key: 'grading', icon: Calculator, labelKey: 'nav.student_grades' },
{ key: 'grade-analytics', icon: BarChart3, labelKey: 'nav.grade-analytics' },
{ key: 'attendance', icon: CalendarCheck, labelKey: 'nav.student_attendance' },
{ key: 'calendar', icon: CalendarIconNav, labelKey: 'nav.calendar' },
{ key: 'competitions', icon: Trophy, labelKey: 'nav.competitions' },
{ key: 'illness', icon: Heart, labelKey: 'nav.illness' },
{ key: 'communication', icon: MessageSquare, labelKey: 'nav.communication' },
{ key: 'student-achievements', icon: Trophy, labelKey: 'achievements.title' },
{ key: 'student-study-planner', icon: Timer, labelKey: 'study_planner.title' },
{ key: 'student-career', icon: Compass, labelKey: 'nav.student-career' },
{ key: 'peer-assessment', icon: UsersRound, labelKey: 'nav.peer-assessment' },
{ key: 'report-cards', icon: FileText, labelKey: 'nav.report_cards' },
{ key: 'school-library', icon: LibraryIcon, labelKey: 'nav.school_library' },
{ key: 'school-events', icon: PartyPopper, labelKey: 'nav.school-events' },
{ key: 'school-newsletter', icon: Newspaper, labelKey: 'nav.school-newsletter' },
{ key: 'school-transport', icon: Bus, labelKey: 'nav.school-transport' },
{ key: 'substitute-teacher', icon: UserCheck, labelKey: 'nav.substitute-teacher' },
{ key: 'notification-center', icon: Bell, labelKey: 'nav.notification_center' },
{ key: 'announcements', icon: Megaphone, labelKey: 'nav.announcements' },
],
},
];
/* ── Announcement Banner ────────────────────────────────────────── */
interface AnnouncementItem {
id: string;
title: string;
content: string;
priority: string;
isPinned: boolean;
author: { id: string; firstName: string; lastName: string };
classGroup: { id: string; name: string } | null;
createdAt: string;
}
function getPriorityColor(priority: string): string {
switch (priority) {
case 'urgent': return 'bg-rose-50 border-rose-300 dark:bg-rose-950/50 dark:border-rose-800';
case 'high': return 'bg-amber-50 border-amber-300 dark:bg-amber-950/50 dark:border-amber-800';
case 'normal': return 'bg-emerald-50 border-emerald-300 dark:bg-emerald-950/50 dark:border-emerald-800';
case 'low': return 'bg-teal-50 border-teal-300 dark:bg-teal-950/50 dark:border-teal-800';
default: return 'bg-emerald-50 border-emerald-300 dark:bg-emerald-950/50 dark:border-emerald-800';
}
}
function getPriorityIcon(priority: string) {
switch (priority) {
case 'urgent': return AlertTriangle;
case 'high': return Info;
default: return Megaphone;
}
}
function AnnouncementBanner({ schoolId }: { schoolId: string | null }) {
const [announcements, setAnnouncements] = useState([]);
const [dismissed, setDismissed] = useState>(new Set());
useEffect(() => {
try {
const stored = localStorage.getItem('ct_dismissed_announcements');
if (stored) setDismissed(new Set(JSON.parse(stored)));
} catch { /* ignore */ }
}, []);
useEffect(() => {
if (!schoolId) return;
apiGet(`/api/announcements?schoolId=${schoolId}&isPinned=true&limit=5`)
.then((data) => setAnnouncements(data))
.catch(() => {});
}, [schoolId]);
const visibleAnnouncements = announcements.filter((a) => !dismissed.has(a.id));
const handleDismiss = (id: string) => {
const newDismissed = new Set(dismissed);
newDismissed.add(id);
setDismissed(newDismissed);
try {
localStorage.setItem('ct_dismissed_announcements', JSON.stringify([...newDismissed]));
} catch { /* ignore */ }
};
if (visibleAnnouncements.length === 0) return null;
return (
{visibleAnnouncements.map((announcement) => {
const PriorityIcon = getPriorityIcon(announcement.priority);
return (
{announcement.title}
{announcement.content.slice(0, 100)}{announcement.content.length > 100 ? '...' : ''}
handleDismiss(announcement.id)}
>
);
})}
);
}
function renderView(view: ViewName) {
switch (view) {
case 'dashboard': return ;
case 'student-portal': return ;
case 'classes': return ;
case 'competencies': return ;
case 'progress': return ;
case 'flower': return ;
case 'matrix': return ;
case 'analytics': return ;
case 'assessments': return ;
case 'grading': return ;
case 'reports': return ;
case 'settings': return ;
case 'student-detail': return ;
case 'attendance': return ;
case 'lesson-plans': return ;
case 'calendar': return ;
case 'parents': return ;
case 'behavior': return ;
case 'coverage': return ;
case 'rubrics': return ;
case 'comments': return ;
case 'notebooks': return ;
case 'drawing': return ;
case 'homework': return ;
case 'portfolio': return ;
case 'timetable': return ;
case 'resources': return ;
case 'competitions': return ;
case 'districts': return ;
case 'subjects': return ;
case 'illness': return ;
case 'communication': return ;
case 'counseling': return ;
case 'disciplinary': return ;
case 'ai-tests': return ;
case 'parent-portal': return ;
case 'notification-center': return ;
case 'announcements': return ;
case 'tablet-grading': return ;
case 'exam-calendar': return ;
case 'peer-assessment': return ;
case 'grade-analytics': return ;
case 'student-achievements': return ;
case 'student-study-planner': return ;
case 'seating-chart': return ;
case 'school-library': return ;
case 'report-cards': return ;
case 'student-wellness': return ;
case 'student-career': return ;
case 'data-import-export': return ;
case 'school-events': return ;
case 'substitute-teacher': return ;
case 'school-newsletter': return ;
case 'school-transport': return ;
default: return ;
}
}
export default function AppLayout() {
const { theme, setTheme } = useTheme();
const currentView = useAppStore((s) => s.currentView);
const setCurrentView = useAppStore((s) => s.setCurrentView);
const currentUser = useAppStore((s) => s.currentUser);
const logout = useAppStore((s) => s.logout);
const locale = useAppStore((s) => s.locale);
const setLocale = useAppStore((s) => s.setLocale);
const storeSchoolYearId = useAppStore((s) => s.schoolYearId);
const setSchoolYearId = useAppStore((s) => s.setSchoolYearId);
const navigateToStudentDetail = useAppStore((s) => s.navigateToStudentDetail);
const setCurrentClass = useAppStore((s) => s.setCurrentClass);
const [schoolYears, setSchoolYears] = useState([]);
const [selectedYearId, setSelectedYearId] = useState(storeSchoolYearId ?? '');
// School branding state
const [schoolBranding, setSchoolBranding] = useState(null);
// Service Worker registration for PWA
useServiceWorker();
// WebSocket connection for real-time push notifications
const { connected: wsConnected } = useWebSocket();
const { notification: pushNotification } = usePushNotifications();
// Quick search (Cmd+K) state
const [searchOpen, setSearchOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [searchStudents, setSearchStudents] = useState([]);
const [searchClasses, setSearchClasses] = useState([]);
const [searchLoading, setSearchLoading] = useState(false);
const [helpOpen, setHelpOpen] = useState(false);
const [notifOpen, setNotifOpen] = useState(false);
const [notifData, setNotifData] = useState(null);
const [notifLoading, setNotifLoading] = useState(false);
const [onboardingOpen, setOnboardingOpen] = useState(false);
const [shortcutsOpen, setShortcutsOpen] = useState(false);
const [recentActions, setRecentActions] = useState<{ key: string; label: string; view?: ViewName; timestamp: number }[]>([]);
const initials = currentUser
? `${currentUser.firstName[0]}${currentUser.lastName[0]}`.toUpperCase()
: '?';
const displayName = currentUser
? `${currentUser.firstName} ${currentUser.lastName}`
: '';
const roleKey = currentUser?.role === 'TEACHER' ? 'role.teacher' : currentUser?.role === 'SCHOOL_ADMIN' || currentUser?.role === 'VICE_PRINCIPAL' ? 'role.school_admin' : currentUser?.role === 'STUDENT' ? 'role.student' : currentUser?.role === 'PARENT' ? 'role.parent' : 'role.super_admin';
const isStudent = currentUser?.role === 'STUDENT';
const isParent = currentUser?.role === 'PARENT';
const activeNavSections = isStudent ? studentNavSections : isParent ? parentNavSections : navSections;
const visibleNavSections = useMemo(
() => filterSectionsForRole(currentUser?.role, activeNavSections),
[activeNavSections, currentUser?.role],
);
useEffect(() => {
if (currentUser && !canAccessView(currentUser.role, currentView)) {
setCurrentView(getFallbackView(currentUser.role));
}
}, [currentUser, currentView, setCurrentView]);
const isDemoUser = currentUser?.isDemo === true || (currentUser?.email?.endsWith('@competencetrack.org') && currentUser?.email?.startsWith('demo'));
const handleLogout = async () => {
await logout();
toast.success(t('auth.logout'));
};
const toggleLocale = () => {
setLocale(locale === 'de' ? 'en' : 'de');
};
// Track recent actions for command palette
const trackAction = useCallback((key: string, label: string, view?: ViewName) => {
setRecentActions((prev) => {
const filtered = prev.filter((a) => a.key !== key);
return [{ key, label, view, timestamp: Date.now() }, ...filtered].slice(0, 5);
});
}, []);
// View shortcut mapping: Ctrl/Cmd + number -> view
const viewShortcuts: Record = {
1: 'dashboard',
2: 'classes',
3: 'progress',
4: 'flower',
5: 'assessments',
6: 'grading',
7: 'reports',
8: 'competencies',
9: 'settings',
};
// Global keyboard shortcuts
useEffect(() => {
function onKey(e: KeyboardEvent) {
const isMod = e.metaKey || e.ctrlKey;
// Ctrl/Cmd + K: Open command palette
if (isMod && !e.shiftKey && e.key.toLowerCase() === 'k') {
e.preventDefault();
setSearchOpen((v) => !v);
return;
}
// Ctrl/Cmd + /: Show keyboard shortcuts help
if (isMod && e.key.toLowerCase() === '/') {
e.preventDefault();
setShortcutsOpen((v) => !v);
return;
}
// Ctrl/Cmd + Shift + N: Add new page to current notebook (when on notebooks view)
if (isMod && e.shiftKey && e.key.toLowerCase() === 'n') {
e.preventDefault();
if (currentView === 'notebooks') {
window.dispatchEvent(new CustomEvent('ct-shortcut', { detail: 'new-page' }));
return;
}
}
// Ctrl/Cmd + Shift + A: Toggle archive mode (when on notebooks view)
if (isMod && e.shiftKey && e.key.toLowerCase() === 'a') {
e.preventDefault();
if (currentView === 'notebooks') {
window.dispatchEvent(new CustomEvent('ct-shortcut', { detail: 'archive-toggle' }));
return;
}
}
// Ctrl/Cmd + N: Context-dependent new action
if (isMod && !e.shiftKey && e.key.toLowerCase() === 'n') {
e.preventDefault();
setSearchOpen(false);
if (currentView === 'notebooks') {
// Open create notebook dialog
window.dispatchEvent(new CustomEvent('ct-shortcut', { detail: 'new-notebook' }));
} else {
// Default: navigate to progress
setCurrentView('progress');
trackAction('new-entry', t('shortcuts.new_entry'), 'progress');
toast.info(t('shortcuts.new_entry'));
}
return;
}
// Ctrl/Cmd + D: Open drawing canvas
if (isMod && e.key.toLowerCase() === 'd') {
e.preventDefault();
if (currentView === 'notebooks') {
window.dispatchEvent(new CustomEvent('ct-shortcut', { detail: 'drawing' }));
} else {
setCurrentView('drawing');
trackAction('drawing', t('shortcuts.drawing'), 'drawing');
}
return;
}
// Ctrl/Cmd + E: Export current notebook as PDF
if (isMod && e.key.toLowerCase() === 'e') {
e.preventDefault();
if (currentView === 'notebooks') {
window.dispatchEvent(new CustomEvent('ct-shortcut', { detail: 'export-pdf' }));
}
return;
}
// Ctrl/Cmd + .: Close current notebook (go back to library)
if (isMod && e.key === '.') {
e.preventDefault();
if (currentView === 'notebooks') {
window.dispatchEvent(new CustomEvent('ct-shortcut', { detail: 'close-notebook' }));
}
return;
}
// Ctrl/Cmd + 1-9: Switch to specific view
if (isMod && e.key >= '1' && e.key <= '9') {
e.preventDefault();
const view = viewShortcuts[Number(e.key)];
if (view) {
setCurrentView(view);
trackAction(`view-${view}`, t(`nav.${view}`), view);
}
return;
}
// Escape: Close any open dialog
if (e.key === 'Escape') {
if (searchOpen) {
setSearchOpen(false);
} else if (shortcutsOpen) {
setShortcutsOpen(false);
} else if (helpOpen) {
setHelpOpen(false);
}
}
}
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [searchOpen, shortcutsOpen, helpOpen, currentView, setCurrentView, trackAction]);
// Debounced search
useEffect(() => {
if (!searchOpen) {
setSearchQuery('');
setSearchStudents([]);
setSearchClasses([]);
return;
}
if (!searchQuery.trim()) {
setSearchStudents([]);
setSearchClasses([]);
return;
}
const q = searchQuery.trim();
const handle = setTimeout(async () => {
setSearchLoading(true);
try {
const [students, classes] = await Promise.all([
fetchStudents(currentUser?.schoolId ?? undefined, undefined, q).catch(() => []),
fetchClasses(currentUser?.schoolId ?? undefined, undefined).catch(() => []),
]);
const lowerQ = q.toLowerCase();
setSearchStudents(students.slice(0, 6));
setSearchClasses(classes.filter((c) => c.name.toLowerCase().includes(lowerQ)).slice(0, 4));
} finally {
setSearchLoading(false);
}
}, 220);
return () => clearTimeout(handle);
}, [searchQuery, searchOpen, currentUser?.schoolId]);
const breadcrumbLabel = useMemo(() => {
if (currentView === 'student-detail') return t('nav.student-detail');
return t(`nav.${currentView}`);
}, [currentView]);
const handleSearchPickStudent = useCallback((s: Student) => {
setSearchOpen(false);
navigateToStudentDetail(s.id, currentView);
}, [navigateToStudentDetail, currentView]);
const handleSearchPickClass = useCallback((c: ClassGroup) => {
setSearchOpen(false);
setCurrentClass(c.id);
setCurrentView('classes');
}, [setCurrentClass, setCurrentView]);
// Check onboarding status on mount
useEffect(() => {
if (!isOnboardingCompleted()) {
setOnboardingOpen(true);
}
}, []);
// Fetch notifications
const loadNotifications = useCallback(async () => {
if (!currentUser?.schoolId) return;
setNotifLoading(true);
try {
const data = await fetchDBNotifications();
setNotifData(data);
} catch {
// ignore
} finally {
setNotifLoading(false);
}
}, [currentUser?.schoolId]);
useEffect(() => {
loadNotifications();
// Refresh notifications every 60 seconds
const interval = setInterval(loadNotifications, 60000);
return () => clearInterval(interval);
}, [loadNotifications]);
// Handle push notifications in real-time (after loadNotifications is defined)
useEffect(() => {
if (!pushNotification) return;
// Play notification sound if enabled
if (getNotificationSoundPref()) {
playNotificationSound();
}
// Show toast notification
const typeInfo = getNotifTypeInfo(pushNotification.type);
toast(typeInfo.label, {
description: pushNotification.message,
duration: 5000,
});
// Refresh notification data to update the bell count
loadNotifications();
}, [pushNotification, loadNotifications]);
const handleMarkAllRead = useCallback(async () => {
if (!notifData || notifData.unreadCount === 0) return;
try {
await markAllNotificationsRead();
setNotifData((prev) => prev ? {
...prev,
notifications: prev.notifications.map((n) => ({ ...n, isRead: true })),
unreadCount: 0,
} : prev);
} catch {
// ignore
}
}, [notifData]);
const handleMarkSingleRead = useCallback(async (id: string) => {
try {
await markSingleNotificationRead(id);
setNotifData((prev) => {
if (!prev) return prev;
const updated = prev.notifications.map((n) =>
n.id === id ? { ...n, isRead: true } : n
);
return {
...prev,
notifications: updated,
unreadCount: updated.filter((n) => !n.isRead).length,
};
});
} catch {
// ignore
}
}, []);
const handleNotificationClick = useCallback((n: DBNotification) => {
if (!n.isRead) {
handleMarkSingleRead(n.id);
}
setNotifOpen(false);
if (n.actionUrl) {
setCurrentView(n.actionUrl as ViewName);
}
}, [handleMarkSingleRead, setCurrentView]);
// Get icon and color for notification type
const getNotifTypeInfo = (type: string) => {
switch (type) {
case 'ASSESSMENT_DUE':
return { icon: AlertTriangle, bg: 'bg-amber-100/80 dark:bg-amber-900/20', text: 'text-amber-600 dark:text-amber-400', label: t('notifications.type_assessment_due') };
case 'MISSING_OBSERVATION':
return { icon: Info, bg: 'bg-teal-100/80 dark:bg-teal-900/20', text: 'text-teal-600 dark:text-teal-400', label: t('notifications.type_missing_observation') };
case 'NOTEBOOK_SHARED':
return { icon: BookMarked, bg: 'bg-emerald-100/80 dark:bg-emerald-900/20', text: 'text-emerald-600 dark:text-emerald-400', label: t('notifications.type_notebook_shared') };
case 'BEHAVIOR_ALERT':
return { icon: Shield, bg: 'bg-rose-100/80 dark:bg-rose-900/20', text: 'text-rose-600 dark:text-rose-400', label: t('notifications.type_behavior_alert') };
case 'GRADE_COMPUTED':
return { icon: Award, bg: 'bg-violet-100/80 dark:bg-violet-900/20', text: 'text-violet-600 dark:text-violet-400', label: t('notifications.type_grade_computed') };
case 'ATTENDANCE_ALERT':
return { icon: UserCheck, bg: 'bg-orange-100/80 dark:bg-orange-900/20', text: 'text-orange-600 dark:text-orange-400', label: t('notifications.type_attendance_alert') };
case 'REPORT_READY':
return { icon: FileText, bg: 'bg-cyan-100/80 dark:bg-cyan-900/20', text: 'text-cyan-600 dark:text-cyan-400', label: t('notifications.type_report_ready') };
default:
return { icon: Bell, bg: 'bg-gray-100/80 dark:bg-gray-900/20', text: 'text-gray-600 dark:text-gray-400', label: t('notifications.type_general') };
}
};
// Group notifications by date
const groupedNotifications = useMemo(() => {
if (!notifData?.notifications) return [];
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const yesterday = new Date(today.getTime() - 86400000);
const weekAgo = new Date(today.getTime() - 7 * 86400000);
const groups: { label: string; notifications: DBNotification[] }[] = [];
const todayItems: DBNotification[] = [];
const yesterdayItems: DBNotification[] = [];
const weekItems: DBNotification[] = [];
const earlierItems: DBNotification[] = [];
for (const n of notifData.notifications) {
const date = new Date(n.createdAt);
if (date >= today) todayItems.push(n);
else if (date >= yesterday) yesterdayItems.push(n);
else if (date >= weekAgo) weekItems.push(n);
else earlierItems.push(n);
}
if (todayItems.length > 0) groups.push({ label: t('notifications.today'), notifications: todayItems });
if (yesterdayItems.length > 0) groups.push({ label: t('notifications.yesterday'), notifications: yesterdayItems });
if (weekItems.length > 0) groups.push({ label: t('notifications.this_week'), notifications: weekItems });
if (earlierItems.length > 0) groups.push({ label: t('notifications.earlier'), notifications: earlierItems });
return groups;
}, [notifData?.notifications]);
useEffect(() => {
async function loadYears() {
if (!currentUser?.schoolId) return;
try {
const years = await fetchSchoolYears(currentUser.schoolId);
setSchoolYears(years);
if (years.length > 0 && !selectedYearId) {
setSelectedYearId(years[0].id);
setSchoolYearId(years[0].id);
}
} catch {
// ignore
}
}
loadYears();
}, [currentUser?.schoolId]);
// Load school branding and apply CSS custom properties
useEffect(() => {
async function loadBranding() {
if (!currentUser?.schoolId) return;
try {
const schools = await apiGet('/api/schools');
const mySchool = schools.find((s) => s.id === currentUser.schoolId);
if (mySchool) {
setSchoolBranding(mySchool);
// Apply branding CSS variables
const root = document.documentElement;
if (mySchool.primaryColor) root.style.setProperty('--brand-primary', mySchool.primaryColor);
if (mySchool.secondaryColor) root.style.setProperty('--brand-secondary', mySchool.secondaryColor);
if (mySchool.accentColor) root.style.setProperty('--brand-accent', mySchool.accentColor);
if (mySchool.fontFamily) root.style.setProperty('--brand-font', `"${mySchool.fontFamily}", sans-serif`);
// Apply font family globally
if (mySchool.fontFamily) {
root.style.fontFamily = `"${mySchool.fontFamily}", sans-serif`;
}
}
} catch {
// ignore branding errors
}
}
loadBranding();
}, [currentUser?.schoolId]);
return (
{/* Offline indicator bar */}
{/* PWA install prompt */}
{/* Offline sync manager */}
{schoolBranding?.logoUrl ? (
) : (
)}
SchulOS
{/* Favorites Section */}
{t('layout.favorites')}
{[
{ key: 'dashboard' as ViewName, icon: LayoutDashboard, labelKey: 'nav.dashboard' },
{ key: 'grading' as ViewName, icon: Calculator, labelKey: 'nav.grading' },
{ key: 'attendance' as ViewName, icon: CalendarCheck, labelKey: 'nav.attendance' },
].map((fav) => (
setCurrentView(fav.key)}
tooltip={t(fav.labelKey)}
className={`group relative transition-all duration-200 min-h-[36px] ${
currentView === fav.key
? 'bg-gradient-to-r from-emerald-100/90 via-emerald-50/70 to-teal-50/50 dark:from-emerald-900/40 dark:via-emerald-900/25 dark:to-teal-900/15 text-emerald-700 dark:text-emerald-300 font-semibold border-l-3 border-emerald-500 rounded-l-none shadow-sm'
: 'text-gray-600 dark:text-gray-400 hover:text-emerald-600 dark:hover:text-emerald-400 hover:bg-emerald-50/50 dark:hover:bg-emerald-900/10'
}`}
>
{t(fav.labelKey)}
))}
{visibleNavSections.map((section) => (
{section.id === 'analysis' && }
{section.id === 'teaching' && }
{section.id === 'setup' && }
{section.id === 'student-main' && }
{t(section.labelKey)}
{section.items.map((item) => (
setCurrentView(item.key)}
tooltip={t(item.labelKey)}
className={`group relative transition-all duration-200 min-h-[44px] ${
currentView === item.key
? 'bg-gradient-to-r from-emerald-100/90 via-emerald-50/70 to-teal-50/50 dark:from-emerald-900/40 dark:via-emerald-900/25 dark:to-teal-900/15 text-emerald-700 dark:text-emerald-300 font-semibold border-l-3 border-emerald-500 rounded-l-none shadow-sm shadow-emerald-100/40 dark:shadow-emerald-900/20'
: 'text-gray-600 dark:text-gray-400 hover:text-emerald-600 dark:hover:text-emerald-400 hover:bg-emerald-50/50 dark:hover:bg-emerald-900/10 hover:translate-x-0.5'
}`}
>
{currentView === item.key && (
)}
{t(item.labelKey)}
{item.key === 'grading' && (
)}
{item.key === 'attendance' && (
)}
))}
))}
{/* School motto or Environmental message */}
{schoolBranding?.motto || t('sidebar.eco_message')}
{initials}
{displayName}
{isDemoUser && (
{t('badge.demo')}
)}
{t(roleKey)}
setTheme(theme === 'dark' ? 'light' : 'dark')}
title={t('theme.toggle')}
>
{theme === 'dark' ? : }
{/* Announcement Banner */}
setCurrentView('dashboard')}
>
{t('polish.breadcrumb_home')}
{breadcrumbLabel}
{/* Right-aligned action group: Search | Help | School Year | Bell | Lang | Avatar */}
{/* Quick Search (Cmd+K) */}
setSearchOpen(true)}
className="hidden md:inline-flex h-9 w-64 px-3 text-xs text-gray-500 dark:text-gray-400 border-emerald-200/50 dark:border-emerald-900/30 bg-white/60 dark:bg-gray-800/50 hover:bg-emerald-50 dark:hover:bg-emerald-900/20 rounded-lg gap-2 justify-between"
title={t('polish.quick_search')}
>
{t('polish.quick_search')}
K
setSearchOpen(true)}
className="md:hidden h-9 w-9 text-emerald-600 dark:text-emerald-400 border-emerald-200/50 dark:border-emerald-900/30 bg-white/60 dark:bg-gray-800/50 hover:bg-emerald-50 dark:hover:bg-emerald-900/20 rounded-lg"
title={t('polish.quick_search')}
>
{/* Help button — opens help dialog, long-press/Ctrl+click opens onboarding tour */}
setHelpOpen(true)}
onContextMenu={(e) => { e.preventDefault(); setOnboardingOpen(true); }}
title={t('polish.help')}
>
{/* School Year Selector */}
{schoolYears.length > 0 && (
{
setSelectedYearId(v);
setSchoolYearId(v);
}}>
{schoolYears.map((yr) => (
{yr.label}
))}
)}
{/* Notification Bell with Popover */}
0 ? { rotate: [0, -15, 15, -10, 10, 0] } : {}}
transition={{ duration: 0.6, ease: 'easeInOut' }}
>
{notifData && notifData.unreadCount > 0 && (
{notifData.unreadCount > 9 ? '9+' : notifData.unreadCount}
)}
{/* Header */}
{t('notifications.title')}
{notifData && notifData.unreadCount > 0 && (
{t('notifications.mark_all_read')}
)}
{/* Content */}
{notifLoading && (
)}
{!notifLoading && notifData && notifData.notifications.length === 0 && (
{t('notifications.empty')}
)}
{!notifLoading && groupedNotifications.map((group) => (
{group.label}
{group.notifications.map((n: DBNotification, i: number) => {
const typeInfo = getNotifTypeInfo(n.type);
const TypeIcon = typeInfo.icon;
return (
handleNotificationClick(n)}
className={`flex items-start gap-3 px-2 py-2.5 rounded-lg transition-colors cursor-pointer min-h-[44px] hover:bg-emerald-50/50 dark:hover:bg-emerald-900/10 ${!n.isRead ? 'bg-emerald-50/30 dark:bg-emerald-900/5' : 'opacity-70'}`}
>
{n.title}
{n.message}
{typeInfo.label}
{!n.isRead && (
{ e.stopPropagation(); handleMarkSingleRead(n.id); }}
className="h-6 w-6 p-0 text-emerald-600 dark:text-emerald-400 hover:bg-emerald-100 dark:hover:bg-emerald-900/20"
title={t('notifications.mark_read')}
>
)}
);
})}
))}
{/* Footer */}
{!notifLoading && notifData && notifData.notifications.length > 0 && (
{ setNotifOpen(false); setCurrentView('notification-center'); }}
className="h-7 text-xs text-emerald-600 dark:text-emerald-400 hover:bg-emerald-100 dark:hover:bg-emerald-900/20 font-semibold"
>
{t('notif_center.title')}
{ setNotifOpen(false); setCurrentView('progress'); }}
className="h-7 text-xs text-emerald-600 dark:text-emerald-400 hover:bg-emerald-100 dark:hover:bg-emerald-900/20"
>
{t('notifications.record_now')}
{ setNotifOpen(false); setCurrentView('assessments'); }}
className="h-7 text-xs text-emerald-600 dark:text-emerald-400 hover:bg-emerald-100 dark:hover:bg-emerald-900/20"
>
{t('notifications.to_assessment')}
{ setNotifOpen(false); setCurrentView('reports'); }}
className="h-7 text-xs text-emerald-600 dark:text-emerald-400 hover:bg-emerald-100 dark:hover:bg-emerald-900/20"
>
{t('notifications.to_report')}
)}
{/* Language toggle (mobile: just flag) */}
{/* User avatar */}
{initials}
{renderView(currentView)}
{/* ─── Command Palette (Cmd+K) ─────────────────────────── */}
{/* Recent actions */}
{!searchQuery.trim() && recentActions.length > 0 && (
{recentActions.map((action) => (
{
if (action.view) {
setCurrentView(action.view);
trackAction(action.key, action.label, action.view);
}
setSearchOpen(false);
}}
>
{action.label}
{t('shortcuts.recent_actions')}
))}
)}
{/* Quick actions (shown when no search query) */}
{!searchQuery.trim() && (
{
setCurrentView('progress');
trackAction('new-entry', t('shortcuts.new_entry'), 'progress');
setSearchOpen(false);
}}
>
{t('shortcuts.new_entry')}
N
{
setCurrentView('assessments');
trackAction('new-assessment', t('action.create_assessment'), 'assessments');
setSearchOpen(false);
}}
>
{t('action.create_assessment')}
5
{
setCurrentView('reports');
trackAction('new-report', t('action.generate_report'), 'reports');
setSearchOpen(false);
}}
>
{t('action.generate_report')}
7
{
setShortcutsOpen(true);
setSearchOpen(false);
}}
>
{t('shortcuts.show_shortcuts')}
/
)}
{/* Available views (shown when no search query) */}
{!searchQuery.trim() && (
{navSections.flatMap((section) => section.items).map((item, idx) => (
{
setCurrentView(item.key);
trackAction(`view-${item.key}`, t(item.labelKey), item.key);
setSearchOpen(false);
}}
>
{t(item.labelKey)}
{idx < 9 && (
{idx + 1}
)}
))}
)}
{/* Search results: Students */}
{searchStudents.length > 0 && (
{searchStudents.map((s) => (
handleSearchPickStudent(s)}
>
{s.firstName[0]}{s.lastName[0]}
{s.firstName} {s.lastName}
{s.enrollments?.[0]?.classGroup?.name ?? ''}
))}
)}
{/* Search results: Classes */}
{searchClasses.length > 0 && (
{searchClasses.map((c) => (
handleSearchPickClass(c)}
>
{c.name}
{t('label.grade')} {c.gradeLevel}
))}
)}
{/* ─── Keyboard Shortcuts Dialog ────────────────────── */}
{/* ─── Help Modal ─────────────────────────────────────────── */}
{t('polish.help')}
SchulOS · {t('footer.version')}
{[
{ kbd: '⌘K', label: t('shortcuts.command_palette') },
{ kbd: '⌘/', label: t('shortcuts.show_shortcuts') },
{ kbd: '⌘N', label: t('shortcuts.new_entry') },
{ kbd: '⌘1-9', label: t('shortcuts.switch_view') },
{ kbd: 'Esc', label: t('shortcuts.close_dialog') },
].map((row) => (
{row.label}
{row.kbd}
))}
{t('app.tagline')}
{/* Restart onboarding tour */}
{ setHelpOpen(false); setShortcutsOpen(true); }}
className="flex-1 text-xs border-emerald-200/50 dark:border-emerald-900/30 text-emerald-600 dark:text-emerald-400 hover:bg-emerald-50 dark:hover:bg-emerald-900/20"
>
{t('shortcuts.show_shortcuts')}
{ setHelpOpen(false); setOnboardingOpen(true); }}
className="flex-1 text-xs border-emerald-200/50 dark:border-emerald-900/30 text-emerald-600 dark:text-emerald-400 hover:bg-emerald-50 dark:hover:bg-emerald-900/20"
>
Tour
{/* ─── Onboarding Tour ─────────────────────────────────────── */}
setOnboardingOpen(false)} />
{/* ─── AI Chat Widget ─────────────────────────────────────────── */}
{/* ─── Virtual Character ──────────────────────────────────────── */}
);
}