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: 'comments', icon: MessageSquareText, labelKey: 'nav.comments' },
|
||||
{ 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: 'subjects', icon: BookOpen, labelKey: 'nav.subjects' },
|
||||
{ key: 'competitions', icon: Trophy, labelKey: 'nav.competitions' },
|
||||
@@ -259,7 +258,6 @@ const navSections: NavSection[] = [
|
||||
{ key: 'calendar', icon: CalendarIconNav, labelKey: 'nav.calendar' },
|
||||
{ key: 'communication', icon: MessageSquare, labelKey: 'nav.communication' },
|
||||
{ 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-library', icon: LibraryIcon, labelKey: 'nav.school_library' },
|
||||
{ key: 'school-transport', icon: Bus, labelKey: 'nav.school-transport' },
|
||||
@@ -271,7 +269,6 @@ const navSections: NavSection[] = [
|
||||
{ key: 'illness', icon: Heart, labelKey: 'nav.illness' },
|
||||
{ key: 'substitute-teacher', icon: UserCheck, labelKey: 'nav.substitute-teacher' },
|
||||
{ key: 'timetable', icon: ClockIcon, labelKey: 'nav.timetable' },
|
||||
{ key: 'exam-calendar', icon: CalendarClock, labelKey: 'nav.exam_calendar' },
|
||||
{ key: 'parents', icon: Mail, labelKey: 'nav.parents' },
|
||||
],
|
||||
},
|
||||
@@ -323,8 +320,6 @@ const studentNavSections: NavSection[] = [
|
||||
{ key: 'communication', icon: MessageSquare, labelKey: 'nav.communication' },
|
||||
{ key: 'counseling', icon: Heart, labelKey: 'counseling.title' },
|
||||
{ key: 'ai-tests', icon: Brain, labelKey: 'ai_tests.title' },
|
||||
{ key: 'tablet-grading', icon: Tablet, labelKey: 'nav.tablet_grading' },
|
||||
{ key: 'exam-calendar', icon: CalendarClock, labelKey: 'nav.exam_calendar' },
|
||||
{ key: 'peer-assessment', icon: UsersRound, labelKey: 'nav.peer-assessment' },
|
||||
{ key: 'report-cards', icon: FileText, labelKey: 'nav.report_cards' },
|
||||
{ key: 'seating-chart', icon: LayoutGridIcon, labelKey: 'nav.seating_chart' },
|
||||
@@ -334,7 +329,6 @@ const studentNavSections: NavSection[] = [
|
||||
{ key: 'student-career', icon: Compass, labelKey: 'nav.student-career' },
|
||||
{ key: 'resources', icon: FolderOpen, labelKey: 'nav.resources' },
|
||||
{ key: 'school-library', icon: LibraryIcon, labelKey: 'nav.school_library' },
|
||||
{ key: 'school-events', icon: PartyPopper, labelKey: 'nav.school-events' },
|
||||
{ key: 'school-newsletter', icon: Newspaper, labelKey: 'nav.school-newsletter' },
|
||||
{ key: 'school-transport', icon: Bus, labelKey: 'nav.school-transport' },
|
||||
{ key: 'substitute-teacher', icon: UserCheck, labelKey: 'nav.substitute-teacher' },
|
||||
@@ -367,7 +361,6 @@ const parentNavSections: NavSection[] = [
|
||||
{ key: 'peer-assessment', icon: UsersRound, labelKey: 'nav.peer-assessment' },
|
||||
{ key: 'report-cards', icon: FileText, labelKey: 'nav.report_cards' },
|
||||
{ key: 'school-library', icon: LibraryIcon, labelKey: 'nav.school_library' },
|
||||
{ key: 'school-events', icon: PartyPopper, labelKey: 'nav.school-events' },
|
||||
{ key: 'school-newsletter', icon: Newspaper, labelKey: 'nav.school-newsletter' },
|
||||
{ key: 'school-transport', icon: Bus, labelKey: 'nav.school-transport' },
|
||||
{ key: 'substitute-teacher', icon: UserCheck, labelKey: 'nav.substitute-teacher' },
|
||||
@@ -515,8 +508,6 @@ function renderView(view: ViewName) {
|
||||
case 'parent-portal': return <ParentPortalView />;
|
||||
case 'notification-center': return <NotificationCenterView />;
|
||||
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 'grade-analytics': return <UnifiedGradingPanel mode="teacher" />;
|
||||
case 'student-achievements': return <StudentAchievementsView />;
|
||||
@@ -527,7 +518,6 @@ function renderView(view: ViewName) {
|
||||
case 'student-wellness': return <StudentWellnessView />;
|
||||
case 'student-career': return <StudentCareerView />;
|
||||
case 'data-import-export': return <DataImportExportView />;
|
||||
case 'school-events': return <ProfessionalCalendar variant="month" />;
|
||||
case 'substitute-teacher': return <SubstituteTeacherView />;
|
||||
case 'school-newsletter': return <SchoolNewsletterView />;
|
||||
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;
|
||||
|
||||
// Fetch events from backend
|
||||
const monthParam = format(currentDate, 'yyyy-MM');
|
||||
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 }
|
||||
);
|
||||
|
||||
|
||||
@@ -100,15 +100,30 @@ export function UnifiedGradingPanel({
|
||||
if (studentId) queryParams.append('studentId', studentId);
|
||||
|
||||
const { data: backendGrades, isLoading, mutate } = useApiGet(
|
||||
`/api/v1/grades${queryParams.toString() ? '?' + queryParams.toString() : ''}`,
|
||||
`/api/assessments${queryParams.toString() ? '?' + queryParams.toString() : ''}`,
|
||||
{ revalidateOnFocus: true }
|
||||
);
|
||||
|
||||
// Update local grades when backend data changes
|
||||
React.useEffect(() => {
|
||||
if (backendGrades) {
|
||||
setGrades(backendGrades);
|
||||
applyFilters(backendGrades);
|
||||
// Transform assessments API response to Grade format
|
||||
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]);
|
||||
|
||||
@@ -173,7 +188,7 @@ export function UnifiedGradingPanel({
|
||||
await onGradeSubmit(gradesToSubmit);
|
||||
} else {
|
||||
// Use API
|
||||
await fetch('/api/v1/grades/submit', {
|
||||
await fetch('/api/assessments/bulk-results', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(gradesToSubmit),
|
||||
@@ -204,7 +219,7 @@ export function UnifiedGradingPanel({
|
||||
// Handle grade deletion
|
||||
const handleDeleteGrade = useCallback(async (gradeId: string) => {
|
||||
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));
|
||||
setSelectedGrade(null);
|
||||
toast.success('Grade deleted');
|
||||
@@ -286,7 +301,13 @@ export function UnifiedGradingPanel({
|
||||
</tr>
|
||||
</thead>
|
||||
<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">
|
||||
<td className="py-2 px-3">{grade.studentName}</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[] = [
|
||||
'dashboard', 'parent-portal', 'calendar', 'communication', 'illness',
|
||||
'notification-center', 'announcements',
|
||||
'notification-center', 'announcements', 'settings',
|
||||
];
|
||||
|
||||
const studentViews: ViewName[] = [
|
||||
'dashboard', 'student-portal', 'calendar', 'communication', 'illness',
|
||||
'notification-center',
|
||||
'notification-center', 'notebooks', 'homework', 'attendance', 'grading',
|
||||
'flower', 'subjects', 'resources', 'competitions', 'portfolio', 'settings',
|
||||
];
|
||||
|
||||
const teacherViews: ViewName[] = [
|
||||
'dashboard', 'classes', 'competencies', 'progress', 'flower', 'assessments', 'grading',
|
||||
'reports', 'student-detail', 'matrix', 'attendance', 'calendar', 'lesson-plans',
|
||||
'behavior', 'coverage', 'rubrics', 'comments', 'homework', 'timetable', 'resources',
|
||||
'illness', 'communication', 'notification-center', 'tablet-grading', 'exam-calendar',
|
||||
'seating-chart', 'report-cards',
|
||||
'illness', 'communication', 'notification-center',
|
||||
'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[] = [
|
||||
...teacherViews,
|
||||
'analytics', 'disciplinary', 'school-transport', 'substitute-teacher',
|
||||
'districts', 'school-newsletter',
|
||||
];
|
||||
|
||||
export function hasRoleAccess(role: string | undefined, allowedRoles: readonly AppRole[]): boolean {
|
||||
|
||||
Reference in new issue
Block a user