// CompetenceTrack — Full Database Schema // SQLite via Prisma ORM | UUIDs for portability | UTC timestamps generator client { provider = "prisma-client-js" } datasource db { provider = "sqlite" url = env("DATABASE_URL") } // ─── Core Organizational Entities ─────────────────────────────────── model School { id String @id @default(cuid()) name String schoolType String // ELEMENTARY, MIDDLE, GYMNASIUM, OTHER country String @default("DE") timezone String @default("Europe/Berlin") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt users User[] schoolYears SchoolYear[] classGroups ClassGroup[] students Student[] subjects Subject[] competencyTemplates CompetencyTemplate[] gradingSchemes GradingScheme[] auditLogs AuditLog[] dataExportRequests DataExportRequest[] behaviorCategories BehaviorCategory[] behaviorIncidents BehaviorIncident[] rubrics Rubric[] commentCategories CommentCategory[] commentBankEntries CommentBankEntry[] notebooks Notebook[] drawings Drawing[] notifications Notification[] calendarEvents CalendarEvent[] parentStudentLinks ParentStudentLink[] } model User { id String @id @default(cuid()) schoolId String? email String @unique passwordHash String firstName String lastName String role String @default("TEACHER") // TEACHER, SCHOOL_ADMIN, SUPER_ADMIN, STUDENT, PARENT locale String @default("de") twoFactorSecret String? isDemo Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime? school School? @relation(fields: [schoolId], references: [id]) classGroupTeachers ClassGroupTeacher[] learningProgressEntries LearningProgressEntry[] assessments Assessment[] generatedReports Report[] auditLogs AuditLog[] dataExportRequests DataExportRequest[] teacherNotes TeacherNote[] attendanceSessions AttendanceSession[] lessonPlans LessonPlan[] parentMessagesSent ParentMessage[] behaviorIncidentsLogged BehaviorIncident[] @relation("BehaviorLogger") behaviorIncidentsResolved BehaviorIncident[] @relation("BehaviorResolver") rubrics Rubric[] commentBankEntries CommentBankEntry[] notebooks Notebook[] drawings Drawing[] notifications Notification[] calendarEvents CalendarEvent[] parentLinks ParentStudentLink[] } model SchoolYear { id String @id @default(cuid()) schoolId String label String // e.g. "2025/2026" startDate DateTime endDate DateTime createdAt DateTime @default(now()) updatedAt DateTime @updatedAt school School @relation(fields: [schoolId], references: [id]) classGroups ClassGroup[] enrollments Enrollment[] computedGrades ComputedGrade[] reports Report[] } model ClassGroup { id String @id @default(cuid()) schoolId String schoolYearId String name String // e.g. "3a" gradeLevel Int schoolType String @default("ELEMENTARY") seatingOrder String? // JSON array of student IDs createdAt DateTime @default(now()) updatedAt DateTime @updatedAt school School @relation(fields: [schoolId], references: [id]) schoolYear SchoolYear @relation(fields: [schoolYearId], references: [id]) teachers ClassGroupTeacher[] enrollments Enrollment[] classCompetencyAssignments ClassCompetencyAssignment[] assessments Assessment[] learningProgressEntries LearningProgressEntry[] gradingSchemes GradingScheme[] computedGrades ComputedGrade[] reports Report[] attendanceSessions AttendanceSession[] lessonPlans LessonPlan[] behaviorIncidents BehaviorIncident[] notebooks Notebook[] drawings Drawing[] calendarEvents CalendarEvent[] } model ClassGroupTeacher { id String @id @default(cuid()) classGroupId String userId String role String @default("SUBJECT_TEACHER") // HOMEROOM_TEACHER, SUBJECT_TEACHER classGroup ClassGroup @relation(fields: [classGroupId], references: [id]) user User @relation(fields: [userId], references: [id]) @@unique([classGroupId, userId]) } model Student { id String @id @default(cuid()) schoolId String firstName String lastName String dateOfBirth DateTime? externalId String? // for import matching createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime? school School @relation(fields: [schoolId], references: [id]) enrollments Enrollment[] learningProgressEntries LearningProgressEntry[] assessmentResults AssessmentResult[] computedGrades ComputedGrade[] reports Report[] teacherNotes TeacherNote[] attendanceRecords AttendanceRecord[] parentContacts ParentContact[] parentMessages ParentMessage[] behaviorIncidents BehaviorIncident[] parentStudentLinks ParentStudentLink[] } model Enrollment { id String @id @default(cuid()) studentId String classGroupId String schoolYearId String startDate DateTime endDate DateTime? // null = still enrolled student Student @relation(fields: [studentId], references: [id]) classGroup ClassGroup @relation(fields: [classGroupId], references: [id]) schoolYear SchoolYear @relation(fields: [schoolYearId], references: [id]) } // ─── Subject & Competency Structure ──────────────────────────────── model Subject { id String @id @default(cuid()) schoolId String? // null = global/template name String gradeLevelMin Int @default(1) gradeLevelMax Int @default(13) school School? @relation(fields: [schoolId], references: [id]) competencyTemplates CompetencyTemplate[] assessments Assessment[] classCompetencyAssignments ClassCompetencyAssignment[] gradingSchemes GradingScheme[] computedGrades ComputedGrade[] attendanceSessions AttendanceSession[] lessonPlans LessonPlan[] rubrics Rubric[] commentBankEntries CommentBankEntry[] notebooks Notebook[] drawings Drawing[] calendarEvents CalendarEvent[] } model CompetencyTemplate { id String @id @default(cuid()) name String description String? subjectId String? schoolType String @default("ELEMENTARY") gradeLevelMin Int @default(1) gradeLevelMax Int @default(4) isGlobalTemplate Boolean @default(false) createdByUserId String? version Int @default(1) schoolId String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt subject Subject? @relation(fields: [subjectId], references: [id]) school School? @relation(fields: [schoolId], references: [id]) categories CompetencyCategory[] classCompetencyAssignments ClassCompetencyAssignment[] } model CompetencyCategory { id String @id @default(cuid()) competencyTemplateId String name String order Int @default(0) color String? // hex color for chart petals competencyTemplate CompetencyTemplate @relation(fields: [competencyTemplateId], references: [id]) competencies Competency[] reportSections ReportSection[] } model Competency { id String @id @default(cuid()) categoryId String code String // e.g. "D.3.LES.01" title String description String? order Int @default(0) category CompetencyCategory @relation(fields: [categoryId], references: [id]) masteryLevelDefinitions MasteryLevelDefinition[] learningProgressEntries LearningProgressEntry[] assessmentCompetencyLinks AssessmentCompetencyLink[] } model MasteryLevelDefinition { id String @id @default(cuid()) competencyId String levelValue Int // e.g. 1-4 label String // e.g. "noch nicht erreicht", "entwickelt sich", "kompetent", "weit entwickelt" description String? competency Competency @relation(fields: [competencyId], references: [id]) } model ClassCompetencyAssignment { id String @id @default(cuid()) classGroupId String subjectId String competencyTemplateId String schoolYearId String clonedTemplateId String? // if customized, points to the school's cloned copy classGroup ClassGroup @relation(fields: [classGroupId], references: [id]) subject Subject @relation(fields: [subjectId], references: [id]) competencyTemplate CompetencyTemplate @relation(fields: [competencyTemplateId], references: [id]) @@unique([classGroupId, subjectId, schoolYearId]) } // ─── Progress Tracking & Assessment ──────────────────────────────── model LearningProgressEntry { id String @id @default(cuid()) studentId String competencyId String teacherId String classGroupId String date DateTime masteryLevelValue Int note String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt student Student @relation(fields: [studentId], references: [id]) competency Competency @relation(fields: [competencyId], references: [id]) teacher User @relation(fields: [teacherId], references: [id]) classGroup ClassGroup @relation(fields: [classGroupId], references: [id]) } model Assessment { id String @id @default(cuid()) classGroupId String subjectId String teacherId String title String date DateTime type String @default("TEST") // TEST, ORAL, PROJECT, HOMEWORK, OTHER maxScore Float? weight Float @default(1.0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt classGroup ClassGroup @relation(fields: [classGroupId], references: [id]) subject Subject @relation(fields: [subjectId], references: [id]) teacher User @relation(fields: [teacherId], references: [id]) assessmentCompetencyLinks AssessmentCompetencyLink[] assessmentResults AssessmentResult[] } model AssessmentCompetencyLink { id String @id @default(cuid()) assessmentId String competencyId String weight Float @default(1.0) assessment Assessment @relation(fields: [assessmentId], references: [id]) competency Competency @relation(fields: [competencyId], references: [id]) @@unique([assessmentId, competencyId]) } model AssessmentResult { id String @id @default(cuid()) assessmentId String studentId String score Float? masteryLevelValue Int? note String? assessment Assessment @relation(fields: [assessmentId], references: [id]) student Student @relation(fields: [studentId], references: [id]) @@unique([assessmentId, studentId]) } // ─── Grading ─────────────────────────────────────────────────────── model GradingScheme { id String @id @default(cuid()) classGroupId String? subjectId String? schoolId String? name String type String @default("NUMERIC_GRADE") // NUMERIC_GRADE, VERBAL_FEEDBACK, COMBINED scaleDefinition String // JSON: e.g. {"min":1,"max":6,"labels":{"1":"sehr gut","6":"ungenügend"}} classGroup ClassGroup? @relation(fields: [classGroupId], references: [id]) subject Subject? @relation(fields: [subjectId], references: [id]) school School? @relation(fields: [schoolId], references: [id]) gradingWeightRules GradingWeightRule[] } model GradingWeightRule { id String @id @default(cuid()) gradingSchemeId String sourceType String // LEARNING_PROGRESS, ASSESSMENT targetRef String? // competencyId, assessmentId, or categoryId weightPercent Float @default(50.0) gradingScheme GradingScheme @relation(fields: [gradingSchemeId], references: [id]) } model ComputedGrade { id String @id @default(cuid()) studentId String subjectId String classGroupId String schoolYearId String period String @default("Semester 1") computedValue Float computedAt DateTime @default(now()) isFinalized Boolean @default(false) overriddenValue Float? overrideReason String? student Student @relation(fields: [studentId], references: [id]) subject Subject @relation(fields: [subjectId], references: [id]) classGroup ClassGroup @relation(fields: [classGroupId], references: [id]) schoolYear SchoolYear @relation(fields: [schoolYearId], references: [id]) } // ─── Reporting ───────────────────────────────────────────────────── model Report { id String @id @default(cuid()) studentId String classGroupId String schoolYearId String period String generatedByUserId String generatedAt DateTime @default(now()) status String @default("DRAFT") // DRAFT, FINAL pdfFilePath String? includesGrades Boolean @default(false) student Student @relation(fields: [studentId], references: [id]) classGroup ClassGroup @relation(fields: [classGroupId], references: [id]) schoolYear SchoolYear @relation(fields: [schoolYearId], references: [id]) generatedByUser User @relation(fields: [generatedByUserId], references: [id]) sections ReportSection[] } model ReportSection { id String @id @default(cuid()) reportId String competencyCategoryId String? generatedText String order Int @default(0) report Report @relation(fields: [reportId], references: [id]) competencyCategory CompetencyCategory? @relation(fields: [competencyCategoryId], references: [id]) } // ─── Auditing & Compliance ───────────────────────────────────────── model AuditLog { id String @id @default(cuid()) userId String? schoolId String? action String entityType String entityId String? timestamp DateTime @default(now()) metadata String? // JSON user User? @relation(fields: [userId], references: [id]) school School? @relation(fields: [schoolId], references: [id]) } model DataExportRequest { id String @id @default(cuid()) requestedByUserId String schoolId String? scope String // STUDENT, CLASS, SCHOOL scopeId String? // studentId, classGroupId, or schoolId status String @default("PENDING") // PENDING, PROCESSING, COMPLETED, FAILED filePath String? requestedAt DateTime @default(now()) completedAt DateTime? requestedByUser User @relation(fields: [requestedByUserId], references: [id]) school School? @relation(fields: [schoolId], references: [id]) } // ─── Attendance Tracking ──────────────────────────────────────────── model AttendanceSession { id String @id @default(cuid()) classGroupId String teacherId String date DateTime subjectId String? period String? // e.g. "1. Stunde", "Morning" status String @default("OPEN") // OPEN, COMPLETED note String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt classGroup ClassGroup @relation(fields: [classGroupId], references: [id]) teacher User @relation(fields: [teacherId], references: [id]) subject Subject? @relation(fields: [subjectId], references: [id]) records AttendanceRecord[] @@index([classGroupId, date]) } model AttendanceRecord { id String @id @default(cuid()) sessionId String studentId String status String @default("PRESENT") // PRESENT, ABSENT, EXCUSED, LATE arrivalTime String? // for LATE status, e.g. "08:25" comment String? session AttendanceSession @relation(fields: [sessionId], references: [id]) student Student @relation(fields: [studentId], references: [id]) @@unique([sessionId, studentId]) @@index([studentId]) } // ─── Teacher Notes ───────────────────────────────────────────────── model TeacherNote { id String @id @default(cuid()) studentId String teacherId String category String // GENERAL, BEHAVIOR, ACADEMIC, INTERVENTION, PARENT_CONTACT content String isPrivate Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt student Student @relation(fields: [studentId], references: [id]) teacher User @relation(fields: [teacherId], references: [id]) @@index([studentId]) @@index([teacherId]) } // ─── Lesson Planning ──────────────────────────────────────────────── model LessonPlan { id String @id @default(cuid()) teacherId String classGroupId String subjectId String? title String description String? date DateTime durationMin Int @default(45) status String @default("draft") // draft|scheduled|completed|cancelled objectives String? // JSON array of strings materials String? // JSON array of strings homework String? reflection String? linkedCompetencyIds String? // JSON array of competency IDs createdAt DateTime @default(now()) updatedAt DateTime @updatedAt teacher User @relation(fields: [teacherId], references: [id]) classGroup ClassGroup @relation(fields: [classGroupId], references: [id]) subject Subject? @relation(fields: [subjectId], references: [id]) @@index([teacherId]) @@index([classGroupId]) @@index([date]) } // ─── Parent Communication ────────────────────────────────────────── model ParentContact { id String @id @default(cuid()) studentId String firstName String lastName String email String? phone String? relationship String @default("parent") // parent|guardian|emergency preferredContact String @default("email") // email|phone|both preferredLanguage String @default("de") notes String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt student Student @relation(fields: [studentId], references: [id]) messages ParentMessage[] @@index([studentId]) @@unique([studentId, email]) } model ParentMessage { id String @id @default(cuid()) parentId String teacherId String studentId String subject String body String category String @default("general") // general|progress|assessment|behavior|attendance|event priority String @default("normal") // low|normal|high|urgent status String @default("sent") // draft|sent|delivered|read|replied readAt DateTime? reply String? replyAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt parent ParentContact @relation(fields: [parentId], references: [id]) teacher User @relation(fields: [teacherId], references: [id]) student Student @relation(fields: [studentId], references: [id]) @@index([teacherId]) @@index([parentId]) @@index([studentId]) @@index([createdAt]) } // ─── Behavior Tracking ───────────────────────────────────────────── model BehaviorCategory { id String @id @default(cuid()) schoolId String school School @relation(fields: [schoolId], references: [id]) name String color String // hex color, e.g. "#10b981" valence String // "positive" | "negative" | "neutral" icon String? // emoji createdAt DateTime @default(now()) updatedAt DateTime @updatedAt incidents BehaviorIncident[] @@unique([schoolId, name]) @@index([schoolId]) } model BehaviorIncident { id String @id @default(cuid()) studentId String student Student @relation(fields: [studentId], references: [id]) teacherId String teacher User @relation("BehaviorLogger", fields: [teacherId], references: [id]) classGroupId String? classGroup ClassGroup? @relation(fields: [classGroupId], references: [id]) schoolId String school School @relation(fields: [schoolId], references: [id]) categoryId String category BehaviorCategory @relation(fields: [categoryId], references: [id]) date DateTime severity String // "minor" | "moderate" | "major" description String location String? // e.g., "Klassenzimmer", "Pausenhof", "Flur" followUpAction String? // e.g., "Elterngespräch", "Nachsitzen", "Lob im Kreis" resolved Boolean @default(false) resolvedAt DateTime? resolvedById String? resolvedBy User? @relation("BehaviorResolver", fields: [resolvedById], references: [id]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([studentId, date]) @@index([classGroupId, date]) @@index([schoolId, date]) @@index([teacherId]) } // ─── Rubric Library ──────────────────────────────────────────────── model Rubric { id String @id @default(cuid()) schoolId String school School @relation(fields: [schoolId], references: [id]) teacherId String teacher User @relation(fields: [teacherId], references: [id]) subjectId String? subject Subject? @relation(fields: [subjectId], references: [id]) title String description String? type String @default("ANALYTIC") // ANALYTIC | HOLISTIC maxPoints Int @default(100) isPublic Boolean @default(false) // shared across school version Int @default(1) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt criteria RubricCriterion[] @@index([schoolId]) @@index([teacherId]) @@index([subjectId]) } model RubricCriterion { id String @id @default(cuid()) rubricId String rubric Rubric @relation(fields: [rubricId], references: [id], onDelete: Cascade) name String // e.g., "Inhalt", "Sprache", "Darstellung" description String? weight Float @default(1.0) // relative weight for this criterion maxPoints Int // max points for this criterion order Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt levels RubricLevel[] @@index([rubricId]) } model RubricLevel { id String @id @default(cuid()) criterionId String criterion RubricCriterion @relation(fields: [criterionId], references: [id], onDelete: Cascade) label String // e.g., "Sehr gut", "Gut", "Befriedigend", "Ausreichend" description String // descriptor for this level points Int // points for this level order Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([criterionId]) } // ─── Report Card Comments Bank ──────────────────────────────────── model CommentCategory { id String @id @default(cuid()) schoolId String school School @relation(fields: [schoolId], references: [id]) name String color String // hex color icon String? // emoji createdAt DateTime @default(now()) updatedAt DateTime @updatedAt comments CommentBankEntry[] @@unique([schoolId, name]) @@index([schoolId]) } model CommentBankEntry { id String @id @default(cuid()) schoolId String school School @relation(fields: [schoolId], references: [id]) teacherId String teacher User @relation(fields: [teacherId], references: [id]) categoryId String category CommentCategory @relation(fields: [categoryId], references: [id]) subjectId String? subject Subject? @relation(fields: [subjectId], references: [id]) title String // short title text String // the full comment text gradeLevel String? // e.g., "3", "3-4", "all" — which grade levels this applies to schoolType String? // e.g., "ELEMENTARY", "SECONDARY" isPublic Boolean @default(false) // shared across school usageCount Int @default(0) // how many times used tags String? // comma-separated tags for search createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([schoolId]) @@index([teacherId]) @@index([categoryId]) @@index([subjectId]) } // ─── Digital Notebooks ───────────────────────────────────────────── model Notebook { id String @id @default(cuid()) schoolId String ownerId String // userId of the student or teacher ownerType String // STUDENT | TEACHER subjectId String? // null = personal/general notebook classGroupId String? title String description String? notebookType String @default("lined") // lined | grid | blank | dotted | music | calligraphy color String @default("#10b981") // cover color icon String? // lucide icon name isArchived Boolean @default(false) isPublic Boolean @default(false) // shared with class sortOrder Int @default(0) isDemo Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime? school School @relation(fields: [schoolId], references: [id]) owner User @relation(fields: [ownerId], references: [id]) subject Subject? @relation(fields: [subjectId], references: [id]) classGroup ClassGroup? @relation(fields: [classGroupId], references: [id]) pages NotebookPage[] @@index([schoolId]) @@index([ownerId]) @@index([subjectId]) @@index([classGroupId]) @@index([isArchived]) } model NotebookPage { id String @id @default(cuid()) notebookId String pageNumber Int title String? contentType String @default("text") // text | drawing | mixed textContent String? // markdown/rich text content drawingData String? // JSON with strokes/paths for canvas background String @default("lined") // lined | grid | blank | dotted | music isBookmark Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt notebook Notebook @relation(fields: [notebookId], references: [id], onDelete: Cascade) versions NotebookPageVersion[] @@index([notebookId]) @@index([pageNumber]) } model NotebookPageVersion { id String @id @default(cuid()) pageId String version Int textContent String? drawingData String? editedBy String? editSummary String? createdAt DateTime @default(now()) page NotebookPage @relation(fields: [pageId], references: [id], onDelete: Cascade) @@index([pageId, version]) @@map("notebook_page_versions") } // ─── Drawing Canvas ──────────────────────────────────────────────── model Drawing { id String @id @default(cuid()) schoolId String ownerId String title String description String? drawingData String // JSON with all strokes, paths, colors imageData String? // base64 encoded PNG thumbnail subjectId String? classGroupId String? isPublic Boolean @default(false) isDemo Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt school School @relation(fields: [schoolId], references: [id]) owner User @relation(fields: [ownerId], references: [id]) subject Subject? @relation(fields: [subjectId], references: [id]) classGroup ClassGroup? @relation(fields: [classGroupId], references: [id]) @@index([schoolId]) @@index([ownerId]) @@index([subjectId]) } model Notification { id String @id @default(cuid()) schoolId String userId String // recipient type String // ASSESSMENT_DUE, MISSING_OBSERVATION, NOTEBOOK_SHARED, BEHAVIOR_ALERT, GRADE_COMPUTED, etc. title String message String isRead Boolean @default(false) actionUrl String? // optional deep link relatedId String? // optional related entity ID createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime? school School @relation(fields: [schoolId], references: [id]) user User @relation(fields: [userId], references: [id]) @@index([userId, isRead]) @@index([schoolId]) @@map("notifications") } // ─── Calendar Events ────────────────────────────────────────────── model CalendarEvent { id String @id @default(cuid()) schoolId String teacherId String title String date DateTime startTime String? // HH:mm format, null = all day endTime String? // HH:mm format, null = all day eventType String @default("reminder") // assessment | lesson | reminder subjectId String? classGroupId String? notes String? allDay Boolean @default(false) recurrencePattern String? // JSON: { type: "daily|weekly|monthly|yearly", interval: 1, endDate?: string, daysOfWeek?: number[] } recurrenceEnd DateTime? // When recurrence ends parentEventId String? // For generated instances of a recurring event createdAt DateTime @default(now()) updatedAt DateTime @updatedAt school School @relation(fields: [schoolId], references: [id]) teacher User @relation(fields: [teacherId], references: [id]) subject Subject? @relation(fields: [subjectId], references: [id]) classGroup ClassGroup? @relation(fields: [classGroupId], references: [id]) parentEvent CalendarEvent? @relation("EventRecurrence", fields: [parentEventId], references: [id]) childEvents CalendarEvent[] @relation("EventRecurrence") @@index([schoolId]) @@index([teacherId]) @@index([date]) @@index([subjectId]) @@index([classGroupId]) @@index([parentEventId]) } // ─── Parent-Student Link ────────────────────────────────────────────── model ParentStudentLink { id String @id @default(cuid()) parentId String // User with PARENT role studentId String // Student record schoolId String relationship String? // mother, father, guardian, etc. createdAt DateTime @default(now()) updatedAt DateTime @updatedAt parent User @relation(fields: [parentId], references: [id]) student Student @relation(fields: [studentId], references: [id]) school School @relation(fields: [schoolId], references: [id]) @@unique([parentId, studentId]) @@map("parent_student_links") }