7c96d89927
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>
127 lines
5.0 KiB
TypeScript
127 lines
5.0 KiB
TypeScript
'use client'
|
|
|
|
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'
|
|
| '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; label: string }[]
|
|
}
|
|
|
|
const RULE_GROUPS: RuleGroup[] = [
|
|
{
|
|
group: 'Lehrer',
|
|
rules: [
|
|
{ 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' },
|
|
{ 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' },
|
|
{ id: 'class-no-gaps', label: 'Keine Freistunden' },
|
|
],
|
|
},
|
|
{
|
|
group: 'Raum',
|
|
rules: [
|
|
{ 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">
|
|
<aside className={`rounded-2xl border backdrop-blur-xl p-3 ${isDark ? 'bg-white/5 border-white/10' : 'bg-white/80 border-black/10'}`}>
|
|
{RULE_GROUPS.map(g => (
|
|
<div key={g.group} className="mb-4 last:mb-0">
|
|
<h4 className={`text-xs uppercase tracking-wide mb-2 px-2 ${isDark ? 'text-white/40' : 'text-slate-500'}`}>{g.group}</h4>
|
|
<div className="space-y-1">
|
|
{g.rules.map(r => {
|
|
const isActive = active === r.id
|
|
return (
|
|
<button
|
|
key={r.id}
|
|
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'
|
|
: isDark ? 'text-white/80 hover:bg-white/10' : 'text-slate-700 hover:bg-slate-100'
|
|
}`}
|
|
>
|
|
<span className="truncate">{r.label}</span>
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</aside>
|
|
|
|
<div>
|
|
<Editor />
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|