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:
Z User committed 2026-08-10 17:46:37 +00:00
1 parent 411ff146a7
commit 1af01dd304
8 files changed
+39 -1008

No files matched your search

-10
View File
@@ -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 />;
-236
View File
@@ -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>
);
}
-208
View File
@@ -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>
);
}
+2 -1
View File
@@ -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 }
); );
+27 -6
View File
@@ -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>
-127
View File
@@ -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;
-416
View File
@@ -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
View File
@@ -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 {