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
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>
155 lines
7.9 KiB
TypeScript
155 lines
7.9 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { roomRequiresTypeApi, roomUnavailableApi, subjectsApi, roomsApi } from '@/lib/stundenplan/api'
|
|
import type {
|
|
RoomRequiresType, RoomUnavailable,
|
|
TimetableSubject, TimetableRoom,
|
|
} from '@/app/stundenplan/types'
|
|
import { useConstraintCrud, ConstraintShell, useShellStyles, DAYS, dayLabel } from './_shell'
|
|
|
|
// ---------- Room Requires Type (Subject → required room_type) ----------
|
|
|
|
type ReqForm = Omit<RoomRequiresType, 'id' | 'created_by_user_id' | 'created_at'>
|
|
const initialReq: ReqForm = { subject_id: '', room_type: '', is_hard: true, weight: 100, active: true, note: '' }
|
|
|
|
export function RoomRequiresTypeEditor() {
|
|
const styles = useShellStyles()
|
|
const [subjects, setSubjects] = useState<TimetableSubject[]>([])
|
|
const crud = useConstraintCrud<RoomRequiresType, ReqForm>(roomRequiresTypeApi, initialReq)
|
|
|
|
useEffect(() => { subjectsApi.list().then(setSubjects).catch(() => setSubjects([])) }, [])
|
|
|
|
const sLabel = (id: string): string => {
|
|
const s = subjects.find(x => x.id === id); return s ? s.name : id.slice(0, 8) + '…'
|
|
}
|
|
|
|
return (
|
|
<ConstraintShell
|
|
testId="room-requires-type-editor"
|
|
title="Fach: benoetigter Raumtyp"
|
|
description={"Beispiel: Sport braucht immer Sporthalle."}
|
|
newLabel="+ Neue Regel"
|
|
newDisabled={subjects.length === 0}
|
|
prereqWarning={subjects.length === 0 ? 'Zuerst Faecher anlegen.' : null}
|
|
emptyText="Keine Regeln vorhanden."
|
|
tableHeaders={['Fach', 'Raumtyp', '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">Raumtyp</label>
|
|
<input required value={crud.form.room_type} onChange={e => crud.setForm({ ...crud.form, room_type: e.target.value })} placeholder="z.B. Sporthalle" 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_rrt" 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_rrt" className="text-sm">Harte Regel</label>
|
|
</div>
|
|
<div className="md:col-span-4 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 RoomRequiresType
|
|
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">{c.room_type}</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>
|
|
)
|
|
}}
|
|
/>
|
|
)
|
|
}
|
|
|
|
// ---------- Room Unavailable ----------
|
|
|
|
type UnavForm = Omit<RoomUnavailable, 'id' | 'created_by_user_id' | 'created_at'>
|
|
const initialUnav: UnavForm = { room_id: '', day_of_week: 1, period_index: 1, is_hard: true, weight: 100, active: true, note: '' }
|
|
|
|
export function RoomUnavailableEditor() {
|
|
const styles = useShellStyles()
|
|
const [rooms, setRooms] = useState<TimetableRoom[]>([])
|
|
const crud = useConstraintCrud<RoomUnavailable, UnavForm>(roomUnavailableApi, initialUnav)
|
|
|
|
useEffect(() => { roomsApi.list().then(setRooms).catch(() => setRooms([])) }, [])
|
|
|
|
const rLabel = (id: string): string => {
|
|
const r = rooms.find(x => x.id === id); return r ? r.name : id.slice(0, 8) + '…'
|
|
}
|
|
|
|
return (
|
|
<ConstraintShell
|
|
testId="room-unavailable-editor"
|
|
title="Raum: nicht verfuegbar"
|
|
description={"Beispiel: Sporthalle Di 5. Stunde Wartung."}
|
|
newLabel="+ Neue Regel"
|
|
newDisabled={rooms.length === 0}
|
|
prereqWarning={rooms.length === 0 ? 'Zuerst Raeume anlegen.' : null}
|
|
emptyText="Keine Regeln vorhanden."
|
|
tableHeaders={['Raum', 'Tag', 'Stunde', '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">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>
|
|
<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">Stunde (1-12)</label>
|
|
<input type="number" min={1} max={12} required value={crud.form.period_index} onChange={e => crud.setForm({ ...crud.form, period_index: 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_ru" 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_ru" className="text-sm">Harte Regel</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 RoomUnavailable
|
|
return (
|
|
<tr key={c.id} className={styles.rowClass}>
|
|
<td className="px-4 py-3 font-medium">{rLabel(c.room_id)}</td>
|
|
<td className="px-4 py-3">{dayLabel(c.day_of_week)}</td>
|
|
<td className="px-4 py-3">{c.period_index}.</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>
|
|
)
|
|
}}
|
|
/>
|
|
)
|
|
}
|