feat: Round 21 - Major feature additions

- Schlaukopf.de clone: Subject Content Management System (7 categories, 416 topics)
- AI Integration: Pollination AI, OpenAI, Anthropic (chat, image/video generation)
- Virtual Character System: 5 animated characters (Owl, Dragon, Robot, Cat, Wizard)
- Illness Reporting with Parent Approval: privacy-first, student reports need parent approval
- Student-Teacher Communication Rooms: real-time chat, voice, file sharing
- Counseling/Guidance System: 4 types, calendar integration
- Disciplinary Committee System: case management, severity levels
- AI Test Generation: auto-generate practice tests before exams
- Teacher Grading Review: AI-powered grading audit
- VICE_PRINCIPAL role with same privileges as SCHOOL_ADMIN
- Class responsible teacher (Klassenlehrer) field
- 15 new Prisma models (80+ total)
- 30+ new API route files
- 8 new frontend components
- 400+ i18n keys
- Professional styling with gradient headers, animated counters, skeleton loading
This commit is contained in:
Mustafa Cetin committed 2026-07-29 20:09:03 +00:00
1 parent 04ae7aa80c
commit f1776932d7
48 files changed
+13559 -4

No files matched your search

+492 -1
View File
@@ -81,6 +81,19 @@ model School {
subjectTopics SubjectTopic[]
district SchoolDistrict? @relation(fields: [districtId], references: [id])
districtId String?
// New features
subjectCategories SubjectCategory[]
subjectContents SubjectContent[]
contentChangeRequests ContentChangeRequest[]
aiSettings AISettings?
virtualCharacters VirtualCharacter[]
illnessReports IllnessReport[]
counselingAppointments CounselingAppointment[]
disciplinaryCommittees DisciplinaryCommittee[]
communicationRooms CommunicationRoom[]
aiTestGenerations AITestGeneration[]
gradingReviews TeacherGradingReview[]
chatMessages ChatMessage[]
}
model User {
@@ -90,7 +103,7 @@ model User {
passwordHash String
firstName String
lastName String
role String @default("TEACHER") // TEACHER, SCHOOL_ADMIN, SUPER_ADMIN, STUDENT, PARENT
role String @default("TEACHER") // TEACHER, SCHOOL_ADMIN, SUPER_ADMIN, STUDENT, PARENT, VICE_PRINCIPAL
locale String @default("de")
twoFactorSecret String?
isDemo Boolean @default(false)
@@ -134,6 +147,29 @@ model User {
rewardPoints RewardPoints[]
studentAnswers StudentAnswer[]
student Student?
// New features
contentChangeRequests ContentChangeRequest[] @relation("ContentRequester")
contentChangeReviews ContentChangeRequest[] @relation("ContentReviewer")
chatMessages ChatMessage[]
illnessReports IllnessReport[]
illnessReportApprovals IllnessReport[] @relation("IllnessApprover")
communicationRoomMembers CommunicationRoomMember[]
communicationMessages CommunicationMessage[]
counselingAppointments CounselingAppointment[] @relation("Counselor")
counselingStudentAppts CounselingAppointment[] @relation("CounselingStudent")
disciplinaryCasesReported DisciplinaryCase[] @relation("CaseReporter")
disciplinaryCasesReviewed DisciplinaryCase[] @relation("CaseReviewer")
disciplinaryCommitteeMembers DisciplinaryCommitteeMember[]
virtualCharacter VirtualCharacter?
aiTestGenerations AITestGeneration[]
gradingReviews TeacherGradingReview[]
gradingReviewComments GradingReviewComment[]
selectedCharacterId String?
// Communication room relations
roomStudent CommunicationRoom[] @relation("RoomStudent")
roomTeacher CommunicationRoom[] @relation("RoomTeacher")
// Class responsible teacher
responsibleClasses ClassGroup[] @relation("ResponsibleTeacher")
}
model SchoolYear {
@@ -160,11 +196,13 @@ model ClassGroup {
gradeLevel Int
schoolType String @default("ELEMENTARY")
seatingOrder String? // JSON array of student IDs
responsibleTeacherId String? // Class responsible teacher (Klassenlehrer)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
school School @relation(fields: [schoolId], references: [id])
schoolYear SchoolYear @relation(fields: [schoolYearId], references: [id])
responsibleTeacher User? @relation("ResponsibleTeacher", fields: [responsibleTeacherId], references: [id])
teachers ClassGroupTeacher[]
enrollments Enrollment[]
classCompetencyAssignments ClassCompetencyAssignment[]
@@ -187,6 +225,7 @@ model ClassGroup {
resources Resource[]
peerAssessments PeerAssessment[]
schoolEvents SchoolEvent[]
aiTestGenerations AITestGeneration[]
}
model ClassGroupTeacher {
@@ -240,6 +279,8 @@ model Student {
healthRecords HealthRecord[]
studentBadges StudentBadge[]
goals StudentGoal[]
illnessReports IllnessReport[]
disciplinaryCases DisciplinaryCase[]
@@index([userId])
}
@@ -285,6 +326,8 @@ model Subject {
resources Resource[]
topics SubjectTopic[]
competitions Competition[]
subjectContents SubjectContent[]
aiTestGenerations AITestGeneration[]
}
model CompetencyTemplate {
@@ -402,6 +445,8 @@ model Assessment {
teacher User @relation(fields: [teacherId], references: [id])
assessmentCompetencyLinks AssessmentCompetencyLink[]
assessmentResults AssessmentResult[]
aiTestGenerations AITestGeneration[]
gradingReviews TeacherGradingReview[]
}
model AssessmentCompetencyLink {
@@ -1937,3 +1982,449 @@ model StudentGoal {
@@index([studentId])
@@index([status])
}
// ─── Subject Content Management (Schlaukopf Clone) ──────────────────
model SubjectCategory {
id String @id @default(cuid())
schoolId String
name String // e.g. "Klasse 5", "Klasse 6", "Oberstufe"
slug String // e.g. "klasse5", "klasse6", "oberstufe"
description String?
icon String? // Lucide icon name
sortOrder Int @default(0)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
school School @relation(fields: [schoolId], references: [id])
contents SubjectContent[]
@@index([schoolId])
@@index([slug])
@@map("subject_categories")
}
model SubjectContent {
id String @id @default(cuid())
schoolId String
categoryId String
subjectId String? // Link to existing Subject
parentId String? // For nested topics
title String
slug String
description String?
icon String? // Lucide icon name
contentType String @default("topic") // topic, exercise, lesson, quiz, vocabulary
content String? // JSON content (questions, explanations, etc.)
difficulty String @default("medium") // easy, medium, hard
questionCount Int @default(0)
sortOrder Int @default(0)
isActive Boolean @default(true)
isPublic Boolean @default(true) // visible to all schools
sourceUrl String? // original schlaukopf.de URL
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
school School @relation(fields: [schoolId], references: [id])
category SubjectCategory @relation(fields: [categoryId], references: [id])
parent SubjectContent? @relation("ContentHierarchy", fields: [parentId], references: [id])
children SubjectContent[] @relation("ContentHierarchy")
changeRequests ContentChangeRequest[]
subject Subject? @relation(fields: [subjectId], references: [id])
@@index([schoolId])
@@index([categoryId])
@@index([parentId])
@@index([slug])
@@index([contentType])
@@map("subject_contents")
}
model ContentChangeRequest {
id String @id @default(cuid())
schoolId String
contentId String
requestedBy String
requestType String // edit, add, delete
title String
description String
proposedChanges String? // JSON with proposed changes
status String @default("pending") // pending, approved, rejected
reviewedBy String?
reviewNote String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
school School @relation(fields: [schoolId], references: [id])
content SubjectContent @relation(fields: [contentId], references: [id])
requester User @relation("ContentRequester", fields: [requestedBy], references: [id])
reviewer User? @relation("ContentReviewer", fields: [reviewedBy], references: [id])
@@index([schoolId])
@@index([contentId])
@@index([status])
@@index([requestedBy])
@@map("content_change_requests")
}
// ─── AI Settings (Pollination, OpenAI, Anthropic) ────────────────────
model AISettings {
id String @id @default(cuid())
schoolId String @unique
// Pollination AI
pollinationEnabled Boolean @default(true)
pollinationApiKey String?
pollinationModel String @default("flux")
// OpenAI
openaiEnabled Boolean @default(false)
openaiApiKey String?
openaiModel String @default("gpt-4o")
// Anthropic
anthropicEnabled Boolean @default(false)
anthropicApiKey String?
anthropicModel String @default("claude-3-5-sonnet-20241022")
// AI Features
aiChatEnabled Boolean @default(true) // Student AI helper
aiImageGenEnabled Boolean @default(true) // Admin/Teacher image generation
aiVideoGenEnabled Boolean @default(false) // Admin/Teacher video generation
aiAutoTestEnabled Boolean @default(true) // Auto test generation before exams
aiGradingAuditEnabled Boolean @default(false) // AI grading audit
aiTopicGenEnabled Boolean @default(true) // Auto topic generation from categories
// Virtual Character
virtualCharacterEnabled Boolean @default(true) // Enable/disable for students
// Restrictions
aiMaxRequestsPerDay Int @default(50) // Student daily limit
aiHelperMode String @default("guided") // guided, full, restricted
aiSystemPrompt String? // Custom system prompt for AI chat
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
school School @relation(fields: [schoolId], references: [id])
@@map("ai_settings")
}
// ─── Virtual Character System ────────────────────────────────────────
model VirtualCharacter {
id String @id @default(cuid())
userId String @unique
schoolId String
characterId String // e.g. "owl", "dragon", "robot", "cat", "wizard"
name String // Custom name for the character
color String @default("#10b981") // Character accent color
level Int @default(1)
xp Int @default(0)
mood String @default("happy") // happy, excited, thinking, sleepy, celebrating
accessories String? // JSON array of unlocked accessories
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
school School @relation(fields: [schoolId], references: [id])
@@index([userId])
@@index([schoolId])
@@map("virtual_characters")
}
// ─── Chat Messages (AI Chatbot & Student-Teacher) ────────────────────
model ChatMessage {
id String @id @default(cuid())
schoolId String
userId String
roomId String? // For room-based chat (null = AI chatbot)
content String
senderType String @default("user") // user, ai, teacher, system
metadata String? // JSON metadata (attachments, etc.)
isRead Boolean @default(false)
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
school School @relation(fields: [schoolId], references: [id])
room CommunicationRoom? @relation(fields: [roomId], references: [id])
@@index([userId])
@@index([roomId])
@@index([schoolId])
@@index([createdAt])
@@map("chat_messages")
}
// ─── Illness Reporting with Parent Approval ──────────────────────────
model IllnessReport {
id String @id @default(cuid())
schoolId String
studentId String
reportedBy String // userId of the person reporting
reporterType String // student, parent
reason String // illness, doctor_visit, other
description String?
startDate DateTime
endDate DateTime?
documentUrl String? // Medical certificate URL
parentApprovalStatus String @default("pending") // pending, approved, rejected
parentApprovedBy String? // userId of approving parent
parentApprovedAt DateTime?
// Status: only visible to student/parent until approved
isVisibleToTeacher Boolean @default(false)
isVisibleToAdmin Boolean @default(false)
status String @default("pending") // pending, approved, rejected, expired
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
school School @relation(fields: [schoolId], references: [id])
student Student @relation(fields: [studentId], references: [id])
reporter User @relation(fields: [reportedBy], references: [id])
approver User? @relation("IllnessApprover", fields: [parentApprovedBy], references: [id])
@@index([schoolId])
@@index([studentId])
@@index([reportedBy])
@@index([parentApprovalStatus])
@@index([status])
@@map("illness_reports")
}
// ─── Communication Rooms (Student-Teacher) ───────────────────────────
model CommunicationRoom {
id String @id @default(cuid())
schoolId String
studentId String
teacherId String
roomType String @default("chat") // chat, voice, video
status String @default("requested") // requested, active, closed
requestedBy String // student or teacher
acceptedAt DateTime?
closedAt DateTime?
closeReason String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
school School @relation(fields: [schoolId], references: [id])
student User @relation("RoomStudent", fields: [studentId], references: [id])
teacher User @relation("RoomTeacher", fields: [teacherId], references: [id])
members CommunicationRoomMember[]
messages CommunicationMessage[]
chatMessages ChatMessage[]
@@index([schoolId])
@@index([studentId])
@@index([teacherId])
@@index([status])
@@map("communication_rooms")
}
model CommunicationRoomMember {
id String @id @default(cuid())
roomId String
userId String
role String @default("member") // member, moderator
joinedAt DateTime @default(now())
room CommunicationRoom @relation(fields: [roomId], references: [id])
user User @relation(fields: [userId], references: [id])
@@unique([roomId, userId])
@@index([roomId])
@@index([userId])
@@map("communication_room_members")
}
model CommunicationMessage {
id String @id @default(cuid())
roomId String
senderId String
content String
messageType String @default("text") // text, voice, image, note_share, file
fileUrl String?
metadata String? // JSON
isRead Boolean @default(false)
createdAt DateTime @default(now())
room CommunicationRoom @relation(fields: [roomId], references: [id])
sender User @relation(fields: [senderId], references: [id])
@@index([roomId])
@@index([senderId])
@@index([createdAt])
@@map("communication_messages")
}
// ─── Counseling / Guidance System ────────────────────────────────────
model CounselingAppointment {
id String @id @default(cuid())
schoolId String
studentId String
counselorId String
requestType String @default("guidance") // guidance, psychological, career, social
description String?
status String @default("requested") // requested, scheduled, completed, cancelled
scheduledAt DateTime?
duration Int @default(30) // minutes
notes String? // Counselor notes
isPrivate Boolean @default(true)
addToCalendar Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
school School @relation(fields: [schoolId], references: [id])
counselor User @relation("Counselor", fields: [counselorId], references: [id])
student User @relation("CounselingStudent", fields: [studentId], references: [id])
@@index([schoolId])
@@index([counselorId])
@@index([studentId])
@@index([status])
@@index([scheduledAt])
@@map("counseling_appointments")
}
// ─── Disciplinary Committee System ───────────────────────────────────
model DisciplinaryCommittee {
id String @id @default(cuid())
schoolId String
name String
description String?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
school School @relation(fields: [schoolId], references: [id])
members DisciplinaryCommitteeMember[]
cases DisciplinaryCase[]
@@index([schoolId])
@@map("disciplinary_committees")
}
model DisciplinaryCommitteeMember {
id String @id @default(cuid())
committeeId String
userId String
role String @default("member") // chair, vice_chair, member
isLead Boolean @default(false)
createdAt DateTime @default(now())
committee DisciplinaryCommittee @relation(fields: [committeeId], references: [id])
user User @relation(fields: [userId], references: [id])
@@unique([committeeId, userId])
@@index([committeeId])
@@index([userId])
@@map("disciplinary_committee_members")
}
model DisciplinaryCase {
id String @id @default(cuid())
schoolId String
committeeId String
studentId String
reportedBy String
reviewedBy String?
caseType String // warning, minor_violation, major_violation, serious_offense
description String
evidence String? // JSON array of evidence URLs
status String @default("open") // open, under_review, resolved, dismissed
resolution String?
resolutionDate DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
committee DisciplinaryCommittee @relation(fields: [committeeId], references: [id])
student Student @relation(fields: [studentId], references: [id])
reporter User @relation("CaseReporter", fields: [reportedBy], references: [id])
reviewer User? @relation("CaseReviewer", fields: [reviewedBy], references: [id])
@@index([schoolId])
@@index([committeeId])
@@index([studentId])
@@index([status])
@@map("disciplinary_cases")
}
// ─── AI Test Generation ──────────────────────────────────────────────
model AITestGeneration {
id String @id @default(cuid())
schoolId String
assessmentId String? // Linked assessment
studentId String? // For individual student tests
classGroupId String? // For class-wide tests
subjectId String?
topicId String? // SubjectContent ID
testType String @default("practice") // practice, mock_exam, review
difficulty String @default("medium")
questionCount Int @default(10)
questions String? // JSON array of generated questions
aiProvider String @default("pollination") // pollination, openai, anthropic
generatedAt DateTime?
isCompleted Boolean @default(false)
completedAt DateTime?
score Float? // Student's score if completed
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
school School @relation(fields: [schoolId], references: [id])
assessment Assessment? @relation(fields: [assessmentId], references: [id])
student User? @relation(fields: [studentId], references: [id])
classGroup ClassGroup? @relation(fields: [classGroupId], references: [id])
subject Subject? @relation(fields: [subjectId], references: [id])
@@index([schoolId])
@@index([assessmentId])
@@index([studentId])
@@index([classGroupId])
@@map("ai_test_generations")
}
// ─── Teacher Grading Review (AI Audit) ───────────────────────────────
model TeacherGradingReview {
id String @id @default(cuid())
schoolId String
assessmentId String
teacherId String
aiProvider String @default("pollination")
status String @default("pending") // pending, in_progress, completed
reviewResult String? // JSON with review findings
discrepanciesFound Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
school School @relation(fields: [schoolId], references: [id])
assessment Assessment @relation(fields: [assessmentId], references: [id])
teacher User @relation(fields: [teacherId], references: [id])
comments GradingReviewComment[]
@@index([schoolId])
@@index([assessmentId])
@@index([teacherId])
@@index([status])
@@map("teacher_grading_reviews")
}
model GradingReviewComment {
id String @id @default(cuid())
reviewId String
userId String
resultId String? // AssessmentResult ID
comment String
suggestedGrade String?
reason String?
createdAt DateTime @default(now())
review TeacherGradingReview @relation(fields: [reviewId], references: [id])
user User @relation(fields: [userId], references: [id])
@@index([reviewId])
@@index([userId])
@@map("grading_review_comments")
}