Lehrkraft-Abwesenheiten
Verwaltung von TeacherAbsence-Datensätzen inkl. automatischer Generierung und Pflege der zugehörigen Substitution-Entwürfe für den Vertretungsplan.
Datenmodell
model TeacherAbsence {
id String @id @default(uuid())
tenant_id String
teacher_id String // referenziert User (es gibt kein Teacher-Modell)
date DateTime @db.Date
date_end DateTime? @db.Date
all_day Boolean @default(true)
from_period Int?
to_period Int?
reason String?
notes String? @default("")
created_by_id String
// ...
substitutions Substitution[]
}
dateunddate_endsind reine Datumsfelder (@db.Date, ohne Uhrzeit-Komponente).notesist nullable (String?) mit Default"".teacher_idverweist auf einenUser(Lehrkraft =Usermitjob_role_id), nicht auf ein separatesTeacher-Modell.dateist der Starttag,date_endoptional (NULL = Eintagesabwesenheit).all_day=falseimpliziertfrom_period/to_period(Wertebereich ausLessonTime.number).- Eine Abwesenheit hängt 1:n an
Substitution(Draft- oder gepflegte Vertretungen).
Server Actions
Datei: lib/actions/vertretungsplan/teacher-absence.ts
| Action | Zweck |
|---|---|
createTeacherAbsence | Legt Abwesenheit an, erstellt Draft-Substitutions für alle betroffenen Timetable-Stunden im Datumsbereich |
updateTeacherAbsence | Aktualisiert Stammdaten und synchronisiert die zugehörigen Draft-Substitutions mit dem neuen Zeitraum/Stundenbereich |
cancelTeacherAbsence | Setzt alle Draft-Substitutions auf cancelled und löscht die Abwesenheit |
getTeacherAbsencesForDate | Tagesabfrage (berücksichtigt Zeiträume via date <= X AND (date_end IS NULL OR date_end >= X)) |
getTeacherAbsencesForTeacher | Abfrage pro Lehrkraft + Zeitraum |
Lesson-Matching
createTeacherAbsence und updateTeacherAbsence nutzen dasselbe Matching:
- Wochentage (Mo–Fr) im Zeitraum via
getWeekdaysInRangeermitteln (UTC). - Pro Wochentag alle
Timetable-Einträge der Lehrkraft mitstatus='active'laden, ggf. eingeschränkt auflesson_time.number ∈ [from_period, to_period]. - Pro Timetable-Eintrag eine
Substitution(timetable_id + date eindeutig) als Draft erzeugen bzw. aktualisieren.
Update-Semantik
updateTeacherAbsence muss bestehende Vertretungen sinnvoll behandeln. Implementierungsregeln:
- Reset unzugewiesener Drafts: Alle
Substitution-Datensätze dieser Abwesenheit mitstatus='draft'undsubstitute_teacher_id IS NULLwerden aufcancelledgesetzt. Dadurch entfallen Drafts für nicht mehr betroffene Stunden. - Pro Timetable-Stunde im neuen Range wird
Substitutionpertimetable_id_date-Lookup geprüft:- Nicht vorhanden → neue Draft-Substitution anlegen.
- Vorhanden, mit Vertreter (
substitute_teacher_id) → Status unverändert lassen, nurteacher_absence_idggf. neu verknüpfen. Damit überleben bereits zugewiesene/veröffentlichte Vertretungen das Edit. - Vorhanden, ohne Vertreter → wieder auf
draftsetzen und mit Abwesenheit verknüpfen (Reaktivierung des in Schritt 1 evtl. cancelten Drafts).
Alles läuft in einer prisma.$transaction.
Schemas
Datei: utils/schemas/teacher-absence-schema.ts
teacherAbsenceCreateSchema— Eingaben für Anlegen.teacherAbsenceUpdateSchema— wie Create-Schema +teacher_absence_id: UUID. Validiert zusätzlich:all_day=false⇒from_period&to_periodgesetztto_period >= from_perioddate_end >= date
UI
Datei: components/vertretungsplan/AbsenceCreateDialog.tsx
Sheet für Create + Edit:
- Create-Modus: Standard-Trigger-Button, Datepicker auf
disabled={{ before: new Date() }}. - Edit-Modus: kontrolliert via
open/onOpenChange, ohne eigenen Trigger; Datumssperre entfällt. Formular wird beim Öffnen ausabsence-Prop befüllt (useEffectaufsheetOpen+absence.id).
<AbsenceCreateDialog
teachers={teachers}
absence={{
id,
teacher_id,
date,
date_end,
all_day,
from_period,
to_period,
reason,
notes
}}
open={editOpen}
onOpenChange={setEditOpen}
/>
Datei: components/vertretungsplan/AbsenceRow.tsx — Dropdown-Aktionen je Zeile (Im Vertretungsplan anzeigen, Bearbeiten, Stornieren). Edit-Item öffnet das kontrollierte Sheet (onSelect mit preventDefault, um Menu-Close + Sheet-Open nicht zu kollidieren).
Permissions
Registriert in lib/permissions-registry.ts:
| Permission | Verwendung |
|---|---|
abwesenheiten:read | Zugriff auf /lehrkraefte/abwesenheiten |
abwesenheiten:create | createTeacherAbsence |
abwesenheiten:write | updateTeacherAbsence |
abwesenheiten:delete | cancelTeacherAbsence |
Die Server Actions prüfen aktuell Authentifizierung + tenantId; Permission-Checks sollten ggf. ergänzt werden (siehe TODO unten).
Bekannte Einschränkungen / TODOs
- Server Actions setzen aktuell keinen expliziten
hasPermission()-Check — nur Session/Tenant-Guard. Permission-Enforcement findet aktuell nur in der Page-Component (abwesenheiten:read) statt. - Beim Editieren über Tenant-Grenzen ist
findFirstmittenant_id-Filter die einzige Absicherung; ein gespoofterteacher_absence_idaus einem fremden Tenant würde mit „nicht gefunden" abbrechen. - Wochenend-Tage werden im Range stillschweigend übersprungen — Reine Wochenend-Abwesenheiten werfen einen Validierungsfehler („Abwesenheiten können nur für Wochentage (Mo–Fr) erfasst werden").
Admin-Tagesvertretungsplan (2-Spalten, Inline-Edit)
Route: app/(backend)/vertretungsplan/[date]/page.tsx. Tag-zentrische Admin-Sicht; lehrkraft-zentrische Sicht über MySubstitutionsWidget auf dem Dashboard.
| Bereich | Komponente | Inhalt |
|---|---|---|
| Header | DayPlanHeader | Datums-Navigator, Counters (offen/Entwurf/veröffentlicht), Publish-Button |
| Sidebar (3/12) | AbsentTeachersSidebar | Abwesende Lehrkräfte des Tages + Filter „Nur offene" / „Nur Konflikte" |
| Hauptspalte (9/12) | DayLayout mit LessonRow + LessonEditPanel | Alle betroffenen Stunden chronologisch; Klick klappt Inline-Edit auf |
Mehrere LessonRows gleichzeitig aufklappbar (Set<string>-State). Lehrkraft-Highlight in der Sidebar filtert die Hauptliste auf deren Stunden, hebt sie aber visuell hervor statt sie zu verstecken.
URL-Deeplink
/vertretungsplan/<date>?teacher=<id>&timetable=<id>
teacher— markiert die Lehrkraft in der Sidebar und expandiert ihre erste Stunde.timetable— expandiert explizit eine bestimmte Stunde.
AbsenceRow.tsx linkt „Im Vertretungsplan anzeigen" mit ?teacher=<absence.teacher.id>.
Konflikt-Erkennung
Pro Tag wird einmalig eine busyMap (Substitute-Teacher → Set<period>) aus aktiven Timetable-Einträgen gebildet. Eine Stunde hat hasConflict=true, wenn die zugewiesene Vertretungslehrkraft in der gleichen Stunde aktiven Unterricht hat. Der „Nur Konflikte"-Filter nutzt diese Flagge.
Typabhängige Formularfelder
LessonEditPanel rendert Eingabefelder dynamisch anhand von SubstitutionType.visible_fields und macht sie pflichtig anhand von required_fields. Werte aller Felder leben gemeinsam im React-State der Komponente — nicht-sichtbare Felder bleiben erhalten, werden beim Speichern aber auf null zurückgesetzt (damit ein Typwechsel keine alten Werte behält).
Feld-IDs (Convention):
| ID | UI | Persistenz |
|---|---|---|
substituteTeacher | SmartSubstitutePicker | Substitution.substitute_teacher_id |
room | Input | Substitution.room_override |
subjectOverride | Select | Substitution.subject_override_id |
newDate | Calendar-Popover | Substitution.new_date |
newPeriod | NumberInput | Substitution.new_period |
material | Textarea | Substitution.material |
eventName | Input | Substitution.event_name |
eventLocation | Input | Substitution.event_location |
accompanyingTeachers | MultiTeacherPicker (Popover-Mehrfachauswahl mit gruppierten Vorschlägen) | Substitution.accompanying_teacher_ids (TEXT[]) |
mergedIntoClass | Select | Substitution.merged_into_class_id |
note | Textarea | Substitution.note |
Sonderfälle
- Ausfall (A) bei Klassenstufe ≤6: gelber Warn-Hinweis ("Aufsichtspflicht beachten") im EditPanel sobald Typ A gewählt und
lesson.classGrade <= 6. - Mitbetreuung (MB): SmartSubstitutePicker bekommt
allowConflicts=true, weil der Mitbetreuende per Definition zur gleichen Zeit Unterricht hat. - Verlegung (VL): beim Speichern wird in
assignSubstitutionautomatisch eine Pendant-Substitution am Zieltermin angelegt (gleiche Klasse,new_periodannew_date-Wochentag, Note „Verlegt aus Originaltermin"). Verknüpfung überSubstitution.linked_substitution_id. Bei Typwechsel weg von VL wird das Pendant gelöscht. - Schulveranstaltung (SV): Bulk-Apply („auf alle Stunden dieser Klasse an diesem Tag anwenden") ist noch nicht umgesetzt — TODO.
SmartSubstitutePicker
Datei: components/vertretungsplan/SmartSubstitutePicker.tsx.
Popover-Combobox mit:
- Gruppen: Empfohlen / Gut geeignet / Möglich / Konflikt (Header mit Icon).
- Evidenz-Tags pro Lehrkraft:
frei, Fach-Kürzel, Klassen-Name,N Vert. diese Woche(Warn). - Konflikt-Auswahl: durchgestrichener Name,
window.confirmzur Bestätigung — außerallowConflicts=true(für MB). - Sonderoptionen-Footer: Ausfall / EVA / Aufsicht via
onSelectSpecial-Callback. - Tastatur:
↑/↓(mit Scroll-into-view),Enter,Esc. Tippen filtert nach Name.
Datenquelle: getGroupedSubstituteSuggestions in lib/actions/vertretungsplan/suggestions.ts. Dies ist die einzige exportierte Vorschlags-Funktion; eine frühere getSuggestedSubstitutes existiert nicht mehr (wurde entfernt).
Phase 5 — Konflikte, Sortierung, Geschwister-Anwendung & Multi-Lehrer
Draft-Reset beim Bearbeiten
assignSubstitution setzt eine Vertretung bei jeder Bearbeitung zurück auf status='draft' und leert published_at/published_by_id. Eine bereits veröffentlichte Vertretung muss nach einer Änderung also erneut über „Alle veröffentlichen" freigegeben werden. Rückgabewert ist nun { substitution, siblings } (zuvor nur das Substitution-Objekt) — siblings sind die übrigen, noch nicht identisch konfigurierten Substitutions derselben teacher_absence_id.
Geschwister-Anwendung (applySubstitutionToSiblings)
Nach dem Speichern bietet ApplyToSiblingsDialog an, die Einstellungen einer Vertretung auf die übrigen Stunden derselben Abwesenheitsperiode zu übertragen. Die Server Action applySubstitutionToSiblings({ source_substitution_id, sibling_ids }) kopiert per updateMany alle inhaltlichen Felder (Typ, kind, Vertreter, Raum, Fach-Override, Material, Event-Felder, merged_into_class_id, accompanying_teacher_ids, Note). Datumsspezifische Felder (new_date, new_period) werden bewusst nicht übernommen. Die Zielstunden werden ebenfalls auf draft gesetzt.
Hinweis: Dies betrifft Stunden derselben Abwesenheitsperiode (mehrere Tage), nicht den unter „Sonderfälle → Schulveranstaltung (SV)" beschriebenen, weiterhin offenen Bulk-Apply „alle Stunden dieser Klasse an diesem Tag".
Verwaiste Vertretungen (orphanedSubstitutions)
getSubstitutionPlanForDate liefert zusätzlich orphanedSubstitutions: Substitutions, die noch an der Abwesenheit hängen, deren zugehöriger Timetable-Eintrag aber inzwischen einen anderen Lehrer hat (Filter: timetable.teacher_id !== teacher_absence.teacher_id). DayLayout zeigt sie über die Prop orphanedLessons separat an. Der leere Rückgabefall (kein gültiger Wochentag) enthält das Feld ebenfalls.
Lehrerwechsel-Konflikt im Stundenplan
Ändert sich beim Bearbeiten eines Timetable-Eintrags der Lehrer (updateTimetableById), prüft die Action zukünftige, bereits bearbeitete Vertretungen (substitution_type_id != null, date >= heute) der bisherigen Abwesenheit. Existieren solche und liegt keine substitutionDecision vor, wird eine SubstitutionConflictError (lib/timetable/substitution-conflicts.ts) geworfen. updateTimetable mappt sie auf TimetableFormState.substitutionConflict; die UI zeigt components/ui/school-classes/substitution-conflict-dialog.tsx. Der Nutzer wählt delete (Substitutions per deleteMany entfernen) oder keep (verwaisen lassen) — die Entscheidung wird als verstecktes Feld substitution_decision erneut gepostet.
Sortierung & Gruppierung in DayLayout
DayLayout gruppiert/sortiert die Stunden über einen SortKey (period | class | teacher | subject | room) und einen Toggle für „Nur offene" / „Nur Konflikte". Gruppen werden per Map aus der sortierten Liste gebildet und mit Stundenzahl je Gruppe angezeigt.
Multi-Lehrer-Auswahl & Farb-Tags
MultiTeacherPicker (für accompanyingTeachers) nutzt dieselbe Datenquelle wie der SmartSubstitutePicker (getGroupedSubstituteSuggestions), erlaubt aber Mehrfachauswahl mit Pills im Trigger. Ohne timetableId/date fällt er auf eine flache Liste zurück. SuggestionTag trägt nun optional ein color-Feld (Fach-Farbe als Dot vor dem Label); getGroupedSubstituteSuggestions befüllt es aus subject.color.
Verwandte Stellen
app/(backend)/lehrkraefte/abwesenheiten/page.tsx— Listenansicht der Abwesenheiten.components/vertretungsplan/DayLayout.tsx— Orchestrierung der Tagesansicht.components/vertretungsplan/LessonRow.tsx/LessonEditPanel.tsx— Stunden-Zeile + Inline-Editor.components/vertretungsplan/AbsentTeachersSidebar.tsx— Sidebar mit Filtern.components/vertretungsplan/SmartSubstitutePicker.tsx— Vorschlags-Popover.lib/actions/vertretungsplan/substitution.ts— Vertretungs-Zuweisung, VL-Pendant-Logik, Day-Loader,applySubstitutionToSiblings,orphanedSubstitutions.lib/actions/vertretungsplan/suggestions.ts— Vorschlagsalgorithmus (flach + gruppiert, inkl. Farb-Tags).lib/timetable/substitution-conflicts.ts—SubstitutionConflict+SubstitutionConflictErrorfür Lehrerwechsel im Stundenplan.components/vertretungsplan/ApplyToSiblingsDialog.tsx— Nachfrage „auf weitere Stunden übertragen".components/vertretungsplan/MultiTeacherPicker.tsx— Mehrfachauswahl von Lehrkräften (begleitende Lehrkräfte).components/ui/school-classes/substitution-conflict-dialog.tsx— Konflikt-Dialog beim Lehrerwechsel.