Files
schulos/src/app/api/attendance/route.ts
T
Mustafa Cetin 86e4b504e4 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)
2026-07-29 15:05:07 +00:00

371 lines
11 KiB
TypeScript

import { NextResponse } from 'next/server';
import { z } from 'zod';
import { db } from '@/lib/db';
import { getSession } from '@/lib/auth';
const statusEnum = z.enum(['PRESENT', 'ABSENT', 'EXCUSED', 'LATE']);
const createSessionSchema = z.object({
classGroupId: z.string().min(1),
date: z.string().min(1),
subjectId: z.string().optional(),
period: z.string().optional(),
});
const updateSessionSchema = z.object({
id: z.string().min(1),
status: z.enum(['OPEN', 'COMPLETED']).optional(),
records: z.array(z.object({
id: z.string().min(1),
status: statusEnum.optional(),
arrivalTime: z.string().optional(),
comment: z.string().optional(),
})).optional(),
});
export async function GET(request: Request) {
try {
const session = await getSession();
if (!session) {
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
}
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');
const dateFrom = searchParams.get('dateFrom');
const dateTo = searchParams.get('dateTo');
const status = searchParams.get('status');
if (!classGroupId) {
return NextResponse.json({ error: 'classGroupId is required' }, { status: 400 });
}
// Verify class belongs to user's school
const classGroup = await db.classGroup.findUnique({
where: { id: classGroupId },
select: { id: true, schoolId: true },
});
if (!classGroup) {
return NextResponse.json({ error: 'Class not found' }, { status: 404 });
}
if (
session.user?.role === 'SCHOOL_ADMIN' &&
session.user.schoolId &&
classGroup.schoolId !== session.user.schoolId
) {
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> = {};
if (dateFrom) dateFilter.gte = new Date(dateFrom);
if (dateTo) dateFilter.lte = new Date(dateTo);
where.date = dateFilter;
}
if (status) {
where.status = status;
}
const sessions = await db.attendanceSession.findMany({
where,
orderBy: { date: 'desc' },
include: {
subject: { select: { id: true, name: true } },
teacher: { select: { id: true, firstName: true, lastName: true } },
records: {
include: {
student: { select: { id: true, firstName: true, lastName: true } },
},
orderBy: { student: { lastName: 'asc' } },
},
},
});
// 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);
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 });
}
if (session.user?.role !== 'TEACHER' && session.user?.role !== 'SCHOOL_ADMIN' && session.user?.role !== 'SUPER_ADMIN') {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const body = await request.json();
const parsed = createSessionSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{ error: 'Validation failed', details: parsed.error.issues },
{ status: 400 }
);
}
const { classGroupId, date, subjectId, period } = parsed.data;
// Verify class exists
const classGroup = await db.classGroup.findUnique({
where: { id: classGroupId },
select: { id: true, schoolId: true },
});
if (!classGroup) {
return NextResponse.json({ error: 'Class not found' }, { status: 404 });
}
if (
session.user?.role === 'SCHOOL_ADMIN' &&
session.user.schoolId &&
classGroup.schoolId !== session.user.schoolId
) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
// Get all enrolled students for the class
const enrollments = await db.enrollment.findMany({
where: {
classGroupId,
endDate: null,
},
select: { studentId: true },
});
// Create the session with records for all students
const attendanceSession = await db.attendanceSession.create({
data: {
classGroupId,
teacherId: session.userId,
date: new Date(date),
subjectId: subjectId || null,
period: period || null,
status: 'OPEN',
records: {
create: enrollments.map((e) => ({
studentId: e.studentId,
status: 'PRESENT',
})),
},
},
include: {
subject: { select: { id: true, name: true } },
teacher: { select: { id: true, firstName: true, lastName: true } },
records: {
include: {
student: { select: { id: true, firstName: true, lastName: true } },
},
orderBy: { student: { lastName: 'asc' } },
},
},
});
await db.auditLog.create({
data: {
userId: session.userId,
schoolId: classGroup.schoolId,
action: 'CREATE',
entityType: 'AttendanceSession',
entityId: attendanceSession.id,
metadata: JSON.stringify({
classGroupId,
date,
studentCount: enrollments.length,
}),
},
});
return NextResponse.json(attendanceSession, { status: 201 });
} catch (error) {
console.error('Attendance POST error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
export async function PUT(request: Request) {
try {
const session = await getSession();
if (!session) {
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 body = await request.json();
const parsed = updateSessionSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{ error: 'Validation failed', details: parsed.error.issues },
{ status: 400 }
);
}
const { id } = parsed.data;
const existing = await db.attendanceSession.findUnique({
where: { id },
include: { classGroup: { select: { schoolId: true } } },
});
if (!existing) {
return NextResponse.json({ error: 'Session not found' }, { status: 404 });
}
const isTeacher = existing.teacherId === session.userId;
const isSchoolAdmin =
session.user?.role === 'SCHOOL_ADMIN' &&
session.user.schoolId &&
existing.classGroup.schoolId === session.user.schoolId;
const isSuperAdmin = session.user?.role === 'SUPER_ADMIN';
if (!isTeacher && !isSchoolAdmin && !isSuperAdmin) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
// Update session status if provided
if (parsed.data.status !== undefined) {
await db.attendanceSession.update({
where: { id },
data: { status: parsed.data.status },
});
}
// Update records if provided
if (parsed.data.records && parsed.data.records.length > 0) {
for (const record of parsed.data.records) {
const updateData: Record<string, unknown> = {};
if (record.status !== undefined) updateData.status = record.status;
if (record.arrivalTime !== undefined) updateData.arrivalTime = record.arrivalTime;
if (record.comment !== undefined) updateData.comment = record.comment;
if (Object.keys(updateData).length > 0) {
await db.attendanceRecord.update({
where: { id: record.id },
data: updateData,
});
}
}
}
// Return updated session
const updated = await db.attendanceSession.findUnique({
where: { id },
include: {
subject: { select: { id: true, name: true } },
teacher: { select: { id: true, firstName: true, lastName: true } },
records: {
include: {
student: { select: { id: true, firstName: true, lastName: true } },
},
orderBy: { student: { lastName: 'asc' } },
},
},
});
await db.auditLog.create({
data: {
userId: session.userId,
schoolId: existing.classGroup.schoolId,
action: 'UPDATE',
entityType: 'AttendanceSession',
entityId: id,
metadata: JSON.stringify({
status: parsed.data.status,
recordsUpdated: parsed.data.records?.length ?? 0,
}),
},
});
return NextResponse.json(updated);
} catch (error) {
console.error('Attendance PUT error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}