Files
breakpilot-lehrer/studio-v2/app/stundenplan/_components/regeln/RegelnHub.tsx
T
Benjamin Admin c2c09e1cd9
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 31s
CI / test-go-edu-search (push) Successful in 30s
CI / test-python-klausur (push) Failing after 3m31s
CI / test-python-agent-core (push) Successful in 18s
CI / test-nodejs-website (push) Successful in 22s
Stundenplan Phase 3c: complete Stammdaten + RegelnHub with 4 editors
Frontend additions in studio-v2:
  - PeriodsManager renders the weekly grid as a Mo–So table with one
    row per period_index. New entries auto-increment period_index so
    the user can hit Anlegen repeatedly for a full day's slots.
  - CurriculumManager joins classes + subjects; new entries refuse to
    open when either prerequisite list is empty (banner instead).
  - AssignmentsManager joins teacher × class × subject with the same
    prerequisite-banner pattern.
  - regeln/RegelnHub: vertical sidebar grouping all 15 constraint
    types by parent entity (Lehrer/Fach/Klasse/Raum). Implemented
    editors are clickable, the other 11 are visibly disabled with
    a 'soon' tag.
  - Three new editors:
      TeacherUnavailableWindowEditor (time-window pattern),
      SubjectMaxConsecutiveEditor (number-input pattern),
      SubjectPreferredPeriodEditor (number range pattern).
  - page.tsx wires every tab to its manager; the not-implemented
    placeholder is gone (no more empty tabs).

Test coverage:
  - e2e/stundenplan.spec.ts rewritten: 23 tests across 7 suites,
    covering all 8 tabs, the new managers' prerequisite banners,
    sub-tab switching in the RegelnHub, and the disabled state of
    not-yet-implemented constraint rules. Each test mocks the
    backend via page.route() so the suite stays hermetic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 23:08:15 +02:00

107 lines
4.4 KiB
TypeScript

'use client'
import { useState } from 'react'
import { useTheme } from '@/lib/ThemeContext'
import { TeacherUnavailableDayEditor } from './TeacherUnavailableDayEditor'
import { TeacherUnavailableWindowEditor } from './TeacherUnavailableWindowEditor'
import { SubjectMaxConsecutiveEditor } from './SubjectMaxConsecutiveEditor'
import { SubjectPreferredPeriodEditor } from './SubjectPreferredPeriodEditor'
type RuleType =
| 'teacher-unavailable-day'
| 'teacher-unavailable-window'
| 'subject-max-consecutive'
| 'subject-preferred-period'
interface RuleGroup {
group: string
rules: { id: RuleType | string; label: string; implemented: boolean }[]
}
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 },
],
},
{
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 },
],
},
{
group: 'Klasse',
rules: [
{ id: 'class-max-hours-day', label: 'Max. Stunden / Tag', implemented: false },
{ id: 'class-no-gaps', label: 'Keine Freistunden', implemented: false },
],
},
{
group: 'Raum',
rules: [
{ id: 'room-requires-type', label: 'Fach benoetigt Raumtyp', implemented: false },
{ id: 'room-unavailable', label: 'Raum nicht verfuegbar', implemented: false },
],
},
]
export function RegelnHub() {
const { isDark } = useTheme()
const [active, setActive] = useState<RuleType>('teacher-unavailable-day')
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
const isDone = r.implemented
return (
<button
key={r.id}
disabled={!isDone}
onClick={() => isDone && setActive(r.id as RuleType)}
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'
}`}
>
<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>
</button>
)
})}
</div>
</div>
))}
</aside>
<div>
{active === 'teacher-unavailable-day' && <TeacherUnavailableDayEditor />}
{active === 'teacher-unavailable-window' && <TeacherUnavailableWindowEditor />}
{active === 'subject-max-consecutive' && <SubjectMaxConsecutiveEditor />}
{active === 'subject-preferred-period' && <SubjectPreferredPeriodEditor />}
</div>
</div>
)
}