import 'package:flutter/material.dart'; import '../api.dart'; import '../main.dart'; class WahlScreen extends StatefulWidget { const WahlScreen({super.key}); @override State createState() => _WahlScreenState(); } class _WahlScreenState extends State { Future? _future; @override void didChangeDependencies() { super.didChangeDependencies(); _future ??= AppScope.of(context).api.guestWahlOverview(); } void _reload() { setState(() { _future = AppScope.of(context).api.guestWahlOverview(); }); } @override Widget build(BuildContext context) { return DefaultTabController( length: 2, child: Scaffold( appBar: AppBar( title: const Text('Workshop-Wahl'), bottom: const TabBar(tabs: [Tab(text: 'Wünsche'), Tab(text: 'Ergebnis')]), ), body: TabBarView( children: [ FutureBuilder( future: _future, builder: (context, snap) { if (snap.connectionState != ConnectionState.done) { return const Center(child: CircularProgressIndicator()); } if (snap.hasError) { return _ErrorView(message: '${snap.error}', onRetry: _reload); } final data = snap.data!; if (data.wahlen.isEmpty) { return const Center(child: Text('Aktuell ist keine Wahl geöffnet.')); } return ListView( padding: const EdgeInsets.all(16), children: [ Text(data.kcName, style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 8), for (final w in data.wahlen) _WahlCard(wahl: w, onSubmitted: _reload), ], ); }, ), const _ErgebnisTab(), ], ), ), ); } } class _ErgebnisTab extends StatefulWidget { const _ErgebnisTab(); @override State<_ErgebnisTab> createState() => _ErgebnisTabState(); } class _ErgebnisTabState extends State<_ErgebnisTab> { Future>? _future; @override void didChangeDependencies() { super.didChangeDependencies(); _future ??= AppScope.of(context).api.guestWahlResults(); } void _reload() { setState(() => _future = AppScope.of(context).api.guestWahlResults()); } @override Widget build(BuildContext context) { return FutureBuilder>( future: _future, builder: (context, snap) { if (snap.connectionState != ConnectionState.done) { return const Center(child: CircularProgressIndicator()); } if (snap.hasError) { return _ErrorView(message: '${snap.error}', onRetry: _reload); } final results = snap.data!; if (results.isEmpty) { return const Center(child: Text('Noch keine Teilnahme an einer Wahl.')); } return RefreshIndicator( onRefresh: () async => _reload(), child: ListView( padding: const EdgeInsets.all(16), children: [for (final r in results) _ResultCard(result: r)], ), ); }, ); } } class _ResultCard extends StatelessWidget { const _ResultCard({required this.result}); final WahlResult result; @override Widget build(BuildContext context) { final (label, color, detail) = switch (result.status) { WahlResultStatus.assigned => ( result.workshopName ?? 'Zugeteilt', Colors.green, result.isForced ? 'Fest zugeteilt (Leitungsteam)' : 'Wunsch ${result.wunschRang ?? '?'}', ), WahlResultStatus.unassigned => ( 'Kein Platz frei', Theme.of(context).colorScheme.error, 'Bitte beim Leitungsteam melden.', ), WahlResultStatus.pending => ( 'Noch nicht zugeteilt', Theme.of(context).colorScheme.outline, 'Die Zuteilung läuft noch.', ), }; return Card( margin: const EdgeInsets.only(bottom: 12), child: ListTile( leading: Icon(Icons.emoji_events, color: color), title: Text('${result.wahlName} · ${result.datumsSchluessel} Teil ${result.teil}', style: Theme.of(context).textTheme.bodySmall), subtitle: Text(label, style: Theme.of(context).textTheme.titleMedium), trailing: Text(detail, textAlign: TextAlign.end), ), ); } } class _WahlCard extends StatefulWidget { const _WahlCard({required this.wahl, required this.onSubmitted}); final Wahl wahl; final VoidCallback onSubmitted; @override State<_WahlCard> createState() => _WahlCardState(); } class _WahlCardState extends State<_WahlCard> { late final List _picked = [...?widget.wahl.meinePrioritaeten]; bool _busy = false; String? _error; bool _done = false; static const _maxPicks = 3; void _toggle(String workshopId) { setState(() { if (_picked.contains(workshopId)) { _picked.remove(workshopId); } else if (_picked.length < _maxPicks) { _picked.add(workshopId); } _done = false; }); } Future _submit() async { setState(() { _busy = true; _error = null; }); try { await AppScope.of(context).api.submitPrioritaeten(widget.wahl.id, _picked); setState(() => _done = true); widget.onSubmitted(); } on ApiException catch (e) { setState(() => _error = e.message); } finally { if (mounted) setState(() => _busy = false); } } @override Widget build(BuildContext context) { final w = widget.wahl; return Card( margin: const EdgeInsets.only(bottom: 16), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(w.name, style: Theme.of(context).textTheme.titleLarge), Text('${w.datumsSchluessel} · Teil ${w.teil}', style: Theme.of(context).textTheme.bodySmall), const SizedBox(height: 4), Text( 'Tippe deine Wünsche in Reihenfolge an (max. $_maxPicks). ' 'Die Zahl zeigt den Rang.', style: Theme.of(context).textTheme.bodySmall, ), const SizedBox(height: 8), for (final ws in w.workshops) _WorkshopRow( workshop: ws, rank: _picked.indexOf(ws.id), enabled: !_busy, onTap: () => _toggle(ws.id), ), const SizedBox(height: 12), if (_error != null) ...[ Text(_error!, style: TextStyle(color: Theme.of(context).colorScheme.error)), const SizedBox(height: 8), ], Row( children: [ FilledButton( onPressed: (_busy || _picked.isEmpty) ? null : _submit, child: _busy ? const SizedBox( height: 18, width: 18, child: CircularProgressIndicator(strokeWidth: 2)) : const Text('Wünsche absenden'), ), const SizedBox(width: 12), if (_done) Row( children: const [ Icon(Icons.check_circle, color: Colors.green, size: 20), SizedBox(width: 4), Text('Gespeichert'), ], ), ], ), ], ), ), ); } } class _WorkshopRow extends StatelessWidget { const _WorkshopRow({ required this.workshop, required this.rank, required this.enabled, required this.onTap, }); final Workshop workshop; final int rank; final bool enabled; final VoidCallback onTap; @override Widget build(BuildContext context) { final selected = rank >= 0; return ListTile( dense: true, enabled: enabled, onTap: onTap, leading: CircleAvatar( radius: 14, backgroundColor: selected ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.surfaceContainerHighest, child: Text( selected ? '${rank + 1}' : '', style: TextStyle( fontSize: 13, color: selected ? Theme.of(context).colorScheme.onPrimary : null, ), ), ), title: Text(workshop.name), subtitle: Text('Kapazität ${workshop.kapazitaet}'), trailing: Icon(selected ? Icons.check_box : Icons.check_box_outline_blank), ); } } class _ErrorView extends StatelessWidget { const _ErrorView({required this.message, required this.onRetry}); final String message; final VoidCallback onRetry; @override Widget build(BuildContext context) { return Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text(message, textAlign: TextAlign.center), const SizedBox(height: 12), OutlinedButton(onPressed: onRetry, child: const Text('Erneut versuchen')), ], ), ), ); } }