feat: Fix Forbidden 403 errors, add notebook split view (text+handwriting), German curriculum templates, role-based navigation

- Fix attendance API: students/parents can now view their own attendance records
- Fix competitions API: all authenticated users can view competitions
- Add notebook split view: text editor + drawing canvas side by side
- Add resizable divider for split view with mouse drag support
- Add 3 new German curriculum templates: Grundschule, Geschichte, Religion/Ethik
- Enhance existing templates with more detailed content
- Fix role comparisons in competitions-view (uppercase role names)
- Add Resources and Settings to student navigation
- Add Grading, Attendance, Competitions to parent navigation
- Add i18n keys for new features (DE + EN)
This commit is contained in:
Mustafa Cetin committed 2026-07-29 15:05:07 +00:00
1 parent 8e7116e57d
commit 86e4b504e4
7 files changed
+334 -31

No files matched your search

+75 -3
View File
@@ -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<string, unknown> = { classGroupId };
if (dateFrom || dateTo) {
const dateFilter: Record<string, unknown> = {};
@@ -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);
+1 -3
View File
@@ -42,9 +42,7 @@ async function getCompetitions(request: Request): Promise<NextResponse> {
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');
+6
View File
@@ -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' },
],
},
];
+3 -3
View File
@@ -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;
+187 -22
View File
@@ -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<string | null>(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({
</Popover>
)}
{/* Drawing mode toggle */}
{/* View mode toggle — Text / Split / Drawing */}
<div className="flex items-center rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden shrink-0">
<Button
variant={drawingMode ? 'default' : 'outline'}
variant={viewMode === 'text' ? 'default' : 'ghost'}
size="sm"
onClick={() => setDrawingMode(!drawingMode)}
className={`min-h-[44px] shrink-0 transition-all ${drawingMode ? 'bg-emerald-600 hover:bg-emerald-700 shadow-md shadow-emerald-500/20' : ''}`}
onClick={() => setViewMode('text')}
className={`min-h-[40px] rounded-none border-0 px-3 ${viewMode === 'text' ? 'bg-emerald-600 hover:bg-emerald-700 text-white' : ''}`}
title={t('notebooks.view_text_only')}
>
<PenLine className="w-4 h-4 mr-1" />
{drawingMode ? t('notebooks.drawing_mode_exit') : t('notebooks.drawing_mode_toggle')}
<Edit3 className="w-4 h-4" />
</Button>
<div className="w-px h-6 bg-gray-200 dark:bg-gray-700" />
<Button
variant={viewMode === 'split' ? 'default' : 'ghost'}
size="sm"
onClick={() => setViewMode('split')}
className={`min-h-[40px] rounded-none border-0 px-3 ${viewMode === 'split' ? 'bg-emerald-600 hover:bg-emerald-700 text-white' : ''}`}
title={t('notebooks.view_split')}
>
<Columns2 className="w-4 h-4" />
</Button>
<div className="w-px h-6 bg-gray-200 dark:bg-gray-700" />
<Button
variant={viewMode === 'drawing' ? 'default' : 'ghost'}
size="sm"
onClick={() => setViewMode('drawing')}
className={`min-h-[40px] rounded-none border-0 px-3 ${viewMode === 'drawing' ? 'bg-emerald-600 hover:bg-emerald-700 text-white' : ''}`}
title={t('notebooks.view_drawing_only')}
>
<PenLine className="w-4 h-4" />
</Button>
</div>
{/* Version history button */}
<Button
@@ -1627,7 +1698,7 @@ function NotebookDetailView({
{/* Page info below thumbnail */}
<button
onClick={() => { setCurrentPageId(page.id); setDrawingMode(false); }}
onClick={() => { setCurrentPageId(page.id); }}
className={`w-full text-left p-2 rounded-b-lg text-sm transition-all min-h-[44px] flex items-center gap-2 ${
currentPageId === page.id
? 'bg-emerald-100 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-300 font-medium'
@@ -1720,8 +1791,8 @@ function NotebookDetailView({
</Button>
</div>
{drawingMode ? (
/* Drawing mode - render DrawingCanvas */
{viewMode === 'drawing' ? (
/* Full drawing mode - render DrawingCanvas */
<motion.div
initial={{ opacity: 0, x: 30 }}
animate={{ opacity: 1, x: 0 }}
@@ -1733,12 +1804,106 @@ function NotebookDetailView({
backgroundType={currentPage.background === 'music' || currentPage.background === 'calligraphy' ? 'blank' : (currentPage.background as 'blank' | 'lined' | 'grid' | 'dotted') ?? (notebook.notebookType as 'blank' | 'lined' | 'grid' | 'dotted')}
initialDrawingData={currentPage.drawingData ?? undefined}
onSave={handleDrawingSave}
onExit={() => setDrawingMode(false)}
onExit={() => setViewMode('text')}
title={currentPage.title ?? `${t('notebooks.page')} ${currentPage.pageNumber}`}
/>
</motion.div>
) : viewMode === 'split' ? (
/* Split view - text editor on left, drawing canvas on right */
<div className="flex-1 flex overflow-hidden">
{/* Left: Text editor */}
<div className="flex flex-col overflow-hidden" style={{ width: `${splitRatio}%` }}>
<WysiwygToolbar editorRef={editorRef} onFormatChange={handleFormatChange} />
<div className="flex-1 overflow-hidden p-3 relative">
<div
className="w-full h-full rounded-xl overflow-hidden"
style={{ backgroundColor: '#fff' }}
>
<ScrollArea className="h-full">
<div
className="min-h-full p-6"
style={getPageBackgroundCSS(currentPage.background ?? notebook.notebookType)}
onMouseMove={handleEditorMouseMove}
>
<div
ref={editorRef}
contentEditable
suppressContentEditableWarning
onInput={handleEditorInput}
data-placeholder={t('notebooks.page_content') + '...'}
className="w-full min-h-[500px] bg-transparent outline-none text-base text-gray-800 dark:text-gray-200 focus:ring-0 prose prose-sm max-w-none [&:empty]:before:content-[attr(data-placeholder)] [&:empty]:before:text-gray-300 [&:empty]:dark:before:text-gray-600 [&_h1]:text-2xl [&_h1]:font-bold [&_h1]:mb-2 [&_h1]:mt-4 [&_h2]:text-xl [&_h2]:font-bold [&_h2]:mb-1.5 [&_h2]:mt-3 [&_h3]:text-lg [&_h3]:font-semibold [&_h3]:mb-1 [&_h3]:mt-2 [&_ul]:list-disc [&_ul]:pl-6 [&_ul]:mb-2 [&_ol]:list-decimal [&_ol]:pl-6 [&_ol]:mb-2 [&_li]:mb-0.5"
style={{ lineHeight: notebook.notebookType === 'lined' || notebook.notebookType === 'calligraphy' ? '32px' : '1.5' }}
/>
</div>
</ScrollArea>
</div>
{cursors.filter(c => c.pageId === currentPage?.id).map((cursor) => (
<motion.div
key={cursor.userId}
initial={{ opacity: 0, scale: 0.5 }}
animate={{ opacity: 1, scale: 1 }}
className="absolute pointer-events-none z-20"
style={{
left: `${cursor.x}%`,
top: `${cursor.y}%`,
transform: 'translate(-4px, -4px)',
}}
>
<MousePointer2 className="w-4 h-4" style={{ color: cursor.color, fill: cursor.color }} />
<div className="px-1.5 py-0.5 rounded text-[10px] font-bold text-white whitespace-nowrap mt-0.5" style={{ backgroundColor: cursor.color }}>
{cursor.userName}
</div>
</motion.div>
))}
</div>
</div>
{/* Resizable divider */}
<div
className="w-2 cursor-col-resize bg-gray-200 dark:bg-gray-700 hover:bg-emerald-400 dark:hover:bg-emerald-600 transition-colors flex items-center justify-center shrink-0"
onMouseDown={(e) => {
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);
}}
>
<div className="w-0.5 h-8 bg-gray-400 dark:bg-gray-500 rounded-full" />
</div>
{/* Right: Drawing canvas */}
<div className="flex flex-col overflow-hidden" style={{ width: `${100 - splitRatio}%` }}>
<div className="flex items-center gap-2 px-3 py-2 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50">
<PenLine className="w-4 h-4 text-emerald-600 dark:text-emerald-400" />
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">{t('notebooks.handwriting_panel')}</span>
<span className="text-xs text-gray-400 dark:text-gray-500 ml-auto">{t('notebooks.finger_stylus_hint')}</span>
</div>
<div className="flex-1 overflow-hidden">
<DrawingCanvas
backgroundType={currentPage.background === 'music' || currentPage.background === 'calligraphy' ? 'blank' : (currentPage.background as 'blank' | 'lined' | 'grid' | 'dotted') ?? (notebook.notebookType as 'blank' | 'lined' | 'grid' | 'dotted')}
initialDrawingData={currentPage.drawingData ?? undefined}
onSave={handleDrawingSave}
title={currentPage.title ?? `${t('notebooks.page')} ${currentPage.pageNumber}`}
/>
</div>
</div>
</div>
) : (
/* Text editing area with WYSIWYG toolbar and page background */
/* Text-only mode with WYSIWYG toolbar and page background */
<div className="flex-1 flex flex-col overflow-hidden">
{/* WYSIWYG toolbar */}
<WysiwygToolbar editorRef={editorRef} onFormatChange={handleFormatChange} />
@@ -1802,7 +1967,7 @@ function NotebookDetailView({
<div className="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
<span>{t('notebooks.page')} {currentPage.pageNumber} / {pages.length}</span>
{/* Drawing indicator when not in drawing mode */}
{currentPage.drawingData && !drawingMode && (
{currentPage.drawingData && viewMode === 'text' && (
<Badge variant="outline" className="text-xs gap-1 border-emerald-300 text-emerald-600 dark:border-emerald-700 dark:text-emerald-400">
<ImageIcon className="w-3 h-3" />
{t('notebooks.view_drawing')}
@@ -1811,11 +1976,11 @@ function NotebookDetailView({
</div>
<div className="flex items-center gap-2">
{/* View drawing button if page has drawing data */}
{currentPage.drawingData && !drawingMode && (
{currentPage.drawingData && viewMode === 'text' && (
<Button
variant="outline"
size="sm"
onClick={() => setDrawingMode(true)}
onClick={() => setViewMode('split')}
className="min-h-[44px]"
>
<ImageIcon className="w-4 h-4 mr-1" />
+22
View File
@@ -1934,6 +1934,11 @@ const de: TranslationDict = {
'notebooks.drawing_mode': 'Zeichnungsmodus',
'notebooks.drawing_mode_toggle': 'Zeichnen',
'notebooks.drawing_mode_exit': 'Zurück zum Text',
'notebooks.view_text_only': 'Nur Text',
'notebooks.view_split': 'Text + Handschrift',
'notebooks.view_drawing_only': 'Nur Zeichnung',
'notebooks.handwriting_panel': 'Handschrift-Panel',
'notebooks.finger_stylus_hint': 'Finger oder Stift',
'notebooks.drawing_placeholder': 'Zeichnungsbereich — wird in einer separaten Komponente integriert',
'notebooks.back_to_library': 'Zurück zur Bibliothek',
'notebooks.open_notebook': 'Heft öffnen',
@@ -2077,6 +2082,12 @@ const de: TranslationDict = {
'notebooks.template_art_desc': 'Blanko-Heft zum Zeichnen',
'notebooks.template_music_desc': 'Musikheft zum Komponieren',
'notebooks.template_science_desc': 'Kariertes Heft fuer Experimente',
'notebooks.template_grundschule': 'Grundschule',
'notebooks.template_grundschule_desc': 'Linertes Heft fuer die Grundschule',
'notebooks.template_history': 'Geschichte',
'notebooks.template_history_desc': 'Geschichtsheft mit Zeitstrahl und Quellenanalyse',
'notebooks.template_religion': 'Religion / Ethik',
'notebooks.template_religion_desc': 'Heft fuer Religion und Ethik',
'notebooks.tab_all': 'Meine Hefte',
'notebooks.owner': 'Von',
@@ -5421,6 +5432,11 @@ const en: TranslationDict = {
'notebooks.drawing_mode': 'Drawing Mode',
'notebooks.drawing_mode_toggle': 'Draw',
'notebooks.drawing_mode_exit': 'Back to Text',
'notebooks.view_text_only': 'Text Only',
'notebooks.view_split': 'Text + Handwriting',
'notebooks.view_drawing_only': 'Drawing Only',
'notebooks.handwriting_panel': 'Handwriting Panel',
'notebooks.finger_stylus_hint': 'Finger or stylus',
'notebooks.drawing_placeholder': 'Drawing area — will be integrated in a separate component',
'notebooks.back_to_library': 'Back to Library',
'notebooks.open_notebook': 'Open Notebook',
@@ -5564,6 +5580,12 @@ const en: TranslationDict = {
'notebooks.template_art_desc': 'Blank notebook for drawing',
'notebooks.template_music_desc': 'Music notebook for composition',
'notebooks.template_science_desc': 'Grid notebook for experiments',
'notebooks.template_grundschule': 'Primary School',
'notebooks.template_grundschule_desc': 'Lined notebook for primary school',
'notebooks.template_history': 'History',
'notebooks.template_history_desc': 'History notebook with timeline and source analysis',
'notebooks.template_religion': 'Religion / Ethics',
'notebooks.template_religion_desc': 'Notebook for religion and ethics',
'notebooks.tab_all': 'My Notebooks',
'notebooks.owner': 'By',
+40
View File
@@ -1605,3 +1605,43 @@ Unresolved Issues:
- Email notification integration (SMTP) still pending
- Advanced analytics dashboard refinements
- Performance optimization for older tablets
---
Task ID: Round-20
Agent: Main Agent
Task: Fix Forbidden errors, enhance notebooks with split view, add German curriculum templates, fix role-based navigation
Date: 2025-07-29
Work Log:
- **Fixed Forbidden (403) errors** for student and parent roles:
- `src/app/api/attendance/route.ts` — Students can now view their own attendance records (filtered by student record matching user name + school). Parents can view their children's attendance.
- `src/app/api/competitions/route.ts` — All authenticated users can now view competitions (GET). Only teachers/admins can create (POST).
- Fixed Student/User model mismatch: attendance API now correctly maps User.id to Student.id via name+school lookup.
- **Enhanced notebooks with split view** (Text + Handwriting simultaneously):
- Replaced `drawingMode` boolean toggle with `viewMode` state: 'text' | 'split' | 'drawing'
- Added three view mode buttons: "Nur Text", "Text + Handschrift", "Nur Zeichnung"
- Split view shows text editor on left, drawing canvas on right with resizable divider
- Drawing canvas in split mode has "Handschrift-Panel" header with "Finger oder Stift" hint
- Resizable divider with mouse drag support (min 20%, max 80%)
- Added `Columns2` icon import from lucide-react
- **Added more German curriculum notebook templates**:
- Enhanced Math template with formula tables and control table
- Enhanced German template with essay structure (Einleitung/Hauptteil/Schluss), reading journal, grammar rules, spelling table, text analysis
- Added Grundschule (Primary School) template with vocabulary, writing, math, reading, Sachkunde pages
- Added Geschichte (History) template with timeline, source analysis, terms, causes/effects
- Added Religion/Ethik template with Bible references, ethics, world religions comparison
- **Fixed role-based navigation**:
- `src/components/competitions-view.tsx` — Fixed role comparisons from lowercase ('admin', 'teacher', 'student') to uppercase ('SCHOOL_ADMIN', 'TEACHER', 'STUDENT')
- `src/components/app-layout.tsx` — Added Resources and Settings to student navigation, added Grading, Attendance, and Competitions to parent navigation
- Added `FolderOpen` icon import
- **Added i18n keys** for new features:
- German: view_text_only, view_split, view_drawing_only, handwriting_panel, finger_stylus_hint, template_grundschule, template_history, template_religion
- English: same keys with English translations
Stage Summary:
- Forbidden 403 errors for students/parents are now fixed for attendance and competitions
- Notebooks now support split view (text + handwriting simultaneously) with resizable divider
- Added 3 new German curriculum templates (Grundschule, Geschichte, Religion/Ethik)
- Enhanced existing templates with more detailed content (math formulas, German essay structure, etc.)
- Role-based navigation properly shows different menus for teacher, student, parent, admin
- QA verified: student login works, student dashboard shows correct nav, attendance no longer 403, competitions no longer 403, split view works in notebooks