Files
breakpilot-lehrer/studio-v2/app/stundenplan/_components/regeln/TeacherExclusionEditors.tsx
T
Benjamin Admin 082a5bb68c
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 31s
CI / test-python-klausur (push) Failing after 2m35s
CI / test-python-agent-core (push) Successful in 20s
CI / test-nodejs-website (push) Successful in 21s
Strip orphan straight-quote pairings in JSX descriptions
The German „X" markers in the description prop combined a curly „
(U+201E) with a straight " (U+0022). The straight quote prematurely
terminated the JavaScript string inside the JSX expression. Removing
both markers around the example text keeps the description readable
and unambiguously valid JSX.

Test selector for the UnavailableWindow description updated to match
the new wording.

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

165 lines
7.8 KiB
TypeScript

'use client'
import { useState, useEffect } from 'react'
import {
teacherExcludedSubjectApi, teacherExcludedRoomApi,
teachersApi, subjectsApi, roomsApi,
} from '@/lib/stundenplan/api'
import type {
TeacherExcludedSubject, TeacherExcludedRoom,
TimetableTeacher, TimetableSubject, TimetableRoom,
} from '@/app/stundenplan/types'
import { useConstraintCrud, ConstraintShell, useShellStyles } from './_shell'
// ---------- Excluded Subject ----------
type SubForm = Omit<TeacherExcludedSubject, 'id' | 'created_by_user_id' | 'created_at'>
const initialSub: SubForm = { teacher_id: '', subject_id: '', is_hard: true, weight: 100, active: true, note: '' }
export function TeacherExcludedSubjectEditor() {
const styles = useShellStyles()
const [teachers, setTeachers] = useState<TimetableTeacher[]>([])
const [subjects, setSubjects] = useState<TimetableSubject[]>([])
const crud = useConstraintCrud<TeacherExcludedSubject, SubForm>(teacherExcludedSubjectApi, initialSub)
useEffect(() => {
teachersApi.list().then(setTeachers).catch(() => setTeachers([]))
subjectsApi.list().then(setSubjects).catch(() => setSubjects([]))
}, [])
const tLabel = (id: string): string => {
const t = teachers.find(x => x.id === id); return t ? `${t.last_name}, ${t.first_name}` : id.slice(0, 8) + '…'
}
const sLabel = (id: string): string => {
const s = subjects.find(x => x.id === id); return s ? s.name : id.slice(0, 8) + '…'
}
const missing = teachers.length === 0 || subjects.length === 0
return (
<ConstraintShell
testId="teacher-excluded-subject-editor"
title="Lehrer: Fach ausgeschlossen"
description={"Beispiel: Anna darf kein Sport unterrichten (oft Pflicht, z.B. Qualifikation/Behinderung)."}
newLabel="+ Neue Regel"
newDisabled={missing}
prereqWarning={missing ? 'Zuerst Lehrer und Faecher anlegen.' : null}
emptyText="Keine Regeln vorhanden."
tableHeaders={['Lehrer', 'Fach', 'Hart', 'Weight']}
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">Fach</label>
<select required value={crud.form.subject_id} onChange={e => crud.setForm({ ...crud.form, subject_id: e.target.value })} className={`w-full px-3 py-2 rounded-lg border ${styles.inputClass}`}>
<option value=""> bitte waehlen </option>
{subjects.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
</select>
</div>
<div className="flex items-center gap-2">
<input type="checkbox" id="is_hard_tes" 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_tes" className="text-sm">Harte Regel</label>
</div>
<div className="flex items-end">
<button type="submit" disabled={crud.submitting} className={styles.submitBtn}>{crud.submitting ? 'Speichert...' : 'Anlegen'}</button>
</div>
</div>
}
renderRow={(item) => {
const c = item as TeacherExcludedSubject
return (
<tr key={c.id} className={styles.rowClass}>
<td className="px-4 py-3 font-medium">{tLabel(c.teacher_id)}</td>
<td className="px-4 py-3">{sLabel(c.subject_id)}</td>
<td className="px-4 py-3">{c.is_hard ? '✓' : '—'}</td>
<td className="px-4 py-3">{c.weight}</td>
<td className="px-4 py-3 text-right"><button onClick={() => crud.remove(c.id)} className={styles.deleteBtn}>Loeschen</button></td>
</tr>
)
}}
/>
)
}
// ---------- Excluded Room ----------
type RoomForm = Omit<TeacherExcludedRoom, 'id' | 'created_by_user_id' | 'created_at'>
const initialRoom: RoomForm = { teacher_id: '', room_id: '', is_hard: true, weight: 100, active: true, note: '' }
export function TeacherExcludedRoomEditor() {
const styles = useShellStyles()
const [teachers, setTeachers] = useState<TimetableTeacher[]>([])
const [rooms, setRooms] = useState<TimetableRoom[]>([])
const crud = useConstraintCrud<TeacherExcludedRoom, RoomForm>(teacherExcludedRoomApi, initialRoom)
useEffect(() => {
teachersApi.list().then(setTeachers).catch(() => setTeachers([]))
roomsApi.list().then(setRooms).catch(() => setRooms([]))
}, [])
const tLabel = (id: string): string => {
const t = teachers.find(x => x.id === id); return t ? `${t.last_name}, ${t.first_name}` : id.slice(0, 8) + '…'
}
const rLabel = (id: string): string => {
const r = rooms.find(x => x.id === id); return r ? r.name : id.slice(0, 8) + '…'
}
const missing = teachers.length === 0 || rooms.length === 0
return (
<ConstraintShell
testId="teacher-excluded-room-editor"
title="Lehrer: Raum ausgeschlossen"
description={"Beispiel: Anna im Rollstuhl, Raum F kein Fahrstuhl."}
newLabel="+ Neue Regel"
newDisabled={missing}
prereqWarning={missing ? 'Zuerst Lehrer und Raeume anlegen.' : null}
emptyText="Keine Regeln vorhanden."
tableHeaders={['Lehrer', 'Raum', 'Hart', 'Weight']}
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">Raum</label>
<select required value={crud.form.room_id} onChange={e => crud.setForm({ ...crud.form, room_id: e.target.value })} className={`w-full px-3 py-2 rounded-lg border ${styles.inputClass}`}>
<option value=""> bitte waehlen </option>
{rooms.map(r => <option key={r.id} value={r.id}>{r.name}</option>)}
</select>
</div>
<div className="flex items-center gap-2">
<input type="checkbox" id="is_hard_ter" 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_ter" className="text-sm">Harte Regel</label>
</div>
<div className="flex items-end">
<button type="submit" disabled={crud.submitting} className={styles.submitBtn}>{crud.submitting ? 'Speichert...' : 'Anlegen'}</button>
</div>
</div>
}
renderRow={(item) => {
const c = item as TeacherExcludedRoom
return (
<tr key={c.id} className={styles.rowClass}>
<td className="px-4 py-3 font-medium">{tLabel(c.teacher_id)}</td>
<td className="px-4 py-3">{rLabel(c.room_id)}</td>
<td className="px-4 py-3">{c.is_hard ? '✓' : '—'}</td>
<td className="px-4 py-3">{c.weight}</td>
<td className="px-4 py-3 text-right"><button onClick={() => crud.remove(c.id)} className={styles.deleteBtn}>Loeschen</button></td>
</tr>
)
}}
/>
)
}