'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, } 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 { 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 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 DashboardView from './dashboard-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 GradingView from './grading-view'; import ReportsView from './reports-view'; import SettingsView from './settings-view'; import StudentDetailView from './student-detail-view'; import AnalyticsView from './analytics-view'; import MasteryMatrixView from './mastery-matrix-view'; import AttendanceView from './attendance-view'; import LessonPlansView from './lesson-plans-view'; import CalendarView from './calendar-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 NotebooksView from './notebooks-view'; import DrawingView from './drawing-view'; import HomeworkView from './homework-view'; import PortfolioView from './portfolio-view'; 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: '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' }, ], }, { id: 'setup', labelKey: 'polish.nav_setup', items: [ { key: 'competencies', icon: BookOpen, labelKey: 'nav.competencies' }, { 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: 'notebooks', icon: BookOpen, labelKey: 'nav.student_notebooks' }, { key: 'flower', icon: Flower2, labelKey: 'nav.student_competencies' }, { key: 'grading', icon: Calculator, labelKey: 'nav.student_grades' }, { 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' }, ], }, ]; // Parent-specific navigation (simplified) const parentNavSections: NavSection[] = [ { id: 'parent-main', labelKey: 'parent.dashboard_title', items: [ { key: 'dashboard', icon: LayoutDashboard, labelKey: 'nav.dashboard' }, { key: 'parents', icon: Mail, labelKey: 'parent.my_children' }, { key: 'parents', icon: MessageSquareText, labelKey: 'parent.messages' }, { key: 'calendar', icon: CalendarIconNav, labelKey: 'nav.calendar' }, ], }, ]; /* ── 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 ? '...' : ''}
); })}
); } function renderView(view: ViewName) { switch (view) { case 'dashboard': 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 ; 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 ?? ''); // 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' ? '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 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]); return ( {/* Offline indicator bar */} {/* PWA install prompt */} {/* Offline sync manager */}

CompetenceTrack

{t('app.subtitle')}

{activeNavSections.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' }`} > {currentView === item.key && ( )} {t(item.labelKey)} ))} ))} {/* Environmental message */}

{t('sidebar.eco_message')}

{initials}

{displayName} {isDemoUser && ( {t('badge.demo')} )}

{t(roleKey)}

{/* Announcement Banner */}
setCurrentView('dashboard')} > {t('polish.breadcrumb_home')} {breadcrumbLabel} {/* Right-aligned action group: Search | Help | School Year | Bell | Lang | Avatar */}
{/* Quick Search (Cmd+K) */} {/* Help button — opens help dialog, long-press/Ctrl+click opens onboarding tour */} {/* School Year Selector */} {schoolYears.length > 0 && ( )} {/* Notification Bell with Popover */} {/* Header */}

{t('notifications.title')}

{notifData && notifData.unreadCount > 0 && ( )}
{/* Content */} {notifLoading && (

Loading…

)} {!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 && ( )}
); })}
))}
{/* Footer */} {!notifLoading && notifData && notifData.notifications.length > 0 && (
)}
{/* Language toggle (mobile: just flag) */} {/* User avatar */} {initials}
{renderView(currentView)}
CompetenceTrack — {t('footer.copyright')} {t('footer.version')} · {t('footer.oss')} {t('footer.version')}
{/* ─── Command Palette (Cmd+K) ─────────────────────────── */}

{t('polish.no_results')}

{/* 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')} CompetenceTrack · {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 */}
{/* ─── Onboarding Tour ─────────────────────────────────────── */} setOnboardingOpen(false)} />
); }