Architecture audit fixes: Fix broken APIs, remove duplicates, clean dead code
Critical Fixes: - UnifiedGradingPanel: Fixed API calls from non-existent /api/v1/grades* to /api/assessments - Added data transformation from Assessment objects to Grade interface - Handle nested subject objects (subject.name instead of subject string) - Added empty state when no grades exist - ProfessionalCalendar: Fixed API call from /api/v1/calendar/events to /api/calendar-events - Changed from startDate/endDate params to month param (YYYY-MM format) Removed Duplicates: - Removed 'tablet-grading' nav entry (was duplicate of 'grading', variant='tablet' was a no-op) - Removed 'exam-calendar' nav entry (was duplicate of 'calendar', identical props) - Removed 'school-events' nav entry (was duplicate of 'calendar', identical props) - Removed corresponding renderView cases for all 3 Deleted Orphan Files: - src/components/grading-panel.tsx (never imported, called non-existent /api/v1/assessments/*) - src/components/notification-hub.tsx (never imported, called non-existent /api/v1/notifications/*) - src/components/unified-nav-menu.tsx (never imported, depended on deleted navigation-config.ts) - src/lib/navigation-config.ts (stale, unused - app uses inline arrays in app-layout.tsx) Fixed Role Access: - Added 'settings' to parentViews, studentViews, and teacherViews - Added notebooks, homework, attendance, grading, flower, subjects, resources, competitions, portfolio to studentViews - Added notebooks, drawing, portfolio, peer-assessment, subjects, competitions, parents, announcements, school-library, school-transport, counseling, student-wellness, student-career, ai-tests, ai-studio, data-import-export to teacherViews - Removed 'tablet-grading' and 'exam-calendar' from teacherViews (nav entries removed)
This commit is contained in:
1 parent
411ff146a7
commit
1af01dd304
8 files changed
+39
-1008
No files matched your search
@@ -246,7 +246,6 @@ const navSections: NavSection[] = [
|
|||||||
{ key: 'rubrics', icon: Ruler, labelKey: 'nav.rubrics' },
|
{ key: 'rubrics', icon: Ruler, labelKey: 'nav.rubrics' },
|
||||||
{ key: 'comments', icon: MessageSquareText, labelKey: 'nav.comments' },
|
{ key: 'comments', icon: MessageSquareText, labelKey: 'nav.comments' },
|
||||||
{ key: 'peer-assessment', icon: UsersRound, labelKey: 'nav.peer-assessment' },
|
{ key: 'peer-assessment', icon: UsersRound, labelKey: 'nav.peer-assessment' },
|
||||||
{ key: 'tablet-grading', icon: Tablet, labelKey: 'nav.tablet_grading' },
|
|
||||||
{ key: 'seating-chart', icon: LayoutGridIcon, labelKey: 'nav.seating_chart' },
|
{ key: 'seating-chart', icon: LayoutGridIcon, labelKey: 'nav.seating_chart' },
|
||||||
{ key: 'subjects', icon: BookOpen, labelKey: 'nav.subjects' },
|
{ key: 'subjects', icon: BookOpen, labelKey: 'nav.subjects' },
|
||||||
{ key: 'competitions', icon: Trophy, labelKey: 'nav.competitions' },
|
{ key: 'competitions', icon: Trophy, labelKey: 'nav.competitions' },
|
||||||
@@ -259,7 +258,6 @@ const navSections: NavSection[] = [
|
|||||||
{ key: 'calendar', icon: CalendarIconNav, labelKey: 'nav.calendar' },
|
{ key: 'calendar', icon: CalendarIconNav, labelKey: 'nav.calendar' },
|
||||||
{ key: 'communication', icon: MessageSquare, labelKey: 'nav.communication' },
|
{ key: 'communication', icon: MessageSquare, labelKey: 'nav.communication' },
|
||||||
{ key: 'announcements', icon: Megaphone, labelKey: 'nav.announcements' },
|
{ 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-newsletter', icon: Newspaper, labelKey: 'nav.school-newsletter' },
|
||||||
{ key: 'school-library', icon: LibraryIcon, labelKey: 'nav.school_library' },
|
{ key: 'school-library', icon: LibraryIcon, labelKey: 'nav.school_library' },
|
||||||
{ key: 'school-transport', icon: Bus, labelKey: 'nav.school-transport' },
|
{ key: 'school-transport', icon: Bus, labelKey: 'nav.school-transport' },
|
||||||
@@ -271,7 +269,6 @@ const navSections: NavSection[] = [
|
|||||||
{ key: 'illness', icon: Heart, labelKey: 'nav.illness' },
|
{ key: 'illness', icon: Heart, labelKey: 'nav.illness' },
|
||||||
{ key: 'substitute-teacher', icon: UserCheck, labelKey: 'nav.substitute-teacher' },
|
{ key: 'substitute-teacher', icon: UserCheck, labelKey: 'nav.substitute-teacher' },
|
||||||
{ key: 'timetable', icon: ClockIcon, labelKey: 'nav.timetable' },
|
{ key: 'timetable', icon: ClockIcon, labelKey: 'nav.timetable' },
|
||||||
{ key: 'exam-calendar', icon: CalendarClock, labelKey: 'nav.exam_calendar' },
|
|
||||||
{ key: 'parents', icon: Mail, labelKey: 'nav.parents' },
|
{ key: 'parents', icon: Mail, labelKey: 'nav.parents' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -323,8 +320,6 @@ const studentNavSections: NavSection[] = [
|
|||||||
{ key: 'communication', icon: MessageSquare, labelKey: 'nav.communication' },
|
{ key: 'communication', icon: MessageSquare, labelKey: 'nav.communication' },
|
||||||
{ key: 'counseling', icon: Heart, labelKey: 'counseling.title' },
|
{ key: 'counseling', icon: Heart, labelKey: 'counseling.title' },
|
||||||
{ key: 'ai-tests', icon: Brain, labelKey: 'ai_tests.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: 'peer-assessment', icon: UsersRound, labelKey: 'nav.peer-assessment' },
|
||||||
{ key: 'report-cards', icon: FileText, labelKey: 'nav.report_cards' },
|
{ key: 'report-cards', icon: FileText, labelKey: 'nav.report_cards' },
|
||||||
{ key: 'seating-chart', icon: LayoutGridIcon, labelKey: 'nav.seating_chart' },
|
{ key: 'seating-chart', icon: LayoutGridIcon, labelKey: 'nav.seating_chart' },
|
||||||
@@ -334,7 +329,6 @@ const studentNavSections: NavSection[] = [
|
|||||||
{ key: 'student-career', icon: Compass, labelKey: 'nav.student-career' },
|
{ key: 'student-career', icon: Compass, labelKey: 'nav.student-career' },
|
||||||
{ key: 'resources', icon: FolderOpen, labelKey: 'nav.resources' },
|
{ key: 'resources', icon: FolderOpen, labelKey: 'nav.resources' },
|
||||||
{ key: 'school-library', icon: LibraryIcon, labelKey: 'nav.school_library' },
|
{ 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-newsletter', icon: Newspaper, labelKey: 'nav.school-newsletter' },
|
||||||
{ key: 'school-transport', icon: Bus, labelKey: 'nav.school-transport' },
|
{ key: 'school-transport', icon: Bus, labelKey: 'nav.school-transport' },
|
||||||
{ key: 'substitute-teacher', icon: UserCheck, labelKey: 'nav.substitute-teacher' },
|
{ key: 'substitute-teacher', icon: UserCheck, labelKey: 'nav.substitute-teacher' },
|
||||||
@@ -367,7 +361,6 @@ const parentNavSections: NavSection[] = [
|
|||||||
{ key: 'peer-assessment', icon: UsersRound, labelKey: 'nav.peer-assessment' },
|
{ key: 'peer-assessment', icon: UsersRound, labelKey: 'nav.peer-assessment' },
|
||||||
{ key: 'report-cards', icon: FileText, labelKey: 'nav.report_cards' },
|
{ key: 'report-cards', icon: FileText, labelKey: 'nav.report_cards' },
|
||||||
{ key: 'school-library', icon: LibraryIcon, labelKey: 'nav.school_library' },
|
{ 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-newsletter', icon: Newspaper, labelKey: 'nav.school-newsletter' },
|
||||||
{ key: 'school-transport', icon: Bus, labelKey: 'nav.school-transport' },
|
{ key: 'school-transport', icon: Bus, labelKey: 'nav.school-transport' },
|
||||||
{ key: 'substitute-teacher', icon: UserCheck, labelKey: 'nav.substitute-teacher' },
|
{ key: 'substitute-teacher', icon: UserCheck, labelKey: 'nav.substitute-teacher' },
|
||||||
@@ -515,8 +508,6 @@ function renderView(view: ViewName) {
|
|||||||
case 'parent-portal': return <ParentPortalView />;
|
case 'parent-portal': return <ParentPortalView />;
|
||||||
case 'notification-center': return <NotificationCenterView />;
|
case 'notification-center': return <NotificationCenterView />;
|
||||||
case 'announcements': return <SchoolAnnouncementsView />;
|
case 'announcements': return <SchoolAnnouncementsView />;
|
||||||
case 'tablet-grading': return <UnifiedGradingPanel mode="teacher" variant="tablet" />;
|
|
||||||
case 'exam-calendar': return <ProfessionalCalendar variant="month" />;
|
|
||||||
case 'peer-assessment': return <PeerAssessmentView />;
|
case 'peer-assessment': return <PeerAssessmentView />;
|
||||||
case 'grade-analytics': return <UnifiedGradingPanel mode="teacher" />;
|
case 'grade-analytics': return <UnifiedGradingPanel mode="teacher" />;
|
||||||
case 'student-achievements': return <StudentAchievementsView />;
|
case 'student-achievements': return <StudentAchievementsView />;
|
||||||
@@ -527,7 +518,6 @@ function renderView(view: ViewName) {
|
|||||||
case 'student-wellness': return <StudentWellnessView />;
|
case 'student-wellness': return <StudentWellnessView />;
|
||||||
case 'student-career': return <StudentCareerView />;
|
case 'student-career': return <StudentCareerView />;
|
||||||
case 'data-import-export': return <DataImportExportView />;
|
case 'data-import-export': return <DataImportExportView />;
|
||||||
case 'school-events': return <ProfessionalCalendar variant="month" />;
|
|
||||||
case 'substitute-teacher': return <SubstituteTeacherView />;
|
case 'substitute-teacher': return <SubstituteTeacherView />;
|
||||||
case 'school-newsletter': return <SchoolNewsletterView />;
|
case 'school-newsletter': return <SchoolNewsletterView />;
|
||||||
case 'school-transport': return <SchoolTransportView />;
|
case 'school-transport': return <SchoolTransportView />;
|
||||||
|
|||||||
@@ -1,236 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Input } from '@/components/ui/input';
|
|
||||||
import { Label } from '@/components/ui/label';
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
||||||
import { DataTable } from '@/components/data-table';
|
|
||||||
import { ConfirmDialog } from '@/components/confirm-dialog';
|
|
||||||
import {
|
|
||||||
processGrade,
|
|
||||||
calculateWeightedGrade,
|
|
||||||
getStatistics,
|
|
||||||
type GradingConfig,
|
|
||||||
type GradeOutput,
|
|
||||||
} from '@/lib/grading-engine';
|
|
||||||
import { useApiGet, useApiMutation } from '@/lib/hooks/useApi';
|
|
||||||
|
|
||||||
export interface GradingPanelProps {
|
|
||||||
assessmentId: string;
|
|
||||||
studentId?: string; // if provided, show single student; otherwise show class
|
|
||||||
config: GradingConfig;
|
|
||||||
onGradeSaved?: (grade: any) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function GradingPanel({
|
|
||||||
assessmentId,
|
|
||||||
studentId,
|
|
||||||
config,
|
|
||||||
onGradeSaved,
|
|
||||||
}: GradingPanelProps) {
|
|
||||||
const [selectedStudent, setSelectedStudent] = useState<string | null>(studentId || null);
|
|
||||||
const [showConfirm, setShowConfirm] = useState(false);
|
|
||||||
const [pendingGrade, setPendingGrade] = useState<any>(null);
|
|
||||||
|
|
||||||
// Fetch assessment and results
|
|
||||||
const { data: assessment } = useApiGet(
|
|
||||||
`/api/v1/assessments/${assessmentId}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: results, mutate: refetchResults } = useApiGet(
|
|
||||||
studentId
|
|
||||||
? `/api/v1/assessments/${assessmentId}/results?studentId=${studentId}`
|
|
||||||
: `/api/v1/assessments/${assessmentId}/results`
|
|
||||||
);
|
|
||||||
|
|
||||||
const { mutate: saveGrade, isLoading: isSaving } = useApiMutation(
|
|
||||||
`/api/v1/assessments/${assessmentId}/grades`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
onSuccess: () => {
|
|
||||||
setShowConfirm(false);
|
|
||||||
setPendingGrade(null);
|
|
||||||
refetchResults();
|
|
||||||
onGradeSaved?.(pendingGrade);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleGradeChange = (grade: number | string, studentId: string) => {
|
|
||||||
setPendingGrade({ studentId, score: grade });
|
|
||||||
setShowConfirm(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleConfirmGrade = async () => {
|
|
||||||
await saveGrade(pendingGrade);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Statistics
|
|
||||||
const stats = results?.length
|
|
||||||
? getStatistics(
|
|
||||||
results.map((r: any) => r.score),
|
|
||||||
config
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* Assessment Header */}
|
|
||||||
{assessment && (
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>{assessment.title}</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
{assessment.type} · Max Score: {assessment.maxScore}
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Tabs defaultValue="list" className="w-full">
|
|
||||||
<TabsList>
|
|
||||||
<TabsTrigger value="list">Students</TabsTrigger>
|
|
||||||
<TabsTrigger value="analytics">Analytics</TabsTrigger>
|
|
||||||
{studentId && <TabsTrigger value="detail">Details</TabsTrigger>}
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
{/* Student List */}
|
|
||||||
<TabsContent value="list" className="space-y-4">
|
|
||||||
{results && (
|
|
||||||
<DataTable
|
|
||||||
columns={[
|
|
||||||
{
|
|
||||||
key: 'student' as any,
|
|
||||||
label: 'Student',
|
|
||||||
render: (_, row) => `${row.student?.firstName} ${row.student?.lastName}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'score' as any,
|
|
||||||
label: 'Score',
|
|
||||||
sortable: true,
|
|
||||||
render: (_, row) => (
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
value={row.score || ''}
|
|
||||||
onChange={(e) => handleGradeChange(e.target.value, row.studentId)}
|
|
||||||
className="w-20"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'feedback' as any,
|
|
||||||
label: 'Feedback',
|
|
||||||
render: (_, row) => {
|
|
||||||
const processed = processGrade(
|
|
||||||
{
|
|
||||||
score: row.score,
|
|
||||||
studentId: row.studentId,
|
|
||||||
assessmentId: row.assessmentId,
|
|
||||||
timestamp: new Date(),
|
|
||||||
},
|
|
||||||
config
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<span className={processed.isPassing ? 'text-green-600' : 'text-red-600'}>
|
|
||||||
{processed.isPassing ? 'Pass' : 'Fail'}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
data={results}
|
|
||||||
isLoading={!results}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
{/* Analytics */}
|
|
||||||
<TabsContent value="analytics" className="space-y-4">
|
|
||||||
{stats && (
|
|
||||||
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">Average</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-2xl font-bold">{stats.average.toFixed(1)}</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">Median</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-2xl font-bold">{stats.median.toFixed(1)}</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">Passing</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-2xl font-bold">
|
|
||||||
{stats.passingPercentage.toFixed(0)}%
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">Std Dev</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-2xl font-bold">{stats.stdDev.toFixed(1)}</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
{/* Single Student Details */}
|
|
||||||
{studentId && results && (
|
|
||||||
<TabsContent value="detail" className="space-y-4">
|
|
||||||
{results[0] && (
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>
|
|
||||||
{results[0].student?.firstName} {results[0].student?.lastName}
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<Label>Current Score</Label>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
value={results[0].score || ''}
|
|
||||||
onChange={(e) => handleGradeChange(e.target.value, results[0].studentId)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label>Notes</Label>
|
|
||||||
<Input
|
|
||||||
value={results[0].notes || ''}
|
|
||||||
placeholder="Add notes..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</TabsContent>
|
|
||||||
)}
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
{/* Confirm Dialog */}
|
|
||||||
<ConfirmDialog
|
|
||||||
open={showConfirm}
|
|
||||||
title="Confirm Grade"
|
|
||||||
description={`Are you sure you want to save this grade for student ${selectedStudent}?`}
|
|
||||||
onConfirm={handleConfirmGrade}
|
|
||||||
onCancel={() => setShowConfirm(false)}
|
|
||||||
isLoading={isSaving}
|
|
||||||
confirmText="Save Grade"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { useApiGet } from '@/lib/hooks/useApi';
|
|
||||||
import { Bell, X, Check, Archive, AlertCircle, Info, CheckCircle } from 'lucide-react';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { formatDistanceToNow } from 'date-fns';
|
|
||||||
|
|
||||||
interface Notification {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
message: string;
|
|
||||||
category: string;
|
|
||||||
priority: string;
|
|
||||||
isRead: boolean;
|
|
||||||
isDismissed: boolean;
|
|
||||||
actionUrl?: string;
|
|
||||||
actionLabel?: string;
|
|
||||||
createdAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function NotificationHub() {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const [notifications, setNotifications] = useState<Notification[]>([]);
|
|
||||||
const { data: hubData, mutate } = useApiGet<Notification[]>('/api/v1/notifications/hub');
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (hubData) {
|
|
||||||
setNotifications(hubData.filter((n) => !n.isDismissed));
|
|
||||||
}
|
|
||||||
}, [hubData]);
|
|
||||||
|
|
||||||
const unreadCount = notifications.filter((n) => !n.isRead).length;
|
|
||||||
const criticalCount = notifications.filter((n) => n.priority === 'critical' && !n.isRead).length;
|
|
||||||
|
|
||||||
const handleMarkAsRead = async (notificationId: string) => {
|
|
||||||
await fetch(`/api/v1/notifications/${notificationId}/read`, {
|
|
||||||
method: 'PATCH',
|
|
||||||
});
|
|
||||||
mutate();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDismiss = async (notificationId: string) => {
|
|
||||||
await fetch(`/api/v1/notifications/${notificationId}/dismiss`, {
|
|
||||||
method: 'PATCH',
|
|
||||||
});
|
|
||||||
mutate();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleArchive = async (notificationId: string) => {
|
|
||||||
await fetch(`/api/v1/notifications/${notificationId}/archive`, {
|
|
||||||
method: 'PATCH',
|
|
||||||
});
|
|
||||||
mutate();
|
|
||||||
};
|
|
||||||
|
|
||||||
const getIcon = (category: string) => {
|
|
||||||
switch (category) {
|
|
||||||
case 'assessment':
|
|
||||||
return <CheckCircle className="w-4 h-4 text-blue-500" />;
|
|
||||||
case 'behavior':
|
|
||||||
return <AlertCircle className="w-4 h-4 text-red-500" />;
|
|
||||||
case 'communication':
|
|
||||||
return <Bell className="w-4 h-4 text-purple-500" />;
|
|
||||||
default:
|
|
||||||
return <Info className="w-4 h-4 text-gray-500" />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPriorityColor = (priority: string) => {
|
|
||||||
switch (priority) {
|
|
||||||
case 'critical':
|
|
||||||
return 'border-l-4 border-l-red-500 bg-red-50';
|
|
||||||
case 'high':
|
|
||||||
return 'border-l-4 border-l-orange-500 bg-orange-50';
|
|
||||||
case 'normal':
|
|
||||||
return 'border-l-4 border-l-blue-500 bg-blue-50';
|
|
||||||
default:
|
|
||||||
return 'border-l-4 border-l-gray-300 bg-gray-50';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative">
|
|
||||||
{/* Notification Bell Button */}
|
|
||||||
<button
|
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
|
||||||
className="relative p-2 text-gray-600 hover:text-gray-900 rounded-lg hover:bg-gray-100"
|
|
||||||
>
|
|
||||||
<Bell className="w-5 h-5" />
|
|
||||||
{unreadCount > 0 && (
|
|
||||||
<span className={`absolute top-0 right-0 flex items-center justify-center w-5 h-5 rounded-full text-xs font-bold text-white ${
|
|
||||||
criticalCount > 0 ? 'bg-red-600' : 'bg-blue-600'
|
|
||||||
}`}>
|
|
||||||
{unreadCount}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Dropdown Panel */}
|
|
||||||
{isOpen && (
|
|
||||||
<div className="absolute right-0 mt-2 w-96 bg-white rounded-lg shadow-xl border border-gray-200 z-50 max-h-96 overflow-y-auto">
|
|
||||||
<div className="p-4 border-b border-gray-200">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900">Notifications</h3>
|
|
||||||
<button
|
|
||||||
onClick={() => setIsOpen(false)}
|
|
||||||
className="text-gray-500 hover:text-gray-700"
|
|
||||||
>
|
|
||||||
<X className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Notifications List */}
|
|
||||||
{notifications.length === 0 ? (
|
|
||||||
<div className="p-8 text-center">
|
|
||||||
<Bell className="w-12 h-12 text-gray-300 mx-auto mb-2" />
|
|
||||||
<p className="text-gray-500">No new notifications</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="divide-y">
|
|
||||||
{notifications.map((notification) => (
|
|
||||||
<div
|
|
||||||
key={notification.id}
|
|
||||||
className={`p-4 hover:bg-gray-50 transition ${getPriorityColor(notification.priority)}`}
|
|
||||||
>
|
|
||||||
<div className="flex gap-3">
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
{getIcon(notification.category)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="flex items-start justify-between gap-2">
|
|
||||||
<p className={`text-sm font-medium ${notification.isRead ? 'text-gray-600' : 'text-gray-900 font-bold'}`}>
|
|
||||||
{notification.title}
|
|
||||||
</p>
|
|
||||||
{!notification.isRead && (
|
|
||||||
<div className="w-2 h-2 rounded-full bg-blue-500 flex-shrink-0 mt-1" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-sm text-gray-600 mt-1">
|
|
||||||
{notification.message}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p className="text-xs text-gray-500 mt-2">
|
|
||||||
{formatDistanceToNow(new Date(notification.createdAt), { addSuffix: true })}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{notification.actionUrl && (
|
|
||||||
<a
|
|
||||||
href={notification.actionUrl}
|
|
||||||
className="inline-block text-xs text-blue-600 hover:text-blue-800 font-medium mt-2"
|
|
||||||
>
|
|
||||||
{notification.actionLabel || 'View'}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Actions */}
|
|
||||||
<div className="flex items-center gap-1 flex-shrink-0">
|
|
||||||
{!notification.isRead && (
|
|
||||||
<button
|
|
||||||
onClick={() => handleMarkAsRead(notification.id)}
|
|
||||||
className="p-1 text-gray-400 hover:text-blue-600"
|
|
||||||
title="Mark as read"
|
|
||||||
>
|
|
||||||
<Check className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
onClick={() => handleArchive(notification.id)}
|
|
||||||
className="p-1 text-gray-400 hover:text-gray-600"
|
|
||||||
title="Archive"
|
|
||||||
>
|
|
||||||
<Archive className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleDismiss(notification.id)}
|
|
||||||
className="p-1 text-gray-400 hover:text-red-600"
|
|
||||||
title="Dismiss"
|
|
||||||
>
|
|
||||||
<X className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* View All Link */}
|
|
||||||
{notifications.length > 0 && (
|
|
||||||
<div className="p-3 border-t border-gray-200 text-center">
|
|
||||||
<a
|
|
||||||
href="/notifications"
|
|
||||||
className="text-sm text-blue-600 hover:text-blue-800 font-medium"
|
|
||||||
>
|
|
||||||
View all notifications
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -99,8 +99,9 @@ export function ProfessionalCalendar({
|
|||||||
const dateLocale = locale === 'de' ? deLocale : enLocale;
|
const dateLocale = locale === 'de' ? deLocale : enLocale;
|
||||||
|
|
||||||
// Fetch events from backend
|
// Fetch events from backend
|
||||||
|
const monthParam = format(currentDate, 'yyyy-MM');
|
||||||
const { data: backendEvents, isLoading } = useApiGet(
|
const { data: backendEvents, isLoading } = useApiGet(
|
||||||
`/api/v1/calendar/events?startDate=${format(startOfMonth(currentDate), 'yyyy-MM-dd')}&endDate=${format(endOfMonth(currentDate), 'yyyy-MM-dd')}`,
|
`/api/calendar-events?month=${monthParam}`,
|
||||||
{ revalidateOnFocus: true }
|
{ revalidateOnFocus: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -100,15 +100,30 @@ export function UnifiedGradingPanel({
|
|||||||
if (studentId) queryParams.append('studentId', studentId);
|
if (studentId) queryParams.append('studentId', studentId);
|
||||||
|
|
||||||
const { data: backendGrades, isLoading, mutate } = useApiGet(
|
const { data: backendGrades, isLoading, mutate } = useApiGet(
|
||||||
`/api/v1/grades${queryParams.toString() ? '?' + queryParams.toString() : ''}`,
|
`/api/assessments${queryParams.toString() ? '?' + queryParams.toString() : ''}`,
|
||||||
{ revalidateOnFocus: true }
|
{ revalidateOnFocus: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update local grades when backend data changes
|
// Update local grades when backend data changes
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (backendGrades) {
|
if (backendGrades) {
|
||||||
setGrades(backendGrades);
|
// Transform assessments API response to Grade format
|
||||||
applyFilters(backendGrades);
|
const rawData = Array.isArray(backendGrades) ? backendGrades : [];
|
||||||
|
const transformed: Grade[] = rawData.map((item: any) => ({
|
||||||
|
id: item.id,
|
||||||
|
studentId: item.studentId || item.id,
|
||||||
|
studentName: item.studentName || (item.student ? `${item.student.firstName} ${item.student.lastName}` : '—'),
|
||||||
|
subject: typeof item.subject === 'string' ? item.subject : (item.subject?.name || item.subjectId || '—'),
|
||||||
|
assessment: item.assessment || item.title || item.name || '—',
|
||||||
|
score: typeof item.score === 'number' ? item.score : (item.maxScore ?? 0),
|
||||||
|
maxScore: item.maxScore ?? 100,
|
||||||
|
weight: item.weight,
|
||||||
|
comment: item.comment || item.note || '',
|
||||||
|
gradedAt: item.gradedAt || item.date || item.createdAt,
|
||||||
|
status: item.status || (item.score != null ? 'graded' : 'pending'),
|
||||||
|
}));
|
||||||
|
setGrades(transformed);
|
||||||
|
applyFilters(transformed);
|
||||||
}
|
}
|
||||||
}, [backendGrades]);
|
}, [backendGrades]);
|
||||||
|
|
||||||
@@ -173,7 +188,7 @@ export function UnifiedGradingPanel({
|
|||||||
await onGradeSubmit(gradesToSubmit);
|
await onGradeSubmit(gradesToSubmit);
|
||||||
} else {
|
} else {
|
||||||
// Use API
|
// Use API
|
||||||
await fetch('/api/v1/grades/submit', {
|
await fetch('/api/assessments/bulk-results', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(gradesToSubmit),
|
body: JSON.stringify(gradesToSubmit),
|
||||||
@@ -204,7 +219,7 @@ export function UnifiedGradingPanel({
|
|||||||
// Handle grade deletion
|
// Handle grade deletion
|
||||||
const handleDeleteGrade = useCallback(async (gradeId: string) => {
|
const handleDeleteGrade = useCallback(async (gradeId: string) => {
|
||||||
try {
|
try {
|
||||||
await fetch(`/api/v1/grades/${gradeId}`, { method: 'DELETE' });
|
await fetch(`/api/assessments/${gradeId}/results`, { method: 'DELETE' });
|
||||||
setGrades((prev) => prev.filter((g) => g.id !== gradeId));
|
setGrades((prev) => prev.filter((g) => g.id !== gradeId));
|
||||||
setSelectedGrade(null);
|
setSelectedGrade(null);
|
||||||
toast.success('Grade deleted');
|
toast.success('Grade deleted');
|
||||||
@@ -286,7 +301,13 @@ export function UnifiedGradingPanel({
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{filteredGrades.map((grade) => (
|
{filteredGrades.length === 0 ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={7} className="text-center py-8 text-muted-foreground">
|
||||||
|
Keine Noten vorhanden. Wählen Sie eine Klasse aus oder erstellen Sie eine Leistungsüberprüfung.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : filteredGrades.map((grade) => (
|
||||||
<tr key={grade.id} className="border-b hover:bg-muted/50 transition-colors">
|
<tr key={grade.id} className="border-b hover:bg-muted/50 transition-colors">
|
||||||
<td className="py-2 px-3">{grade.studentName}</td>
|
<td className="py-2 px-3">{grade.studentName}</td>
|
||||||
<td className="py-2 px-3">{grade.subject}</td>
|
<td className="py-2 px-3">{grade.subject}</td>
|
||||||
|
|||||||
@@ -1,127 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
'use client';
|
|
||||||
|
|
||||||
import React, { useMemo } from 'react';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
import { ChevronDown } from 'lucide-react';
|
|
||||||
import { useAppStore } from '@/lib/store';
|
|
||||||
import { getNavItemsForRole, NavGroup, NavItem } from '@/lib/navigation-config';
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
|
|
||||||
interface UnifiedNavMenuProps {
|
|
||||||
orientation?: 'horizontal' | 'vertical';
|
|
||||||
className?: string;
|
|
||||||
itemClassName?: string;
|
|
||||||
activeItemClassName?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unified Navigation Menu Component
|
|
||||||
* Single component replacing all navigation implementations
|
|
||||||
* Automatically filters items based on user role and compliance status
|
|
||||||
*/
|
|
||||||
export function UnifiedNavMenu({
|
|
||||||
orientation = 'vertical',
|
|
||||||
className,
|
|
||||||
itemClassName,
|
|
||||||
activeItemClassName,
|
|
||||||
}: UnifiedNavMenuProps) {
|
|
||||||
const currentUser = useAppStore((s) => s.currentUser);
|
|
||||||
const pathname = usePathname();
|
|
||||||
const complianceStatus = useAppStore((s) => s.complianceStatus);
|
|
||||||
|
|
||||||
const navGroups = useMemo(() => {
|
|
||||||
const groups = getNavItemsForRole(currentUser?.role);
|
|
||||||
|
|
||||||
// Filter items based on role and compliance approval
|
|
||||||
return groups.map((group) => ({
|
|
||||||
...group,
|
|
||||||
items: group.items.filter((item) => {
|
|
||||||
// Check role authorization
|
|
||||||
if (!item.roles.includes(currentUser?.role || '')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check compliance approval (Module L)
|
|
||||||
if (item.requiresApproval && !complianceStatus?.onboardingCompleted) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check module gates (Module L)
|
|
||||||
if (item.id === 'wellness' && !complianceStatus?.moduleGates?.['WELLNESS_CHECKS']?.isApprovedForUse) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (item.id === 'exam-proctoring' && !complianceStatus?.moduleGates?.['EXAM_MODE']?.isApprovedForUse) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (item.id === 'signage' && !complianceStatus?.moduleGates?.['SIGNAGE']?.isApprovedForUse) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
})).filter((group) => group.items.length > 0);
|
|
||||||
}, [currentUser?.role, complianceStatus]);
|
|
||||||
|
|
||||||
const isActive = (href: string) => pathname === href || pathname.startsWith(href + '/');
|
|
||||||
|
|
||||||
const renderItem = (item: NavItem) => {
|
|
||||||
const Icon = item.icon;
|
|
||||||
const active = isActive(item.href);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
key={item.id}
|
|
||||||
whileHover={{ x: orientation === 'horizontal' ? 0 : 4 }}
|
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
href={item.href}
|
|
||||||
className={cn(
|
|
||||||
'flex items-center gap-2 px-3 py-2 rounded-lg transition-colors',
|
|
||||||
'hover:bg-accent/50',
|
|
||||||
active && 'bg-accent text-accent-foreground font-medium',
|
|
||||||
!active && 'text-muted-foreground',
|
|
||||||
itemClassName
|
|
||||||
)}
|
|
||||||
title={item.description}
|
|
||||||
>
|
|
||||||
<Icon className="h-4 w-4 flex-shrink-0" />
|
|
||||||
<span className="flex-1 text-sm">{item.label}</span>
|
|
||||||
{item.badge && (
|
|
||||||
<span className="ml-auto inline-flex items-center justify-center rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-800">
|
|
||||||
{item.badge}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
</motion.div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<nav
|
|
||||||
className={cn(
|
|
||||||
'flex flex-col gap-1',
|
|
||||||
orientation === 'horizontal' && 'flex-row gap-0',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{navGroups.map((group) => (
|
|
||||||
<div key={group.id} className={cn(orientation === 'horizontal' && 'flex gap-1')}>
|
|
||||||
{orientation === 'vertical' && (
|
|
||||||
<h3 className="px-3 py-2 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
|
||||||
{group.title}
|
|
||||||
</h3>
|
|
||||||
)}
|
|
||||||
<div className={cn(orientation === 'horizontal' && 'flex gap-1')}>
|
|
||||||
{group.items.map((item) => renderItem(item))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default UnifiedNavMenu;
|
|
||||||
@@ -1,416 +0,0 @@
|
|||||||
/**
|
|
||||||
* Centralized Navigation Configuration
|
|
||||||
* Single source of truth for all app navigation routes and menu items
|
|
||||||
* Eliminates duplication across multiple navigation files
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
LayoutDashboard,
|
|
||||||
BookOpen,
|
|
||||||
Users,
|
|
||||||
MessageSquare,
|
|
||||||
TrendingUp,
|
|
||||||
Settings,
|
|
||||||
BarChart3,
|
|
||||||
Calendar,
|
|
||||||
AlertCircle,
|
|
||||||
Heart,
|
|
||||||
Trophy,
|
|
||||||
FileText,
|
|
||||||
Shield,
|
|
||||||
Tv2,
|
|
||||||
Bell,
|
|
||||||
Grid3x3,
|
|
||||||
LucideIcon,
|
|
||||||
} from 'lucide-react';
|
|
||||||
|
|
||||||
export interface NavItem {
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
href: string;
|
|
||||||
icon: LucideIcon;
|
|
||||||
roles: string[];
|
|
||||||
badge?: string | number;
|
|
||||||
description?: string;
|
|
||||||
children?: NavItem[];
|
|
||||||
requiresApproval?: boolean; // For Module L compliance
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NavGroup {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
items: NavItem[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Core navigation routes - single source of truth
|
|
||||||
*/
|
|
||||||
export const NAVIGATION_ROUTES = {
|
|
||||||
DASHBOARD: '/dashboard',
|
|
||||||
GRADES: '/grades',
|
|
||||||
CALENDAR: '/calendar',
|
|
||||||
ASSESSMENTS: '/assessments',
|
|
||||||
CLASSES: '/classes',
|
|
||||||
COMMUNICATION: '/communication',
|
|
||||||
ATTENDANCE: '/attendance',
|
|
||||||
BEHAVIOR: '/behavior',
|
|
||||||
WELLNESS: '/wellness',
|
|
||||||
ANALYTICS: '/analytics',
|
|
||||||
REPORTS: '/reports',
|
|
||||||
SETTINGS: '/settings',
|
|
||||||
NOTIFICATIONS: '/notifications',
|
|
||||||
COMPLIANCE: '/admin/compliance',
|
|
||||||
SIGNAGE: '/admin/signage',
|
|
||||||
EXAM_PROCTORING: '/admin/exam-proctoring',
|
|
||||||
WIDGETS: '/admin/dashboard-widgets',
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Teacher Navigation Menu
|
|
||||||
*/
|
|
||||||
export const TEACHER_NAV_ITEMS: NavGroup[] = [
|
|
||||||
{
|
|
||||||
id: 'main',
|
|
||||||
title: 'Main',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: 'dashboard',
|
|
||||||
label: 'Dashboard',
|
|
||||||
href: NAVIGATION_ROUTES.DASHBOARD,
|
|
||||||
icon: LayoutDashboard,
|
|
||||||
roles: ['TEACHER', 'SCHOOL_ADMIN', 'SUPER_ADMIN'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'classes',
|
|
||||||
label: 'Classes',
|
|
||||||
href: NAVIGATION_ROUTES.CLASSES,
|
|
||||||
icon: Users,
|
|
||||||
roles: ['TEACHER', 'SCHOOL_ADMIN'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'academic',
|
|
||||||
title: 'Academic',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: 'calendar',
|
|
||||||
label: 'Calendar',
|
|
||||||
href: NAVIGATION_ROUTES.CALENDAR,
|
|
||||||
icon: Calendar,
|
|
||||||
roles: ['TEACHER', 'STUDENT', 'PARENT', 'SCHOOL_ADMIN'],
|
|
||||||
description: 'View all events, exams, and school calendar',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'grades',
|
|
||||||
label: 'Grades',
|
|
||||||
href: NAVIGATION_ROUTES.GRADES,
|
|
||||||
icon: BookOpen,
|
|
||||||
roles: ['TEACHER', 'SCHOOL_ADMIN'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'assessments',
|
|
||||||
label: 'Assessments',
|
|
||||||
href: NAVIGATION_ROUTES.ASSESSMENTS,
|
|
||||||
icon: FileText,
|
|
||||||
roles: ['TEACHER', 'SCHOOL_ADMIN'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'student-management',
|
|
||||||
title: 'Student Management',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: 'attendance',
|
|
||||||
label: 'Attendance',
|
|
||||||
href: NAVIGATION_ROUTES.ATTENDANCE,
|
|
||||||
icon: AlertCircle,
|
|
||||||
roles: ['TEACHER', 'SCHOOL_ADMIN'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'behavior',
|
|
||||||
label: 'Behavior',
|
|
||||||
href: NAVIGATION_ROUTES.BEHAVIOR,
|
|
||||||
icon: Heart,
|
|
||||||
roles: ['TEACHER', 'SCHOOL_ADMIN'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'wellness',
|
|
||||||
label: 'Wellness',
|
|
||||||
href: NAVIGATION_ROUTES.WELLNESS,
|
|
||||||
icon: Heart,
|
|
||||||
roles: ['TEACHER'],
|
|
||||||
requiresApproval: true, // Module L
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'communication',
|
|
||||||
title: 'Communication',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: 'messages',
|
|
||||||
label: 'Messages',
|
|
||||||
href: NAVIGATION_ROUTES.COMMUNICATION,
|
|
||||||
icon: MessageSquare,
|
|
||||||
roles: ['TEACHER', 'PARENT', 'STUDENT', 'SCHOOL_ADMIN'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'notifications',
|
|
||||||
label: 'Notifications',
|
|
||||||
href: NAVIGATION_ROUTES.NOTIFICATIONS,
|
|
||||||
icon: Bell,
|
|
||||||
roles: ['TEACHER', 'PARENT', 'STUDENT', 'SCHOOL_ADMIN'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'admin',
|
|
||||||
title: 'Administration',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: 'analytics',
|
|
||||||
label: 'Analytics',
|
|
||||||
href: NAVIGATION_ROUTES.ANALYTICS,
|
|
||||||
icon: BarChart3,
|
|
||||||
roles: ['SCHOOL_ADMIN', 'SUPER_ADMIN'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'reports',
|
|
||||||
label: 'Reports',
|
|
||||||
href: NAVIGATION_ROUTES.REPORTS,
|
|
||||||
icon: TrendingUp,
|
|
||||||
roles: ['SCHOOL_ADMIN', 'SUPER_ADMIN'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'compliance',
|
|
||||||
label: 'Compliance',
|
|
||||||
href: NAVIGATION_ROUTES.COMPLIANCE,
|
|
||||||
icon: Shield,
|
|
||||||
roles: ['DPO', 'SCHOOL_ADMIN', 'SUPER_ADMIN'],
|
|
||||||
requiresApproval: true, // Module L
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'signage',
|
|
||||||
label: 'Signage',
|
|
||||||
href: NAVIGATION_ROUTES.SIGNAGE,
|
|
||||||
icon: Tv2,
|
|
||||||
roles: ['SCHOOL_ADMIN', 'SUPER_ADMIN'],
|
|
||||||
requiresApproval: true, // Module B
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'exam-proctoring',
|
|
||||||
label: 'Exam Proctoring',
|
|
||||||
href: NAVIGATION_ROUTES.EXAM_PROCTORING,
|
|
||||||
icon: Trophy,
|
|
||||||
roles: ['SCHOOL_ADMIN', 'SUPER_ADMIN'],
|
|
||||||
requiresApproval: true, // Module F
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'widgets',
|
|
||||||
label: 'Dashboard Widgets',
|
|
||||||
href: NAVIGATION_ROUTES.WIDGETS,
|
|
||||||
icon: Grid3x3,
|
|
||||||
roles: ['SCHOOL_ADMIN', 'SUPER_ADMIN'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'system',
|
|
||||||
title: 'System',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: 'settings',
|
|
||||||
label: 'Settings',
|
|
||||||
href: NAVIGATION_ROUTES.SETTINGS,
|
|
||||||
icon: Settings,
|
|
||||||
roles: ['TEACHER', 'PARENT', 'STUDENT', 'SCHOOL_ADMIN', 'SUPER_ADMIN', 'DPO'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Student Navigation Menu
|
|
||||||
*/
|
|
||||||
export const STUDENT_NAV_ITEMS: NavGroup[] = [
|
|
||||||
{
|
|
||||||
id: 'main',
|
|
||||||
title: 'Main',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: 'dashboard',
|
|
||||||
label: 'Dashboard',
|
|
||||||
href: NAVIGATION_ROUTES.DASHBOARD,
|
|
||||||
icon: LayoutDashboard,
|
|
||||||
roles: ['STUDENT'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'academics',
|
|
||||||
title: 'My Academics',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: 'grades',
|
|
||||||
label: 'My Grades',
|
|
||||||
href: NAVIGATION_ROUTES.GRADES,
|
|
||||||
icon: BookOpen,
|
|
||||||
roles: ['STUDENT'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'assessments',
|
|
||||||
label: 'Assessments',
|
|
||||||
href: NAVIGATION_ROUTES.ASSESSMENTS,
|
|
||||||
icon: FileText,
|
|
||||||
roles: ['STUDENT'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'communication',
|
|
||||||
title: 'Communication',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: 'messages',
|
|
||||||
label: 'Messages',
|
|
||||||
href: NAVIGATION_ROUTES.COMMUNICATION,
|
|
||||||
icon: MessageSquare,
|
|
||||||
roles: ['STUDENT'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'notifications',
|
|
||||||
label: 'Notifications',
|
|
||||||
href: NAVIGATION_ROUTES.NOTIFICATIONS,
|
|
||||||
icon: Bell,
|
|
||||||
roles: ['STUDENT'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'system',
|
|
||||||
title: 'System',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: 'settings',
|
|
||||||
label: 'Settings',
|
|
||||||
href: NAVIGATION_ROUTES.SETTINGS,
|
|
||||||
icon: Settings,
|
|
||||||
roles: ['STUDENT'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parent Navigation Menu
|
|
||||||
*/
|
|
||||||
export const PARENT_NAV_ITEMS: NavGroup[] = [
|
|
||||||
{
|
|
||||||
id: 'main',
|
|
||||||
title: 'Main',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: 'dashboard',
|
|
||||||
label: 'Dashboard',
|
|
||||||
href: NAVIGATION_ROUTES.DASHBOARD,
|
|
||||||
icon: LayoutDashboard,
|
|
||||||
roles: ['PARENT'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'child-info',
|
|
||||||
title: 'Child Information',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: 'grades',
|
|
||||||
label: 'Grades & Progress',
|
|
||||||
href: NAVIGATION_ROUTES.GRADES,
|
|
||||||
icon: BookOpen,
|
|
||||||
roles: ['PARENT'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'attendance',
|
|
||||||
label: 'Attendance',
|
|
||||||
href: NAVIGATION_ROUTES.ATTENDANCE,
|
|
||||||
icon: AlertCircle,
|
|
||||||
roles: ['PARENT'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'communication',
|
|
||||||
title: 'Communication',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: 'messages',
|
|
||||||
label: 'Messages',
|
|
||||||
href: NAVIGATION_ROUTES.COMMUNICATION,
|
|
||||||
icon: MessageSquare,
|
|
||||||
roles: ['PARENT'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'notifications',
|
|
||||||
label: 'Notifications',
|
|
||||||
href: NAVIGATION_ROUTES.NOTIFICATIONS,
|
|
||||||
icon: Bell,
|
|
||||||
roles: ['PARENT'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'system',
|
|
||||||
title: 'System',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: 'settings',
|
|
||||||
label: 'Settings',
|
|
||||||
href: NAVIGATION_ROUTES.SETTINGS,
|
|
||||||
icon: Settings,
|
|
||||||
roles: ['PARENT'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get navigation items for a specific user role
|
|
||||||
*/
|
|
||||||
export function getNavItemsForRole(role?: string): NavGroup[] {
|
|
||||||
if (!role) return [];
|
|
||||||
|
|
||||||
switch (role) {
|
|
||||||
case 'STUDENT':
|
|
||||||
return STUDENT_NAV_ITEMS;
|
|
||||||
case 'PARENT':
|
|
||||||
return PARENT_NAV_ITEMS;
|
|
||||||
case 'TEACHER':
|
|
||||||
case 'SCHOOL_ADMIN':
|
|
||||||
case 'SUPER_ADMIN':
|
|
||||||
case 'DPO':
|
|
||||||
case 'VICE_PRINCIPAL':
|
|
||||||
return TEACHER_NAV_ITEMS;
|
|
||||||
default:
|
|
||||||
return TEACHER_NAV_ITEMS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Flatten navigation structure for routing
|
|
||||||
*/
|
|
||||||
export function flattenNavItems(groups: NavGroup[]): NavItem[] {
|
|
||||||
const items: NavItem[] = [];
|
|
||||||
|
|
||||||
groups.forEach((group) => {
|
|
||||||
group.items.forEach((item) => {
|
|
||||||
items.push(item);
|
|
||||||
if (item.children) {
|
|
||||||
items.push(...item.children);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
+10
-4
@@ -8,25 +8,31 @@ export const STUDENT_MANAGER_ROLES: readonly AppRole[] = ['SCHOOL_ADMIN', 'SUPER
|
|||||||
|
|
||||||
const parentViews: ViewName[] = [
|
const parentViews: ViewName[] = [
|
||||||
'dashboard', 'parent-portal', 'calendar', 'communication', 'illness',
|
'dashboard', 'parent-portal', 'calendar', 'communication', 'illness',
|
||||||
'notification-center', 'announcements',
|
'notification-center', 'announcements', 'settings',
|
||||||
];
|
];
|
||||||
|
|
||||||
const studentViews: ViewName[] = [
|
const studentViews: ViewName[] = [
|
||||||
'dashboard', 'student-portal', 'calendar', 'communication', 'illness',
|
'dashboard', 'student-portal', 'calendar', 'communication', 'illness',
|
||||||
'notification-center',
|
'notification-center', 'notebooks', 'homework', 'attendance', 'grading',
|
||||||
|
'flower', 'subjects', 'resources', 'competitions', 'portfolio', 'settings',
|
||||||
];
|
];
|
||||||
|
|
||||||
const teacherViews: ViewName[] = [
|
const teacherViews: ViewName[] = [
|
||||||
'dashboard', 'classes', 'competencies', 'progress', 'flower', 'assessments', 'grading',
|
'dashboard', 'classes', 'competencies', 'progress', 'flower', 'assessments', 'grading',
|
||||||
'reports', 'student-detail', 'matrix', 'attendance', 'calendar', 'lesson-plans',
|
'reports', 'student-detail', 'matrix', 'attendance', 'calendar', 'lesson-plans',
|
||||||
'behavior', 'coverage', 'rubrics', 'comments', 'homework', 'timetable', 'resources',
|
'behavior', 'coverage', 'rubrics', 'comments', 'homework', 'timetable', 'resources',
|
||||||
'illness', 'communication', 'notification-center', 'tablet-grading', 'exam-calendar',
|
'illness', 'communication', 'notification-center',
|
||||||
'seating-chart', 'report-cards',
|
'seating-chart', 'report-cards', 'notebooks', 'drawing', 'portfolio',
|
||||||
|
'peer-assessment', 'subjects', 'competitions', 'parents',
|
||||||
|
'announcements', 'school-library', 'school-transport',
|
||||||
|
'counseling', 'student-wellness', 'student-career',
|
||||||
|
'ai-tests', 'ai-studio', 'settings', 'data-import-export',
|
||||||
];
|
];
|
||||||
|
|
||||||
const vicePrincipalViews: ViewName[] = [
|
const vicePrincipalViews: ViewName[] = [
|
||||||
...teacherViews,
|
...teacherViews,
|
||||||
'analytics', 'disciplinary', 'school-transport', 'substitute-teacher',
|
'analytics', 'disciplinary', 'school-transport', 'substitute-teacher',
|
||||||
|
'districts', 'school-newsletter',
|
||||||
];
|
];
|
||||||
|
|
||||||
export function hasRoleAccess(role: string | undefined, allowedRoles: readonly AppRole[]): boolean {
|
export function hasRoleAccess(role: string | undefined, allowedRoles: readonly AppRole[]): boolean {
|
||||||
|
|||||||
Reference in new issue
Block a user