feat: Round 21 - Major feature additions
- Schlaukopf.de clone: Subject Content Management System (7 categories, 416 topics) - AI Integration: Pollination AI, OpenAI, Anthropic (chat, image/video generation) - Virtual Character System: 5 animated characters (Owl, Dragon, Robot, Cat, Wizard) - Illness Reporting with Parent Approval: privacy-first, student reports need parent approval - Student-Teacher Communication Rooms: real-time chat, voice, file sharing - Counseling/Guidance System: 4 types, calendar integration - Disciplinary Committee System: case management, severity levels - AI Test Generation: auto-generate practice tests before exams - Teacher Grading Review: AI-powered grading audit - VICE_PRINCIPAL role with same privileges as SCHOOL_ADMIN - Class responsible teacher (Klassenlehrer) field - 15 new Prisma models (80+ total) - 30+ new API route files - 8 new frontend components - 400+ i18n keys - Professional styling with gradient headers, animated counters, skeleton loading
This commit is contained in:
1 parent
04ae7aa80c
commit
f1776932d7
48 files changed
+13559
-4
No files matched your search
@@ -0,0 +1,87 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { withRateLimit } from '@/lib/rate-limit';
|
||||
|
||||
// ── GET: Get single AI settings record ──
|
||||
async function getAISetting(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const settings = await db.aISettings.findUnique({ where: { id } });
|
||||
|
||||
if (!settings) {
|
||||
return NextResponse.json({ error: 'AI settings not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json(settings);
|
||||
} catch (error) {
|
||||
console.error('AISettings GET [id] error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// ── PUT: Update AI settings by ID ──
|
||||
async function updateAISetting(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
if (
|
||||
session.user.role !== 'SCHOOL_ADMIN' &&
|
||||
session.user.role !== 'VICE_PRINCIPAL' &&
|
||||
session.user.role !== 'SUPER_ADMIN'
|
||||
) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
const { createdAt, updatedAt, ...updateData } = body;
|
||||
|
||||
const settings = await db.aISettings.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
});
|
||||
|
||||
return NextResponse.json(settings);
|
||||
} catch (error) {
|
||||
console.error('AISettings PUT [id] error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// ── DELETE: Delete AI settings ──
|
||||
async function deleteAISetting(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
if (
|
||||
session.user.role !== 'SCHOOL_ADMIN' &&
|
||||
session.user.role !== 'SUPER_ADMIN'
|
||||
) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
await db.aISettings.delete({ where: { id } });
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('AISettings DELETE [id] error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export const GET = withRateLimit(getAISetting, 'dataRead');
|
||||
export const PUT = withRateLimit(updateAISetting, 'dataWrite');
|
||||
export const DELETE = withRateLimit(deleteAISetting, 'dataWrite');
|
||||
@@ -0,0 +1,85 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { withRateLimit } from '@/lib/rate-limit';
|
||||
|
||||
// ── GET: Get AI settings for the school ──
|
||||
async function getAISettings(request: Request) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const schoolId = searchParams.get('schoolId') || session.user.schoolId;
|
||||
|
||||
if (!schoolId) {
|
||||
return NextResponse.json({ error: 'School ID required' }, { status: 400 });
|
||||
}
|
||||
|
||||
let settings = await db.aISettings.findUnique({
|
||||
where: { schoolId },
|
||||
});
|
||||
|
||||
// Create default settings if none exist
|
||||
if (!settings) {
|
||||
settings = await db.aISettings.create({
|
||||
data: { schoolId },
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json(settings);
|
||||
} catch (error) {
|
||||
console.error('AISettings GET error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// ── PUT: Update AI settings ──
|
||||
async function updateAISettings(request: Request) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
if (
|
||||
session.user.role !== 'SCHOOL_ADMIN' &&
|
||||
session.user.role !== 'VICE_PRINCIPAL' &&
|
||||
session.user.role !== 'SUPER_ADMIN'
|
||||
) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const schoolId = body.schoolId || session.user.schoolId;
|
||||
if (!schoolId) {
|
||||
return NextResponse.json({ error: 'School ID required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Remove fields that shouldn't be directly set
|
||||
const { id, schoolId: _sid, createdAt, updatedAt, ...updateData } = body;
|
||||
|
||||
let settings = await db.aISettings.findUnique({ where: { schoolId } });
|
||||
|
||||
if (!settings) {
|
||||
settings = await db.aISettings.create({
|
||||
data: { schoolId, ...updateData },
|
||||
});
|
||||
} else {
|
||||
settings = await db.aISettings.update({
|
||||
where: { schoolId },
|
||||
data: updateData,
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json(settings);
|
||||
} catch (error) {
|
||||
console.error('AISettings PUT error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export const GET = withRateLimit(getAISettings, 'dataRead');
|
||||
export const PUT = withRateLimit(updateAISettings, 'dataWrite');
|
||||
@@ -0,0 +1,80 @@
|
||||
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 test = await db.aITestGeneration.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
classGroup: { select: { id: true, name: true } },
|
||||
subject: { select: { id: true, name: true } },
|
||||
assessment: { select: { id: true, title: true } },
|
||||
},
|
||||
});
|
||||
|
||||
if (!test) {
|
||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json(test);
|
||||
} catch (error) {
|
||||
console.error('AITests 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 { id } = await params;
|
||||
const body = await request.json();
|
||||
const { answers, isCompleted, score } = body;
|
||||
|
||||
const existing = await db.aITestGeneration.findUnique({ where: { id } });
|
||||
if (!existing) {
|
||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const updateData: Record<string, unknown> = {};
|
||||
if (isCompleted !== undefined) {
|
||||
updateData.isCompleted = isCompleted;
|
||||
updateData.completedAt = isCompleted ? new Date() : null;
|
||||
}
|
||||
if (score !== undefined) {
|
||||
updateData.score = score;
|
||||
}
|
||||
|
||||
const test = await db.aITestGeneration.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
include: {
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
classGroup: { select: { id: true, name: true } },
|
||||
subject: { select: { id: true, name: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(test);
|
||||
} catch (error) {
|
||||
console.error('AITests PUT [id] error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
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 !== 'SCHOOL_ADMIN' && role !== 'SUPER_ADMIN' && role !== 'VICE_PRINCIPAL' && role !== 'TEACHER') {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { schoolId } = body;
|
||||
|
||||
if (!schoolId) {
|
||||
return NextResponse.json({ error: 'schoolId is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Find assessments with dates in approximately 2 weeks
|
||||
const twoWeeksFromNow = new Date();
|
||||
twoWeeksFromNow.setDate(twoWeeksFromNow.getDate() + 14);
|
||||
const twoWeeksRange = new Date();
|
||||
twoWeeksRange.setDate(twoWeeksRange.getDate() + 16);
|
||||
|
||||
const upcomingAssessments = await db.assessment.findMany({
|
||||
where: {
|
||||
schoolId,
|
||||
date: {
|
||||
gte: twoWeeksFromNow,
|
||||
lte: twoWeeksRange,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
classGroup: { select: { id: true, name: true } },
|
||||
subject: { select: { id: true, name: true } },
|
||||
},
|
||||
});
|
||||
|
||||
if (upcomingAssessments.length === 0) {
|
||||
return NextResponse.json({ message: 'No upcoming assessments found', generated: 0 });
|
||||
}
|
||||
|
||||
const generatedTests = [];
|
||||
|
||||
for (const assessment of upcomingAssessments) {
|
||||
// Check if a test already exists for this assessment
|
||||
const existingTest = await db.aITestGeneration.findFirst({
|
||||
where: { assessmentId: assessment.id },
|
||||
});
|
||||
if (existingTest) continue;
|
||||
|
||||
const topic = assessment.title || assessment.subject?.name || 'General';
|
||||
const gradeLevel = assessment.classGroup?.name?.replace(/[^0-9]/g, '') || '8';
|
||||
|
||||
// Generate questions using AI
|
||||
const systemPrompt = `Du bist ein Lehrer. Erstelle eine Übungsklausur zum Thema ${topic} für die Klasse ${gradeLevel}. Erstelle 10 Fragen mit Schwierigkeitsgrad medium. Gib die Fragen im JSON-Format zurück: [{question, options, correctAnswer, explanation}]`;
|
||||
|
||||
let questions: string;
|
||||
try {
|
||||
const ai = await ZAI.create();
|
||||
const response = await ai.chat.completions.create({
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: `Erstelle 10 Fragen zum Thema "${topic}" im Schwierigkeitsgrad "medium" als JSON-Array.` },
|
||||
],
|
||||
});
|
||||
|
||||
const content = response?.choices?.[0]?.message?.content || '';
|
||||
const jsonMatch = content.match(/\[[\s\S]*\]/);
|
||||
if (jsonMatch) {
|
||||
questions = jsonMatch[0];
|
||||
JSON.parse(questions);
|
||||
} else {
|
||||
questions = JSON.stringify([]);
|
||||
}
|
||||
} catch (aiError) {
|
||||
console.error('AI generation error for assessment', assessment.id, aiError);
|
||||
questions = JSON.stringify(
|
||||
Array.from({ length: 10 }, (_, i) => ({
|
||||
question: `${topic} - Frage ${i + 1}`,
|
||||
options: ['Option A', 'Option B', 'Option C', 'Option D'],
|
||||
correctAnswer: 'Option A',
|
||||
explanation: `Erklaerung zu Frage ${i + 1}`,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
const test = await db.aITestGeneration.create({
|
||||
data: {
|
||||
schoolId,
|
||||
assessmentId: assessment.id,
|
||||
classGroupId: assessment.classGroupId,
|
||||
subjectId: assessment.subjectId,
|
||||
testType: 'practice',
|
||||
difficulty: 'medium',
|
||||
questionCount: 10,
|
||||
questions,
|
||||
aiProvider: 'pollination',
|
||||
generatedAt: new Date(),
|
||||
isCompleted: false,
|
||||
},
|
||||
});
|
||||
|
||||
generatedTests.push(test);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
message: `Generated ${generatedTests.length} practice tests`,
|
||||
generated: generatedTests.length,
|
||||
tests: generatedTests,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('AITests auto-generate error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
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 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;
|
||||
const status = searchParams.get('status');
|
||||
|
||||
if (!schoolId) {
|
||||
return NextResponse.json([]);
|
||||
}
|
||||
|
||||
const role = session.user?.role;
|
||||
const userId = session.userId;
|
||||
|
||||
const where: Record<string, unknown> = { schoolId };
|
||||
if (status) where.status = status;
|
||||
|
||||
if (role === 'STUDENT') {
|
||||
where.studentId = userId;
|
||||
} else if (role === 'TEACHER') {
|
||||
// Teachers see tests for their classes
|
||||
const teacherClasses = await db.classGroupTeacher.findMany({
|
||||
where: { userId },
|
||||
select: { classGroupId: true },
|
||||
});
|
||||
const classIds = teacherClasses.map((tc) => tc.classGroupId);
|
||||
if (classIds.length > 0) {
|
||||
where.OR = [
|
||||
{ classGroupId: { in: classIds } },
|
||||
{ studentId: null },
|
||||
];
|
||||
}
|
||||
}
|
||||
// ADMIN, VICE_PRINCIPAL, SUPER_ADMIN see all
|
||||
|
||||
const tests = await db.aITestGeneration.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
classGroup: { select: { id: true, name: true } },
|
||||
subject: { select: { id: true, name: true } },
|
||||
assessment: { select: { id: true, title: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(tests);
|
||||
} catch (error) {
|
||||
console.error('AITests 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,
|
||||
topic,
|
||||
testType,
|
||||
difficulty,
|
||||
questionCount,
|
||||
gradeLevel,
|
||||
subjectId,
|
||||
classGroupId,
|
||||
assessmentId,
|
||||
studentId,
|
||||
} = body;
|
||||
|
||||
if (!schoolId || !topic) {
|
||||
return NextResponse.json({ error: 'schoolId and topic are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const count = questionCount || 10;
|
||||
const diff = difficulty || 'medium';
|
||||
const type = testType || 'practice';
|
||||
const grade = gradeLevel || '8';
|
||||
|
||||
// Generate questions using AI
|
||||
const systemPrompt = `Du bist ein Lehrer. Erstelle eine Übungsklausur zum Thema ${topic} für die Klasse ${grade}. Erstelle ${count} Fragen mit Schwierigkeitsgrad ${diff}. Gib die Fragen im JSON-Format zurück: [{question, options, correctAnswer, explanation}]`;
|
||||
|
||||
let questions: string | null = null;
|
||||
try {
|
||||
const ai = await ZAI.create();
|
||||
const response = await ai.chat.completions.create({
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: `Erstelle ${count} Fragen zum Thema "${topic}" im Schwierigkeitsgrad "${diff}" als JSON-Array.` },
|
||||
],
|
||||
});
|
||||
|
||||
const content = response?.choices?.[0]?.message?.content || '';
|
||||
// Try to extract JSON from the response
|
||||
const jsonMatch = content.match(/\[[\s\S]*\]/);
|
||||
if (jsonMatch) {
|
||||
questions = jsonMatch[0];
|
||||
// Validate it's valid JSON
|
||||
JSON.parse(questions);
|
||||
} else {
|
||||
questions = JSON.stringify([]);
|
||||
}
|
||||
} catch (aiError) {
|
||||
console.error('AI generation error:', aiError);
|
||||
// Fallback: create placeholder questions
|
||||
questions = JSON.stringify(
|
||||
Array.from({ length: count }, (_, i) => ({
|
||||
question: `${topic} - Frage ${i + 1}`,
|
||||
options: ['Option A', 'Option B', 'Option C', 'Option D'],
|
||||
correctAnswer: 'Option A',
|
||||
explanation: `Erklaerung zu Frage ${i + 1}`,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
const test = await db.aITestGeneration.create({
|
||||
data: {
|
||||
schoolId,
|
||||
assessmentId: assessmentId || null,
|
||||
studentId: studentId || (session.user?.role === 'STUDENT' ? session.userId : null),
|
||||
classGroupId: classGroupId || null,
|
||||
subjectId: subjectId || null,
|
||||
testType: type,
|
||||
difficulty: diff,
|
||||
questionCount: count,
|
||||
questions,
|
||||
aiProvider: 'pollination',
|
||||
generatedAt: new Date(),
|
||||
isCompleted: false,
|
||||
},
|
||||
include: {
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
classGroup: { select: { id: true, name: true } },
|
||||
subject: { select: { id: true, name: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(test, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('AITests POST error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { schoolId } = session.user;
|
||||
if (!schoolId) {
|
||||
return NextResponse.json({ error: 'No school assigned' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Check if virtual character is enabled
|
||||
const aiSettings = await db.aISettings.findUnique({
|
||||
where: { schoolId },
|
||||
select: { virtualCharacterEnabled: true },
|
||||
});
|
||||
|
||||
const enabled = aiSettings?.virtualCharacterEnabled ?? true;
|
||||
|
||||
// Get or create virtual character
|
||||
let character = await db.virtualCharacter.findUnique({
|
||||
where: { userId: session.userId },
|
||||
});
|
||||
|
||||
if (!character) {
|
||||
// Create default character
|
||||
character = await db.virtualCharacter.create({
|
||||
data: {
|
||||
userId: session.userId,
|
||||
schoolId,
|
||||
characterId: 'owl',
|
||||
name: 'Eule',
|
||||
color: '#10b981',
|
||||
level: 1,
|
||||
xp: 0,
|
||||
mood: 'happy',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({ character, enabled });
|
||||
} catch (error) {
|
||||
console.error('Virtual Character GET error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(request: Request) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { schoolId } = session.user;
|
||||
if (!schoolId) {
|
||||
return NextResponse.json({ error: 'No school assigned' }, { status: 403 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { characterId, name, color, mood } = body;
|
||||
|
||||
// Get existing character
|
||||
const existing = await db.virtualCharacter.findUnique({
|
||||
where: { userId: session.userId },
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
// Create new character
|
||||
const character = await db.virtualCharacter.create({
|
||||
data: {
|
||||
userId: session.userId,
|
||||
schoolId,
|
||||
characterId: characterId || 'owl',
|
||||
name: name || 'Eule',
|
||||
color: color || '#10b981',
|
||||
level: 1,
|
||||
xp: 0,
|
||||
mood: mood || 'happy',
|
||||
},
|
||||
});
|
||||
return NextResponse.json(character);
|
||||
}
|
||||
|
||||
// Update existing character
|
||||
const character = await db.virtualCharacter.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
...(characterId ? { characterId } : {}),
|
||||
...(name ? { name } : {}),
|
||||
...(color ? { color } : {}),
|
||||
...(mood ? { mood } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(character);
|
||||
} catch (error) {
|
||||
console.error('Virtual Character PUT error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
|
||||
const DEFAULT_SYSTEM_PROMPT =
|
||||
'Du bist ein hilfreicher Lernassistent fuer Schueler. Du erklaerst Konzepte und hilfst beim Verstehen, aber du loest keine Hausaufgaben. Du motivierst und ermutigst die Schueler. Antworte auf Deutsch, es sei denn der Schueler fragt auf Englisch.';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { schoolId } = session.user;
|
||||
if (!schoolId) {
|
||||
return NextResponse.json({ error: 'No school assigned' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Check AI settings
|
||||
const aiSettings = await db.aISettings.findUnique({
|
||||
where: { schoolId },
|
||||
});
|
||||
|
||||
if (!aiSettings || !aiSettings.aiChatEnabled) {
|
||||
return NextResponse.json(
|
||||
{ error: 'AI chat is disabled for your school' },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check daily rate limit
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
const todayRequestCount = await db.chatMessage.count({
|
||||
where: {
|
||||
userId: session.userId,
|
||||
schoolId,
|
||||
senderType: 'user',
|
||||
roomId: null, // AI chat messages have null roomId
|
||||
createdAt: { gte: today },
|
||||
},
|
||||
});
|
||||
|
||||
if (todayRequestCount >= aiSettings.aiMaxRequestsPerDay) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Daily request limit reached',
|
||||
retryAfter: 86400,
|
||||
limit: aiSettings.aiMaxRequestsPerDay,
|
||||
},
|
||||
{ status: 429 }
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { message, conversationHistory = [] } = body;
|
||||
|
||||
if (!message || typeof message !== 'string' || message.trim().length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Message is required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Save user message
|
||||
await db.chatMessage.create({
|
||||
data: {
|
||||
schoolId,
|
||||
userId: session.userId,
|
||||
roomId: null,
|
||||
content: message,
|
||||
senderType: 'user',
|
||||
isRead: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Build system prompt
|
||||
const systemPrompt = aiSettings.aiSystemPrompt || DEFAULT_SYSTEM_PROMPT;
|
||||
|
||||
// Add helper mode restrictions
|
||||
let modeInstructions = '';
|
||||
if (aiSettings.aiHelperMode === 'guided') {
|
||||
modeInstructions =
|
||||
' Du gibst gefuehrte Hinweise und Erklaerungen, ohne die komplette Loesung zu verraten. Du hilfst dem Schueler, selbst auf die Loesung zu kommen.';
|
||||
} else if (aiSettings.aiHelperMode === 'restricted') {
|
||||
modeInstructions =
|
||||
' Du darfst nur kurze Hinweise geben und keine ausfuehrlichen Erklaerungen. Verweise auf Lehrbuecher oder die Lehrkraft.';
|
||||
}
|
||||
// 'full' mode: no extra restrictions
|
||||
|
||||
const fullSystemPrompt = systemPrompt + modeInstructions;
|
||||
|
||||
// Build messages for AI
|
||||
const aiMessages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }> = [
|
||||
{ role: 'system', content: fullSystemPrompt },
|
||||
];
|
||||
|
||||
// Add conversation history (last 10 messages for context)
|
||||
const recentHistory = conversationHistory.slice(-10);
|
||||
for (const msg of recentHistory) {
|
||||
if (msg.role === 'user' || msg.role === 'assistant') {
|
||||
aiMessages.push({ role: msg.role, content: msg.content });
|
||||
}
|
||||
}
|
||||
|
||||
aiMessages.push({ role: 'user', content: message });
|
||||
|
||||
// Call AI
|
||||
const zai = await ZAI.create();
|
||||
const completion = await zai.chat.completions.create({
|
||||
messages: aiMessages,
|
||||
});
|
||||
|
||||
const aiResponse =
|
||||
completion.choices?.[0]?.message?.content ||
|
||||
'Es tut mir leid, ich konnte keine Antwort generieren. Bitte versuche es erneut.';
|
||||
|
||||
// Save AI response
|
||||
await db.chatMessage.create({
|
||||
data: {
|
||||
schoolId,
|
||||
userId: session.userId,
|
||||
roomId: null,
|
||||
content: aiResponse,
|
||||
senderType: 'ai',
|
||||
isRead: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Award XP to virtual character if exists
|
||||
const virtualChar = await db.virtualCharacter.findUnique({
|
||||
where: { userId: session.userId },
|
||||
});
|
||||
if (virtualChar) {
|
||||
const xpGain = 5;
|
||||
const newXp = virtualChar.xp + xpGain;
|
||||
const xpPerLevel = 100;
|
||||
const newLevel = Math.floor(newXp / xpPerLevel) + 1;
|
||||
await db.virtualCharacter.update({
|
||||
where: { id: virtualChar.id },
|
||||
data: {
|
||||
xp: newXp,
|
||||
level: newLevel,
|
||||
mood: newLevel > virtualChar.level ? 'celebrating' : 'happy',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
response: aiResponse,
|
||||
requestsToday: todayRequestCount + 1,
|
||||
maxRequests: aiSettings.aiMaxRequestsPerDay,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('AI Chat error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { schoolId } = session.user;
|
||||
if (!schoolId) {
|
||||
return NextResponse.json({ error: 'No school assigned' }, { status: 403 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const limit = parseInt(searchParams.get('limit') || '50', 10);
|
||||
|
||||
// Get chat history for this user (AI chat only, roomId is null)
|
||||
const messages = await db.chatMessage.findMany({
|
||||
where: {
|
||||
userId: session.userId,
|
||||
schoolId,
|
||||
roomId: null,
|
||||
},
|
||||
orderBy: { createdAt: 'asc' },
|
||||
take: limit,
|
||||
});
|
||||
|
||||
// Get today's request count
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const todayRequestCount = await db.chatMessage.count({
|
||||
where: {
|
||||
userId: session.userId,
|
||||
schoolId,
|
||||
senderType: 'user',
|
||||
roomId: null,
|
||||
createdAt: { gte: today },
|
||||
},
|
||||
});
|
||||
|
||||
// Get AI settings for rate limit info
|
||||
const aiSettings = await db.aISettings.findUnique({
|
||||
where: { schoolId },
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
messages,
|
||||
requestsToday: todayRequestCount,
|
||||
maxRequests: aiSettings?.aiMaxRequestsPerDay ?? 50,
|
||||
chatEnabled: aiSettings?.aiChatEnabled ?? true,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('AI Chat GET error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
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?.user) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Only admin/teacher can generate topics
|
||||
const role = session.user.role;
|
||||
if (role !== 'SCHOOL_ADMIN' && role !== 'SUPER_ADMIN' && role !== 'TEACHER') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Only teachers and admins can generate topics' },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
const { schoolId } = session.user;
|
||||
if (!schoolId) {
|
||||
return NextResponse.json({ error: 'No school assigned' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Check AI settings
|
||||
const aiSettings = await db.aISettings.findUnique({
|
||||
where: { schoolId },
|
||||
});
|
||||
|
||||
if (!aiSettings || !aiSettings.aiTopicGenEnabled) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Topic generation is disabled for your school' },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { categoryName, subjectName, gradeLevel, additionalContext } = body;
|
||||
|
||||
if (!categoryName || !subjectName) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Category name and subject name are required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Build prompt for topic generation
|
||||
const prompt = `Generiere eine Liste von Lernthemen fuer die Kategorie "${categoryName}" im Fach ${subjectName}${gradeLevel ? ` fuer die Jahrgangsstufe ${gradeLevel}` : ''}.${additionalContext ? ` Zusaetzlicher Kontext: ${additionalContext}` : ''}
|
||||
|
||||
Bitte gib die Themen als JSON-Array zurueck, wobei jedes Thema folgende Felder hat:
|
||||
- "name": Der Name des Themas (kurz und praegnant)
|
||||
- "description": Eine kurze Beschreibung des Themas (1-2 Saetze)
|
||||
- "difficulty": Die Schwierigkeit (1-5, wobei 1 am einfachsten ist)
|
||||
- "keywords": Ein Array von 3-5 Schluesselwoertern
|
||||
|
||||
Generiere 5-10 passende Themen. Antworte NUR mit dem JSON-Array, ohne Markdown-Formatierung.`;
|
||||
|
||||
const zai = await ZAI.create();
|
||||
const completion = await zai.chat.completions.create({
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content:
|
||||
'Du bist ein Lehrplan-Experte. Du generierst strukturierte Lernthemen fuer den Schulunterricht. Du antwortest ausschliesslich mit validem JSON.',
|
||||
},
|
||||
{ role: 'user', content: prompt },
|
||||
],
|
||||
});
|
||||
|
||||
const aiResponse = completion.choices?.[0]?.message?.content || '';
|
||||
|
||||
// Parse the JSON response
|
||||
let topics;
|
||||
try {
|
||||
// Try to extract JSON from the response (might be wrapped in code blocks)
|
||||
const jsonMatch = aiResponse.match(/\[[\s\S]*\]/);
|
||||
if (jsonMatch) {
|
||||
topics = JSON.parse(jsonMatch[0]);
|
||||
} else {
|
||||
topics = JSON.parse(aiResponse);
|
||||
}
|
||||
} catch {
|
||||
// If parsing fails, return the raw response
|
||||
return NextResponse.json({
|
||||
topics: [],
|
||||
rawResponse: aiResponse,
|
||||
error: 'Failed to parse AI response as JSON',
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
topics,
|
||||
categoryName,
|
||||
subjectName,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('AI Topic Generation error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
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?.user) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Only admin/teacher can generate images
|
||||
const role = session.user.role;
|
||||
if (role !== 'SCHOOL_ADMIN' && role !== 'SUPER_ADMIN' && role !== 'TEACHER') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Only teachers and admins can generate images' },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
const { schoolId } = session.user;
|
||||
if (!schoolId) {
|
||||
return NextResponse.json({ error: 'No school assigned' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Check AI settings
|
||||
const aiSettings = await db.aISettings.findUnique({
|
||||
where: { schoolId },
|
||||
});
|
||||
|
||||
if (!aiSettings || !aiSettings.aiImageGenEnabled) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Image generation is disabled for your school' },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { prompt, size = '1024x1024' } = body;
|
||||
|
||||
if (!prompt || typeof prompt !== 'string' || prompt.trim().length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Prompt is required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Generate image using z-ai-web-dev-sdk
|
||||
const zai = await ZAI.create();
|
||||
const response = await zai.images.generations.create({
|
||||
prompt,
|
||||
size,
|
||||
});
|
||||
|
||||
const imageData = response.data?.[0];
|
||||
|
||||
if (!imageData) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to generate image' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
image: {
|
||||
url: imageData.url || null,
|
||||
b64_json: imageData.b64_json || null,
|
||||
revised_prompt: imageData.revised_prompt || prompt,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('AI Image Generation error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
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?.user) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { schoolId } = session.user;
|
||||
if (!schoolId) {
|
||||
return NextResponse.json({ error: 'No school assigned' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Only admin/teacher can view AI settings
|
||||
const role = session.user.role;
|
||||
if (role !== 'SCHOOL_ADMIN' && role !== 'SUPER_ADMIN' && role !== 'TEACHER') {
|
||||
// Students/parents can only see limited info
|
||||
const aiSettings = await db.aISettings.findUnique({
|
||||
where: { schoolId },
|
||||
select: {
|
||||
aiChatEnabled: true,
|
||||
aiMaxRequestsPerDay: true,
|
||||
aiHelperMode: true,
|
||||
virtualCharacterEnabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(aiSettings || { aiChatEnabled: true, aiMaxRequestsPerDay: 50, aiHelperMode: 'guided', virtualCharacterEnabled: true });
|
||||
}
|
||||
|
||||
const aiSettings = await db.aISettings.findUnique({
|
||||
where: { schoolId },
|
||||
});
|
||||
|
||||
if (!aiSettings) {
|
||||
// Return default settings
|
||||
return NextResponse.json({
|
||||
schoolId,
|
||||
pollinationEnabled: true,
|
||||
pollinationApiKey: null,
|
||||
pollinationModel: 'flux',
|
||||
openaiEnabled: false,
|
||||
openaiApiKey: null,
|
||||
openaiModel: 'gpt-4o',
|
||||
anthropicEnabled: false,
|
||||
anthropicApiKey: null,
|
||||
anthropicModel: 'claude-3-5-sonnet-20241022',
|
||||
aiChatEnabled: true,
|
||||
aiImageGenEnabled: true,
|
||||
aiVideoGenEnabled: false,
|
||||
aiAutoTestEnabled: true,
|
||||
aiGradingAuditEnabled: false,
|
||||
aiTopicGenEnabled: true,
|
||||
virtualCharacterEnabled: true,
|
||||
aiMaxRequestsPerDay: 50,
|
||||
aiHelperMode: 'guided',
|
||||
aiSystemPrompt: null,
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json(aiSettings);
|
||||
} catch (error) {
|
||||
console.error('AI Settings GET error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(request: Request) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Only admin can update AI settings
|
||||
const role = session.user.role;
|
||||
if (role !== 'SCHOOL_ADMIN' && role !== 'SUPER_ADMIN') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Only admins can update AI settings' },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
const { schoolId } = session.user;
|
||||
if (!schoolId) {
|
||||
return NextResponse.json({ error: 'No school assigned' }, { status: 403 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
// Update or create AI settings
|
||||
const aiSettings = await db.aISettings.upsert({
|
||||
where: { schoolId },
|
||||
update: {
|
||||
pollinationEnabled: body.pollinationEnabled ?? undefined,
|
||||
pollinationApiKey: body.pollinationApiKey ?? undefined,
|
||||
pollinationModel: body.pollinationModel ?? undefined,
|
||||
openaiEnabled: body.openaiEnabled ?? undefined,
|
||||
openaiApiKey: body.openaiApiKey ?? undefined,
|
||||
openaiModel: body.openaiModel ?? undefined,
|
||||
anthropicEnabled: body.anthropicEnabled ?? undefined,
|
||||
anthropicApiKey: body.anthropicApiKey ?? undefined,
|
||||
anthropicModel: body.anthropicModel ?? undefined,
|
||||
aiChatEnabled: body.aiChatEnabled ?? undefined,
|
||||
aiImageGenEnabled: body.aiImageGenEnabled ?? undefined,
|
||||
aiVideoGenEnabled: body.aiVideoGenEnabled ?? undefined,
|
||||
aiAutoTestEnabled: body.aiAutoTestEnabled ?? undefined,
|
||||
aiGradingAuditEnabled: body.aiGradingAuditEnabled ?? undefined,
|
||||
aiTopicGenEnabled: body.aiTopicGenEnabled ?? undefined,
|
||||
virtualCharacterEnabled: body.virtualCharacterEnabled ?? undefined,
|
||||
aiMaxRequestsPerDay: body.aiMaxRequestsPerDay ?? undefined,
|
||||
aiHelperMode: body.aiHelperMode ?? undefined,
|
||||
aiSystemPrompt: body.aiSystemPrompt ?? undefined,
|
||||
},
|
||||
create: {
|
||||
schoolId,
|
||||
pollinationEnabled: body.pollinationEnabled ?? true,
|
||||
pollinationApiKey: body.pollinationApiKey ?? null,
|
||||
pollinationModel: body.pollinationModel ?? 'flux',
|
||||
openaiEnabled: body.openaiEnabled ?? false,
|
||||
openaiApiKey: body.openaiApiKey ?? null,
|
||||
openaiModel: body.openaiModel ?? 'gpt-4o',
|
||||
anthropicEnabled: body.anthropicEnabled ?? false,
|
||||
anthropicApiKey: body.anthropicApiKey ?? null,
|
||||
anthropicModel: body.anthropicModel ?? 'claude-3-5-sonnet-20241022',
|
||||
aiChatEnabled: body.aiChatEnabled ?? true,
|
||||
aiImageGenEnabled: body.aiImageGenEnabled ?? true,
|
||||
aiVideoGenEnabled: body.aiVideoGenEnabled ?? false,
|
||||
aiAutoTestEnabled: body.aiAutoTestEnabled ?? true,
|
||||
aiGradingAuditEnabled: body.aiGradingAuditEnabled ?? false,
|
||||
aiTopicGenEnabled: body.aiTopicGenEnabled ?? true,
|
||||
virtualCharacterEnabled: body.virtualCharacterEnabled ?? true,
|
||||
aiMaxRequestsPerDay: body.aiMaxRequestsPerDay ?? 50,
|
||||
aiHelperMode: body.aiHelperMode ?? 'guided',
|
||||
aiSystemPrompt: body.aiSystemPrompt ?? null,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(aiSettings);
|
||||
} catch (error) {
|
||||
console.error('AI Settings PUT error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
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?.user) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Only admin/teacher can generate videos
|
||||
const role = session.user.role;
|
||||
if (role !== 'SCHOOL_ADMIN' && role !== 'SUPER_ADMIN' && role !== 'TEACHER') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Only teachers and admins can generate videos' },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
const { schoolId } = session.user;
|
||||
if (!schoolId) {
|
||||
return NextResponse.json({ error: 'No school assigned' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Check AI settings
|
||||
const aiSettings = await db.aISettings.findUnique({
|
||||
where: { schoolId },
|
||||
});
|
||||
|
||||
if (!aiSettings || !aiSettings.aiVideoGenEnabled) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Video generation is disabled for your school' },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { prompt, quality = 'standard', size = '1280x720' } = body;
|
||||
|
||||
if (!prompt || typeof prompt !== 'string' || prompt.trim().length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Prompt is required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Generate video using z-ai-web-dev-sdk
|
||||
const zai = await ZAI.create();
|
||||
const task = await zai.video.generations.create({
|
||||
prompt,
|
||||
quality,
|
||||
size,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
taskId: task.id || task.taskId || 'pending',
|
||||
status: task.status || 'processing',
|
||||
message: 'Video generation started. Check status for updates.',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('AI Video Generation error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ const registerSchema = z.object({
|
||||
firstName: z.string().min(1),
|
||||
lastName: z.string().min(1),
|
||||
schoolId: z.string().optional(),
|
||||
role: z.enum(['TEACHER', 'SCHOOL_ADMIN', 'SUPER_ADMIN']).default('TEACHER'),
|
||||
role: z.enum(['TEACHER', 'SCHOOL_ADMIN', 'SUPER_ADMIN', 'VICE_PRINCIPAL']).default('TEACHER'),
|
||||
});
|
||||
|
||||
export const POST = withRateLimit(async function POST(request: Request) {
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
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 room = await db.communicationRoom.findUnique({ where: { id } });
|
||||
|
||||
if (!room) {
|
||||
return NextResponse.json({ error: 'Room not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const role = session.user?.role;
|
||||
const userId = session.userId;
|
||||
|
||||
// Access control
|
||||
if (role === 'STUDENT' && room.studentId !== userId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
} else if (role === 'TEACHER' && room.teacherId !== userId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
} else if (role === 'PARENT') {
|
||||
const parentLink = await db.parentStudentLink.findFirst({
|
||||
where: { parentId: userId, studentId: room.studentId },
|
||||
});
|
||||
if (!parentLink) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
} else if (role === 'SCHOOL_ADMIN' || role === 'VICE_PRINCIPAL') {
|
||||
if (room.schoolId !== session.user?.schoolId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const page = parseInt(searchParams.get('page') || '1');
|
||||
const limit = parseInt(searchParams.get('limit') || '50');
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const [messages, total] = await Promise.all([
|
||||
db.communicationMessage.findMany({
|
||||
where: { roomId: id },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
skip,
|
||||
take: limit,
|
||||
include: {
|
||||
sender: { select: { id: true, firstName: true, lastName: true, role: true } },
|
||||
},
|
||||
}),
|
||||
db.communicationMessage.count({ where: { roomId: id } }),
|
||||
]);
|
||||
|
||||
// Mark messages as read for the current user
|
||||
await db.communicationMessage.updateMany({
|
||||
where: {
|
||||
roomId: id,
|
||||
senderId: { not: userId },
|
||||
isRead: false,
|
||||
},
|
||||
data: { isRead: true },
|
||||
});
|
||||
|
||||
return NextResponse.json({ messages, total, page, limit });
|
||||
} catch (error) {
|
||||
console.error('CommunicationMessage GET error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
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 room = await db.communicationRoom.findUnique({ where: { id } });
|
||||
|
||||
if (!room) {
|
||||
return NextResponse.json({ error: 'Room not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const role = session.user?.role;
|
||||
const userId = session.userId;
|
||||
|
||||
// Only student and teacher can send messages (not admin, not parent)
|
||||
if (role !== 'STUDENT' && role !== 'TEACHER') {
|
||||
return NextResponse.json({ error: 'Only participants can send messages' }, { status: 403 });
|
||||
}
|
||||
|
||||
if (room.studentId !== userId && room.teacherId !== userId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
if (room.status !== 'active') {
|
||||
return NextResponse.json({ error: 'Room is not active' }, { status: 400 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { content, messageType, fileUrl, metadata } = body;
|
||||
|
||||
if (!content && !fileUrl) {
|
||||
return NextResponse.json({ error: 'Content or fileUrl is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const message = await db.communicationMessage.create({
|
||||
data: {
|
||||
roomId: id,
|
||||
senderId: userId,
|
||||
content: content || '',
|
||||
messageType: messageType || 'text',
|
||||
fileUrl: fileUrl || null,
|
||||
metadata: metadata || null,
|
||||
},
|
||||
include: {
|
||||
sender: { select: { id: true, firstName: true, lastName: true, role: true } },
|
||||
},
|
||||
});
|
||||
|
||||
// Update room's updatedAt
|
||||
await db.communicationRoom.update({
|
||||
where: { id },
|
||||
data: { updatedAt: new Date() },
|
||||
});
|
||||
|
||||
return NextResponse.json(message, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('CommunicationMessage POST error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
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 room = await db.communicationRoom.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
teacher: { select: { id: true, firstName: true, lastName: true } },
|
||||
members: {
|
||||
include: { user: { select: { id: true, firstName: true, lastName: true, role: true } } },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!room) {
|
||||
return NextResponse.json({ error: 'Room not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const role = session.user?.role;
|
||||
const userId = session.userId;
|
||||
|
||||
// Access control
|
||||
if (role === 'STUDENT' && room.studentId !== userId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
} else if (role === 'TEACHER' && room.teacherId !== userId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
} else if (role === 'PARENT') {
|
||||
const parentLink = await db.parentStudentLink.findFirst({
|
||||
where: { parentId: userId, studentId: room.studentId },
|
||||
});
|
||||
if (!parentLink) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
} else if (role === 'SCHOOL_ADMIN' || role === 'VICE_PRINCIPAL') {
|
||||
if (room.schoolId !== session.user?.schoolId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json(room);
|
||||
} catch (error) {
|
||||
console.error('CommunicationRoom 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 { id } = await params;
|
||||
const room = await db.communicationRoom.findUnique({ where: { id } });
|
||||
|
||||
if (!room) {
|
||||
return NextResponse.json({ error: 'Room not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const role = session.user?.role;
|
||||
const userId = session.userId;
|
||||
const body = await request.json();
|
||||
|
||||
// Teacher accepts a room
|
||||
if (body.status === 'active' && room.status === 'requested') {
|
||||
if (role !== 'TEACHER' || room.teacherId !== userId) {
|
||||
return NextResponse.json({ error: 'Only the assigned teacher can accept' }, { status: 403 });
|
||||
}
|
||||
|
||||
const updated = await db.communicationRoom.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: 'active',
|
||||
acceptedAt: new Date(),
|
||||
},
|
||||
include: {
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
teacher: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(updated);
|
||||
}
|
||||
|
||||
// Teacher or admin closes a room
|
||||
if (body.status === 'closed') {
|
||||
if (role !== 'TEACHER' || room.teacherId !== userId) {
|
||||
if (role !== 'SCHOOL_ADMIN' && role !== 'VICE_PRINCIPAL' && role !== 'SUPER_ADMIN') {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
const updated = await db.communicationRoom.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: 'closed',
|
||||
closedAt: new Date(),
|
||||
closeReason: body.closeReason || null,
|
||||
},
|
||||
include: {
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
teacher: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(updated);
|
||||
}
|
||||
|
||||
// Teacher rejects a room request
|
||||
if (body.status === 'rejected' && room.status === 'requested') {
|
||||
if (role !== 'TEACHER' || room.teacherId !== userId) {
|
||||
return NextResponse.json({ error: 'Only the assigned teacher can reject' }, { status: 403 });
|
||||
}
|
||||
|
||||
const updated = await db.communicationRoom.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: 'closed',
|
||||
closedAt: new Date(),
|
||||
closeReason: body.closeReason || 'Request rejected',
|
||||
},
|
||||
include: {
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
teacher: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(updated);
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Invalid status transition' }, { status: 400 });
|
||||
} catch (error) {
|
||||
console.error('CommunicationRoom 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 { id } = await params;
|
||||
const room = await db.communicationRoom.findUnique({ where: { id } });
|
||||
|
||||
if (!room) {
|
||||
return NextResponse.json({ error: 'Room not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const role = session.user?.role;
|
||||
|
||||
// Only admin can delete rooms
|
||||
if (role !== 'SUPER_ADMIN' && role !== 'SCHOOL_ADMIN' && role !== 'VICE_PRINCIPAL') {
|
||||
return NextResponse.json({ error: 'Only admins can delete rooms' }, { status: 403 });
|
||||
}
|
||||
|
||||
await db.communicationMessage.deleteMany({ where: { roomId: id } });
|
||||
await db.communicationRoomMember.deleteMany({ where: { roomId: id } });
|
||||
await db.communicationRoom.delete({ where: { id } });
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('CommunicationRoom DELETE [id] error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
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 status = searchParams.get('status');
|
||||
|
||||
const role = session.user?.role;
|
||||
const userId = session.userId;
|
||||
|
||||
const where: Record<string, unknown> = {};
|
||||
|
||||
if (role === 'STUDENT') {
|
||||
where.studentId = userId;
|
||||
} else if (role === 'TEACHER') {
|
||||
where.teacherId = userId;
|
||||
} else if (role === 'SCHOOL_ADMIN' || role === 'VICE_PRINCIPAL') {
|
||||
where.schoolId = session.user?.schoolId;
|
||||
} else if (role === 'SUPER_ADMIN') {
|
||||
// Can see all
|
||||
} else if (role === 'PARENT') {
|
||||
// Parents can see rooms for their children
|
||||
const parentLinks = await db.parentStudentLink.findMany({
|
||||
where: { parentId: userId },
|
||||
select: { studentId: true },
|
||||
});
|
||||
const childUserIds: string[] = [];
|
||||
for (const link of parentLinks) {
|
||||
const student = await db.student.findUnique({
|
||||
where: { id: link.studentId },
|
||||
select: { userId: true },
|
||||
});
|
||||
if (student?.userId) childUserIds.push(student.userId);
|
||||
}
|
||||
where.studentId = { in: childUserIds };
|
||||
}
|
||||
|
||||
if (status) where.status = status;
|
||||
|
||||
const rooms = await db.communicationRoom.findMany({
|
||||
where,
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
include: {
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
teacher: { select: { id: true, firstName: true, lastName: true } },
|
||||
messages: {
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 1,
|
||||
select: { id: true, content: true, messageType: true, createdAt: true, senderId: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(rooms);
|
||||
} catch (error) {
|
||||
console.error('CommunicationRoom 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;
|
||||
const userId = session.userId;
|
||||
|
||||
// Only students can request conversations
|
||||
if (role !== 'STUDENT') {
|
||||
return NextResponse.json({ error: 'Only students can request conversations' }, { status: 403 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { reason } = body;
|
||||
|
||||
// Find the student's class and the responsible teacher
|
||||
const student = await db.student.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ userId },
|
||||
{
|
||||
userId: null,
|
||||
firstName: session.user?.firstName,
|
||||
lastName: session.user?.lastName,
|
||||
schoolId: session.user?.schoolId,
|
||||
},
|
||||
],
|
||||
},
|
||||
include: {
|
||||
enrollments: {
|
||||
where: { endDate: null },
|
||||
take: 1,
|
||||
include: {
|
||||
classGroup: {
|
||||
include: {
|
||||
responsibleTeacher: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!student) {
|
||||
return NextResponse.json({ error: 'Student profile not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const currentEnrollment = student.enrollments[0];
|
||||
if (!currentEnrollment) {
|
||||
return NextResponse.json({ error: 'No active class enrollment found' }, { status: 400 });
|
||||
}
|
||||
|
||||
const teacher = currentEnrollment.classGroup.responsibleTeacher;
|
||||
if (!teacher) {
|
||||
return NextResponse.json({ error: 'No class teacher assigned' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Check if there's already an active/requested room
|
||||
const existingRoom = await db.communicationRoom.findFirst({
|
||||
where: {
|
||||
studentId: userId,
|
||||
teacherId: teacher.id,
|
||||
status: { in: ['requested', 'active'] },
|
||||
},
|
||||
});
|
||||
|
||||
if (existingRoom) {
|
||||
return NextResponse.json({ error: 'Active conversation already exists with this teacher' }, { status: 400 });
|
||||
}
|
||||
|
||||
const room = await db.communicationRoom.create({
|
||||
data: {
|
||||
schoolId: student.schoolId,
|
||||
studentId: userId,
|
||||
teacherId: teacher.id,
|
||||
roomType: 'chat',
|
||||
status: 'requested',
|
||||
requestedBy: userId,
|
||||
},
|
||||
include: {
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
teacher: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
});
|
||||
|
||||
// Create room members
|
||||
await db.communicationRoomMember.createMany({
|
||||
data: [
|
||||
{ roomId: room.id, userId, role: 'member' },
|
||||
{ roomId: room.id, userId: teacher.id, role: 'moderator' },
|
||||
],
|
||||
});
|
||||
|
||||
// If there's a reason, send it as the first message
|
||||
if (reason) {
|
||||
await db.communicationMessage.create({
|
||||
data: {
|
||||
roomId: room.id,
|
||||
senderId: userId,
|
||||
content: reason,
|
||||
messageType: 'text',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json(room, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('CommunicationRoom POST error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { withRateLimit } from '@/lib/rate-limit';
|
||||
|
||||
// ── PUT: Approve or reject a change request ──
|
||||
const updateChangeRequestSchema = z.object({
|
||||
status: z.enum(['approved', 'rejected']),
|
||||
reviewNote: z.string().max(2000).optional().nullable(),
|
||||
});
|
||||
|
||||
async function updateChangeRequest(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
if (
|
||||
session.user.role !== 'SCHOOL_ADMIN' &&
|
||||
session.user.role !== 'VICE_PRINCIPAL' &&
|
||||
session.user.role !== 'SUPER_ADMIN'
|
||||
) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
const parsed = updateChangeRequestSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Validation failed', details: parsed.error.issues },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const changeRequest = await db.contentChangeRequest.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: parsed.data.status,
|
||||
reviewedBy: session.userId,
|
||||
reviewNote: parsed.data.reviewNote,
|
||||
},
|
||||
include: {
|
||||
content: { select: { id: true, title: true } },
|
||||
requester: { select: { id: true, firstName: true, lastName: true } },
|
||||
reviewer: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
});
|
||||
|
||||
// If approved and it's an edit request, apply the proposed changes to the content
|
||||
if (parsed.data.status === 'approved' && changeRequest.proposedChanges) {
|
||||
try {
|
||||
const proposed = JSON.parse(changeRequest.proposedChanges);
|
||||
if (changeRequest.content?.id && proposed) {
|
||||
await db.subjectContent.update({
|
||||
where: { id: changeRequest.contentId },
|
||||
data: proposed,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// If proposed changes are not valid JSON, skip auto-apply
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json(changeRequest);
|
||||
} catch (error) {
|
||||
console.error('ContentChangeRequest PUT [id] error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export const PUT = withRateLimit(updateChangeRequest, 'dataWrite');
|
||||
@@ -0,0 +1,112 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { withRateLimit } from '@/lib/rate-limit';
|
||||
|
||||
// ── GET: List change requests ──
|
||||
async function getChangeRequests(request: Request) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const schoolId = searchParams.get('schoolId') || session.user.schoolId;
|
||||
const status = searchParams.get('status');
|
||||
|
||||
if (!schoolId) {
|
||||
return NextResponse.json([]);
|
||||
}
|
||||
|
||||
const where: Record<string, unknown> = { schoolId };
|
||||
if (status) where.status = status;
|
||||
|
||||
// SCHOOL_ADMIN, VICE_PRINCIPAL see all; TEACHER sees own
|
||||
if (session.user.role === 'TEACHER') {
|
||||
where.requestedBy = session.userId;
|
||||
}
|
||||
|
||||
const requests = await db.contentChangeRequest.findMany({
|
||||
where,
|
||||
include: {
|
||||
content: { select: { id: true, title: true, contentType: true } },
|
||||
requester: { select: { id: true, firstName: true, lastName: true } },
|
||||
reviewer: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
return NextResponse.json(requests);
|
||||
} catch (error) {
|
||||
console.error('ContentChangeRequests GET error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// ── POST: Create a change request ──
|
||||
const createChangeRequestSchema = z.object({
|
||||
schoolId: z.string().optional(),
|
||||
contentId: z.string().min(1),
|
||||
requestType: z.enum(['edit', 'add', 'delete']),
|
||||
title: z.string().min(1).max(300),
|
||||
description: z.string().min(1).max(5000),
|
||||
proposedChanges: z.string().optional().nullable(),
|
||||
});
|
||||
|
||||
async function createChangeRequest(request: Request) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
if (
|
||||
session.user.role !== 'TEACHER' &&
|
||||
session.user.role !== 'SCHOOL_ADMIN' &&
|
||||
session.user.role !== 'VICE_PRINCIPAL' &&
|
||||
session.user.role !== 'SUPER_ADMIN'
|
||||
) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const parsed = createChangeRequestSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Validation failed', details: parsed.error.issues },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const schoolId = parsed.data.schoolId || session.user.schoolId;
|
||||
if (!schoolId) {
|
||||
return NextResponse.json({ error: 'User must belong to a school' }, { status: 400 });
|
||||
}
|
||||
|
||||
const changeRequest = await db.contentChangeRequest.create({
|
||||
data: {
|
||||
schoolId,
|
||||
contentId: parsed.data.contentId,
|
||||
requestedBy: session.userId,
|
||||
requestType: parsed.data.requestType,
|
||||
title: parsed.data.title,
|
||||
description: parsed.data.description,
|
||||
proposedChanges: parsed.data.proposedChanges,
|
||||
},
|
||||
include: {
|
||||
content: { select: { id: true, title: true } },
|
||||
requester: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(changeRequest, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('ContentChangeRequest POST error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export const GET = withRateLimit(getChangeRequests, 'dataRead');
|
||||
export const POST = withRateLimit(createChangeRequest, 'dataWrite');
|
||||
@@ -0,0 +1,155 @@
|
||||
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 appointment = await db.counselingAppointment.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
counselor: { select: { id: true, firstName: true, lastName: true } },
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
});
|
||||
|
||||
if (!appointment) {
|
||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const role = session.user?.role;
|
||||
const userId = session.userId;
|
||||
if (
|
||||
role !== 'SCHOOL_ADMIN' &&
|
||||
role !== 'SUPER_ADMIN' &&
|
||||
role !== 'VICE_PRINCIPAL' &&
|
||||
appointment.counselorId !== userId &&
|
||||
appointment.studentId !== userId
|
||||
) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
return NextResponse.json(appointment);
|
||||
} catch (error) {
|
||||
console.error('Counseling 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 { id } = await params;
|
||||
const body = await request.json();
|
||||
const { scheduledAt, duration, notes, status, addToCalendar } = body;
|
||||
|
||||
const existing = await db.counselingAppointment.findUnique({ where: { id } });
|
||||
if (!existing) {
|
||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const role = session.user?.role;
|
||||
const userId = session.userId;
|
||||
|
||||
// Only counselor, admin, or the student can update
|
||||
const isCounselor = existing.counselorId === userId;
|
||||
const isStudent = existing.studentId === userId;
|
||||
const isAdmin = role === 'SCHOOL_ADMIN' || role === 'SUPER_ADMIN' || role === 'VICE_PRINCIPAL';
|
||||
|
||||
if (!isCounselor && !isStudent && !isAdmin) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const updateData: Record<string, unknown> = {};
|
||||
if (scheduledAt !== undefined) updateData.scheduledAt = scheduledAt ? new Date(scheduledAt) : null;
|
||||
if (duration !== undefined) updateData.duration = duration;
|
||||
if (notes !== undefined) updateData.notes = notes;
|
||||
if (status !== undefined) updateData.status = status;
|
||||
|
||||
const appointment = await db.counselingAppointment.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
include: {
|
||||
counselor: { select: { id: true, firstName: true, lastName: true } },
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
});
|
||||
|
||||
// Add to calendar if requested and scheduled
|
||||
if (addToCalendar && appointment.scheduledAt && appointment.status === 'scheduled') {
|
||||
try {
|
||||
await db.calendarEvent.create({
|
||||
data: {
|
||||
schoolId: appointment.schoolId,
|
||||
userId: appointment.counselorId,
|
||||
title: `Counseling: ${appointment.student.firstName} ${appointment.student.lastName}`,
|
||||
description: `Type: ${appointment.requestType}${appointment.notes ? `\nNotes: ${appointment.notes}` : ''}`,
|
||||
startTime: appointment.scheduledAt,
|
||||
endTime: new Date(new Date(appointment.scheduledAt).getTime() + (appointment.duration || 30) * 60000),
|
||||
category: 'meeting',
|
||||
isAllDay: false,
|
||||
},
|
||||
});
|
||||
} catch (calErr) {
|
||||
console.error('Calendar event creation error:', calErr);
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json(appointment);
|
||||
} catch (error) {
|
||||
console.error('Counseling 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 { id } = await params;
|
||||
const existing = await db.counselingAppointment.findUnique({ where: { id } });
|
||||
if (!existing) {
|
||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const role = session.user?.role;
|
||||
const userId = session.userId;
|
||||
const isCounselor = existing.counselorId === userId;
|
||||
const isAdmin = role === 'SCHOOL_ADMIN' || role === 'SUPER_ADMIN' || role === 'VICE_PRINCIPAL';
|
||||
|
||||
if (!isCounselor && !isAdmin) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const appointment = await db.counselingAppointment.update({
|
||||
where: { id },
|
||||
data: { status: 'cancelled' },
|
||||
});
|
||||
|
||||
return NextResponse.json(appointment);
|
||||
} catch (error) {
|
||||
console.error('Counseling DELETE [id] error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
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 status = searchParams.get('status');
|
||||
|
||||
const role = session.user?.role;
|
||||
const userId = session.userId;
|
||||
|
||||
let schoolId: string | undefined;
|
||||
if (role === 'SCHOOL_ADMIN' || role === 'VICE_PRINCIPAL') {
|
||||
schoolId = session.user?.schoolId ?? undefined;
|
||||
} else {
|
||||
schoolId = schoolIdParam ?? session.user?.schoolId ?? undefined;
|
||||
}
|
||||
|
||||
if (!schoolId) {
|
||||
return NextResponse.json([]);
|
||||
}
|
||||
|
||||
const where: Record<string, unknown> = { schoolId };
|
||||
if (status) where.status = status;
|
||||
|
||||
// Role-based filtering
|
||||
if (role === 'STUDENT') {
|
||||
where.studentId = userId;
|
||||
} else if (role === 'TEACHER') {
|
||||
// Teachers see their own counseling appointments (as counselor)
|
||||
where.counselorId = userId;
|
||||
}
|
||||
// ADMIN, VICE_PRINCIPAL, SUPER_ADMIN see all
|
||||
|
||||
const appointments = await db.counselingAppointment.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
counselor: { select: { id: true, firstName: true, lastName: true } },
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(appointments);
|
||||
} catch (error) {
|
||||
console.error('Counseling 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, requestType, description, addToCalendar, isPrivate } = body;
|
||||
|
||||
if (!schoolId) {
|
||||
return NextResponse.json({ error: 'schoolId is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Only students can request counseling
|
||||
const role = session.user?.role;
|
||||
if (role !== 'STUDENT' && role !== 'SCHOOL_ADMIN' && role !== 'SUPER_ADMIN') {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Find a counselor teacher for this school
|
||||
const counselor = await db.user.findFirst({
|
||||
where: {
|
||||
schoolId,
|
||||
role: 'TEACHER',
|
||||
counselingAppointments: { some: {} },
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
// If no counselor found via existing appointments, look for any teacher assigned
|
||||
let counselorId = counselor?.id;
|
||||
if (!counselorId) {
|
||||
// Check if there's an AISettings with a counselor assigned
|
||||
const aiSettings = await db.aISettings.findFirst({
|
||||
where: { schoolId },
|
||||
});
|
||||
if (aiSettings) {
|
||||
// Use a generic approach - find any teacher in the school
|
||||
const anyTeacher = await db.user.findFirst({
|
||||
where: { schoolId, role: 'TEACHER' },
|
||||
select: { id: true },
|
||||
});
|
||||
counselorId = anyTeacher?.id;
|
||||
}
|
||||
}
|
||||
|
||||
if (!counselorId) {
|
||||
const anyTeacher = await db.user.findFirst({
|
||||
where: { schoolId, role: 'TEACHER' },
|
||||
select: { id: true },
|
||||
});
|
||||
counselorId = anyTeacher?.id;
|
||||
}
|
||||
|
||||
if (!counselorId) {
|
||||
return NextResponse.json({ error: 'No counselor available' }, { status: 400 });
|
||||
}
|
||||
|
||||
const appointment = await db.counselingAppointment.create({
|
||||
data: {
|
||||
schoolId,
|
||||
studentId: session.userId,
|
||||
counselorId,
|
||||
requestType: requestType || 'guidance',
|
||||
description: description || null,
|
||||
status: 'requested',
|
||||
addToCalendar: addToCalendar ?? true,
|
||||
isPrivate: isPrivate ?? true,
|
||||
},
|
||||
include: {
|
||||
counselor: { select: { id: true, firstName: true, lastName: true } },
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(appointment, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('Counseling POST error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
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 disciplinaryCase = await db.disciplinaryCase.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
reporter: { select: { id: true, firstName: true, lastName: true } },
|
||||
reviewer: { select: { id: true, firstName: true, lastName: true } },
|
||||
committee: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
members: {
|
||||
include: {
|
||||
user: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!disciplinaryCase) {
|
||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json(disciplinaryCase);
|
||||
} catch (error) {
|
||||
console.error('DisciplinaryCases 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 { id } = await params;
|
||||
const body = await request.json();
|
||||
const { status, resolution, reviewedBy } = body;
|
||||
|
||||
const existing = await db.disciplinaryCase.findUnique({ where: { id } });
|
||||
if (!existing) {
|
||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const role = session.user?.role;
|
||||
const userId = session.userId;
|
||||
|
||||
// Check if user is committee member or admin
|
||||
const isCommitteeMember = await db.disciplinaryCommitteeMember.findFirst({
|
||||
where: { committeeId: existing.committeeId, userId },
|
||||
});
|
||||
const isAdmin = role === 'SCHOOL_ADMIN' || role === 'SUPER_ADMIN' || role === 'VICE_PRINCIPAL';
|
||||
|
||||
if (!isCommitteeMember && !isAdmin) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const updateData: Record<string, unknown> = {};
|
||||
if (status) updateData.status = status;
|
||||
if (resolution) updateData.resolution = resolution;
|
||||
if (reviewedBy || status === 'under_review' || status === 'resolved' || status === 'dismissed') {
|
||||
updateData.reviewedBy = reviewedBy || userId;
|
||||
}
|
||||
if (status === 'resolved' || status === 'dismissed') {
|
||||
updateData.resolutionDate = new Date();
|
||||
}
|
||||
|
||||
const updatedCase = await db.disciplinaryCase.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
include: {
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
reporter: { select: { id: true, firstName: true, lastName: true } },
|
||||
reviewer: { select: { id: true, firstName: true, lastName: true } },
|
||||
committee: { select: { id: true, name: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(updatedCase);
|
||||
} catch (error) {
|
||||
console.error('DisciplinaryCases PUT [id] error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
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 schoolId = searchParams.get('schoolId') ?? session.user?.schoolId;
|
||||
const status = searchParams.get('status');
|
||||
const committeeId = searchParams.get('committeeId');
|
||||
|
||||
if (!schoolId) {
|
||||
return NextResponse.json([]);
|
||||
}
|
||||
|
||||
const role = session.user?.role;
|
||||
const userId = session.userId;
|
||||
|
||||
const where: Record<string, unknown> = { schoolId };
|
||||
if (status) where.status = status;
|
||||
if (committeeId) where.committeeId = committeeId;
|
||||
|
||||
// TEACHER: see cases they reported OR cases in their committee
|
||||
if (role === 'TEACHER') {
|
||||
const memberOf = await db.disciplinaryCommitteeMember.findMany({
|
||||
where: { userId },
|
||||
select: { committeeId: true },
|
||||
});
|
||||
const committeeIds = memberOf.map((m) => m.committeeId);
|
||||
|
||||
if (committeeIds.length > 0) {
|
||||
where.OR = [
|
||||
{ reportedBy: userId },
|
||||
{ committeeId: { in: committeeIds } },
|
||||
];
|
||||
} else {
|
||||
where.reportedBy = userId;
|
||||
}
|
||||
}
|
||||
// ADMIN, VICE_PRINCIPAL, SUPER_ADMIN see all
|
||||
|
||||
const cases = await db.disciplinaryCase.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
reporter: { select: { id: true, firstName: true, lastName: true } },
|
||||
reviewer: { select: { id: true, firstName: true, lastName: true } },
|
||||
committee: { select: { id: true, name: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(cases);
|
||||
} catch (error) {
|
||||
console.error('DisciplinaryCases 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 { schoolId, committeeId, studentId, caseType, description, evidence } = body;
|
||||
|
||||
if (!schoolId || !committeeId || !studentId || !caseType || !description) {
|
||||
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
||||
}
|
||||
|
||||
const newCase = await db.disciplinaryCase.create({
|
||||
data: {
|
||||
schoolId,
|
||||
committeeId,
|
||||
studentId,
|
||||
reportedBy: session.userId,
|
||||
caseType,
|
||||
description,
|
||||
evidence: evidence || null,
|
||||
status: 'open',
|
||||
},
|
||||
include: {
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
reporter: { select: { id: true, firstName: true, lastName: true } },
|
||||
committee: { select: { id: true, name: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(newCase, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('DisciplinaryCases POST error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
|
||||
function isAdmin(role: string | undefined): boolean {
|
||||
return role === 'SCHOOL_ADMIN' || role === 'SUPER_ADMIN' || role === 'VICE_PRINCIPAL';
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
if (!isAdmin(session.user?.role)) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const committee = await db.disciplinaryCommittee.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
members: {
|
||||
include: {
|
||||
user: { select: { id: true, firstName: true, lastName: true, role: true } },
|
||||
},
|
||||
},
|
||||
cases: {
|
||||
include: {
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
reporter: { select: { id: true, firstName: true, lastName: true } },
|
||||
reviewer: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!committee) {
|
||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json(committee);
|
||||
} catch (error) {
|
||||
console.error('DisciplinaryCommittees 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 });
|
||||
}
|
||||
|
||||
if (!isAdmin(session.user?.role)) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
const { name, description, isActive, members } = body;
|
||||
|
||||
const existing = await db.disciplinaryCommittee.findUnique({ where: { id } });
|
||||
if (!existing) {
|
||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const updateData: Record<string, unknown> = {};
|
||||
if (name !== undefined) updateData.name = name;
|
||||
if (description !== undefined) updateData.description = description;
|
||||
if (isActive !== undefined) updateData.isActive = isActive;
|
||||
|
||||
// Handle member updates
|
||||
if (members && Array.isArray(members)) {
|
||||
// Delete existing members and recreate
|
||||
await db.disciplinaryCommitteeMember.deleteMany({ where: { committeeId: id } });
|
||||
updateData.members = {
|
||||
create: members.map((m: { userId: string; role: string; isLead: boolean }) => ({
|
||||
userId: m.userId,
|
||||
role: m.role || 'member',
|
||||
isLead: m.isLead || false,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
const committee = await db.disciplinaryCommittee.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
include: {
|
||||
members: {
|
||||
include: {
|
||||
user: { select: { id: true, firstName: true, lastName: true, role: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(committee);
|
||||
} catch (error) {
|
||||
console.error('DisciplinaryCommittees 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 });
|
||||
}
|
||||
|
||||
if (!isAdmin(session.user?.role)) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const existing = await db.disciplinaryCommittee.findUnique({ where: { id } });
|
||||
if (!existing) {
|
||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Deactivate instead of deleting
|
||||
const committee = await db.disciplinaryCommittee.update({
|
||||
where: { id },
|
||||
data: { isActive: false },
|
||||
});
|
||||
|
||||
return NextResponse.json(committee);
|
||||
} catch (error) {
|
||||
console.error('DisciplinaryCommittees DELETE [id] error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
|
||||
function isAdmin(role: string | undefined): boolean {
|
||||
return role === 'SCHOOL_ADMIN' || role === 'SUPER_ADMIN' || role === 'VICE_PRINCIPAL';
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
if (!isAdmin(session.user?.role)) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const schoolId = searchParams.get('schoolId') ?? session.user?.schoolId;
|
||||
if (!schoolId) {
|
||||
return NextResponse.json([]);
|
||||
}
|
||||
|
||||
const committees = await db.disciplinaryCommittee.findMany({
|
||||
where: { schoolId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
members: {
|
||||
include: {
|
||||
user: { select: { id: true, firstName: true, lastName: true, role: true } },
|
||||
},
|
||||
},
|
||||
_count: { select: { cases: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(committees);
|
||||
} catch (error) {
|
||||
console.error('DisciplinaryCommittees 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 (!isAdmin(session.user?.role)) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { schoolId, name, description, members } = body;
|
||||
|
||||
if (!schoolId || !name) {
|
||||
return NextResponse.json({ error: 'schoolId and name are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const committee = await db.disciplinaryCommittee.create({
|
||||
data: {
|
||||
schoolId,
|
||||
name,
|
||||
description: description || null,
|
||||
isActive: true,
|
||||
members: members?.length
|
||||
? {
|
||||
create: members.map((m: { userId: string; role: string; isLead: boolean }) => ({
|
||||
userId: m.userId,
|
||||
role: m.role || 'member',
|
||||
isLead: m.isLead || false,
|
||||
})),
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
include: {
|
||||
members: {
|
||||
include: {
|
||||
user: { select: { id: true, firstName: true, lastName: true, role: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(committee, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('DisciplinaryCommittees POST error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
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 comments = await db.gradingReviewComment.findMany({
|
||||
where: { reviewId: id },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
user: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(comments);
|
||||
} catch (error) {
|
||||
console.error('GradingReviewComments GET error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
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();
|
||||
const { comment, suggestedGrade, reason, resultId } = body;
|
||||
|
||||
if (!comment) {
|
||||
return NextResponse.json({ error: 'comment is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const existing = await db.teacherGradingReview.findUnique({ where: { id } });
|
||||
if (!existing) {
|
||||
return NextResponse.json({ error: 'Review not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const newComment = await db.gradingReviewComment.create({
|
||||
data: {
|
||||
reviewId: id,
|
||||
userId: session.userId,
|
||||
resultId: resultId || null,
|
||||
comment,
|
||||
suggestedGrade: suggestedGrade || null,
|
||||
reason: reason || null,
|
||||
},
|
||||
include: {
|
||||
user: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(newComment, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('GradingReviewComments POST error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
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('GradingReviews 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 { id } = await params;
|
||||
const body = await request.json();
|
||||
const { status } = body;
|
||||
|
||||
const existing = await db.teacherGradingReview.findUnique({ where: { id } });
|
||||
if (!existing) {
|
||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const updateData: Record<string, unknown> = {};
|
||||
if (status) updateData.status = status;
|
||||
|
||||
const review = await db.teacherGradingReview.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
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 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(review);
|
||||
} catch (error) {
|
||||
console.error('GradingReviews PUT [id] error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
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 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;
|
||||
const status = searchParams.get('status');
|
||||
|
||||
if (!schoolId) {
|
||||
return NextResponse.json([]);
|
||||
}
|
||||
|
||||
const role = session.user?.role;
|
||||
if (role !== 'SCHOOL_ADMIN' && role !== 'SUPER_ADMIN' && role !== 'VICE_PRINCIPAL' && role !== 'TEACHER') {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const where: Record<string, unknown> = { schoolId };
|
||||
if (status) where.status = status;
|
||||
|
||||
if (role === 'TEACHER') {
|
||||
where.teacherId = session.userId;
|
||||
}
|
||||
|
||||
const reviews = await db.teacherGradingReview.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
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(reviews);
|
||||
} catch (error) {
|
||||
console.error('GradingReviews 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 !== 'SCHOOL_ADMIN' && role !== 'SUPER_ADMIN' && role !== 'VICE_PRINCIPAL') {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { schoolId, assessmentId, teacherId } = body;
|
||||
|
||||
if (!schoolId || !assessmentId || !teacherId) {
|
||||
return NextResponse.json({ error: 'schoolId, assessmentId, and teacherId are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Get assessment results for the teacher
|
||||
const assessment = await db.assessment.findUnique({
|
||||
where: { id: assessmentId },
|
||||
include: {
|
||||
results: {
|
||||
where: { teacherId },
|
||||
include: {
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!assessment) {
|
||||
return NextResponse.json({ error: 'Assessment not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Generate AI review
|
||||
const systemPrompt = 'Du bist ein erfahrener Lehrer. Überprüfe die Benotung und gib Feedback. Prüfe auf Fairness und Konsistenz.';
|
||||
|
||||
const gradingData = assessment.results.map((r: { student: { firstName: string; lastName: string }; score: number | null; maxScore: number | null; grade: string | null }) => ({
|
||||
student: `${r.student.firstName} ${r.student.lastName}`,
|
||||
score: r.score,
|
||||
maxScore: r.maxScore,
|
||||
grade: r.grade,
|
||||
}));
|
||||
|
||||
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 folgende Benotung und gib Feedback zu Fairness und Konsistenz:\n\n${JSON.stringify(gradingData, null, 2)}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
reviewResult = response?.choices?.[0]?.message?.content || 'Review could not be generated';
|
||||
} catch (aiError) {
|
||||
console.error('AI review error:', aiError);
|
||||
reviewResult = JSON.stringify({
|
||||
overall: 'AI review could not be generated. Manual review recommended.',
|
||||
fairness: 'unknown',
|
||||
consistency: 'unknown',
|
||||
recommendations: ['Please review grading manually'],
|
||||
});
|
||||
}
|
||||
|
||||
// Count discrepancies
|
||||
const scores = assessment.results
|
||||
.map((r: { score: number | null; maxScore: number | null }) => r.score && r.maxScore ? (r.score / r.maxScore) * 100 : null)
|
||||
.filter((s: number | null): s is number => s !== null);
|
||||
if (scores.length > 0) {
|
||||
const avg = scores.reduce((a: number, b: number) => a + b, 0) / scores.length;
|
||||
discrepanciesFound = scores.filter((s: number) => Math.abs(s - avg) > 30).length;
|
||||
}
|
||||
|
||||
const review = await db.teacherGradingReview.create({
|
||||
data: {
|
||||
schoolId,
|
||||
assessmentId,
|
||||
teacherId,
|
||||
aiProvider: 'pollination',
|
||||
status: 'completed',
|
||||
reviewResult,
|
||||
discrepanciesFound,
|
||||
},
|
||||
include: {
|
||||
assessment: { select: { id: true, title: true } },
|
||||
teacher: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(review, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('GradingReviews POST error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
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 report = await db.illnessReport.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
reporter: { select: { id: true, firstName: true, lastName: true } },
|
||||
approver: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
});
|
||||
|
||||
if (!report) {
|
||||
return NextResponse.json({ error: 'Report not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const role = session.user?.role;
|
||||
const userId = session.userId;
|
||||
|
||||
// Access control
|
||||
if (role === 'STUDENT') {
|
||||
const student = await db.student.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ userId },
|
||||
{ userId: null, firstName: session.user?.firstName, lastName: session.user?.lastName, schoolId: session.user?.schoolId },
|
||||
],
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
if (!student || student.id !== report.studentId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
} else if (role === 'PARENT') {
|
||||
const parentLink = await db.parentStudentLink.findFirst({
|
||||
where: { parentId: userId, studentId: report.studentId },
|
||||
});
|
||||
if (!parentLink) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
} else if (role === 'TEACHER') {
|
||||
if (!report.isVisibleToTeacher) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
} else if (role === 'SCHOOL_ADMIN' || role === 'VICE_PRINCIPAL') {
|
||||
if (!report.isVisibleToAdmin) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json(report);
|
||||
} catch (error) {
|
||||
console.error('IllnessReport 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 { id } = await params;
|
||||
const report = await db.illnessReport.findUnique({ where: { id } });
|
||||
|
||||
if (!report) {
|
||||
return NextResponse.json({ error: 'Report not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const role = session.user?.role;
|
||||
const userId = session.userId;
|
||||
const body = await request.json();
|
||||
|
||||
// PARENT can approve/reject pending reports
|
||||
if (body.parentApprovalStatus && (role === 'PARENT' || role === 'SCHOOL_ADMIN' || role === 'VICE_PRINCIPAL')) {
|
||||
const newStatus = body.parentApprovalStatus;
|
||||
if (newStatus !== 'approved' && newStatus !== 'rejected') {
|
||||
return NextResponse.json({ error: 'Invalid approval status' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (role === 'PARENT') {
|
||||
const parentLink = await db.parentStudentLink.findFirst({
|
||||
where: { parentId: userId, studentId: report.studentId },
|
||||
});
|
||||
if (!parentLink) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
const updated = await db.illnessReport.update({
|
||||
where: { id },
|
||||
data: {
|
||||
parentApprovalStatus: newStatus,
|
||||
parentApprovedBy: userId,
|
||||
parentApprovedAt: new Date(),
|
||||
isVisibleToTeacher: newStatus === 'approved',
|
||||
isVisibleToAdmin: newStatus === 'approved',
|
||||
status: newStatus === 'approved' ? 'approved' : 'rejected',
|
||||
},
|
||||
include: {
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
reporter: { select: { id: true, firstName: true, lastName: true } },
|
||||
approver: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(updated);
|
||||
}
|
||||
|
||||
// STUDENT can update own pending reports (before parent approval)
|
||||
if (role === 'STUDENT') {
|
||||
const student = await db.student.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ userId },
|
||||
{ userId: null, firstName: session.user?.firstName, lastName: session.user?.lastName, schoolId: session.user?.schoolId },
|
||||
],
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
if (!student || student.id !== report.studentId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
if (report.parentApprovalStatus !== 'pending') {
|
||||
return NextResponse.json({ error: 'Cannot update report after parent approval' }, { status: 400 });
|
||||
}
|
||||
|
||||
const updateData: Record<string, unknown> = {};
|
||||
if (body.reason) updateData.reason = body.reason;
|
||||
if (body.description !== undefined) updateData.description = body.description;
|
||||
if (body.startDate) updateData.startDate = new Date(body.startDate);
|
||||
if (body.endDate !== undefined) updateData.endDate = body.endDate ? new Date(body.endDate) : null;
|
||||
if (body.documentUrl !== undefined) updateData.documentUrl = body.documentUrl;
|
||||
|
||||
const updated = await db.illnessReport.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
include: {
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
reporter: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(updated);
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
} catch (error) {
|
||||
console.error('IllnessReport 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 { id } = await params;
|
||||
const report = await db.illnessReport.findUnique({ where: { id } });
|
||||
|
||||
if (!report) {
|
||||
return NextResponse.json({ error: 'Report not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const role = session.user?.role;
|
||||
const userId = session.userId;
|
||||
|
||||
// Only the reporter or admin can delete
|
||||
if (role !== 'SUPER_ADMIN' && role !== 'SCHOOL_ADMIN' && role !== 'VICE_PRINCIPAL') {
|
||||
if (report.reportedBy !== userId) {
|
||||
return NextResponse.json({ error: 'Only the reporter or admin can delete' }, { status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
await db.illnessReport.delete({ where: { id } });
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('IllnessReport DELETE [id] error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
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 role = session.user?.role;
|
||||
const userId = session.userId;
|
||||
|
||||
if (role !== 'PARENT' && role !== 'SCHOOL_ADMIN' && role !== 'VICE_PRINCIPAL') {
|
||||
return NextResponse.json({ error: 'Only parents or admins can approve' }, { status: 403 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { reportId, action } = body; // action: "approve" or "reject"
|
||||
|
||||
if (!reportId || !action) {
|
||||
return NextResponse.json({ error: 'reportId and action are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (action !== 'approve' && action !== 'reject') {
|
||||
return NextResponse.json({ error: 'Action must be "approve" or "reject"' }, { status: 400 });
|
||||
}
|
||||
|
||||
const report = await db.illnessReport.findUnique({ where: { id: reportId } });
|
||||
if (!report) {
|
||||
return NextResponse.json({ error: 'Report not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (report.parentApprovalStatus !== 'pending') {
|
||||
return NextResponse.json({ error: 'Report already processed' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Verify parent is linked to this student
|
||||
if (role === 'PARENT') {
|
||||
const parentLink = await db.parentStudentLink.findFirst({
|
||||
where: { parentId: userId, studentId: report.studentId },
|
||||
});
|
||||
if (!parentLink) {
|
||||
return NextResponse.json({ error: 'You can only approve reports for your children' }, { status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
const newStatus = action === 'approve' ? 'approved' : 'rejected';
|
||||
|
||||
const updated = await db.illnessReport.update({
|
||||
where: { id: reportId },
|
||||
data: {
|
||||
parentApprovalStatus: newStatus,
|
||||
parentApprovedBy: userId,
|
||||
parentApprovedAt: new Date(),
|
||||
isVisibleToTeacher: newStatus === 'approved',
|
||||
isVisibleToAdmin: newStatus === 'approved',
|
||||
status: newStatus === 'approved' ? 'approved' : 'rejected',
|
||||
},
|
||||
include: {
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
reporter: { select: { id: true, firstName: true, lastName: true } },
|
||||
approver: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(updated);
|
||||
} catch (error) {
|
||||
console.error('IllnessReport approve error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
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 studentId = searchParams.get('studentId');
|
||||
const status = searchParams.get('status');
|
||||
const parentApprovalStatus = searchParams.get('parentApprovalStatus');
|
||||
|
||||
const role = session.user?.role;
|
||||
const userId = session.userId;
|
||||
|
||||
const where: Record<string, unknown> = {};
|
||||
|
||||
if (role === 'STUDENT') {
|
||||
// Students can only see their own reports
|
||||
const student = await db.student.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ userId },
|
||||
{
|
||||
userId: null,
|
||||
firstName: session.user?.firstName,
|
||||
lastName: session.user?.lastName,
|
||||
schoolId: session.user?.schoolId,
|
||||
},
|
||||
],
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
if (!student) {
|
||||
return NextResponse.json([]);
|
||||
}
|
||||
where.studentId = student.id;
|
||||
} else if (role === 'PARENT') {
|
||||
// Parents can see reports for their children
|
||||
const parentLinks = await db.parentStudentLink.findMany({
|
||||
where: { parentId: userId },
|
||||
select: { studentId: true },
|
||||
});
|
||||
const childIds = parentLinks.map((l) => l.studentId);
|
||||
if (childIds.length === 0) {
|
||||
return NextResponse.json([]);
|
||||
}
|
||||
where.studentId = { in: childIds };
|
||||
} else if (role === 'TEACHER') {
|
||||
// Teachers can only see approved reports
|
||||
where.isVisibleToTeacher = true;
|
||||
where.schoolId = session.user?.schoolId;
|
||||
} else if (role === 'SCHOOL_ADMIN' || role === 'VICE_PRINCIPAL') {
|
||||
// Admin can see all approved reports
|
||||
where.isVisibleToAdmin = true;
|
||||
where.schoolId = session.user?.schoolId;
|
||||
} else if (role === 'SUPER_ADMIN') {
|
||||
// Super admin can see all
|
||||
}
|
||||
|
||||
if (studentId) where.studentId = studentId;
|
||||
if (status) where.status = status;
|
||||
if (parentApprovalStatus) where.parentApprovalStatus = parentApprovalStatus;
|
||||
|
||||
const reports = await db.illnessReport.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
reporter: { select: { id: true, firstName: true, lastName: true } },
|
||||
approver: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(reports);
|
||||
} catch (error) {
|
||||
console.error('IllnessReport 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;
|
||||
const userId = session.userId;
|
||||
|
||||
if (role !== 'STUDENT' && role !== 'PARENT') {
|
||||
return NextResponse.json({ error: 'Only students and parents can report illness' }, { status: 403 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { studentId, reason, description, startDate, endDate, documentUrl } = body;
|
||||
|
||||
if (!studentId || !reason || !startDate) {
|
||||
return NextResponse.json(
|
||||
{ error: 'studentId, reason, and startDate are required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Verify the student exists and belongs to the same school
|
||||
const student = await db.student.findUnique({
|
||||
where: { id: studentId },
|
||||
select: { schoolId: true, userId: true, firstName: true, lastName: true },
|
||||
});
|
||||
|
||||
if (!student) {
|
||||
return NextResponse.json({ error: 'Student not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Authorization checks
|
||||
if (role === 'STUDENT') {
|
||||
const isOwnStudent =
|
||||
student.userId === userId ||
|
||||
(student.userId === null &&
|
||||
student.firstName === session.user?.firstName &&
|
||||
student.lastName === session.user?.lastName &&
|
||||
student.schoolId === session.user?.schoolId);
|
||||
if (!isOwnStudent) {
|
||||
return NextResponse.json({ error: 'You can only report illness for yourself' }, { status: 403 });
|
||||
}
|
||||
} else if (role === 'PARENT') {
|
||||
const parentLink = await db.parentStudentLink.findFirst({
|
||||
where: { parentId: userId, studentId },
|
||||
});
|
||||
if (!parentLink) {
|
||||
return NextResponse.json({ error: 'You can only report illness for your children' }, { status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
const reporterType = role === 'STUDENT' ? 'student' : 'parent';
|
||||
const isParentReport = reporterType === 'parent';
|
||||
|
||||
// When student reports: needs parent approval, not visible to teacher/admin
|
||||
// When parent reports: auto-approved, visible to teacher/admin
|
||||
const report = await db.illnessReport.create({
|
||||
data: {
|
||||
schoolId: student.schoolId,
|
||||
studentId,
|
||||
reportedBy: userId,
|
||||
reporterType,
|
||||
reason,
|
||||
description: description || null,
|
||||
startDate: new Date(startDate),
|
||||
endDate: endDate ? new Date(endDate) : null,
|
||||
documentUrl: documentUrl || null,
|
||||
parentApprovalStatus: isParentReport ? 'approved' : 'pending',
|
||||
parentApprovedBy: isParentReport ? userId : null,
|
||||
parentApprovedAt: isParentReport ? new Date() : null,
|
||||
isVisibleToTeacher: isParentReport,
|
||||
isVisibleToAdmin: isParentReport,
|
||||
status: isParentReport ? 'approved' : 'pending',
|
||||
},
|
||||
include: {
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
reporter: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(report, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('IllnessReport POST error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { withRateLimit } from '@/lib/rate-limit';
|
||||
|
||||
// ── GET: Get single category ──
|
||||
async function getCategory(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const category = await db.subjectCategory.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
_count: { select: { contents: { where: { isActive: true } } } },
|
||||
},
|
||||
});
|
||||
|
||||
if (!category) {
|
||||
return NextResponse.json({ error: 'Category not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json(category);
|
||||
} catch (error) {
|
||||
console.error('SubjectCategory GET [id] error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// ── PUT: Update category ──
|
||||
const updateCategorySchema = z.object({
|
||||
name: z.string().min(1).max(200).optional(),
|
||||
slug: z.string().min(1).max(200).optional(),
|
||||
description: z.string().max(1000).optional().nullable(),
|
||||
icon: z.string().optional().nullable(),
|
||||
sortOrder: z.number().int().optional(),
|
||||
isActive: z.boolean().optional(),
|
||||
});
|
||||
|
||||
async function updateCategory(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
if (
|
||||
session.user.role !== 'SCHOOL_ADMIN' &&
|
||||
session.user.role !== 'VICE_PRINCIPAL' &&
|
||||
session.user.role !== 'SUPER_ADMIN'
|
||||
) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
const parsed = updateCategorySchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Validation failed', details: parsed.error.issues },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const category = await db.subjectCategory.update({
|
||||
where: { id },
|
||||
data: parsed.data,
|
||||
});
|
||||
|
||||
return NextResponse.json(category);
|
||||
} catch (error) {
|
||||
console.error('SubjectCategory PUT [id] error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// ── DELETE: Soft delete / deactivate category ──
|
||||
async function deleteCategory(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
if (
|
||||
session.user.role !== 'SCHOOL_ADMIN' &&
|
||||
session.user.role !== 'VICE_PRINCIPAL' &&
|
||||
session.user.role !== 'SUPER_ADMIN'
|
||||
) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
// Soft delete by deactivating
|
||||
const category = await db.subjectCategory.update({
|
||||
where: { id },
|
||||
data: { isActive: false },
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true, category });
|
||||
} catch (error) {
|
||||
console.error('SubjectCategory DELETE [id] error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export const GET = withRateLimit(getCategory, 'dataRead');
|
||||
export const PUT = withRateLimit(updateCategory, 'dataWrite');
|
||||
export const DELETE = withRateLimit(deleteCategory, 'dataWrite');
|
||||
@@ -0,0 +1,101 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { withRateLimit } from '@/lib/rate-limit';
|
||||
|
||||
// ── GET: List subject categories ──
|
||||
async function getCategories(request: Request) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const schoolId = searchParams.get('schoolId') || session.user.schoolId;
|
||||
const includeInactive = searchParams.get('includeInactive') === 'true';
|
||||
|
||||
if (!schoolId) {
|
||||
return NextResponse.json([]);
|
||||
}
|
||||
|
||||
const where: Record<string, unknown> = { schoolId };
|
||||
if (!includeInactive) {
|
||||
where.isActive = true;
|
||||
}
|
||||
|
||||
const categories = await db.subjectCategory.findMany({
|
||||
where,
|
||||
include: {
|
||||
_count: { select: { contents: { where: { isActive: true } } } },
|
||||
},
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
});
|
||||
|
||||
return NextResponse.json(categories);
|
||||
} catch (error) {
|
||||
console.error('SubjectCategories GET error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// ── POST: Create a new subject category ──
|
||||
const createCategorySchema = z.object({
|
||||
schoolId: z.string().optional(),
|
||||
name: z.string().min(1).max(200),
|
||||
slug: z.string().min(1).max(200),
|
||||
description: z.string().max(1000).optional().nullable(),
|
||||
icon: z.string().optional().nullable(),
|
||||
sortOrder: z.number().int().default(0),
|
||||
});
|
||||
|
||||
async function createCategory(request: Request) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
if (
|
||||
session.user.role !== 'SCHOOL_ADMIN' &&
|
||||
session.user.role !== 'VICE_PRINCIPAL' &&
|
||||
session.user.role !== 'SUPER_ADMIN'
|
||||
) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const parsed = createCategorySchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Validation failed', details: parsed.error.issues },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const schoolId = parsed.data.schoolId || session.user.schoolId;
|
||||
if (!schoolId) {
|
||||
return NextResponse.json({ error: 'User must belong to a school' }, { status: 400 });
|
||||
}
|
||||
|
||||
const category = await db.subjectCategory.create({
|
||||
data: {
|
||||
schoolId,
|
||||
name: parsed.data.name,
|
||||
slug: parsed.data.slug,
|
||||
description: parsed.data.description,
|
||||
icon: parsed.data.icon,
|
||||
sortOrder: parsed.data.sortOrder,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(category, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('SubjectCategory POST error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export const GET = withRateLimit(getCategories, 'dataRead');
|
||||
export const POST = withRateLimit(createCategory, 'dataWrite');
|
||||
@@ -0,0 +1,140 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { withRateLimit } from '@/lib/rate-limit';
|
||||
|
||||
// ── GET: Get single content ──
|
||||
async function getContent(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const content = await db.subjectContent.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
category: { select: { id: true, name: true, slug: true } },
|
||||
subject: { select: { id: true, name: true } },
|
||||
parent: { select: { id: true, title: true, slug: true } },
|
||||
children: {
|
||||
where: { isActive: true },
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!content) {
|
||||
return NextResponse.json({ error: 'Content not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json(content);
|
||||
} catch (error) {
|
||||
console.error('SubjectContent GET [id] error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// ── PUT: Update content ──
|
||||
const updateContentSchema = z.object({
|
||||
title: z.string().min(1).max(300).optional(),
|
||||
slug: z.string().min(1).max(300).optional(),
|
||||
description: z.string().max(2000).optional().nullable(),
|
||||
icon: z.string().optional().nullable(),
|
||||
contentType: z.enum(['topic', 'exercise', 'lesson', 'quiz', 'vocabulary']).optional(),
|
||||
content: z.string().optional().nullable(),
|
||||
difficulty: z.enum(['easy', 'medium', 'hard']).optional(),
|
||||
questionCount: z.number().int().optional(),
|
||||
sortOrder: z.number().int().optional(),
|
||||
isActive: z.boolean().optional(),
|
||||
isPublic: z.boolean().optional(),
|
||||
sourceUrl: z.string().optional().nullable(),
|
||||
subjectId: z.string().optional().nullable(),
|
||||
parentId: z.string().optional().nullable(),
|
||||
});
|
||||
|
||||
async function updateContent(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
const parsed = updateContentSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Validation failed', details: parsed.error.issues },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// SCHOOL_ADMIN, VICE_PRINCIPAL can edit directly; TEACHER must use change request
|
||||
if (session.user.role === 'TEACHER') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Teachers must submit a change request to edit content' },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
session.user.role !== 'SCHOOL_ADMIN' &&
|
||||
session.user.role !== 'VICE_PRINCIPAL' &&
|
||||
session.user.role !== 'SUPER_ADMIN'
|
||||
) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const contentItem = await db.subjectContent.update({
|
||||
where: { id },
|
||||
data: parsed.data,
|
||||
include: {
|
||||
category: { select: { id: true, name: true } },
|
||||
subject: { select: { id: true, name: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(contentItem);
|
||||
} catch (error) {
|
||||
console.error('SubjectContent PUT [id] error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// ── DELETE: Delete content ──
|
||||
async function deleteContent(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
if (
|
||||
session.user.role !== 'SCHOOL_ADMIN' &&
|
||||
session.user.role !== 'VICE_PRINCIPAL' &&
|
||||
session.user.role !== 'SUPER_ADMIN'
|
||||
) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
// Soft delete by deactivating
|
||||
const contentItem = await db.subjectContent.update({
|
||||
where: { id },
|
||||
data: { isActive: false },
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true, content: contentItem });
|
||||
} catch (error) {
|
||||
console.error('SubjectContent DELETE [id] error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export const GET = withRateLimit(getContent, 'dataRead');
|
||||
export const PUT = withRateLimit(updateContent, 'dataWrite');
|
||||
export const DELETE = withRateLimit(deleteContent, 'dataWrite');
|
||||
@@ -0,0 +1,136 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { withRateLimit } from '@/lib/rate-limit';
|
||||
|
||||
// ── GET: List subject contents ──
|
||||
async function getContents(request: Request) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const schoolId = searchParams.get('schoolId') || session.user.schoolId;
|
||||
const categoryId = searchParams.get('categoryId');
|
||||
const subjectId = searchParams.get('subjectId');
|
||||
const contentType = searchParams.get('contentType');
|
||||
const parentId = searchParams.get('parentId');
|
||||
const isActive = searchParams.get('isActive');
|
||||
|
||||
if (!schoolId) {
|
||||
return NextResponse.json([]);
|
||||
}
|
||||
|
||||
const where: Record<string, unknown> = { schoolId };
|
||||
if (categoryId) where.categoryId = categoryId;
|
||||
if (subjectId) where.subjectId = subjectId;
|
||||
if (contentType) where.contentType = contentType;
|
||||
if (parentId) where.parentId = parentId;
|
||||
if (isActive !== null && isActive !== undefined) {
|
||||
where.isActive = isActive === 'true';
|
||||
}
|
||||
|
||||
const contents = await db.subjectContent.findMany({
|
||||
where,
|
||||
include: {
|
||||
category: { select: { id: true, name: true, slug: true } },
|
||||
subject: { select: { id: true, name: true } },
|
||||
_count: { select: { children: { where: { isActive: true } } } },
|
||||
},
|
||||
orderBy: [{ sortOrder: 'asc' }, { createdAt: 'desc' }],
|
||||
});
|
||||
|
||||
return NextResponse.json(contents);
|
||||
} catch (error) {
|
||||
console.error('SubjectContents GET error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// ── POST: Create subject content ──
|
||||
const createContentSchema = z.object({
|
||||
schoolId: z.string().optional(),
|
||||
categoryId: z.string().min(1),
|
||||
subjectId: z.string().optional().nullable(),
|
||||
parentId: z.string().optional().nullable(),
|
||||
title: z.string().min(1).max(300),
|
||||
slug: z.string().min(1).max(300),
|
||||
description: z.string().max(2000).optional().nullable(),
|
||||
icon: z.string().optional().nullable(),
|
||||
contentType: z.enum(['topic', 'exercise', 'lesson', 'quiz', 'vocabulary']).default('topic'),
|
||||
content: z.string().optional().nullable(),
|
||||
difficulty: z.enum(['easy', 'medium', 'hard']).default('medium'),
|
||||
questionCount: z.number().int().default(0),
|
||||
sortOrder: z.number().int().default(0),
|
||||
isActive: z.boolean().default(true),
|
||||
isPublic: z.boolean().default(true),
|
||||
sourceUrl: z.string().optional().nullable(),
|
||||
});
|
||||
|
||||
async function createContent(request: Request) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
if (
|
||||
session.user.role !== 'TEACHER' &&
|
||||
session.user.role !== 'SCHOOL_ADMIN' &&
|
||||
session.user.role !== 'VICE_PRINCIPAL' &&
|
||||
session.user.role !== 'SUPER_ADMIN'
|
||||
) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const parsed = createContentSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Validation failed', details: parsed.error.issues },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const schoolId = parsed.data.schoolId || session.user.schoolId;
|
||||
if (!schoolId) {
|
||||
return NextResponse.json({ error: 'User must belong to a school' }, { status: 400 });
|
||||
}
|
||||
|
||||
const contentItem = await db.subjectContent.create({
|
||||
data: {
|
||||
schoolId,
|
||||
categoryId: parsed.data.categoryId,
|
||||
subjectId: parsed.data.subjectId,
|
||||
parentId: parsed.data.parentId,
|
||||
title: parsed.data.title,
|
||||
slug: parsed.data.slug,
|
||||
description: parsed.data.description,
|
||||
icon: parsed.data.icon,
|
||||
contentType: parsed.data.contentType,
|
||||
content: parsed.data.content,
|
||||
difficulty: parsed.data.difficulty,
|
||||
questionCount: parsed.data.questionCount,
|
||||
sortOrder: parsed.data.sortOrder,
|
||||
isActive: parsed.data.isActive,
|
||||
isPublic: parsed.data.isPublic,
|
||||
sourceUrl: parsed.data.sourceUrl,
|
||||
},
|
||||
include: {
|
||||
category: { select: { id: true, name: true } },
|
||||
subject: { select: { id: true, name: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(contentItem, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('SubjectContent POST error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export const GET = withRateLimit(getContents, 'dataRead');
|
||||
export const POST = withRateLimit(createContent, 'dataWrite');
|
||||
@@ -0,0 +1,233 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { withRateLimit } from '@/lib/rate-limit';
|
||||
|
||||
// Schlaukopf.de content structure for Gymnasium
|
||||
const SCHLAUKOPF_CATEGORIES = [
|
||||
{ name: 'Klasse 5', slug: 'klasse5', icon: 'GraduationCap', sortOrder: 0 },
|
||||
{ name: 'Klasse 6', slug: 'klasse6', icon: 'GraduationCap', sortOrder: 1 },
|
||||
{ name: 'Klasse 7', slug: 'klasse7', icon: 'GraduationCap', sortOrder: 2 },
|
||||
{ name: 'Klasse 8', slug: 'klasse8', icon: 'GraduationCap', sortOrder: 3 },
|
||||
{ name: 'Klasse 9', slug: 'klasse9', icon: 'GraduationCap', sortOrder: 4 },
|
||||
{ name: 'Klasse 10', slug: 'klasse10', icon: 'GraduationCap', sortOrder: 5 },
|
||||
{ name: 'Oberstufe', slug: 'oberstufe', icon: 'Award', sortOrder: 6 },
|
||||
];
|
||||
|
||||
const SUBJECTS_BY_CLASS: Record<string, Array<{ name: string; slug: string; icon: string; topics: string[] }>> = {
|
||||
'klasse5': [
|
||||
{ name: 'Mathematik', slug: 'mathematik', icon: 'Calculator', topics: ['Grundwissen', 'Natuerliche Zahlen', 'Brueche', 'Geometrie', 'Groessen', 'Terme'] },
|
||||
{ name: 'Deutsch', slug: 'deutsch', icon: 'BookOpen', topics: ['Grammatik', 'Rechtschreibung', 'Wortarten', 'Satzarten', 'Aufsatz', 'Lesen'] },
|
||||
{ name: 'Englisch', slug: 'englisch', icon: 'Globe', topics: ['Vocabulary', 'Grammar', 'Tenses', 'Reading', 'Listening'] },
|
||||
{ name: 'Franzoesisch', slug: 'franzoesisch', icon: 'Globe', topics: ['Vokabeln', 'Grammatik', 'Konjugation', 'Lesen'] },
|
||||
{ name: 'Biologie', slug: 'biologie', icon: 'FlaskConical', topics: ['Lebewesen', 'Zellen', 'Mensch', 'Pflanzen', 'Tiere'] },
|
||||
{ name: 'Geografie', slug: 'geografie', icon: 'Map', topics: ['Sonnensystem', 'Deutschland', 'Europa', 'Klima'] },
|
||||
{ name: 'Musik', slug: 'musik', icon: 'Music', topics: ['Noten', 'Intervalle', 'Rhythmus', 'Instrumente'] },
|
||||
{ name: 'Geschichte', slug: 'geschichte', icon: 'History', topics: ['Fruehgeschichte', 'Aegypten', 'Griechenland', 'Roemer'] },
|
||||
{ name: 'Natur und Technik', slug: 'natur-und-technik', icon: 'Zap', topics: ['Akustik', 'Licht', 'Magnetismus', 'Elektrizitaet'] },
|
||||
{ name: 'Ethik und Religion', slug: 'ethik', icon: 'Heart', topics: ['Werte', 'Religionen', 'Ethik', 'Philosophie'] },
|
||||
{ name: 'Latein', slug: 'latein', icon: 'BookOpen', topics: ['Vokabeln', 'Grammatik', 'Uebersetzung', 'Kultur'] },
|
||||
],
|
||||
'klasse6': [
|
||||
{ name: 'Mathematik', slug: 'mathematik', icon: 'Calculator', topics: ['Bruchrechnung', 'Dezimalzahlen', 'Geometrie', 'Gleichungen', 'Proportionalitaet'] },
|
||||
{ name: 'Deutsch', slug: 'deutsch', icon: 'BookOpen', topics: ['Grammatik', 'Rechtschreibung', 'Epik', 'Lyrik', 'Medien'] },
|
||||
{ name: 'Englisch', slug: 'englisch', icon: 'Globe', topics: ['Vocabulary', 'Grammar', 'Tenses', 'Writing', 'Speaking'] },
|
||||
{ name: 'Franzoesisch', slug: 'franzoesisch', icon: 'Globe', topics: ['Vokabeln', 'Grammatik', 'Passe Compose', 'Lesen'] },
|
||||
{ name: 'Biologie', slug: 'biologie', icon: 'FlaskConical', topics: ['Oekologie', 'Evolution', 'Sinnesorgane', 'Fortbewegung'] },
|
||||
{ name: 'Geografie', slug: 'geografie', icon: 'Map', topics: ['Naturgefaehrdungen', 'Wirtschaft', 'Bevoelkerung', 'Globalisierung'] },
|
||||
{ name: 'Musik', slug: 'musik', icon: 'Music', topics: ['Tonleiter', 'Akkorde', 'Musikgeschichte', 'Komponisten'] },
|
||||
{ name: 'Geschichte', slug: 'geschichte', icon: 'History', topics: ['Mittelalter', 'Kreuzzuege', 'Kloster', 'Ritter'] },
|
||||
{ name: 'Natur und Technik', slug: 'natur-und-technik', icon: 'Zap', topics: ['Optik', 'Mechanik', 'Stoffe', 'Geraete'] },
|
||||
{ name: 'Ethik und Religion', slug: 'ethik', icon: 'Heart', topics: ['Moral', 'Gerechtigkeit', 'Freiheit', 'Verantwortung'] },
|
||||
{ name: 'Latein', slug: 'latein', icon: 'BookOpen', topics: ['Vokabeln', 'Grammatik', 'Ablativ', 'Kultur'] },
|
||||
],
|
||||
'klasse7': [
|
||||
{ name: 'Mathematik', slug: 'mathematik', icon: 'Calculator', topics: ['Terme', 'Gleichungen', 'Prozentrechnung', 'Statistik', 'Geometrie'] },
|
||||
{ name: 'Deutsch', slug: 'deutsch', icon: 'BookOpen', topics: ['Grammatik', 'Literatur', 'Sachtexte', 'Schreiben', 'Rhetorik'] },
|
||||
{ name: 'Englisch', slug: 'englisch', icon: 'Globe', topics: ['Vocabulary', 'Grammar', 'Tenses', 'Literature', 'Debate'] },
|
||||
{ name: 'Franzoesisch', slug: 'franzoesisch', icon: 'Globe', topics: ['Vokabeln', 'Grammatik', 'Subjonctif', 'Landeskunde'] },
|
||||
{ name: 'Biologie', slug: 'biologie', icon: 'FlaskConical', topics: ['Genetik', 'Zellteilung', 'Stoffwechsel', 'Nervensystem'] },
|
||||
{ name: 'Geografie', slug: 'geografie', icon: 'Map', topics: ['Nordamerika', 'Suedamerika', 'Asien', 'Klimawandel'] },
|
||||
{ name: 'Musik', slug: 'musik', icon: 'Music', topics: ['Musikformen', 'Jazz', 'Pop', 'Filmmusik'] },
|
||||
{ name: 'Geschichte', slug: 'geschichte', icon: 'History', topics: ['Entdeckungen', 'Reformation', 'Dreissigjaehriger Krieg', 'Aufklaerung'] },
|
||||
{ name: 'Natur und Technik', slug: 'natur-und-technik', icon: 'Zap', topics: ['Chemie', 'Elektrizitaet', 'Kraefte', 'Energie'] },
|
||||
{ name: 'Ethik und Religion', slug: 'ethik', icon: 'Heart', topics: ['Menschenwuerde', 'Toleranz', 'Konflikte', 'Frieden'] },
|
||||
{ name: 'Latein', slug: 'latein', icon: 'BookOpen', topics: ['Vokabeln', 'Konjunktiv', 'Caesar', 'Kultur'] },
|
||||
],
|
||||
'klasse8': [
|
||||
{ name: 'Mathematik', slug: 'mathematik', icon: 'Calculator', topics: ['Quadratische Gleichungen', 'Funktionen', 'Satz des Pythagoras', 'Wahrscheinlichkeit', 'Aehnlichkeit'] },
|
||||
{ name: 'Deutsch', slug: 'deutsch', icon: 'BookOpen', topics: ['Dramatik', 'Lyrik', 'Epik', 'Medien', 'Kommunikation'] },
|
||||
{ name: 'Englisch', slug: 'englisch', icon: 'Globe', topics: ['Vocabulary', 'Grammar', 'Conditionals', 'Reported Speech', 'Writing'] },
|
||||
{ name: 'Franzoesisch', slug: 'franzoesisch', icon: 'Globe', topics: ['Vokabeln', 'Grammatik', 'Conditionnel', 'Frankophone'] },
|
||||
{ name: 'Biologie', slug: 'biologie', icon: 'FlaskConical', topics: ['Immunsystem', 'Hormone', 'Evolution', 'Verhaltensbiologie'] },
|
||||
{ name: 'Physik', slug: 'physik', icon: 'Zap', topics: ['Mechanik', 'Elektrizitaet', 'Optik', 'Akustik'] },
|
||||
{ name: 'Chemie', slug: 'chemie', icon: 'FlaskConical', topics: ['Atombau', 'Periodensystem', 'Bindungen', 'Reaktionen'] },
|
||||
{ name: 'Geschichte', slug: 'geschichte', icon: 'History', topics: ['Industrielle Revolution', 'Imperialismus', 'Erster Weltkrieg', 'Weimarer Republik'] },
|
||||
{ name: 'Geografie', slug: 'geografie', icon: 'Map', topics: ['Afrika', 'Australien', 'Polarregionen', 'Nachhaltigkeit'] },
|
||||
{ name: 'Ethik und Religion', slug: 'ethik', icon: 'Heart', topics: ['Ethik der Technik', 'Bioethik', 'Medienethik', 'Weltethos'] },
|
||||
{ name: 'Latein', slug: 'latein', icon: 'BookOpen', topics: ['Vokabeln', 'Partizip', 'Cicero', 'Kultur'] },
|
||||
],
|
||||
'klasse9': [
|
||||
{ name: 'Mathematik', slug: 'mathematik', icon: 'Calculator', topics: ['Analysis', 'Trigonometrie', 'Stochastik', 'Vektorrechnung', 'Funktionen'] },
|
||||
{ name: 'Deutsch', slug: 'deutsch', icon: 'BookOpen', topics: ['Literaturgeschichte', 'Expressionismus', 'Kurzgeschichten', 'Sachtexte', 'Rhetorik'] },
|
||||
{ name: 'Englisch', slug: 'englisch', icon: 'Globe', topics: ['Vocabulary', 'Grammar', 'Shakespeare', 'Debate', 'Writing'] },
|
||||
{ name: 'Franzoesisch', slug: 'franzoesisch', icon: 'Globe', topics: ['Vokabeln', 'Grammatik', 'Literatur', 'Zivilisation'] },
|
||||
{ name: 'Biologie', slug: 'biologie', icon: 'FlaskConical', topics: ['Neurobiologie', 'Oekologie', 'Gentechnik', 'Evolution'] },
|
||||
{ name: 'Physik', slug: 'physik', icon: 'Zap', topics: ['Elektromagnetismus', 'Kernphysik', 'Mechanik', 'Quantenphysik'] },
|
||||
{ name: 'Chemie', slug: 'chemie', icon: 'FlaskConical', topics: ['Organische Chemie', 'Saeuren', 'Redoxreaktionen', 'Analytik'] },
|
||||
{ name: 'Geschichte', slug: 'geschichte', icon: 'History', topics: ['Nationalsozialismus', 'Zweiter Weltkrieg', 'Holocaust', 'Nachkriegszeit'] },
|
||||
{ name: 'Geografie', slug: 'geografie', icon: 'Map', topics: ['Europa', 'Globalisierung', 'Migration', 'Wirtschaftsgeografie'] },
|
||||
{ name: 'Ethik und Religion', slug: 'ethik', icon: 'Heart', topics: ['Existenzphilosophie', 'Politische Ethik', 'Recht', 'Gerechtigkeit'] },
|
||||
{ name: 'Latein', slug: 'latein', icon: 'BookOpen', topics: ['Vokabeln', 'Uebersetzung', 'Ovid', 'Kultur'] },
|
||||
],
|
||||
'klasse10': [
|
||||
{ name: 'Mathematik', slug: 'mathematik', icon: 'Calculator', topics: ['Analysis', 'Wahrscheinlichkeit', 'Vektorrechnung', 'Kurven', 'Grenzwerte'] },
|
||||
{ name: 'Deutsch', slug: 'deutsch', icon: 'BookOpen', topics: ['Literatur', 'Klassik', 'Romantik', 'Gegenwartsliteratur', 'Erörterung'] },
|
||||
{ name: 'Englisch', slug: 'englisch', icon: 'Globe', topics: ['Vocabulary', 'Grammar', 'Literature', 'Creative Writing', 'Presentations'] },
|
||||
{ name: 'Franzoesisch', slug: 'franzoesisch', icon: 'Globe', topics: ['Vokabeln', 'Grammatik', 'Literatur', 'Film'] },
|
||||
{ name: 'Biologie', slug: 'biologie', icon: 'FlaskConical', topics: ['Genetik', 'Immunologie', 'Verhaltensforschung', 'Oekologie'] },
|
||||
{ name: 'Physik', slug: 'physik', icon: 'Zap', topics: ['Relativitaetstheorie', 'Quantenphysik', 'Astrophysik', 'Technik'] },
|
||||
{ name: 'Chemie', slug: 'chemie', icon: 'FlaskConical', topics: ['Polymerchemie', 'Umweltchemie', 'Biochemie', 'Labor'] },
|
||||
{ name: 'Geschichte', slug: 'geschichte', icon: 'History', topics: ['Kalter Krieg', 'Deutsche Teilung', 'Wiedervereinigung', 'Europa'] },
|
||||
{ name: 'Geografie', slug: 'geografie', icon: 'Map', topics: ['Deutschland', 'Raumplanung', 'Entwicklung', 'Ressourcen'] },
|
||||
{ name: 'Ethik und Religion', slug: 'ethik', icon: 'Heart', topics: ['Ethik der Zukunft', 'Kuenstliche Intelligenz', 'Nachhaltigkeit', 'Weltreligionen'] },
|
||||
{ name: 'Informatik', slug: 'informatik', icon: 'Code', topics: ['Programmierung', 'Algorithmen', 'Datenbanken', 'Netzwerke'] },
|
||||
],
|
||||
'oberstufe': [
|
||||
{ name: 'Mathematik', slug: 'mathematik', icon: 'Calculator', topics: ['Analysis', 'Lineare Algebra', 'Stochastik', 'Analytische Geometrie'] },
|
||||
{ name: 'Deutsch', slug: 'deutsch', icon: 'BookOpen', topics: ['Literatur', 'Sprache', 'Medien', 'Kommunikation'] },
|
||||
{ name: 'Englisch', slug: 'englisch', icon: 'Globe', topics: ['Literature', 'Linguistics', 'Cultural Studies', 'Academic Writing'] },
|
||||
{ name: 'Franzoesisch', slug: 'franzoesisch', icon: 'Globe', topics: ['Literature', 'Civilisation', 'Grammaire', 'Expression'] },
|
||||
{ name: 'Biologie', slug: 'biologie', icon: 'FlaskConical', topics: ['Molekularbiologie', 'Neurobiologie', 'Oekologie', 'Evolution'] },
|
||||
{ name: 'Physik', slug: 'physik', icon: 'Zap', topics: ['Mechanik', 'Elektrodynamik', 'Thermodynamik', 'Quantenphysik'] },
|
||||
{ name: 'Chemie', slug: 'chemie', icon: 'FlaskConical', topics: ['Organische Chemie', 'Physikalische Chemie', 'Analytik', 'Umwelt'] },
|
||||
{ name: 'Geschichte', slug: 'geschichte', icon: 'History', topics: ['20. Jahrhundert', 'Globalgeschichte', 'Erinnerungskultur', 'Quellenanalyse'] },
|
||||
{ name: 'Geografie', slug: 'geografie', icon: 'Map', topics: ['Globalisierung', 'Klimawandel', 'Urbanisierung', 'Geoinformatik'] },
|
||||
{ name: 'Informatik', slug: 'informatik', icon: 'Code', topics: ['Objektorientierung', 'Datenstrukturen', 'Datenbanken', 'Theoretische Informatik'] },
|
||||
{ name: 'Wirtschaft', slug: 'wirtschaft', icon: 'TrendingUp', topics: ['Volkswirtschaft', 'Betriebswirtschaft', 'Finanzen', 'Markt'] },
|
||||
{ name: 'Kunst', slug: 'kunst', icon: 'Palette', topics: ['Kunstgeschichte', 'Malerei', 'Skulptur', 'Neue Medien'] },
|
||||
{ name: 'Sport', slug: 'sport', icon: 'Dumbbell', topics: ['Trainingslehre', 'Sportpsychologie', 'Bewegungsanalyse', 'Gesundheit'] },
|
||||
],
|
||||
};
|
||||
|
||||
// ── POST: Seed subject categories and content ──
|
||||
async function seedData(request: Request) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
if (
|
||||
session.user.role !== 'SCHOOL_ADMIN' &&
|
||||
session.user.role !== 'VICE_PRINCIPAL' &&
|
||||
session.user.role !== 'SUPER_ADMIN'
|
||||
) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const schoolId = session.user.schoolId;
|
||||
if (!schoolId) {
|
||||
return NextResponse.json({ error: 'User must belong to a school' }, { status: 400 });
|
||||
}
|
||||
|
||||
let categoriesCreated = 0;
|
||||
let contentsCreated = 0;
|
||||
|
||||
// Create categories
|
||||
for (const catData of SCHLAUKOPF_CATEGORIES) {
|
||||
const existing = await db.subjectCategory.findFirst({
|
||||
where: { schoolId, slug: catData.slug },
|
||||
});
|
||||
|
||||
if (existing) continue;
|
||||
|
||||
const category = await db.subjectCategory.create({
|
||||
data: {
|
||||
schoolId,
|
||||
name: catData.name,
|
||||
slug: catData.slug,
|
||||
icon: catData.icon,
|
||||
sortOrder: catData.sortOrder,
|
||||
},
|
||||
});
|
||||
|
||||
categoriesCreated++;
|
||||
|
||||
// Create subjects and topics for this class
|
||||
const subjects = SUBJECTS_BY_CLASS[catData.slug] || [];
|
||||
for (const subj of subjects) {
|
||||
// Check if subject already exists
|
||||
const existingContent = await db.subjectContent.findFirst({
|
||||
where: { schoolId, categoryId: category.id, slug: subj.slug },
|
||||
});
|
||||
|
||||
if (existingContent) continue;
|
||||
|
||||
// Create the subject content
|
||||
const subjectContent = await db.subjectContent.create({
|
||||
data: {
|
||||
schoolId,
|
||||
categoryId: category.id,
|
||||
title: subj.name,
|
||||
slug: subj.slug,
|
||||
icon: subj.icon,
|
||||
contentType: 'topic',
|
||||
sortOrder: subjects.indexOf(subj),
|
||||
isActive: true,
|
||||
isPublic: true,
|
||||
},
|
||||
});
|
||||
|
||||
contentsCreated++;
|
||||
|
||||
// Create subtopics
|
||||
for (let i = 0; i < subj.topics.length; i++) {
|
||||
const topicName = subj.topics[i];
|
||||
const topicSlug = `${subj.slug}-${topicName.toLowerCase().replace(/[^a-z0-9]/g, '-')}`;
|
||||
|
||||
const existingTopic = await db.subjectContent.findFirst({
|
||||
where: { schoolId, slug: topicSlug, parentId: subjectContent.id },
|
||||
});
|
||||
|
||||
if (existingTopic) continue;
|
||||
|
||||
await db.subjectContent.create({
|
||||
data: {
|
||||
schoolId,
|
||||
categoryId: category.id,
|
||||
parentId: subjectContent.id,
|
||||
title: topicName,
|
||||
slug: topicSlug,
|
||||
contentType: 'topic',
|
||||
sortOrder: i,
|
||||
isActive: true,
|
||||
isPublic: true,
|
||||
questionCount: Math.floor(Math.random() * 20) + 5,
|
||||
},
|
||||
});
|
||||
|
||||
contentsCreated++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure AI settings exist
|
||||
const existingSettings = await db.aISettings.findUnique({ where: { schoolId } });
|
||||
if (!existingSettings) {
|
||||
await db.aISettings.create({ data: { schoolId } });
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
categoriesCreated,
|
||||
contentsCreated,
|
||||
message: `Created ${categoriesCreated} categories and ${contentsCreated} content items`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Subject seed POST error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export const POST = withRateLimit(seedData, 'dataWrite');
|
||||
Reference in new issue
Block a user