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
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>
107 lines
4.4 KiB
TypeScript
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>
|
|
)
|
|
}
|