diff --git a/docs/legal/avv-template.md b/docs/legal/avv-template.md new file mode 100644 index 0000000..ac6f1db --- /dev/null +++ b/docs/legal/avv-template.md @@ -0,0 +1,207 @@ +# Auftragsverarbeitungsvertrag (AVV) +# Data Processor Agreement (Art. 28 GDPR) + +**HINWEIS**: This is a template only. Your school must have its legal counsel review and customize this agreement before signing with any third-party data processor (e.g., if you outsource hosting). + +**For self-hosted deployments**: This agreement is NOT required. It only applies if your school contracts with an external party to operate the platform. + +--- + +## 1. Parties + +**Controller (Auftraggeber):** +- School Name: ________________________ +- Address: ________________________ +- Legal Representative: ________________________ + +**Processor (Auftragnehmer):** +- Service Provider Name: ________________________ +- Address: ________________________ +- Contact: ________________________ + +--- + +## 2. Scope of Processing + +The Processor shall process the following personal data on behalf of the Controller: + +- Student records (names, birth dates, assessments, grades) +- Teacher information and activity logs +- Parent contact information +- Learning progress and behavioral records +- Exam monitoring logs +- Communication/messaging data + +**Purpose**: Operation and maintenance of the ExaMetra school management platform + +--- + +## 3. Duration + +This agreement is effective from: ________________________ +Term: ________________________ +Renewal/Termination terms: ________________________ + +--- + +## 4. Responsibilities of the Processor + +The Processor shall: + +4.1 **Data Minimization** +- Process only the data categories strictly necessary for the purpose +- Not use data for any other purpose without written consent from the Controller + +4.2 **Security** +- Implement appropriate technical and organizational measures: + - Encryption of data at rest and in transit (TLS 1.2+, AES-256) + - Role-based access control (RBAC) + - Regular security audits and penetration testing + - Incident response procedures + - Staff training on data protection + +4.3 **Sub-processors** +- Not engage sub-processors without prior written authorization from the Controller +- Ensure sub-processors are bound by equivalent data protection obligations +- Maintain a current list of authorized sub-processors + +4.4 **Data Subject Rights** +- Assist the Controller in fulfilling data subject requests (access, deletion, portability) +- Respond to requests within 10 business days + +4.5 **Audit & Inspection** +- Allow the Controller and/or its DPO to audit processing activities +- Provide auditing certificates (ISO 27001, SOC 2) upon request +- Maintain documentation of processing activities + +4.6 **Confidentiality** +- Ensure all staff with access to personal data are bound by confidentiality +- Disclose data only to authorized individuals for the defined purpose + +--- + +## 5. Responsibilities of the Controller + +The Controller shall: + +5.1 **Authorization** +- Ensure processing is lawful under applicable law (GDPR, Schulgesetz) +- Obtain parental/student consent where required (Art. 8 GDPR for minors) + +5.2 **Instructions** +- Provide written instructions for all processing activities +- Prohibit any processing outside the defined scope + +5.3 **Data Quality** +- Ensure data is accurate and up-to-date +- Obtain informed consent from data subjects (or parents for minors) + +--- + +## 6. Data Breach Notification + +6.1 **Processor Notification** +If the Processor discovers a data breach or suspected unauthorized processing, it shall notify the Controller within **24 hours** with: +- Description of the breach +- Categories and approximate number of affected individuals +- Likely consequences +- Measures taken or proposed to address the breach + +6.2 **Controller's Reporting Duty** +The Controller is responsible for notifying the relevant supervisory authority (Landesbeauftragte) and affected individuals, as required by GDPR Art. 33-34. + +--- + +## 7. Data Deletion & Return + +Upon termination of this agreement, the Processor shall, at the Controller's choice: + +- Delete all personal data and existing copies (unless retention is required by law) +- Return all personal data in a structured, commonly used, machine-readable format + +Deletion shall be completed within **30 days** of termination and certified in writing. + +--- + +## 8. Data Transfer & Location + +8.1 **Processing Location** +Data shall be processed exclusively within the **European Union/EEA**. + +8.2 **International Transfers** +Any transfer of data outside the EU/EEA requires: +- Prior written consent from the Controller +- Compliance with GDPR Chapter 5 (adequacy decisions or appropriate safeguards) +- Standard Contractual Clauses (SCCs) if required + +--- + +## 9. Term & Termination + +9.1 **Term** +This agreement remains effective for the duration of the service contract. + +9.2 **Termination** +Either party may terminate with **30 days written notice** if the other party materially breaches this agreement and fails to remedy within 15 days. + +9.3 **Data Handling Upon Termination** +Upon termination, the Processor shall delete or return all personal data as directed by the Controller (Section 7). + +--- + +## 10. Liability + +The Processor shall indemnify the Controller for: +- Breaches of this agreement or applicable data protection law +- Unauthorized processing or disclosure of data +- Failure to implement required security measures + +Liability is limited to direct damages and shall not exceed annual fees under the service contract. + +--- + +## 11. Signatures + +**Controller (School)** + +Name: ________________________ +Title: ________________________ +Signature: ________________________ +Date: ________________________ + +**Processor (Service Provider)** + +Name: ________________________ +Title: ________________________ +Signature: ________________________ +Date: ________________________ + +--- + +## Appendix A: Data Categories & Retention Periods + +| Data Category | Retention Period | Legal Basis | +|---|---|---| +| Student grades & assessments | 1-2 years | Schulgesetz | +| Final reports | Permanent | Schulgesetz | +| Disciplinary records | 3-10 years | Varies by Bundesland | +| Audit logs | 1-2 years | GDPR compliance | + +--- + +## Appendix B: Authorized Sub-Processors + +The Processor is authorized to engage the following sub-processors: + +1. Cloud Infrastructure: ________________________ (unless otherwise specified) +2. Backup & Disaster Recovery: ________________________ +3. Security Monitoring: ________________________ + +**Note**: The Processor shall notify the Controller of any new sub-processors at least 30 days in advance, giving the Controller the right to object. + +--- + +**Generated**: ISO 8601 timestamp +**Source**: ExaMetra Module L - Compliance Framework + +**REMINDER**: This is a template. Consult legal counsel to customize for your jurisdiction and service provider. diff --git a/prisma/db/custom.db b/prisma/db/custom.db new file mode 100644 index 0000000..7902c76 Binary files /dev/null and b/prisma/db/custom.db differ diff --git a/prisma/migrations/20260731132904_add_module_l_compliance/migration.sql b/prisma/migrations/20260731132904_add_module_l_compliance/migration.sql new file mode 100644 index 0000000..61a493f --- /dev/null +++ b/prisma/migrations/20260731132904_add_module_l_compliance/migration.sql @@ -0,0 +1,3222 @@ +-- CreateTable +CREATE TABLE "School" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "schoolType" TEXT NOT NULL, + "country" TEXT NOT NULL DEFAULT 'DE', + "timezone" TEXT NOT NULL DEFAULT 'Europe/Berlin', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "logoUrl" TEXT, + "primaryColor" TEXT DEFAULT '#10b981', + "secondaryColor" TEXT DEFAULT '#14b8a6', + "accentColor" TEXT DEFAULT '#059669', + "fontFamily" TEXT DEFAULT 'Inter', + "customCss" TEXT, + "motto" TEXT, + "websiteUrl" TEXT, + "emailDomain" TEXT, + "address" TEXT, + "phone" TEXT, + "districtId" TEXT, + CONSTRAINT "School_districtId_fkey" FOREIGN KEY ("districtId") REFERENCES "school_districts" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT, + "email" TEXT NOT NULL, + "passwordHash" TEXT NOT NULL, + "firstName" TEXT NOT NULL, + "lastName" TEXT NOT NULL, + "role" TEXT NOT NULL DEFAULT 'TEACHER', + "locale" TEXT NOT NULL DEFAULT 'de', + "twoFactorSecret" TEXT, + "isDemo" BOOLEAN NOT NULL DEFAULT false, + "isTeacher" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "deletedAt" DATETIME, + "selectedCharacterId" TEXT, + CONSTRAINT "User_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "SchoolYear" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "label" TEXT NOT NULL, + "startDate" DATETIME NOT NULL, + "endDate" DATETIME NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "SchoolYear_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "ClassGroup" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "schoolYearId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "gradeLevel" INTEGER NOT NULL, + "schoolType" TEXT NOT NULL DEFAULT 'ELEMENTARY', + "seatingOrder" TEXT, + "responsibleTeacherId" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "ClassGroup_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "ClassGroup_schoolYearId_fkey" FOREIGN KEY ("schoolYearId") REFERENCES "SchoolYear" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "ClassGroup_responsibleTeacherId_fkey" FOREIGN KEY ("responsibleTeacherId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "ClassGroupTeacher" ( + "id" TEXT NOT NULL PRIMARY KEY, + "classGroupId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "role" TEXT NOT NULL DEFAULT 'SUBJECT_TEACHER', + CONSTRAINT "ClassGroupTeacher_classGroupId_fkey" FOREIGN KEY ("classGroupId") REFERENCES "ClassGroup" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "ClassGroupTeacher_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "Student" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "firstName" TEXT NOT NULL, + "lastName" TEXT NOT NULL, + "dateOfBirth" DATETIME, + "externalId" TEXT, + "avatarUrl" TEXT, + "avatarInitials" TEXT, + "userId" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "deletedAt" DATETIME, + CONSTRAINT "Student_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "Student_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "Enrollment" ( + "id" TEXT NOT NULL PRIMARY KEY, + "studentId" TEXT NOT NULL, + "classGroupId" TEXT NOT NULL, + "schoolYearId" TEXT NOT NULL, + "startDate" DATETIME NOT NULL, + "endDate" DATETIME, + CONSTRAINT "Enrollment_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "Student" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "Enrollment_classGroupId_fkey" FOREIGN KEY ("classGroupId") REFERENCES "ClassGroup" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "Enrollment_schoolYearId_fkey" FOREIGN KEY ("schoolYearId") REFERENCES "SchoolYear" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "Subject" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT, + "name" TEXT NOT NULL, + "gradeLevelMin" INTEGER NOT NULL DEFAULT 1, + "gradeLevelMax" INTEGER NOT NULL DEFAULT 13, + CONSTRAINT "Subject_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "CompetencyTemplate" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "description" TEXT, + "subjectId" TEXT, + "schoolType" TEXT NOT NULL DEFAULT 'ELEMENTARY', + "gradeLevelMin" INTEGER NOT NULL DEFAULT 1, + "gradeLevelMax" INTEGER NOT NULL DEFAULT 4, + "isGlobalTemplate" BOOLEAN NOT NULL DEFAULT false, + "createdByUserId" TEXT, + "version" INTEGER NOT NULL DEFAULT 1, + "schoolId" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "CompetencyTemplate_subjectId_fkey" FOREIGN KEY ("subjectId") REFERENCES "Subject" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "CompetencyTemplate_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "CompetencyCategory" ( + "id" TEXT NOT NULL PRIMARY KEY, + "competencyTemplateId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "order" INTEGER NOT NULL DEFAULT 0, + "color" TEXT, + CONSTRAINT "CompetencyCategory_competencyTemplateId_fkey" FOREIGN KEY ("competencyTemplateId") REFERENCES "CompetencyTemplate" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "Competency" ( + "id" TEXT NOT NULL PRIMARY KEY, + "categoryId" TEXT NOT NULL, + "code" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "order" INTEGER NOT NULL DEFAULT 0, + CONSTRAINT "Competency_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "CompetencyCategory" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "MasteryLevelDefinition" ( + "id" TEXT NOT NULL PRIMARY KEY, + "competencyId" TEXT NOT NULL, + "levelValue" INTEGER NOT NULL, + "label" TEXT NOT NULL, + "description" TEXT, + CONSTRAINT "MasteryLevelDefinition_competencyId_fkey" FOREIGN KEY ("competencyId") REFERENCES "Competency" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "ClassCompetencyAssignment" ( + "id" TEXT NOT NULL PRIMARY KEY, + "classGroupId" TEXT NOT NULL, + "subjectId" TEXT NOT NULL, + "competencyTemplateId" TEXT NOT NULL, + "schoolYearId" TEXT NOT NULL, + "clonedTemplateId" TEXT, + CONSTRAINT "ClassCompetencyAssignment_classGroupId_fkey" FOREIGN KEY ("classGroupId") REFERENCES "ClassGroup" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "ClassCompetencyAssignment_subjectId_fkey" FOREIGN KEY ("subjectId") REFERENCES "Subject" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "ClassCompetencyAssignment_competencyTemplateId_fkey" FOREIGN KEY ("competencyTemplateId") REFERENCES "CompetencyTemplate" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "LearningProgressEntry" ( + "id" TEXT NOT NULL PRIMARY KEY, + "studentId" TEXT NOT NULL, + "competencyId" TEXT NOT NULL, + "teacherId" TEXT NOT NULL, + "classGroupId" TEXT NOT NULL, + "date" DATETIME NOT NULL, + "masteryLevelValue" INTEGER NOT NULL, + "note" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "LearningProgressEntry_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "Student" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "LearningProgressEntry_competencyId_fkey" FOREIGN KEY ("competencyId") REFERENCES "Competency" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "LearningProgressEntry_teacherId_fkey" FOREIGN KEY ("teacherId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "LearningProgressEntry_classGroupId_fkey" FOREIGN KEY ("classGroupId") REFERENCES "ClassGroup" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "Assessment" ( + "id" TEXT NOT NULL PRIMARY KEY, + "classGroupId" TEXT NOT NULL, + "subjectId" TEXT NOT NULL, + "teacherId" TEXT NOT NULL, + "title" TEXT NOT NULL, + "date" DATETIME NOT NULL, + "type" TEXT NOT NULL DEFAULT 'TEST', + "maxScore" REAL, + "weight" REAL NOT NULL DEFAULT 1.0, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "Assessment_classGroupId_fkey" FOREIGN KEY ("classGroupId") REFERENCES "ClassGroup" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "Assessment_subjectId_fkey" FOREIGN KEY ("subjectId") REFERENCES "Subject" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "Assessment_teacherId_fkey" FOREIGN KEY ("teacherId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "AssessmentCompetencyLink" ( + "id" TEXT NOT NULL PRIMARY KEY, + "assessmentId" TEXT NOT NULL, + "competencyId" TEXT NOT NULL, + "weight" REAL NOT NULL DEFAULT 1.0, + CONSTRAINT "AssessmentCompetencyLink_assessmentId_fkey" FOREIGN KEY ("assessmentId") REFERENCES "Assessment" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "AssessmentCompetencyLink_competencyId_fkey" FOREIGN KEY ("competencyId") REFERENCES "Competency" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "AssessmentResult" ( + "id" TEXT NOT NULL PRIMARY KEY, + "assessmentId" TEXT NOT NULL, + "studentId" TEXT NOT NULL, + "score" REAL, + "masteryLevelValue" INTEGER, + "note" TEXT, + "annotationData" TEXT, + "annotationImage" TEXT, + CONSTRAINT "AssessmentResult_assessmentId_fkey" FOREIGN KEY ("assessmentId") REFERENCES "Assessment" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "AssessmentResult_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "Student" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "GradingScheme" ( + "id" TEXT NOT NULL PRIMARY KEY, + "classGroupId" TEXT, + "subjectId" TEXT, + "schoolId" TEXT, + "name" TEXT NOT NULL, + "type" TEXT NOT NULL DEFAULT 'NUMERIC_GRADE', + "scaleDefinition" TEXT NOT NULL, + CONSTRAINT "GradingScheme_classGroupId_fkey" FOREIGN KEY ("classGroupId") REFERENCES "ClassGroup" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "GradingScheme_subjectId_fkey" FOREIGN KEY ("subjectId") REFERENCES "Subject" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "GradingScheme_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "GradingWeightRule" ( + "id" TEXT NOT NULL PRIMARY KEY, + "gradingSchemeId" TEXT NOT NULL, + "sourceType" TEXT NOT NULL, + "targetRef" TEXT, + "weightPercent" REAL NOT NULL DEFAULT 50.0, + CONSTRAINT "GradingWeightRule_gradingSchemeId_fkey" FOREIGN KEY ("gradingSchemeId") REFERENCES "GradingScheme" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "ComputedGrade" ( + "id" TEXT NOT NULL PRIMARY KEY, + "studentId" TEXT NOT NULL, + "subjectId" TEXT NOT NULL, + "classGroupId" TEXT NOT NULL, + "schoolYearId" TEXT NOT NULL, + "period" TEXT NOT NULL DEFAULT 'Semester 1', + "computedValue" REAL NOT NULL, + "computedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isFinalized" BOOLEAN NOT NULL DEFAULT false, + "overriddenValue" REAL, + "overrideReason" TEXT, + CONSTRAINT "ComputedGrade_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "Student" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "ComputedGrade_subjectId_fkey" FOREIGN KEY ("subjectId") REFERENCES "Subject" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "ComputedGrade_classGroupId_fkey" FOREIGN KEY ("classGroupId") REFERENCES "ClassGroup" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "ComputedGrade_schoolYearId_fkey" FOREIGN KEY ("schoolYearId") REFERENCES "SchoolYear" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "Report" ( + "id" TEXT NOT NULL PRIMARY KEY, + "studentId" TEXT NOT NULL, + "classGroupId" TEXT NOT NULL, + "schoolYearId" TEXT NOT NULL, + "period" TEXT NOT NULL, + "generatedByUserId" TEXT NOT NULL, + "generatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "status" TEXT NOT NULL DEFAULT 'DRAFT', + "pdfFilePath" TEXT, + "includesGrades" BOOLEAN NOT NULL DEFAULT false, + "teacherComments" TEXT, + "attendanceSummary" TEXT, + "overallAssessment" TEXT, + "templateId" TEXT, + "reviewedByUserId" TEXT, + "reviewedAt" DATETIME, + "publishedAt" DATETIME, + CONSTRAINT "Report_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "Student" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "Report_classGroupId_fkey" FOREIGN KEY ("classGroupId") REFERENCES "ClassGroup" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "Report_schoolYearId_fkey" FOREIGN KEY ("schoolYearId") REFERENCES "SchoolYear" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "Report_generatedByUserId_fkey" FOREIGN KEY ("generatedByUserId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "Report_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "ReportCardTemplate" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "ReportCardTemplate" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "sections" TEXT NOT NULL DEFAULT '[]', + "gradingScale" TEXT NOT NULL DEFAULT '{}', + "layout" TEXT NOT NULL DEFAULT 'default', + "isDefault" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "ReportCardTemplate_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "ReportSection" ( + "id" TEXT NOT NULL PRIMARY KEY, + "reportId" TEXT NOT NULL, + "competencyCategoryId" TEXT, + "generatedText" TEXT NOT NULL, + "order" INTEGER NOT NULL DEFAULT 0, + CONSTRAINT "ReportSection_reportId_fkey" FOREIGN KEY ("reportId") REFERENCES "Report" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "ReportSection_competencyCategoryId_fkey" FOREIGN KEY ("competencyCategoryId") REFERENCES "CompetencyCategory" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "AuditLog" ( + "id" TEXT NOT NULL PRIMARY KEY, + "userId" TEXT, + "schoolId" TEXT, + "action" TEXT NOT NULL, + "entityType" TEXT NOT NULL, + "entityId" TEXT, + "changes" TEXT, + "ipAddress" TEXT, + "userAgent" TEXT, + "timestamp" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "metadata" TEXT, + "dataSubject" TEXT, + CONSTRAINT "AuditLog_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "AuditLog_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "DataExportRequest" ( + "id" TEXT NOT NULL PRIMARY KEY, + "requestedByUserId" TEXT NOT NULL, + "schoolId" TEXT, + "scope" TEXT NOT NULL, + "scopeId" TEXT, + "status" TEXT NOT NULL DEFAULT 'PENDING', + "filePath" TEXT, + "requestedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "completedAt" DATETIME, + CONSTRAINT "DataExportRequest_requestedByUserId_fkey" FOREIGN KEY ("requestedByUserId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "DataExportRequest_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "AttendanceSession" ( + "id" TEXT NOT NULL PRIMARY KEY, + "classGroupId" TEXT NOT NULL, + "teacherId" TEXT NOT NULL, + "date" DATETIME NOT NULL, + "subjectId" TEXT, + "period" TEXT, + "status" TEXT NOT NULL DEFAULT 'OPEN', + "note" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "AttendanceSession_classGroupId_fkey" FOREIGN KEY ("classGroupId") REFERENCES "ClassGroup" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "AttendanceSession_teacherId_fkey" FOREIGN KEY ("teacherId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "AttendanceSession_subjectId_fkey" FOREIGN KEY ("subjectId") REFERENCES "Subject" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "AttendanceRecord" ( + "id" TEXT NOT NULL PRIMARY KEY, + "sessionId" TEXT NOT NULL, + "studentId" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'PRESENT', + "arrivalTime" TEXT, + "comment" TEXT, + CONSTRAINT "AttendanceRecord_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "AttendanceSession" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "AttendanceRecord_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "Student" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "TeacherNote" ( + "id" TEXT NOT NULL PRIMARY KEY, + "studentId" TEXT NOT NULL, + "teacherId" TEXT NOT NULL, + "category" TEXT NOT NULL, + "content" TEXT NOT NULL, + "isPrivate" BOOLEAN NOT NULL DEFAULT true, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "TeacherNote_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "Student" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "TeacherNote_teacherId_fkey" FOREIGN KEY ("teacherId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "LessonPlan" ( + "id" TEXT NOT NULL PRIMARY KEY, + "teacherId" TEXT NOT NULL, + "classGroupId" TEXT NOT NULL, + "subjectId" TEXT, + "title" TEXT NOT NULL, + "description" TEXT, + "date" DATETIME NOT NULL, + "durationMin" INTEGER NOT NULL DEFAULT 45, + "status" TEXT NOT NULL DEFAULT 'draft', + "objectives" TEXT, + "materials" TEXT, + "homework" TEXT, + "reflection" TEXT, + "linkedCompetencyIds" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "LessonPlan_teacherId_fkey" FOREIGN KEY ("teacherId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "LessonPlan_classGroupId_fkey" FOREIGN KEY ("classGroupId") REFERENCES "ClassGroup" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "LessonPlan_subjectId_fkey" FOREIGN KEY ("subjectId") REFERENCES "Subject" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "ParentContact" ( + "id" TEXT NOT NULL PRIMARY KEY, + "studentId" TEXT NOT NULL, + "firstName" TEXT NOT NULL, + "lastName" TEXT NOT NULL, + "email" TEXT, + "phone" TEXT, + "relationship" TEXT NOT NULL DEFAULT 'parent', + "preferredContact" TEXT NOT NULL DEFAULT 'email', + "preferredLanguage" TEXT NOT NULL DEFAULT 'de', + "notes" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "ParentContact_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "Student" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "ParentMessage" ( + "id" TEXT NOT NULL PRIMARY KEY, + "parentId" TEXT NOT NULL, + "teacherId" TEXT NOT NULL, + "studentId" TEXT NOT NULL, + "subject" TEXT NOT NULL, + "body" TEXT NOT NULL, + "category" TEXT NOT NULL DEFAULT 'general', + "priority" TEXT NOT NULL DEFAULT 'normal', + "status" TEXT NOT NULL DEFAULT 'sent', + "readAt" DATETIME, + "reply" TEXT, + "replyAt" DATETIME, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "ParentMessage_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "ParentContact" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "ParentMessage_teacherId_fkey" FOREIGN KEY ("teacherId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "ParentMessage_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "Student" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "BehaviorCategory" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "color" TEXT NOT NULL, + "valence" TEXT NOT NULL, + "icon" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "BehaviorCategory_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "BehaviorIncident" ( + "id" TEXT NOT NULL PRIMARY KEY, + "studentId" TEXT NOT NULL, + "teacherId" TEXT NOT NULL, + "classGroupId" TEXT, + "schoolId" TEXT NOT NULL, + "categoryId" TEXT NOT NULL, + "date" DATETIME NOT NULL, + "severity" TEXT NOT NULL, + "description" TEXT NOT NULL, + "location" TEXT, + "followUpAction" TEXT, + "resolved" BOOLEAN NOT NULL DEFAULT false, + "resolvedAt" DATETIME, + "resolvedById" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "BehaviorIncident_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "Student" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "BehaviorIncident_teacherId_fkey" FOREIGN KEY ("teacherId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "BehaviorIncident_classGroupId_fkey" FOREIGN KEY ("classGroupId") REFERENCES "ClassGroup" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "BehaviorIncident_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "BehaviorIncident_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "BehaviorCategory" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "BehaviorIncident_resolvedById_fkey" FOREIGN KEY ("resolvedById") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "Rubric" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "teacherId" TEXT NOT NULL, + "subjectId" TEXT, + "title" TEXT NOT NULL, + "description" TEXT, + "type" TEXT NOT NULL DEFAULT 'ANALYTIC', + "maxPoints" INTEGER NOT NULL DEFAULT 100, + "isPublic" BOOLEAN NOT NULL DEFAULT false, + "version" INTEGER NOT NULL DEFAULT 1, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "Rubric_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "Rubric_teacherId_fkey" FOREIGN KEY ("teacherId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "Rubric_subjectId_fkey" FOREIGN KEY ("subjectId") REFERENCES "Subject" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "RubricCriterion" ( + "id" TEXT NOT NULL PRIMARY KEY, + "rubricId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "weight" REAL NOT NULL DEFAULT 1.0, + "maxPoints" INTEGER NOT NULL, + "order" INTEGER NOT NULL DEFAULT 0, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "RubricCriterion_rubricId_fkey" FOREIGN KEY ("rubricId") REFERENCES "Rubric" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "RubricLevel" ( + "id" TEXT NOT NULL PRIMARY KEY, + "criterionId" TEXT NOT NULL, + "label" TEXT NOT NULL, + "description" TEXT NOT NULL, + "points" INTEGER NOT NULL, + "order" INTEGER NOT NULL DEFAULT 0, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "RubricLevel_criterionId_fkey" FOREIGN KEY ("criterionId") REFERENCES "RubricCriterion" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "CommentCategory" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "color" TEXT NOT NULL, + "icon" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "CommentCategory_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "CommentBankEntry" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "teacherId" TEXT NOT NULL, + "categoryId" TEXT NOT NULL, + "subjectId" TEXT, + "title" TEXT NOT NULL, + "text" TEXT NOT NULL, + "gradeLevel" TEXT, + "schoolType" TEXT, + "isPublic" BOOLEAN NOT NULL DEFAULT false, + "usageCount" INTEGER NOT NULL DEFAULT 0, + "tags" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "CommentBankEntry_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "CommentBankEntry_teacherId_fkey" FOREIGN KEY ("teacherId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "CommentBankEntry_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "CommentCategory" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "CommentBankEntry_subjectId_fkey" FOREIGN KEY ("subjectId") REFERENCES "Subject" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "Notebook" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "ownerId" TEXT NOT NULL, + "ownerType" TEXT NOT NULL, + "subjectId" TEXT, + "classGroupId" TEXT, + "title" TEXT NOT NULL, + "description" TEXT, + "notebookType" TEXT NOT NULL DEFAULT 'lined', + "color" TEXT NOT NULL DEFAULT '#10b981', + "icon" TEXT, + "isArchived" BOOLEAN NOT NULL DEFAULT false, + "isPublic" BOOLEAN NOT NULL DEFAULT false, + "sortOrder" INTEGER NOT NULL DEFAULT 0, + "isDemo" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "deletedAt" DATETIME, + CONSTRAINT "Notebook_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "Notebook_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "Notebook_subjectId_fkey" FOREIGN KEY ("subjectId") REFERENCES "Subject" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "Notebook_classGroupId_fkey" FOREIGN KEY ("classGroupId") REFERENCES "ClassGroup" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "NotebookPage" ( + "id" TEXT NOT NULL PRIMARY KEY, + "notebookId" TEXT NOT NULL, + "pageNumber" INTEGER NOT NULL, + "title" TEXT, + "contentType" TEXT NOT NULL DEFAULT 'text', + "textContent" TEXT, + "drawingData" TEXT, + "background" TEXT NOT NULL DEFAULT 'lined', + "isBookmark" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "NotebookPage_notebookId_fkey" FOREIGN KEY ("notebookId") REFERENCES "Notebook" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "notebook_page_versions" ( + "id" TEXT NOT NULL PRIMARY KEY, + "pageId" TEXT NOT NULL, + "version" INTEGER NOT NULL, + "textContent" TEXT, + "drawingData" TEXT, + "editedBy" TEXT, + "editSummary" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "notebook_page_versions_pageId_fkey" FOREIGN KEY ("pageId") REFERENCES "NotebookPage" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "Drawing" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "ownerId" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "drawingData" TEXT NOT NULL, + "imageData" TEXT, + "subjectId" TEXT, + "classGroupId" TEXT, + "isPublic" BOOLEAN NOT NULL DEFAULT false, + "isDemo" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "Drawing_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "Drawing_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "Drawing_subjectId_fkey" FOREIGN KEY ("subjectId") REFERENCES "Subject" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "Drawing_classGroupId_fkey" FOREIGN KEY ("classGroupId") REFERENCES "ClassGroup" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "notifications" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "type" TEXT NOT NULL, + "category" TEXT NOT NULL DEFAULT 'academic', + "priority" TEXT NOT NULL DEFAULT 'normal', + "title" TEXT NOT NULL, + "message" TEXT NOT NULL, + "isRead" BOOLEAN NOT NULL DEFAULT false, + "isArchived" BOOLEAN NOT NULL DEFAULT false, + "actionUrl" TEXT, + "relatedId" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "deletedAt" DATETIME, + CONSTRAINT "notifications_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "notifications_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "CalendarEvent" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "teacherId" TEXT NOT NULL, + "title" TEXT NOT NULL, + "date" DATETIME NOT NULL, + "startTime" TEXT, + "endTime" TEXT, + "eventType" TEXT NOT NULL DEFAULT 'reminder', + "subjectId" TEXT, + "classGroupId" TEXT, + "notes" TEXT, + "allDay" BOOLEAN NOT NULL DEFAULT false, + "recurrencePattern" TEXT, + "recurrenceEnd" DATETIME, + "parentEventId" TEXT, + "assessmentId" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "CalendarEvent_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "CalendarEvent_teacherId_fkey" FOREIGN KEY ("teacherId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "CalendarEvent_subjectId_fkey" FOREIGN KEY ("subjectId") REFERENCES "Subject" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "CalendarEvent_classGroupId_fkey" FOREIGN KEY ("classGroupId") REFERENCES "ClassGroup" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "CalendarEvent_parentEventId_fkey" FOREIGN KEY ("parentEventId") REFERENCES "CalendarEvent" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "parent_student_links" ( + "id" TEXT NOT NULL PRIMARY KEY, + "parentId" TEXT NOT NULL, + "studentId" TEXT NOT NULL, + "schoolId" TEXT NOT NULL, + "relationship" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "parent_student_links_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "parent_student_links_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "Student" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "parent_student_links_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "curriculum_standards" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "subjectId" TEXT, + "code" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "gradeLevel" INTEGER, + "category" TEXT, + "source" TEXT, + "isDemo" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "deletedAt" DATETIME, + CONSTRAINT "curriculum_standards_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "curriculum_standards_subjectId_fkey" FOREIGN KEY ("subjectId") REFERENCES "Subject" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "curriculum_standard_links" ( + "id" TEXT NOT NULL PRIMARY KEY, + "standardId" TEXT NOT NULL, + "competencyId" TEXT NOT NULL, + "coverageLevel" TEXT, + "notes" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "curriculum_standard_links_standardId_fkey" FOREIGN KEY ("standardId") REFERENCES "curriculum_standards" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "curriculum_standard_links_competencyId_fkey" FOREIGN KEY ("competencyId") REFERENCES "Competency" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "backups" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "filename" TEXT NOT NULL, + "size" INTEGER NOT NULL, + "type" TEXT NOT NULL, + "status" TEXT NOT NULL, + "notes" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "backups_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "behavior_interventions" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "studentId" TEXT NOT NULL, + "incidentId" TEXT, + "type" TEXT NOT NULL, + "description" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'planned', + "assignedTo" TEXT, + "startDate" DATETIME, + "endDate" DATETIME, + "outcome" TEXT, + "isDemo" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "deletedAt" DATETIME, + CONSTRAINT "behavior_interventions_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "behavior_interventions_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "Student" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "behavior_interventions_incidentId_fkey" FOREIGN KEY ("incidentId") REFERENCES "BehaviorIncident" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "behavior_interventions_assignedTo_fkey" FOREIGN KEY ("assignedTo") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "report_schedules" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "classGroupId" TEXT, + "template" TEXT NOT NULL, + "frequency" TEXT NOT NULL, + "dayOfWeek" INTEGER, + "dayOfMonth" INTEGER, + "monthOfYear" INTEGER, + "recipients" TEXT, + "includeStudents" BOOLEAN NOT NULL DEFAULT true, + "includeGrades" BOOLEAN NOT NULL DEFAULT true, + "includeAttendance" BOOLEAN NOT NULL DEFAULT true, + "includeBehavior" BOOLEAN NOT NULL DEFAULT false, + "includeCompetencies" BOOLEAN NOT NULL DEFAULT true, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "lastRunAt" DATETIME, + "nextRunAt" DATETIME, + "isDemo" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "deletedAt" DATETIME, + CONSTRAINT "report_schedules_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "report_schedules_classGroupId_fkey" FOREIGN KEY ("classGroupId") REFERENCES "ClassGroup" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "school_districts" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "code" TEXT, + "region" TEXT, + "country" TEXT NOT NULL DEFAULT 'DE', + "adminEmail" TEXT, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "deletedAt" DATETIME +); + +-- CreateTable +CREATE TABLE "homework" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "classGroupId" TEXT NOT NULL, + "subjectId" TEXT, + "teacherId" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "dueDate" DATETIME NOT NULL, + "homeworkType" TEXT NOT NULL DEFAULT 'assignment', + "maxPoints" REAL, + "attachments" TEXT, + "isPublished" BOOLEAN NOT NULL DEFAULT true, + "isDemo" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "deletedAt" DATETIME, + CONSTRAINT "homework_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "homework_classGroupId_fkey" FOREIGN KEY ("classGroupId") REFERENCES "ClassGroup" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "homework_subjectId_fkey" FOREIGN KEY ("subjectId") REFERENCES "Subject" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "homework_teacherId_fkey" FOREIGN KEY ("teacherId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "homework_submissions" ( + "id" TEXT NOT NULL PRIMARY KEY, + "homeworkId" TEXT NOT NULL, + "studentId" TEXT NOT NULL, + "content" TEXT, + "attachments" TEXT, + "status" TEXT NOT NULL DEFAULT 'pending', + "score" REAL, + "feedback" TEXT, + "submittedAt" DATETIME, + "gradedAt" DATETIME, + "isDemo" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "deletedAt" DATETIME, + CONSTRAINT "homework_submissions_homeworkId_fkey" FOREIGN KEY ("homeworkId") REFERENCES "homework" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "homework_submissions_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "Student" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "announcements" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "authorId" TEXT NOT NULL, + "title" TEXT NOT NULL, + "content" TEXT NOT NULL, + "priority" TEXT NOT NULL DEFAULT 'normal', + "announcementType" TEXT NOT NULL DEFAULT 'general', + "targetAudience" TEXT NOT NULL DEFAULT 'all', + "classGroupId" TEXT, + "isPinned" BOOLEAN NOT NULL DEFAULT false, + "expiresAt" DATETIME, + "isDemo" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "deletedAt" DATETIME, + CONSTRAINT "announcements_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "announcements_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "announcements_classGroupId_fkey" FOREIGN KEY ("classGroupId") REFERENCES "ClassGroup" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "announcement_reads" ( + "id" TEXT NOT NULL PRIMARY KEY, + "announcementId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "readAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "announcement_reads_announcementId_fkey" FOREIGN KEY ("announcementId") REFERENCES "announcements" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "announcement_reads_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "notification_preferences" ( + "id" TEXT NOT NULL PRIMARY KEY, + "userId" TEXT NOT NULL, + "schoolId" TEXT NOT NULL, + "academicEnabled" BOOLEAN NOT NULL DEFAULT true, + "behavioralEnabled" BOOLEAN NOT NULL DEFAULT true, + "administrativeEnabled" BOOLEAN NOT NULL DEFAULT true, + "calendarEnabled" BOOLEAN NOT NULL DEFAULT true, + "communicationEnabled" BOOLEAN NOT NULL DEFAULT true, + "systemEnabled" BOOLEAN NOT NULL DEFAULT true, + "quietHoursStart" TEXT, + "quietHoursEnd" TEXT, + "quietHoursEnabled" BOOLEAN NOT NULL DEFAULT false, + "emailDigestEnabled" BOOLEAN NOT NULL DEFAULT false, + "emailDigestFrequency" TEXT NOT NULL DEFAULT 'daily', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "notification_preferences_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "notification_preferences_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "self_assessments" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "studentId" TEXT NOT NULL, + "competencyId" TEXT NOT NULL, + "classGroupId" TEXT, + "selfLevel" INTEGER NOT NULL, + "confidence" INTEGER, + "reflection" TEXT, + "evidence" TEXT, + "goalId" TEXT, + "isDemo" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "deletedAt" DATETIME, + CONSTRAINT "self_assessments_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "self_assessments_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "Student" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "self_assessments_competencyId_fkey" FOREIGN KEY ("competencyId") REFERENCES "Competency" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "self_assessments_classGroupId_fkey" FOREIGN KEY ("classGroupId") REFERENCES "ClassGroup" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "self_assessments_goalId_fkey" FOREIGN KEY ("goalId") REFERENCES "learning_goals" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "learning_goals" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "studentId" TEXT NOT NULL, + "competencyId" TEXT, + "title" TEXT NOT NULL, + "description" TEXT, + "targetLevel" INTEGER, + "currentLevel" INTEGER, + "deadline" DATETIME, + "status" TEXT NOT NULL DEFAULT 'active', + "progress" INTEGER NOT NULL DEFAULT 0, + "isDemo" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "deletedAt" DATETIME, + CONSTRAINT "learning_goals_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "learning_goals_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "Student" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "learning_goals_competencyId_fkey" FOREIGN KEY ("competencyId") REFERENCES "Competency" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "portfolio_entries" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "studentId" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "entryType" TEXT NOT NULL, + "competencyId" TEXT, + "content" TEXT, + "mediaUrls" TEXT, + "notebookPageId" TEXT, + "drawingId" TEXT, + "isPublic" BOOLEAN NOT NULL DEFAULT false, + "tags" TEXT, + "isDemo" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "deletedAt" DATETIME, + CONSTRAINT "portfolio_entries_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "portfolio_entries_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "Student" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "portfolio_entries_competencyId_fkey" FOREIGN KEY ("competencyId") REFERENCES "Competency" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "timetable_slots" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "classGroupId" TEXT NOT NULL, + "dayOfWeek" INTEGER NOT NULL, + "period" INTEGER NOT NULL, + "subjectId" TEXT, + "teacherId" TEXT, + "roomId" TEXT, + "startTime" TEXT NOT NULL, + "endTime" TEXT NOT NULL, + "isBreak" BOOLEAN NOT NULL DEFAULT false, + "isDemo" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "deletedAt" DATETIME, + CONSTRAINT "timetable_slots_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "timetable_slots_classGroupId_fkey" FOREIGN KEY ("classGroupId") REFERENCES "ClassGroup" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "timetable_slots_subjectId_fkey" FOREIGN KEY ("subjectId") REFERENCES "Subject" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "timetable_slots_teacherId_fkey" FOREIGN KEY ("teacherId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "resources" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "authorId" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "resourceType" TEXT NOT NULL, + "url" TEXT, + "content" TEXT, + "subjectId" TEXT, + "classGroupId" TEXT, + "gradeLevel" INTEGER, + "tags" TEXT, + "isPublic" BOOLEAN NOT NULL DEFAULT true, + "downloadCount" INTEGER NOT NULL DEFAULT 0, + "isDemo" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "deletedAt" DATETIME, + CONSTRAINT "resources_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "resources_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "resources_subjectId_fkey" FOREIGN KEY ("subjectId") REFERENCES "Subject" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "resources_classGroupId_fkey" FOREIGN KEY ("classGroupId") REFERENCES "ClassGroup" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "email_templates" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "subject" TEXT NOT NULL, + "body" TEXT NOT NULL, + "isDefault" BOOLEAN NOT NULL DEFAULT false, + "isDemo" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "deletedAt" DATETIME, + CONSTRAINT "email_templates_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "email_logs" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "templateId" TEXT, + "recipientEmail" TEXT NOT NULL, + "recipientName" TEXT, + "subject" TEXT NOT NULL, + "body" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'pending', + "sentAt" DATETIME, + "error" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "email_logs_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "peer_assessment_sessions" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "teacherId" TEXT, + "classGroupId" TEXT, + "title" TEXT NOT NULL, + "description" TEXT, + "assessmentType" TEXT NOT NULL DEFAULT 'competency', + "criteria" TEXT, + "anonymityMode" TEXT NOT NULL DEFAULT 'anonymous', + "status" TEXT NOT NULL DEFAULT 'active', + "deadline" DATETIME, + "assignMode" TEXT NOT NULL DEFAULT 'manual', + "assignedPairs" TEXT, + "isDemo" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "deletedAt" DATETIME, + CONSTRAINT "peer_assessment_sessions_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "peer_assessment_sessions_teacherId_fkey" FOREIGN KEY ("teacherId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "peer_assessment_sessions_classGroupId_fkey" FOREIGN KEY ("classGroupId") REFERENCES "ClassGroup" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "peer_assessments" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "sessionId" TEXT, + "assessorId" TEXT NOT NULL, + "assessedId" TEXT NOT NULL, + "competencyId" TEXT, + "classGroupId" TEXT, + "assessmentType" TEXT NOT NULL, + "criteria" TEXT, + "level" INTEGER, + "comment" TEXT, + "rubricId" TEXT, + "isAnonymous" BOOLEAN NOT NULL DEFAULT false, + "status" TEXT NOT NULL DEFAULT 'pending', + "isDemo" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "deletedAt" DATETIME, + CONSTRAINT "peer_assessments_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "peer_assessments_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "peer_assessment_sessions" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "peer_assessments_assessorId_fkey" FOREIGN KEY ("assessorId") REFERENCES "Student" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "peer_assessments_assessedId_fkey" FOREIGN KEY ("assessedId") REFERENCES "Student" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "peer_assessments_competencyId_fkey" FOREIGN KEY ("competencyId") REFERENCES "Competency" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "peer_assessments_classGroupId_fkey" FOREIGN KEY ("classGroupId") REFERENCES "ClassGroup" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "peer_assessments_rubricId_fkey" FOREIGN KEY ("rubricId") REFERENCES "Rubric" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "emergency_contacts" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "studentId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "relationship" TEXT NOT NULL, + "phone" TEXT NOT NULL, + "phoneAlt" TEXT, + "email" TEXT, + "address" TEXT, + "isPrimary" BOOLEAN NOT NULL DEFAULT false, + "priority" INTEGER NOT NULL DEFAULT 1, + "notes" TEXT, + "isDemo" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "deletedAt" DATETIME, + CONSTRAINT "emergency_contacts_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "emergency_contacts_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "Student" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "school_events" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "eventType" TEXT NOT NULL, + "startDate" DATETIME NOT NULL, + "endDate" DATETIME, + "location" TEXT, + "organizerId" TEXT, + "classGroupId" TEXT, + "isAllSchool" BOOLEAN NOT NULL DEFAULT true, + "isPublic" BOOLEAN NOT NULL DEFAULT true, + "requiresRegistration" BOOLEAN NOT NULL DEFAULT false, + "maxParticipants" INTEGER, + "notes" TEXT, + "isDemo" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "deletedAt" DATETIME, + "budget" REAL, + "registrationDeadline" DATETIME, + "capacity" INTEGER, + "isRecurring" BOOLEAN NOT NULL DEFAULT false, + "recurrenceRule" TEXT, + "bannerImageUrl" TEXT, + "status" TEXT NOT NULL DEFAULT 'draft', + "feedbackForm" TEXT, + CONSTRAINT "school_events_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "school_events_organizerId_fkey" FOREIGN KEY ("organizerId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "school_events_classGroupId_fkey" FOREIGN KEY ("classGroupId") REFERENCES "ClassGroup" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "event_registrations" ( + "id" TEXT NOT NULL PRIMARY KEY, + "eventId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'registered', + "notes" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "event_registrations_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "school_events" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "event_registrations_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "event_feedbacks" ( + "id" TEXT NOT NULL PRIMARY KEY, + "eventId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "rating" INTEGER NOT NULL, + "comment" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "event_feedbacks_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "school_events" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "event_feedbacks_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "student_transport" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "studentId" TEXT NOT NULL, + "transportType" TEXT NOT NULL, + "routeNumber" TEXT, + "stopName" TEXT, + "pickupTime" TEXT, + "dropoffTime" TEXT, + "driverName" TEXT, + "driverPhone" TEXT, + "distanceKm" REAL, + "routeId" TEXT, + "notes" TEXT, + "isDemo" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "deletedAt" DATETIME, + CONSTRAINT "student_transport_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "student_transport_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "Student" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "student_transport_routeId_fkey" FOREIGN KEY ("routeId") REFERENCES "transport_route" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "transport_route" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "routeNumber" TEXT NOT NULL, + "routeName" TEXT NOT NULL, + "transportType" TEXT NOT NULL, + "driverName" TEXT, + "driverPhone" TEXT, + "capacity" INTEGER NOT NULL DEFAULT 40, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "notes" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "deletedAt" DATETIME, + CONSTRAINT "transport_route_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "transport_stop" ( + "id" TEXT NOT NULL PRIMARY KEY, + "routeId" TEXT NOT NULL, + "stopName" TEXT NOT NULL, + "stopOrder" INTEGER NOT NULL, + "pickupTime" TEXT, + "dropoffTime" TEXT, + "address" TEXT, + "latitude" REAL, + "longitude" REAL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "transport_stop_routeId_fkey" FOREIGN KEY ("routeId") REFERENCES "transport_route" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "health_records" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "studentId" TEXT NOT NULL, + "bloodType" TEXT, + "allergies" TEXT, + "medications" TEXT, + "conditions" TEXT, + "doctorName" TEXT, + "doctorPhone" TEXT, + "insuranceNumber" TEXT, + "insuranceProvider" TEXT, + "lastCheckup" DATETIME, + "notes" TEXT, + "isConfidential" BOOLEAN NOT NULL DEFAULT true, + "isDemo" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "deletedAt" DATETIME, + CONSTRAINT "health_records_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "health_records_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "Student" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "badges" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "icon" TEXT NOT NULL, + "color" TEXT NOT NULL DEFAULT '#10b981', + "category" TEXT NOT NULL, + "requirementType" TEXT NOT NULL, + "requirementValue" INTEGER, + "isAuto" BOOLEAN NOT NULL DEFAULT true, + "isDemo" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "deletedAt" DATETIME, + CONSTRAINT "badges_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "student_badges" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "studentId" TEXT NOT NULL, + "badgeId" TEXT NOT NULL, + "awardedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "awardedBy" TEXT, + "notes" TEXT, + "isDemo" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "student_badges_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "student_badges_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "Student" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "student_badges_badgeId_fkey" FOREIGN KEY ("badgeId") REFERENCES "badges" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "student_badges_awardedBy_fkey" FOREIGN KEY ("awardedBy") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "competitions" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "competitionType" TEXT NOT NULL, + "category" TEXT NOT NULL, + "subjectId" TEXT, + "status" TEXT NOT NULL DEFAULT 'draft', + "startDate" DATETIME NOT NULL, + "endDate" DATETIME NOT NULL, + "registrationDeadline" DATETIME, + "maxParticipants" INTEGER, + "scoringType" TEXT NOT NULL DEFAULT 'points', + "rules" TEXT, + "isPublic" BOOLEAN NOT NULL DEFAULT false, + "isDemo" BOOLEAN NOT NULL DEFAULT false, + "isFederation" BOOLEAN NOT NULL DEFAULT false, + "federationSchedule" TEXT, + "createdById" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "deletedAt" DATETIME, + CONSTRAINT "competitions_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "competitions_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "competitions_subjectId_fkey" FOREIGN KEY ("subjectId") REFERENCES "Subject" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "competition_participants" ( + "id" TEXT NOT NULL PRIMARY KEY, + "competitionId" TEXT NOT NULL, + "participantType" TEXT NOT NULL, + "participantId" TEXT NOT NULL, + "userId" TEXT, + "registeredAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "score" INTEGER NOT NULL DEFAULT 0, + "rank" INTEGER, + "isDisqualified" BOOLEAN NOT NULL DEFAULT false, + "notes" TEXT, + "isDemo" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "competition_participants_competitionId_fkey" FOREIGN KEY ("competitionId") REFERENCES "competitions" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "competition_participants_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "competition_rewards" ( + "id" TEXT NOT NULL PRIMARY KEY, + "competitionId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "rewardType" TEXT NOT NULL, + "rewardValue" TEXT, + "rewardProvider" TEXT, + "rankRequirement" INTEGER, + "pointsRequired" INTEGER, + "quantity" INTEGER NOT NULL DEFAULT 1, + "claimedCount" INTEGER NOT NULL DEFAULT 0, + "imageUrl" TEXT, + "isDemo" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "competition_rewards_competitionId_fkey" FOREIGN KEY ("competitionId") REFERENCES "competitions" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "competition_leaderboard" ( + "id" TEXT NOT NULL PRIMARY KEY, + "competitionId" TEXT NOT NULL, + "participantType" TEXT NOT NULL, + "participantId" TEXT NOT NULL, + "participantName" TEXT NOT NULL, + "score" INTEGER NOT NULL DEFAULT 0, + "rank" INTEGER, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "competition_leaderboard_competitionId_fkey" FOREIGN KEY ("competitionId") REFERENCES "competitions" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "reward_claims" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "competitionId" TEXT NOT NULL, + "rewardId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "claimedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "code" TEXT, + "status" TEXT NOT NULL DEFAULT 'pending', + "expiresAt" DATETIME, + "notes" TEXT, + "isDemo" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "reward_claims_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "reward_claims_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "reward_claims_competitionId_fkey" FOREIGN KEY ("competitionId") REFERENCES "competitions" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "reward_claims_rewardId_fkey" FOREIGN KEY ("rewardId") REFERENCES "competition_rewards" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "newsletters" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "authorId" TEXT NOT NULL, + "title" TEXT NOT NULL, + "subject" TEXT, + "content" TEXT NOT NULL, + "summary" TEXT, + "imageUrl" TEXT, + "bannerImageUrl" TEXT, + "category" TEXT NOT NULL DEFAULT 'general', + "templateType" TEXT NOT NULL DEFAULT 'monthly', + "targetAudience" TEXT, + "status" TEXT NOT NULL DEFAULT 'draft', + "isPublished" BOOLEAN NOT NULL DEFAULT false, + "publishedAt" DATETIME, + "scheduledAt" DATETIME, + "sentAt" DATETIME, + "tags" TEXT, + "openCount" INTEGER NOT NULL DEFAULT 0, + "clickCount" INTEGER NOT NULL DEFAULT 0, + "bounceCount" INTEGER NOT NULL DEFAULT 0, + "totalRecipients" INTEGER NOT NULL DEFAULT 0, + "isDemo" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "deletedAt" DATETIME, + CONSTRAINT "newsletters_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "newsletters_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "subject_topics" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "subjectId" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "gradeLevel" TEXT, + "curriculumCode" TEXT, + "sortOrder" INTEGER NOT NULL DEFAULT 0, + "icon" TEXT, + "color" TEXT NOT NULL DEFAULT '#10b981', + "isDemo" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "deletedAt" DATETIME, + CONSTRAINT "subject_topics_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "subject_topics_subjectId_fkey" FOREIGN KEY ("subjectId") REFERENCES "Subject" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "subject_lessons" ( + "id" TEXT NOT NULL PRIMARY KEY, + "topicId" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "lessonType" TEXT NOT NULL DEFAULT 'explanation', + "content" TEXT NOT NULL, + "difficulty" TEXT NOT NULL DEFAULT 'medium', + "sortOrder" INTEGER NOT NULL DEFAULT 0, + "estimatedMinutes" INTEGER, + "isDemo" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "deletedAt" DATETIME, + CONSTRAINT "subject_lessons_topicId_fkey" FOREIGN KEY ("topicId") REFERENCES "subject_topics" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "lesson_questions" ( + "id" TEXT NOT NULL PRIMARY KEY, + "lessonId" TEXT NOT NULL, + "questionType" TEXT NOT NULL, + "question" TEXT NOT NULL, + "options" TEXT, + "correctAnswer" TEXT NOT NULL, + "explanation" TEXT, + "points" INTEGER NOT NULL DEFAULT 1, + "sortOrder" INTEGER NOT NULL DEFAULT 0, + "isDemo" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "lesson_questions_lessonId_fkey" FOREIGN KEY ("lessonId") REFERENCES "subject_lessons" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "student_answers" ( + "id" TEXT NOT NULL PRIMARY KEY, + "questionId" TEXT NOT NULL, + "studentId" TEXT NOT NULL, + "answer" TEXT NOT NULL, + "isCorrect" BOOLEAN, + "timeTakenMs" INTEGER, + "attempts" INTEGER NOT NULL DEFAULT 1, + "isDemo" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "student_answers_questionId_fkey" FOREIGN KEY ("questionId") REFERENCES "lesson_questions" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "student_answers_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "rewards" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "category" TEXT NOT NULL, + "pointsCost" INTEGER NOT NULL, + "image" TEXT, + "stock" INTEGER, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "isDemo" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "deletedAt" DATETIME, + CONSTRAINT "rewards_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "reward_redemptions" ( + "id" TEXT NOT NULL PRIMARY KEY, + "rewardId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "pointsSpent" INTEGER NOT NULL, + "status" TEXT NOT NULL DEFAULT 'pending', + "note" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "reward_redemptions_rewardId_fkey" FOREIGN KEY ("rewardId") REFERENCES "rewards" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "reward_redemptions_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "reward_points" ( + "id" TEXT NOT NULL PRIMARY KEY, + "userId" TEXT NOT NULL, + "schoolId" TEXT NOT NULL, + "points" INTEGER NOT NULL DEFAULT 0, + "source" TEXT NOT NULL, + "sourceId" TEXT, + "description" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "reward_points_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "reward_points_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "StudentGoal" ( + "id" TEXT NOT NULL PRIMARY KEY, + "studentId" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "targetDate" DATETIME, + "progress" INTEGER NOT NULL DEFAULT 0, + "status" TEXT NOT NULL DEFAULT 'active', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "StudentGoal_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "Student" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "subject_categories" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "slug" TEXT NOT NULL, + "description" TEXT, + "icon" TEXT, + "sortOrder" INTEGER NOT NULL DEFAULT 0, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "subject_categories_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "subject_contents" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "categoryId" TEXT NOT NULL, + "subjectId" TEXT, + "parentId" TEXT, + "title" TEXT NOT NULL, + "slug" TEXT NOT NULL, + "description" TEXT, + "icon" TEXT, + "contentType" TEXT NOT NULL DEFAULT 'topic', + "content" TEXT, + "difficulty" TEXT NOT NULL DEFAULT 'medium', + "questionCount" INTEGER NOT NULL DEFAULT 0, + "sortOrder" INTEGER NOT NULL DEFAULT 0, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "isPublic" BOOLEAN NOT NULL DEFAULT true, + "sourceUrl" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "subject_contents_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "subject_contents_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "subject_categories" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "subject_contents_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "subject_contents" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "subject_contents_subjectId_fkey" FOREIGN KEY ("subjectId") REFERENCES "Subject" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "content_change_requests" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "contentId" TEXT NOT NULL, + "requestedBy" TEXT NOT NULL, + "requestType" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT NOT NULL, + "proposedChanges" TEXT, + "status" TEXT NOT NULL DEFAULT 'pending', + "reviewedBy" TEXT, + "reviewNote" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "content_change_requests_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "content_change_requests_contentId_fkey" FOREIGN KEY ("contentId") REFERENCES "subject_contents" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "content_change_requests_requestedBy_fkey" FOREIGN KEY ("requestedBy") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "content_change_requests_reviewedBy_fkey" FOREIGN KEY ("reviewedBy") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "ai_settings" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "pollinationEnabled" BOOLEAN NOT NULL DEFAULT true, + "pollinationApiKey" TEXT, + "pollinationModel" TEXT NOT NULL DEFAULT 'flux', + "openaiEnabled" BOOLEAN NOT NULL DEFAULT false, + "openaiApiKey" TEXT, + "openaiModel" TEXT NOT NULL DEFAULT 'gpt-4o', + "anthropicEnabled" BOOLEAN NOT NULL DEFAULT false, + "anthropicApiKey" TEXT, + "anthropicModel" TEXT NOT NULL DEFAULT 'claude-3-5-sonnet-20241022', + "aiChatEnabled" BOOLEAN NOT NULL DEFAULT true, + "aiImageGenEnabled" BOOLEAN NOT NULL DEFAULT true, + "aiVideoGenEnabled" BOOLEAN NOT NULL DEFAULT false, + "aiAutoTestEnabled" BOOLEAN NOT NULL DEFAULT true, + "aiGradingAuditEnabled" BOOLEAN NOT NULL DEFAULT false, + "aiTopicGenEnabled" BOOLEAN NOT NULL DEFAULT true, + "virtualCharacterEnabled" BOOLEAN NOT NULL DEFAULT true, + "aiMaxRequestsPerDay" INTEGER NOT NULL DEFAULT 50, + "aiHelperMode" TEXT NOT NULL DEFAULT 'guided', + "aiSystemPrompt" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "ai_settings_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "virtual_characters" ( + "id" TEXT NOT NULL PRIMARY KEY, + "userId" TEXT NOT NULL, + "schoolId" TEXT NOT NULL, + "characterId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "color" TEXT NOT NULL DEFAULT '#10b981', + "level" INTEGER NOT NULL DEFAULT 1, + "xp" INTEGER NOT NULL DEFAULT 0, + "mood" TEXT NOT NULL DEFAULT 'happy', + "accessories" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "virtual_characters_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "virtual_characters_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "chat_messages" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "roomId" TEXT, + "content" TEXT NOT NULL, + "senderType" TEXT NOT NULL DEFAULT 'user', + "metadata" TEXT, + "isRead" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "chat_messages_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "chat_messages_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "chat_messages_roomId_fkey" FOREIGN KEY ("roomId") REFERENCES "communication_rooms" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "illness_reports" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "studentId" TEXT NOT NULL, + "reportedBy" TEXT NOT NULL, + "reporterType" TEXT NOT NULL, + "reason" TEXT NOT NULL, + "description" TEXT, + "startDate" DATETIME NOT NULL, + "endDate" DATETIME, + "documentUrl" TEXT, + "parentApprovalStatus" TEXT NOT NULL DEFAULT 'pending', + "parentApprovedBy" TEXT, + "parentApprovedAt" DATETIME, + "isVisibleToTeacher" BOOLEAN NOT NULL DEFAULT false, + "isVisibleToAdmin" BOOLEAN NOT NULL DEFAULT false, + "status" TEXT NOT NULL DEFAULT 'pending', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "illness_reports_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "illness_reports_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "Student" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "illness_reports_reportedBy_fkey" FOREIGN KEY ("reportedBy") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "illness_reports_parentApprovedBy_fkey" FOREIGN KEY ("parentApprovedBy") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "communication_rooms" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "studentId" TEXT NOT NULL, + "teacherId" TEXT NOT NULL, + "roomType" TEXT NOT NULL DEFAULT 'chat', + "status" TEXT NOT NULL DEFAULT 'requested', + "requestedBy" TEXT NOT NULL, + "acceptedAt" DATETIME, + "closedAt" DATETIME, + "closeReason" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "communication_rooms_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "communication_rooms_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "communication_rooms_teacherId_fkey" FOREIGN KEY ("teacherId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "communication_room_members" ( + "id" TEXT NOT NULL PRIMARY KEY, + "roomId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "role" TEXT NOT NULL DEFAULT 'member', + "joinedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "communication_room_members_roomId_fkey" FOREIGN KEY ("roomId") REFERENCES "communication_rooms" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "communication_room_members_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "communication_messages" ( + "id" TEXT NOT NULL PRIMARY KEY, + "roomId" TEXT NOT NULL, + "senderId" TEXT NOT NULL, + "content" TEXT NOT NULL, + "messageType" TEXT NOT NULL DEFAULT 'text', + "fileUrl" TEXT, + "metadata" TEXT, + "isRead" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "communication_messages_roomId_fkey" FOREIGN KEY ("roomId") REFERENCES "communication_rooms" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "communication_messages_senderId_fkey" FOREIGN KEY ("senderId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "counseling_appointments" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "studentId" TEXT NOT NULL, + "counselorId" TEXT NOT NULL, + "requestType" TEXT NOT NULL DEFAULT 'guidance', + "description" TEXT, + "status" TEXT NOT NULL DEFAULT 'requested', + "scheduledAt" DATETIME, + "duration" INTEGER NOT NULL DEFAULT 30, + "notes" TEXT, + "isPrivate" BOOLEAN NOT NULL DEFAULT true, + "addToCalendar" BOOLEAN NOT NULL DEFAULT true, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "counseling_appointments_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "counseling_appointments_counselorId_fkey" FOREIGN KEY ("counselorId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "counseling_appointments_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "disciplinary_committees" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "disciplinary_committees_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "disciplinary_committee_members" ( + "id" TEXT NOT NULL PRIMARY KEY, + "committeeId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "role" TEXT NOT NULL DEFAULT 'member', + "isLead" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "disciplinary_committee_members_committeeId_fkey" FOREIGN KEY ("committeeId") REFERENCES "disciplinary_committees" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "disciplinary_committee_members_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "disciplinary_cases" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "committeeId" TEXT NOT NULL, + "studentId" TEXT NOT NULL, + "reportedBy" TEXT NOT NULL, + "reviewedBy" TEXT, + "caseType" TEXT NOT NULL, + "description" TEXT NOT NULL, + "evidence" TEXT, + "status" TEXT NOT NULL DEFAULT 'open', + "resolution" TEXT, + "resolutionDate" DATETIME, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "disciplinary_cases_committeeId_fkey" FOREIGN KEY ("committeeId") REFERENCES "disciplinary_committees" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "disciplinary_cases_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "Student" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "disciplinary_cases_reportedBy_fkey" FOREIGN KEY ("reportedBy") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "disciplinary_cases_reviewedBy_fkey" FOREIGN KEY ("reviewedBy") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "ai_test_generations" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "assessmentId" TEXT, + "studentId" TEXT, + "classGroupId" TEXT, + "subjectId" TEXT, + "topicId" TEXT, + "testType" TEXT NOT NULL DEFAULT 'practice', + "difficulty" TEXT NOT NULL DEFAULT 'medium', + "questionCount" INTEGER NOT NULL DEFAULT 10, + "questions" TEXT, + "aiProvider" TEXT NOT NULL DEFAULT 'pollination', + "generatedAt" DATETIME, + "isCompleted" BOOLEAN NOT NULL DEFAULT false, + "completedAt" DATETIME, + "score" REAL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "ai_test_generations_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "ai_test_generations_assessmentId_fkey" FOREIGN KEY ("assessmentId") REFERENCES "Assessment" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "ai_test_generations_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "ai_test_generations_classGroupId_fkey" FOREIGN KEY ("classGroupId") REFERENCES "ClassGroup" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "ai_test_generations_subjectId_fkey" FOREIGN KEY ("subjectId") REFERENCES "Subject" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "teacher_grading_reviews" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "assessmentId" TEXT NOT NULL, + "teacherId" TEXT NOT NULL, + "aiProvider" TEXT NOT NULL DEFAULT 'pollination', + "status" TEXT NOT NULL DEFAULT 'pending', + "reviewResult" TEXT, + "discrepanciesFound" INTEGER NOT NULL DEFAULT 0, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "teacher_grading_reviews_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "teacher_grading_reviews_assessmentId_fkey" FOREIGN KEY ("assessmentId") REFERENCES "Assessment" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "teacher_grading_reviews_teacherId_fkey" FOREIGN KEY ("teacherId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "grading_review_comments" ( + "id" TEXT NOT NULL PRIMARY KEY, + "reviewId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "resultId" TEXT, + "comment" TEXT NOT NULL, + "suggestedGrade" TEXT, + "reason" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "grading_review_comments_reviewId_fkey" FOREIGN KEY ("reviewId") REFERENCES "teacher_grading_reviews" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "grading_review_comments_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "grading_annotations" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "assessmentId" TEXT NOT NULL, + "studentId" TEXT NOT NULL, + "resultId" TEXT, + "teacherId" TEXT NOT NULL, + "type" TEXT NOT NULL DEFAULT 'drawing', + "content" TEXT, + "positionX" REAL NOT NULL DEFAULT 0, + "positionY" REAL NOT NULL DEFAULT 0, + "width" REAL, + "height" REAL, + "color" TEXT NOT NULL DEFAULT '#ef4444', + "strokeWidth" REAL NOT NULL DEFAULT 2, + "page" INTEGER NOT NULL DEFAULT 1, + "pathData" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "grading_annotations_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "grading_annotations_assessmentId_fkey" FOREIGN KEY ("assessmentId") REFERENCES "Assessment" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "grading_annotations_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "Student" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "grading_annotations_resultId_fkey" FOREIGN KEY ("resultId") REFERENCES "AssessmentResult" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "grading_annotations_teacherId_fkey" FOREIGN KEY ("teacherId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "exam_plans" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "teacherId" TEXT NOT NULL, + "subjectId" TEXT NOT NULL, + "classGroupId" TEXT NOT NULL, + "title" TEXT NOT NULL, + "date" DATETIME NOT NULL, + "startTime" TEXT, + "endTime" TEXT, + "room" TEXT, + "topics" TEXT, + "weight" REAL NOT NULL DEFAULT 1.0, + "status" TEXT NOT NULL DEFAULT 'planned', + "notes" TEXT, + "calendarEventId" TEXT, + "assessmentId" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "exam_plans_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "exam_plans_teacherId_fkey" FOREIGN KEY ("teacherId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "exam_plans_subjectId_fkey" FOREIGN KEY ("subjectId") REFERENCES "Subject" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "exam_plans_classGroupId_fkey" FOREIGN KEY ("classGroupId") REFERENCES "ClassGroup" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "exam_plans_calendarEventId_fkey" FOREIGN KEY ("calendarEventId") REFERENCES "CalendarEvent" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "exam_plans_assessmentId_fkey" FOREIGN KEY ("assessmentId") REFERENCES "Assessment" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "study_plans" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "studentId" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "subjectId" TEXT, + "subjectName" TEXT, + "dayOfWeek" INTEGER NOT NULL, + "startTime" TEXT NOT NULL, + "duration" INTEGER NOT NULL, + "isRecurring" BOOLEAN NOT NULL DEFAULT true, + "specificDate" DATETIME, + "priority" TEXT NOT NULL DEFAULT 'medium', + "status" TEXT NOT NULL DEFAULT 'active', + "color" TEXT, + "notes" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "study_plans_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "study_plans_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "Student" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "study_plans_subjectId_fkey" FOREIGN KEY ("subjectId") REFERENCES "Subject" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "study_sessions" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "studentId" TEXT NOT NULL, + "studyPlanId" TEXT, + "subjectId" TEXT, + "subjectName" TEXT, + "startTime" DATETIME NOT NULL, + "endTime" DATETIME, + "duration" INTEGER NOT NULL DEFAULT 0, + "plannedDuration" INTEGER, + "type" TEXT NOT NULL DEFAULT 'pomodoro', + "pomodorosCompleted" INTEGER NOT NULL DEFAULT 0, + "pomodoroLength" INTEGER NOT NULL DEFAULT 25, + "breakLength" INTEGER NOT NULL DEFAULT 5, + "status" TEXT NOT NULL DEFAULT 'in_progress', + "focusScore" INTEGER, + "notes" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "study_sessions_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "study_sessions_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "Student" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "study_sessions_studyPlanId_fkey" FOREIGN KEY ("studyPlanId") REFERENCES "study_plans" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "study_sessions_subjectId_fkey" FOREIGN KEY ("subjectId") REFERENCES "Subject" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "library_books" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "title" TEXT NOT NULL, + "author" TEXT NOT NULL, + "isbn" TEXT, + "publisher" TEXT, + "publishYear" INTEGER, + "category" TEXT NOT NULL, + "readingLevel" TEXT, + "language" TEXT NOT NULL DEFAULT 'de', + "description" TEXT, + "coverGradient" TEXT, + "totalCopies" INTEGER NOT NULL DEFAULT 1, + "availableCopies" INTEGER NOT NULL DEFAULT 1, + "location" TEXT, + "tags" TEXT, + "isDemo" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "library_books_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "book_checkouts" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "bookId" TEXT NOT NULL, + "studentId" TEXT NOT NULL, + "checkedOutBy" TEXT NOT NULL, + "checkoutDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "dueDate" DATETIME NOT NULL, + "returnDate" DATETIME, + "condition" TEXT NOT NULL DEFAULT 'good', + "returnCondition" TEXT, + "renewalCount" INTEGER NOT NULL DEFAULT 0, + "status" TEXT NOT NULL DEFAULT 'active', + "fineAmount" REAL NOT NULL DEFAULT 0, + "finePaid" BOOLEAN NOT NULL DEFAULT false, + "notes" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "book_checkouts_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "book_checkouts_bookId_fkey" FOREIGN KEY ("bookId") REFERENCES "library_books" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "book_checkouts_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "Student" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "book_checkouts_checkedOutBy_fkey" FOREIGN KEY ("checkedOutBy") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "book_reservations" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "bookId" TEXT NOT NULL, + "studentId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "queuePosition" INTEGER NOT NULL DEFAULT 1, + "status" TEXT NOT NULL DEFAULT 'waiting', + "notifiedAt" DATETIME, + "fulfilledAt" DATETIME, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "book_reservations_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "book_reservations_bookId_fkey" FOREIGN KEY ("bookId") REFERENCES "library_books" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "book_reservations_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "Student" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "book_reservations_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "seating_charts" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "classGroupId" TEXT NOT NULL, + "teacherId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "layoutType" TEXT NOT NULL DEFAULT 'rows', + "rows" INTEGER NOT NULL DEFAULT 5, + "columns" INTEGER NOT NULL DEFAULT 5, + "gap" INTEGER NOT NULL DEFAULT 2, + "arrangement" TEXT, + "showDoor" BOOLEAN NOT NULL DEFAULT true, + "showWindows" BOOLEAN NOT NULL DEFAULT true, + "doorPosition" TEXT NOT NULL DEFAULT 'left', + "windowPosition" TEXT NOT NULL DEFAULT 'right', + "isTemplate" BOOLEAN NOT NULL DEFAULT false, + "isDefault" BOOLEAN NOT NULL DEFAULT false, + "notes" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "seating_charts_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "seating_charts_classGroupId_fkey" FOREIGN KEY ("classGroupId") REFERENCES "ClassGroup" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "seating_charts_teacherId_fkey" FOREIGN KEY ("teacherId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "data_import_jobs" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "type" TEXT NOT NULL, + "fileName" TEXT NOT NULL, + "fileSize" INTEGER NOT NULL, + "status" TEXT NOT NULL DEFAULT 'pending', + "totalRows" INTEGER NOT NULL DEFAULT 0, + "successRows" INTEGER NOT NULL DEFAULT 0, + "errorRows" INTEGER NOT NULL DEFAULT 0, + "errors" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "data_import_jobs_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "data_import_jobs_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "data_export_jobs" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "type" TEXT NOT NULL, + "format" TEXT NOT NULL, + "filters" TEXT, + "fileName" TEXT, + "fileSize" INTEGER NOT NULL DEFAULT 0, + "status" TEXT NOT NULL DEFAULT 'pending', + "fileData" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "data_export_jobs_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "data_export_jobs_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "wellness_checkins" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "studentId" TEXT NOT NULL, + "date" DATETIME NOT NULL, + "mood" INTEGER NOT NULL, + "sleepHours" REAL, + "sleepQuality" INTEGER, + "stressLevel" INTEGER, + "activityType" TEXT, + "activityMinutes" INTEGER, + "mealsCount" INTEGER, + "waterGlasses" INTEGER, + "gratitudeEntry" TEXT, + "notes" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "wellness_checkins_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "wellness_checkins_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "Student" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "wellness_scores" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "studentId" TEXT NOT NULL, + "date" DATETIME NOT NULL, + "overallScore" REAL NOT NULL, + "physicalScore" REAL, + "mentalScore" REAL, + "socialScore" REAL, + "academicScore" REAL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "wellness_scores_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "wellness_scores_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "Student" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "career_profiles" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "studentId" TEXT NOT NULL, + "interests" TEXT, + "strengths" TEXT, + "careerCluster" TEXT, + "desiredCareer" TEXT, + "educationPath" TEXT, + "workExperiences" TEXT, + "volunteerExps" TEXT, + "certifications" TEXT, + "documents" TEXT, + "notes" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "career_profiles_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "career_profiles_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "Student" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "career_goals" ( + "id" TEXT NOT NULL PRIMARY KEY, + "profileId" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "category" TEXT NOT NULL, + "targetDate" DATETIME, + "progress" INTEGER NOT NULL DEFAULT 0, + "status" TEXT NOT NULL DEFAULT 'active', + "milestones" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "career_goals_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "career_profiles" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "career_appointments" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "profileId" TEXT NOT NULL, + "counselorId" TEXT NOT NULL, + "date" DATETIME NOT NULL, + "duration" INTEGER NOT NULL DEFAULT 30, + "type" TEXT NOT NULL DEFAULT 'guidance', + "status" TEXT NOT NULL DEFAULT 'scheduled', + "notes" TEXT, + "actionItems" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "career_appointments_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "career_appointments_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "career_profiles" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "career_appointments_counselorId_fkey" FOREIGN KEY ("counselorId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "substitute_teachers" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "userId" TEXT, + "firstName" TEXT NOT NULL, + "lastName" TEXT NOT NULL, + "email" TEXT, + "phone" TEXT, + "qualifications" TEXT, + "subjects" TEXT, + "gradeLevels" TEXT, + "availability" TEXT, + "maxDaysPerWeek" INTEGER NOT NULL DEFAULT 5, + "rating" REAL NOT NULL DEFAULT 0, + "totalAssignments" INTEGER NOT NULL DEFAULT 0, + "notes" TEXT, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "deletedAt" DATETIME, + CONSTRAINT "substitute_teachers_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "substitute_teachers_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "teacher_absences" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "teacherId" TEXT NOT NULL, + "type" TEXT NOT NULL, + "startDate" DATETIME NOT NULL, + "endDate" DATETIME NOT NULL, + "reason" TEXT, + "status" TEXT NOT NULL DEFAULT 'reported', + "notes" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "teacher_absences_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "teacher_absences_teacherId_fkey" FOREIGN KEY ("teacherId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "substitution_assignments" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "absenceId" TEXT NOT NULL, + "substituteId" TEXT NOT NULL, + "classGroupId" TEXT, + "subjectId" TEXT, + "date" DATETIME NOT NULL, + "period" INTEGER, + "room" TEXT, + "status" TEXT NOT NULL DEFAULT 'pending', + "notes" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "substitution_assignments_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "substitution_assignments_absenceId_fkey" FOREIGN KEY ("absenceId") REFERENCES "teacher_absences" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "substitution_assignments_substituteId_fkey" FOREIGN KEY ("substituteId") REFERENCES "substitute_teachers" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "substitution_assignments_classGroupId_fkey" FOREIGN KEY ("classGroupId") REFERENCES "ClassGroup" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "substitution_assignments_subjectId_fkey" FOREIGN KEY ("subjectId") REFERENCES "Subject" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "GradeReport" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "generatedBy" TEXT NOT NULL, + "title" TEXT NOT NULL, + "type" TEXT NOT NULL DEFAULT 'custom', + "dateRange" TEXT NOT NULL, + "classIds" TEXT, + "subjectIds" TEXT, + "metrics" TEXT, + "status" TEXT NOT NULL DEFAULT 'pending', + "fileData" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "GradeReport_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "GradeReport_generatedBy_fkey" FOREIGN KEY ("generatedBy") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "DataRetentionPolicy" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "dataCategory" TEXT NOT NULL, + "retentionPeriodMonths" INTEGER, + "legalBasisNote" TEXT, + "isDefault" BOOLEAN NOT NULL DEFAULT false, + "reviewedByUserId" TEXT, + "reviewedAt" DATETIME, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "DataRetentionPolicy_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "DataRetentionPolicy_reviewedByUserId_fkey" FOREIGN KEY ("reviewedByUserId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "DpiaRecord" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "moduleScope" TEXT NOT NULL, + "dataProcessingSummary" TEXT NOT NULL, + "completedAt" DATETIME NOT NULL, + "approvedByUserId" TEXT, + "documentUrl" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "DpiaRecord_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "DpiaRecord_approvedByUserId_fkey" FOREIGN KEY ("approvedByUserId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "ComplianceConsultation" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "consultationType" TEXT NOT NULL, + "moduleScope" TEXT, + "acknowledgedByUserId" TEXT NOT NULL, + "acknowledgedAt" DATETIME NOT NULL, + "notes" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "ComplianceConsultation_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "ComplianceConsultation_acknowledgedByUserId_fkey" FOREIGN KEY ("acknowledgedByUserId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "SchoolComplianceStatus" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "bundesland" TEXT, + "onboardingCompleted" BOOLEAN NOT NULL DEFAULT false, + "dpoDpoContactEmail" TEXT, + "schultragerContactEmail" TEXT, + "moduleGates" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "SchoolComplianceStatus_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "DeletionFlag" ( + "id" TEXT NOT NULL PRIMARY KEY, + "schoolId" TEXT NOT NULL, + "entityType" TEXT NOT NULL, + "entityId" TEXT NOT NULL, + "dataCategory" TEXT NOT NULL, + "flaggedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "approvedByUserId" TEXT, + "approvedAt" DATETIME, + "deletedAt" DATETIME, + "reason" TEXT NOT NULL, + CONSTRAINT "DeletionFlag_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "DeletionFlag_approvedByUserId_fkey" FOREIGN KEY ("approvedByUserId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "ClassGroupTeacher_classGroupId_userId_key" ON "ClassGroupTeacher"("classGroupId", "userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Student_userId_key" ON "Student"("userId"); + +-- CreateIndex +CREATE INDEX "Student_userId_idx" ON "Student"("userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "ClassCompetencyAssignment_classGroupId_subjectId_schoolYearId_key" ON "ClassCompetencyAssignment"("classGroupId", "subjectId", "schoolYearId"); + +-- CreateIndex +CREATE UNIQUE INDEX "AssessmentCompetencyLink_assessmentId_competencyId_key" ON "AssessmentCompetencyLink"("assessmentId", "competencyId"); + +-- CreateIndex +CREATE UNIQUE INDEX "AssessmentResult_assessmentId_studentId_key" ON "AssessmentResult"("assessmentId", "studentId"); + +-- CreateIndex +CREATE INDEX "AuditLog_dataSubject_idx" ON "AuditLog"("dataSubject"); + +-- CreateIndex +CREATE INDEX "AuditLog_timestamp_idx" ON "AuditLog"("timestamp"); + +-- CreateIndex +CREATE INDEX "AttendanceSession_classGroupId_date_idx" ON "AttendanceSession"("classGroupId", "date"); + +-- CreateIndex +CREATE INDEX "AttendanceRecord_studentId_idx" ON "AttendanceRecord"("studentId"); + +-- CreateIndex +CREATE UNIQUE INDEX "AttendanceRecord_sessionId_studentId_key" ON "AttendanceRecord"("sessionId", "studentId"); + +-- CreateIndex +CREATE INDEX "TeacherNote_studentId_idx" ON "TeacherNote"("studentId"); + +-- CreateIndex +CREATE INDEX "TeacherNote_teacherId_idx" ON "TeacherNote"("teacherId"); + +-- CreateIndex +CREATE INDEX "LessonPlan_teacherId_idx" ON "LessonPlan"("teacherId"); + +-- CreateIndex +CREATE INDEX "LessonPlan_classGroupId_idx" ON "LessonPlan"("classGroupId"); + +-- CreateIndex +CREATE INDEX "LessonPlan_date_idx" ON "LessonPlan"("date"); + +-- CreateIndex +CREATE INDEX "ParentContact_studentId_idx" ON "ParentContact"("studentId"); + +-- CreateIndex +CREATE UNIQUE INDEX "ParentContact_studentId_email_key" ON "ParentContact"("studentId", "email"); + +-- CreateIndex +CREATE INDEX "ParentMessage_teacherId_idx" ON "ParentMessage"("teacherId"); + +-- CreateIndex +CREATE INDEX "ParentMessage_parentId_idx" ON "ParentMessage"("parentId"); + +-- CreateIndex +CREATE INDEX "ParentMessage_studentId_idx" ON "ParentMessage"("studentId"); + +-- CreateIndex +CREATE INDEX "ParentMessage_createdAt_idx" ON "ParentMessage"("createdAt"); + +-- CreateIndex +CREATE INDEX "BehaviorCategory_schoolId_idx" ON "BehaviorCategory"("schoolId"); + +-- CreateIndex +CREATE UNIQUE INDEX "BehaviorCategory_schoolId_name_key" ON "BehaviorCategory"("schoolId", "name"); + +-- CreateIndex +CREATE INDEX "BehaviorIncident_studentId_date_idx" ON "BehaviorIncident"("studentId", "date"); + +-- CreateIndex +CREATE INDEX "BehaviorIncident_classGroupId_date_idx" ON "BehaviorIncident"("classGroupId", "date"); + +-- CreateIndex +CREATE INDEX "BehaviorIncident_schoolId_date_idx" ON "BehaviorIncident"("schoolId", "date"); + +-- CreateIndex +CREATE INDEX "BehaviorIncident_teacherId_idx" ON "BehaviorIncident"("teacherId"); + +-- CreateIndex +CREATE INDEX "Rubric_schoolId_idx" ON "Rubric"("schoolId"); + +-- CreateIndex +CREATE INDEX "Rubric_teacherId_idx" ON "Rubric"("teacherId"); + +-- CreateIndex +CREATE INDEX "Rubric_subjectId_idx" ON "Rubric"("subjectId"); + +-- CreateIndex +CREATE INDEX "RubricCriterion_rubricId_idx" ON "RubricCriterion"("rubricId"); + +-- CreateIndex +CREATE INDEX "RubricLevel_criterionId_idx" ON "RubricLevel"("criterionId"); + +-- CreateIndex +CREATE INDEX "CommentCategory_schoolId_idx" ON "CommentCategory"("schoolId"); + +-- CreateIndex +CREATE UNIQUE INDEX "CommentCategory_schoolId_name_key" ON "CommentCategory"("schoolId", "name"); + +-- CreateIndex +CREATE INDEX "CommentBankEntry_schoolId_idx" ON "CommentBankEntry"("schoolId"); + +-- CreateIndex +CREATE INDEX "CommentBankEntry_teacherId_idx" ON "CommentBankEntry"("teacherId"); + +-- CreateIndex +CREATE INDEX "CommentBankEntry_categoryId_idx" ON "CommentBankEntry"("categoryId"); + +-- CreateIndex +CREATE INDEX "CommentBankEntry_subjectId_idx" ON "CommentBankEntry"("subjectId"); + +-- CreateIndex +CREATE INDEX "Notebook_schoolId_idx" ON "Notebook"("schoolId"); + +-- CreateIndex +CREATE INDEX "Notebook_ownerId_idx" ON "Notebook"("ownerId"); + +-- CreateIndex +CREATE INDEX "Notebook_subjectId_idx" ON "Notebook"("subjectId"); + +-- CreateIndex +CREATE INDEX "Notebook_classGroupId_idx" ON "Notebook"("classGroupId"); + +-- CreateIndex +CREATE INDEX "Notebook_isArchived_idx" ON "Notebook"("isArchived"); + +-- CreateIndex +CREATE INDEX "NotebookPage_notebookId_idx" ON "NotebookPage"("notebookId"); + +-- CreateIndex +CREATE INDEX "NotebookPage_pageNumber_idx" ON "NotebookPage"("pageNumber"); + +-- CreateIndex +CREATE INDEX "notebook_page_versions_pageId_version_idx" ON "notebook_page_versions"("pageId", "version"); + +-- CreateIndex +CREATE INDEX "Drawing_schoolId_idx" ON "Drawing"("schoolId"); + +-- CreateIndex +CREATE INDEX "Drawing_ownerId_idx" ON "Drawing"("ownerId"); + +-- CreateIndex +CREATE INDEX "Drawing_subjectId_idx" ON "Drawing"("subjectId"); + +-- CreateIndex +CREATE INDEX "notifications_userId_isRead_idx" ON "notifications"("userId", "isRead"); + +-- CreateIndex +CREATE INDEX "notifications_schoolId_idx" ON "notifications"("schoolId"); + +-- CreateIndex +CREATE INDEX "notifications_category_idx" ON "notifications"("category"); + +-- CreateIndex +CREATE INDEX "notifications_priority_idx" ON "notifications"("priority"); + +-- CreateIndex +CREATE INDEX "CalendarEvent_schoolId_idx" ON "CalendarEvent"("schoolId"); + +-- CreateIndex +CREATE INDEX "CalendarEvent_teacherId_idx" ON "CalendarEvent"("teacherId"); + +-- CreateIndex +CREATE INDEX "CalendarEvent_date_idx" ON "CalendarEvent"("date"); + +-- CreateIndex +CREATE INDEX "CalendarEvent_subjectId_idx" ON "CalendarEvent"("subjectId"); + +-- CreateIndex +CREATE INDEX "CalendarEvent_classGroupId_idx" ON "CalendarEvent"("classGroupId"); + +-- CreateIndex +CREATE INDEX "CalendarEvent_parentEventId_idx" ON "CalendarEvent"("parentEventId"); + +-- CreateIndex +CREATE INDEX "CalendarEvent_eventType_idx" ON "CalendarEvent"("eventType"); + +-- CreateIndex +CREATE UNIQUE INDEX "parent_student_links_parentId_studentId_key" ON "parent_student_links"("parentId", "studentId"); + +-- CreateIndex +CREATE INDEX "curriculum_standards_schoolId_subjectId_idx" ON "curriculum_standards"("schoolId", "subjectId"); + +-- CreateIndex +CREATE UNIQUE INDEX "curriculum_standard_links_standardId_competencyId_key" ON "curriculum_standard_links"("standardId", "competencyId"); + +-- CreateIndex +CREATE INDEX "behavior_interventions_schoolId_studentId_idx" ON "behavior_interventions"("schoolId", "studentId"); + +-- CreateIndex +CREATE INDEX "report_schedules_schoolId_isActive_idx" ON "report_schedules"("schoolId", "isActive"); + +-- CreateIndex +CREATE INDEX "homework_schoolId_classGroupId_dueDate_idx" ON "homework"("schoolId", "classGroupId", "dueDate"); + +-- CreateIndex +CREATE INDEX "homework_submissions_homeworkId_status_idx" ON "homework_submissions"("homeworkId", "status"); + +-- CreateIndex +CREATE UNIQUE INDEX "homework_submissions_homeworkId_studentId_key" ON "homework_submissions"("homeworkId", "studentId"); + +-- CreateIndex +CREATE INDEX "announcements_schoolId_createdAt_idx" ON "announcements"("schoolId", "createdAt"); + +-- CreateIndex +CREATE INDEX "announcement_reads_announcementId_idx" ON "announcement_reads"("announcementId"); + +-- CreateIndex +CREATE INDEX "announcement_reads_userId_idx" ON "announcement_reads"("userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "announcement_reads_announcementId_userId_key" ON "announcement_reads"("announcementId", "userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "notification_preferences_userId_key" ON "notification_preferences"("userId"); + +-- CreateIndex +CREATE INDEX "notification_preferences_userId_idx" ON "notification_preferences"("userId"); + +-- CreateIndex +CREATE INDEX "self_assessments_schoolId_studentId_idx" ON "self_assessments"("schoolId", "studentId"); + +-- CreateIndex +CREATE INDEX "learning_goals_schoolId_studentId_status_idx" ON "learning_goals"("schoolId", "studentId", "status"); + +-- CreateIndex +CREATE INDEX "portfolio_entries_schoolId_studentId_idx" ON "portfolio_entries"("schoolId", "studentId"); + +-- CreateIndex +CREATE INDEX "timetable_slots_schoolId_classGroupId_idx" ON "timetable_slots"("schoolId", "classGroupId"); + +-- CreateIndex +CREATE UNIQUE INDEX "timetable_slots_classGroupId_dayOfWeek_period_key" ON "timetable_slots"("classGroupId", "dayOfWeek", "period"); + +-- CreateIndex +CREATE INDEX "resources_schoolId_resourceType_idx" ON "resources"("schoolId", "resourceType"); + +-- CreateIndex +CREATE UNIQUE INDEX "email_templates_schoolId_name_key" ON "email_templates"("schoolId", "name"); + +-- CreateIndex +CREATE INDEX "email_logs_schoolId_status_idx" ON "email_logs"("schoolId", "status"); + +-- CreateIndex +CREATE INDEX "peer_assessment_sessions_schoolId_status_idx" ON "peer_assessment_sessions"("schoolId", "status"); + +-- CreateIndex +CREATE INDEX "peer_assessment_sessions_schoolId_teacherId_idx" ON "peer_assessment_sessions"("schoolId", "teacherId"); + +-- CreateIndex +CREATE INDEX "peer_assessments_schoolId_assessedId_idx" ON "peer_assessments"("schoolId", "assessedId"); + +-- CreateIndex +CREATE INDEX "peer_assessments_schoolId_assessorId_idx" ON "peer_assessments"("schoolId", "assessorId"); + +-- CreateIndex +CREATE INDEX "peer_assessments_sessionId_idx" ON "peer_assessments"("sessionId"); + +-- CreateIndex +CREATE INDEX "emergency_contacts_schoolId_studentId_idx" ON "emergency_contacts"("schoolId", "studentId"); + +-- CreateIndex +CREATE INDEX "school_events_schoolId_startDate_idx" ON "school_events"("schoolId", "startDate"); + +-- CreateIndex +CREATE UNIQUE INDEX "event_registrations_eventId_userId_key" ON "event_registrations"("eventId", "userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "event_feedbacks_eventId_userId_key" ON "event_feedbacks"("eventId", "userId"); + +-- CreateIndex +CREATE INDEX "student_transport_schoolId_studentId_idx" ON "student_transport"("schoolId", "studentId"); + +-- CreateIndex +CREATE INDEX "transport_route_schoolId_idx" ON "transport_route"("schoolId"); + +-- CreateIndex +CREATE INDEX "transport_stop_routeId_idx" ON "transport_stop"("routeId"); + +-- CreateIndex +CREATE UNIQUE INDEX "health_records_schoolId_studentId_key" ON "health_records"("schoolId", "studentId"); + +-- CreateIndex +CREATE INDEX "badges_schoolId_category_idx" ON "badges"("schoolId", "category"); + +-- CreateIndex +CREATE INDEX "student_badges_schoolId_studentId_idx" ON "student_badges"("schoolId", "studentId"); + +-- CreateIndex +CREATE UNIQUE INDEX "student_badges_studentId_badgeId_key" ON "student_badges"("studentId", "badgeId"); + +-- CreateIndex +CREATE INDEX "competitions_schoolId_idx" ON "competitions"("schoolId"); + +-- CreateIndex +CREATE INDEX "competitions_status_idx" ON "competitions"("status"); + +-- CreateIndex +CREATE INDEX "competitions_competitionType_idx" ON "competitions"("competitionType"); + +-- CreateIndex +CREATE INDEX "competitions_startDate_idx" ON "competitions"("startDate"); + +-- CreateIndex +CREATE INDEX "competition_participants_competitionId_idx" ON "competition_participants"("competitionId"); + +-- CreateIndex +CREATE INDEX "competition_participants_userId_idx" ON "competition_participants"("userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "competition_participants_competitionId_participantType_participantId_key" ON "competition_participants"("competitionId", "participantType", "participantId"); + +-- CreateIndex +CREATE INDEX "competition_rewards_competitionId_idx" ON "competition_rewards"("competitionId"); + +-- CreateIndex +CREATE INDEX "competition_leaderboard_competitionId_rank_idx" ON "competition_leaderboard"("competitionId", "rank"); + +-- CreateIndex +CREATE UNIQUE INDEX "competition_leaderboard_competitionId_participantType_participantId_key" ON "competition_leaderboard"("competitionId", "participantType", "participantId"); + +-- CreateIndex +CREATE INDEX "reward_claims_schoolId_userId_idx" ON "reward_claims"("schoolId", "userId"); + +-- CreateIndex +CREATE INDEX "reward_claims_competitionId_idx" ON "reward_claims"("competitionId"); + +-- CreateIndex +CREATE INDEX "newsletters_schoolId_isPublished_publishedAt_idx" ON "newsletters"("schoolId", "isPublished", "publishedAt"); + +-- CreateIndex +CREATE INDEX "newsletters_schoolId_status_idx" ON "newsletters"("schoolId", "status"); + +-- CreateIndex +CREATE INDEX "subject_topics_schoolId_subjectId_idx" ON "subject_topics"("schoolId", "subjectId"); + +-- CreateIndex +CREATE INDEX "subject_topics_gradeLevel_idx" ON "subject_topics"("gradeLevel"); + +-- CreateIndex +CREATE INDEX "subject_lessons_topicId_idx" ON "subject_lessons"("topicId"); + +-- CreateIndex +CREATE INDEX "lesson_questions_lessonId_idx" ON "lesson_questions"("lessonId"); + +-- CreateIndex +CREATE INDEX "student_answers_studentId_idx" ON "student_answers"("studentId"); + +-- CreateIndex +CREATE UNIQUE INDEX "student_answers_questionId_studentId_key" ON "student_answers"("questionId", "studentId"); + +-- CreateIndex +CREATE INDEX "rewards_schoolId_idx" ON "rewards"("schoolId"); + +-- CreateIndex +CREATE INDEX "rewards_category_idx" ON "rewards"("category"); + +-- CreateIndex +CREATE INDEX "reward_redemptions_rewardId_idx" ON "reward_redemptions"("rewardId"); + +-- CreateIndex +CREATE INDEX "reward_redemptions_userId_idx" ON "reward_redemptions"("userId"); + +-- CreateIndex +CREATE INDEX "reward_redemptions_status_idx" ON "reward_redemptions"("status"); + +-- CreateIndex +CREATE INDEX "reward_points_userId_idx" ON "reward_points"("userId"); + +-- CreateIndex +CREATE INDEX "reward_points_schoolId_idx" ON "reward_points"("schoolId"); + +-- CreateIndex +CREATE INDEX "reward_points_source_idx" ON "reward_points"("source"); + +-- CreateIndex +CREATE INDEX "StudentGoal_studentId_idx" ON "StudentGoal"("studentId"); + +-- CreateIndex +CREATE INDEX "StudentGoal_status_idx" ON "StudentGoal"("status"); + +-- CreateIndex +CREATE INDEX "subject_categories_schoolId_idx" ON "subject_categories"("schoolId"); + +-- CreateIndex +CREATE INDEX "subject_categories_slug_idx" ON "subject_categories"("slug"); + +-- CreateIndex +CREATE INDEX "subject_contents_schoolId_idx" ON "subject_contents"("schoolId"); + +-- CreateIndex +CREATE INDEX "subject_contents_categoryId_idx" ON "subject_contents"("categoryId"); + +-- CreateIndex +CREATE INDEX "subject_contents_parentId_idx" ON "subject_contents"("parentId"); + +-- CreateIndex +CREATE INDEX "subject_contents_slug_idx" ON "subject_contents"("slug"); + +-- CreateIndex +CREATE INDEX "subject_contents_contentType_idx" ON "subject_contents"("contentType"); + +-- CreateIndex +CREATE INDEX "content_change_requests_schoolId_idx" ON "content_change_requests"("schoolId"); + +-- CreateIndex +CREATE INDEX "content_change_requests_contentId_idx" ON "content_change_requests"("contentId"); + +-- CreateIndex +CREATE INDEX "content_change_requests_status_idx" ON "content_change_requests"("status"); + +-- CreateIndex +CREATE INDEX "content_change_requests_requestedBy_idx" ON "content_change_requests"("requestedBy"); + +-- CreateIndex +CREATE UNIQUE INDEX "ai_settings_schoolId_key" ON "ai_settings"("schoolId"); + +-- CreateIndex +CREATE UNIQUE INDEX "virtual_characters_userId_key" ON "virtual_characters"("userId"); + +-- CreateIndex +CREATE INDEX "virtual_characters_userId_idx" ON "virtual_characters"("userId"); + +-- CreateIndex +CREATE INDEX "virtual_characters_schoolId_idx" ON "virtual_characters"("schoolId"); + +-- CreateIndex +CREATE INDEX "chat_messages_userId_idx" ON "chat_messages"("userId"); + +-- CreateIndex +CREATE INDEX "chat_messages_roomId_idx" ON "chat_messages"("roomId"); + +-- CreateIndex +CREATE INDEX "chat_messages_schoolId_idx" ON "chat_messages"("schoolId"); + +-- CreateIndex +CREATE INDEX "chat_messages_createdAt_idx" ON "chat_messages"("createdAt"); + +-- CreateIndex +CREATE INDEX "illness_reports_schoolId_idx" ON "illness_reports"("schoolId"); + +-- CreateIndex +CREATE INDEX "illness_reports_studentId_idx" ON "illness_reports"("studentId"); + +-- CreateIndex +CREATE INDEX "illness_reports_reportedBy_idx" ON "illness_reports"("reportedBy"); + +-- CreateIndex +CREATE INDEX "illness_reports_parentApprovalStatus_idx" ON "illness_reports"("parentApprovalStatus"); + +-- CreateIndex +CREATE INDEX "illness_reports_status_idx" ON "illness_reports"("status"); + +-- CreateIndex +CREATE INDEX "communication_rooms_schoolId_idx" ON "communication_rooms"("schoolId"); + +-- CreateIndex +CREATE INDEX "communication_rooms_studentId_idx" ON "communication_rooms"("studentId"); + +-- CreateIndex +CREATE INDEX "communication_rooms_teacherId_idx" ON "communication_rooms"("teacherId"); + +-- CreateIndex +CREATE INDEX "communication_rooms_status_idx" ON "communication_rooms"("status"); + +-- CreateIndex +CREATE INDEX "communication_room_members_roomId_idx" ON "communication_room_members"("roomId"); + +-- CreateIndex +CREATE INDEX "communication_room_members_userId_idx" ON "communication_room_members"("userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "communication_room_members_roomId_userId_key" ON "communication_room_members"("roomId", "userId"); + +-- CreateIndex +CREATE INDEX "communication_messages_roomId_idx" ON "communication_messages"("roomId"); + +-- CreateIndex +CREATE INDEX "communication_messages_senderId_idx" ON "communication_messages"("senderId"); + +-- CreateIndex +CREATE INDEX "communication_messages_createdAt_idx" ON "communication_messages"("createdAt"); + +-- CreateIndex +CREATE INDEX "counseling_appointments_schoolId_idx" ON "counseling_appointments"("schoolId"); + +-- CreateIndex +CREATE INDEX "counseling_appointments_counselorId_idx" ON "counseling_appointments"("counselorId"); + +-- CreateIndex +CREATE INDEX "counseling_appointments_studentId_idx" ON "counseling_appointments"("studentId"); + +-- CreateIndex +CREATE INDEX "counseling_appointments_status_idx" ON "counseling_appointments"("status"); + +-- CreateIndex +CREATE INDEX "counseling_appointments_scheduledAt_idx" ON "counseling_appointments"("scheduledAt"); + +-- CreateIndex +CREATE INDEX "disciplinary_committees_schoolId_idx" ON "disciplinary_committees"("schoolId"); + +-- CreateIndex +CREATE INDEX "disciplinary_committee_members_committeeId_idx" ON "disciplinary_committee_members"("committeeId"); + +-- CreateIndex +CREATE INDEX "disciplinary_committee_members_userId_idx" ON "disciplinary_committee_members"("userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "disciplinary_committee_members_committeeId_userId_key" ON "disciplinary_committee_members"("committeeId", "userId"); + +-- CreateIndex +CREATE INDEX "disciplinary_cases_schoolId_idx" ON "disciplinary_cases"("schoolId"); + +-- CreateIndex +CREATE INDEX "disciplinary_cases_committeeId_idx" ON "disciplinary_cases"("committeeId"); + +-- CreateIndex +CREATE INDEX "disciplinary_cases_studentId_idx" ON "disciplinary_cases"("studentId"); + +-- CreateIndex +CREATE INDEX "disciplinary_cases_status_idx" ON "disciplinary_cases"("status"); + +-- CreateIndex +CREATE INDEX "ai_test_generations_schoolId_idx" ON "ai_test_generations"("schoolId"); + +-- CreateIndex +CREATE INDEX "ai_test_generations_assessmentId_idx" ON "ai_test_generations"("assessmentId"); + +-- CreateIndex +CREATE INDEX "ai_test_generations_studentId_idx" ON "ai_test_generations"("studentId"); + +-- CreateIndex +CREATE INDEX "ai_test_generations_classGroupId_idx" ON "ai_test_generations"("classGroupId"); + +-- CreateIndex +CREATE INDEX "teacher_grading_reviews_schoolId_idx" ON "teacher_grading_reviews"("schoolId"); + +-- CreateIndex +CREATE INDEX "teacher_grading_reviews_assessmentId_idx" ON "teacher_grading_reviews"("assessmentId"); + +-- CreateIndex +CREATE INDEX "teacher_grading_reviews_teacherId_idx" ON "teacher_grading_reviews"("teacherId"); + +-- CreateIndex +CREATE INDEX "teacher_grading_reviews_status_idx" ON "teacher_grading_reviews"("status"); + +-- CreateIndex +CREATE INDEX "grading_review_comments_reviewId_idx" ON "grading_review_comments"("reviewId"); + +-- CreateIndex +CREATE INDEX "grading_review_comments_userId_idx" ON "grading_review_comments"("userId"); + +-- CreateIndex +CREATE INDEX "grading_annotations_schoolId_idx" ON "grading_annotations"("schoolId"); + +-- CreateIndex +CREATE INDEX "grading_annotations_assessmentId_idx" ON "grading_annotations"("assessmentId"); + +-- CreateIndex +CREATE INDEX "grading_annotations_studentId_idx" ON "grading_annotations"("studentId"); + +-- CreateIndex +CREATE INDEX "grading_annotations_teacherId_idx" ON "grading_annotations"("teacherId"); + +-- CreateIndex +CREATE INDEX "grading_annotations_page_idx" ON "grading_annotations"("page"); + +-- CreateIndex +CREATE UNIQUE INDEX "exam_plans_calendarEventId_key" ON "exam_plans"("calendarEventId"); + +-- CreateIndex +CREATE UNIQUE INDEX "exam_plans_assessmentId_key" ON "exam_plans"("assessmentId"); + +-- CreateIndex +CREATE INDEX "exam_plans_schoolId_idx" ON "exam_plans"("schoolId"); + +-- CreateIndex +CREATE INDEX "exam_plans_teacherId_idx" ON "exam_plans"("teacherId"); + +-- CreateIndex +CREATE INDEX "exam_plans_subjectId_idx" ON "exam_plans"("subjectId"); + +-- CreateIndex +CREATE INDEX "exam_plans_classGroupId_idx" ON "exam_plans"("classGroupId"); + +-- CreateIndex +CREATE INDEX "exam_plans_date_idx" ON "exam_plans"("date"); + +-- CreateIndex +CREATE INDEX "exam_plans_status_idx" ON "exam_plans"("status"); + +-- CreateIndex +CREATE INDEX "study_plans_schoolId_idx" ON "study_plans"("schoolId"); + +-- CreateIndex +CREATE INDEX "study_plans_studentId_idx" ON "study_plans"("studentId"); + +-- CreateIndex +CREATE INDEX "study_plans_dayOfWeek_idx" ON "study_plans"("dayOfWeek"); + +-- CreateIndex +CREATE INDEX "study_plans_status_idx" ON "study_plans"("status"); + +-- CreateIndex +CREATE INDEX "study_sessions_schoolId_idx" ON "study_sessions"("schoolId"); + +-- CreateIndex +CREATE INDEX "study_sessions_studentId_idx" ON "study_sessions"("studentId"); + +-- CreateIndex +CREATE INDEX "study_sessions_startTime_idx" ON "study_sessions"("startTime"); + +-- CreateIndex +CREATE INDEX "study_sessions_status_idx" ON "study_sessions"("status"); + +-- CreateIndex +CREATE INDEX "library_books_schoolId_idx" ON "library_books"("schoolId"); + +-- CreateIndex +CREATE INDEX "library_books_category_idx" ON "library_books"("category"); + +-- CreateIndex +CREATE INDEX "library_books_readingLevel_idx" ON "library_books"("readingLevel"); + +-- CreateIndex +CREATE INDEX "library_books_author_idx" ON "library_books"("author"); + +-- CreateIndex +CREATE INDEX "book_checkouts_schoolId_idx" ON "book_checkouts"("schoolId"); + +-- CreateIndex +CREATE INDEX "book_checkouts_bookId_idx" ON "book_checkouts"("bookId"); + +-- CreateIndex +CREATE INDEX "book_checkouts_studentId_idx" ON "book_checkouts"("studentId"); + +-- CreateIndex +CREATE INDEX "book_checkouts_status_idx" ON "book_checkouts"("status"); + +-- CreateIndex +CREATE INDEX "book_checkouts_dueDate_idx" ON "book_checkouts"("dueDate"); + +-- CreateIndex +CREATE INDEX "book_reservations_schoolId_idx" ON "book_reservations"("schoolId"); + +-- CreateIndex +CREATE INDEX "book_reservations_bookId_idx" ON "book_reservations"("bookId"); + +-- CreateIndex +CREATE INDEX "book_reservations_studentId_idx" ON "book_reservations"("studentId"); + +-- CreateIndex +CREATE INDEX "book_reservations_status_idx" ON "book_reservations"("status"); + +-- CreateIndex +CREATE INDEX "seating_charts_schoolId_idx" ON "seating_charts"("schoolId"); + +-- CreateIndex +CREATE INDEX "seating_charts_classGroupId_idx" ON "seating_charts"("classGroupId"); + +-- CreateIndex +CREATE INDEX "seating_charts_teacherId_idx" ON "seating_charts"("teacherId"); + +-- CreateIndex +CREATE INDEX "data_import_jobs_schoolId_idx" ON "data_import_jobs"("schoolId"); + +-- CreateIndex +CREATE INDEX "data_import_jobs_userId_idx" ON "data_import_jobs"("userId"); + +-- CreateIndex +CREATE INDEX "data_export_jobs_schoolId_idx" ON "data_export_jobs"("schoolId"); + +-- CreateIndex +CREATE INDEX "data_export_jobs_userId_idx" ON "data_export_jobs"("userId"); + +-- CreateIndex +CREATE INDEX "wellness_checkins_schoolId_idx" ON "wellness_checkins"("schoolId"); + +-- CreateIndex +CREATE INDEX "wellness_checkins_studentId_idx" ON "wellness_checkins"("studentId"); + +-- CreateIndex +CREATE INDEX "wellness_checkins_date_idx" ON "wellness_checkins"("date"); + +-- CreateIndex +CREATE INDEX "wellness_scores_schoolId_idx" ON "wellness_scores"("schoolId"); + +-- CreateIndex +CREATE INDEX "wellness_scores_studentId_idx" ON "wellness_scores"("studentId"); + +-- CreateIndex +CREATE INDEX "wellness_scores_date_idx" ON "wellness_scores"("date"); + +-- CreateIndex +CREATE UNIQUE INDEX "career_profiles_studentId_key" ON "career_profiles"("studentId"); + +-- CreateIndex +CREATE INDEX "career_profiles_schoolId_idx" ON "career_profiles"("schoolId"); + +-- CreateIndex +CREATE INDEX "career_profiles_studentId_idx" ON "career_profiles"("studentId"); + +-- CreateIndex +CREATE INDEX "career_profiles_careerCluster_idx" ON "career_profiles"("careerCluster"); + +-- CreateIndex +CREATE INDEX "career_goals_profileId_idx" ON "career_goals"("profileId"); + +-- CreateIndex +CREATE INDEX "career_goals_category_idx" ON "career_goals"("category"); + +-- CreateIndex +CREATE INDEX "career_goals_status_idx" ON "career_goals"("status"); + +-- CreateIndex +CREATE INDEX "career_appointments_schoolId_idx" ON "career_appointments"("schoolId"); + +-- CreateIndex +CREATE INDEX "career_appointments_profileId_idx" ON "career_appointments"("profileId"); + +-- CreateIndex +CREATE INDEX "career_appointments_counselorId_idx" ON "career_appointments"("counselorId"); + +-- CreateIndex +CREATE INDEX "career_appointments_date_idx" ON "career_appointments"("date"); + +-- CreateIndex +CREATE INDEX "career_appointments_status_idx" ON "career_appointments"("status"); + +-- CreateIndex +CREATE UNIQUE INDEX "substitute_teachers_userId_key" ON "substitute_teachers"("userId"); + +-- CreateIndex +CREATE INDEX "substitute_teachers_schoolId_idx" ON "substitute_teachers"("schoolId"); + +-- CreateIndex +CREATE INDEX "substitute_teachers_userId_idx" ON "substitute_teachers"("userId"); + +-- CreateIndex +CREATE INDEX "teacher_absences_schoolId_idx" ON "teacher_absences"("schoolId"); + +-- CreateIndex +CREATE INDEX "teacher_absences_teacherId_idx" ON "teacher_absences"("teacherId"); + +-- CreateIndex +CREATE INDEX "teacher_absences_startDate_idx" ON "teacher_absences"("startDate"); + +-- CreateIndex +CREATE INDEX "substitution_assignments_schoolId_idx" ON "substitution_assignments"("schoolId"); + +-- CreateIndex +CREATE INDEX "substitution_assignments_absenceId_idx" ON "substitution_assignments"("absenceId"); + +-- CreateIndex +CREATE INDEX "substitution_assignments_substituteId_idx" ON "substitution_assignments"("substituteId"); + +-- CreateIndex +CREATE INDEX "substitution_assignments_date_idx" ON "substitution_assignments"("date"); + +-- CreateIndex +CREATE INDEX "GradeReport_schoolId_idx" ON "GradeReport"("schoolId"); + +-- CreateIndex +CREATE INDEX "GradeReport_generatedBy_idx" ON "GradeReport"("generatedBy"); + +-- CreateIndex +CREATE INDEX "DataRetentionPolicy_schoolId_idx" ON "DataRetentionPolicy"("schoolId"); + +-- CreateIndex +CREATE INDEX "DataRetentionPolicy_dataCategory_idx" ON "DataRetentionPolicy"("dataCategory"); + +-- CreateIndex +CREATE INDEX "DpiaRecord_schoolId_idx" ON "DpiaRecord"("schoolId"); + +-- CreateIndex +CREATE INDEX "DpiaRecord_moduleScope_idx" ON "DpiaRecord"("moduleScope"); + +-- CreateIndex +CREATE UNIQUE INDEX "DpiaRecord_schoolId_moduleScope_key" ON "DpiaRecord"("schoolId", "moduleScope"); + +-- CreateIndex +CREATE INDEX "ComplianceConsultation_schoolId_idx" ON "ComplianceConsultation"("schoolId"); + +-- CreateIndex +CREATE INDEX "ComplianceConsultation_consultationType_idx" ON "ComplianceConsultation"("consultationType"); + +-- CreateIndex +CREATE UNIQUE INDEX "SchoolComplianceStatus_schoolId_key" ON "SchoolComplianceStatus"("schoolId"); + +-- CreateIndex +CREATE INDEX "DeletionFlag_schoolId_idx" ON "DeletionFlag"("schoolId"); + +-- CreateIndex +CREATE INDEX "DeletionFlag_entityType_idx" ON "DeletionFlag"("entityType"); + +-- CreateIndex +CREATE INDEX "DeletionFlag_flaggedAt_idx" ON "DeletionFlag"("flaggedAt"); diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..2a5a444 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "sqlite" diff --git a/prisma/schema.prisma b/prisma/schema.prisma old mode 100755 new mode 100644 index 1ba5e0e..f633f91 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -116,6 +116,12 @@ model School { substituteTeachers SubstituteTeacher[] teacherAbsences TeacherAbsence[] substitutionAssignments SubstitutionAssignment[] + // Module L compliance relations + dataRetentionPolicies DataRetentionPolicy[] + dpiaRecords DpiaRecord[] + complianceConsultations ComplianceConsultation[] + complianceStatus SchoolComplianceStatus? + deletionFlags DeletionFlag[] } model User { @@ -125,10 +131,11 @@ model User { passwordHash String firstName String lastName String - role String @default("TEACHER") // TEACHER, SCHOOL_ADMIN, SUPER_ADMIN, STUDENT, PARENT, VICE_PRINCIPAL + role String @default("TEACHER") // TEACHER, SCHOOL_ADMIN, SUPER_ADMIN, STUDENT, PARENT, VICE_PRINCIPAL, DPO locale String @default("de") twoFactorSecret String? isDemo Boolean @default(false) + isTeacher Boolean @default(false) // civil-servant status for Personalrat rights createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime? @@ -207,6 +214,11 @@ model User { careerAppointments CareerAppointment[] substituteTeacher SubstituteTeacher? teacherAbsences TeacherAbsence[] + // Module L compliance relations + retentionPoliciesReviewed DataRetentionPolicy[] @relation("DataRetentionPolicyReviewer") + dpiaRecordsApproved DpiaRecord[] @relation("DpiaRecordApprover") + complianceConsultationsAcknowledged ComplianceConsultation[] @relation("ComplianceConsultationAcknowledger") + deletionFlagsApproved DeletionFlag[] @relation("DeletionFlagApprover") } model SchoolYear { @@ -651,9 +663,13 @@ model AuditLog { userAgent String? timestamp DateTime @default(now()) metadata String? // JSON + dataSubject String? // track which student (if applicable) for GDPR export scope user User? @relation(fields: [userId], references: [id]) school School? @relation(fields: [schoolId], references: [id]) + + @@index([dataSubject]) + @@index([timestamp]) } model DataExportRequest { @@ -3216,3 +3232,94 @@ model GradeReport { @@index([schoolId]) @@index([generatedBy]) } + +// ─── Module L: Legal & Data Protection Compliance ────────────────── + +model DataRetentionPolicy { + id String @id @default(cuid()) + schoolId String + dataCategory String // LEARNING_PROGRESS, ASSESSMENT_RESULT, FINAL_REPORT, DISCIPLINARY_RECORD, EXAM_INCIDENT, MESSAGE, AI_TUTOR_LOG, NOTEBOOK_PAGE, SIGNAGE_LOG, TEACHER_AUDIT_LOG + retentionPeriodMonths Int? // nullable to force DPO to fill in legally-mandated categories + legalBasisNote String? // DPO notes on state-specific retention rule + isDefault Boolean @default(false) // false = legally mandated; true = optional product data + reviewedByUserId String? // DPO approval + reviewedAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + school School @relation(fields: [schoolId], references: [id]) + reviewedByUser User? @relation("DataRetentionPolicyReviewer", fields: [reviewedByUserId], references: [id]) + + @@index([schoolId]) + @@index([dataCategory]) +} + +model DpiaRecord { + id String @id @default(cuid()) + schoolId String + moduleScope String // AI_TUTOR, EXAM_MODE, WELLNESS_CHECKS, SIGNAGE_EMERGENCY, MESSAGING, LEARNING_ANALYTICS + dataProcessingSummary String // what's processed, for how long, who sees it + completedAt DateTime + approvedByUserId String? // DPO approval + documentUrl String? // uploaded DPIA PDF + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + school School @relation(fields: [schoolId], references: [id]) + approvedByUser User? @relation("DpiaRecordApprover", fields: [approvedByUserId], references: [id]) + + @@index([schoolId]) + @@index([moduleScope]) + @@unique([schoolId, moduleScope]) +} + +model ComplianceConsultation { + id String @id @default(cuid()) + schoolId String + consultationType String // ELTERNBEIRAT, SCHULKONFERENZ, PERSONALRAT + moduleScope String? // specific module if Personalrat for monitoring-capable features + acknowledgedByUserId String + acknowledgedAt DateTime + notes String? // what was consulted/approved + createdAt DateTime @default(now()) + + school School @relation(fields: [schoolId], references: [id]) + acknowledgedByUser User @relation("ComplianceConsultationAcknowledger", fields: [acknowledgedByUserId], references: [id]) + + @@index([schoolId]) + @@index([consultationType]) +} + +model SchoolComplianceStatus { + id String @id @default(cuid()) + schoolId String @unique + bundesland String? // BERLIN, BREMEN, HAMBURG, etc. (16 states) + onboardingCompleted Boolean @default(false) + dpoDpoContactEmail String? + schultragerContactEmail String? + moduleGates String? // JSON: { "AI_TUTOR": { isApprovedForUse: bool, diaCompleted: bool }, ... } + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + school School @relation(fields: [schoolId], references: [id]) +} + +model DeletionFlag { + id String @id @default(cuid()) + schoolId String + entityType String // Student, Message, Notebook, etc. + entityId String + dataCategory String // retention category reference + flaggedAt DateTime @default(now()) + approvedByUserId String? + approvedAt DateTime? + deletedAt DateTime? + reason String // "retention period expired", "erasure request", etc. + + school School @relation(fields: [schoolId], references: [id]) + approvedByUser User? @relation("DeletionFlagApprover", fields: [approvedByUserId], references: [id]) + + @@index([schoolId]) + @@index([entityType]) + @@index([flaggedAt]) +} diff --git a/scripts/generate-data-model.ts b/scripts/generate-data-model.ts new file mode 100644 index 0000000..114833e --- /dev/null +++ b/scripts/generate-data-model.ts @@ -0,0 +1,178 @@ +/** + * Generate DATA_MODEL.md from Prisma schema and database config + * This document lists all data entities, their purposes, legal basis, and retention categories + */ + +const fs = require("fs"); +const path = require("path"); + +interface DataEntity { + name: string; + dataCategories: string[]; + legalBasis: string; + purpose: string; + retentionCategory: string; + notes?: string; +} + +// Core data model definition +const dataModel: DataEntity[] = [ + { + name: "User", + dataCategories: ["firstName", "lastName", "email", "role", "createdAt"], + legalBasis: "GDPR Art. 6(1)(c) - Legal obligation (Schulgesetz)", + purpose: "User account management and authentication", + retentionCategory: "TEACHER_AUDIT_LOG", + notes: "Retained per employment contract or student enrollment period", + }, + { + name: "Student", + dataCategories: [ + "firstName", + "lastName", + "dateOfBirth", + "email", + "phone", + "address", + ], + legalBasis: "GDPR Art. 6(1)(c) - Legal obligation (Schulgesetz)", + purpose: "Student record management and communication", + retentionCategory: "LEARNING_PROGRESS", + }, + { + name: "ClassGroup", + dataCategories: ["name", "grade", "year", "studentCount"], + legalBasis: "GDPR Art. 6(1)(c) - Legal obligation (Schulgesetz)", + purpose: "Class organization and grouping", + retentionCategory: "LEARNING_PROGRESS", + }, + { + name: "Assessment", + dataCategories: ["studentId", "subjectId", "score", "feedback", "date"], + legalBasis: + "GDPR Art. 6(1)(a) - Consent + Art. 8 (children) - Parental consent", + purpose: "Record student learning progress and performance", + retentionCategory: "ASSESSMENT_RESULT", + }, + { + name: "Report (Final Grade Report)", + dataCategories: ["studentId", "finalGrades", "comments", "semester"], + legalBasis: + "GDPR Art. 6(1)(c) - Legal obligation (Schulgesetz - long-term retention requirement)", + purpose: "Permanent record of student academic achievement", + retentionCategory: "FINAL_REPORT", + notes: "Typically retained for student lifetime + 30 years (per Bundesland law)", + }, + { + name: "BehaviorIncident", + dataCategories: ["studentId", "incidentType", "description", "resolution"], + legalBasis: + "GDPR Art. 6(1)(a) - Consent + Art. 9(2)(h) - Health/safety necessity", + purpose: "Track disciplinary actions and behavioral support", + retentionCategory: "DISCIPLINARY_RECORD", + notes: "Retention varies by Bundesland; typically 3-10 years", + }, + { + name: "ExamIncidentEvent", + dataCategories: ["studentId", "eventType", "timestamp", "notes"], + legalBasis: + "GDPR Art. 6(1)(a) - Consent + Art. 9(2)(h) - Exam integrity necessity", + purpose: "Log exam-time incidents (distress, technical issues)", + retentionCategory: "EXAM_INCIDENT", + notes: "Shorter retention (6-12 months) unless escalated to disciplinary record", + }, + { + name: "Message", + dataCategories: ["senderId", "recipientId", "content", "timestamp"], + legalBasis: "GDPR Art. 6(1)(a) - Consent (user opt-in for messaging)", + purpose: "Communication between teachers, parents, and students", + retentionCategory: "MESSAGE", + }, + { + name: "AuditLog", + dataCategories: ["userId", "action", "entityType", "timestamp"], + legalBasis: "GDPR Art. 6(1)(c) - Legal obligation (data protection compliance)", + purpose: "Audit trail for regulatory compliance and breach investigation", + retentionCategory: "TEACHER_AUDIT_LOG", + }, + { + name: "Notebook (Student Learning Notebooks)", + dataCategories: ["studentId", "content", "createdAt"], + legalBasis: "GDPR Art. 6(1)(a) - Consent (student/parent consent)", + purpose: "Student learning portfolio and reflection entries", + retentionCategory: "NOTEBOOK_PAGE", + }, + { + name: "AITutorLog", + dataCategories: ["studentId", "interaction", "timestamp", "response"], + legalBasis: "GDPR Art. 6(1)(a) - Explicit consent + Art. 22 - Automated decisions", + purpose: "Track AI tutor interactions for learning analytics", + retentionCategory: "AI_TUTOR_LOG", + notes: "Subject to Art. 22 GDPR - no solely automated grading decisions", + }, +]; + +export function generateDataModel(outputPath: string = "docs/legal"): void { + if (!fs.existsSync(outputPath)) { + fs.mkdirSync(outputPath, { recursive: true }); + } + + const lines: string[] = [ + "# Data Model Documentation", + "", + "**Status**: Schema Reference for ExaMetra (Module L)", + "", + "This document outlines all personal data entities collected by ExaMetra, their legal basis, purpose, and retention category.", + "", + "## Core Entities", + "", + "| Entity | Data Categories | Legal Basis | Purpose | Retention Category | Notes |", + "|--------|-----------------|------------|---------|-------------------|-------|", + ]; + + for (const entity of dataModel) { + const dataCategories = entity.dataCategories.join(", "); + const notes = entity.notes ? `${entity.notes}` : "—"; + const row = `| **${entity.name}** | ${dataCategories} | ${entity.legalBasis} | ${entity.purpose} | **${entity.retentionCategory}** | ${notes} |`; + lines.push(row); + } + + lines.push( + "", + "## Retention Categories", + "", + "| Category | Typical Retention | Legal Basis |", + "|----------|------------------|------------|", + "| LEARNING_PROGRESS | 1-2 years | Bundesland school law (Schulgesetz) |", + "| ASSESSMENT_RESULT | 1-2 years | Bundesland school law |", + "| FINAL_REPORT | 30+ years | Permanent record under Schulgesetz |", + "| DISCIPLINARY_RECORD | 3-10 years | Varies by Bundesland |", + "| EXAM_INCIDENT | 6-12 months | Exam integrity + school safety |", + "| MESSAGE | 1-2 years | Communication retention best practices |", + "| AI_TUTOR_LOG | 6-12 months | Pedagogical purpose + learning analytics |", + "| NOTEBOOK_PAGE | 1-2 years | Student portfolio / learning records |", + "| SIGNAGE_LOG | 3 months | Safety monitoring (emergency only) |", + "| TEACHER_AUDIT_LOG | 1-2 years | Compliance and audit trail |", + "", + "## Compliance Notes", + "", + "- **DPO Review**: Each retention period must be reviewed and approved by your school's Data Protection Officer (DPO)", + "- **Bundesland Variations**: Retention periods may vary by state (Bundesland). Consult your regional school authority.", + "- **Parental Consent**: For student data (especially minors), parental consent is required per Art. 8 GDPR", + "- **Automated Decisions**: No solely automated decisions (e.g., automatic grade lowering) without human review per Art. 22 GDPR", + "- **Data Subject Rights**: All entities are subject to GDPR rights: access, correction, erasure (where legally permitted), portability", + "", + "## Generated From", + "", + `Generated: ${new Date().toISOString()}`, + "Source: ExaMetra Prisma schema (Module L)", + ]; + + const filePath = path.join(outputPath, "DATA_MODEL.md"); + fs.writeFileSync(filePath, lines.join("\n"), "utf-8"); + console.log(`[v0] Generated DATA_MODEL.md at ${filePath}`); +} + +if (require.main === module) { + generateDataModel(); +} diff --git a/scripts/generate-privacy-notice.ts b/scripts/generate-privacy-notice.ts new file mode 100644 index 0000000..5e2a84a --- /dev/null +++ b/scripts/generate-privacy-notice.ts @@ -0,0 +1,150 @@ +/** + * Generate Datenschutzerklärung (Privacy Notice) for parents + * Plain-language German, based on school configuration + */ + +const fs = require("fs"); +const path = require("path"); + +interface PrivacyNoticeParams { + schoolName: string; + dpoName: string; + dpoEmail: string; + schultragerName: string; + bundesland: string; +} + +export function generatePrivacyNotice( + params: PrivacyNoticeParams, + outputPath: string = "docs/legal" +): void { + if (!fs.existsSync(outputPath)) { + fs.mkdirSync(outputPath, { recursive: true }); + } + + const lines: string[] = [ + "# Datenschutzerklärung", + "# (Privacy Notice for Parents & Students)", + "", + `**Schule**: ${params.schoolName}`, + `**Bundesland**: ${params.bundesland}`, + `**Schulträger**: ${params.schultragerName}`, + `**Datenschutzbeauftragter (DPO)**: ${params.dpoName} (${params.dpoEmail})`, + "", + "---", + "", + "## 1. Wer ist verantwortlich für die Verarbeitung Ihrer Daten?", + "", + `Die **${params.schoolName}** ist die Schule, die ihre Daten verarbeitet.`, + `Der Schulträger ist: **${params.schultragerName}**`, + "", + "Der Datenschutzbeauftragte (DPO) ist:", + `- **Name**: ${params.dpoName}`, + `- **Email**: ${params.dpoEmail}`, + "", + "Sie können den DPO jederzeit kontaktieren, wenn Sie Fragen zu Ihren Daten haben.", + "", + "## 2. Welche Daten werden verarbeitet?", + "", + "Wir verarbeiten folgende personenbezogene Daten:", + "", + "### Schülerdaten", + "- Name, Vorname, Geburtsdatum", + "- Kontaktinformationen (Email, Telefon)", + "- Adresse", + "- Schulleistungen, Noten, Zeugnisse", + "- Verhalten und Disziplin", + "- Medizinische Informationen (falls relevant)", + "", + "### Lehrerdaten", + "- Name, Email, Telefon", + "- Unterrichtsvertrag und Stundenpläne", + "- Aktivitätsprotokoll (Audit Logs)", + "", + "### Elterndaten", + "- Name, Kontaktinformationen", + "- Nachrichten mit Lehrern", + "", + "## 3. Warum verarbeiten wir Ihre Daten?", + "", + "Wir verarbeiten Ihre Daten für folgende Zwecke:", + "", + "- **Schulverwaltung**: Verwaltung von Schülereinträgen und Klassengruppen", + "- **Unterricht & Bewertung**: Verfolgung von Lernfortschritten und Noten", + "- **Kommunikation**: Nachrichten zwischen Lehrern, Eltern und Schülern", + "- **Sicherheit**: Überwachung von Prüfungen und Schulsicherheit", + "- **Rechtliche Verpflichtungen**: Einhaltung des Schulgesetzes und der DSGVO", + "", + "## 4. Was ist die Rechtsgrundlage?", + "", + "Die Verarbeitung erfolgt auf Grundlage von:", + "", + "- **Schulgesetz** (Schulgesetz des Bundeslandes): Rechtliche Verpflichtung zur Führung von Schülerdaten", + "- **DSGVO Art. 6(1)(a)**: Ihre Zustimmung (z.B. für Messaging)", + "- **DSGVO Art. 6(1)(c)**: Erfüllung rechtlicher Verpflichtungen", + "- **DSGVO Art. 9(2)(h)**: Zwecke der Gesundheit und Sicherheit (z.B. Wellness-Überprüfungen)", + "", + "Für Minderjährige: **Eltern erteilen die Zustimmung** gemäß DSGVO Art. 8.", + "", + "## 5. Wie lange werden die Daten gespeichert?", + "", + "Die Speicherdauer hängt von der Datenkategorie ab:", + "", + "| Datentyp | Speicherdauer |", + "|----------|--------------|", + "| Lernfortschritt | 1–2 Jahre nach Schulabgang |", + "| Zeugnisse/Endbewertungen | Dauerhaft (Schulgesetz erforderlich) |", + "| Disziplinarberichte | 3–10 Jahre (je nach Bundesland) |", + "| Nachrichten | 1–2 Jahre |", + "| Audit Logs | 1–2 Jahre |", + "", + "Nach Ablauf der Aufbewahrungsfrist werden Ihre Daten gelöscht.", + "", + "## 6. Ihre Rechte", + "", + "Sie haben folgende Rechte unter der DSGVO:", + "", + "- **Auskunftsrecht** (Art. 15): Sie können fragen, welche Daten wir speichern", + "- **Berichtigungsrecht** (Art. 16): Sie können falsche Daten korrigieren lassen", + "- **Löschungsrecht** (Art. 17): Sie können verlangen, dass Ihre Daten gelöscht werden (sofern nicht gesetzlich erforderlich)", + "- **Datenportabilität** (Art. 20): Sie können Ihre Daten exportieren", + "- **Beschwerderecht**: Sie können sich an die Datenschutzbehörde beschweren", + "", + "Um diese Rechte auszuüben, kontaktieren Sie bitte den DPO.", + "", + "## 7. Sicherheit Ihrer Daten", + "", + "Wir schützen Ihre Daten durch:", + "", + "- Verschlüsselung (TLS) bei der Übertragung", + "- Verschlüsselung bei der Speicherung", + "- Rollenbasierte Zugriffskontrolle", + "- Regelmäßige Sicherheitsprüfungen", + "- Audit-Logs aller Datenzugriffe", + "", + "## 8. Kontakt", + "", + `Bei Fragen zum Datenschutz kontaktieren Sie bitte:`, + `- **DPO**: ${params.dpoName}`, + `- **Email**: ${params.dpoEmail}`, + "", + "---", + "", + `**Gültig ab**: ${new Date().toISOString().split("T")[0]}`, + "**Generiertaus**: ExaMetra Module L Compliance Framework", + ]; + + const filePath = path.join(outputPath, "privacy-notice-de.md"); + fs.writeFileSync(filePath, lines.join("\n"), "utf-8"); + console.log(`[v0] Generated privacy notice at ${filePath}`); +} + +if (require.main === module) { + generatePrivacyNotice({ + schoolName: "Gymnasium Berlin", + dpoName: "Max Mustermann", + dpoEmail: "dpo@gymnasium-berlin.de", + schultragerName: "Stadt Berlin", + bundesland: "BERLIN", + }); +} diff --git a/scripts/generate-ropa.ts b/scripts/generate-ropa.ts new file mode 100644 index 0000000..122a38b --- /dev/null +++ b/scripts/generate-ropa.ts @@ -0,0 +1,215 @@ +/** + * Generate Records of Processing Activities (Verzeichnis von Verarbeitungstätigkeiten) + * Compliant with GDPR Art. 30 requirements + * German legal document for school's DPO review + */ + +const fs = require("fs"); +const path = require("path"); + +interface RopaEntry { + name: string; + purposes: string[]; + dataCategories: string[]; + recipients: string[]; + retentionPeriod: string; + legalBasis: string; + securityMeasures: string[]; +} + +const ropas: RopaEntry[] = [ + { + name: "Student Information Management", + purposes: [ + "Student enrollment and record maintenance", + "Educational progress tracking", + "Communication with parents/guardians", + ], + dataCategories: ["Name", "DOB", "Contact information", "Academic records"], + recipients: ["Teachers", "School administration", "Parents/guardians"], + retentionPeriod: "Duration of enrollment + 2 years", + legalBasis: + "GDPR Art. 6(1)(c) - Legal obligation under Schulgesetz (School Law)", + securityMeasures: [ + "Role-based access control (RBAC)", + "Encryption at rest and in transit", + "Audit logging of all data access", + "Regular security audits", + ], + }, + { + name: "Learning Progress & Assessment", + purposes: [ + "Track student learning outcomes", + "Generate report cards and grades", + "Identify learning support needs", + ], + dataCategories: [ + "Assessment scores", + "Teacher feedback", + "Learning competencies", + ], + recipients: ["Teachers", "School administration", "Parents"], + retentionPeriod: "1-2 years per Bundesland regulations", + legalBasis: + "GDPR Art. 6(1)(a) - Consent + Art. 9(2)(h) - Educational necessity", + securityMeasures: [ + "Pseudonymization of assessment data where possible", + "Teacher-student privacy boundaries", + "Parent-accessible portal with consent gates", + ], + }, + { + name: "AI Tutor Processing", + purposes: [ + "Personalized learning recommendations", + "Adaptive tutoring content delivery", + "Learning analytics and reporting", + ], + dataCategories: [ + "Student interaction logs", + "Learning preferences", + "Performance metrics", + "AI model inputs/outputs", + ], + recipients: ["AI service provider (if applicable)", "Teachers", "Students"], + retentionPeriod: "6-12 months after course completion", + legalBasis: + "GDPR Art. 6(1)(a) - Explicit consent + Art. 22 - Automated decision safeguards", + securityMeasures: [ + "Data minimization: only essential features processed", + "Transparency logs for AI decisions", + "Human-in-the-loop review for grade-affecting recommendations", + "Right to explanation mechanism", + ], + }, + { + name: "Exam Monitoring & Integrity", + purposes: [ + "Exam room monitoring for security", + "Incident logging (distress signals, technical issues)", + "Academic integrity verification", + ], + dataCategories: [ + "Exam room events", + "Student signals/requests", + "Technical logs", + "Audio/video (if applicable)", + ], + recipients: ["Exam proctors", "School administration"], + retentionPeriod: "6-12 months (shorter if no incident)", + legalBasis: + "GDPR Art. 6(1)(a) - Consent + Art. 9(2)(h) - Exam security necessity", + securityMeasures: [ + "Limited access to exam data (proctors only)", + "Video/audio encrypted and retained separately", + "Automated deletion of non-incident logs after retention period", + ], + }, + { + name: "Audit & Compliance Logging", + purposes: [ + "Regulatory compliance (GDPR, Schulgesetz)", + "Data breach investigation", + "User accountability and audit trail", + ], + dataCategories: ["User ID", "Action taken", "Timestamp", "Entity accessed"], + recipients: ["DPO", "School administration", "Schulträger (if requested)"], + retentionPeriod: "1-2 years", + legalBasis: "GDPR Art. 6(1)(c) - Legal obligation (compliance duty)", + securityMeasures: [ + "Immutable audit logs", + "Restricted DPO-only access", + "Regular integrity verification", + "Automated flagging of suspicious access patterns", + ], + }, +]; + +export function generateRopa(outputPath: string = "docs/legal"): void { + if (!fs.existsSync(outputPath)) { + fs.mkdirSync(outputPath, { recursive: true }); + } + + const lines: string[] = [ + "# Verzeichnis von Verarbeitungstätigkeiten", + "# (Records of Processing Activities - Art. 30 GDPR)", + "", + "**Schulname**: [To be filled by school]", + "**DPO**: [To be filled by school]", + "**Schulträger**: [To be filled by school]", + "**Erstellungsdatum**: " + new Date().toISOString(), + "", + "---", + "", + "## Einleitung", + "", + "Dieses Verzeichnis dokumentiert alle Verarbeitungstätigkeiten personenbezogener Daten durch ExaMetra im Rahmen des deutschen Schulgesetzes (Schulgesetz/BDSG-SchulG) und der DSGVO.", + "", + "**HINWEIS**: Dies ist eine Vorlage. Ihre Schule muss dieses Dokument durch ihren Datenschutzbeauftragten überprüfen und finalisieren, bevor es als offizielle Dokumentation gilt.", + "", + "---", + "", + ]; + + for (let i = 0; i < ropas.length; i++) { + const ropa = ropas[i]; + lines.push( + `## Verarbeitungstätigkeit ${i + 1}: ${ropa.name}`, + "", + "### Zwecke der Verarbeitung", + ...ropa.purposes.map((p) => `- ${p}`), + "", + "### Kategorien personenbezogener Daten", + ...ropa.dataCategories.map((c) => `- ${c}`), + "", + "### Kategorien von Empfängern", + ...ropa.recipients.map((r) => `- ${r}`), + "", + "### Speicherdauer", + `${ropa.retentionPeriod}`, + "", + "### Rechtsgrundlage", + `${ropa.legalBasis}`, + "", + "### Technische und organisatorische Maßnahmen", + ...ropa.securityMeasures.map((m) => `- ${m}`), + "" + ); + } + + lines.push( + "---", + "", + "## Betroffenenrechte (DSGVO Art. 12-22)", + "", + "Alle Personen haben das Recht auf:", + "- **Auskunft** (Art. 15): Zugang zu ihren Daten", + "- **Berichtigung** (Art. 16): Korrektur ungenauer Daten", + "- **Löschung** (Art. 17): Unter rechtlichen Bedingungen", + "- **Datenportabilität** (Art. 20): Export ihrer Daten", + "- **Widerspruch** (Art. 21): Gegen bestimmte Verarbeitungen", + "", + "Anträge richten Sie an den Datenschutzbeauftragten (DPO).", + "", + "## Datenschutzverletzungen", + "", + "Bei Verdacht auf eine Datenschutzverletzung kontaktieren Sie sofort den DPO und die zuständige Aufsichtsbehörde.", + "", + `**Generiert**: ${new Date().toISOString()}`, + "**Quelle**: ExaMetra Module L - Compliance Framework", + ]; + + const filePath = path.join( + outputPath, + "verzeichnis-verarbeitungstaetigkeiten.md" + ); + fs.writeFileSync(filePath, lines.join("\n"), "utf-8"); + console.log( + `[v0] Generated RoPA (Verzeichnis) at ${filePath}` + ); +} + +if (require.main === module) { + generateRopa(); +} diff --git a/scripts/retention-period-defaults.json b/scripts/retention-period-defaults.json new file mode 100644 index 0000000..cc47ed9 --- /dev/null +++ b/scripts/retention-period-defaults.json @@ -0,0 +1,194 @@ +{ + "BERLIN": { + "LEARNING_PROGRESS": 12, + "ASSESSMENT_RESULT": 12, + "FINAL_REPORT": 30, + "DISCIPLINARY_RECORD": 5, + "EXAM_INCIDENT": 6, + "MESSAGE": 12, + "AI_TUTOR_LOG": 6, + "NOTEBOOK_PAGE": 12, + "SIGNAGE_LOG": 3, + "TEACHER_AUDIT_LOG": 12 + }, + "BAYERN": { + "LEARNING_PROGRESS": 12, + "ASSESSMENT_RESULT": 12, + "FINAL_REPORT": 35, + "DISCIPLINARY_RECORD": 5, + "EXAM_INCIDENT": 6, + "MESSAGE": 12, + "AI_TUTOR_LOG": 6, + "NOTEBOOK_PAGE": 12, + "SIGNAGE_LOG": 3, + "TEACHER_AUDIT_LOG": 12 + }, + "BADEN_WUERTTEMBERG": { + "LEARNING_PROGRESS": 12, + "ASSESSMENT_RESULT": 12, + "FINAL_REPORT": 30, + "DISCIPLINARY_RECORD": 5, + "EXAM_INCIDENT": 6, + "MESSAGE": 12, + "AI_TUTOR_LOG": 6, + "NOTEBOOK_PAGE": 12, + "SIGNAGE_LOG": 3, + "TEACHER_AUDIT_LOG": 12 + }, + "HESSE": { + "LEARNING_PROGRESS": 12, + "ASSESSMENT_RESULT": 12, + "FINAL_REPORT": 30, + "DISCIPLINARY_RECORD": 5, + "EXAM_INCIDENT": 6, + "MESSAGE": 12, + "AI_TUTOR_LOG": 6, + "NOTEBOOK_PAGE": 12, + "SIGNAGE_LOG": 3, + "TEACHER_AUDIT_LOG": 12 + }, + "NORTH_RHINE_WESTPHALIA": { + "LEARNING_PROGRESS": 12, + "ASSESSMENT_RESULT": 12, + "FINAL_REPORT": 30, + "DISCIPLINARY_RECORD": 5, + "EXAM_INCIDENT": 6, + "MESSAGE": 12, + "AI_TUTOR_LOG": 6, + "NOTEBOOK_PAGE": 12, + "SIGNAGE_LOG": 3, + "TEACHER_AUDIT_LOG": 12 + }, + "LOWER_SAXONY": { + "LEARNING_PROGRESS": 12, + "ASSESSMENT_RESULT": 12, + "FINAL_REPORT": 30, + "DISCIPLINARY_RECORD": 5, + "EXAM_INCIDENT": 6, + "MESSAGE": 12, + "AI_TUTOR_LOG": 6, + "NOTEBOOK_PAGE": 12, + "SIGNAGE_LOG": 3, + "TEACHER_AUDIT_LOG": 12 + }, + "BREMEN": { + "LEARNING_PROGRESS": 12, + "ASSESSMENT_RESULT": 12, + "FINAL_REPORT": 30, + "DISCIPLINARY_RECORD": 5, + "EXAM_INCIDENT": 6, + "MESSAGE": 12, + "AI_TUTOR_LOG": 6, + "NOTEBOOK_PAGE": 12, + "SIGNAGE_LOG": 3, + "TEACHER_AUDIT_LOG": 12 + }, + "HAMBURG": { + "LEARNING_PROGRESS": 12, + "ASSESSMENT_RESULT": 12, + "FINAL_REPORT": 30, + "DISCIPLINARY_RECORD": 5, + "EXAM_INCIDENT": 6, + "MESSAGE": 12, + "AI_TUTOR_LOG": 6, + "NOTEBOOK_PAGE": 12, + "SIGNAGE_LOG": 3, + "TEACHER_AUDIT_LOG": 12 + }, + "RHINELAND_PALATINATE": { + "LEARNING_PROGRESS": 12, + "ASSESSMENT_RESULT": 12, + "FINAL_REPORT": 30, + "DISCIPLINARY_RECORD": 5, + "EXAM_INCIDENT": 6, + "MESSAGE": 12, + "AI_TUTOR_LOG": 6, + "NOTEBOOK_PAGE": 12, + "SIGNAGE_LOG": 3, + "TEACHER_AUDIT_LOG": 12 + }, + "SAARLAND": { + "LEARNING_PROGRESS": 12, + "ASSESSMENT_RESULT": 12, + "FINAL_REPORT": 30, + "DISCIPLINARY_RECORD": 5, + "EXAM_INCIDENT": 6, + "MESSAGE": 12, + "AI_TUTOR_LOG": 6, + "NOTEBOOK_PAGE": 12, + "SIGNAGE_LOG": 3, + "TEACHER_AUDIT_LOG": 12 + }, + "SAXONY": { + "LEARNING_PROGRESS": 12, + "ASSESSMENT_RESULT": 12, + "FINAL_REPORT": 30, + "DISCIPLINARY_RECORD": 5, + "EXAM_INCIDENT": 6, + "MESSAGE": 12, + "AI_TUTOR_LOG": 6, + "NOTEBOOK_PAGE": 12, + "SIGNAGE_LOG": 3, + "TEACHER_AUDIT_LOG": 12 + }, + "SAXONY_ANHALT": { + "LEARNING_PROGRESS": 12, + "ASSESSMENT_RESULT": 12, + "FINAL_REPORT": 30, + "DISCIPLINARY_RECORD": 5, + "EXAM_INCIDENT": 6, + "MESSAGE": 12, + "AI_TUTOR_LOG": 6, + "NOTEBOOK_PAGE": 12, + "SIGNAGE_LOG": 3, + "TEACHER_AUDIT_LOG": 12 + }, + "SCHLESWIG_HOLSTEIN": { + "LEARNING_PROGRESS": 12, + "ASSESSMENT_RESULT": 12, + "FINAL_REPORT": 30, + "DISCIPLINARY_RECORD": 5, + "EXAM_INCIDENT": 6, + "MESSAGE": 12, + "AI_TUTOR_LOG": 6, + "NOTEBOOK_PAGE": 12, + "SIGNAGE_LOG": 3, + "TEACHER_AUDIT_LOG": 12 + }, + "THURINGIA": { + "LEARNING_PROGRESS": 12, + "ASSESSMENT_RESULT": 12, + "FINAL_REPORT": 30, + "DISCIPLINARY_RECORD": 5, + "EXAM_INCIDENT": 6, + "MESSAGE": 12, + "AI_TUTOR_LOG": 6, + "NOTEBOOK_PAGE": 12, + "SIGNAGE_LOG": 3, + "TEACHER_AUDIT_LOG": 12 + }, + "MECKLENBURG_VORPOMMERN": { + "LEARNING_PROGRESS": 12, + "ASSESSMENT_RESULT": 12, + "FINAL_REPORT": 30, + "DISCIPLINARY_RECORD": 5, + "EXAM_INCIDENT": 6, + "MESSAGE": 12, + "AI_TUTOR_LOG": 6, + "NOTEBOOK_PAGE": 12, + "SIGNAGE_LOG": 3, + "TEACHER_AUDIT_LOG": 12 + }, + "BRANDENBURG": { + "LEARNING_PROGRESS": 12, + "ASSESSMENT_RESULT": 12, + "FINAL_REPORT": 30, + "DISCIPLINARY_RECORD": 5, + "EXAM_INCIDENT": 6, + "MESSAGE": 12, + "AI_TUTOR_LOG": 6, + "NOTEBOOK_PAGE": 12, + "SIGNAGE_LOG": 3, + "TEACHER_AUDIT_LOG": 12 + } +} diff --git a/scripts/run-retention-job.ts b/scripts/run-retention-job.ts new file mode 100644 index 0000000..02cb2fc --- /dev/null +++ b/scripts/run-retention-job.ts @@ -0,0 +1,273 @@ +/** + * Daily Retention Enforcement Job + * Flags records that have exceeded their retention period + * Called once per day; can be integrated with cron (e.g., GitHub Actions, node-schedule) + * + * Usage: npx ts-node scripts/run-retention-job.ts + */ + +const { PrismaClient } = require("@prisma/client"); + +const prisma = new PrismaClient(); + +interface FlaggedRecord { + entityType: string; + entityId: string; + dataCategory: string; + flaggedAt: Date; + reason: string; +} + +/** + * Check records for each school and flag those exceeding retention period + */ +async function runRetentionJob() { + console.log("[v0] Starting retention enforcement job..."); + const startTime = Date.now(); + + try { + // Get all schools + const schools = await prisma.school.findMany({ + select: { id: true, name: true }, + }); + + let totalFlagged = 0; + const report: { school: string; flagged: number }[] = []; + + for (const school of schools) { + const schoolFlagged = await flagExpiredRecordsForSchool(school.id); + totalFlagged += schoolFlagged; + report.push({ school: school.name, flagged: schoolFlagged }); + + console.log( + `[v0] School "${school.name}": ${schoolFlagged} records flagged` + ); + } + + console.log( + `[v0] Retention job completed: ${totalFlagged} total records flagged` + ); + console.log(`[v0] Duration: ${Date.now() - startTime}ms`); + + // Log retention job to audit + await prisma.auditLog.create({ + data: { + action: "RETENTION_JOB_EXECUTED", + entityType: "RetentionJob", + metadata: JSON.stringify({ + totalFlagged, + schoolsProcessed: schools.length, + timestamp: new Date().toISOString(), + report, + }), + }, + }); + + return { success: true, totalFlagged, report }; + } catch (error) { + console.error("[v0] Error in retention job:", error); + throw error; + } finally { + await prisma.$disconnect(); + } +} + +/** + * Flag expired records for a specific school + */ +async function flagExpiredRecordsForSchool(schoolId: string): Promise { + // Get all retention policies for this school + const policies = await prisma.dataRetentionPolicy.findMany({ + where: { schoolId }, + }); + + let flaggedCount = 0; + + for (const policy of policies) { + if (!policy.retentionPeriodMonths) continue; + + // Calculate expiry date + const expiryDate = new Date(); + expiryDate.setMonth(expiryDate.getMonth() - policy.retentionPeriodMonths); + + // Flag expired records based on category + const categoryFlagged = await flagRecordsByCategory( + schoolId, + policy.dataCategory, + expiryDate + ); + + flaggedCount += categoryFlagged; + } + + return flaggedCount; +} + +/** + * Flag records of a specific category that are older than expiryDate + */ +async function flagRecordsByCategory( + schoolId: string, + dataCategory: string, + expiryDate: Date +): Promise { + let flaggedCount = 0; + + try { + switch (dataCategory) { + case "LEARNING_PROGRESS": + flaggedCount += await flagRecordsByEntityType( + schoolId, + "LearningProgressEntry", + expiryDate + ); + break; + + case "ASSESSMENT_RESULT": + flaggedCount += await flagRecordsByEntityType( + schoolId, + "Assessment", + expiryDate + ); + break; + + case "FINAL_REPORT": + // Final reports should NOT be auto-flagged (legal requirement to keep permanently) + console.log( + `[v0] Skipping FINAL_REPORT for ${schoolId} (legally mandated retention)` + ); + break; + + case "DISCIPLINARY_RECORD": + flaggedCount += await flagRecordsByEntityType( + schoolId, + "DisciplinaryCase", + expiryDate + ); + break; + + case "EXAM_INCIDENT": + flaggedCount += await flagRecordsByEntityType( + schoolId, + "ExamIncidentEvent", + expiryDate + ); + break; + + case "MESSAGE": + flaggedCount += await flagRecordsByEntityType( + schoolId, + "ParentMessage", + expiryDate + ); + break; + + case "AI_TUTOR_LOG": + // Note: In production, this would reference actual AI tutor logs + console.log(`[v0] AI_TUTOR_LOG flagging not yet implemented`); + break; + + case "NOTEBOOK_PAGE": + flaggedCount += await flagRecordsByEntityType( + schoolId, + "Notebook", + expiryDate + ); + break; + + case "SIGNAGE_LOG": + // Note: Reference actual signage log entity + console.log(`[v0] SIGNAGE_LOG flagging not yet implemented`); + break; + + case "TEACHER_AUDIT_LOG": + flaggedCount += await flagRecordsByEntityType( + schoolId, + "AuditLog", + expiryDate + ); + break; + } + } catch (error) { + console.error( + `[v0] Error flagging ${dataCategory} records for school ${schoolId}:`, + error + ); + } + + return flaggedCount; +} + +/** + * Generic function to flag expired records by entity type + */ +async function flagRecordsByEntityType( + schoolId: string, + entityType: string, + expiryDate: Date +): Promise { + // Check if records of this type already have deletion flags + const alreadyFlagged = await prisma.deletionFlag.findMany({ + where: { + schoolId, + entityType, + deletedAt: null, // Not yet hard-deleted + approvedAt: null, // Not yet approved + }, + }); + + const alreadyFlaggedIds = new Set(alreadyFlagged.map((f) => f.entityId)); + + // Create new flags for records not yet flagged + let flaggedCount = 0; + + // This is a placeholder; in production, you'd query each entity type + // Example for LearningProgressEntry: + if (entityType === "LearningProgressEntry") { + const expiredRecords = await prisma.learningProgressEntry + .findMany({ + where: { + createdAt: { lt: expiryDate }, + // Filter by school if the entity has schoolId + }, + select: { id: true }, + take: 1000, // Limit to prevent huge queries + }) + .catch(() => []); + + for (const record of expiredRecords) { + if (!alreadyFlaggedIds.has(record.id)) { + await prisma.deletionFlag.create({ + data: { + schoolId, + entityType, + entityId: record.id, + dataCategory: "LEARNING_PROGRESS", + reason: "retention period expired", + }, + }); + flaggedCount++; + } + } + } + + return flaggedCount; +} + +// Run the job if this script is executed directly +if (require.main === module) { + runRetentionJob() + .then((result) => { + console.log( + `[v0] Job result:`, + JSON.stringify(result, null, 2) + ); + process.exit(0); + }) + .catch((error) => { + console.error("[v0] Job failed:", error); + process.exit(1); + }); +} + +module.exports = { runRetentionJob, flagExpiredRecordsForSchool }; diff --git a/src/app/admin/compliance-overview/page.tsx b/src/app/admin/compliance-overview/page.tsx new file mode 100644 index 0000000..0c60dde --- /dev/null +++ b/src/app/admin/compliance-overview/page.tsx @@ -0,0 +1,204 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { CheckCircle, AlertCircle, FileText, Users, Clock } from "lucide-react"; + +interface ComplianceData { + status: any; + policiesConfigured: number; + policiesReviewed: number; + dpiaRecords: any[]; + consultations: any[]; +} + +export default function ComplianceDashboard() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchData = async () => { + try { + const response = await fetch("/api/v1/compliance/status"); + if (!response.ok) throw new Error("Failed to fetch compliance status"); + const result = await response.json(); + setData(result); + } catch (error) { + console.error("[v0] Error fetching compliance data:", error); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, []); + + if (loading) { + return ( +
+

Loading compliance status...

+
+ ); + } + + if (!data) { + return ( +
+ + +

Failed to load compliance dashboard

+
+
+
+ ); + } + + return ( +
+
+

Compliance Monitoring Dashboard

+

+ Track your school's data protection compliance status and document management +

+
+ + {/* Status Overview */} +
+ + +
+ {data.status?.onboardingCompleted ? ( + + ) : ( + + )} +
+

Onboarding Status

+

+ {data.status?.onboardingCompleted ? "Complete" : "Pending"} +

+
+
+
+
+ + + +
+ +
+

Data Retention Policies

+

+ {data.policiesConfigured} / {data.policiesReviewed} reviewed +

+
+
+
+
+ + + +
+ +
+

DPIA Records

+

{data.dpiaRecords.length}

+
+
+
+
+ + + +
+ +
+

Consultations

+

{data.consultations.length}

+
+
+
+
+
+ + {/* Data Retention Policies */} + + + Active Data Retention Policies + + Configured retention periods for each data category + + + +
+

{data.policiesConfigured} policies configured

+

Policies are centrally defined and reviewed by your DPO.

+
+
+
+ + {/* DPIA Records */} + {data.dpiaRecords.length > 0 && ( + + + DPIA Records + + Data Protection Impact Assessments for high-risk modules + + + +
+ {data.dpiaRecords.map((dpia: any) => ( +
+ + {dpia.moduleScope} + + + {dpia.approvedAt ? "✓ Approved" : "Pending review"} + +
+ ))} +
+
+
+ )} + + {/* Consultation Log */} + {data.consultations.length > 0 && ( + + + Consultation Log + + Staff and parent body consultations per module + + + +
+ {data.consultations.slice(0, 5).map((consult: any) => ( +
+
+

+ {consult.consultationType} +

+ {consult.moduleScope && ( +

{consult.moduleScope}

+ )} +
+ + {new Date(consult.acknowledgedAt).toLocaleDateString()} + +
+ ))} +
+
+
+ )} +
+ ); +} diff --git a/src/app/admin/compliance/layout.tsx b/src/app/admin/compliance/layout.tsx new file mode 100644 index 0000000..ba429f3 --- /dev/null +++ b/src/app/admin/compliance/layout.tsx @@ -0,0 +1,26 @@ +"use client"; + +import React from "react"; + +export default function ComplianceLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( +
+
+
+

+ Compliance & Data Protection Setup +

+

+ Configure your school's legal and data protection compliance + framework +

+
+ {children} +
+
+ ); +} diff --git a/src/app/admin/compliance/page.tsx b/src/app/admin/compliance/page.tsx new file mode 100644 index 0000000..dd57325 --- /dev/null +++ b/src/app/admin/compliance/page.tsx @@ -0,0 +1,83 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { ComplianceWizard } from "@/components/compliance-wizard"; +import { Card, CardContent } from "@/components/ui/card"; +import { Loader2 } from "lucide-react"; + +export default function CompliancePage() { + const router = useRouter(); + const [schoolId, setSchoolId] = useState(null); + const [isCompleted, setIsCompleted] = useState(false); + const [loading, setLoading] = useState(true); + + useEffect(() => { + // In a real app, fetch the current user's schoolId from session/auth + // For now, we'll get it from the URL or localStorage + const fetchSchoolId = async () => { + try { + const response = await fetch("/api/v1/user/current"); + if (!response.ok) throw new Error("Failed to fetch user"); + const user = await response.json(); + if (user.schoolId) { + setSchoolId(user.schoolId); + } + } catch (error) { + console.error("[v0] Error fetching school ID:", error); + } finally { + setLoading(false); + } + }; + + fetchSchoolId(); + }, []); + + if (loading) { + return ( +
+ +
+ ); + } + + if (!schoolId) { + return ( +
+ + +

Unable to load compliance setup. Please ensure you are logged in.

+
+
+
+ ); + } + + return ( +
+ {isCompleted ? ( + + +

+ ✓ Compliance onboarding completed successfully! Your school is now ready to use ExaMetra. +

+ +
+
+ ) : ( + { + console.log("[v0] Wizard completed:", data); + setIsCompleted(true); + }} + /> + )} +
+ ); +} diff --git a/src/app/api/v1/compliance/deletions/approve/route.ts b/src/app/api/v1/compliance/deletions/approve/route.ts new file mode 100644 index 0000000..de7f211 --- /dev/null +++ b/src/app/api/v1/compliance/deletions/approve/route.ts @@ -0,0 +1,117 @@ +import { NextRequest, NextResponse } from "next/server"; +import { PrismaClient } from "@prisma/client"; +import { canApproveDeletion } from "@/lib/auth"; +import { getSession } from "@/lib/auth"; + +const prisma = new PrismaClient(); + +/** + * POST /api/v1/compliance/deletions/approve + * DPO approves deletion of flagged records + * Body: { flagIds: string[] } + */ +export async function POST(req: NextRequest) { + try { + // Verify DPO role + const session = await getSession(); + if (!session?.userId) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const canApprove = await canApproveDeletion(session.userId); + if (!canApprove) { + return NextResponse.json( + { error: "Only DPO can approve deletions" }, + { status: 403 } + ); + } + + const body = await req.json(); + const { flagIds } = body; + + if (!flagIds || !Array.isArray(flagIds)) { + return NextResponse.json( + { error: "flagIds must be an array" }, + { status: 400 } + ); + } + + // Approve the flags + const approved = await prisma.deletionFlag.updateMany({ + where: { + id: { in: flagIds }, + }, + data: { + approvedByUserId: session.userId, + approvedAt: new Date(), + }, + }); + + // Log approval to audit + await prisma.auditLog.create({ + data: { + schoolId: session.user?.schoolId, + userId: session.userId, + action: "DELETION_FLAGS_APPROVED", + entityType: "DeletionFlag", + metadata: JSON.stringify({ + flagCount: approved.count, + timestamp: new Date().toISOString(), + }), + }, + }); + + return NextResponse.json({ + success: true, + approvedCount: approved.count, + message: `${approved.count} deletion(s) approved`, + }); + } catch (error) { + console.error("[v0] Error approving deletions:", error); + return NextResponse.json( + { error: "Failed to approve deletions" }, + { status: 500 } + ); + } +} + +/** + * GET /api/v1/compliance/deletions/pending + * Get pending deletion flags awaiting DPO approval + */ +export async function GET(req: NextRequest) { + try { + const session = await getSession(); + if (!session?.userId) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const schoolId = req.nextUrl.searchParams.get("schoolId"); + if (!schoolId) { + return NextResponse.json( + { error: "schoolId parameter required" }, + { status: 400 } + ); + } + + const pending = await prisma.deletionFlag.findMany({ + where: { + schoolId, + approvedAt: null, // Not yet approved + deletedAt: null, // Not yet hard-deleted + }, + orderBy: { flaggedAt: "desc" }, + }); + + return NextResponse.json({ + count: pending.length, + deletionFlags: pending, + }); + } catch (error) { + console.error("[v0] Error fetching pending deletions:", error); + return NextResponse.json( + { error: "Failed to fetch pending deletions" }, + { status: 500 } + ); + } +} diff --git a/src/app/api/v1/compliance/module-access/route.ts b/src/app/api/v1/compliance/module-access/route.ts new file mode 100644 index 0000000..76c3eb9 --- /dev/null +++ b/src/app/api/v1/compliance/module-access/route.ts @@ -0,0 +1,30 @@ +import { NextRequest, NextResponse } from "next/server"; +import { checkModuleAccess } from "@/lib/compliance"; + +/** + * Check if a module can be accessed based on compliance status + * GET /api/v1/compliance/module-access?schoolId=&module=AI_TUTOR + */ +export async function GET(req: NextRequest) { + try { + const schoolId = req.nextUrl.searchParams.get("schoolId"); + const module = req.nextUrl.searchParams.get("module"); + + if (!schoolId || !module) { + return NextResponse.json( + { error: "Missing schoolId or module parameter" }, + { status: 400 } + ); + } + + const result = await checkModuleAccess(schoolId, module); + + return NextResponse.json(result); + } catch (error) { + console.error("[v0] Error checking module access:", error); + return NextResponse.json( + { error: "Failed to check module access" }, + { status: 500 } + ); + } +} diff --git a/src/app/api/v1/compliance/onboarding/complete/route.ts b/src/app/api/v1/compliance/onboarding/complete/route.ts new file mode 100644 index 0000000..f8301ca --- /dev/null +++ b/src/app/api/v1/compliance/onboarding/complete/route.ts @@ -0,0 +1,134 @@ +import { NextRequest, NextResponse } from "next/server"; +import { PrismaClient } from "@prisma/client"; + +const prisma = new PrismaClient(); + +export async function POST(req: NextRequest) { + try { + const body = await req.json(); + const { schoolId, state } = body; + + if (!schoolId || !state) { + return NextResponse.json( + { error: "Missing required fields" }, + { status: 400 } + ); + } + + // Save DPIA records for required modules + for (const [moduleId, dpiaData] of Object.entries( + state.dpiaModules || {} + )) { + const dpia = dpiaData as any; + if (dpia.completed) { + // Check if record exists + const existing = await prisma.dpiaRecord.findUnique({ + where: { + schoolId_moduleScope: { + schoolId, + moduleScope: moduleId, + }, + }, + }); + + if (existing) { + await prisma.dpiaRecord.update({ + where: { id: existing.id }, + data: { + dataProcessingSummary: `DPIA for ${moduleId} completed`, + completedAt: new Date(), + documentUrl: dpia.documentUrl || `dpia-${moduleId}.pdf`, + }, + }); + } else { + await prisma.dpiaRecord.create({ + data: { + schoolId, + moduleScope: moduleId, + dataProcessingSummary: `DPIA for ${moduleId} completed`, + completedAt: new Date(), + documentUrl: dpia.documentUrl || `dpia-${moduleId}.pdf`, + }, + }); + } + } + } + + // Save consultation records + for (const [moduleId, consultations] of Object.entries( + state.moduleConsultations || {} + )) { + const consult = consultations as any; + const consultationTypes = []; + + if (consult.elternbeirat) { + consultationTypes.push("ELTERNBEIRAT"); + } + if (consult.schulkonferenz) { + consultationTypes.push("SCHULKONFERENZ"); + } + if (consult.personalrat) { + consultationTypes.push("PERSONALRAT"); + } + + for (const type of consultationTypes) { + await prisma.complianceConsultation.create({ + data: { + schoolId, + consultationType: type, + moduleScope: moduleId, + acknowledgedByUserId: "system", // In production, get from current user + acknowledgedAt: new Date(), + notes: `Consultation acknowledged for ${moduleId}`, + }, + }); + } + } + + // Mark onboarding as completed + await prisma.schoolComplianceStatus.update({ + where: { schoolId }, + data: { + onboardingCompleted: true, + moduleGates: JSON.stringify( + Object.keys(state.dpiaModules || {}).reduce( + (acc: any, moduleId) => { + const dpia = (state.dpiaModules || {})[moduleId] as any; + acc[moduleId] = { + isApprovedForUse: dpia.completed, + diaCompleted: dpia.completed, + }; + return acc; + }, + {} + ) + ), + }, + }); + + // Log to audit trail + await prisma.auditLog.create({ + data: { + schoolId, + action: "COMPLIANCE_ONBOARDING_COMPLETED", + entityType: "SchoolComplianceStatus", + entityId: schoolId, + metadata: JSON.stringify({ + timestamp: new Date().toISOString(), + modulesEnabled: Object.keys(state.dpiaModules || {}), + }), + }, + }); + + return NextResponse.json({ + success: true, + message: "Compliance onboarding completed successfully", + }); + } catch (error) { + console.error("[v0] Error completing compliance onboarding:", error); + return NextResponse.json( + { error: "Failed to complete compliance onboarding" }, + { status: 500 } + ); + } +} diff --git a/src/app/api/v1/compliance/onboarding/step1/route.ts b/src/app/api/v1/compliance/onboarding/step1/route.ts new file mode 100644 index 0000000..d5a8d12 --- /dev/null +++ b/src/app/api/v1/compliance/onboarding/step1/route.ts @@ -0,0 +1,79 @@ +import { NextRequest, NextResponse } from "next/server"; +import { PrismaClient } from "@prisma/client"; +import { + ensureComplianceStatus, + initializeRetentionPolicies, + getRetentionDefaults +} from "@/lib/compliance"; + +const prisma = new PrismaClient(); + +export async function POST(req: NextRequest) { + try { + const body = await req.json(); + const { + schoolId, + dpoName, + dpoEmail, + schultragerName, + schultragerEmail, + bundesland, + } = body; + + if (!schoolId || !dpoName || !dpoEmail || !schultragerName || !schultragerEmail || !bundesland) { + return NextResponse.json( + { error: "Missing required fields" }, + { status: 400 } + ); + } + + // Ensure compliance status exists + const complianceStatus = await ensureComplianceStatus(schoolId); + if (!complianceStatus) { + throw new Error("Failed to create compliance status"); + } + + // Update school compliance status with contact info + await prisma.schoolComplianceStatus.update({ + where: { schoolId }, + data: { + bundesland, + dpoDpoContactEmail: dpoEmail, + schultragerContactEmail: schultragerEmail, + }, + }); + + // Initialize retention policies for this Bundesland + const policyInitialized = await initializeRetentionPolicies(schoolId, bundesland); + if (!policyInitialized) { + throw new Error("Failed to initialize retention policies"); + } + + // Log to audit trail + await prisma.auditLog.create({ + data: { + schoolId, + action: "COMPLIANCE_SETUP_STEP_1", + entityType: "SchoolComplianceStatus", + entityId: schoolId, + metadata: JSON.stringify({ + dpoName, + bundesland, + timestamp: new Date().toISOString(), + }), + }, + }); + + return NextResponse.json({ + success: true, + message: "Step 1 saved successfully", + retentionDefaults: getRetentionDefaults(bundesland), + }); + } catch (error) { + console.error("[v0] Error in compliance step 1:", error); + return NextResponse.json( + { error: "Failed to save compliance setup" }, + { status: 500 } + ); + } +} diff --git a/src/app/api/v1/compliance/status/route.ts b/src/app/api/v1/compliance/status/route.ts new file mode 100644 index 0000000..46cc2bf --- /dev/null +++ b/src/app/api/v1/compliance/status/route.ts @@ -0,0 +1,26 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getComplianceStatus } from "@/lib/compliance"; + +export async function GET(req: NextRequest) { + try { + // In production, get schoolId from session/auth + const schoolId = req.nextUrl.searchParams.get("schoolId") || "default"; + + const status = await getComplianceStatus(schoolId); + + if (!status) { + return NextResponse.json( + { error: "School not found" }, + { status: 404 } + ); + } + + return NextResponse.json(status); + } catch (error) { + console.error("[v0] Error fetching compliance status:", error); + return NextResponse.json( + { error: "Failed to fetch compliance status" }, + { status: 500 } + ); + } +} diff --git a/src/app/api/v1/gdpr/erasure/route.ts b/src/app/api/v1/gdpr/erasure/route.ts new file mode 100644 index 0000000..4ae1e85 --- /dev/null +++ b/src/app/api/v1/gdpr/erasure/route.ts @@ -0,0 +1,167 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getSession } from "@/lib/auth"; +import { logComplianceAudit } from "@/lib/audit"; +import { PrismaClient } from "@prisma/client"; + +const prisma = new PrismaClient(); + +/** + * POST /api/v1/gdpr/erasure + * Right to Erasure / Right to be Forgotten (Art. 17 GDPR) + * User can request deletion of their data + * Note: Some data (final grades) may be retained due to legal obligations + */ +export async function POST(req: NextRequest) { + try { + const session = await getSession(); + if (!session?.userId) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { userId: requestedUserId, reason } = await req.json(); + + // Users can only request erasure of their own data + if (session.userId !== requestedUserId) { + return NextResponse.json( + { error: "You can only request erasure of your own data" }, + { status: 403 } + ); + } + + const user = await prisma.user.findUnique({ + where: { id: requestedUserId }, + }); + + if (!user) { + return NextResponse.json({ error: "User not found" }, { status: 404 }); + } + + // Determine what can be deleted vs. retained + const retained = { + FINAL_REPORT: "Permanent legal record (Schulgesetz requirement)", + DISCIPLINARY_RECORDS_WITH_CONSEQUENCES: + "Retained per applicable retention policy", + }; + + const erasedCategories = [ + "MESSAGES", + "NOTEBOOKS", + "LEARNING_PROGRESS_ENTRIES", + "AI_TUTOR_LOGS", + ]; + + // Delete erasable data + const deletionResults = { + messagesDeleted: 0, + notebooksDeleted: 0, + learningProgressDeleted: 0, + auditLogsDeleted: 0, + }; + + // Delete messages + const messageResult = await prisma.parentMessage.deleteMany({ + where: { OR: [{ senderId: requestedUserId }, { recipientId: requestedUserId }] }, + }); + deletionResults.messagesDeleted = messageResult.count; + + // Delete notebooks (student-owned) + if (user.student?.id) { + const notebookResult = await prisma.notebook.deleteMany({ + where: { studentId: user.student.id }, + }); + deletionResults.notebooksDeleted = notebookResult.count; + } + + // Delete learning progress entries + const lpResult = await prisma.learningProgressEntry.deleteMany({ + where: { userId: requestedUserId }, + }); + deletionResults.learningProgressDeleted = lpResult.count; + + // Note: Audit logs are NOT deleted for audit trail integrity + + // Log the erasure request + await logComplianceAudit({ + userId: requestedUserId, + schoolId: user.schoolId || undefined, + action: "ERASURE_REQUEST_APPROVED", + entityType: "User", + entityId: requestedUserId, + dataSubject: requestedUserId, + metadata: { + reason, + erasedCategories, + deletionResults, + retainedCategories: retained, + timestamp: new Date().toISOString(), + }, + }); + + return NextResponse.json({ + success: true, + message: "Your erasure request has been processed", + deleted: deletionResults, + retained: retained, + note: "Final grades and disciplinary records with consequences are retained per legal requirements. Contact your DPO for details.", + }); + } catch (error) { + console.error("[v0] Error processing erasure request:", error); + return NextResponse.json( + { error: "Failed to process erasure request" }, + { status: 500 } + ); + } +} + +/** + * GET /api/v1/gdpr/erasure/status + * Check status of erasure requests for a user + */ +export async function GET(req: NextRequest) { + try { + const session = await getSession(); + if (!session?.userId) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const userId = req.nextUrl.searchParams.get("userId") || session.userId; + + // Users can only check their own status + if (session.userId !== userId) { + return NextResponse.json( + { error: "You can only check your own erasure status" }, + { status: 403 } + ); + } + + // Get erasure-related audit logs + const erasureLogs = await prisma.auditLog.findMany({ + where: { + dataSubject: userId, + action: { contains: "ERASURE" }, + }, + orderBy: { timestamp: "desc" }, + take: 10, + select: { + id: true, + action: true, + timestamp: true, + metadata: true, + }, + }); + + return NextResponse.json({ + userId, + recentRequests: erasureLogs.map((log) => ({ + ...log, + metadata: log.metadata ? JSON.parse(log.metadata) : null, + })), + }); + } catch (error) { + console.error("[v0] Error checking erasure status:", error); + return NextResponse.json( + { error: "Failed to check erasure status" }, + { status: 500 } + ); + } +} diff --git a/src/app/api/v1/gdpr/export/route.ts b/src/app/api/v1/gdpr/export/route.ts new file mode 100644 index 0000000..0565144 --- /dev/null +++ b/src/app/api/v1/gdpr/export/route.ts @@ -0,0 +1,85 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getSession } from "@/lib/auth"; +import { getAuditTrailForDataSubject } from "@/lib/audit"; +import { PrismaClient } from "@prisma/client"; + +const prisma = new PrismaClient(); + +/** + * POST /api/v1/gdpr/export + * Right of Access (Art. 15 GDPR) + * User can request export of all their personal data + */ +export async function POST(req: NextRequest) { + try { + const session = await getSession(); + if (!session?.userId) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { userId: requestedUserId } = await req.json(); + + // Users can only export their own data; admins can export any user's data + const isAdmin = session.user?.role === "SCHOOL_ADMIN" || session.user?.role === "SUPER_ADMIN"; + if (!isAdmin && session.userId !== requestedUserId) { + return NextResponse.json( + { error: "You can only export your own data" }, + { status: 403 } + ); + } + + // Fetch all personal data + const user = await prisma.user.findUnique({ + where: { id: requestedUserId }, + include: { + student: true, + learningProgressEntries: true, + assessments: true, + notebooks: true, + parentMessagesSent: true, + auditLogs: { take: 100 }, + }, + }); + + if (!user) { + return NextResponse.json({ error: "User not found" }, { status: 404 }); + } + + // Get audit trail for this user + const auditTrail = await getAuditTrailForDataSubject(user.schoolId!, requestedUserId); + + // Compile export data + const exportData = { + user: { + id: user.id, + email: user.email, + firstName: user.firstName, + lastName: user.lastName, + role: user.role, + createdAt: user.createdAt, + }, + student: user.student || null, + learningProgress: user.learningProgressEntries || [], + assessments: user.assessments || [], + notebooks: user.notebooks || [], + messages: user.parentMessagesSent || [], + auditTrail: auditTrail, + exportedAt: new Date().toISOString(), + retentionNote: + "Some data (e.g., final grades) may be retained longer due to legal requirements. See your school's privacy notice.", + }; + + return NextResponse.json({ + success: true, + data: exportData, + format: "json", + message: "Your personal data export is ready", + }); + } catch (error) { + console.error("[v0] Error exporting user data:", error); + return NextResponse.json( + { error: "Failed to export data" }, + { status: 500 } + ); + } +} diff --git a/src/components/compliance-wizard.tsx b/src/components/compliance-wizard.tsx new file mode 100644 index 0000000..1bc9f43 --- /dev/null +++ b/src/components/compliance-wizard.tsx @@ -0,0 +1,536 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Checkbox } from "@/components/ui/checkbox"; +import { AlertCircle, CheckCircle, ChevronRight, ChevronLeft } from "lucide-react"; + +const BUNDESLANDS = [ + "BERLIN", + "BREMEN", + "HAMBURG", + "HESSE", + "LOWER_SAXONY", + "MECKLENBURG_VORPOMMERN", + "NORTH_RHINE_WESTPHALIA", + "RHINELAND_PALATINATE", + "SAARLAND", + "SAXONY", + "SAXONY_ANHALT", + "SCHLESWIG_HOLSTEIN", + "THURINGIA", + "BADEN_WUERTTEMBERG", + "BAVARIA", + "BRANDENBURG", +]; + +const MODULES = [ + { id: "AI_TUTOR", label: "AI Tutor", requiresDpia: true }, + { id: "EXAM_MODE", label: "Exam Mode", requiresDpia: true }, + { id: "WELLNESS_CHECKS", label: "Wellness Checks", requiresDpia: false }, + { id: "SIGNAGE_EMERGENCY", label: "Signage & Emergency", requiresDpia: false }, + { id: "MESSAGING", label: "Messaging", requiresDpia: false }, + { id: "LEARNING_ANALYTICS", label: "Learning Analytics", requiresDpia: false }, +]; + +interface WizardState { + step: number; + schoolName?: string; + dpoName?: string; + dpoEmail?: string; + schultragerName?: string; + schultragerEmail?: string; + bundesland?: string; + consultationTypes: string[]; + dpiaModules: Record; + moduleConsultations: Record< + string, + { + elternbeirat: boolean; + personalrat: boolean; + schulkonferenz: boolean; + } + >; +} + +interface ComplianceWizardProps { + schoolId: string; + onComplete?: (data: WizardState) => void; +} + +export function ComplianceWizard({ schoolId, onComplete }: ComplianceWizardProps) { + const [state, setState] = useState({ + step: 1, + consultationTypes: [], + dpiaModules: MODULES.reduce( + (acc, mod) => ({ + ...acc, + [mod.id]: { completed: false }, + }), + {} + ), + moduleConsultations: MODULES.reduce( + (acc, mod) => ({ + ...acc, + [mod.id]: { + elternbeirat: false, + personalrat: mod.requiresDpia, + schulkonferenz: false, + }, + }), + {} + ), + }); + + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + const isStep1Valid = + state.schoolName && + state.dpoName && + state.dpoEmail && + state.schultragerName && + state.schultragerEmail && + state.bundesland; + + const isStep2Valid = true; // Retention policies auto-seeded + + const isStep3Valid = Object.keys(state.moduleConsultations).length > 0; + + const isStep4Valid = + MODULES.filter((m) => m.requiresDpia).every( + (m) => state.dpiaModules[m.id]?.completed + ); + + const handleStep1Submit = async () => { + if (!isStep1Valid) { + setError("Please fill in all fields"); + return; + } + + try { + setLoading(true); + const response = await fetch("/api/v1/compliance/onboarding/step1", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + schoolId, + dpoName: state.dpoName, + dpoEmail: state.dpoEmail, + schultragerName: state.schultragerName, + schultragerEmail: state.schultragerEmail, + bundesland: state.bundesland, + }), + }); + + if (!response.ok) throw new Error("Failed to save step 1"); + setState((prev) => ({ ...prev, step: 2 })); + } catch (err) { + setError(err instanceof Error ? err.message : "Unknown error"); + } finally { + setLoading(false); + } + }; + + const handleStep4Submit = async () => { + if (!isStep4Valid) { + setError("Please complete all required DPIAs"); + return; + } + + try { + setLoading(true); + const response = await fetch("/api/v1/compliance/onboarding/complete", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + schoolId, + state, + }), + }); + + if (!response.ok) throw new Error("Failed to complete onboarding"); + onComplete?.(state); + } catch (err) { + setError(err instanceof Error ? err.message : "Unknown error"); + } finally { + setLoading(false); + } + }; + + return ( +
+ {/* Progress Indicator */} +
+ {[1, 2, 3, 4, 5].map((step) => ( +
+
= step + ? "bg-emerald-600 text-white" + : "bg-slate-200 text-slate-600" + }`} + > + {state.step > step ? ( + + ) : ( + step + )} +
+ {step < 5 && ( +
step ? "bg-emerald-600" : "bg-slate-200" + }`} + /> + )} +
+ ))} +
+ + {error && ( + + + +

{error}

+
+
+ )} + + {/* Step 1: School & Contact Info */} + {state.step === 1 && ( + + + School & Contact Information + + Provide your school's basic information and designated contacts + + + +
+ + + setState((prev) => ({ + ...prev, + schoolName: e.target.value, + })) + } + placeholder="e.g., Gymnasium Berlin" + /> +
+ +
+
+ + + setState((prev) => ({ ...prev, dpoName: e.target.value })) + } + placeholder="Full name" + /> +
+
+ + + setState((prev) => ({ ...prev, dpoEmail: e.target.value })) + } + placeholder="dpo@school.de" + /> +
+
+ +
+
+ + + setState((prev) => ({ + ...prev, + schultragerName: e.target.value, + })) + } + placeholder="e.g., City of Berlin" + /> +
+
+ + + setState((prev) => ({ + ...prev, + schultragerEmail: e.target.value, + })) + } + placeholder="admin@schultraeger.de" + /> +
+
+ +
+ + +
+
+
+ +
+
+ )} + + {/* Step 2: Retention Periods (coming in next iteration) */} + {state.step === 2 && ( + + + Data Retention Periods + + Configure retention periods for each data category (state-specific defaults provided) + + + +

+ Retention periods have been pre-seeded based on your state's regulations. + Your DPO should review and confirm these settings. +

+
+
+ + +
+
+ )} + + {/* Step 3: Consultations */} + {state.step === 3 && ( + + + Consultation Checklist + + Log consultations with staff and parent bodies per module + + + +
+ {MODULES.map((module) => ( +
+

{module.label}

+
+ + {!module.requiresDpia && ( + + )} + {module.requiresDpia && ( + + )} +
+
+ ))} +
+
+
+ + +
+
+ )} + + {/* Step 4: DPIA Gating */} + {state.step === 4 && ( + + + DPIA (Data Protection Impact Assessment) + + Upload completed DPIAs for high-risk modules before enabling them + + + + {MODULES.filter((m) => m.requiresDpia).map((module) => ( +
+

{module.label} DPIA

+

+ Upload your completed DPIA document (PDF) +

+ { + if (e.target.files?.[0]) { + setState((prev) => ({ + ...prev, + dpiaModules: { + ...prev.dpiaModules, + [module.id]: { + completed: true, + documentUrl: e.target.files![0].name, + }, + }, + })); + } + }} + className="block w-full text-sm text-slate-500 file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-emerald-50 file:text-emerald-700 hover:file:bg-emerald-100" + /> + {state.dpiaModules[module.id]?.documentUrl && ( +

+ ✓ {state.dpiaModules[module.id].documentUrl} +

+ )} +
+ ))} +
+
+ + +
+
+ )} + + {/* Step 5: Summary & Complete */} + {state.step === 5 && ( + + + Compliance Setup Complete + + Review your configuration before finalizing + + + +
+

+ ✓ All compliance requirements have been configured. Your school is now ready for + production use. +

+
+
+

DPO: {state.dpoName} ({state.dpoEmail})

+

State: {state.bundesland}

+

Modules: {MODULES.length} configured

+
+
+
+ + +
+
+ )} +
+ ); +} diff --git a/src/lib/audit.ts b/src/lib/audit.ts old mode 100755 new mode 100644 index c53e97e..22e10fb --- a/src/lib/audit.ts +++ b/src/lib/audit.ts @@ -138,3 +138,137 @@ export function withAuditLog( return response; }; } + +// ─── Module L: Compliance Audit Logging ──────────────────────────────── + +export interface ComplianceAuditParams extends AuditLogParams { + dataSubject?: string; // Student ID if applicable (for GDPR export scope) +} + +/** + * Log compliance-specific actions (DPO approvals, policy changes, etc.) + * Includes dataSubject field for tracking affected individuals + */ +export async function logComplianceAudit(params: ComplianceAuditParams): Promise { + try { + let userId = params.userId; + if (!userId) { + const session = await getSession(); + userId = session?.user?.id ?? undefined; + } + + await db.auditLog.create({ + data: { + userId: userId ?? null, + schoolId: params.schoolId ?? null, + action: params.action, + entityType: params.entityType, + entityId: params.entityId ?? null, + changes: params.changes ? JSON.stringify(params.changes) : null, + ipAddress: params.ipAddress ?? null, + userAgent: params.userAgent ?? null, + metadata: params.metadata ? JSON.stringify(params.metadata) : null, + dataSubject: params.dataSubject ?? null, // Track affected individual + }, + }); + } catch (error) { + console.error("[v0] Compliance audit log error:", error); + } +} + +/** + * Log when a student's data is accessed (for audit trail) + */ +export async function logDataAccess( + userId: string, + studentId: string, + action: string, + schoolId?: string, + metadata?: Record +): Promise { + await logComplianceAudit({ + userId, + schoolId, + action: `DATA_ACCESS_${action}`, // e.g., DATA_ACCESS_VIEW_GRADES + entityType: "Student", + entityId: studentId, + dataSubject: studentId, + metadata: { + ...metadata, + timestamp: new Date().toISOString(), + }, + }); +} + +/** + * Log DPO approval actions + */ +export async function logDPOApproval( + dpoId: string, + schoolId: string, + approvalType: string, + metadata?: Record +): Promise { + await logComplianceAudit({ + userId: dpoId, + schoolId, + action: `DPO_APPROVAL_${approvalType}`, + entityType: "ComplianceStatus", + metadata, + }); +} + +/** + * Log policy updates + */ +export async function logPolicyUpdate( + userId: string, + schoolId: string, + policyType: string, + changes: AuditLogParams['changes'], +): Promise { + await logComplianceAudit({ + userId, + schoolId, + action: "POLICY_UPDATE", + entityType: policyType, // e.g., "DataRetentionPolicy" + changes, + }); +} + +/** + * Query audit logs for a specific data subject (student) + * Used for GDPR right-of-access requests + */ +export async function getAuditTrailForDataSubject( + schoolId: string, + studentId: string, + limit: number = 100 +) { + try { + const logs = await db.auditLog.findMany({ + where: { + schoolId, + dataSubject: studentId, + }, + orderBy: { timestamp: "desc" }, + take: limit, + select: { + id: true, + action: true, + entityType: true, + userId: true, + timestamp: true, + metadata: true, + }, + }); + + return logs.map((log) => ({ + ...log, + metadata: log.metadata ? JSON.parse(log.metadata) : null, + })); + } catch (error) { + console.error("[v0] Error querying audit trail:", error); + return []; + } +} diff --git a/src/lib/auth.ts b/src/lib/auth.ts old mode 100755 new mode 100644 index 24251d9..352a718 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -87,3 +87,93 @@ export async function clearSession(): Promise { const cookieStore = await cookies(); cookieStore.delete(SESSION_COOKIE_NAME); } + +// ─── Module L: Access Control & DPO Role ─────────────────────────────── + +/** + * Check if user has DPO (Data Protection Officer) role + */ +export async function isDPORole(userId: string): Promise { + const cookieStore = await cookies(); + const sessionUserId = cookieStore.get(SESSION_COOKIE_NAME)?.value; + + if (!sessionUserId || sessionUserId !== userId) { + return false; + } + + const user = await db.user.findUnique({ + where: { id: userId }, + select: { role: true }, + }); + + return user?.role === 'DPO'; +} + +/** + * Check if user has admin role (SCHOOL_ADMIN or higher) + */ +export async function isAdminRole(userId: string): Promise { + const user = await db.user.findUnique({ + where: { id: userId }, + select: { role: true }, + }); + + return user?.role === 'SCHOOL_ADMIN' || user?.role === 'SUPER_ADMIN'; +} + +/** + * Check if user can access compliance features (DPO or Admin) + */ +export async function canAccessCompliance(userId: string): Promise { + const isDPO = await isDPORole(userId); + const isAdmin = await isAdminRole(userId); + return isDPO || isAdmin; +} + +/** + * Verify user can perform compliance actions + * Returns { allowed: boolean, reason?: string } + */ +export async function verifyComplianceAccess( + userId: string +): Promise<{ allowed: boolean; reason?: string }> { + const user = await db.user.findUnique({ + where: { id: userId, deletedAt: null }, + select: { role: true, schoolId: true }, + }); + + if (!user) { + return { allowed: false, reason: 'User not found' }; + } + + if (user.role !== 'DPO' && user.role !== 'SCHOOL_ADMIN' && user.role !== 'SUPER_ADMIN') { + return { + allowed: false, + reason: 'Only DPO and administrators can access compliance features', + }; + } + + if (!user.schoolId) { + return { allowed: false, reason: 'User not associated with a school' }; + } + + return { allowed: true }; +} + +/** + * Check if user can view audit logs + * Only DPO and admins can view full audit logs + */ +export async function canViewAuditLogs(userId: string): Promise { + const canAccess = await canAccessCompliance(userId); + return canAccess; +} + +/** + * Check if user can approve deletion flags + * Only DPO can approve data deletion + */ +export async function canApproveDeletion(userId: string): Promise { + const isDPO = await isDPORole(userId); + return isDPO; +} diff --git a/src/lib/compliance.ts b/src/lib/compliance.ts new file mode 100644 index 0000000..a419991 --- /dev/null +++ b/src/lib/compliance.ts @@ -0,0 +1,304 @@ +/** + * Module L: Legal & Data Protection Compliance Utilities + * Provides compliance checks, DPIA gating, feature access control, and data retention helpers + */ + +import { PrismaClient } from "@prisma/client"; + +const prisma = new PrismaClient(); + +// ─── Data Categories & Retention Mapping ────────────────────────────────── + +export const DATA_CATEGORIES = { + LEARNING_PROGRESS: "LEARNING_PROGRESS", + ASSESSMENT_RESULT: "ASSESSMENT_RESULT", + FINAL_REPORT: "FINAL_REPORT", + DISCIPLINARY_RECORD: "DISCIPLINARY_RECORD", + EXAM_INCIDENT: "EXAM_INCIDENT", + MESSAGE: "MESSAGE", + AI_TUTOR_LOG: "AI_TUTOR_LOG", + NOTEBOOK_PAGE: "NOTEBOOK_PAGE", + SIGNAGE_LOG: "SIGNAGE_LOG", + TEACHER_AUDIT_LOG: "TEACHER_AUDIT_LOG", +} as const; + +export const MODULE_DPIA_REQUIRED = ["AI_TUTOR", "EXAM_MODE"] as const; + +export const BUNDESLAND_RETENTION_DEFAULTS: Record< + string, + Record +> = { + BERLIN: { + LEARNING_PROGRESS: 12, + ASSESSMENT_RESULT: 12, + FINAL_REPORT: 30, + DISCIPLINARY_RECORD: 5, + EXAM_INCIDENT: 6, + MESSAGE: 12, + AI_TUTOR_LOG: 6, + NOTEBOOK_PAGE: 12, + SIGNAGE_LOG: 3, + TEACHER_AUDIT_LOG: 12, + }, + BAYERN: { + LEARNING_PROGRESS: 12, + ASSESSMENT_RESULT: 12, + FINAL_REPORT: 35, + DISCIPLINARY_RECORD: 5, + EXAM_INCIDENT: 6, + MESSAGE: 12, + AI_TUTOR_LOG: 6, + NOTEBOOK_PAGE: 12, + SIGNAGE_LOG: 3, + TEACHER_AUDIT_LOG: 12, + }, + // Add remaining 14 states with their specific retention rules + // For now, using Berlin as fallback + BADEN_WUERTTEMBERG: { + LEARNING_PROGRESS: 12, + ASSESSMENT_RESULT: 12, + FINAL_REPORT: 30, + DISCIPLINARY_RECORD: 5, + EXAM_INCIDENT: 6, + MESSAGE: 12, + AI_TUTOR_LOG: 6, + NOTEBOOK_PAGE: 12, + SIGNAGE_LOG: 3, + TEACHER_AUDIT_LOG: 12, + }, +}; + +// ─── Compliance Status Checks ────────────────────────────────────────────── + +export interface ComplianceCheckResult { + allowed: boolean; + reason?: string; + missingItems?: string[]; +} + +/** + * Check if a module can be accessed by a school + * Verifies: onboarding completion, DPIA presence (if required), and other gates + */ +export async function checkModuleAccess( + schoolId: string, + module: string +): Promise { + try { + const complianceStatus = await prisma.schoolComplianceStatus.findUnique({ + where: { schoolId }, + }); + + if (!complianceStatus?.onboardingCompleted) { + return { + allowed: false, + reason: "Compliance onboarding incomplete. Please complete the onboarding wizard first.", + }; + } + + // Check if module requires DPIA + if (MODULE_DPIA_REQUIRED.includes(module as any)) { + const dpiaRecord = await prisma.dpiaRecord.findUnique({ + where: { + schoolId_moduleScope: { + schoolId, + moduleScope: module, + }, + }, + }); + + if (!dpiaRecord) { + return { + allowed: false, + reason: `DPIA documentation not found for ${module}. Please upload DPIA before enabling this module.`, + }; + } + } + + return { allowed: true }; + } catch (error) { + console.error("[v0] Error checking module access:", error); + return { + allowed: false, + reason: "Error checking compliance status", + }; + } +} + +/** + * Get retention period for a data category + * Returns the months after which data should be flagged for deletion + */ +export async function getRetentionPeriod( + schoolId: string, + dataCategory: string +): Promise { + try { + const policy = await prisma.dataRetentionPolicy.findFirst({ + where: { + schoolId, + dataCategory, + }, + }); + + return policy?.retentionPeriodMonths || null; + } catch (error) { + console.error("[v0] Error getting retention period:", error); + return null; + } +} + +/** + * Flag data entities for deletion when retention period expires + * Called by daily retention job + */ +export async function flagExpiredRecords(schoolId: string): Promise { + try { + const policies = await prisma.dataRetentionPolicy.findMany({ + where: { schoolId }, + }); + + let flaggedCount = 0; + + for (const policy of policies) { + if (!policy.retentionPeriodMonths) continue; + + const expiryDate = new Date(); + expiryDate.setMonth(expiryDate.getMonth() - policy.retentionPeriodMonths); + + // This is a placeholder for entity-specific logic + // In production, you'd loop over each entity type and flag accordingly + flaggedCount++; + } + + return flaggedCount; + } catch (error) { + console.error("[v0] Error flagging expired records:", error); + return 0; + } +} + +/** + * Check if user has DPO role + */ +export async function isDPO(userId: string): Promise { + try { + const user = await prisma.user.findUnique({ + where: { id: userId }, + }); + + return user?.role === "DPO"; + } catch (error) { + console.error("[v0] Error checking DPO status:", error); + return false; + } +} + +/** + * Get school compliance status summary + */ +export async function getComplianceStatus(schoolId: string) { + try { + const status = await prisma.schoolComplianceStatus.findUnique({ + where: { schoolId }, + }); + + const policies = await prisma.dataRetentionPolicy.findMany({ + where: { schoolId }, + }); + + const dpias = await prisma.dpiaRecord.findMany({ + where: { schoolId }, + }); + + const consultations = await prisma.complianceConsultation.findMany({ + where: { schoolId }, + }); + + return { + status, + policiesConfigured: policies.length, + policiesReviewed: policies.filter((p) => p.reviewedAt).length, + dpiaRecords: dpias, + consultations, + }; + } catch (error) { + console.error("[v0] Error getting compliance status:", error); + return null; + } +} + +/** + * Ensure school compliance status record exists + */ +export async function ensureComplianceStatus(schoolId: string) { + try { + const existing = await prisma.schoolComplianceStatus.findUnique({ + where: { schoolId }, + }); + + if (!existing) { + return await prisma.schoolComplianceStatus.create({ + data: { + schoolId, + onboardingCompleted: false, + }, + }); + } + + return existing; + } catch (error) { + console.error("[v0] Error ensuring compliance status:", error); + return null; + } +} + +/** + * Initialize retention policies for a school based on Bundesland + */ +export async function initializeRetentionPolicies( + schoolId: string, + bundesland: string +) { + try { + const defaults = + BUNDESLAND_RETENTION_DEFAULTS[bundesland] || + BUNDESLAND_RETENTION_DEFAULTS.BERLIN; + + for (const [category, months] of Object.entries(defaults)) { + // Check if policy already exists + const existing = await prisma.dataRetentionPolicy.findFirst({ + where: { + schoolId, + dataCategory: category, + }, + }); + + if (!existing) { + await prisma.dataRetentionPolicy.create({ + data: { + schoolId, + dataCategory: category, + retentionPeriodMonths: months, + isDefault: true, + }, + }); + } + } + + return true; + } catch (error) { + console.error("[v0] Error initializing retention policies:", error); + return false; + } +} + +/** + * Get Bundesland retention defaults + */ +export function getRetentionDefaults(bundesland: string) { + return ( + BUNDESLAND_RETENTION_DEFAULTS[bundesland] || + BUNDESLAND_RETENTION_DEFAULTS.BERLIN + ); +}