Zum Hauptinhalt springen

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[]
}
  • date und date_end sind reine Datumsfelder (@db.Date, ohne Uhrzeit-Komponente).
  • notes ist nullable (String?) mit Default "".
  • teacher_id verweist auf einen User (Lehrkraft = User mit job_role_id), nicht auf ein separates Teacher-Modell.
  • date ist der Starttag, date_end optional (NULL = Eintagesabwesenheit).
  • all_day=false impliziert from_period/to_period (Wertebereich aus LessonTime.number).
  • Eine Abwesenheit hängt 1:n an Substitution (Draft- oder gepflegte Vertretungen).

Server Actions​

Datei: lib/actions/vertretungsplan/teacher-absence.ts

ActionZweck
createTeacherAbsenceLegt Abwesenheit an, erstellt Draft-Substitutions für alle betroffenen Timetable-Stunden im Datumsbereich
updateTeacherAbsenceAktualisiert Stammdaten und synchronisiert die zugehörigen Draft-Substitutions mit dem neuen Zeitraum/Stundenbereich
cancelTeacherAbsenceSetzt alle Draft-Substitutions auf cancelled und löscht die Abwesenheit
getTeacherAbsencesForDateTagesabfrage (berücksichtigt Zeiträume via date <= X AND (date_end IS NULL OR date_end >= X))
getTeacherAbsencesForTeacherAbfrage pro Lehrkraft + Zeitraum

Lesson-Matching​

createTeacherAbsence und updateTeacherAbsence nutzen dasselbe Matching:

  1. Wochentage (Mo–Fr) im Zeitraum via getWeekdaysInRange ermitteln (UTC).
  2. Pro Wochentag alle Timetable-Einträge der Lehrkraft mit status='active' laden, ggf. eingeschränkt auf lesson_time.number ∈ [from_period, to_period].
  3. Pro Timetable-Eintrag eine Substitution (timetable_id + date eindeutig) als Draft erzeugen bzw. aktualisieren.

Update-Semantik​

updateTeacherAbsence muss bestehende Vertretungen sinnvoll behandeln. Implementierungsregeln:

  1. Reset unzugewiesener Drafts: Alle Substitution-Datensätze dieser Abwesenheit mit status='draft' und substitute_teacher_id IS NULL werden auf cancelled gesetzt. Dadurch entfallen Drafts für nicht mehr betroffene Stunden.
  2. Pro Timetable-Stunde im neuen Range wird Substitution per timetable_id_date-Lookup geprüft:
    • Nicht vorhanden → neue Draft-Substitution anlegen.
    • Vorhanden, mit Vertreter (substitute_teacher_id) → Status unverändert lassen, nur teacher_absence_id ggf. neu verknüpfen. Damit überleben bereits zugewiesene/veröffentlichte Vertretungen das Edit.
    • Vorhanden, ohne Vertreter → wieder auf draft setzen 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_period gesetzt
    • to_period >= from_period
    • date_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 aus absence-Prop befüllt (useEffect auf sheetOpen + 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:

PermissionVerwendung
abwesenheiten:readZugriff auf /lehrkraefte/abwesenheiten
abwesenheiten:createcreateTeacherAbsence
abwesenheiten:writeupdateTeacherAbsence
abwesenheiten:deletecancelTeacherAbsence

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 findFirst mit tenant_id-Filter die einzige Absicherung; ein gespoofter teacher_absence_id aus 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.

BereichKomponenteInhalt
HeaderDayPlanHeaderDatums-Navigator, Counters (offen/Entwurf/veröffentlicht), Publish-Button
Sidebar (3/12)AbsentTeachersSidebarAbwesende Lehrkräfte des Tages + Filter „Nur offene" / „Nur Konflikte"
Hauptspalte (9/12)DayLayout mit LessonRow + LessonEditPanelAlle 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.

/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):

IDUIPersistenz
substituteTeacherSmartSubstitutePickerSubstitution.substitute_teacher_id
roomInputSubstitution.room_override
subjectOverrideSelectSubstitution.subject_override_id
newDateCalendar-PopoverSubstitution.new_date
newPeriodNumberInputSubstitution.new_period
materialTextareaSubstitution.material
eventNameInputSubstitution.event_name
eventLocationInputSubstitution.event_location
accompanyingTeachersMultiTeacherPicker (Popover-Mehrfachauswahl mit gruppierten Vorschlägen)Substitution.accompanying_teacher_ids (TEXT[])
mergedIntoClassSelectSubstitution.merged_into_class_id
noteTextareaSubstitution.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 assignSubstitution automatisch eine Pendant-Substitution am Zieltermin angelegt (gleiche Klasse, new_period an new_date-Wochentag, Note „Verlegt aus Originaltermin"). Verknüpfung über Substitution.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.confirm zur Bestätigung — außer allowConflicts=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 + SubstitutionConflictError fü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.