Round 22: Exam Calendar, Tablet Grading, Student Achievements, Notification Center, Announcements, Styling
- Exam Calendar View: Teacher exam planning with calendar integration - Teachers create/modify exams, admin full CRUD, students read-only - CalendarEvent integration, 2-week warning, AI test availability badge - Monthly/weekly calendar view, list view, statistics - Tablet Grading View: Stylus annotation and AI grading audit - HTML5 Canvas with PointerEvent pressure sensitivity - Tool palette: pen, highlighter, eraser, text, stamp - Quick grade buttons (German 1-6 scale) - AI grading audit with detailed feedback and suggestions - Student Achievements View: Badges, XP, leaderboard - Badge collection grid with category filters - Level/XP system with progress bars - Leaderboard with opt-in - Daily/weekly challenges - Student Study Planner View: Pomodoro timer, weekly schedule - Pomodoro timer with focus score - Weekly study schedule with drag-and-drop - AI study recommendations - Session history and statistics - Notification Center View: Categories, filters, bulk actions - 6 categories, 4 priority levels - Real-time polling (10 seconds) - Preferences with quiet hours - Statistics charts - School Announcements View: CRUD, read receipts - 6 announcement types with gradient headers - Pin-to-top, read receipts, target audience - Admin/teacher create, students/parents view - Bug fixes: AnnouncementRead model user relation, User API schoolId field - i18n: 200+ new keys, breadcrumb navigation fixes - Styling: Gradient headers, animations, hover effects across all new views
This commit is contained in:
1 parent
9e22214470
commit
490bd0650f
30 files changed
+10844
-31
No files matched your search
@@ -0,0 +1,39 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
|
||||
export async function GET(
|
||||
_request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const review = await db.teacherGradingReview.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
assessment: { select: { id: true, title: true } },
|
||||
teacher: { select: { id: true, firstName: true, lastName: true } },
|
||||
comments: {
|
||||
include: {
|
||||
user: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!review) {
|
||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json(review);
|
||||
} catch (error) {
|
||||
console.error('AI Grading Audit GET [id] error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
|
||||
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 { schoolId, assessmentId, studentId } = body;
|
||||
|
||||
if (!schoolId || !assessmentId || !studentId) {
|
||||
return NextResponse.json({ error: 'schoolId, assessmentId, and studentId are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Get assessment and student data
|
||||
const assessment = await db.assessment.findUnique({
|
||||
where: { id: assessmentId },
|
||||
include: {
|
||||
results: {
|
||||
where: { studentId },
|
||||
include: {
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!assessment) {
|
||||
return NextResponse.json({ error: 'Assessment not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Get annotations for this student's submission
|
||||
const annotations = await db.gradingAnnotation.findMany({
|
||||
where: { assessmentId, studentId },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
});
|
||||
|
||||
const studentResult = assessment.results[0];
|
||||
const student = studentResult?.student;
|
||||
|
||||
// Generate AI grading audit
|
||||
const systemPrompt = `Du bist ein erfahrener Lehrer und Prüfer an einer deutschen Schule.
|
||||
Du überprüfst die Benotung eines Schülers und gibst detailliertes Feedback.
|
||||
Die deutsche Notenskala: 1 (sehr gut), 2 (gut), 3 (befriedigend), 4 (ausreichend), 5 (mangelhaft), 6 (ungenügend).
|
||||
Analysiere die Leistung und die gegebene Benotung. Wenn die Benotung nicht angemessen ist, schlage eine Korrektur vor mit Begründung.
|
||||
Antworte immer auf Deutsch und im JSON-Format.`;
|
||||
|
||||
const gradingInfo = {
|
||||
studentName: student ? `${student.firstName} ${student.lastName}` : 'Unknown',
|
||||
assessmentTitle: assessment.title,
|
||||
assessmentType: assessment.type,
|
||||
maxScore: assessment.maxScore,
|
||||
score: studentResult?.score ?? null,
|
||||
note: studentResult?.note ?? null,
|
||||
annotationsCount: annotations.length,
|
||||
annotationTypes: annotations.map((a: { type: string; content: string | null }) => ({ type: a.type, content: a.content })),
|
||||
teacherAnnotations: annotations.filter((a: { type: string }) => a.type === 'stamp' || a.type === 'text').map((a: { type: string; content: string | null; color: string }) => ({ type: a.type, content: a.content, color: a.color })),
|
||||
};
|
||||
|
||||
let reviewResult: string;
|
||||
let discrepanciesFound = 0;
|
||||
|
||||
try {
|
||||
const ai = await ZAI.create();
|
||||
const response = await ai.chat.completions.create({
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{
|
||||
role: 'user',
|
||||
content: `Überprüfe die Benotung für diesen Schüler und gib Feedback:\n\n${JSON.stringify(gradingInfo, null, 2)}\n\nGib deine Antwort als JSON mit folgenden Feldern:\n- currentGrade: die aktuelle Benotung\n- suggestedGrade: deine vorgeschlagene Note\n- reason: detaillierte Begründung auf Deutsch\n- discrepancies: Liste der gefundenen Unstimmigkeiten\n- overallAssessment: Gesamtbewertung der Benotung`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
reviewResult = response?.choices?.[0]?.message?.content || 'Review could not be generated';
|
||||
} catch (aiError) {
|
||||
console.error('AI grading audit error:', aiError);
|
||||
reviewResult = JSON.stringify({
|
||||
currentGrade: studentResult?.score ?? 'N/A',
|
||||
suggestedGrade: 'N/A',
|
||||
reason: 'AI review could not be generated. Manual review recommended.',
|
||||
discrepancies: ['AI analysis unavailable — please review manually'],
|
||||
overallAssessment: 'AI audit could not be completed due to a technical error. Please review the grading manually.',
|
||||
});
|
||||
}
|
||||
|
||||
// Count discrepancies
|
||||
try {
|
||||
const parsed = typeof reviewResult === 'string' ? JSON.parse(reviewResult) : reviewResult;
|
||||
discrepanciesFound = Array.isArray(parsed.discrepancies) ? parsed.discrepancies.length : 0;
|
||||
} catch {
|
||||
discrepanciesFound = 0;
|
||||
}
|
||||
|
||||
// Create or update the grading review
|
||||
const review = await db.teacherGradingReview.create({
|
||||
data: {
|
||||
schoolId,
|
||||
assessmentId,
|
||||
teacherId: session.userId,
|
||||
aiProvider: 'pollination',
|
||||
status: 'completed',
|
||||
reviewResult,
|
||||
discrepanciesFound,
|
||||
},
|
||||
include: {
|
||||
assessment: { select: { id: true, title: true } },
|
||||
teacher: { select: { id: true, firstName: true, lastName: true } },
|
||||
comments: {
|
||||
include: {
|
||||
user: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(review, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('AI Grading Audit POST error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// CompetenceTrack — Announcement Read Receipt API
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
|
||||
export async function POST(
|
||||
_request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const session = await getSession();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
// Verify announcement exists
|
||||
const announcement = await db.announcement.findUnique({
|
||||
where: { id, deletedAt: null },
|
||||
});
|
||||
|
||||
if (!announcement) {
|
||||
return NextResponse.json({ error: 'Announcement not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Upsert read receipt
|
||||
const readReceipt = await db.announcementRead.upsert({
|
||||
where: {
|
||||
announcementId_userId: {
|
||||
announcementId: id,
|
||||
userId: session.user.id,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
readAt: new Date(),
|
||||
},
|
||||
create: {
|
||||
announcementId: id,
|
||||
userId: session.user.id,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ readReceipt });
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
_request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const session = await getSession();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
// Get all read receipts for this announcement
|
||||
const reads = await db.announcementRead.findMany({
|
||||
where: { announcementId: id },
|
||||
include: {
|
||||
user: {
|
||||
select: { id: true, firstName: true, lastName: true },
|
||||
},
|
||||
},
|
||||
orderBy: { readAt: 'desc' },
|
||||
});
|
||||
|
||||
return NextResponse.json({ reads, totalReads: reads.length });
|
||||
}
|
||||
@@ -5,11 +5,13 @@ import { getSession } from '@/lib/auth';
|
||||
|
||||
const priorityEnum = z.enum(['low', 'normal', 'high', 'urgent']);
|
||||
const targetAudienceEnum = z.enum(['all', 'teachers', 'students', 'parents', 'class']);
|
||||
const announcementTypeEnum = z.enum(['general', 'urgent', 'event', 'holiday', 'exam', 'deadline']);
|
||||
|
||||
const updateAnnouncementSchema = z.object({
|
||||
title: z.string().min(1).max(200).optional(),
|
||||
content: z.string().min(1).max(10000).optional(),
|
||||
priority: priorityEnum.optional(),
|
||||
announcementType: announcementTypeEnum.optional(),
|
||||
targetAudience: targetAudienceEnum.optional(),
|
||||
classGroupId: z.string().optional().nullable(),
|
||||
isPinned: z.boolean().optional(),
|
||||
@@ -17,7 +19,7 @@ const updateAnnouncementSchema = z.object({
|
||||
});
|
||||
|
||||
function isTeacherOrAdmin(role: string | undefined): boolean {
|
||||
return role === 'TEACHER' || role === 'SCHOOL_ADMIN' || role === 'SUPER_ADMIN';
|
||||
return role === 'TEACHER' || role === 'SCHOOL_ADMIN' || role === 'SUPER_ADMIN' || role === 'VICE_PRINCIPAL';
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
@@ -36,6 +38,12 @@ export async function GET(
|
||||
include: {
|
||||
author: { select: { id: true, firstName: true, lastName: true } },
|
||||
classGroup: { select: { id: true, name: true } },
|
||||
reads: {
|
||||
include: {
|
||||
user: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
orderBy: { readAt: 'desc' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -43,7 +51,21 @@ export async function GET(
|
||||
return NextResponse.json({ error: 'Announcement not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json(announcement);
|
||||
// Check if current user has read it
|
||||
const readReceipt = await db.announcementRead.findUnique({
|
||||
where: {
|
||||
announcementId_userId: {
|
||||
announcementId: id,
|
||||
userId: session.userId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
...announcement,
|
||||
isReadByCurrentUser: !!readReceipt,
|
||||
totalReads: announcement.reads.length,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Announcement GET [id] error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
@@ -87,6 +109,7 @@ export async function PUT(
|
||||
if (parsed.data.title !== undefined) updateData.title = parsed.data.title;
|
||||
if (parsed.data.content !== undefined) updateData.content = parsed.data.content;
|
||||
if (parsed.data.priority !== undefined) updateData.priority = parsed.data.priority;
|
||||
if (parsed.data.announcementType !== undefined) updateData.announcementType = parsed.data.announcementType;
|
||||
if (parsed.data.targetAudience !== undefined) updateData.targetAudience = parsed.data.targetAudience;
|
||||
if (parsed.data.classGroupId !== undefined) updateData.classGroupId = parsed.data.classGroupId || null;
|
||||
if (parsed.data.isPinned !== undefined) updateData.isPinned = parsed.data.isPinned;
|
||||
|
||||
@@ -5,12 +5,14 @@ import { getSession } from '@/lib/auth';
|
||||
|
||||
const priorityEnum = z.enum(['low', 'normal', 'high', 'urgent']);
|
||||
const targetAudienceEnum = z.enum(['all', 'teachers', 'students', 'parents', 'class']);
|
||||
const announcementTypeEnum = z.enum(['general', 'urgent', 'event', 'holiday', 'exam', 'deadline']);
|
||||
|
||||
const createAnnouncementSchema = z.object({
|
||||
schoolId: z.string().min(1),
|
||||
title: z.string().min(1).max(200),
|
||||
content: z.string().min(1).max(10000),
|
||||
priority: priorityEnum.optional().default('normal'),
|
||||
announcementType: announcementTypeEnum.optional().default('general'),
|
||||
targetAudience: targetAudienceEnum.optional().default('all'),
|
||||
classGroupId: z.string().optional().nullable(),
|
||||
isPinned: z.boolean().optional().default(false),
|
||||
@@ -18,7 +20,7 @@ const createAnnouncementSchema = z.object({
|
||||
});
|
||||
|
||||
function isTeacherOrAdmin(role: string | undefined): boolean {
|
||||
return role === 'TEACHER' || role === 'SCHOOL_ADMIN' || role === 'SUPER_ADMIN';
|
||||
return role === 'TEACHER' || role === 'SCHOOL_ADMIN' || role === 'SUPER_ADMIN' || role === 'VICE_PRINCIPAL';
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
@@ -34,10 +36,12 @@ export async function GET(request: Request) {
|
||||
const targetAudience = searchParams.get('targetAudience');
|
||||
const classGroupId = searchParams.get('classGroupId');
|
||||
const isPinned = searchParams.get('isPinned');
|
||||
const announcementType = searchParams.get('announcementType');
|
||||
const limit = parseInt(searchParams.get('limit') || '50', 10);
|
||||
const includeReads = searchParams.get('includeReads') === 'true';
|
||||
|
||||
let schoolId: string | undefined;
|
||||
if (session.user?.role === 'SCHOOL_ADMIN') {
|
||||
if (session.user?.role === 'SCHOOL_ADMIN' || session.user?.role === 'VICE_PRINCIPAL') {
|
||||
schoolId = session.user.schoolId ?? undefined;
|
||||
} else {
|
||||
schoolId = schoolIdParam ?? session.user?.schoolId ?? undefined;
|
||||
@@ -59,6 +63,7 @@ export async function GET(request: Request) {
|
||||
if (targetAudience && targetAudience !== 'all') where.targetAudience = targetAudience;
|
||||
if (classGroupId && classGroupId !== 'all') where.classGroupId = classGroupId;
|
||||
if (isPinned === 'true') where.isPinned = true;
|
||||
if (announcementType && announcementType !== 'all') where.announcementType = announcementType;
|
||||
|
||||
// Filter by role-based target audience
|
||||
if (session.user?.role === 'STUDENT') {
|
||||
@@ -83,10 +88,52 @@ export async function GET(request: Request) {
|
||||
include: {
|
||||
author: { select: { id: true, firstName: true, lastName: true } },
|
||||
classGroup: { select: { id: true, name: true } },
|
||||
...(includeReads ? {
|
||||
reads: {
|
||||
include: {
|
||||
user: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
orderBy: { readAt: 'desc' as const },
|
||||
},
|
||||
} : {}),
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(announcements);
|
||||
// Add read status for the current user
|
||||
const currentUserId = session.userId;
|
||||
const announcementsWithReadStatus = await Promise.all(
|
||||
announcements.map(async (a) => {
|
||||
if (!includeReads) {
|
||||
const readReceipt = await db.announcementRead.findUnique({
|
||||
where: {
|
||||
announcementId_userId: {
|
||||
announcementId: a.id,
|
||||
userId: currentUserId,
|
||||
},
|
||||
},
|
||||
});
|
||||
return {
|
||||
...a,
|
||||
isReadByCurrentUser: !!readReceipt,
|
||||
};
|
||||
}
|
||||
const readReceipt = await db.announcementRead.findUnique({
|
||||
where: {
|
||||
announcementId_userId: {
|
||||
announcementId: a.id,
|
||||
userId: currentUserId,
|
||||
},
|
||||
},
|
||||
});
|
||||
return {
|
||||
...a,
|
||||
isReadByCurrentUser: !!readReceipt,
|
||||
totalReads: a.reads?.length ?? 0,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return NextResponse.json(announcementsWithReadStatus);
|
||||
} catch (error) {
|
||||
console.error('Announcements GET error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
@@ -138,6 +185,7 @@ export async function POST(request: Request) {
|
||||
title: rest.title,
|
||||
content: rest.content,
|
||||
priority: rest.priority,
|
||||
announcementType: rest.announcementType,
|
||||
targetAudience: rest.targetAudience,
|
||||
classGroupId: classGroupId || null,
|
||||
isPinned: rest.isPinned,
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
|
||||
// ── GET: Get single exam plan ──────────────────────────────────────────
|
||||
export async function GET(
|
||||
_request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
const examPlan = await db.examPlan.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
subject: { select: { id: true, name: true } },
|
||||
classGroup: { select: { id: true, name: true, gradeLevel: true } },
|
||||
teacher: { select: { id: true, firstName: true, lastName: true } },
|
||||
calendarEvent: { select: { id: true } },
|
||||
assessment: { select: { id: true, title: true } },
|
||||
},
|
||||
});
|
||||
|
||||
if (!examPlan) {
|
||||
return NextResponse.json({ error: 'Exam plan not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Role-based access check
|
||||
if (
|
||||
session.user?.role === 'STUDENT' ||
|
||||
session.user?.role === 'PARENT'
|
||||
) {
|
||||
// Students and parents can view but not modify
|
||||
} else if (session.user?.role === 'TEACHER') {
|
||||
// Teachers can only view their own exams
|
||||
if (examPlan.teacherId !== session.userId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json(examPlan);
|
||||
} catch (error) {
|
||||
console.error('ExamPlan GET error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// ── PUT: Update exam plan ──────────────────────────────────────────────
|
||||
export async function PUT(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const userRole = session.user?.role;
|
||||
|
||||
if (
|
||||
userRole !== 'TEACHER' &&
|
||||
userRole !== 'SCHOOL_ADMIN' &&
|
||||
userRole !== 'VICE_PRINCIPAL' &&
|
||||
userRole !== 'SUPER_ADMIN'
|
||||
) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const existing = await db.examPlan.findUnique({ where: { id } });
|
||||
if (!existing) {
|
||||
return NextResponse.json({ error: 'Exam plan not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Teachers can only edit their own exams
|
||||
if (userRole === 'TEACHER' && existing.teacherId !== session.userId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const {
|
||||
title,
|
||||
date,
|
||||
startTime,
|
||||
endTime,
|
||||
subjectId,
|
||||
classGroupId,
|
||||
room,
|
||||
topics,
|
||||
weight,
|
||||
status,
|
||||
notes,
|
||||
} = body;
|
||||
|
||||
const updateData: Record<string, unknown> = {};
|
||||
if (title !== undefined) updateData.title = title;
|
||||
if (date !== undefined) updateData.date = new Date(date);
|
||||
if (startTime !== undefined) updateData.startTime = startTime || null;
|
||||
if (endTime !== undefined) updateData.endTime = endTime || null;
|
||||
if (subjectId !== undefined) updateData.subjectId = subjectId;
|
||||
if (classGroupId !== undefined) updateData.classGroupId = classGroupId;
|
||||
if (room !== undefined) updateData.room = room || null;
|
||||
if (topics !== undefined) updateData.topics = topics ? JSON.stringify(topics) : null;
|
||||
if (weight !== undefined) updateData.weight = weight;
|
||||
if (status !== undefined) updateData.status = status;
|
||||
if (notes !== undefined) updateData.notes = notes || null;
|
||||
|
||||
const updated = await db.examPlan.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
include: {
|
||||
subject: { select: { id: true, name: true } },
|
||||
classGroup: { select: { id: true, name: true, gradeLevel: true } },
|
||||
teacher: { select: { id: true, firstName: true, lastName: true } },
|
||||
calendarEvent: { select: { id: true } },
|
||||
assessment: { select: { id: true, title: true } },
|
||||
},
|
||||
});
|
||||
|
||||
// Also update the linked CalendarEvent
|
||||
if (existing.calendarEventId) {
|
||||
const calendarUpdateData: Record<string, unknown> = {};
|
||||
if (title !== undefined) calendarUpdateData.title = `[Exam] ${title}`;
|
||||
if (date !== undefined) calendarUpdateData.date = new Date(date);
|
||||
if (startTime !== undefined) calendarUpdateData.startTime = startTime || null;
|
||||
if (endTime !== undefined) calendarUpdateData.endTime = endTime || null;
|
||||
if (subjectId !== undefined) calendarUpdateData.subjectId = subjectId;
|
||||
if (classGroupId !== undefined) calendarUpdateData.classGroupId = classGroupId;
|
||||
if (notes !== undefined) calendarUpdateData.notes = notes || null;
|
||||
if (startTime !== undefined || endTime !== undefined) {
|
||||
calendarUpdateData.allDay = !startTime && !endTime;
|
||||
}
|
||||
|
||||
if (Object.keys(calendarUpdateData).length > 0) {
|
||||
await db.calendarEvent.update({
|
||||
where: { id: existing.calendarEventId },
|
||||
data: calendarUpdateData,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json(updated);
|
||||
} catch (error) {
|
||||
console.error('ExamPlan PUT error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// ── DELETE: Delete exam plan ───────────────────────────────────────────
|
||||
export async function DELETE(
|
||||
_request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const userRole = session.user?.role;
|
||||
|
||||
if (
|
||||
userRole !== 'TEACHER' &&
|
||||
userRole !== 'SCHOOL_ADMIN' &&
|
||||
userRole !== 'VICE_PRINCIPAL' &&
|
||||
userRole !== 'SUPER_ADMIN'
|
||||
) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const existing = await db.examPlan.findUnique({ where: { id } });
|
||||
if (!existing) {
|
||||
return NextResponse.json({ error: 'Exam plan not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Teachers can only delete their own exams
|
||||
if (userRole === 'TEACHER' && existing.teacherId !== session.userId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Delete the linked CalendarEvent first
|
||||
if (existing.calendarEventId) {
|
||||
await db.calendarEvent.delete({ where: { id: existing.calendarEventId } }).catch(() => {});
|
||||
}
|
||||
|
||||
await db.examPlan.delete({ where: { id } });
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('ExamPlan DELETE error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { differenceInCalendarDays } from 'date-fns';
|
||||
|
||||
// ── GET: List exam plans with filters ──────────────────────────────────
|
||||
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 schoolId = searchParams.get('schoolId') ?? session.user?.schoolId ?? undefined;
|
||||
const classId = searchParams.get('classId');
|
||||
const subjectId = searchParams.get('subjectId');
|
||||
const teacherId = searchParams.get('teacherId');
|
||||
const status = searchParams.get('status');
|
||||
const dateFrom = searchParams.get('dateFrom');
|
||||
const dateTo = searchParams.get('dateTo');
|
||||
const viewMode = searchParams.get('viewMode') ?? 'all'; // all, upcoming, past
|
||||
|
||||
if (!schoolId) {
|
||||
return NextResponse.json({ error: 'schoolId is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const where: Record<string, unknown> = { schoolId };
|
||||
|
||||
if (classId) where.classGroupId = classId;
|
||||
if (subjectId) where.subjectId = subjectId;
|
||||
if (teacherId) where.teacherId = teacherId;
|
||||
if (status) where.status = status;
|
||||
|
||||
// Date filters
|
||||
const dateFilter: Record<string, Date> = {};
|
||||
if (dateFrom) dateFilter.gte = new Date(dateFrom);
|
||||
if (dateTo) dateFilter.lte = new Date(dateTo);
|
||||
if (Object.keys(dateFilter).length > 0) where.date = dateFilter;
|
||||
|
||||
// View mode filters
|
||||
const now = new Date();
|
||||
if (viewMode === 'upcoming') {
|
||||
where.date = { ...((where.date as Record<string, Date>) || {}), gte: now };
|
||||
} else if (viewMode === 'past') {
|
||||
where.date = { ...((where.date as Record<string, Date>) || {}), lt: now };
|
||||
}
|
||||
|
||||
// Role-based access
|
||||
if (session.user?.role === 'STUDENT') {
|
||||
const student = await db.student.findFirst({
|
||||
where: { userId: session.userId },
|
||||
select: { id: true },
|
||||
});
|
||||
if (student) {
|
||||
const enrollments = await db.enrollment.findMany({
|
||||
where: { studentId: student.id },
|
||||
select: { classGroupId: true },
|
||||
});
|
||||
const classIds = enrollments.map((e) => e.classGroupId);
|
||||
where.classGroupId = { in: classIds };
|
||||
}
|
||||
} else if (session.user?.role === 'PARENT') {
|
||||
const parentLinks = await db.parentStudentLink.findMany({
|
||||
where: { parentId: session.userId },
|
||||
select: { studentId: true },
|
||||
});
|
||||
const studentIds = parentLinks.map((l) => l.studentId);
|
||||
const enrollments = await db.enrollment.findMany({
|
||||
where: { studentId: { in: studentIds } },
|
||||
select: { classGroupId: true },
|
||||
});
|
||||
const classIds = enrollments.map((e) => e.classGroupId);
|
||||
where.classGroupId = { in: classIds };
|
||||
} else if (session.user?.role === 'TEACHER') {
|
||||
// Teachers see their own exams by default, or all if explicitly requested
|
||||
if (!teacherId) {
|
||||
where.teacherId = session.userId;
|
||||
}
|
||||
}
|
||||
|
||||
const examPlans = await db.examPlan.findMany({
|
||||
where,
|
||||
include: {
|
||||
subject: { select: { id: true, name: true } },
|
||||
classGroup: { select: { id: true, name: true, gradeLevel: true } },
|
||||
teacher: { select: { id: true, firstName: true, lastName: true } },
|
||||
calendarEvent: { select: { id: true } },
|
||||
assessment: { select: { id: true, title: true } },
|
||||
},
|
||||
orderBy: { date: 'asc' },
|
||||
});
|
||||
|
||||
// Add computed fields
|
||||
const enriched = examPlans.map((plan) => {
|
||||
const daysUntil = differenceInCalendarDays(new Date(plan.date), now);
|
||||
const isWithinTwoWeeks = daysUntil >= 0 && daysUntil <= 14;
|
||||
return {
|
||||
...plan,
|
||||
daysUntil,
|
||||
isWithinTwoWeeks,
|
||||
countdownLabel:
|
||||
daysUntil === 0
|
||||
? 'today'
|
||||
: daysUntil === 1
|
||||
? 'tomorrow'
|
||||
: daysUntil < 7
|
||||
? `${daysUntil} days`
|
||||
: daysUntil < 14
|
||||
? `${Math.floor(daysUntil / 7)} week`
|
||||
: `${Math.floor(daysUntil / 7)} weeks`,
|
||||
};
|
||||
});
|
||||
|
||||
// Check for AI test availability for each exam
|
||||
const examIdsWithTests = await db.aITestGeneration.findMany({
|
||||
where: {
|
||||
classGroupId: { in: enriched.map((e) => e.classGroupId) },
|
||||
subjectId: { in: enriched.map((e) => e.subjectId) },
|
||||
isCompleted: true,
|
||||
},
|
||||
select: { classGroupId: true, subjectId: true },
|
||||
});
|
||||
|
||||
const testSet = new Set(
|
||||
examIdsWithTests.map((t) => `${t.classGroupId}-${t.subjectId}`)
|
||||
);
|
||||
|
||||
const result = enriched.map((plan) => ({
|
||||
...plan,
|
||||
hasAITest: testSet.has(`${plan.classGroupId}-${plan.subjectId}`),
|
||||
}));
|
||||
|
||||
// Statistics (for admin view)
|
||||
let stats = null;
|
||||
if (
|
||||
session.user?.role === 'SCHOOL_ADMIN' ||
|
||||
session.user?.role === 'VICE_PRINCIPAL' ||
|
||||
session.user?.role === 'SUPER_ADMIN'
|
||||
) {
|
||||
const totalExams = await db.examPlan.count({ where: { schoolId } });
|
||||
const upcomingExams = await db.examPlan.count({
|
||||
where: { schoolId, date: { gte: now } },
|
||||
});
|
||||
const plannedExams = await db.examPlan.count({
|
||||
where: { schoolId, status: 'planned' },
|
||||
});
|
||||
const confirmedExams = await db.examPlan.count({
|
||||
where: { schoolId, status: 'confirmed' },
|
||||
});
|
||||
const completedExams = await db.examPlan.count({
|
||||
where: { schoolId, status: 'completed' },
|
||||
});
|
||||
const cancelledExams = await db.examPlan.count({
|
||||
where: { schoolId, status: 'cancelled' },
|
||||
});
|
||||
|
||||
// Exams per subject
|
||||
const examsPerSubject = await db.examPlan.groupBy({
|
||||
by: ['subjectId'],
|
||||
where: { schoolId },
|
||||
_count: { id: true },
|
||||
});
|
||||
|
||||
const subjectNames = await db.subject.findMany({
|
||||
where: {
|
||||
id: { in: examsPerSubject.map((e) => e.subjectId) },
|
||||
},
|
||||
select: { id: true, name: true },
|
||||
});
|
||||
|
||||
const subjectMap = Object.fromEntries(
|
||||
subjectNames.map((s) => [s.id, s.name])
|
||||
);
|
||||
|
||||
// Exams per class
|
||||
const examsPerClass = await db.examPlan.groupBy({
|
||||
by: ['classGroupId'],
|
||||
where: { schoolId },
|
||||
_count: { id: true },
|
||||
});
|
||||
|
||||
const classNames = await db.classGroup.findMany({
|
||||
where: {
|
||||
id: { in: examsPerClass.map((e) => e.classGroupId) },
|
||||
},
|
||||
select: { id: true, name: true },
|
||||
});
|
||||
|
||||
const classMap = Object.fromEntries(
|
||||
classNames.map((c) => [c.id, c.name])
|
||||
);
|
||||
|
||||
stats = {
|
||||
totalExams,
|
||||
upcomingExams,
|
||||
byStatus: { planned: plannedExams, confirmed: confirmedExams, completed: completedExams, cancelled: cancelledExams },
|
||||
bySubject: examsPerSubject.map((e) => ({
|
||||
subjectId: e.subjectId,
|
||||
subjectName: subjectMap[e.subjectId] || e.subjectId,
|
||||
count: e._count.id,
|
||||
})),
|
||||
byClass: examsPerClass.map((e) => ({
|
||||
classGroupId: e.classGroupId,
|
||||
className: classMap[e.classGroupId] || e.classGroupId,
|
||||
count: e._count.id,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
return NextResponse.json({ exams: result, stats });
|
||||
} catch (error) {
|
||||
console.error('ExamPlans GET error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// ── POST: Create an exam plan ──────────────────────────────────────────
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
const userRole = session.user?.role;
|
||||
if (
|
||||
userRole !== 'TEACHER' &&
|
||||
userRole !== 'SCHOOL_ADMIN' &&
|
||||
userRole !== 'VICE_PRINCIPAL' &&
|
||||
userRole !== 'SUPER_ADMIN'
|
||||
) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const {
|
||||
title,
|
||||
date,
|
||||
startTime,
|
||||
endTime,
|
||||
subjectId,
|
||||
classGroupId,
|
||||
schoolId,
|
||||
room,
|
||||
topics,
|
||||
weight,
|
||||
status,
|
||||
notes,
|
||||
} = body;
|
||||
|
||||
if (!title || !date || !schoolId || !subjectId || !classGroupId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'title, date, schoolId, subjectId, and classGroupId are required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Create the exam plan
|
||||
const examPlan = await db.examPlan.create({
|
||||
data: {
|
||||
schoolId,
|
||||
teacherId: session.userId,
|
||||
subjectId,
|
||||
classGroupId,
|
||||
title,
|
||||
date: new Date(date),
|
||||
startTime: startTime || null,
|
||||
endTime: endTime || null,
|
||||
room: room || null,
|
||||
topics: topics ? JSON.stringify(topics) : null,
|
||||
weight: weight ?? 1.0,
|
||||
status: status || 'planned',
|
||||
notes: notes || null,
|
||||
},
|
||||
include: {
|
||||
subject: { select: { id: true, name: true } },
|
||||
classGroup: { select: { id: true, name: true, gradeLevel: true } },
|
||||
teacher: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
});
|
||||
|
||||
// Also create a CalendarEvent for the exam
|
||||
const calendarEvent = await db.calendarEvent.create({
|
||||
data: {
|
||||
schoolId,
|
||||
teacherId: session.userId,
|
||||
title: `[Exam] ${title}`,
|
||||
date: new Date(date),
|
||||
startTime: startTime || null,
|
||||
endTime: endTime || null,
|
||||
eventType: 'exam',
|
||||
subjectId,
|
||||
classGroupId,
|
||||
notes: notes || null,
|
||||
allDay: !startTime && !endTime,
|
||||
},
|
||||
});
|
||||
|
||||
// Link the calendar event to the exam plan
|
||||
await db.examPlan.update({
|
||||
where: { id: examPlan.id },
|
||||
data: { calendarEventId: calendarEvent.id },
|
||||
});
|
||||
|
||||
const result = {
|
||||
...examPlan,
|
||||
calendarEventId: calendarEvent.id,
|
||||
};
|
||||
|
||||
return NextResponse.json(result, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('ExamPlans POST error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
|
||||
const updateAnnotationSchema = z.object({
|
||||
type: z.enum(['drawing', 'text', 'highlight', 'stamp']).optional(),
|
||||
content: z.string().optional(),
|
||||
positionX: z.number().optional(),
|
||||
positionY: z.number().optional(),
|
||||
width: z.number().optional(),
|
||||
height: z.number().optional(),
|
||||
color: z.string().optional(),
|
||||
strokeWidth: z.number().optional(),
|
||||
page: z.number().optional(),
|
||||
pathData: z.string().optional(),
|
||||
});
|
||||
|
||||
export async function GET(
|
||||
_request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const annotation = await db.gradingAnnotation.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
teacher: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
});
|
||||
|
||||
if (!annotation) {
|
||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json(annotation);
|
||||
} catch (error) {
|
||||
console.error('GradingAnnotation GET [id] error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
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 { id } = await params;
|
||||
const body = await request.json();
|
||||
const parsed = updateAnnotationSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Validation failed', details: parsed.error.issues },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const existing = await db.gradingAnnotation.findUnique({ where: { id } });
|
||||
if (!existing) {
|
||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Only the creator or admin can update
|
||||
if (existing.teacherId !== session.userId && role !== 'SCHOOL_ADMIN' && role !== 'SUPER_ADMIN') {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const annotation = await db.gradingAnnotation.update({
|
||||
where: { id },
|
||||
data: parsed.data,
|
||||
include: {
|
||||
teacher: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(annotation);
|
||||
} catch (error) {
|
||||
console.error('GradingAnnotation PUT [id] error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
_request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
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 { id } = await params;
|
||||
const existing = await db.gradingAnnotation.findUnique({ where: { id } });
|
||||
if (!existing) {
|
||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (existing.teacherId !== session.userId && role !== 'SCHOOL_ADMIN' && role !== 'SUPER_ADMIN') {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
await db.gradingAnnotation.delete({ where: { id } });
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('GradingAnnotation DELETE [id] error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
|
||||
const createAnnotationSchema = z.object({
|
||||
schoolId: z.string().min(1),
|
||||
assessmentId: z.string().min(1),
|
||||
studentId: z.string().min(1),
|
||||
resultId: z.string().optional(),
|
||||
type: z.enum(['drawing', 'text', 'highlight', 'stamp']).default('drawing'),
|
||||
content: z.string().optional(),
|
||||
positionX: z.number().default(0),
|
||||
positionY: z.number().default(0),
|
||||
width: z.number().optional(),
|
||||
height: z.number().optional(),
|
||||
color: z.string().default('#ef4444'),
|
||||
strokeWidth: z.number().default(2),
|
||||
page: z.number().default(1),
|
||||
pathData: 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');
|
||||
const page = searchParams.get('page');
|
||||
|
||||
if (!assessmentId) {
|
||||
return NextResponse.json({ error: 'assessmentId is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const where: Record<string, unknown> = { assessmentId };
|
||||
if (studentId) where.studentId = studentId;
|
||||
if (page) where.page = parseInt(page, 10);
|
||||
|
||||
const annotations = await db.gradingAnnotation.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'asc' },
|
||||
include: {
|
||||
teacher: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(annotations);
|
||||
} catch (error) {
|
||||
console.error('GradingAnnotations 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 = createAnnotationSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Validation failed', details: parsed.error.issues },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const data = parsed.data;
|
||||
const annotation = await db.gradingAnnotation.create({
|
||||
data: {
|
||||
schoolId: data.schoolId,
|
||||
assessmentId: data.assessmentId,
|
||||
studentId: data.studentId,
|
||||
resultId: data.resultId,
|
||||
teacherId: session.userId,
|
||||
type: data.type,
|
||||
content: data.content,
|
||||
positionX: data.positionX,
|
||||
positionY: data.positionY,
|
||||
width: data.width,
|
||||
height: data.height,
|
||||
color: data.color,
|
||||
strokeWidth: data.strokeWidth,
|
||||
page: data.page,
|
||||
pathData: data.pathData,
|
||||
},
|
||||
include: {
|
||||
teacher: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(annotation, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('GradingAnnotations POST error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// CompetenceTrack — Single Notification API
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
|
||||
export async function GET(
|
||||
_request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const session = await getSession();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const notification = await db.notification.findFirst({
|
||||
where: { id, userId: session.user.id, deletedAt: null },
|
||||
});
|
||||
|
||||
if (!notification) {
|
||||
return NextResponse.json({ error: 'Notification not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json(notification);
|
||||
}
|
||||
|
||||
export async function PUT(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const session = await getSession();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
const { isRead, isArchived, action } = body as {
|
||||
isRead?: boolean;
|
||||
isArchived?: boolean;
|
||||
action?: 'read' | 'unread' | 'archive' | 'unarchive' | 'dismiss';
|
||||
};
|
||||
|
||||
const existing = await db.notification.findFirst({
|
||||
where: { id, userId: session.user.id, deletedAt: null },
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
return NextResponse.json({ error: 'Notification not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const updateData: Record<string, unknown> = {};
|
||||
if (action === 'read' || isRead === true) {
|
||||
updateData.isRead = true;
|
||||
} else if (action === 'unread' || isRead === false) {
|
||||
updateData.isRead = false;
|
||||
}
|
||||
if (action === 'archive' || isArchived === true) {
|
||||
updateData.isArchived = true;
|
||||
} else if (action === 'unarchive' || isArchived === false) {
|
||||
updateData.isArchived = false;
|
||||
}
|
||||
if (action === 'dismiss') {
|
||||
updateData.deletedAt = new Date();
|
||||
}
|
||||
|
||||
const notification = await db.notification.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
});
|
||||
|
||||
return NextResponse.json({ notification });
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid request' }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
_request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const session = await getSession();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const existing = await db.notification.findFirst({
|
||||
where: { id, userId: session.user.id, deletedAt: null },
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
return NextResponse.json({ error: 'Notification not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
await db.notification.update({
|
||||
where: { id },
|
||||
data: { deletedAt: new Date() },
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
// CompetenceTrack — Notification Preferences API
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
|
||||
export async function GET() {
|
||||
const session = await getSession();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const schoolId = session.user.schoolId;
|
||||
if (!schoolId) {
|
||||
return NextResponse.json({ error: 'No school assigned' }, { status: 400 });
|
||||
}
|
||||
|
||||
let prefs = await db.notificationPreferences.findUnique({
|
||||
where: { userId: session.user.id },
|
||||
});
|
||||
|
||||
// Create default preferences if none exist
|
||||
if (!prefs) {
|
||||
prefs = await db.notificationPreferences.create({
|
||||
data: {
|
||||
userId: session.user.id,
|
||||
schoolId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json(prefs);
|
||||
}
|
||||
|
||||
export async function PUT(request: Request) {
|
||||
const session = await getSession();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const schoolId = session.user.schoolId;
|
||||
if (!schoolId) {
|
||||
return NextResponse.json({ error: 'No school assigned' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
const {
|
||||
academicEnabled,
|
||||
behavioralEnabled,
|
||||
administrativeEnabled,
|
||||
calendarEnabled,
|
||||
communicationEnabled,
|
||||
systemEnabled,
|
||||
quietHoursStart,
|
||||
quietHoursEnd,
|
||||
quietHoursEnabled,
|
||||
emailDigestEnabled,
|
||||
emailDigestFrequency,
|
||||
} = body as {
|
||||
academicEnabled?: boolean;
|
||||
behavioralEnabled?: boolean;
|
||||
administrativeEnabled?: boolean;
|
||||
calendarEnabled?: boolean;
|
||||
communicationEnabled?: boolean;
|
||||
systemEnabled?: boolean;
|
||||
quietHoursStart?: string | null;
|
||||
quietHoursEnd?: string | null;
|
||||
quietHoursEnabled?: boolean;
|
||||
emailDigestEnabled?: boolean;
|
||||
emailDigestFrequency?: string;
|
||||
};
|
||||
|
||||
const updateData: Record<string, unknown> = {};
|
||||
if (academicEnabled !== undefined) updateData.academicEnabled = academicEnabled;
|
||||
if (behavioralEnabled !== undefined) updateData.behavioralEnabled = behavioralEnabled;
|
||||
if (administrativeEnabled !== undefined) updateData.administrativeEnabled = administrativeEnabled;
|
||||
if (calendarEnabled !== undefined) updateData.calendarEnabled = calendarEnabled;
|
||||
if (communicationEnabled !== undefined) updateData.communicationEnabled = communicationEnabled;
|
||||
if (systemEnabled !== undefined) updateData.systemEnabled = systemEnabled;
|
||||
if (quietHoursStart !== undefined) updateData.quietHoursStart = quietHoursStart;
|
||||
if (quietHoursEnd !== undefined) updateData.quietHoursEnd = quietHoursEnd;
|
||||
if (quietHoursEnabled !== undefined) updateData.quietHoursEnabled = quietHoursEnabled;
|
||||
if (emailDigestEnabled !== undefined) updateData.emailDigestEnabled = emailDigestEnabled;
|
||||
if (emailDigestFrequency !== undefined) updateData.emailDigestFrequency = emailDigestFrequency;
|
||||
|
||||
const prefs = await db.notificationPreferences.upsert({
|
||||
where: { userId: session.user.id },
|
||||
update: updateData,
|
||||
create: {
|
||||
userId: session.user.id,
|
||||
schoolId,
|
||||
...updateData,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(prefs);
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid request' }, { status: 400 });
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
// CompetenceTrack — Notifications API (DB-backed)
|
||||
// CompetenceTrack — Notifications API (DB-backed, Enhanced)
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
|
||||
export async function GET() {
|
||||
export async function GET(request: Request) {
|
||||
const session = await getSession();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
@@ -19,18 +19,54 @@ export async function GET() {
|
||||
});
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const category = searchParams.get('category');
|
||||
const priority = searchParams.get('priority');
|
||||
const isRead = searchParams.get('isRead');
|
||||
const isArchived = searchParams.get('isArchived');
|
||||
const dateFrom = searchParams.get('dateFrom');
|
||||
const dateTo = searchParams.get('dateTo');
|
||||
const limit = parseInt(searchParams.get('limit') || '50', 10);
|
||||
const offset = parseInt(searchParams.get('offset') || '0', 10);
|
||||
|
||||
// Build where clause for DB notifications
|
||||
const where: Record<string, unknown> = {
|
||||
userId,
|
||||
deletedAt: null,
|
||||
};
|
||||
|
||||
if (category && category !== 'all') where.category = category;
|
||||
if (priority && priority !== 'all') where.priority = priority;
|
||||
if (isRead === 'true') where.isRead = true;
|
||||
else if (isRead === 'false') where.isRead = false;
|
||||
if (isArchived === 'true') where.isArchived = true;
|
||||
else if (isArchived === 'false') where.isArchived = false;
|
||||
else if (!isArchived) where.isArchived = false; // default: not archived
|
||||
|
||||
if (dateFrom || dateTo) {
|
||||
const dateFilter: Record<string, unknown> = {};
|
||||
if (dateFrom) dateFilter.gte = new Date(dateFrom);
|
||||
if (dateTo) dateFilter.lte = new Date(dateTo);
|
||||
where.createdAt = dateFilter;
|
||||
}
|
||||
|
||||
// Fetch DB notifications for the current user
|
||||
const notifications = await db.notification.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: limit,
|
||||
skip: offset,
|
||||
});
|
||||
|
||||
const unreadCount = await db.notification.count({
|
||||
where: {
|
||||
userId,
|
||||
deletedAt: null,
|
||||
isRead: false,
|
||||
isArchived: false,
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 50,
|
||||
});
|
||||
|
||||
const unreadCount = notifications.filter((n) => !n.isRead).length;
|
||||
|
||||
// Also generate dynamic notifications from existing data
|
||||
const now = new Date();
|
||||
const sevenDaysFromNow = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
|
||||
@@ -71,12 +107,15 @@ export async function GET() {
|
||||
}
|
||||
|
||||
// Upcoming assessments (next 7 days)
|
||||
const upcomingAssessments: Array<{
|
||||
const dynamicNotifications: Array<{
|
||||
id: string;
|
||||
type: string;
|
||||
category: string;
|
||||
priority: string;
|
||||
title: string;
|
||||
message: string;
|
||||
isRead: boolean;
|
||||
isArchived: boolean;
|
||||
actionUrl: string | null;
|
||||
relatedId: string | null;
|
||||
createdAt: string;
|
||||
@@ -94,26 +133,25 @@ export async function GET() {
|
||||
include: {
|
||||
classGroup: { select: { name: true } },
|
||||
subject: { select: { name: true } },
|
||||
assessmentResults: { select: { id: true } },
|
||||
},
|
||||
orderBy: { date: 'asc' },
|
||||
take: 10,
|
||||
});
|
||||
|
||||
for (const a of assessments) {
|
||||
const diffMs = new Date(a.date).getTime() - now.getTime();
|
||||
const daysUntil = Math.max(0, Math.ceil(diffMs / (1000 * 60 * 60 * 24)));
|
||||
// Check if we already have a DB notification for this assessment
|
||||
const existing = notifications.find(
|
||||
(n) => n.type === 'ASSESSMENT_DUE' && n.relatedId === a.id
|
||||
);
|
||||
if (!existing) {
|
||||
upcomingAssessments.push({
|
||||
dynamicNotifications.push({
|
||||
id: `assessment-${a.id}`,
|
||||
type: 'ASSESSMENT_DUE',
|
||||
category: 'academic',
|
||||
priority: 'high',
|
||||
title: a.title,
|
||||
message: `${a.classGroup.name} - ${a.subject.name}`,
|
||||
isRead: false,
|
||||
isArchived: false,
|
||||
actionUrl: 'assessments',
|
||||
relatedId: a.id,
|
||||
createdAt: new Date().toISOString(),
|
||||
@@ -153,12 +191,15 @@ export async function GET() {
|
||||
(n) => n.type === 'MISSING_OBSERVATION' && n.relatedId === e.student.id
|
||||
);
|
||||
if (!existing) {
|
||||
upcomingAssessments.push({
|
||||
dynamicNotifications.push({
|
||||
id: `missing-${e.student.id}-${e.classGroupId}`,
|
||||
type: 'MISSING_OBSERVATION',
|
||||
category: 'academic',
|
||||
priority: 'normal',
|
||||
title: `${e.student.firstName} ${e.student.lastName}`,
|
||||
message: e.classGroup.name,
|
||||
isRead: false,
|
||||
isArchived: false,
|
||||
actionUrl: 'progress',
|
||||
relatedId: e.student.id,
|
||||
createdAt: new Date().toISOString(),
|
||||
@@ -173,19 +214,41 @@ export async function GET() {
|
||||
const dbNotifications = notifications.map((n) => ({
|
||||
id: n.id,
|
||||
type: n.type,
|
||||
category: n.category,
|
||||
priority: n.priority,
|
||||
title: n.title,
|
||||
message: n.message,
|
||||
isRead: n.isRead,
|
||||
isArchived: n.isArchived,
|
||||
actionUrl: n.actionUrl,
|
||||
relatedId: n.relatedId,
|
||||
createdAt: n.createdAt.toISOString(),
|
||||
}));
|
||||
|
||||
const allNotifications = [...dbNotifications, ...upcomingAssessments];
|
||||
const allNotifications = [...dbNotifications, ...dynamicNotifications];
|
||||
|
||||
// Get statistics
|
||||
const totalByCategory = await db.notification.groupBy({
|
||||
by: ['category'],
|
||||
where: {
|
||||
userId,
|
||||
deletedAt: null,
|
||||
isArchived: false,
|
||||
},
|
||||
_count: { id: true },
|
||||
});
|
||||
|
||||
const stats = {
|
||||
byCategory: totalByCategory.map((item) => ({
|
||||
category: item.category,
|
||||
count: item._count.id,
|
||||
})),
|
||||
};
|
||||
|
||||
return NextResponse.json({
|
||||
notifications: allNotifications,
|
||||
unreadCount: allNotifications.filter((n) => !n.isRead).length,
|
||||
unreadCount,
|
||||
stats,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -197,10 +260,15 @@ export async function PUT(request: Request) {
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { ids, markAll } = body as { ids?: string[]; markAll?: boolean };
|
||||
const { ids, markAll, action, archiveAll, deleteIds } = body as {
|
||||
ids?: string[];
|
||||
markAll?: boolean;
|
||||
action?: 'read' | 'unread' | 'archive' | 'unarchive';
|
||||
archiveAll?: boolean;
|
||||
deleteIds?: string[];
|
||||
};
|
||||
|
||||
if (markAll) {
|
||||
// Mark all notifications as read
|
||||
await db.notification.updateMany({
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
@@ -212,16 +280,48 @@ export async function PUT(request: Request) {
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
if (archiveAll) {
|
||||
await db.notification.updateMany({
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
deletedAt: null,
|
||||
isArchived: false,
|
||||
},
|
||||
data: { isArchived: true },
|
||||
});
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
if (ids && Array.isArray(ids)) {
|
||||
// Mark specific DB notifications as read
|
||||
const dbIds = ids.filter((id) => !id.startsWith('assessment-') && !id.startsWith('missing-'));
|
||||
if (dbIds.length > 0) {
|
||||
const updateData: Record<string, unknown> = {};
|
||||
if (action === 'read') updateData.isRead = true;
|
||||
else if (action === 'unread') updateData.isRead = false;
|
||||
else if (action === 'archive') updateData.isArchived = true;
|
||||
else if (action === 'unarchive') updateData.isArchived = false;
|
||||
else updateData.isRead = true; // default: mark read
|
||||
|
||||
await db.notification.updateMany({
|
||||
where: {
|
||||
id: { in: dbIds },
|
||||
userId: session.user.id,
|
||||
},
|
||||
data: updateData,
|
||||
});
|
||||
}
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
if (deleteIds && Array.isArray(deleteIds)) {
|
||||
const dbIds = deleteIds.filter((id) => !id.startsWith('assessment-') && !id.startsWith('missing-'));
|
||||
if (dbIds.length > 0) {
|
||||
await db.notification.updateMany({
|
||||
where: {
|
||||
id: { in: dbIds },
|
||||
userId: session.user.id,
|
||||
},
|
||||
data: { isRead: true },
|
||||
data: { deletedAt: new Date() },
|
||||
});
|
||||
}
|
||||
return NextResponse.json({ success: true });
|
||||
@@ -241,10 +341,12 @@ export async function POST(request: Request) {
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { schoolId, userId, type, title, message, actionUrl, relatedId } = body as {
|
||||
const { schoolId, userId, type, category, priority, title, message, actionUrl, relatedId } = body as {
|
||||
schoolId: string;
|
||||
userId: string;
|
||||
type: string;
|
||||
category?: string;
|
||||
priority?: string;
|
||||
title: string;
|
||||
message: string;
|
||||
actionUrl?: string;
|
||||
@@ -255,11 +357,27 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Determine category from type if not provided
|
||||
const categoryMap: Record<string, string> = {
|
||||
ASSESSMENT_DUE: 'academic',
|
||||
MISSING_OBSERVATION: 'academic',
|
||||
NOTEBOOK_SHARED: 'academic',
|
||||
BEHAVIOR_ALERT: 'behavioral',
|
||||
GRADE_COMPUTED: 'academic',
|
||||
ATTENDANCE_ALERT: 'administrative',
|
||||
REPORT_READY: 'administrative',
|
||||
CALENDAR_EVENT: 'calendar',
|
||||
COMMUNICATION: 'communication',
|
||||
SYSTEM: 'system',
|
||||
};
|
||||
|
||||
const notification = await db.notification.create({
|
||||
data: {
|
||||
schoolId,
|
||||
userId,
|
||||
type,
|
||||
category: category || categoryMap[type] || 'system',
|
||||
priority: priority || 'normal',
|
||||
title,
|
||||
message,
|
||||
actionUrl: actionUrl ?? null,
|
||||
@@ -284,16 +402,15 @@ export async function DELETE(request: Request) {
|
||||
const { ids } = body as { ids?: string[] };
|
||||
|
||||
if (ids && Array.isArray(ids)) {
|
||||
// Soft-delete specific notifications
|
||||
const dbIds = ids.filter((id) => !id.startsWith('assessment-') && !id.startsWith('missing-'));
|
||||
await db.notification.updateMany({
|
||||
where: {
|
||||
id: { in: ids },
|
||||
id: { in: dbIds },
|
||||
userId: session.user.id,
|
||||
},
|
||||
data: { deletedAt: new Date() },
|
||||
});
|
||||
} else {
|
||||
// Soft-delete all notifications for the user
|
||||
await db.notification.updateMany({
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
|
||||
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 schoolIdParam = searchParams.get('schoolId');
|
||||
const studentIdParam = searchParams.get('studentId');
|
||||
|
||||
let schoolId: string | undefined;
|
||||
if (session.user?.role === 'SCHOOL_ADMIN') {
|
||||
schoolId = session.user.schoolId ?? undefined;
|
||||
} else {
|
||||
schoolId = schoolIdParam ?? session.user?.schoolId ?? undefined;
|
||||
}
|
||||
|
||||
if (!schoolId) return NextResponse.json([]);
|
||||
|
||||
// If student is requesting their own data
|
||||
if (session.user?.role === 'STUDENT') {
|
||||
const student = await db.student.findFirst({
|
||||
where: { userId: session.userId, schoolId, deletedAt: null },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!student) return NextResponse.json({ badges: [], character: null, leaderboard: [] });
|
||||
|
||||
const studentId = student.id;
|
||||
|
||||
// Get student's earned badges
|
||||
const studentBadges = await db.studentBadge.findMany({
|
||||
where: { schoolId, studentId },
|
||||
orderBy: { awardedAt: 'desc' },
|
||||
include: {
|
||||
badge: true,
|
||||
awardedByUser: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
});
|
||||
|
||||
// Get all available badges for the school
|
||||
const allBadges = await db.badge.findMany({
|
||||
where: { schoolId, deletedAt: null },
|
||||
orderBy: { category: 'asc' },
|
||||
});
|
||||
|
||||
// Get virtual character
|
||||
const character = await db.virtualCharacter.findUnique({
|
||||
where: { userId: session.userId },
|
||||
});
|
||||
|
||||
// Get class-level leaderboard (students in same class, ordered by XP)
|
||||
const enrollment = await db.enrollment.findFirst({
|
||||
where: { studentId, endDate: null },
|
||||
select: { classGroupId: true },
|
||||
});
|
||||
|
||||
let leaderboard: Array<{ studentId: string; firstName: string; lastName: string; xp: number; level: number; badgeCount: number }> = [];
|
||||
if (enrollment) {
|
||||
const classStudents = await db.enrollment.findMany({
|
||||
where: { classGroupId: enrollment.classGroupId, endDate: null },
|
||||
select: { studentId: true },
|
||||
});
|
||||
const classStudentIds = classStudents.map(e => e.studentId);
|
||||
|
||||
const studentData = await db.student.findMany({
|
||||
where: { id: { in: classStudentIds }, deletedAt: null },
|
||||
select: {
|
||||
id: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
userId: true,
|
||||
studentBadges: { select: { id: true } },
|
||||
},
|
||||
});
|
||||
|
||||
const charData = await db.virtualCharacter.findMany({
|
||||
where: { userId: { in: studentData.map(s => s.userId).filter(Boolean) as string[] } },
|
||||
select: { userId: true, xp: true, level: true },
|
||||
});
|
||||
|
||||
const charMap = new Map(charData.map(c => [c.userId, c]));
|
||||
leaderboard = studentData
|
||||
.map(s => {
|
||||
const char = s.userId ? charMap.get(s.userId) : null;
|
||||
return {
|
||||
studentId: s.id,
|
||||
firstName: s.firstName,
|
||||
lastName: s.lastName,
|
||||
xp: char?.xp ?? 0,
|
||||
level: char?.level ?? 1,
|
||||
badgeCount: s.studentBadges.length,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => b.xp - a.xp)
|
||||
.slice(0, 20);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
badges: allBadges.map(b => ({
|
||||
...b,
|
||||
earned: studentBadges.some(sb => sb.badgeId === b.id),
|
||||
earnedAt: studentBadges.find(sb => sb.badgeId === b.id)?.awardedAt ?? null,
|
||||
})),
|
||||
earnedBadges: studentBadges,
|
||||
character,
|
||||
leaderboard,
|
||||
totalXP: character?.xp ?? 0,
|
||||
level: character?.level ?? 1,
|
||||
});
|
||||
}
|
||||
|
||||
// Teacher/Admin view
|
||||
if (studentIdParam) {
|
||||
const studentBadges = await db.studentBadge.findMany({
|
||||
where: { schoolId, studentId: studentIdParam },
|
||||
orderBy: { awardedAt: 'desc' },
|
||||
include: {
|
||||
badge: true,
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
awardedByUser: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
});
|
||||
|
||||
const allBadges = await db.badge.findMany({
|
||||
where: { schoolId, deletedAt: null },
|
||||
orderBy: { category: 'asc' },
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
badges: allBadges.map(b => ({
|
||||
...b,
|
||||
earned: studentBadges.some(sb => sb.badgeId === b.id),
|
||||
earnedAt: studentBadges.find(sb => sb.badgeId === b.id)?.awardedAt ?? null,
|
||||
})),
|
||||
earnedBadges: studentBadges,
|
||||
});
|
||||
}
|
||||
|
||||
// Return all badges for school
|
||||
const allBadges = await db.badge.findMany({
|
||||
where: { schoolId, deletedAt: null },
|
||||
orderBy: { category: 'asc' },
|
||||
});
|
||||
|
||||
return NextResponse.json({ badges: allBadges });
|
||||
} catch (error) {
|
||||
console.error('StudentAchievements 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 body = await request.json();
|
||||
const { action, schoolId, studentId, badgeId, xpAmount, characterData } = body;
|
||||
|
||||
if (action === 'award_badge') {
|
||||
if (!schoolId || !studentId || !badgeId) {
|
||||
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Check if already awarded
|
||||
const existing = await db.studentBadge.findUnique({
|
||||
where: { studentId_badgeId: { studentId, badgeId } },
|
||||
});
|
||||
if (existing) return NextResponse.json({ error: 'Badge already awarded' }, { status: 409 });
|
||||
|
||||
const badge = await db.badge.findUnique({ where: { id: badgeId, deletedAt: null } });
|
||||
if (!badge) return NextResponse.json({ error: 'Badge not found' }, { status: 404 });
|
||||
|
||||
const studentBadge = await db.studentBadge.create({
|
||||
data: {
|
||||
schoolId,
|
||||
studentId,
|
||||
badgeId,
|
||||
awardedBy: session.userId,
|
||||
},
|
||||
include: { badge: true },
|
||||
});
|
||||
|
||||
// Award XP for badge
|
||||
const student = await db.student.findUnique({ where: { id: studentId }, select: { userId: true } });
|
||||
if (student?.userId) {
|
||||
await upsertVirtualCharacter(student.userId, schoolId, 50);
|
||||
}
|
||||
|
||||
return NextResponse.json(studentBadge, { status: 201 });
|
||||
}
|
||||
|
||||
if (action === 'add_xp') {
|
||||
if (!schoolId || !studentId || xpAmount === undefined) {
|
||||
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
||||
}
|
||||
|
||||
const student = await db.student.findUnique({ where: { id: studentId }, select: { userId: true } });
|
||||
if (!student?.userId) return NextResponse.json({ error: 'Student has no user account' }, { status: 400 });
|
||||
|
||||
const character = await upsertVirtualCharacter(student.userId, schoolId, xpAmount);
|
||||
return NextResponse.json(character);
|
||||
}
|
||||
|
||||
if (action === 'update_character') {
|
||||
if (!characterData) {
|
||||
return NextResponse.json({ error: 'Missing characterData' }, { status: 400 });
|
||||
}
|
||||
|
||||
const character = await db.virtualCharacter.upsert({
|
||||
where: { userId: session.userId },
|
||||
update: {
|
||||
characterId: characterData.characterId ?? undefined,
|
||||
name: characterData.name ?? undefined,
|
||||
color: characterData.color ?? undefined,
|
||||
mood: characterData.mood ?? undefined,
|
||||
},
|
||||
create: {
|
||||
userId: session.userId,
|
||||
schoolId: schoolId ?? session.user?.schoolId ?? '',
|
||||
characterId: characterData.characterId ?? 'owl',
|
||||
name: characterData.name ?? 'My Companion',
|
||||
color: characterData.color ?? '#10b981',
|
||||
mood: characterData.mood ?? 'happy',
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(character);
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Invalid action' }, { status: 400 });
|
||||
} catch (error) {
|
||||
console.error('StudentAchievements POST error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
async function upsertVirtualCharacter(userId: string, schoolId: string, xpGain: number) {
|
||||
const existing = await db.virtualCharacter.findUnique({ where: { userId } });
|
||||
|
||||
if (existing) {
|
||||
const newXp = existing.xp + xpGain;
|
||||
const newLevel = Math.floor(newXp / 100) + 1;
|
||||
|
||||
return db.virtualCharacter.update({
|
||||
where: { userId },
|
||||
data: {
|
||||
xp: newXp,
|
||||
level: newLevel,
|
||||
mood: newLevel > existing.level ? 'celebrating' : 'happy',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return db.virtualCharacter.create({
|
||||
data: {
|
||||
userId,
|
||||
schoolId,
|
||||
characterId: 'owl',
|
||||
name: 'My Companion',
|
||||
xp: xpGain,
|
||||
level: Math.floor(xpGain / 100) + 1,
|
||||
mood: 'happy',
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
|
||||
export async function PUT(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session) return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
|
||||
// Verify the plan belongs to the student
|
||||
const existing = await db.studyPlan.findUnique({ where: { id } });
|
||||
if (!existing) return NextResponse.json({ error: 'Plan not found' }, { status: 404 });
|
||||
|
||||
if (session.user?.role === 'STUDENT') {
|
||||
const student = await db.student.findFirst({
|
||||
where: { userId: session.userId, schoolId: existing.schoolId, deletedAt: null },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!student || student.id !== existing.studentId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
const plan = await db.studyPlan.update({
|
||||
where: { id },
|
||||
data: {
|
||||
title: body.title ?? undefined,
|
||||
description: body.description ?? undefined,
|
||||
subjectId: body.subjectId ?? undefined,
|
||||
subjectName: body.subjectName ?? undefined,
|
||||
dayOfWeek: body.dayOfWeek ?? undefined,
|
||||
startTime: body.startTime ?? undefined,
|
||||
duration: body.duration ?? undefined,
|
||||
isRecurring: body.isRecurring ?? undefined,
|
||||
specificDate: body.specificDate ? new Date(body.specificDate) : undefined,
|
||||
priority: body.priority ?? undefined,
|
||||
status: body.status ?? undefined,
|
||||
color: body.color ?? undefined,
|
||||
notes: body.notes ?? undefined,
|
||||
},
|
||||
include: {
|
||||
subject: { select: { id: true, name: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(plan);
|
||||
} catch (error) {
|
||||
console.error('StudyPlan PUT error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session) return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
const existing = await db.studyPlan.findUnique({ where: { id } });
|
||||
if (!existing) return NextResponse.json({ error: 'Plan not found' }, { status: 404 });
|
||||
|
||||
if (session.user?.role === 'STUDENT') {
|
||||
const student = await db.student.findFirst({
|
||||
where: { userId: session.userId, schoolId: existing.schoolId, deletedAt: null },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!student || student.id !== existing.studentId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
// Soft delete by setting status to cancelled
|
||||
await db.studyPlan.update({
|
||||
where: { id },
|
||||
data: { status: 'cancelled' },
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('StudyPlan DELETE error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
|
||||
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 schoolIdParam = searchParams.get('schoolId');
|
||||
const studentIdParam = searchParams.get('studentId');
|
||||
|
||||
let schoolId: string | undefined;
|
||||
if (session.user?.role === 'SCHOOL_ADMIN') {
|
||||
schoolId = session.user.schoolId ?? undefined;
|
||||
} else {
|
||||
schoolId = schoolIdParam ?? session.user?.schoolId ?? undefined;
|
||||
}
|
||||
|
||||
if (!schoolId) return NextResponse.json([]);
|
||||
|
||||
let studentId = studentIdParam;
|
||||
|
||||
// If student is requesting their own data
|
||||
if (session.user?.role === 'STUDENT' && !studentId) {
|
||||
const student = await db.student.findFirst({
|
||||
where: { userId: session.userId, schoolId, deletedAt: null },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!student) return NextResponse.json([]);
|
||||
studentId = student.id;
|
||||
}
|
||||
|
||||
if (!studentId) return NextResponse.json([]);
|
||||
|
||||
// Verify access
|
||||
if (session.user?.role === 'STUDENT') {
|
||||
const student = await db.student.findUnique({
|
||||
where: { id: studentId },
|
||||
select: { userId: true },
|
||||
});
|
||||
if (!student || student.userId !== session.userId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
const studyPlans = await db.studyPlan.findMany({
|
||||
where: { schoolId, studentId, status: 'active' },
|
||||
orderBy: [{ dayOfWeek: 'asc' }, { startTime: 'asc' }],
|
||||
include: {
|
||||
subject: { select: { id: true, name: true } },
|
||||
},
|
||||
});
|
||||
|
||||
// Get recent sessions
|
||||
const recentSessions = await db.studySession.findMany({
|
||||
where: { schoolId, studentId },
|
||||
orderBy: { startTime: 'desc' },
|
||||
take: 10,
|
||||
include: {
|
||||
subject: { select: { id: true, name: true } },
|
||||
},
|
||||
});
|
||||
|
||||
// Get weekly stats
|
||||
const oneWeekAgo = new Date();
|
||||
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
|
||||
|
||||
const weekSessions = await db.studySession.findMany({
|
||||
where: {
|
||||
schoolId,
|
||||
studentId,
|
||||
status: 'completed',
|
||||
startTime: { gte: oneWeekAgo },
|
||||
},
|
||||
});
|
||||
|
||||
const totalMinutesThisWeek = weekSessions.reduce((sum, s) => sum + s.duration, 0);
|
||||
const subjectTimeMap = new Map<string, number>();
|
||||
weekSessions.forEach(s => {
|
||||
const name = s.subjectName ?? 'Other';
|
||||
subjectTimeMap.set(name, (subjectTimeMap.get(name) ?? 0) + s.duration);
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
plans: studyPlans,
|
||||
recentSessions,
|
||||
weeklyStats: {
|
||||
totalMinutes: totalMinutesThisWeek,
|
||||
sessionsCount: weekSessions.length,
|
||||
subjectBreakdown: Object.fromEntries(subjectTimeMap),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('StudyPlanner 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 body = await request.json();
|
||||
const {
|
||||
schoolId,
|
||||
studentId,
|
||||
title,
|
||||
description,
|
||||
subjectId,
|
||||
subjectName,
|
||||
dayOfWeek,
|
||||
startTime,
|
||||
duration,
|
||||
isRecurring,
|
||||
specificDate,
|
||||
priority,
|
||||
color,
|
||||
notes,
|
||||
} = body;
|
||||
|
||||
if (!schoolId || !studentId || !title || dayOfWeek === undefined || !startTime || !duration) {
|
||||
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Verify access
|
||||
if (session.user?.role === 'STUDENT') {
|
||||
const student = await db.student.findUnique({
|
||||
where: { id: studentId },
|
||||
select: { userId: true, schoolId: true },
|
||||
});
|
||||
if (!student || student.userId !== session.userId || student.schoolId !== schoolId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
const plan = await db.studyPlan.create({
|
||||
data: {
|
||||
schoolId,
|
||||
studentId,
|
||||
title,
|
||||
description: description || null,
|
||||
subjectId: subjectId || null,
|
||||
subjectName: subjectName || null,
|
||||
dayOfWeek,
|
||||
startTime,
|
||||
duration,
|
||||
isRecurring: isRecurring ?? true,
|
||||
specificDate: specificDate ? new Date(specificDate) : null,
|
||||
priority: priority || 'medium',
|
||||
color: color || null,
|
||||
notes: notes || null,
|
||||
},
|
||||
include: {
|
||||
subject: { select: { id: true, name: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(plan, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('StudyPlanner POST error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session) return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const {
|
||||
schoolId,
|
||||
studentId,
|
||||
studyPlanId,
|
||||
subjectId,
|
||||
subjectName,
|
||||
startTime,
|
||||
plannedDuration,
|
||||
type,
|
||||
pomodoroLength,
|
||||
breakLength,
|
||||
notes,
|
||||
} = body;
|
||||
|
||||
if (!schoolId || !studentId) {
|
||||
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Verify access
|
||||
if (session.user?.role === 'STUDENT') {
|
||||
const student = await db.student.findUnique({
|
||||
where: { id: studentId },
|
||||
select: { userId: true, schoolId: true },
|
||||
});
|
||||
if (!student || student.userId !== session.userId || student.schoolId !== schoolId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
const studySession = await db.studySession.create({
|
||||
data: {
|
||||
schoolId,
|
||||
studentId,
|
||||
studyPlanId: studyPlanId || null,
|
||||
subjectId: subjectId || null,
|
||||
subjectName: subjectName || null,
|
||||
startTime: startTime ? new Date(startTime) : new Date(),
|
||||
plannedDuration: plannedDuration || null,
|
||||
type: type || 'pomodoro',
|
||||
pomodoroLength: pomodoroLength || 25,
|
||||
breakLength: breakLength || 5,
|
||||
notes: notes || null,
|
||||
status: 'in_progress',
|
||||
},
|
||||
});
|
||||
|
||||
// Award XP for starting a study session
|
||||
const student = await db.student.findUnique({ where: { id: studentId }, select: { userId: true } });
|
||||
if (student?.userId) {
|
||||
const existing = await db.virtualCharacter.findUnique({ where: { userId: student.userId } });
|
||||
if (existing) {
|
||||
await db.virtualCharacter.update({
|
||||
where: { userId: student.userId },
|
||||
data: { xp: existing.xp + 5, level: Math.floor((existing.xp + 5) / 100) + 1 },
|
||||
});
|
||||
} else {
|
||||
await db.virtualCharacter.create({
|
||||
data: {
|
||||
userId: student.userId,
|
||||
schoolId,
|
||||
characterId: 'owl',
|
||||
name: 'My Companion',
|
||||
xp: 5,
|
||||
level: 1,
|
||||
mood: 'happy',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json(studySession, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('StudySession 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 });
|
||||
|
||||
const body = await request.json();
|
||||
const { sessionId, endTime, duration, pomodorosCompleted, focusScore, status, notes } = body;
|
||||
|
||||
if (!sessionId) {
|
||||
return NextResponse.json({ error: 'sessionId is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const existing = await db.studySession.findUnique({ where: { id: sessionId } });
|
||||
if (!existing) return NextResponse.json({ error: 'Session not found' }, { status: 404 });
|
||||
|
||||
// Verify access
|
||||
if (session.user?.role === 'STUDENT') {
|
||||
const student = await db.student.findFirst({
|
||||
where: { userId: session.userId, schoolId: existing.schoolId, deletedAt: null },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!student || student.id !== existing.studentId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
const updated = await db.studySession.update({
|
||||
where: { id: sessionId },
|
||||
data: {
|
||||
endTime: endTime ? new Date(endTime) : undefined,
|
||||
duration: duration ?? undefined,
|
||||
pomodorosCompleted: pomodorosCompleted ?? undefined,
|
||||
focusScore: focusScore ?? undefined,
|
||||
status: status ?? undefined,
|
||||
notes: notes ?? undefined,
|
||||
},
|
||||
});
|
||||
|
||||
// Award XP for completing a study session
|
||||
if (status === 'completed' && duration) {
|
||||
const xpGain = Math.floor(duration / 5); // 1 XP per 5 minutes
|
||||
const student = await db.student.findUnique({ where: { id: existing.studentId }, select: { userId: true } });
|
||||
if (student?.userId) {
|
||||
const char = await db.virtualCharacter.findUnique({ where: { userId: student.userId } });
|
||||
if (char) {
|
||||
const newXp = char.xp + xpGain;
|
||||
await db.virtualCharacter.update({
|
||||
where: { userId: student.userId },
|
||||
data: { xp: newXp, level: Math.floor(newXp / 100) + 1 },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json(updated);
|
||||
} catch (error) {
|
||||
console.error('StudySession PUT error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ const updateUserSchema = z.object({
|
||||
email: z.string().email().optional(),
|
||||
role: z.enum(['TEACHER', 'SCHOOL_ADMIN', 'SUPER_ADMIN', 'VICE_PRINCIPAL']).optional(),
|
||||
locale: z.string().optional(),
|
||||
schoolId: z.string().optional(),
|
||||
});
|
||||
|
||||
export async function GET(
|
||||
|
||||
Reference in new issue
Block a user