feat: Round 22 - Exam calendar, parent portal, teacher grading, deep styling

- Exam calendar integration: Plan Exam button, countdown badges, exam filter
- Parent portal: Children overview, illness approvals, grades, attendance
- Teacher grading: Stylus annotation, AI review, bulk grading mode
- Deep schlaukopf crawling: AI-powered content extraction from schlaukopf.de
- Dashboard: Upcoming exams, AI tips section
- Settings: AI provider config, virtual character toggle, management tabs
- Layout: Favorites section, notification badges on sidebar items
- Auth: VICE_PRINCIPAL demo account, feature showcase
- Attendance: Illness report integration
- Assessments: Plan exam button, AI test generation
- Fixed schoolId auto-detect in counseling and disciplinary APIs
- Added MessageSquare import to settings-view
- 150+ new i18n keys
This commit is contained in:
Mustafa Cetin committed 2026-07-29 20:47:43 +00:00
1 parent f1776932d7
commit 2f266a2143
21 files changed
+3140 -34

No files matched your search

+111
View File
@@ -0,0 +1,111 @@
import { NextResponse } from 'next/server';
import { z } from 'zod';
import { db } from '@/lib/db';
import { getSession } from '@/lib/auth';
const saveAnnotationSchema = z.object({
assessmentId: z.string().min(1),
studentId: z.string().min(1),
annotationData: z.string().min(1),
annotationImage: z.string().optional(),
});
export async function GET(request: Request) {
try {
const session = await getSession();
if (!session) {
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
}
const { searchParams } = new URL(request.url);
const assessmentId = searchParams.get('assessmentId');
const studentId = searchParams.get('studentId');
if (!assessmentId || !studentId) {
return NextResponse.json({ error: 'assessmentId and studentId are required' }, { status: 400 });
}
const result = await db.assessmentResult.findUnique({
where: {
assessmentId_studentId: { assessmentId, studentId },
},
select: {
id: true,
annotationData: true,
annotationImage: true,
note: true,
score: true,
},
});
if (!result) {
return NextResponse.json({ annotation: null });
}
return NextResponse.json({
annotation: result.annotationData ? {
id: result.id,
annotationData: result.annotationData,
annotationImage: result.annotationImage,
} : null,
});
} catch (error) {
console.error('Grading Annotate GET error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
export async function POST(request: Request) {
try {
const session = await getSession();
if (!session) {
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
}
const role = session.user?.role;
if (role !== 'TEACHER' && role !== 'SCHOOL_ADMIN' && role !== 'SUPER_ADMIN' && role !== 'VICE_PRINCIPAL') {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const body = await request.json();
const parsed = saveAnnotationSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{ error: 'Validation failed', details: parsed.error.issues },
{ status: 400 }
);
}
const { assessmentId, studentId, annotationData, annotationImage } = parsed.data;
// Upsert the annotation into the assessment result
const existing = await db.assessmentResult.findUnique({
where: { assessmentId_studentId: { assessmentId, studentId } },
});
if (existing) {
const updated = await db.assessmentResult.update({
where: { id: existing.id },
data: {
annotationData,
annotationImage: annotationImage ?? existing.annotationImage,
},
});
return NextResponse.json({ annotation: { id: updated.id, annotationData: updated.annotationData, annotationImage: updated.annotationImage } });
} else {
// Create a new result with just the annotation
const created = await db.assessmentResult.create({
data: {
assessmentId,
studentId,
annotationData,
annotationImage: annotationImage ?? null,
},
});
return NextResponse.json({ annotation: { id: created.id, annotationData: created.annotationData, annotationImage: created.annotationImage } }, { status: 201 });
}
} catch (error) {
console.error('Grading Annotate POST error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}