Zum Hauptinhalt springen

Digitales Klassenbuch

Erfassung von Anwesenheiten (Attendance), Unterrichtsinhalten/Hausaufgaben (ClassbookEntry), digitalen Entschuldigungen (AbsenceNote) sowie der mandantenweiten Konfiguration (ClassbookSettings). Abwesenheitsgründe werden über frei konfigurierbare AbsenceType-Datensätze typisiert.

Datenmodell​

Attendance​

model Attendance {
id String @id @default(uuid())
tenant_id String
timetable_id String
student_id String
date DateTime @db.Date
status AttendanceStatus @default(present)
absence_type_id String?
minutes_late Int?
minutes_early Int?
notes String? @default("")
source AttendanceSource @default(teacher)
propagated Boolean @default(false)
propagated_from String? // Soft-Referenz auf Attendance.id (kein FK)
recorded_by_id String
is_correction Boolean @default(false)
corrects_id String?
// ...
@@unique([timetable_id, student_id, date])
}
  • Ein Eintrag identifiziert sich eindeutig über (timetable_id, student_id, date). Normale Erfassung läuft daher als upsert über diesen Unique-Constraint.
  • propagated_from ist bewusst kein Fremdschlüssel — die Ursprungs-Quelle (Klassenlehrkraft-Eintrag) kann korrigiert oder gelöscht werden, ohne den propagierten Eintrag zu brechen.
  • Korrekturen werden nicht in-place überschrieben: bei is_correction=true wird ein neuer Datensatz mit corrects_id angelegt, der alte bleibt erhalten (Self-Relation AttendanceCorrection).

AttendanceStatus:

WertBedeutung
not_checkednoch nicht geprüft (Default-Anzeige für Schüler ohne Eintrag)
presentanwesend
absentabwesend (noch nicht kategorisiert)
absent_excusedentschuldigt
absent_unexcusedunentschuldigt
lateverspätet
left_earlyfrühzeitig gegangen
remoteOnline-/Hybridunterricht

AttendanceSource:

WertBedeutung
teacherFachlehrkraft hat selbst eingetragen
class_teacherKlassenlehrkraft hat eingetragen (ggf. propagiert)
parent_noteEltern-Meldung → automatisch vorbesetzt
adminSekretariat / Admin-Korrektur

AbsenceType​

Frei konfigurierbare Abwesenheitsgründe pro Mandant. @@unique([tenant_id, name]). Felder u. a. short_name, color (Default #888780), counts_as_absent, is_excused, is_system, sort_order. Wird über Attendance.absence_type_id (FK mit onDelete: SetNull) referenziert.

AbsenceNote​

model AbsenceNote {
id String @id @default(uuid())
tenant_id String
student_id String
attendance_id String? @unique
date_from DateTime @db.Date
date_to DateTime @db.Date
reason String?
status AbsenceNoteStatus @default(pending)
submitted_by_portal_user_id String?
submitted_by_user_id String?
reviewed_by_id String?
reviewed_at DateTime?
review_notes String?
file_id String?
// ...
}
  • status-Enum AbsenceNoteStatus: pending → accepted | rejected.
  • Optionale Verknüpfung zu genau einem Attendance-Eintrag (@unique). Bei Annahme wird dieser auf absent_excused gesetzt.
  • submitted_by_portal_user_id referenziert einen PortalUser (Eltern-Portal), submitted_by_user_id einen internen User. file_id verweist optional auf ein hochgeladenes Dokument (File, onDelete: SetNull).

ClassbookEntry​

model ClassbookEntry {
id String @id @default(uuid())
tenant_id String
timetable_id String
date DateTime @db.Date
lesson_content String?
homework String?
notes String? @default("")
created_by_id String
updated_by_id String?
// ...
@@unique([timetable_id, date])
}

Lehrstoff-/Hausaufgabeneintrag je Stunde+Datum. Eindeutig über (timetable_id, date), daher upsert. created_by_id/updated_by_id dokumentieren Autorenschaft.

ClassbookSettings​

Ein Datensatz pro Mandant (tenant_id @unique). Steuert das Verhalten des Klassenbuchs:

FeldDefaultZweck
mode (ClassbookMode)per_teacherper_teacher (jede LK trägt selbst ein) oder class_teacher (Klassenlehrer-Prinzip)
allow_parent_notestrueErlaubt Eltern-Entschuldigungen über das Portal
require_absence_typefalseErzwingt Auswahl eines AbsenceType bei Abwesenheit
propagate_class_teachertrueÜbertragung der Ganztagesabwesenheit auf Folgestunden
lock_after_daysnullSperrfrist für Bearbeitung (NULL = nie gesperrt)
show_lesson_contenttrueLehrstoff-Spalte anzeigen
show_homeworktrueHausaufgaben-Spalte anzeigen

Server Actions​

Anwesenheiten — lib/actions/klassenbuch/attendance.ts​

ActionZweck
recordAttendanceEinzelnen Eintrag erfassen. Normalfall = upsert über (timetable_id, student_id, date); bei is_correction=true neuer Datensatz mit corrects_id.
recordBulkAttendanceMehrere Schüler einer Stunde in einer prisma.$transaction per upsert.
propagateClassTeacherAttendanceKlassenlehrer-Modus: überträgt Ganztagesabwesenheit auf alle aktiven Timetable-Einträge der Klasse am Wochentag.
getAttendanceForLessonLiefert für eine Stunde alle Schüler; fehlende Einträge werden als virtueller not_checked-Eintrag (id: virtual_<studentId>, nicht persistiert) ergänzt.
getAttendanceSummaryForStudentFehlzeiten-Übersicht eines Schülers über die Datumsgrenzen des Schuljahres; zählt nur is_correction=false.

Berechtigung: recordAttendance und recordBulkAttendance prüfen vor dem Schreiben canRecordAttendance (siehe Berechtigungslogik). propagateClassTeacherAttendance prüft aktuell nur Session/Tenant, keinen canRecordAttendance-Aufruf.

Propagierungs-Semantik: In propagateClassTeacherAttendance werden bestehende Fachlehrkraft-Einträge (propagated=false) nicht überschrieben — pro Stunde wird zunächst geprüft, ob ein solcher Eintrag existiert, und ggf. übersprungen. Andernfalls upsert mit source=class_teacher und propagated=true.

Entschuldigungen — lib/actions/klassenbuch/absence-note.ts​

ActionZweck
reviewAbsenceNoteKlassenlehrkraft nimmt eine pending-Entschuldigung an oder lehnt sie ab. Bei accepted und vorhandener attendance_id wird der Attendance-Status defensiv per updateMany mit Tenant-Scope auf absent_excused gesetzt.
getPendingAbsenceNotesLiefert alle pending-Entschuldigungen einer Klasse (gefiltert über student.school_class_id).

reviewAbsenceNote lehnt eine erneute Bearbeitung ab, wenn status !== 'pending'.

Lehrstoff/Hausaufgaben — lib/actions/klassenbuch/classbook-entry.ts​

ActionZweck
saveClassbookEntryupsert über (timetable_id, date); setzt beim Update updated_by_id.
getClassbookEntriesForWeekLädt Einträge einer Klasse für Mo–Fr (week_start + 4 Tage).

Konfiguration — lib/actions/klassenbuch/settings.ts​

ActionZweck
getClassbookSettingsLädt die Settings; legt bei Fehlen einen Default-Datensatz an (alle Schema-Defaults).
updateClassbookSettingsupsert der Settings. Nur ADMIN — bei abweichender Rolle Rückgabe mit Fehlermeldung.

Alle Server Actions verlangen session.user.tenantId (sonst throw new Error('Unauthorized')) und scopen sämtliche Queries auf tenant_id. Eingaben werden vor DB-Zugriff per zod validiert. Datums-Strings (YYYY-MM-DD) werden über parseIsoDate auf UTC-Mitternacht normalisiert.

Berechtigungslogik​

Datei: lib/klassenbuch/permissions.ts ('server-only').

canRecordAttendance​

canRecordAttendance({ userId, timetableId, tenantId, userRole }):
Promise<{ allowed: boolean; reason?: string }>

Regeln:

  1. userRole === 'ADMIN' → immer erlaubt.
  2. Klassenlehrkraft (timetable.school_class.teacher_id === userId) → darf alle Stunden ihrer Klasse erfassen.
  3. Fachlehrkraft (timetable.teacher_id === userId) → darf nur ihre eigenen Stunden erfassen.
  4. Sonst allowed: false.

Der Timetable-Lookup ist auf tenant_id gescopet; ein nicht gefundener Eintrag liefert allowed: false.

isClassbookLocked​

isClassbookLocked({ date, lockAfterDays }): boolean

Gibt true zurück, wenn lockAfterDays !== null und der Eintrag älter als lockAfterDays Tage ist (Date.now() - date.getTime() > lockAfterDays * 24h). Bei null nie gesperrt.

Permissions-Registry​

In lib/permissions-registry.ts registriert:

PermissionVerwendung
klassenbuch:readZugriff auf /klassenbuch (Page-Component prüft via hasPermission(session, 'klassenbuch', 'read'), sonst Redirect /dashboard?no_access=1)
klassenbuch:createKlassenbuch-Einträge erstellen
klassenbuch:writeKlassenbuch bearbeiten
klassenbuch:deleteKlassenbuch-Einträge löschen

Hinweis: Die feingranulare Erfassungs-Berechtigung läuft in den Schreib-Actions über canRecordAttendance (Klassen-/Fachlehrkraft-Logik), nicht über die klassenbuch:*-Permissions. Letztere schützen den Seitenzugriff.

Routen​

RouteInhalt
app/(backend)/klassenbuch/page.tsxHauptseite, 3-Spalten-Layout (ClassbookLayout): Klassenliste, Stundenliste des Tages, Erfassungsbereich. Query-Parameter class, date, view (today/week), week, lesson.
app/(backend)/klassenbuch/[classId]/page.tsxEinstiegspunkt pro Klasse
app/(backend)/klassenbuch/[classId]/[date]/page.tsxEinstiegspunkt pro Klasse+Datum
app/(backend)/klassenbuch/[classId]/[date]/[timetableId]/page.tsxReiner Redirect auf /klassenbuch?class=…&date=…&view=today&lesson=…

Sichtbarkeitslogik der Hauptseite​

  • Klassenauswahl (Spalte 1): Es werden nur Klassen geladen, in denen der User Klassenlehrkraft (teacher_id) oder im aktiven Stundenplan als teacher_id eingetragen ist.
  • Stundenfilter (Spalte 2): Ist der User Klassenlehrkraft der gewählten Klasse, sieht er alle Stunden des Wochentags; Fachlehrkräfte sehen nur ihre eigenen (teacher_id: userId).
  • Auto-Auswahl: Am heutigen Tag wird über die Uhrzeit die aktuelle/nächste Stunde vorausgewählt (findCurrentOrNextLesson), Freistunden (class_subject_id === null) werden übersprungen.
  • Wochenansicht (view=week): Lädt den vollen Wochen-Stundenplan plus aggregierte Anwesenheits-Counts (prisma.attendance.groupBy).
  • Zusätzlich werden die eigenen Vertretungsstunden der Woche über getMySubstitutionsForWeek (Vertretungsplan-Modul) eingebunden.

Verwandte Stellen​

  • components/klassenbuch/ClassbookLayout.tsx — 3-Spalten-Orchestrierung der Hauptseite.
  • lib/actions/vertretungsplan/substitution.ts — getMySubstitutionsForWeek (Einbindung der eigenen Vertretungen).