Files
breakpilot-lehrer/studio-v2/app/stundenplan/_components/regeln/SubjectPreferredPeriodEditor.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

88 lines
4.5 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 { subjectPreferredPeriodApi, subjectsApi } from '@/lib/stundenplan/api'
import type { SubjectPreferredPeriod, TimetableSubject } from '@/app/stundenplan/types'
import { useConstraintCrud, ConstraintShell, useShellStyles } from './_shell'
type FormState = Omit<SubjectPreferredPeriod, 'id' | 'created_by_user_id' | 'created_at'>
const initialForm: FormState = {
subject_id: '', period_from: 1, period_to: 4, is_hard: false, weight: 40, active: true, note: '',
}
export function SubjectPreferredPeriodEditor() {
const styles = useShellStyles()
const crud = useConstraintCrud<SubjectPreferredPeriod, FormState>(subjectPreferredPeriodApi, initialForm, {
onBeforeSubmit: (f) => f.period_to < f.period_from ? '"Bis"-Stunde darf nicht kleiner sein als "Von"-Stunde.' : null,
})
const [subjects, setSubjects] = useState<TimetableSubject[]>([])
useEffect(() => { subjectsApi.list().then(setSubjects).catch(() => setSubjects([])) }, [])
const sLabel = (id: string): string => {
const s = subjects.find(x => x.id === id)
return s ? `${s.name} (${s.short_code})` : id.slice(0, 8) + '…'
}
return (
<ConstraintShell
testId="subject-preferred-period-editor"
title="Fach: Bevorzugter Stunden-Bereich"
description={"Beispiel: Hauptfaecher lieber in den ersten 4 Stunden (Soft-Regel)."}
newLabel="+ Neue Regel"
newDisabled={subjects.length === 0}
prereqWarning={subjects.length === 0 ? 'Zuerst Faecher anlegen.' : null}
emptyText="Keine Regeln vorhanden."
tableHeaders={['Fach', 'Bereich', '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">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>
<label className="block text-sm mb-1 opacity-70">Von Stunde</label>
<input type="number" min={1} max={12} required value={crud.form.period_from} onChange={e => crud.setForm({ ...crud.form, period_from: parseInt(e.target.value) || 1 })} className={`w-full px-3 py-2 rounded-lg border ${styles.inputClass}`} />
</div>
<div>
<label className="block text-sm mb-1 opacity-70">Bis Stunde</label>
<input type="number" min={1} max={12} required value={crud.form.period_to} onChange={e => crud.setForm({ ...crud.form, period_to: parseInt(e.target.value) || 1 })} className={`w-full px-3 py-2 rounded-lg border ${styles.inputClass}`} />
</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="flex items-center gap-2">
<input type="checkbox" id="is_hard_pp" 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_pp" className="text-sm">Harte Regel (selten sinnvoll hier)</label>
</div>
<div className="md:col-span-3 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 SubjectPreferredPeriod
return (
<tr key={c.id} className={styles.rowClass}>
<td className="px-4 py-3 font-medium">{sLabel(c.subject_id)}</td>
<td className="px-4 py-3">Stunde {c.period_from}{c.period_to}</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>
)
}}
/>
)
}