Stundenplan Phase 3d: all 15 constraint editors via shared shell
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 30s
CI / test-go-edu-search (push) Successful in 29s
CI / test-python-klausur (push) Failing after 2m38s
CI / test-python-agent-core (push) Successful in 18s
CI / test-nodejs-website (push) Successful in 20s

Backend was already complete in Phase 2; this finishes the UI.

  - regeln/_shell.tsx introduces useConstraintCrud (handles list/create/
    delete state + reload), ConstraintShell (header, prereq banner,
    form toggle, error display, empty/loading/table render), and
    useShellStyles for the recurring theme tokens. Each editor now
    only carries its schema-specific bits.
  - Existing 4 editors (TeacherUnavailableDay/Window, SubjectMax
    Consecutive/PreferredPeriod) refactored onto the shell — every
    Playwright selector preserved.
  - 11 new editors covering the remaining constraint tables:
      TeacherMaxHours{Day,Week}, TeacherExcluded{Subject,Room},
      Subject{MinDayGap,ContiguousWhenRepeated,DoubleLesson},
      Class{MaxHoursDay,NoGaps},
      Room{RequiresType,Unavailable}.
  - RegelnHub now references all 15 editors directly — no more 'soon'
    placeholders. The two duplicate 'Max. Stunden / Tag' entries
    (teacher + class) are intentional and disambiguated by group.

Tests:
  - e2e/stundenplan.spec.ts: mock routes added for all 11 new constraint
    endpoints. RegelnHub suite gains a single test that switches
    through 13 uniquely-labelled editors, plus a dedicated test for
    the two duplicate 'Max. Stunden / Tag' labels.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-21 23:27:34 +02:00
parent c2c09e1cd9
commit 7c96d89927
12 changed files with 1330 additions and 584 deletions
@@ -4,61 +4,91 @@ import { useState } from 'react'
import { useTheme } from '@/lib/ThemeContext'
import { TeacherUnavailableDayEditor } from './TeacherUnavailableDayEditor'
import { TeacherUnavailableWindowEditor } from './TeacherUnavailableWindowEditor'
import { TeacherMaxHoursDayEditor, TeacherMaxHoursWeekEditor } from './TeacherMaxHoursEditors'
import { TeacherExcludedSubjectEditor, TeacherExcludedRoomEditor } from './TeacherExclusionEditors'
import { SubjectMaxConsecutiveEditor } from './SubjectMaxConsecutiveEditor'
import { SubjectPreferredPeriodEditor } from './SubjectPreferredPeriodEditor'
import {
SubjectMinDayGapEditor, SubjectContiguousWhenRepeatedEditor, SubjectDoubleLessonEditor,
} from './SubjectSimpleEditors'
import { ClassMaxHoursDayEditor, ClassNoGapsEditor } from './ClassEditors'
import { RoomRequiresTypeEditor, RoomUnavailableEditor } from './RoomEditors'
type RuleType =
| 'teacher-unavailable-day'
| 'teacher-unavailable-window'
| 'subject-max-consecutive'
| 'subject-preferred-period'
| 'teacher-unavailable-day' | 'teacher-unavailable-window'
| 'teacher-max-hours-day' | 'teacher-max-hours-week'
| 'teacher-excluded-subject' | 'teacher-excluded-room'
| 'subject-min-day-gap' | 'subject-max-consecutive'
| 'subject-contiguous-when-repeated' | 'subject-preferred-period'
| 'subject-double-lesson'
| 'class-max-hours-day' | 'class-no-gaps'
| 'room-requires-type' | 'room-unavailable'
interface RuleGroup {
group: string
rules: { id: RuleType | string; label: string; implemented: boolean }[]
rules: { id: RuleType; label: string }[]
}
const RULE_GROUPS: RuleGroup[] = [
{
group: 'Lehrer',
rules: [
{ id: 'teacher-unavailable-day', label: 'Tag nicht verfuegbar', implemented: true },
{ id: 'teacher-unavailable-window', label: 'Zeitfenster nicht verfuegbar', implemented: true },
{ id: 'teacher-max-hours-day', label: 'Max. Stunden / Tag', implemented: false },
{ id: 'teacher-max-hours-week', label: 'Max. Stunden / Woche', implemented: false },
{ id: 'teacher-excluded-subject', label: 'Fach ausgeschlossen', implemented: false },
{ id: 'teacher-excluded-room', label: 'Raum ausgeschlossen', implemented: false },
{ id: 'teacher-unavailable-day', label: 'Tag nicht verfuegbar' },
{ id: 'teacher-unavailable-window', label: 'Zeitfenster nicht verfuegbar' },
{ id: 'teacher-max-hours-day', label: 'Max. Stunden / Tag' },
{ id: 'teacher-max-hours-week', label: 'Max. Stunden / Woche' },
{ id: 'teacher-excluded-subject', label: 'Fach ausgeschlossen' },
{ id: 'teacher-excluded-room', label: 'Raum ausgeschlossen' },
],
},
{
group: 'Fach',
rules: [
{ id: 'subject-max-consecutive', label: 'Max. Stunden am Stueck', implemented: true },
{ id: 'subject-preferred-period', label: 'Bevorzugter Stunden-Bereich', implemented: true },
{ id: 'subject-min-day-gap', label: 'Min. Tagesabstand', implemented: false },
{ id: 'subject-contiguous-when-repeated', label: 'Bei Mehrfach: zusammenhaengend', implemented: false },
{ id: 'subject-double-lesson', label: 'Doppelstunde bevorzugt', implemented: false },
{ id: 'subject-max-consecutive', label: 'Max. Stunden am Stueck' },
{ id: 'subject-preferred-period', label: 'Bevorzugter Stunden-Bereich' },
{ id: 'subject-min-day-gap', label: 'Min. Tagesabstand' },
{ id: 'subject-contiguous-when-repeated', label: 'Bei Mehrfach: zusammenhaengend' },
{ id: 'subject-double-lesson', label: 'Doppelstunde bevorzugt' },
],
},
{
group: 'Klasse',
rules: [
{ id: 'class-max-hours-day', label: 'Max. Stunden / Tag', implemented: false },
{ id: 'class-no-gaps', label: 'Keine Freistunden', implemented: false },
{ id: 'class-max-hours-day', label: 'Max. Stunden / Tag' },
{ id: 'class-no-gaps', label: 'Keine Freistunden' },
],
},
{
group: 'Raum',
rules: [
{ id: 'room-requires-type', label: 'Fach benoetigt Raumtyp', implemented: false },
{ id: 'room-unavailable', label: 'Raum nicht verfuegbar', implemented: false },
{ id: 'room-requires-type', label: 'Fach benoetigt Raumtyp' },
{ id: 'room-unavailable', label: 'Raum nicht verfuegbar' },
],
},
]
const EDITORS: Record<RuleType, React.ComponentType> = {
'teacher-unavailable-day': TeacherUnavailableDayEditor,
'teacher-unavailable-window': TeacherUnavailableWindowEditor,
'teacher-max-hours-day': TeacherMaxHoursDayEditor,
'teacher-max-hours-week': TeacherMaxHoursWeekEditor,
'teacher-excluded-subject': TeacherExcludedSubjectEditor,
'teacher-excluded-room': TeacherExcludedRoomEditor,
'subject-min-day-gap': SubjectMinDayGapEditor,
'subject-max-consecutive': SubjectMaxConsecutiveEditor,
'subject-contiguous-when-repeated': SubjectContiguousWhenRepeatedEditor,
'subject-preferred-period': SubjectPreferredPeriodEditor,
'subject-double-lesson': SubjectDoubleLessonEditor,
'class-max-hours-day': ClassMaxHoursDayEditor,
'class-no-gaps': ClassNoGapsEditor,
'room-requires-type': RoomRequiresTypeEditor,
'room-unavailable': RoomUnavailableEditor,
}
export function RegelnHub() {
const { isDark } = useTheme()
const [active, setActive] = useState<RuleType>('teacher-unavailable-day')
const Editor = EDITORS[active]
return (
<div className="grid grid-cols-1 md:grid-cols-[260px_1fr] gap-6" data-testid="regeln-hub">
@@ -69,24 +99,17 @@ export function RegelnHub() {
<div className="space-y-1">
{g.rules.map(r => {
const isActive = active === r.id
const isDone = r.implemented
return (
<button
key={r.id}
disabled={!isDone}
onClick={() => isDone && setActive(r.id as RuleType)}
onClick={() => setActive(r.id)}
className={`w-full text-left px-3 py-2 rounded-lg text-sm transition-colors ${
isActive
? isDark ? 'bg-indigo-500/20 text-white' : 'bg-indigo-100 text-indigo-900'
: isDone
? isDark ? 'text-white/80 hover:bg-white/10' : 'text-slate-700 hover:bg-slate-100'
: isDark ? 'text-white/30 cursor-not-allowed' : 'text-slate-400 cursor-not-allowed'
: isDark ? 'text-white/80 hover:bg-white/10' : 'text-slate-700 hover:bg-slate-100'
}`}
>
<span className="flex items-center justify-between gap-2">
<span className="truncate">{r.label}</span>
{!isDone && <span className="text-xs opacity-60">soon</span>}
</span>
<span className="truncate">{r.label}</span>
</button>
)
})}
@@ -96,10 +119,7 @@ export function RegelnHub() {
</aside>
<div>
{active === 'teacher-unavailable-day' && <TeacherUnavailableDayEditor />}
{active === 'teacher-unavailable-window' && <TeacherUnavailableWindowEditor />}
{active === 'subject-max-consecutive' && <SubjectMaxConsecutiveEditor />}
{active === 'subject-preferred-period' && <SubjectPreferredPeriodEditor />}
<Editor />
</div>
</div>
)