From e5c761a179f7dbe04b8193fd6062f9e223d9f673 Mon Sep 17 00:00:00 2001 From: Z User Date: Tue, 11 Aug 2026 11:03:24 +0000 Subject: [PATCH] Architecture cleanup: Remove orphans from audit report (37 files) Deleted orphan component files (4): - dashboard-customizer.tsx (never imported) - unified-communication.tsx (never imported) - data-table.tsx (never imported) - widget-container.tsx (only imported by orphan dashboard-customizer) Deleted unused shadcn/ui primitives (15): - accordion, aspect-ratio, calendar, carousel, chart, form, hover-card, input-otp, menubar, navigation-menu, pagination, radio-group, resizable, toaster, toggle-group (All verified as zero importers) Deleted orphan API route groups (6 directories, 13 route files): - ai-grading-audit (no caller) - demo-data (no caller) - exam-plans (no caller) - grade-analytics (no caller, dead renderView case also removed) - grading-annotations (no caller) - grading-reviews (no caller) Removed dead grade-analytics renderView case + ViewName type entry Removed 10 orphan database models from schema: - NotificationHub, NotificationArchive (superseded by Notification) - WidgetTemplate (dashboard customizer feature abandoned) - EmergencySignage, SignageMessage, SignageSchedule, SignageAuditLog (signage feature - no API routes implemented) - SchoolPolicy, EscalationPolicy, ClassMessagingLock (policy/messaging lock feature - never wired) Also removed 16 relation fields pointing to deleted models. Schema validated, db pushed, lint passes, all 10 tested modules work. --- prisma/schema.prisma | 231 ----------- src/app/api/ai-grading-audit/[id]/route.ts | 39 -- src/app/api/ai-grading-audit/route.ts | 133 ------- src/app/api/demo-data/route.ts | 165 -------- src/app/api/exam-plans/[id]/route.ts | 200 ---------- src/app/api/exam-plans/route.ts | 318 ---------------- .../api/grade-analytics/comparison/route.ts | 358 ------------------ .../api/grade-analytics/distribution/route.ts | 184 --------- src/app/api/grade-analytics/reports/route.ts | 232 ------------ src/app/api/grade-analytics/route.ts | 250 ------------ src/app/api/grade-analytics/trends/route.ts | 215 ----------- src/app/api/grading-annotations/[id]/route.ts | 129 ------- src/app/api/grading-annotations/route.ts | 108 ------ .../grading-reviews/[id]/comments/route.ts | 73 ---- src/app/api/grading-reviews/[id]/route.ts | 82 ---- src/app/api/grading-reviews/route.ts | 158 -------- src/components/app-layout.tsx | 1 - src/components/dashboard-customizer.tsx | 112 ------ src/components/data-table.tsx | 198 ---------- src/components/ui/accordion.tsx | 66 ---- src/components/ui/aspect-ratio.tsx | 11 - src/components/ui/calendar.tsx | 213 ----------- src/components/ui/carousel.tsx | 241 ------------ src/components/ui/chart.tsx | 351 ----------------- src/components/ui/form.tsx | 167 -------- src/components/ui/hover-card.tsx | 44 --- src/components/ui/input-otp.tsx | 77 ---- src/components/ui/menubar.tsx | 276 -------------- src/components/ui/navigation-menu.tsx | 168 -------- src/components/ui/pagination.tsx | 127 ------- src/components/ui/radio-group.tsx | 45 --- src/components/ui/resizable.tsx | 56 --- src/components/ui/toaster.tsx | 35 -- src/components/ui/toggle-group.tsx | 73 ---- src/components/unified-communication.tsx | 58 --- src/components/widget-container.tsx | 84 ---- src/lib/store.ts | 1 - 37 files changed, 5279 deletions(-) delete mode 100755 src/app/api/ai-grading-audit/[id]/route.ts delete mode 100755 src/app/api/ai-grading-audit/route.ts delete mode 100755 src/app/api/demo-data/route.ts delete mode 100755 src/app/api/exam-plans/[id]/route.ts delete mode 100755 src/app/api/exam-plans/route.ts delete mode 100644 src/app/api/grade-analytics/comparison/route.ts delete mode 100644 src/app/api/grade-analytics/distribution/route.ts delete mode 100644 src/app/api/grade-analytics/reports/route.ts delete mode 100644 src/app/api/grade-analytics/route.ts delete mode 100644 src/app/api/grade-analytics/trends/route.ts delete mode 100755 src/app/api/grading-annotations/[id]/route.ts delete mode 100755 src/app/api/grading-annotations/route.ts delete mode 100755 src/app/api/grading-reviews/[id]/comments/route.ts delete mode 100755 src/app/api/grading-reviews/[id]/route.ts delete mode 100755 src/app/api/grading-reviews/route.ts delete mode 100644 src/components/dashboard-customizer.tsx delete mode 100644 src/components/data-table.tsx delete mode 100755 src/components/ui/accordion.tsx delete mode 100755 src/components/ui/aspect-ratio.tsx delete mode 100755 src/components/ui/calendar.tsx delete mode 100755 src/components/ui/carousel.tsx delete mode 100755 src/components/ui/chart.tsx delete mode 100755 src/components/ui/form.tsx delete mode 100755 src/components/ui/hover-card.tsx delete mode 100755 src/components/ui/input-otp.tsx delete mode 100755 src/components/ui/menubar.tsx delete mode 100755 src/components/ui/navigation-menu.tsx delete mode 100755 src/components/ui/pagination.tsx delete mode 100755 src/components/ui/radio-group.tsx delete mode 100755 src/components/ui/resizable.tsx delete mode 100755 src/components/ui/toaster.tsx delete mode 100755 src/components/ui/toggle-group.tsx delete mode 100644 src/components/unified-communication.tsx delete mode 100644 src/components/widget-container.tsx diff --git a/prisma/schema.prisma b/prisma/schema.prisma index dece4fc..b5bdb0a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -35,9 +35,6 @@ model School { phone String? // school phone number users User[] - schoolPolicies SchoolPolicy[] - escalationPolicies EscalationPolicy[] - classMessagingLocks ClassMessagingLock[] schoolYears SchoolYear[] classGroups ClassGroup[] students Student[] @@ -126,19 +123,12 @@ model School { complianceStatus SchoolComplianceStatus? deletionFlags DeletionFlag[] // Module B signage relations - emergencySignages EmergencySignage[] - signageMessages SignageMessage[] - signageSchedules SignageSchedule[] - signageAuditLogs SignageAuditLog[] // Module F exam mode relations examSessions ExamSession[] // Module H notifications relations - notificationHubs NotificationHub[] - notificationArchives NotificationArchive[] // Module J widgets relations dashboardWidgets DashboardWidget[] dashboardLayouts DashboardLayout[] - widgetTemplates WidgetTemplate[] } model User { @@ -240,15 +230,10 @@ model User { complianceConsultationsAcknowledged ComplianceConsultation[] @relation("ComplianceConsultationAcknowledger") deletionFlagsApproved DeletionFlag[] @relation("DeletionFlagApprover") // Module B signage relations - signageMessagesCreated SignageMessage[] // Module H notifications relations - notificationHubs NotificationHub[] - notificationArchives NotificationArchive[] // Module J widgets relations dashboardWidgets DashboardWidget[] dashboardLayouts DashboardLayout[] - escalationPoliciesOverridden EscalationPolicy[] @relation("EscalationOverriddenBy") - classMessagingLocksLocked ClassMessagingLock[] @relation("ClassMessagingLockLockedBy") passwordResetTokens PasswordResetToken[] twoFactorRecoveryCodes TwoFactorRecoveryCode[] } @@ -338,7 +323,6 @@ model ClassGroup { examPlans ExamPlan[] seatingCharts SeatingChart[] substitutionAssignments SubstitutionAssignment[] - classMessagingLocks ClassMessagingLock[] } model ClassGroupTeacher { @@ -3439,99 +3423,6 @@ model DeletionFlag { // ─── Module B: Emergency Signage System ───────────────────────────── -model EmergencySignage { - id String @id @default(cuid()) - schoolId String - displayName String // Room name or location (e.g., "Main Hallway", "Cafeteria TV 1") - signageType String @default("digital_display") // digital_display, led_board, tv_monitor - resolution String @default("1920x1080") // resolution info - location String // Physical location in school - isActive Boolean @default(true) - currentMessageId String? // Current active message - brightness Int @default(100) // 1-100 - volume Int @default(50) // 1-100 for audio - ipAddress String? // For network monitoring - lastHeartbeat DateTime? // Last connection check - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - school School @relation(fields: [schoolId], references: [id]) - messages SignageMessage[] - schedules SignageSchedule[] - - @@index([schoolId]) - @@index([isActive]) - @@map("emergency_signages") -} - -model SignageMessage { - id String @id @default(cuid()) - schoolId String - signageId String - title String - content String // HTML or plain text - messageType String // emergency, notification, schedule, announcement, alert - priority String @default("normal") // low, normal, high, critical - backgroundColor String? // Hex color - textColor String? // Hex color - displayDuration Int @default(10) // seconds to display - soundAlert Boolean @default(false) // Play sound on emergency - soundFile String? // URL to audio file - createdBy String // userId who created - createdAt DateTime @default(now()) - startTime DateTime? - endTime DateTime? - displayOrder Int @default(0) - - school School @relation(fields: [schoolId], references: [id]) - signage EmergencySignage @relation(fields: [signageId], references: [id], onDelete: Cascade) - creator User @relation(fields: [createdBy], references: [id]) - audits SignageAuditLog[] - - @@index([schoolId]) - @@index([signageId]) - @@index([messageType]) - @@index([priority]) - @@map("signage_messages") -} - -model SignageSchedule { - id String @id @default(cuid()) - schoolId String - signageId String - messageId String? // Optional: specific message to display - dayOfWeek Int // 0-6 (Sunday-Saturday) - startTime String // HH:mm format - endTime String // HH:mm format - isRecurring Boolean @default(true) - createdBy String // userId - createdAt DateTime @default(now()) - - school School @relation(fields: [schoolId], references: [id]) - signage EmergencySignage @relation(fields: [signageId], references: [id], onDelete: Cascade) - - @@index([schoolId]) - @@index([signageId]) - @@map("signage_schedules") -} - -model SignageAuditLog { - id String @id @default(cuid()) - schoolId String - messageId String - action String // created, updated, displayed, error, offline - details String? // JSON: { status, device, error, etc } - createdAt DateTime @default(now()) - - school School @relation(fields: [schoolId], references: [id]) - message SignageMessage @relation(fields: [messageId], references: [id], onDelete: Cascade) - - @@index([schoolId]) - @@index([messageId]) - @@index([action]) - @@map("signage_audit_logs") -} - // ─── Module F: Exam Mode / Digital Test Environment ────────────────── model ExamSession { @@ -3618,55 +3509,6 @@ model ExamWarning { // ─── Module H: Centralized Notifications Hub ─────────────────────── -model NotificationHub { - id String @id @default(cuid()) - schoolId String - userId String - category String // assessment, behavior, communication, wellness, system, announcement - priority String @default("normal") // low, normal, high, critical - title String - message String - actionUrl String? // Link to related resource - actionLabel String? // e.g. "View Grade", "Respond" - isRead Boolean @default(false) - readAt DateTime? - isDismissed Boolean @default(false) - dismissedAt DateTime? - sourceType String? // assessment, incident, message, announcement, etc - sourceId String? // ID of related entity - createdAt DateTime @default(now()) - expiresAt DateTime? // Auto-archive if set - - school School @relation(fields: [schoolId], references: [id]) - user User @relation(fields: [userId], references: [id]) - - @@index([schoolId]) - @@index([userId]) - @@index([isRead]) - @@index([category]) - @@index([createdAt]) - @@map("notification_hubs") -} - -model NotificationArchive { - id String @id @default(cuid()) - schoolId String - userId String - originalId String? // reference to NotificationHub.id if archived - title String - message String - category String - archivedAt DateTime @default(now()) - - school School @relation(fields: [schoolId], references: [id]) - user User @relation(fields: [userId], references: [id]) - - @@index([schoolId]) - @@index([userId]) - @@index([archivedAt]) - @@map("notification_archives") -} - // ─── Module J: Customizable Dashboard Widgets ────────────────────── model DashboardWidget { @@ -3716,76 +3558,3 @@ model DashboardLayout { @@map("dashboard_layouts") } -model WidgetTemplate { - id String @id @default(cuid()) - schoolId String - widgetType String // Same types as DashboardWidget - name String - description String? - defaultConfig String // JSON: default configuration - thumbnail String? // URL to preview image - createdAt DateTime @default(now()) - - school School @relation(fields: [schoolId], references: [id]) - - @@unique([schoolId, widgetType]) - @@index([schoolId]) - @@map("widget_templates") -} - -model SchoolPolicy { - id String @id @default(cuid()) - schoolId String - policyType String // ESCALATION, CLASS_LOCKING, etc. - policyData String // JSON: policy configuration - isActive Boolean @default(true) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - school School @relation(fields: [schoolId], references: [id]) - - @@index([schoolId]) - @@map("school_policies") -} - -model EscalationPolicy { - id String @id @default(cuid()) - schoolId String - fromRole String // PARENT, TEACHER, ADMIN, etc. - toRole String // ADMIN, TEACHER, etc. - requiredPriorContactRole String // TEACHER, etc. - waitingPeriodBusinessDays Int @default(3) - isActive Boolean @default(true) - overriddenByUserId String? // nullable, admin-configured exceptions - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - school School @relation(fields: [schoolId], references: [id]) - overriddenByUser User? @relation("EscalationOverriddenBy", fields: [overriddenByUserId], references: [id]) - - @@index([schoolId]) - @@index([fromRole, toRole]) - @@map("escalation_policies") -} - -model ClassMessagingLock { - id String @id @default(cuid()) - classGroupId String - schoolId String - lockedByUserId String // teacher who locked - lockedFrom DateTime - lockedUntil DateTime? // nullable = "until class ends" - reason String? // optional reason for lock - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - classGroup ClassGroup @relation(fields: [classGroupId], references: [id]) - school School @relation(fields: [schoolId], references: [id]) - lockedByUser User @relation("ClassMessagingLockLockedBy", fields: [lockedByUserId], references: [id]) - - @@index([classGroupId]) - @@index([schoolId]) - @@index([lockedByUserId]) - @@index([lockedFrom, lockedUntil]) - @@map("class_messaging_locks") -} diff --git a/src/app/api/ai-grading-audit/[id]/route.ts b/src/app/api/ai-grading-audit/[id]/route.ts deleted file mode 100755 index 9f9085a..0000000 --- a/src/app/api/ai-grading-audit/[id]/route.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { NextResponse } from 'next/server'; -import { db } from '@/lib/db'; -import { getSession } from '@/lib/auth'; - -export async function GET( - _request: Request, - { params }: { params: Promise<{ id: string }> } -) { - try { - const session = await getSession(); - if (!session) { - return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); - } - - const { id } = await params; - const review = await db.teacherGradingReview.findUnique({ - where: { id }, - include: { - assessment: { select: { id: true, title: true } }, - teacher: { select: { id: true, firstName: true, lastName: true } }, - comments: { - include: { - user: { select: { id: true, firstName: true, lastName: true } }, - }, - orderBy: { createdAt: 'desc' }, - }, - }, - }); - - if (!review) { - return NextResponse.json({ error: 'Not found' }, { status: 404 }); - } - - return NextResponse.json(review); - } catch (error) { - console.error('AI Grading Audit GET [id] error:', error); - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); - } -} diff --git a/src/app/api/ai-grading-audit/route.ts b/src/app/api/ai-grading-audit/route.ts deleted file mode 100755 index 2228f23..0000000 --- a/src/app/api/ai-grading-audit/route.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { NextResponse } from 'next/server'; -import { db } from '@/lib/db'; -import { getSession } from '@/lib/auth'; -import ZAI from 'z-ai-web-dev-sdk'; - -export async function POST(request: Request) { - try { - const session = await getSession(); - if (!session) { - return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); - } - - const role = session.user?.role; - if (role !== 'TEACHER' && role !== 'SCHOOL_ADMIN' && role !== 'SUPER_ADMIN' && role !== 'VICE_PRINCIPAL') { - return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); - } - - const body = await request.json(); - const { schoolId, assessmentId, studentId } = body; - - if (!schoolId || !assessmentId || !studentId) { - return NextResponse.json({ error: 'schoolId, assessmentId, and studentId are required' }, { status: 400 }); - } - - // Get assessment and student data - const assessment = await db.assessment.findUnique({ - where: { id: assessmentId }, - include: { - assessmentResults: { - where: { studentId }, - include: { - student: { select: { id: true, firstName: true, lastName: true } }, - }, - }, - }, - }); - - if (!assessment) { - return NextResponse.json({ error: 'Assessment not found' }, { status: 404 }); - } - - // Get annotations for this student's submission - const annotations = await db.gradingAnnotation.findMany({ - where: { assessmentId, studentId }, - orderBy: { createdAt: 'asc' }, - }); - - const studentResult = assessment.assessmentResults[0]; - const student = studentResult?.student; - - // Generate AI grading audit - const systemPrompt = `Du bist ein erfahrener Lehrer und Prüfer an einer deutschen Schule. -Du überprüfst die Benotung eines Schülers und gibst detailliertes Feedback. -Die deutsche Notenskala: 1 (sehr gut), 2 (gut), 3 (befriedigend), 4 (ausreichend), 5 (mangelhaft), 6 (ungenügend). -Analysiere die Leistung und die gegebene Benotung. Wenn die Benotung nicht angemessen ist, schlage eine Korrektur vor mit Begründung. -Antworte immer auf Deutsch und im JSON-Format.`; - - const gradingInfo = { - studentName: student ? `${student.firstName} ${student.lastName}` : 'Unknown', - assessmentTitle: assessment.title, - assessmentType: assessment.type, - maxScore: assessment.maxScore, - score: studentResult?.score ?? null, - note: studentResult?.note ?? null, - annotationsCount: annotations.length, - annotationTypes: annotations.map((a: { type: string; content: string | null }) => ({ type: a.type, content: a.content })), - teacherAnnotations: annotations.filter((a: { type: string }) => a.type === 'stamp' || a.type === 'text').map((a: { type: string; content: string | null; color: string }) => ({ type: a.type, content: a.content, color: a.color })), - }; - - let reviewResult: string; - let discrepanciesFound = 0; - - try { - const ai = await ZAI.create(); - const response = await ai.chat.completions.create({ - messages: [ - { role: 'system', content: systemPrompt }, - { - role: 'user', - content: `Überprüfe die Benotung für diesen Schüler und gib Feedback:\n\n${JSON.stringify(gradingInfo, null, 2)}\n\nGib deine Antwort als JSON mit folgenden Feldern:\n- currentGrade: die aktuelle Benotung\n- suggestedGrade: deine vorgeschlagene Note\n- reason: detaillierte Begründung auf Deutsch\n- discrepancies: Liste der gefundenen Unstimmigkeiten\n- overallAssessment: Gesamtbewertung der Benotung`, - }, - ], - }); - - reviewResult = response?.choices?.[0]?.message?.content || 'Review could not be generated'; - } catch (aiError) { - console.error('AI grading audit error:', aiError); - reviewResult = JSON.stringify({ - currentGrade: studentResult?.score ?? 'N/A', - suggestedGrade: 'N/A', - reason: 'AI review could not be generated. Manual review recommended.', - discrepancies: ['AI analysis unavailable — please review manually'], - overallAssessment: 'AI audit could not be completed due to a technical error. Please review the grading manually.', - }); - } - - // Count discrepancies - try { - const parsed = typeof reviewResult === 'string' ? JSON.parse(reviewResult) : reviewResult; - discrepanciesFound = Array.isArray(parsed.discrepancies) ? parsed.discrepancies.length : 0; - } catch { - discrepanciesFound = 0; - } - - // Create or update the grading review - const review = await db.teacherGradingReview.create({ - data: { - schoolId, - assessmentId, - teacherId: session.userId, - aiProvider: 'pollination', - status: 'completed', - reviewResult, - discrepanciesFound, - }, - include: { - assessment: { select: { id: true, title: true } }, - teacher: { select: { id: true, firstName: true, lastName: true } }, - comments: { - include: { - user: { select: { id: true, firstName: true, lastName: true } }, - }, - orderBy: { createdAt: 'desc' }, - }, - }, - }); - - return NextResponse.json(review, { status: 201 }); - } catch (error) { - console.error('AI Grading Audit POST error:', error); - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); - } -} diff --git a/src/app/api/demo-data/route.ts b/src/app/api/demo-data/route.ts deleted file mode 100755 index 0aa5316..0000000 --- a/src/app/api/demo-data/route.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { NextResponse } from 'next/server'; -import { db } from '@/lib/db'; -import { getSession } from '@/lib/auth'; - -// ── GET: Get demo data statistics ── -export async function GET() { - try { - const session = await getSession(); - if (!session?.user) { - return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); - } - - // Only SCHOOL_ADMIN or SUPER_ADMIN can view demo data stats - if (!['SCHOOL_ADMIN', 'SUPER_ADMIN'].includes(session.user.role)) { - return NextResponse.json({ error: 'Access denied' }, { status: 403 }); - } - - // Get demo user IDs for cascade counting - const demoUsers = await db.user.findMany({ - where: { isDemo: true }, - select: { id: true }, - }); - const demoUserIds = demoUsers.map((u) => u.id); - - const [ - demoUserCount, - demoNotebookCount, - demoDrawingCount, - cascadeNotebookCount, - cascadeDrawingCount, - ] = await Promise.all([ - db.user.count({ where: { isDemo: true } }), - db.notebook.count({ where: { isDemo: true } }), - db.drawing.count({ where: { isDemo: true } }), - demoUserIds.length > 0 - ? db.notebook.count({ - where: { ownerId: { in: demoUserIds }, isDemo: false }, - }) - : 0, - demoUserIds.length > 0 - ? db.drawing.count({ - where: { ownerId: { in: demoUserIds }, isDemo: false }, - }) - : 0, - ]); - - return NextResponse.json({ - User: demoUserCount, - Notebook: demoNotebookCount, - Drawing: demoDrawingCount, - _cascadeNotebooks: cascadeNotebookCount, - _cascadeDrawings: cascadeDrawingCount, - _total: - demoUserCount + - demoNotebookCount + - demoDrawingCount + - cascadeNotebookCount + - cascadeDrawingCount, - }); - } catch (error) { - console.error('Demo data stats error:', error); - return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 } - ); - } -} - -// ── DELETE: Delete ALL demo data ── -export async function DELETE() { - try { - const session = await getSession(); - if (!session?.user) { - return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); - } - - // Only SCHOOL_ADMIN or SUPER_ADMIN can delete demo data - if (!['SCHOOL_ADMIN', 'SUPER_ADMIN'].includes(session.user.role)) { - return NextResponse.json({ error: 'Access denied' }, { status: 403 }); - } - - const summary: Record = {}; - - // 1. Get all demo user IDs for cascade deletion - const demoUsers = await db.user.findMany({ - where: { isDemo: true }, - select: { id: true }, - }); - const demoUserIds = demoUsers.map((u) => u.id); - - // 2. Delete NotebookPages for notebooks owned by demo users - // (NotebookPage has cascade delete on Notebook, but we need to handle - // notebooks where isDemo=true OR ownerId is a demo user) - const demoNotebookIds = await db.notebook.findMany({ - where: { - OR: [{ isDemo: true }, { ownerId: { in: demoUserIds } }], - }, - select: { id: true }, - }); - const demoNotebookIdList = demoNotebookIds.map((n) => n.id); - - let deletedPages = 0; - if (demoNotebookIdList.length > 0) { - deletedPages = (await db.notebookPage.deleteMany({ - where: { notebookId: { in: demoNotebookIdList } }, - })) as unknown as number; - summary.NotebookPage = typeof deletedPages === 'number' ? deletedPages : (deletedPages as { count: number }).count ?? 0; - } else { - summary.NotebookPage = 0; - } - - // 3. Delete notebooks where isDemo=true OR ownerId is a demo user - const deletedNotebooks = await db.notebook.deleteMany({ - where: { - OR: [{ isDemo: true }, { ownerId: { in: demoUserIds } }], - }, - }); - summary.Notebook = - typeof deletedNotebooks === 'number' - ? deletedNotebooks - : (deletedNotebooks as { count: number }).count ?? 0; - - // 4. Delete drawings where isDemo=true OR ownerId is a demo user - const deletedDrawings = await db.drawing.deleteMany({ - where: { - OR: [{ isDemo: true }, { ownerId: { in: demoUserIds } }], - }, - }); - summary.Drawing = - typeof deletedDrawings === 'number' - ? deletedDrawings - : (deletedDrawings as { count: number }).count ?? 0; - - // 5. Delete demo users (soft-deleted users that are demo) - const deletedUsers = await db.user.deleteMany({ - where: { isDemo: true }, - }); - summary.User = - typeof deletedUsers === 'number' - ? deletedUsers - : (deletedUsers as { count: number }).count ?? 0; - - // 6. Log the cleanup in AuditLog - await db.auditLog.create({ - data: { - userId: session.user.id, - schoolId: session.user.schoolId, - action: 'DEMO_DATA_CLEANUP', - entityType: 'System', - metadata: JSON.stringify(summary), - }, - }); - - return NextResponse.json({ - message: 'Demo data cleanup completed', - deleted: summary, - }); - } catch (error) { - console.error('Demo data cleanup error:', error); - return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 } - ); - } -} diff --git a/src/app/api/exam-plans/[id]/route.ts b/src/app/api/exam-plans/[id]/route.ts deleted file mode 100755 index 24d4f86..0000000 --- a/src/app/api/exam-plans/[id]/route.ts +++ /dev/null @@ -1,200 +0,0 @@ -import { NextResponse } from 'next/server'; -import { db } from '@/lib/db'; -import { getSession } from '@/lib/auth'; - -// ── GET: Get single exam plan ────────────────────────────────────────── -export async function GET( - _request: Request, - { params }: { params: Promise<{ id: string }> } -) { - try { - const session = await getSession(); - if (!session) { - return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); - } - - const { id } = await params; - - const examPlan = await db.examPlan.findUnique({ - where: { id }, - include: { - subject: { select: { id: true, name: true } }, - classGroup: { select: { id: true, name: true, gradeLevel: true } }, - teacher: { select: { id: true, firstName: true, lastName: true } }, - calendarEvent: { select: { id: true } }, - assessment: { select: { id: true, title: true } }, - }, - }); - - if (!examPlan) { - return NextResponse.json({ error: 'Exam plan not found' }, { status: 404 }); - } - - // Role-based access check - if ( - session.user?.role === 'STUDENT' || - session.user?.role === 'PARENT' - ) { - // Students and parents can view but not modify - } else if (session.user?.role === 'TEACHER') { - // Teachers can only view their own exams - if (examPlan.teacherId !== session.userId) { - return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); - } - } - - return NextResponse.json(examPlan); - } catch (error) { - console.error('ExamPlan GET error:', error); - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); - } -} - -// ── PUT: Update exam plan ────────────────────────────────────────────── -export async function PUT( - request: Request, - { params }: { params: Promise<{ id: string }> } -) { - try { - const session = await getSession(); - if (!session) { - return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); - } - - const { id } = await params; - const userRole = session.user?.role; - - if ( - userRole !== 'TEACHER' && - userRole !== 'SCHOOL_ADMIN' && - userRole !== 'VICE_PRINCIPAL' && - userRole !== 'SUPER_ADMIN' - ) { - return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); - } - - const existing = await db.examPlan.findUnique({ where: { id } }); - if (!existing) { - return NextResponse.json({ error: 'Exam plan not found' }, { status: 404 }); - } - - // Teachers can only edit their own exams - if (userRole === 'TEACHER' && existing.teacherId !== session.userId) { - return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); - } - - const body = await request.json(); - const { - title, - date, - startTime, - endTime, - subjectId, - classGroupId, - room, - topics, - weight, - status, - notes, - } = body; - - const updateData: Record = {}; - if (title !== undefined) updateData.title = title; - if (date !== undefined) updateData.date = new Date(date); - if (startTime !== undefined) updateData.startTime = startTime || null; - if (endTime !== undefined) updateData.endTime = endTime || null; - if (subjectId !== undefined) updateData.subjectId = subjectId; - if (classGroupId !== undefined) updateData.classGroupId = classGroupId; - if (room !== undefined) updateData.room = room || null; - if (topics !== undefined) updateData.topics = topics ? JSON.stringify(topics) : null; - if (weight !== undefined) updateData.weight = weight; - if (status !== undefined) updateData.status = status; - if (notes !== undefined) updateData.notes = notes || null; - - const updated = await db.examPlan.update({ - where: { id }, - data: updateData, - include: { - subject: { select: { id: true, name: true } }, - classGroup: { select: { id: true, name: true, gradeLevel: true } }, - teacher: { select: { id: true, firstName: true, lastName: true } }, - calendarEvent: { select: { id: true } }, - assessment: { select: { id: true, title: true } }, - }, - }); - - // Also update the linked CalendarEvent - if (existing.calendarEventId) { - const calendarUpdateData: Record = {}; - if (title !== undefined) calendarUpdateData.title = `[Exam] ${title}`; - if (date !== undefined) calendarUpdateData.date = new Date(date); - if (startTime !== undefined) calendarUpdateData.startTime = startTime || null; - if (endTime !== undefined) calendarUpdateData.endTime = endTime || null; - if (subjectId !== undefined) calendarUpdateData.subjectId = subjectId; - if (classGroupId !== undefined) calendarUpdateData.classGroupId = classGroupId; - if (notes !== undefined) calendarUpdateData.notes = notes || null; - if (startTime !== undefined || endTime !== undefined) { - calendarUpdateData.allDay = !startTime && !endTime; - } - - if (Object.keys(calendarUpdateData).length > 0) { - await db.calendarEvent.update({ - where: { id: existing.calendarEventId }, - data: calendarUpdateData, - }); - } - } - - return NextResponse.json(updated); - } catch (error) { - console.error('ExamPlan PUT error:', error); - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); - } -} - -// ── DELETE: Delete exam plan ─────────────────────────────────────────── -export async function DELETE( - _request: Request, - { params }: { params: Promise<{ id: string }> } -) { - try { - const session = await getSession(); - if (!session) { - return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); - } - - const { id } = await params; - const userRole = session.user?.role; - - if ( - userRole !== 'TEACHER' && - userRole !== 'SCHOOL_ADMIN' && - userRole !== 'VICE_PRINCIPAL' && - userRole !== 'SUPER_ADMIN' - ) { - return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); - } - - const existing = await db.examPlan.findUnique({ where: { id } }); - if (!existing) { - return NextResponse.json({ error: 'Exam plan not found' }, { status: 404 }); - } - - // Teachers can only delete their own exams - if (userRole === 'TEACHER' && existing.teacherId !== session.userId) { - return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); - } - - // Delete the linked CalendarEvent first - if (existing.calendarEventId) { - await db.calendarEvent.delete({ where: { id: existing.calendarEventId } }).catch(() => {}); - } - - await db.examPlan.delete({ where: { id } }); - - return NextResponse.json({ success: true }); - } catch (error) { - console.error('ExamPlan DELETE error:', error); - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); - } -} diff --git a/src/app/api/exam-plans/route.ts b/src/app/api/exam-plans/route.ts deleted file mode 100755 index 1b35b4d..0000000 --- a/src/app/api/exam-plans/route.ts +++ /dev/null @@ -1,318 +0,0 @@ -// @ts-nocheck -import { NextResponse } from 'next/server'; -import { db } from '@/lib/db'; -import { getSession } from '@/lib/auth'; -import { differenceInCalendarDays } from 'date-fns'; - -// ── GET: List exam plans with filters ────────────────────────────────── -export async function GET(request: Request) { - try { - const session = await getSession(); - if (!session) { - return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); - } - - const { searchParams } = new URL(request.url); - const schoolId = searchParams.get('schoolId') ?? session.user?.schoolId ?? undefined; - const classId = searchParams.get('classId'); - const subjectId = searchParams.get('subjectId'); - const teacherId = searchParams.get('teacherId'); - const status = searchParams.get('status'); - const dateFrom = searchParams.get('dateFrom'); - const dateTo = searchParams.get('dateTo'); - const viewMode = searchParams.get('viewMode') ?? 'all'; // all, upcoming, past - - if (!schoolId) { - return NextResponse.json({ error: 'schoolId is required' }, { status: 400 }); - } - - const where: Record = { schoolId }; - - if (classId) where.classGroupId = classId; - if (subjectId) where.subjectId = subjectId; - if (teacherId) where.teacherId = teacherId; - if (status) where.status = status; - - // Date filters - const dateFilter: Record = {}; - if (dateFrom) dateFilter.gte = new Date(dateFrom); - if (dateTo) dateFilter.lte = new Date(dateTo); - if (Object.keys(dateFilter).length > 0) where.date = dateFilter; - - // View mode filters - const now = new Date(); - if (viewMode === 'upcoming') { - where.date = { ...((where.date as Record) || {}), gte: now }; - } else if (viewMode === 'past') { - where.date = { ...((where.date as Record) || {}), lt: now }; - } - - // Role-based access - if (session.user?.role === 'STUDENT') { - const student = await db.student.findFirst({ - where: { userId: session.userId }, - select: { id: true }, - }); - if (student) { - const enrollments = await db.enrollment.findMany({ - where: { studentId: student.id }, - select: { classGroupId: true }, - }); - const classIds = enrollments.map((e) => e.classGroupId); - where.classGroupId = { in: classIds }; - } - } else if (session.user?.role === 'PARENT') { - const parentLinks = await db.parentStudentLink.findMany({ - where: { parentId: session.userId }, - select: { studentId: true }, - }); - const studentIds = parentLinks.map((l) => l.studentId); - const enrollments = await db.enrollment.findMany({ - where: { studentId: { in: studentIds } }, - select: { classGroupId: true }, - }); - const classIds = enrollments.map((e) => e.classGroupId); - where.classGroupId = { in: classIds }; - } else if (session.user?.role === 'TEACHER') { - // Teachers see their own exams by default, or all if explicitly requested - if (!teacherId) { - where.teacherId = session.userId; - } - } - - const examPlans = await db.examPlan.findMany({ - where, - include: { - subject: { select: { id: true, name: true } }, - classGroup: { select: { id: true, name: true, gradeLevel: true } }, - teacher: { select: { id: true, firstName: true, lastName: true } }, - calendarEvent: { select: { id: true } }, - assessment: { select: { id: true, title: true } }, - }, - orderBy: { date: 'asc' }, - }); - - // Add computed fields - const enriched = examPlans.map((plan) => { - const daysUntil = differenceInCalendarDays(new Date(plan.date), now); - const isWithinTwoWeeks = daysUntil >= 0 && daysUntil <= 14; - return { - ...plan, - daysUntil, - isWithinTwoWeeks, - countdownLabel: - daysUntil === 0 - ? 'today' - : daysUntil === 1 - ? 'tomorrow' - : daysUntil < 7 - ? `${daysUntil} days` - : daysUntil < 14 - ? `${Math.floor(daysUntil / 7)} week` - : `${Math.floor(daysUntil / 7)} weeks`, - }; - }); - - // Check for AI test availability for each exam - const examIdsWithTests = await db.aITestGeneration.findMany({ - where: { - classGroupId: { in: enriched.map((e) => e.classGroupId) }, - subjectId: { in: enriched.map((e) => e.subjectId) }, - isCompleted: true, - }, - select: { classGroupId: true, subjectId: true }, - }); - - const testSet = new Set( - examIdsWithTests.map((t) => `${t.classGroupId}-${t.subjectId}`) - ); - - const result = enriched.map((plan) => ({ - ...plan, - hasAITest: testSet.has(`${plan.classGroupId}-${plan.subjectId}`), - })); - - // Statistics (for admin view) - let stats = null; - if ( - session.user?.role === 'SCHOOL_ADMIN' || - session.user?.role === 'VICE_PRINCIPAL' || - session.user?.role === 'SUPER_ADMIN' - ) { - const totalExams = await db.examPlan.count({ where: { schoolId } }); - const upcomingExams = await db.examPlan.count({ - where: { schoolId, date: { gte: now } }, - }); - const plannedExams = await db.examPlan.count({ - where: { schoolId, status: 'planned' }, - }); - const confirmedExams = await db.examPlan.count({ - where: { schoolId, status: 'confirmed' }, - }); - const completedExams = await db.examPlan.count({ - where: { schoolId, status: 'completed' }, - }); - const cancelledExams = await db.examPlan.count({ - where: { schoolId, status: 'cancelled' }, - }); - - // Exams per subject - const examsPerSubject = await db.examPlan.groupBy({ - by: ['subjectId'], - where: { schoolId }, - _count: { id: true }, - }); - - const subjectNames = await db.subject.findMany({ - where: { - id: { in: examsPerSubject.map((e) => e.subjectId) }, - }, - select: { id: true, name: true }, - }); - - const subjectMap = Object.fromEntries( - subjectNames.map((s) => [s.id, s.name]) - ); - - // Exams per class - const examsPerClass = await db.examPlan.groupBy({ - by: ['classGroupId'], - where: { schoolId }, - _count: { id: true }, - }); - - const classNames = await db.classGroup.findMany({ - where: { - id: { in: examsPerClass.map((e) => e.classGroupId) }, - }, - select: { id: true, name: true }, - }); - - const classMap = Object.fromEntries( - classNames.map((c) => [c.id, c.name]) - ); - - stats = { - totalExams, - upcomingExams, - byStatus: { planned: plannedExams, confirmed: confirmedExams, completed: completedExams, cancelled: cancelledExams }, - bySubject: examsPerSubject.map((e) => ({ - subjectId: e.subjectId, - subjectName: subjectMap[e.subjectId] || e.subjectId, - count: e._count.id, - })), - byClass: examsPerClass.map((e) => ({ - classGroupId: e.classGroupId, - className: classMap[e.classGroupId] || e.classGroupId, - count: e._count.id, - })), - }; - } - - return NextResponse.json({ exams: result, stats }); - } catch (error) { - console.error('ExamPlans GET error:', error); - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); - } -} - -// ── POST: Create an exam plan ────────────────────────────────────────── -export async function POST(request: Request) { - try { - const session = await getSession(); - if (!session) { - return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); - } - - const userRole = session.user?.role; - if ( - userRole !== 'TEACHER' && - userRole !== 'SCHOOL_ADMIN' && - userRole !== 'VICE_PRINCIPAL' && - userRole !== 'SUPER_ADMIN' - ) { - return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); - } - - const body = await request.json(); - const { - title, - date, - startTime, - endTime, - subjectId, - classGroupId, - schoolId, - room, - topics, - weight, - status, - notes, - } = body; - - if (!title || !date || !schoolId || !subjectId || !classGroupId) { - return NextResponse.json( - { error: 'title, date, schoolId, subjectId, and classGroupId are required' }, - { status: 400 } - ); - } - - // Create the exam plan - const examPlan = await db.examPlan.create({ - data: { - schoolId, - teacherId: session.userId, - subjectId, - classGroupId, - title, - date: new Date(date), - startTime: startTime || null, - endTime: endTime || null, - room: room || null, - topics: topics ? JSON.stringify(topics) : null, - weight: weight ?? 1.0, - status: status || 'planned', - notes: notes || null, - }, - include: { - subject: { select: { id: true, name: true } }, - classGroup: { select: { id: true, name: true, gradeLevel: true } }, - teacher: { select: { id: true, firstName: true, lastName: true } }, - }, - }); - - // Also create a CalendarEvent for the exam - const calendarEvent = await db.calendarEvent.create({ - data: { - schoolId, - teacherId: session.userId, - title: `[Exam] ${title}`, - date: new Date(date), - startTime: startTime || null, - endTime: endTime || null, - eventType: 'exam', - subjectId, - classGroupId, - notes: notes || null, - allDay: !startTime && !endTime, - }, - }); - - // Link the calendar event to the exam plan - await db.examPlan.update({ - where: { id: examPlan.id }, - data: { calendarEventId: calendarEvent.id }, - }); - - const result = { - ...examPlan, - calendarEventId: calendarEvent.id, - }; - - return NextResponse.json(result, { status: 201 }); - } catch (error) { - console.error('ExamPlans POST error:', error); - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); - } -} -// @ts-nocheck diff --git a/src/app/api/grade-analytics/comparison/route.ts b/src/app/api/grade-analytics/comparison/route.ts deleted file mode 100644 index 5499f29..0000000 --- a/src/app/api/grade-analytics/comparison/route.ts +++ /dev/null @@ -1,358 +0,0 @@ -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 ?? undefined; - const schoolYearId = searchParams.get('schoolYearId'); - const compareType = searchParams.get('compareType') ?? 'class'; // class, subject, teacher - - const role = session.user?.role; - const isAdmin = role === 'SUPER_ADMIN' || role === 'SCHOOL_ADMIN' || role === 'VICE_PRINCIPAL'; - - // Only admins can do teacher comparison - if (compareType === 'teacher' && !isAdmin) { - return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }); - } - - // Build where clause - const gradeWhere: Record = {}; - if (schoolYearId) gradeWhere.schoolYearId = schoolYearId; - if (schoolId) { - gradeWhere.classGroup = { schoolId }; - } - - const grades = await db.computedGrade.findMany({ - where: gradeWhere, - select: { - computedValue: true, - overriddenValue: true, - subjectId: true, - classGroupId: true, - studentId: true, - subject: { select: { id: true, name: true } }, - classGroup: { select: { id: true, name: true, gradeLevel: true } }, - }, - }); - - if (compareType === 'class') { - // Class comparison - const classMap: Record = {}; - - grades.forEach((g) => { - const val = g.overriddenValue ?? g.computedValue; - if (!classMap[g.classGroupId]) { - classMap[g.classGroupId] = { - name: g.classGroup.name, - gradeLevel: g.classGroup.gradeLevel, - grades: [], - average: 0, - median: 0, - stdDev: 0, - gradeCount: 0, - }; - } - classMap[g.classGroupId].grades.push(val); - }); - - // Calculate statistics - Object.values(classMap).forEach((c) => { - c.grades.sort((a, b) => a - b); - c.gradeCount = c.grades.length; - c.average = c.grades.length > 0 ? Math.round((c.grades.reduce((s, v) => s + v, 0) / c.grades.length) * 100) / 100 : 0; - - // Median - const mid = Math.floor(c.grades.length / 2); - c.median = c.grades.length > 0 - ? c.grades.length % 2 !== 0 - ? c.grades[mid] - : Math.round(((c.grades[mid - 1] + c.grades[mid]) / 2) * 100) / 100 - : 0; - - // Standard deviation - const variance = c.grades.length > 0 - ? c.grades.reduce((s, v) => s + Math.pow(v - c.average, 2), 0) / c.grades.length - : 0; - c.stdDev = Math.round(Math.sqrt(variance) * 100) / 100; - }); - - // Class ranking (anonymized) - const classRanking = Object.entries(classMap) - .map(([id, data]) => ({ id, ...data })) - .sort((a, b) => a.average - b.average) - .map((c, idx) => ({ - ...c, - rank: idx + 1, - anonymizedName: `Class ${String.fromCharCode(65 + idx)}`, // A, B, C, ... - })); - - // Subject comparison across classes - const subjectByClass: Record> = {}; - grades.forEach((g) => { - const val = g.overriddenValue ?? g.computedValue; - if (!subjectByClass[g.subjectId]) { - subjectByClass[g.subjectId] = {}; - } - if (!subjectByClass[g.subjectId][g.classGroupId]) { - subjectByClass[g.subjectId][g.classGroupId] = { - total: 0, - count: 0, - name: g.classGroup.name, - }; - } - subjectByClass[g.subjectId][g.classGroupId].total += val; - subjectByClass[g.subjectId][g.classGroupId].count += 1; - }); - - // Get subject names - const subjects = await db.subject.findMany({ - where: schoolId ? { schoolId } : {}, - select: { id: true, name: true }, - }); - const subjectNameMap: Record = {}; - subjects.forEach((s) => { subjectNameMap[s.id] = s.name; }); - - const subjectComparison = Object.entries(subjectByClass).map(([subjectId, classData]) => ({ - subjectId, - subjectName: subjectNameMap[subjectId] || subjectId, - classes: Object.entries(classData).map(([classId, data]) => ({ - classId, - className: data.name, - average: data.count > 0 ? Math.round((data.total / data.count) * 100) / 100 : 0, - count: data.count, - })), - })); - - return NextResponse.json({ - type: 'class', - classRanking, - subjectComparison, - total: grades.length, - }); - } - - if (compareType === 'subject') { - // Subject comparison - const subjectMap: Record = {}; - - grades.forEach((g) => { - const val = g.overriddenValue ?? g.computedValue; - if (!subjectMap[g.subjectId]) { - subjectMap[g.subjectId] = { - name: g.subject.name, - grades: [], - average: 0, - variance: 0, - gradeCount: 0, - }; - } - subjectMap[g.subjectId].grades.push(val); - }); - - Object.values(subjectMap).forEach((s) => { - s.gradeCount = s.grades.length; - s.average = s.grades.length > 0 ? Math.round((s.grades.reduce((sum, v) => sum + v, 0) / s.grades.length) * 100) / 100 : 0; - const variance = s.grades.length > 0 - ? s.grades.reduce((sum, v) => sum + Math.pow(v - s.average, 2), 0) / s.grades.length - : 0; - s.variance = Math.round(variance * 100) / 100; - }); - - // Cross-subject correlation - const subjectIds = Object.keys(subjectMap); - const studentSubjectGrades: Record> = {}; - grades.forEach((g) => { - const val = g.overriddenValue ?? g.computedValue; - if (!studentSubjectGrades[g.studentId]) { - studentSubjectGrades[g.studentId] = {}; - } - if (!studentSubjectGrades[g.studentId][g.subjectId]) { - studentSubjectGrades[g.studentId][g.subjectId] = []; - } - studentSubjectGrades[g.studentId][g.subjectId].push(val); - }); - - // Calculate correlation matrix - const correlationMatrix: Array<{ - subject1: string; - subject2: string; - name1: string; - name2: string; - correlation: number; - }> = []; - - for (let i = 0; i < subjectIds.length; i++) { - for (let j = i; j < subjectIds.length; j++) { - const s1 = subjectIds[i]; - const s2 = subjectIds[j]; - const pairs: Array<{ x: number; y: number }> = []; - - Object.entries(studentSubjectGrades).forEach(([, subjects]) => { - const g1 = subjects[s1]; - const g2 = subjects[s2]; - if (g1 && g2 && g1.length > 0 && g2.length > 0) { - pairs.push({ - x: g1.reduce((a, b) => a + b, 0) / g1.length, - y: g2.reduce((a, b) => a + b, 0) / g2.length, - }); - } - }); - - if (pairs.length >= 3) { - const n = pairs.length; - const sumX = pairs.reduce((s, p) => s + p.x, 0); - const sumY = pairs.reduce((s, p) => s + p.y, 0); - const sumXY = pairs.reduce((s, p) => s + p.x * p.y, 0); - const sumX2 = pairs.reduce((s, p) => s + p.x * p.x, 0); - const sumY2 = pairs.reduce((s, p) => s + p.y * p.y, 0); - - const numerator = n * sumXY - sumX * sumY; - const denominator = Math.sqrt((n * sumX2 - sumX * sumX) * (n * sumY2 - sumY * sumY)); - const correlation = denominator !== 0 ? Math.round((numerator / denominator) * 100) / 100 : 0; - - correlationMatrix.push({ - subject1: s1, - subject2: s2, - name1: subjectMap[s1].name, - name2: subjectMap[s2].name, - correlation, - }); - } - } - } - - // Subject difficulty ranking - const subjectRanking = Object.entries(subjectMap) - .map(([id, data]) => ({ - id, - name: data.name, - average: data.average, - variance: data.variance, - gradeCount: data.gradeCount, - difficulty: data.average <= 2 ? 'easy' : data.average <= 3 ? 'moderate' : data.average <= 4 ? 'challenging' : 'hard', - })) - .sort((a, b) => b.average - a.average); - - // Subject recommendations - const recommendations = subjectRanking.map((s) => { - const recs: string[] = []; - if (s.average >= 4.5) { - recs.push('Consider additional support materials and tutoring'); - recs.push('Review curriculum difficulty and pacing'); - } else if (s.average >= 3.5) { - recs.push('Monitor student progress closely'); - recs.push('Consider differentiated instruction'); - } else { - recs.push('Performance is on track'); - recs.push('Continue current teaching approach'); - } - if (s.variance > 1.5) { - recs.push('High variance suggests need for differentiated instruction'); - } - return { subjectId: s.id, subjectName: s.name, recommendations: recs }; - }); - - return NextResponse.json({ - type: 'subject', - subjectRanking, - correlationMatrix, - recommendations, - total: grades.length, - }); - } - - if (compareType === 'teacher') { - // Teacher comparison (admin only) - if (!isAdmin) { - return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }); - } - - const teacherClasses = await db.classGroupTeacher.findMany({ - where: schoolId ? { classGroup: { schoolId } } : {}, - select: { - userId: true, - role: true, - user: { select: { id: true, firstName: true, lastName: true } }, - classGroup: { select: { id: true, name: true } }, - }, - }); - - const teacherMap: Record = {}; - - teacherClasses.forEach((tc) => { - if (!teacherMap[tc.userId]) { - teacherMap[tc.userId] = { - name: `${tc.user.firstName} ${tc.user.lastName}`, - classIds: [], - grades: [], - average: 0, - classCount: 0, - gradeCount: 0, - }; - } - if (!teacherMap[tc.userId].classIds.includes(tc.classGroup.id)) { - teacherMap[tc.userId].classIds.push(tc.classGroup.id); - } - }); - - // Assign grades to teachers based on class - grades.forEach((g) => { - Object.entries(teacherMap).forEach(([, teacher]) => { - if (teacher.classIds.includes(g.classGroupId)) { - teacher.grades.push(g.overriddenValue ?? g.computedValue); - } - }); - }); - - // Calculate teacher stats - Object.values(teacherMap).forEach((t) => { - t.gradeCount = t.grades.length; - t.classCount = t.classIds.length; - t.average = t.grades.length > 0 ? Math.round((t.grades.reduce((s, v) => s + v, 0) / t.grades.length) * 100) / 100 : 0; - }); - - const teacherRanking = Object.entries(teacherMap) - .map(([id, data]) => ({ id, ...data })) - .sort((a, b) => a.average - b.average); - - return NextResponse.json({ - type: 'teacher', - teacherRanking, - total: grades.length, - }); - } - - return NextResponse.json({ error: 'Invalid compareType parameter' }, { status: 400 }); - } catch (error) { - console.error('Grade comparison error:', error); - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); - } -} diff --git a/src/app/api/grade-analytics/distribution/route.ts b/src/app/api/grade-analytics/distribution/route.ts deleted file mode 100644 index a44e9a3..0000000 --- a/src/app/api/grade-analytics/distribution/route.ts +++ /dev/null @@ -1,184 +0,0 @@ -// @ts-nocheck -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 ?? undefined; - const classGroupId = searchParams.get('classGroupId'); - const subjectId = searchParams.get('subjectId'); - const schoolYearId = searchParams.get('schoolYearId'); - const groupBy = searchParams.get('groupBy') ?? 'overall'; // overall, subject, class - - const role = session.user?.role; - const isStudent = role === 'STUDENT'; - const isParent = role === 'PARENT'; - - // Build where clause - const gradeWhere: Record = {}; - if (classGroupId) gradeWhere.classGroupId = classGroupId; - if (subjectId) gradeWhere.subjectId = subjectId; - if (schoolYearId) gradeWhere.schoolYearId = schoolYearId; - - if (schoolId) { - gradeWhere.classGroup = { schoolId }; - } - - // Role-based scoping - if (isStudent && session.user?.id) { - const student = await db.student.findFirst({ - where: { userId: session.user.id, deletedAt: null }, - }); - if (student) gradeWhere.studentId = student.id; - } - - if (isParent && session.user?.id) { - const parentLinks = await db.parentStudentLink.findMany({ - where: { parentId: session.user.id }, - select: { studentId: true }, - }); - const studentIds = parentLinks.map((l) => l.studentId); - gradeWhere.studentId = { in: studentIds }; - } - - // Get all grades with subject and class info - const grades = await db.computedGrade.findMany({ - where: gradeWhere, - select: { - computedValue: true, - overriddenValue: true, - subjectId: true, - classGroupId: true, - studentId: true, - subject: { select: { id: true, name: true } }, - classGroup: { select: { id: true, name: true } }, - }, - }); - - // Grade colors for German 1-6 scale - const gradeColors: Record = { - 1: '#10b981', // emerald - 2: '#22c55e', // green - 3: '#eab308', // yellow - 4: '#f59e0b', // amber - 5: '#f97316', // orange - 6: '#ef4444', // red - }; - - if (groupBy === 'overall') { - // Overall distribution - const distribution: Record = {}; - for (let i = 1; i <= 6; i++) { - distribution[i] = { count: 0, percentage: 0, color: gradeColors[i] }; - } - grades.forEach((g) => { - const val = g.overriddenValue ?? g.computedValue; - const rounded = Math.round(val); - if (rounded >= 1 && rounded <= 6) { - distribution[rounded].count++; - } - }); - const total = grades.length || 1; - Object.values(distribution).forEach((d) => { - d.percentage = Math.round((d.count / total) * 10000) / 100; - }); - - // Bell curve data (normal distribution overlay) - const mean = grades.reduce((s, g) => s + (g.overriddenValue ?? g.computedValue), 0) / (grades.length || 1); - const variance = grades.reduce((s, g) => { - const val = g.overriddenValue ?? g.computedValue; - return s + Math.pow(val - mean, 2); - }, 0) / (grades.length || 1); - const stdDev = Math.sqrt(variance); - - const bellCurveData = []; - for (let x = 0.5; x <= 6.5; x += 0.1) { - const exponent = -Math.pow(x - mean, 2) / (2 * (stdDev * stdDev || 1)); - const y = (1 / (stdDev * Math.sqrt(2 * Math.PI) || 1)) * Math.exp(exponent); - bellCurveData.push({ x: Math.round(x * 10) / 10, y: Math.round(y * 1000) / 1000 }); - } - - return NextResponse.json({ - type: 'overall', - distribution: Object.entries(distribution).map(([grade, data]) => ({ - grade: parseInt(grade), - ...data, - label: grade === '1' ? 'Sehr gut' : grade === '2' ? 'Gut' : grade === '3' ? 'Befriedigend' : grade === '4' ? 'Ausreichend' : grade === '5' ? 'Mangelhaft' : 'Ungenügend', - })), - bellCurve: bellCurveData, - mean: Math.round(mean * 100) / 100, - stdDev: Math.round(stdDev * 100) / 100, - total: grades.length, - }); - } - - if (groupBy === 'subject') { - // Distribution by subject - const subjectMap: Record }> = {}; - grades.forEach((g) => { - const val = g.overriddenValue ?? g.computedValue; - const rounded = Math.round(val); - if (!subjectMap[g.subjectId]) { - subjectMap[g.subjectId] = { name: g.subject.name, distribution: { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0 } }; - } - if (rounded >= 1 && rounded <= 6) { - subjectMap[g.subjectId].distribution[rounded]++; - } - }); - - const subjectDistribution = Object.entries(subjectMap).map(([id, data]) => ({ - id, - name: data.name, - distribution: data.distribution, - })); - - return NextResponse.json({ - type: 'subject', - subjects: subjectDistribution, - gradeColors, - total: grades.length, - }); - } - - if (groupBy === 'class') { - // Distribution by class - const classMap: Record }> = {}; - grades.forEach((g) => { - const val = g.overriddenValue ?? g.computedValue; - const rounded = Math.round(val); - if (!classMap[g.classGroupId]) { - classMap[g.classGroupId] = { name: g.classGroup.name, distribution: { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0 } }; - } - if (rounded >= 1 && rounded <= 6) { - classMap[g.classGroupId].distribution[rounded]++; - } - }); - - const classDistribution = Object.entries(classMap).map(([id, data]) => ({ - id, - name: data.name, - distribution: data.distribution, - })); - - return NextResponse.json({ - type: 'class', - classes: classDistribution, - gradeColors, - total: grades.length, - }); - } - - return NextResponse.json({ error: 'Invalid groupBy parameter' }, { status: 400 }); - } catch (error) { - console.error('Grade distribution error:', error); - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); - } -} -// @ts-nocheck diff --git a/src/app/api/grade-analytics/reports/route.ts b/src/app/api/grade-analytics/reports/route.ts deleted file mode 100644 index c608ba1..0000000 --- a/src/app/api/grade-analytics/reports/route.ts +++ /dev/null @@ -1,232 +0,0 @@ -import { NextResponse } from 'next/server'; -import { db } from '@/lib/db'; -import { getSession } from '@/lib/auth'; - -// GET: List grade reports -export async function GET(request: Request) { - try { - const session = await getSession(); - if (!session) { - return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); - } - - const { searchParams } = new URL(request.url); - const schoolId = searchParams.get('schoolId') ?? session.user?.schoolId ?? undefined; - const type = searchParams.get('type'); - - const role = session.user?.role; - const isAdmin = role === 'SUPER_ADMIN' || role === 'SCHOOL_ADMIN' || role === 'VICE_PRINCIPAL'; - const isTeacher = role === 'TEACHER'; - - const where: Record = {}; - if (schoolId) where.schoolId = schoolId; - if (type) where.type = type; - - // Non-admin users can only see their own reports - if (!isAdmin && !isTeacher) { - where.generatedBy = session.userId; - } - - const reports = await db.gradeReport.findMany({ - where, - include: { - generator: { - select: { id: true, firstName: true, lastName: true }, - }, - }, - orderBy: { createdAt: 'desc' }, - }); - - return NextResponse.json({ - reports: reports.map((r) => ({ - id: r.id, - title: r.title, - type: r.type, - dateRange: r.dateRange, - classIds: r.classIds, - subjectIds: r.subjectIds, - metrics: r.metrics, - status: r.status, - generatedBy: r.generator ? `${r.generator.firstName} ${r.generator.lastName}` : 'Unknown', - createdAt: r.createdAt, - hasFileData: !!r.fileData, - })), - }); - } catch (error) { - console.error('Grade reports list error:', error); - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); - } -} - -// POST: Generate a new grade report -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 isAdmin = role === 'SUPER_ADMIN' || role === 'SCHOOL_ADMIN' || role === 'VICE_PRINCIPAL'; - const isTeacher = role === 'TEACHER'; - - if (!isAdmin && !isTeacher) { - return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }); - } - - const body = await request.json(); - const { - title, - type, - dateRange, - classIds, - subjectIds, - metrics, - } = body; - - if (!title || !type || !dateRange) { - return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }); - } - - const schoolId = body.schoolId ?? session.user?.schoolId; - if (!schoolId) { - return NextResponse.json({ error: 'School ID required' }, { status: 400 }); - } - - // Build the grade data for the report - const gradeWhere: Record = { - classGroup: { schoolId }, - }; - - const parsedDateRange = typeof dateRange === 'string' ? JSON.parse(dateRange) : dateRange; - if (parsedDateRange.from || parsedDateRange.to) { - if (parsedDateRange.from) { - gradeWhere.computedAt = { gte: new Date(parsedDateRange.from) }; - } - if (parsedDateRange.to) { - gradeWhere.computedAt = { ...(gradeWhere.computedAt as object || {}), lte: new Date(parsedDateRange.to) }; - } - } - - const parsedClassIds = typeof classIds === 'string' ? JSON.parse(classIds) : classIds; - const parsedSubjectIds = typeof subjectIds === 'string' ? JSON.parse(subjectIds) : subjectIds; - const parsedMetrics = typeof metrics === 'string' ? JSON.parse(metrics) : metrics; - - if (parsedClassIds && Array.isArray(parsedClassIds) && parsedClassIds.length > 0) { - gradeWhere.classGroupId = { in: parsedClassIds }; - } - if (parsedSubjectIds && Array.isArray(parsedSubjectIds) && parsedSubjectIds.length > 0) { - gradeWhere.subjectId = { in: parsedSubjectIds }; - } - - // Fetch grades for report - const grades = await db.computedGrade.findMany({ - where: gradeWhere, - include: { - student: { select: { id: true, firstName: true, lastName: true } }, - subject: { select: { id: true, name: true } }, - classGroup: { select: { id: true, name: true } }, - }, - orderBy: { computedAt: 'asc' }, - }); - - // Generate report data - const reportData: Record = { - generatedAt: new Date().toISOString(), - generatedBy: `${session.user?.firstName} ${session.user?.lastName}`, - title, - type, - dateRange: parsedDateRange, - totalGrades: grades.length, - }; - - // Include selected metrics - if (!parsedMetrics || parsedMetrics.includes('distribution')) { - const distribution: Record = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0 }; - grades.forEach((g) => { - const val = g.overriddenValue ?? g.computedValue; - const rounded = Math.round(val); - if (rounded >= 1 && rounded <= 6) distribution[rounded]++; - }); - reportData.distribution = distribution; - } - - if (!parsedMetrics || parsedMetrics.includes('averages')) { - const avg = grades.length > 0 - ? Math.round((grades.reduce((s, g) => s + (g.overriddenValue ?? g.computedValue), 0) / grades.length) * 100) / 100 - : 0; - reportData.averageGrade = avg; - } - - if (!parsedMetrics || parsedMetrics.includes('classComparison')) { - const classAvg: Record = {}; - grades.forEach((g) => { - const val = g.overriddenValue ?? g.computedValue; - if (!classAvg[g.classGroupId]) { - classAvg[g.classGroupId] = { name: g.classGroup.name, average: 0, count: 0 }; - } - classAvg[g.classGroupId].average += val; - classAvg[g.classGroupId].count += 1; - }); - Object.values(classAvg).forEach((c) => { - c.average = c.count > 0 ? Math.round((c.average / c.count) * 100) / 100 : 0; - }); - reportData.classComparison = classAvg; - } - - if (!parsedMetrics || parsedMetrics.includes('subjectAnalysis')) { - const subjectAvg: Record = {}; - grades.forEach((g) => { - const val = g.overriddenValue ?? g.computedValue; - if (!subjectAvg[g.subjectId]) { - subjectAvg[g.subjectId] = { name: g.subject.name, average: 0, count: 0 }; - } - subjectAvg[g.subjectId].average += val; - subjectAvg[g.subjectId].count += 1; - }); - Object.values(subjectAvg).forEach((s) => { - s.average = s.count > 0 ? Math.round((s.average / s.count) * 100) / 100 : 0; - }); - reportData.subjectAnalysis = subjectAvg; - } - - if (!parsedMetrics || parsedMetrics.includes('studentGrades')) { - reportData.studentGrades = grades.map((g) => ({ - student: `${g.student.firstName} ${g.student.lastName}`, - subject: g.subject.name, - class: g.classGroup.name, - grade: g.overriddenValue ?? g.computedValue, - isFinalized: g.isFinalized, - period: g.period, - })); - } - - // Create the report record - const report = await db.gradeReport.create({ - data: { - schoolId, - generatedBy: session.userId, - title, - type, - dateRange: typeof dateRange === 'string' ? dateRange : JSON.stringify(dateRange), - classIds: classIds ? (typeof classIds === 'string' ? classIds : JSON.stringify(classIds)) : null, - subjectIds: subjectIds ? (typeof subjectIds === 'string' ? subjectIds : JSON.stringify(subjectIds)) : null, - metrics: metrics ? (typeof metrics === 'string' ? metrics : JSON.stringify(metrics)) : null, - status: 'completed', - fileData: JSON.stringify(reportData), - }, - }); - - return NextResponse.json({ - id: report.id, - title: report.title, - type: report.type, - status: report.status, - createdAt: report.createdAt, - reportData, - }); - } catch (error) { - console.error('Grade report generation error:', error); - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); - } -} diff --git a/src/app/api/grade-analytics/route.ts b/src/app/api/grade-analytics/route.ts deleted file mode 100644 index b2b6538..0000000 --- a/src/app/api/grade-analytics/route.ts +++ /dev/null @@ -1,250 +0,0 @@ -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 ?? undefined; - const classGroupId = searchParams.get('classGroupId'); - const subjectId = searchParams.get('subjectId'); - const schoolYearId = searchParams.get('schoolYearId'); - - const role = session.user?.role; - const isStudent = role === 'STUDENT'; - const isParent = role === 'PARENT'; - - // Build where clause for ComputedGrade - const gradeWhere: Record = {}; - if (classGroupId) gradeWhere.classGroupId = classGroupId; - if (subjectId) gradeWhere.subjectId = subjectId; - if (schoolYearId) gradeWhere.schoolYearId = schoolYearId; - - // Scope by school - if (schoolId) { - const classWhere: Record = { schoolId }; - gradeWhere.classGroup = { schoolId }; - if (Object.keys(classWhere).length > 0) { - gradeWhere.classGroup = classWhere; - } - } - - // Role-based scoping - if (isStudent && session.user?.id) { - const student = await db.student.findFirst({ - where: { userId: session.user.id, deletedAt: null }, - }); - if (student) { - gradeWhere.studentId = student.id; - } - } - - if (isParent && session.user?.id) { - const parentLinks = await db.parentStudentLink.findMany({ - where: { parentId: session.user.id }, - select: { studentId: true }, - }); - const studentIds = parentLinks.map((l) => l.studentId); - gradeWhere.studentId = { in: studentIds }; - } - - // Get total grades count - const totalGrades = await db.computedGrade.count({ where: gradeWhere }); - - // Get average grade - const gradeAgg = await db.computedGrade.aggregate({ - where: gradeWhere, - _avg: { computedValue: true }, - _min: { computedValue: true }, - _max: { computedValue: true }, - }); - - // Get grade distribution (1-6 scale) - const allGrades = await db.computedGrade.findMany({ - where: gradeWhere, - select: { - computedValue: true, - overriddenValue: true, - subjectId: true, - classGroupId: true, - studentId: true, - period: true, - isFinalized: true, - }, - }); - - // Grade distribution - const distribution: Record = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0 }; - allGrades.forEach((g) => { - const val = g.overriddenValue ?? g.computedValue; - const rounded = Math.round(val); - if (rounded >= 1 && rounded <= 6) { - distribution[rounded]++; - } - }); - - // Finalized vs draft count - const finalizedCount = allGrades.filter((g) => g.isFinalized).length; - const draftCount = totalGrades - finalizedCount; - - // Top performers (lowest grade = best on German scale) - const studentGradeMap: Record = {}; - for (const g of allGrades) { - const val = g.overriddenValue ?? g.computedValue; - if (!studentGradeMap[g.studentId]) { - studentGradeMap[g.studentId] = { total: 0, count: 0, name: '' }; - } - studentGradeMap[g.studentId].total += val; - studentGradeMap[g.studentId].count += 1; - } - - // Get student names - const studentIds = Object.keys(studentGradeMap); - const students = await db.student.findMany({ - where: { id: { in: studentIds }, deletedAt: null }, - select: { id: true, firstName: true, lastName: true }, - }); - students.forEach((s) => { - if (studentGradeMap[s.id]) { - studentGradeMap[s.id].name = `${s.firstName} ${s.lastName}`; - } - }); - - // Calculate averages and sort - const studentAverages = studentIds - .map((id) => ({ - id, - name: studentGradeMap[id].name, - average: studentGradeMap[id].count > 0 ? studentGradeMap[id].total / studentGradeMap[id].count : 0, - gradeCount: studentGradeMap[id].count, - })) - .sort((a, b) => a.average - b.average); // Lower = better on German scale - - const topPerformers = studentAverages.slice(0, 5); - const bottomPerformers = studentAverages.slice(-5).reverse(); - - // Risk students (average >= 4.5, trending towards 5-6) - const riskStudents = studentAverages.filter((s) => s.average >= 4.5 && s.gradeCount >= 2); - - // Get classes for filter - const classes = await db.classGroup.findMany({ - where: schoolId ? { schoolId } : {}, - select: { id: true, name: true, gradeLevel: true }, - orderBy: { gradeLevel: 'asc' }, - }); - - // Get subjects for filter - const subjects = await db.subject.findMany({ - where: schoolId ? { schoolId } : {}, - select: { id: true, name: true }, - orderBy: { name: 'asc' }, - }); - - // Subject difficulty ranking - const subjectGradeMap: Record = {}; - for (const g of allGrades) { - const val = g.overriddenValue ?? g.computedValue; - if (!subjectGradeMap[g.subjectId]) { - subjectGradeMap[g.subjectId] = { total: 0, count: 0, name: '' }; - } - subjectGradeMap[g.subjectId].total += val; - subjectGradeMap[g.subjectId].count += 1; - } - - subjects.forEach((s) => { - if (subjectGradeMap[s.id]) { - subjectGradeMap[s.id].name = s.name; - } - }); - - const subjectDifficulty = Object.entries(subjectGradeMap) - .map(([id, data]) => ({ - id, - name: data.name, - average: data.count > 0 ? data.total / data.count : 0, - gradeCount: data.count, - })) - .sort((a, b) => b.average - a.average); // Higher average = harder - - // Class average comparison - const classGradeMap: Record = {}; - for (const g of allGrades) { - const val = g.overriddenValue ?? g.computedValue; - if (!classGradeMap[g.classGroupId]) { - classGradeMap[g.classGroupId] = { total: 0, count: 0, name: '' }; - } - classGradeMap[g.classGroupId].total += val; - classGradeMap[g.classGroupId].count += 1; - } - - classes.forEach((c) => { - if (classGradeMap[c.id]) { - classGradeMap[c.id].name = c.name; - } - }); - - const classAverages = Object.entries(classGradeMap) - .map(([id, data]) => ({ - id, - name: data.name, - average: data.count > 0 ? data.total / data.count : 0, - gradeCount: data.count, - })) - .sort((a, b) => a.average - b.average); - - // Teacher comparison (admin only) - let teacherComparison: Array<{ id: string; name: string; average: number; gradeCount: number }> = []; - if (role === 'SUPER_ADMIN' || role === 'SCHOOL_ADMIN' || role === 'VICE_PRINCIPAL') { - const teacherIds = await db.classGroupTeacher.findMany({ - where: schoolId ? { classGroup: { schoolId } } : {}, - select: { userId: true, user: { select: { id: true, firstName: true, lastName: true } } }, - distinct: ['userId'], - }); - - for (const t of teacherIds) { - const teacherClasses = await db.classGroupTeacher.findMany({ - where: { userId: t.userId }, - select: { classGroupId: true }, - }); - const tClassIds = teacherClasses.map((tc) => tc.classGroupId); - const tGrades = allGrades.filter((g) => tClassIds.includes(g.classGroupId)); - if (tGrades.length > 0) { - const avg = tGrades.reduce((sum, g) => sum + (g.overriddenValue ?? g.computedValue), 0) / tGrades.length; - teacherComparison.push({ - id: t.user.id, - name: `${t.user.firstName} ${t.user.lastName}`, - average: Math.round(avg * 100) / 100, - gradeCount: tGrades.length, - }); - } - } - teacherComparison.sort((a, b) => a.average - b.average); - } - - return NextResponse.json({ - totalGrades, - averageGrade: gradeAgg._avg.computedValue ? Math.round(gradeAgg._avg.computedValue * 100) / 100 : 0, - minGrade: gradeAgg._min.computedValue ?? 0, - maxGrade: gradeAgg._max.computedValue ?? 0, - finalizedCount, - draftCount, - distribution, - topPerformers, - bottomPerformers, - riskStudents, - classes, - subjects, - subjectDifficulty, - classAverages, - teacherComparison, - }); - } catch (error) { - console.error('Grade analytics error:', error); - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); - } -} diff --git a/src/app/api/grade-analytics/trends/route.ts b/src/app/api/grade-analytics/trends/route.ts deleted file mode 100644 index d85acc7..0000000 --- a/src/app/api/grade-analytics/trends/route.ts +++ /dev/null @@ -1,215 +0,0 @@ -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 ?? undefined; - const classGroupId = searchParams.get('classGroupId'); - const subjectId = searchParams.get('subjectId'); - const schoolYearId = searchParams.get('schoolYearId'); - const studentId = searchParams.get('studentId'); - - const role = session.user?.role; - const isStudent = role === 'STUDENT'; - const isParent = role === 'PARENT'; - - // Build where clause for assessments - const assessmentWhere: Record = {}; - if (classGroupId) assessmentWhere.classGroupId = classGroupId; - if (subjectId) assessmentWhere.subjectId = subjectId; - - if (schoolId) { - assessmentWhere.classGroup = { schoolId }; - } - - // Get assessments with results over time - const assessments = await db.assessment.findMany({ - where: assessmentWhere, - select: { - id: true, - title: true, - date: true, - type: true, - subjectId: true, - classGroupId: true, - maxScore: true, - subject: { select: { id: true, name: true } }, - classGroup: { select: { id: true, name: true } }, - assessmentResults: { - select: { - studentId: true, - score: true, - masteryLevelValue: true, - student: { - select: { id: true, firstName: true, lastName: true }, - }, - }, - ...(studentId ? { where: { studentId } } : {}), - }, - }, - orderBy: { date: 'asc' }, - }); - - // Role-based scoping for student/parent - let filteredAssessments = assessments; - if (isStudent && session.user?.id) { - const student = await db.student.findFirst({ - where: { userId: session.user.id, deletedAt: null }, - }); - if (student) { - filteredAssessments = assessments.map((a) => ({ - ...a, - assessmentResults: a.assessmentResults.filter((r) => r.studentId === student.id), - })); - } - } - - if (isParent && session.user?.id) { - const parentLinks = await db.parentStudentLink.findMany({ - where: { parentId: session.user.id }, - select: { studentId: true }, - }); - const childIds = parentLinks.map((l) => l.studentId); - filteredAssessments = assessments.map((a) => ({ - ...a, - assessmentResults: a.assessmentResults.filter((r) => childIds.includes(r.studentId)), - })); - } - - // Build trend data by month - const monthMap: Record = {}; - - filteredAssessments.forEach((a) => { - const monthKey = a.date.toISOString().substring(0, 7); // YYYY-MM - if (!monthMap[monthKey]) { - monthMap[monthKey] = { date: monthKey, average: 0, count: 0, grade1: 0, grade2: 0, grade3: 0, grade4: 0, grade5: 0, grade6: 0 }; - } - a.assessmentResults.forEach((r) => { - if (r.score !== null && a.maxScore) { - // Convert score to German grade (1-6 scale) - const percentage = r.score / a.maxScore; - let grade: number; - if (percentage >= 0.92) grade = 1; - else if (percentage >= 0.81) grade = 2; - else if (percentage >= 0.67) grade = 3; - else if (percentage >= 0.50) grade = 4; - else if (percentage >= 0.30) grade = 5; - else grade = 6; - - monthMap[monthKey].count++; - monthMap[monthKey].average += grade; - if (grade === 1) monthMap[monthKey].grade1++; - else if (grade === 2) monthMap[monthKey].grade2++; - else if (grade === 3) monthMap[monthKey].grade3++; - else if (grade === 4) monthMap[monthKey].grade4++; - else if (grade === 5) monthMap[monthKey].grade5++; - else monthMap[monthKey].grade6++; - } - }); - }); - - // Calculate averages - Object.values(monthMap).forEach((m) => { - m.average = m.count > 0 ? Math.round((m.average / m.count) * 100) / 100 : 0; - }); - - const trendData = Object.values(monthMap).sort((a, b) => a.date.localeCompare(b.date)); - - // Student trajectory data - const studentTrajectories: Record }> = {}; - filteredAssessments.forEach((a) => { - const monthKey = a.date.toISOString().substring(0, 7); - a.assessmentResults.forEach((r) => { - if (r.score !== null && a.maxScore) { - const percentage = r.score / a.maxScore; - let grade: number; - if (percentage >= 0.92) grade = 1; - else if (percentage >= 0.81) grade = 2; - else if (percentage >= 0.67) grade = 3; - else if (percentage >= 0.50) grade = 4; - else if (percentage >= 0.30) grade = 5; - else grade = 6; - - if (!studentTrajectories[r.studentId]) { - studentTrajectories[r.studentId] = { - name: `${r.student.firstName} ${r.student.lastName}`, - data: [], - }; - } - // Check if we already have data for this month - const existing = studentTrajectories[r.studentId].data.find((d) => d.date === monthKey); - if (!existing) { - studentTrajectories[r.studentId].data.push({ date: monthKey, grade }); - } - } - }); - }); - - // Sort trajectory data by date - Object.values(studentTrajectories).forEach((t) => { - t.data.sort((a, b) => a.date.localeCompare(b.date)); - }); - - // Improvement/regression detection - const studentImprovements: Array<{ - studentId: string; - name: string; - trend: 'improving' | 'stable' | 'regressing'; - firstGrade: number; - lastGrade: number; - change: number; - }> = []; - - Object.entries(studentTrajectories).forEach(([id, data]) => { - if (data.data.length >= 2) { - const firstGrade = data.data[0].grade; - const lastGrade = data.data[data.data.length - 1].grade; - const change = firstGrade - lastGrade; // Negative = regressing (grade went up), Positive = improving - let trend: 'improving' | 'stable' | 'regressing' = 'stable'; - if (change >= 0.5) trend = 'improving'; - else if (change <= -0.5) trend = 'regressing'; - - studentImprovements.push({ - studentId: id, - name: data.name, - trend, - firstGrade, - lastGrade, - change: Math.round(change * 100) / 100, - }); - } - }); - - // Percentile calculation - const allStudentAvgs = Object.entries(studentTrajectories).map(([id, data]) => { - const avg = data.data.length > 0 ? data.data.reduce((s, d) => s + d.grade, 0) / data.data.length : 0; - return { id, name: data.name, average: avg }; - }).sort((a, b) => a.average - b.average); - - const percentileData = allStudentAvgs.map((s, idx) => ({ - ...s, - percentile: Math.round((idx / Math.max(allStudentAvgs.length - 1, 1)) * 100), - })); - - return NextResponse.json({ - trendData, - studentTrajectories: Object.entries(studentTrajectories).map(([id, data]) => ({ - id, - ...data, - })), - studentImprovements, - percentileData, - totalAssessments: filteredAssessments.length, - }); - } catch (error) { - console.error('Grade trends error:', error); - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); - } -} diff --git a/src/app/api/grading-annotations/[id]/route.ts b/src/app/api/grading-annotations/[id]/route.ts deleted file mode 100755 index 897e577..0000000 --- a/src/app/api/grading-annotations/[id]/route.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { NextResponse } from 'next/server'; -import { z } from 'zod'; -import { db } from '@/lib/db'; -import { getSession } from '@/lib/auth'; - -const updateAnnotationSchema = z.object({ - type: z.enum(['drawing', 'text', 'highlight', 'stamp']).optional(), - content: z.string().optional(), - positionX: z.number().optional(), - positionY: z.number().optional(), - width: z.number().optional(), - height: z.number().optional(), - color: z.string().optional(), - strokeWidth: z.number().optional(), - page: z.number().optional(), - pathData: z.string().optional(), -}); - -export async function GET( - _request: Request, - { params }: { params: Promise<{ id: string }> } -) { - try { - const session = await getSession(); - if (!session) { - return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); - } - - const { id } = await params; - const annotation = await db.gradingAnnotation.findUnique({ - where: { id }, - include: { - teacher: { select: { id: true, firstName: true, lastName: true } }, - }, - }); - - if (!annotation) { - return NextResponse.json({ error: 'Not found' }, { status: 404 }); - } - - return NextResponse.json(annotation); - } catch (error) { - console.error('GradingAnnotation GET [id] error:', error); - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); - } -} - -export async function PUT( - request: Request, - { params }: { params: Promise<{ id: string }> } -) { - try { - const session = await getSession(); - if (!session) { - return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); - } - - const role = session.user?.role; - if (role !== 'TEACHER' && role !== 'SCHOOL_ADMIN' && role !== 'SUPER_ADMIN' && role !== 'VICE_PRINCIPAL') { - return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); - } - - const { id } = await params; - const body = await request.json(); - const parsed = updateAnnotationSchema.safeParse(body); - if (!parsed.success) { - return NextResponse.json( - { error: 'Validation failed', details: parsed.error.issues }, - { status: 400 } - ); - } - - const existing = await db.gradingAnnotation.findUnique({ where: { id } }); - if (!existing) { - return NextResponse.json({ error: 'Not found' }, { status: 404 }); - } - - // Only the creator or admin can update - if (existing.teacherId !== session.userId && role !== 'SCHOOL_ADMIN' && role !== 'SUPER_ADMIN') { - return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); - } - - const annotation = await db.gradingAnnotation.update({ - where: { id }, - data: parsed.data, - include: { - teacher: { select: { id: true, firstName: true, lastName: true } }, - }, - }); - - return NextResponse.json(annotation); - } catch (error) { - console.error('GradingAnnotation PUT [id] error:', error); - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); - } -} - -export async function DELETE( - _request: Request, - { params }: { params: Promise<{ id: string }> } -) { - try { - const session = await getSession(); - if (!session) { - return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); - } - - const role = session.user?.role; - if (role !== 'TEACHER' && role !== 'SCHOOL_ADMIN' && role !== 'SUPER_ADMIN' && role !== 'VICE_PRINCIPAL') { - return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); - } - - const { id } = await params; - const existing = await db.gradingAnnotation.findUnique({ where: { id } }); - if (!existing) { - return NextResponse.json({ error: 'Not found' }, { status: 404 }); - } - - if (existing.teacherId !== session.userId && role !== 'SCHOOL_ADMIN' && role !== 'SUPER_ADMIN') { - return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); - } - - await db.gradingAnnotation.delete({ where: { id } }); - return NextResponse.json({ success: true }); - } catch (error) { - console.error('GradingAnnotation DELETE [id] error:', error); - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); - } -} diff --git a/src/app/api/grading-annotations/route.ts b/src/app/api/grading-annotations/route.ts deleted file mode 100755 index 75c85fe..0000000 --- a/src/app/api/grading-annotations/route.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { NextResponse } from 'next/server'; -import { z } from 'zod'; -import { db } from '@/lib/db'; -import { getSession } from '@/lib/auth'; - -const createAnnotationSchema = z.object({ - schoolId: z.string().min(1), - assessmentId: z.string().min(1), - studentId: z.string().min(1), - resultId: z.string().optional(), - type: z.enum(['drawing', 'text', 'highlight', 'stamp']).default('drawing'), - content: z.string().optional(), - positionX: z.number().default(0), - positionY: z.number().default(0), - width: z.number().optional(), - height: z.number().optional(), - color: z.string().default('#ef4444'), - strokeWidth: z.number().default(2), - page: z.number().default(1), - pathData: z.string().optional(), -}); - -export async function GET(request: Request) { - try { - const session = await getSession(); - if (!session) { - return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); - } - - const { searchParams } = new URL(request.url); - const assessmentId = searchParams.get('assessmentId'); - const studentId = searchParams.get('studentId'); - const page = searchParams.get('page'); - - if (!assessmentId) { - return NextResponse.json({ error: 'assessmentId is required' }, { status: 400 }); - } - - const where: Record = { assessmentId }; - if (studentId) where.studentId = studentId; - if (page) where.page = parseInt(page, 10); - - const annotations = await db.gradingAnnotation.findMany({ - where, - orderBy: { createdAt: 'asc' }, - include: { - teacher: { select: { id: true, firstName: true, lastName: true } }, - }, - }); - - return NextResponse.json(annotations); - } catch (error) { - console.error('GradingAnnotations GET error:', error); - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); - } -} - -export async function POST(request: Request) { - try { - const session = await getSession(); - if (!session) { - return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); - } - - const role = session.user?.role; - if (role !== 'TEACHER' && role !== 'SCHOOL_ADMIN' && role !== 'SUPER_ADMIN' && role !== 'VICE_PRINCIPAL') { - return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); - } - - const body = await request.json(); - const parsed = createAnnotationSchema.safeParse(body); - if (!parsed.success) { - return NextResponse.json( - { error: 'Validation failed', details: parsed.error.issues }, - { status: 400 } - ); - } - - const data = parsed.data; - const annotation = await db.gradingAnnotation.create({ - data: { - schoolId: data.schoolId, - assessmentId: data.assessmentId, - studentId: data.studentId, - resultId: data.resultId, - teacherId: session.userId, - type: data.type, - content: data.content, - positionX: data.positionX, - positionY: data.positionY, - width: data.width, - height: data.height, - color: data.color, - strokeWidth: data.strokeWidth, - page: data.page, - pathData: data.pathData, - }, - include: { - teacher: { select: { id: true, firstName: true, lastName: true } }, - }, - }); - - return NextResponse.json(annotation, { status: 201 }); - } catch (error) { - console.error('GradingAnnotations POST error:', error); - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); - } -} diff --git a/src/app/api/grading-reviews/[id]/comments/route.ts b/src/app/api/grading-reviews/[id]/comments/route.ts deleted file mode 100755 index c340060..0000000 --- a/src/app/api/grading-reviews/[id]/comments/route.ts +++ /dev/null @@ -1,73 +0,0 @@ -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 }); - } -} diff --git a/src/app/api/grading-reviews/[id]/route.ts b/src/app/api/grading-reviews/[id]/route.ts deleted file mode 100755 index ff9ffbd..0000000 --- a/src/app/api/grading-reviews/[id]/route.ts +++ /dev/null @@ -1,82 +0,0 @@ -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 = {}; - 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 }); - } -} diff --git a/src/app/api/grading-reviews/route.ts b/src/app/api/grading-reviews/route.ts deleted file mode 100755 index e49f62e..0000000 --- a/src/app/api/grading-reviews/route.ts +++ /dev/null @@ -1,158 +0,0 @@ -// @ts-nocheck -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 = { 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: { - assessmentResults: { - 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.assessmentResults.map((r: { student: { firstName: string; lastName: string }; score: number | null; masteryLevelValue: number | null }) => ({ - student: `${r.student.firstName} ${r.student.lastName}`, - score: r.score, - maxScore: assessment.maxScore, - grade: r.masteryLevelValue, - })); - - 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.assessmentResults - .map((r: { score: number | null }) => r.score && assessment.maxScore ? (r.score / assessment.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 }); - } -} diff --git a/src/components/app-layout.tsx b/src/components/app-layout.tsx index aaf35fd..d529e2c 100644 --- a/src/components/app-layout.tsx +++ b/src/components/app-layout.tsx @@ -488,7 +488,6 @@ function renderView(view: ViewName) { case 'notification-center': return ; case 'announcements': return ; case 'peer-assessment': return ; - case 'grade-analytics': return ; case 'student-achievements': return ; case 'student-study-planner': return ; case 'seating-chart': return ; diff --git a/src/components/dashboard-customizer.tsx b/src/components/dashboard-customizer.tsx deleted file mode 100644 index 1930285..0000000 --- a/src/components/dashboard-customizer.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import React, { useState } from 'react'; -import { Button } from '@/components/ui/button'; -import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; -import { WidgetContainer } from './widget-container'; -import { WIDGET_TYPES, WIDGET_SIZES, getDefaultConfig } from '@/lib/widget-utils'; -import { Plus, Edit2, Save } from 'lucide-react'; - -interface DashboardCustomizerProps { - schoolId: string; - userId: string; - widgets: any[]; - onWidgetAdd: (widgetType: string) => void; - onWidgetRemove: (widgetId: string) => void; - onLayoutSave: () => void; -} - -export function DashboardCustomizer({ - schoolId, - userId, - widgets, - onWidgetAdd, - onWidgetRemove, - onLayoutSave, -}: DashboardCustomizerProps) { - const [isEditing, setIsEditing] = useState(false); - const [showWidgetGallery, setShowWidgetGallery] = useState(false); - - const availableWidgets = Object.values(WIDGET_TYPES).filter( - (type) => !widgets.some((w) => w.widgetType === type) - ); - - return ( - <> -
- - - {isEditing && ( - <> - - - - )} -
- - {/* Widget Gallery Dialog */} - - - - Widget hinzufügen - -
- {availableWidgets.map((type) => ( - - ))} -
-
-
- - {/* Dashboard Grid */} -
- {widgets.map((widget) => ( - onWidgetRemove(widget.id)} - > -
- {widget.widgetType} widget content -
-
- ))} -
- - ); -} diff --git a/src/components/data-table.tsx b/src/components/data-table.tsx deleted file mode 100644 index 1ae76f9..0000000 --- a/src/components/data-table.tsx +++ /dev/null @@ -1,198 +0,0 @@ -'use client'; - -import React, { useState, useMemo } from 'react'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/table'; -import { Button } from '@/components/ui/button'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; -import { Input } from '@/components/ui/input'; -import { ChevronDown, MoreHorizontal, Search } from 'lucide-react'; - -export interface DataTableColumn { - key: keyof T; - label: string; - sortable?: boolean; - filterable?: boolean; - render?: (value: any, row: T) => React.ReactNode; - width?: string; -} - -export interface DataTableAction { - label: string; - onClick: (row: T) => void; - variant?: 'default' | 'destructive'; -} - -export interface DataTableProps { - columns: DataTableColumn[]; - data: T[]; - actions?: DataTableAction[]; - onRowClick?: (row: T) => void; - searchableFields?: (keyof T)[]; - isLoading?: boolean; - rowKey?: keyof T; -} - -export function DataTable>({ - columns, - data, - actions, - onRowClick, - searchableFields = [], - isLoading = false, - rowKey = 'id' as keyof T, -}: DataTableProps) { - const [searchTerm, setSearchTerm] = useState(''); - const [sortConfig, setSortConfig] = useState<{ - key: keyof T; - direction: 'asc' | 'desc'; - } | null>(null); - - // Filter data based on search - const filteredData = useMemo(() => { - if (!searchTerm || searchableFields.length === 0) return data; - - return data.filter((row) => - searchableFields.some((field) => { - const value = row[field]; - return value?.toString().toLowerCase().includes(searchTerm.toLowerCase()); - }) - ); - }, [data, searchTerm, searchableFields]); - - // Sort data - const sortedData = useMemo(() => { - if (!sortConfig) return filteredData; - - return [...filteredData].sort((a, b) => { - const aVal = a[sortConfig.key]; - const bVal = b[sortConfig.key]; - - if (aVal < bVal) return sortConfig.direction === 'asc' ? -1 : 1; - if (aVal > bVal) return sortConfig.direction === 'asc' ? 1 : -1; - return 0; - }); - }, [filteredData, sortConfig]); - - const handleSort = (key: keyof T) => { - setSortConfig((prev) => - prev?.key === key && prev.direction === 'asc' - ? { key, direction: 'desc' } - : { key, direction: 'asc' } - ); - }; - - if (isLoading) { - return
Loading...
; - } - - return ( -
- {searchableFields.length > 0 && ( -
- - setSearchTerm(e.target.value)} - className="pl-10" - /> -
- )} - -
- - - - {columns.map((col) => ( - -
- {col.label} - {col.sortable && ( - - )} -
-
- ))} - {actions && } -
-
- - {sortedData.map((row, idx) => ( - onRowClick?.(row)} - className="cursor-pointer hover:bg-gray-50" - > - {columns.map((col) => ( - - {col.render ? col.render(row[col.key], row) : row[col.key]} - - ))} - {actions && ( - - - e.stopPropagation()}> - - - - {actions.map((action) => ( - { - e.stopPropagation(); - action.onClick(row); - }} - className={ - action.variant === 'destructive' ? 'text-red-600' : '' - } - > - {action.label} - - ))} - - - - )} - - ))} - -
-
- - {sortedData.length === 0 && ( -
Keine Daten gefunden
- )} -
- ); -} diff --git a/src/components/ui/accordion.tsx b/src/components/ui/accordion.tsx deleted file mode 100755 index 4a8cca4..0000000 --- a/src/components/ui/accordion.tsx +++ /dev/null @@ -1,66 +0,0 @@ -"use client" - -import * as React from "react" -import * as AccordionPrimitive from "@radix-ui/react-accordion" -import { ChevronDownIcon } from "lucide-react" - -import { cn } from "@/lib/utils" - -function Accordion({ - ...props -}: React.ComponentProps) { - return -} - -function AccordionItem({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function AccordionTrigger({ - className, - children, - ...props -}: React.ComponentProps) { - return ( - - svg]:rotate-180", - className - )} - {...props} - > - {children} - - - - ) -} - -function AccordionContent({ - className, - children, - ...props -}: React.ComponentProps) { - return ( - -
{children}
-
- ) -} - -export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/src/components/ui/aspect-ratio.tsx b/src/components/ui/aspect-ratio.tsx deleted file mode 100755 index 3df3fd0..0000000 --- a/src/components/ui/aspect-ratio.tsx +++ /dev/null @@ -1,11 +0,0 @@ -"use client" - -import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" - -function AspectRatio({ - ...props -}: React.ComponentProps) { - return -} - -export { AspectRatio } diff --git a/src/components/ui/calendar.tsx b/src/components/ui/calendar.tsx deleted file mode 100755 index 4d7c46a..0000000 --- a/src/components/ui/calendar.tsx +++ /dev/null @@ -1,213 +0,0 @@ -"use client" - -import * as React from "react" -import { - ChevronDownIcon, - ChevronLeftIcon, - ChevronRightIcon, -} from "lucide-react" -import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker" - -import { cn } from "@/lib/utils" -import { Button, buttonVariants } from "@/components/ui/button" - -function Calendar({ - className, - classNames, - showOutsideDays = true, - captionLayout = "label", - buttonVariant = "ghost", - formatters, - components, - ...props -}: React.ComponentProps & { - buttonVariant?: React.ComponentProps["variant"] -}) { - const defaultClassNames = getDefaultClassNames() - - return ( - svg]:rotate-180`, - String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, - className - )} - captionLayout={captionLayout} - formatters={{ - formatMonthDropdown: (date) => - date.toLocaleString("default", { month: "short" }), - ...formatters, - }} - classNames={{ - root: cn("w-fit", defaultClassNames.root), - months: cn( - "flex gap-4 flex-col md:flex-row relative", - defaultClassNames.months - ), - month: cn("flex flex-col w-full gap-4", defaultClassNames.month), - nav: cn( - "flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between", - defaultClassNames.nav - ), - button_previous: cn( - buttonVariants({ variant: buttonVariant }), - "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", - defaultClassNames.button_previous - ), - button_next: cn( - buttonVariants({ variant: buttonVariant }), - "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", - defaultClassNames.button_next - ), - month_caption: cn( - "flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)", - defaultClassNames.month_caption - ), - dropdowns: cn( - "w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5", - defaultClassNames.dropdowns - ), - dropdown_root: cn( - "relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md", - defaultClassNames.dropdown_root - ), - dropdown: cn( - "absolute bg-popover inset-0 opacity-0", - defaultClassNames.dropdown - ), - caption_label: cn( - "select-none font-medium", - captionLayout === "label" - ? "text-sm" - : "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5", - defaultClassNames.caption_label - ), - table: "w-full border-collapse", - weekdays: cn("flex", defaultClassNames.weekdays), - weekday: cn( - "text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none", - defaultClassNames.weekday - ), - week: cn("flex w-full mt-2", defaultClassNames.week), - week_number_header: cn( - "select-none w-(--cell-size)", - defaultClassNames.week_number_header - ), - week_number: cn( - "text-[0.8rem] select-none text-muted-foreground", - defaultClassNames.week_number - ), - day: cn( - "relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none", - defaultClassNames.day - ), - range_start: cn( - "rounded-l-md bg-accent", - defaultClassNames.range_start - ), - range_middle: cn("rounded-none", defaultClassNames.range_middle), - range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end), - today: cn( - "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none", - defaultClassNames.today - ), - outside: cn( - "text-muted-foreground aria-selected:text-muted-foreground", - defaultClassNames.outside - ), - disabled: cn( - "text-muted-foreground opacity-50", - defaultClassNames.disabled - ), - hidden: cn("invisible", defaultClassNames.hidden), - ...classNames, - }} - components={{ - Root: ({ className, rootRef, ...props }) => { - return ( -
- ) - }, - Chevron: ({ className, orientation, ...props }) => { - if (orientation === "left") { - return ( - - ) - } - - if (orientation === "right") { - return ( - - ) - } - - return ( - - ) - }, - DayButton: CalendarDayButton, - WeekNumber: ({ children, ...props }) => { - return ( - -
- {children} -
- - ) - }, - ...components, - }} - {...props} - /> - ) -} - -function CalendarDayButton({ - className, - day, - modifiers, - ...props -}: React.ComponentProps) { - const defaultClassNames = getDefaultClassNames() - - const ref = React.useRef(null) - React.useEffect(() => { - if (modifiers.focused) ref.current?.focus() - }, [modifiers.focused]) - - return ( - - ) -} - -function CarouselNext({ - className, - variant = "outline", - size = "icon", - ...props -}: React.ComponentProps) { - const { orientation, scrollNext, canScrollNext } = useCarousel() - - return ( - - ) -} - -export { - type CarouselApi, - Carousel, - CarouselContent, - CarouselItem, - CarouselPrevious, - CarouselNext, -} diff --git a/src/components/ui/chart.tsx b/src/components/ui/chart.tsx deleted file mode 100755 index 4687cb6..0000000 --- a/src/components/ui/chart.tsx +++ /dev/null @@ -1,351 +0,0 @@ -"use client" - -import * as React from "react" -import * as RechartsPrimitive from "recharts" - -import { cn } from "@/lib/utils" - -// Format: { THEME_NAME: CSS_SELECTOR } -const THEMES = { light: "", dark: ".dark" } as const - -export type ChartConfig = { - [k in string]: { - label?: React.ReactNode - icon?: React.ComponentType - } & ( - | { color?: string; theme?: never } - | { color?: never; theme: Record } - ) -} - -type ChartContextProps = { - config: ChartConfig -} - -const ChartContext = React.createContext(null) - -function useChart() { - const context = React.useContext(ChartContext) - - if (!context) { - throw new Error("useChart must be used within a ") - } - - return context -} - -function ChartContainer({ - id, - className, - children, - config, - ...props -}: React.ComponentProps<"div"> & { - config: ChartConfig - children: React.ComponentProps< - typeof RechartsPrimitive.ResponsiveContainer - >["children"] -}) { - const uniqueId = React.useId() - const chartId = `chart-${id || uniqueId.replace(/:/g, "")}` - - return ( - -
- - - {children} - -
-
- ) -} - -const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { - const colorConfig = Object.entries(config).filter( - ([, config]) => config.theme || config.color - ) - - if (!colorConfig.length) { - return null - } - - const cssText = Object.entries(THEMES) - .map( - ([theme, prefix]) => ` -${prefix} [data-chart=${id}] { -${colorConfig - .map(([key, itemConfig]) => { - const color = - itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || - itemConfig.color - return color ? ` --color-${key}: ${color};` : null - }) - .join("\n")} -} -` - ) - .join("\n") - - // Use ref to set textContent instead of dangerouslySetInnerHTML to avoid - // React 19 "Encountered a script tag" warning for style elements - return