import 'package:flutter/material.dart'; import '../api.dart'; import '../browser.dart' as browser; import '../main.dart'; import 'ui.dart'; /// LT: manage the Wahlen of one KC — create, add workshops, run the /// assignment algorithm, view the result. class WahlAdminScreen extends StatefulWidget { const WahlAdminScreen({super.key, required this.kcId}); final String kcId; @override State createState() => _WahlAdminScreenState(); } class _WahlAdminScreenState extends State { Future>? _future; Api get _api => AppScope.of(context).api; @override void didChangeDependencies() { super.didChangeDependencies(); _future ??= _api.wahlenForKc(widget.kcId); } void _reload() => setState(() => _future = _api.wahlenForKc(widget.kcId)); Future _create() async { final api = _api; final v = await showDialog<(String, String, String)>( context: context, builder: (_) => const _NewWahlDialog(), ); if (v == null || !mounted) return; try { await api.createWahl(widget.kcId, v.$1, v.$2, v.$3); if (mounted) _reload(); } catch (e) { if (mounted) toast(context, '$e'); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Wahlen')), floatingActionButton: FloatingActionButton.extended( onPressed: _create, icon: const Icon(Icons.add), label: const Text('Wahl'), ), body: FutureBuilder>( future: _future, builder: (context, snap) { if (snap.connectionState != ConnectionState.done) { return const Center(child: CircularProgressIndicator()); } if (snap.hasError) return ErrorText('${snap.error}', onRetry: _reload); final wahlen = snap.data!; if (wahlen.isEmpty) { return const Center(child: Text('Noch keine Wahlen. Unten anlegen.')); } return ListView( children: [ for (final w in wahlen) ListTile( leading: Icon(w.isOpen ? Icons.lock_open : Icons.lock), title: Text(w.name), subtitle: Text('${w.datumsSchluessel} · Teil ${w.teil}'), trailing: const Icon(Icons.chevron_right), onTap: () => Navigator.of(context).push( MaterialPageRoute(builder: (_) => WahlDetailScreen(wahl: w)), ), ), ], ); }, ), ); } } class WahlDetailScreen extends StatefulWidget { const WahlDetailScreen({super.key, required this.wahl}); final WahlAdmin wahl; @override State createState() => _WahlDetailScreenState(); } class _WahlDetailScreenState extends State { Future>? _workshops; Future>? _results; Future>? _teilnehmer; late bool _isOpen = widget.wahl.isOpen; List _workshopCache = const []; bool _running = false; Api get _api => AppScope.of(context).api; @override void didChangeDependencies() { super.didChangeDependencies(); _workshops ??= _api.workshopsForWahl(widget.wahl.id).then((w) { _workshopCache = w; return w; }); _results ??= _api.zuteilungResults(widget.wahl.id); _teilnehmer ??= _api.wahlTeilnehmer(widget.wahl.id); } void _reloadWorkshops() => setState(() { _workshops = _api.workshopsForWahl(widget.wahl.id).then((w) { _workshopCache = w; return w; }); }); void _reloadResults() => setState(() => _results = _api.zuteilungResults(widget.wahl.id)); void _reloadTeilnehmer() => setState(() => _teilnehmer = _api.wahlTeilnehmer(widget.wahl.id)); Future _toggleOpen(bool value) async { final api = _api; setState(() => _isOpen = value); try { await api.setWahlOpen(widget.wahl.id, value); } catch (e) { if (mounted) { setState(() => _isOpen = !value); toast(context, '$e'); } } } Future _exportCsv() async { final api = _api; try { final csv = await api.zuteilungCsv(widget.wahl.id); browser.downloadText('zuteilung-${widget.wahl.name}.csv', csv); } catch (e) { if (mounted) toast(context, '$e'); } } Future _forceFor(TeilnehmerRow t) async { final api = _api; final workshopId = await showDialog( context: context, builder: (_) => SimpleDialog( title: Text('Zuteilung für ${t.name}'), children: [ for (final w in _workshopCache) SimpleDialogOption( onPressed: () => Navigator.of(context).pop(w.id), child: Text(w.name), ), ], ), ); if (workshopId == null || !mounted) return; try { await api.forceZuteilung(widget.wahl.id, t.id, workshopId); if (mounted) _reloadTeilnehmer(); } catch (e) { if (mounted) toast(context, '$e'); } } Future _addWorkshop() async { final api = _api; final v = await showDialog<(String, int, int)>( context: context, builder: (_) => const _NewWorkshopDialog(), ); if (v == null || !mounted) return; try { await api.createWorkshop(widget.wahl.id, v.$1, v.$2, v.$3); if (mounted) _reloadWorkshops(); } catch (e) { if (mounted) toast(context, '$e'); } } Future _run() async { final api = _api; setState(() => _running = true); try { await api.runZuteilung(widget.wahl.id); if (mounted) _reloadResults(); } catch (e) { if (mounted) toast(context, '$e'); } finally { if (mounted) setState(() => _running = false); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(widget.wahl.name)), body: ListView( padding: const EdgeInsets.all(16), children: [ Card( child: SwitchListTile( title: const Text('Wahl geöffnet'), subtitle: Text(_isOpen ? 'Konfis können Wünsche abgeben' : 'Geschlossen — keine neuen Einreichungen'), value: _isOpen, onChanged: _toggleOpen, ), ), const SizedBox(height: 8), SectionHeader('Workshops', action: TextButton.icon( onPressed: _addWorkshop, icon: const Icon(Icons.add), label: const Text('Hinzufügen'), )), FutureBuilder>( future: _workshops, builder: (context, snap) { if (snap.connectionState != ConnectionState.done) { return const LinearProgressIndicator(); } if (snap.hasError) return Text('Fehler: ${snap.error}'); final ws = snap.data!; if (ws.isEmpty) return const Text('Noch keine Workshops.'); return Column( children: [ for (final w in ws) ListTile( dense: true, leading: const Icon(Icons.groups), title: Text(w.name), subtitle: Text('Kapazität ${w.kapazitaet} · min. ${w.minTeilnehmer}'), ), ], ); }, ), const Divider(height: 40), SectionHeader('Teilnehmer:innen'), const SizedBox(height: 4), FutureBuilder>( future: _teilnehmer, builder: (context, snap) { if (snap.connectionState != ConnectionState.done) { return const LinearProgressIndicator(); } if (snap.hasError) return Text('Fehler: ${snap.error}'); final rows = snap.data!; if (rows.isEmpty) return const Text('Noch keine Einreichungen.'); return Column( children: [ for (final t in rows) ListTile( dense: true, leading: const Icon(Icons.person), title: Text(t.name), subtitle: Text('Wünsche: ${t.prioritaeten.length}' '${t.forcedWorkshopId != null ? ' · fest zugeteilt' : ''}'), trailing: TextButton( onPressed: () => _forceFor(t), child: const Text('Zuteilen'), ), ), ], ); }, ), const Divider(height: 40), Row( children: [ Expanded( child: Text('Zuteilung', style: Theme.of(context).textTheme.titleMedium), ), IconButton( tooltip: 'CSV exportieren', onPressed: _exportCsv, icon: const Icon(Icons.download), ), FilledButton.icon( onPressed: _running ? null : _run, icon: _running ? const SizedBox( height: 16, width: 16, child: CircularProgressIndicator(strokeWidth: 2)) : const Icon(Icons.play_arrow), label: const Text('Ausführen'), ), ], ), const SizedBox(height: 8), FutureBuilder>( future: _results, builder: (context, snap) { if (snap.connectionState != ConnectionState.done) { return const LinearProgressIndicator(); } if (snap.hasError) return Text('Fehler: ${snap.error}'); final rows = snap.data!; if (rows.isEmpty) { return const Text('Noch keine Zuteilung berechnet.'); } return Column( children: [ for (final r in rows) ListTile( dense: true, title: Text(r.name), subtitle: Text(r.workshopName ?? 'UNZUGETEILT'), trailing: Text( r.isForced ? 'fest' : r.wunschRang > 0 ? 'Wunsch ${r.wunschRang}' : '—', ), ), ], ); }, ), ], ), ); } } class _NewWahlDialog extends StatefulWidget { const _NewWahlDialog(); @override State<_NewWahlDialog> createState() => _NewWahlDialogState(); } class _NewWahlDialogState extends State<_NewWahlDialog> { final _name = TextEditingController(); final _datum = TextEditingController(); final _teil = TextEditingController(text: '1'); @override Widget build(BuildContext context) { return AlertDialog( title: const Text('Neue Wahl'), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField(controller: _name, decoration: const InputDecoration(labelText: 'Name')), TextField( controller: _datum, decoration: const InputDecoration(labelText: 'Datumsschlüssel (z. B. 2026-06-13)')), TextField(controller: _teil, decoration: const InputDecoration(labelText: 'Teil')), ], ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Abbrechen')), FilledButton( onPressed: () => Navigator.of(context).pop( (_name.text.trim(), _datum.text.trim(), _teil.text.trim()), ), child: const Text('Anlegen'), ), ], ); } } class _NewWorkshopDialog extends StatefulWidget { const _NewWorkshopDialog(); @override State<_NewWorkshopDialog> createState() => _NewWorkshopDialogState(); } class _NewWorkshopDialogState extends State<_NewWorkshopDialog> { final _name = TextEditingController(); final _kap = TextEditingController(text: '12'); final _min = TextEditingController(text: '0'); @override Widget build(BuildContext context) { return AlertDialog( title: const Text('Neuer Workshop'), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField(controller: _name, decoration: const InputDecoration(labelText: 'Name')), TextField( controller: _kap, keyboardType: TextInputType.number, decoration: const InputDecoration(labelText: 'Kapazität')), TextField( controller: _min, keyboardType: TextInputType.number, decoration: const InputDecoration(labelText: 'Mindestteilnehmer')), ], ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Abbrechen')), FilledButton( onPressed: () => Navigator.of(context).pop(( _name.text.trim(), int.tryParse(_kap.text) ?? 0, int.tryParse(_min.text) ?? 0, )), child: const Text('Anlegen'), ), ], ); } }