Files
breakpilot-lehrer/studio-v2/app/stundenplan/_components/regeln/TeacherUnavailableWindowEditor.tsx
T
Benjamin Admin 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
Stundenplan Phase 3d: all 15 constraint editors via shared shell
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>
2026-05-21 23:27:34 +02:00

100 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useState, useEffect } from 'react'
import { teacherUnavailableWindowApi, teachersApi } from '@/lib/stundenplan/api'
import type { TeacherUnavailableWindow, TimetableTeacher } from '@/app/stundenplan/types'
import { useConstraintCrud, ConstraintShell, useShellStyles, DAYS, dayLabel } from './_shell'
type FormState = Omit<TeacherUnavailableWindow, 'id' | 'created_by_user_id' | 'created_at'>
const initialForm: FormState = {
teacher_id: '', day_of_week: 2, start_time: '13:00', end_time: '17:00',
is_hard: true, weight: 100, active: true, note: '',
}
export function TeacherUnavailableWindowEditor() {
const styles = useShellStyles()
const crud = useConstraintCrud<TeacherUnavailableWindow, FormState>(teacherUnavailableWindowApi, initialForm)
const [teachers, setTeachers] = useState<TimetableTeacher[]>([])
useEffect(() => { teachersApi.list().then(setTeachers).catch(() => setTeachers([])) }, [])
const teacherLabel = (id: string): string => {
const t = teachers.find(x => x.id === id)
return t ? `${t.last_name}, ${t.first_name}` : id.slice(0, 8) + '…'
}
return (
<ConstraintShell
testId="teacher-unavailable-window-editor"
title="Lehrer: Zeitfenster nicht verfuegbar"
description="Beispiel: „Lehrer Z Dienstags 13:0017:00 nicht"."
newLabel="+ Neue Regel"
newDisabled={teachers.length === 0}
prereqWarning={teachers.length === 0 ? 'Zuerst Lehrer anlegen.' : null}
emptyText="Keine Regeln vorhanden."
tableHeaders={['Lehrer', 'Tag', 'Zeitfenster', 'Hart', 'Notiz']}
state={crud}
formBody={
<>
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
<div>
<label className="block text-sm mb-1 opacity-70">Lehrer</label>
<select required value={crud.form.teacher_id} onChange={e => crud.setForm({ ...crud.form, teacher_id: e.target.value })} className={`w-full px-3 py-2 rounded-lg border ${styles.inputClass}`}>
<option value="">— bitte waehlen —</option>
{teachers.map(t => <option key={t.id} value={t.id}>{t.last_name}, {t.first_name}</option>)}
</select>
</div>
<div>
<label className="block text-sm mb-1 opacity-70">Wochentag</label>
<select value={crud.form.day_of_week} onChange={e => crud.setForm({ ...crud.form, day_of_week: parseInt(e.target.value) })} className={`w-full px-3 py-2 rounded-lg border ${styles.inputClass}`}>
{DAYS.map(d => <option key={d.v} value={d.v}>{d.label}</option>)}
</select>
</div>
<div>
<label className="block text-sm mb-1 opacity-70">Von</label>
<input type="time" required value={crud.form.start_time} onChange={e => crud.setForm({ ...crud.form, start_time: e.target.value })} className={`w-full px-3 py-2 rounded-lg border ${styles.inputClass}`} />
</div>
<div>
<label className="block text-sm mb-1 opacity-70">Bis</label>
<input type="time" required value={crud.form.end_time} onChange={e => crud.setForm({ ...crud.form, end_time: e.target.value })} className={`w-full px-3 py-2 rounded-lg border ${styles.inputClass}`} />
</div>
<div className="flex items-center gap-2">
<input type="checkbox" id="is_hard_w" checked={crud.form.is_hard} onChange={e => crud.setForm({ ...crud.form, is_hard: e.target.checked })} className="w-5 h-5" />
<label htmlFor="is_hard_w" className="text-sm">Harte Regel</label>
</div>
<div>
<label className="block text-sm mb-1 opacity-70">Weight (0-100)</label>
<input type="number" min={0} max={100} value={crud.form.weight} onChange={e => crud.setForm({ ...crud.form, weight: parseInt(e.target.value) || 0 })} className={`w-full px-3 py-2 rounded-lg border ${styles.inputClass}`} />
</div>
<div className="md:col-span-2 flex items-end">
<button type="submit" disabled={crud.submitting} className={styles.submitBtn}>
{crud.submitting ? 'Speichert...' : 'Anlegen'}
</button>
</div>
</div>
<div className="mt-3">
<label className="block text-sm mb-1 opacity-70">Begruendung (optional)</label>
<input value={crud.form.note || ''} onChange={e => crud.setForm({ ...crud.form, note: e.target.value })} className={`w-full px-3 py-2 rounded-lg border ${styles.inputClass}`} />
</div>
</>
}
renderRow={(item) => {
const c = item as TeacherUnavailableWindow
return (
<tr key={c.id} className={styles.rowClass}>
<td className="px-4 py-3 font-medium">{teacherLabel(c.teacher_id)}</td>
<td className="px-4 py-3">{dayLabel(c.day_of_week)}</td>
<td className="px-4 py-3">{c.start_time}{c.end_time}</td>
<td className="px-4 py-3">{c.is_hard ? '✓' : '—'}</td>
<td className="px-4 py-3 text-sm opacity-70">{c.note || '—'}</td>
<td className="px-4 py-3 text-right">
<button onClick={() => crud.remove(c.id)} className={styles.deleteBtn}>Loeschen</button>
</td>
</tr>
)
}}
/>
)
}