diff --git a/src/app/api/attendance/route.ts b/src/app/api/attendance/route.ts index ff7b268..7435f37 100644 --- a/src/app/api/attendance/route.ts +++ b/src/app/api/attendance/route.ts @@ -30,9 +30,8 @@ export async function GET(request: Request) { return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); } - if (session.user?.role !== 'TEACHER' && session.user?.role !== 'SCHOOL_ADMIN' && session.user?.role !== 'SUPER_ADMIN') { - return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); - } + const role = session.user?.role; + const isTeacherOrAdmin = role === 'TEACHER' || role === 'SCHOOL_ADMIN' || role === 'SUPER_ADMIN'; const { searchParams } = new URL(request.url); const classGroupId = searchParams.get('classGroupId'); @@ -62,6 +61,47 @@ export async function GET(request: Request) { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); } + // Students can only view their own attendance records + if (role === 'STUDENT') { + // Find the student record matching this user (by name + school) + const studentRecord = await db.student.findFirst({ + where: { + schoolId: session.user.schoolId ?? undefined, + firstName: session.user.firstName, + lastName: session.user.lastName, + deletedAt: null, + }, + }); + if (!studentRecord) { + // No student record found for this user - return empty + return NextResponse.json([]); + } + // Verify the student is enrolled in this class + const enrollment = await db.enrollment.findFirst({ + where: { classGroupId, studentId: studentRecord.id, endDate: null }, + }); + if (!enrollment) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } + } + + // Parents can view attendance if they have a child in the class + if (role === 'PARENT') { + const childEnrollment = await db.enrollment.findFirst({ + where: { classGroupId, endDate: null }, + include: { + student: { + include: { + parentLinks: { where: { parentId: session.userId } }, + }, + }, + }, + }); + if (!childEnrollment || childEnrollment.student.parentLinks.length === 0) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } + } + const where: Record = { classGroupId }; if (dateFrom || dateTo) { const dateFilter: Record = {}; @@ -88,6 +128,38 @@ export async function GET(request: Request) { }, }); + // For students, filter records to only show their own + if (role === 'STUDENT') { + const studentRecord = await db.student.findFirst({ + where: { + schoolId: session.user.schoolId ?? undefined, + firstName: session.user.firstName, + lastName: session.user.lastName, + deletedAt: null, + }, + }); + const studentId = studentRecord?.id ?? ''; + const filteredSessions = sessions.map((s) => ({ + ...s, + records: s.records.filter((r) => r.studentId === studentId), + })); + return NextResponse.json(filteredSessions); + } + + // For parents, filter records to only show their children's + if (role === 'PARENT') { + const childIds = await db.parentStudentLink.findMany({ + where: { parentId: session.userId }, + select: { studentId: true }, + }); + const childIdSet = new Set(childIds.map((c) => c.studentId)); + const filteredSessions = sessions.map((s) => ({ + ...s, + records: s.records.filter((r) => childIdSet.has(r.studentId)), + })); + return NextResponse.json(filteredSessions); + } + return NextResponse.json(sessions); } catch (error) { console.error('Attendance GET error:', error); diff --git a/src/app/api/competitions/route.ts b/src/app/api/competitions/route.ts index 33ec617..f9555cb 100644 --- a/src/app/api/competitions/route.ts +++ b/src/app/api/competitions/route.ts @@ -42,9 +42,7 @@ async function getCompetitions(request: Request): Promise { try { const session = await getSession(); if (!session) return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); - if (!isTeacherOrAdmin(session.user?.role)) { - return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); - } + // All authenticated users can view competitions (students, parents, teachers, admins) const { searchParams } = new URL(request.url); const schoolIdParam = searchParams.get('schoolId'); diff --git a/src/components/app-layout.tsx b/src/components/app-layout.tsx index a28abb3..7e397a1 100644 --- a/src/components/app-layout.tsx +++ b/src/components/app-layout.tsx @@ -58,6 +58,7 @@ import { Clock as ClockIcon, Library as LibraryIcon, Trophy, + FolderOpen, } from 'lucide-react'; import { Sidebar, @@ -237,6 +238,8 @@ const studentNavSections: NavSection[] = [ { key: 'portfolio', icon: Briefcase, labelKey: 'nav.portfolio' }, { key: 'calendar', icon: CalendarIconNav, labelKey: 'nav.calendar' }, { key: 'competitions', icon: Trophy, labelKey: 'nav.competitions' }, + { key: 'resources', icon: FolderOpen, labelKey: 'nav.resources' }, + { key: 'settings', icon: Settings, labelKey: 'nav.settings' }, ], }, ]; @@ -250,7 +253,10 @@ const parentNavSections: NavSection[] = [ { key: 'dashboard', icon: LayoutDashboard, labelKey: 'nav.dashboard' }, { key: 'parents', icon: Mail, labelKey: 'parent.my_children' }, { key: 'parents', icon: MessageSquareText, labelKey: 'parent.messages' }, + { key: 'grading', icon: Calculator, labelKey: 'nav.student_grades' }, + { key: 'attendance', icon: CalendarCheck, labelKey: 'nav.student_attendance' }, { key: 'calendar', icon: CalendarIconNav, labelKey: 'nav.calendar' }, + { key: 'competitions', icon: Trophy, labelKey: 'nav.competitions' }, ], }, ]; diff --git a/src/components/competitions-view.tsx b/src/components/competitions-view.tsx index 6bd0358..bd98f78 100644 --- a/src/components/competitions-view.tsx +++ b/src/components/competitions-view.tsx @@ -588,9 +588,9 @@ export default function CompetitionsView() { // Register const [isRegistering, setIsRegistering] = useState(false); - const isAdmin = currentUser?.role === 'admin'; - const isTeacher = currentUser?.role === 'teacher'; - const isStudent = currentUser?.role === 'student'; + const isAdmin = currentUser?.role === 'SCHOOL_ADMIN' || currentUser?.role === 'SUPER_ADMIN'; + const isTeacher = currentUser?.role === 'TEACHER'; + const isStudent = currentUser?.role === 'STUDENT'; const canCreate = isAdmin || isTeacher; const schoolId = currentUser?.schoolId; diff --git a/src/components/notebooks-view.tsx b/src/components/notebooks-view.tsx index 4209f95..2b21c73 100644 --- a/src/components/notebooks-view.tsx +++ b/src/components/notebooks-view.tsx @@ -15,7 +15,7 @@ import { Copy, FlaskConical, Languages, Calculator, Paintbrush, Megaphone, Strikethrough, Type, Highlighter, GripVertical, Clock, History, ZoomIn, ZoomOut, Image as ImageIcon, - Users as UsersIcon, Radio, MousePointer2, + Users as UsersIcon, Radio, MousePointer2, Columns2, ChevronRight, XCircle, Trophy, Zap, Flame, Dumbbell, Heart, RotateCcw, ArrowRight, Play, } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; @@ -161,9 +161,10 @@ const NOTEBOOK_TEMPLATES: NotebookTemplate[] = [ pages: [ { title: 'Aufgaben', content: '# Aufgaben\n\n1. \n2. \n3. \n' }, { title: 'Rechnungen', content: '# Rechnungen\n\n' }, - { title: 'Formeln', content: '# Formeln\n\n' }, + { title: 'Formeln', content: '# Formeln\n\n| Name | Formel |\n|------|--------|\n| Flaeche Rechteck | A = a · b |\n| Umfang Rechteck | U = 2·(a+b) |\n| Flaeche Dreieck | A = ½ · g · h |\n' }, { title: 'Geometrie', content: '# Geometrie\n\n' }, { title: 'Ergebnisse', content: '# Ergebnisse\n\n' }, + { title: 'Kontrolle', content: '# Kontrolle\n\n| Aufgabe | Ergebnis | Richtig? |\n|---------|----------|----------|\n| 1 | | |\n| 2 | | |\n| 3 | | |\n' }, ], }, { @@ -175,11 +176,12 @@ const NOTEBOOK_TEMPLATES: NotebookTemplate[] = [ icon: 'BookOpen', iconComponent: BookOpen, pages: [ - { title: 'Aufsaetze', content: '# Aufsaetze\n\n' }, - { title: 'Lesetagebuch', content: '# Lesetagebuch\n\n' }, - { title: 'Grammatik', content: '# Grammatik\n\n' }, - { title: 'Rechtschreibung', content: '# Rechtschreibung\n\n' }, - { title: 'Kreatives Schreiben', content: '# Kreatives Schreiben\n\n' }, + { title: 'Aufsaetze', content: '# Aufsaetze\n\n**Einleitung:**\n\n**Hauptteil:**\n\n**Schluss:**\n\n' }, + { title: 'Lesetagebuch', content: '# Lesetagebuch\n\n| Datum | Buch | Seiten | Meine Meinung |\n|-------|------|--------|---------------|\n| | | | |\n' }, + { title: 'Grammatik', content: '# Grammatik\n\n**Wortart:** \n\n**Regel:** \n\n**Beispiele:**\n1. \n2. \n3. \n' }, + { title: 'Rechtschreibung', content: '# Rechtschreibung\n\n| Falsch | Richtig | Regel |\n|--------|---------|-------|\n| | | |\n' }, + { title: 'Kreatives Schreiben', content: '# Kreatives Schreiben\n\n**Freies Schreiben:**\n\n' }, + { title: 'Textanalyse', content: '# Textanalyse\n\n**Titel:** \n\n**Autor:** \n\n**Textsorte:** \n\n**Zusammenfassung:**\n\n**Sprachliche Mittel:**\n\n' }, ], }, { @@ -246,6 +248,52 @@ const NOTEBOOK_TEMPLATES: NotebookTemplate[] = [ { title: 'Fragen', content: '# Fragen\n\n' }, ], }, + { + key: 'grundschule', + titleKey: 'notebooks.template_grundschule', + descKey: 'notebooks.template_grundschule_desc', + notebookType: 'lined', + color: '#f97316', + icon: 'Book', + iconComponent: Book, + pages: [ + { title: 'Wortschatz', content: '# Wortschatz\n\n| Wort | Artikel | Plural |\n|------|---------|--------|\n| | | |\n' }, + { title: 'Schreiben lernen', content: '# Schreiben lernen\n\n' }, + { title: 'Rechnen', content: '# Rechnen\n\n1. \n2. \n3. \n\n**Malfolgen:**\n\n| 1er | 2er | 5er | 10er |\n|-----|-----|-----|------|\n| 1 | 2 | 5 | 10 |\n| 2 | 4 | 10 | 20 |\n| 3 | 6 | 15 | 30 |\n' }, + { title: 'Lesen', content: '# Lesen\n\n**Gelesenes Buch:**\n\n**Meine Meinung:**\n\n' }, + { title: 'Sachkunde', content: '# Sachkunde\n\n**Thema:**\n\n**Was ich weiss:**\n\n**Was ich gelernt habe:**\n\n' }, + ], + }, + { + key: 'history', + titleKey: 'notebooks.template_history', + descKey: 'notebooks.template_history_desc', + notebookType: 'lined', + color: '#92400e', + icon: 'BookMarked', + iconComponent: BookMarked, + pages: [ + { title: 'Zeitstrahl', content: '# Zeitstrahl\n\n| Jahr | Ereignis |\n|------|----------|\n| | |\n' }, + { title: 'Quellenanalyse', content: '# Quellenanalyse\n\n**Quellenart:**\n\n**Entstehungszeit:**\n\n**Verfasser:**\n\n**Aussage:**\n\n**Historischer Kontext:**\n\n' }, + { title: 'Begriffe', content: '# Begriffe\n\n| Begriff | Definition |\n|---------|------------|\n| | |\n' }, + { title: 'Ursachen und Wirkungen', content: '# Ursachen und Wirkungen\n\n**Ursachen:**\n1. \n2. \n\n**Wirkungen:**\n1. \n2. \n' }, + ], + }, + { + key: 'religion', + titleKey: 'notebooks.template_religion', + descKey: 'notebooks.template_religion_desc', + notebookType: 'lined', + color: '#f97316', + icon: 'Heart', + iconComponent: Heart, + pages: [ + { title: 'Bibelstellen', content: '# Bibelstellen\n\n| Stelle | Text | Bedeutung |\n|--------|------|----------|\n| | | |\n' }, + { title: 'Gedanken', content: '# Gedanken\n\n' }, + { title: 'Ethik', content: '# Ethik\n\n**Situation:**\n\n**Verschiedene Sichtweisen:**\n\n**Meine Meinung:**\n\n' }, + { title: 'Weltreligionen', content: '# Weltreligionen\n\n| Religion | Gruender | Heilige Schrift |\n|----------|----------|----------------|\n| Christentum | Jesus | Bibel |\n| Islam | Mohammed | Koran |\n| Judentum | Moses | Tora |\n| Buddhismus | Buddha | Tripitaka |\n| Hinduismus | - | Veden |\n' }, + ], + }, ]; // ─── CSS Background Patterns ───────────────────────────────────────── @@ -1017,7 +1065,8 @@ function NotebookDetailView({ const [currentPageId, setCurrentPageId] = useState(null); const [pageContent, setPageContent] = useState(''); const [pageTitle, setPageTitle] = useState(''); - const [drawingMode, setDrawingMode] = useState(false); + const [viewMode, setViewMode] = useState<'text' | 'split' | 'drawing'>('text'); + const [splitRatio, setSplitRatio] = useState(50); // percentage for text side const [saving, setSaving] = useState(false); const [autoSaveStatus, setAutoSaveStatus] = useState<'idle' | 'saving' | 'saved'>('idle'); @@ -1534,16 +1583,38 @@ function NotebookDetailView({ )} - {/* Drawing mode toggle */} - + {/* View mode toggle — Text / Split / Drawing */} +
+ +
+ +
+ +
{/* Version history button */}
- {drawingMode ? ( - /* Drawing mode - render DrawingCanvas */ + {viewMode === 'drawing' ? ( + /* Full drawing mode - render DrawingCanvas */ setDrawingMode(false)} + onExit={() => setViewMode('text')} title={currentPage.title ?? `${t('notebooks.page')} ${currentPage.pageNumber}`} /> + ) : viewMode === 'split' ? ( + /* Split view - text editor on left, drawing canvas on right */ +
+ {/* Left: Text editor */} +
+ +
+
+ +
+
+
+ +
+ {cursors.filter(c => c.pageId === currentPage?.id).map((cursor) => ( + + +
+ {cursor.userName} +
+
+ ))} +
+
+ + {/* Resizable divider */} +
{ + e.preventDefault(); + const startX = e.clientX; + const startRatio = splitRatio; + const container = e.currentTarget.parentElement; + if (!container) return; + const containerWidth = container.getBoundingClientRect().width; + + const handleMouseMove = (moveEvent: MouseEvent) => { + const delta = moveEvent.clientX - startX; + const newRatio = Math.min(80, Math.max(20, startRatio + (delta / containerWidth) * 100)); + setSplitRatio(newRatio); + }; + const handleMouseUp = () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + }; + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + }} + > +
+
+ + {/* Right: Drawing canvas */} +
+
+ + {t('notebooks.handwriting_panel')} + {t('notebooks.finger_stylus_hint')} +
+
+ +
+
+
) : ( - /* Text editing area with WYSIWYG toolbar and page background */ + /* Text-only mode with WYSIWYG toolbar and page background */
{/* WYSIWYG toolbar */} @@ -1802,7 +1967,7 @@ function NotebookDetailView({
{t('notebooks.page')} {currentPage.pageNumber} / {pages.length} {/* Drawing indicator when not in drawing mode */} - {currentPage.drawingData && !drawingMode && ( + {currentPage.drawingData && viewMode === 'text' && ( {t('notebooks.view_drawing')} @@ -1811,11 +1976,11 @@ function NotebookDetailView({
{/* View drawing button if page has drawing data */} - {currentPage.drawingData && !drawingMode && ( + {currentPage.drawingData && viewMode === 'text' && (