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 alsupsertüber diesen Unique-Constraint. propagated_fromist 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=truewird ein neuer Datensatz mitcorrects_idangelegt, der alte bleibt erhalten (Self-RelationAttendanceCorrection).
AttendanceStatus:
| Wert | Bedeutung |
|---|---|
not_checked | noch nicht geprüft (Default-Anzeige für Schüler ohne Eintrag) |
present | anwesend |
absent | abwesend (noch nicht kategorisiert) |
absent_excused | entschuldigt |
absent_unexcused | unentschuldigt |
late | verspätet |
left_early | frühzeitig gegangen |
remote | Online-/Hybridunterricht |
AttendanceSource:
| Wert | Bedeutung |
|---|---|
teacher | Fachlehrkraft hat selbst eingetragen |
class_teacher | Klassenlehrkraft hat eingetragen (ggf. propagiert) |
parent_note | Eltern-Meldung → automatisch vorbesetzt |
admin | Sekretariat / 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-EnumAbsenceNoteStatus:pending→accepted|rejected.- Optionale Verknüpfung zu genau einem
Attendance-Eintrag (@unique). Bei Annahme wird dieser aufabsent_excusedgesetzt. submitted_by_portal_user_idreferenziert einenPortalUser(Eltern-Portal),submitted_by_user_ideinen internenUser.file_idverweist 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:
| Feld | Default | Zweck |
|---|---|---|
mode (ClassbookMode) | per_teacher | per_teacher (jede LK trägt selbst ein) oder class_teacher (Klassenlehrer-Prinzip) |
allow_parent_notes | true | Erlaubt Eltern-Entschuldigungen über das Portal |
require_absence_type | false | Erzwingt Auswahl eines AbsenceType bei Abwesenheit |
propagate_class_teacher | true | Übertragung der Ganztagesabwesenheit auf Folgestunden |
lock_after_days | null | Sperrfrist für Bearbeitung (NULL = nie gesperrt) |
show_lesson_content | true | Lehrstoff-Spalte anzeigen |
show_homework | true | Hausaufgaben-Spalte anzeigen |
Server Actions
Anwesenheiten — lib/actions/klassenbuch/attendance.ts
| Action | Zweck |
|---|---|
recordAttendance | Einzelnen Eintrag erfassen. Normalfall = upsert über (timetable_id, student_id, date); bei is_correction=true neuer Datensatz mit corrects_id. |
recordBulkAttendance | Mehrere Schüler einer Stunde in einer prisma.$transaction per upsert. |
propagateClassTeacherAttendance | Klassenlehrer-Modus: überträgt Ganztagesabwesenheit auf alle aktiven Timetable-Einträge der Klasse am Wochentag. |
getAttendanceForLesson | Liefert für eine Stunde alle Schüler; fehlende Einträge werden als virtueller not_checked-Eintrag (id: virtual_<studentId>, nicht persistiert) ergänzt. |
getAttendanceSummaryForStudent | Fehlzeiten-Ü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
| Action | Zweck |
|---|---|
reviewAbsenceNote | Klassenlehrkraft 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. |
getPendingAbsenceNotes | Liefert 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
| Action | Zweck |
|---|---|
saveClassbookEntry | upsert über (timetable_id, date); setzt beim Update updated_by_id. |
getClassbookEntriesForWeek | Lädt Einträge einer Klasse für Mo–Fr (week_start + 4 Tage). |
Konfiguration — lib/actions/klassenbuch/settings.ts
| Action | Zweck |
|---|---|
getClassbookSettings | Lädt die Settings; legt bei Fehlen einen Default-Datensatz an (alle Schema-Defaults). |
updateClassbookSettings | upsert 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:
userRole === 'ADMIN'→ immer erlaubt.- Klassenlehrkraft (
timetable.school_class.teacher_id === userId) → darf alle Stunden ihrer Klasse erfassen. - Fachlehrkraft (
timetable.teacher_id === userId) → darf nur ihre eigenen Stunden erfassen. - 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:
| Permission | Verwendung |
|---|---|
klassenbuch:read | Zugriff auf /klassenbuch (Page-Component prüft via hasPermission(session, 'klassenbuch', 'read'), sonst Redirect /dashboard?no_access=1) |
klassenbuch:create | Klassenbuch-Einträge erstellen |
klassenbuch:write | Klassenbuch bearbeiten |
klassenbuch:delete | Klassenbuch-Einträge löschen |
Hinweis: Die feingranulare Erfassungs-Berechtigung läuft in den Schreib-Actions über
canRecordAttendance(Klassen-/Fachlehrkraft-Logik), nicht über dieklassenbuch:*-Permissions. Letztere schützen den Seitenzugriff.
Routen
| Route | Inhalt |
|---|---|
app/(backend)/klassenbuch/page.tsx | Hauptseite, 3-Spalten-Layout (ClassbookLayout): Klassenliste, Stundenliste des Tages, Erfassungsbereich. Query-Parameter class, date, view (today/week), week, lesson. |
app/(backend)/klassenbuch/[classId]/page.tsx | Einstiegspunkt pro Klasse |
app/(backend)/klassenbuch/[classId]/[date]/page.tsx | Einstiegspunkt pro Klasse+Datum |
app/(backend)/klassenbuch/[classId]/[date]/[timetableId]/page.tsx | Reiner 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 alsteacher_ideingetragen 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).