feat: LT Wahl controls (open/close, Force-Zuteilung, CSV) + file upload
Backend: - PATCH /api/wahl/:wahlId (isOpen) to open/close a Wahl. - GET /api/wahl/:wahlId/teilnehmer (LT): participants with their priorities and any existing Force-Zuteilung. - Verified against local Postgres: PATCH toggles isOpen, teilnehmer list returns, CSV export works. (A stale dev server on :3000 masked this at first — real routes are fine.) Client (client/app/): - Wahl detail: open/close switch, participant list with a "Zuteilen" (Force-Zuteilung) action, CSV export via a browser download (browser.downloadText). - files_admin_screen.dart: LT file upload — browser.pickFile() + visibility picker -> multipart POST /api/files/:kcId; list existing files. Reachable from KcDetailScreen. - browser_web.dart gains pickFile()/downloadText() (native <input file> + Blob), with throwing stubs for the VM. - Dropped the file_picker package again (heavy transitive deps, and the native web input is enough); disk on this box is nearly full. flutter analyze/test/build web green; backend npm test 56. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../api.dart';
|
||||
import '../browser.dart' as browser;
|
||||
import '../main.dart';
|
||||
import 'ui.dart';
|
||||
|
||||
/// LT file management for one KC: upload with a visibility tier + list.
|
||||
class FilesAdminScreen extends StatefulWidget {
|
||||
const FilesAdminScreen({super.key, required this.kcId});
|
||||
final String kcId;
|
||||
|
||||
@override
|
||||
State<FilesAdminScreen> createState() => _FilesAdminScreenState();
|
||||
}
|
||||
|
||||
class _FilesAdminScreenState extends State<FilesAdminScreen> {
|
||||
Future<List<FileEntry>>? _files;
|
||||
bool _uploading = false;
|
||||
Api get _api => AppScope.of(context).api;
|
||||
|
||||
static const _visibilities = {
|
||||
'ALLE': 'Alle (inkl. Konfis)',
|
||||
'ALLE_AUSSER_KONFIS': 'Team (ohne Konfis)',
|
||||
'NUR_LT': 'Nur Leitungsteam',
|
||||
};
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_files ??= _api.files(widget.kcId);
|
||||
}
|
||||
|
||||
void _reload() => setState(() => _files = _api.files(widget.kcId));
|
||||
|
||||
Future<void> _upload() async {
|
||||
final api = _api;
|
||||
final picked = await browser.pickFile();
|
||||
if (picked == null || !mounted) return;
|
||||
final visibility = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (_) => SimpleDialog(
|
||||
title: Text('Sichtbarkeit für „${picked.name}“'),
|
||||
children: [
|
||||
for (final e in _visibilities.entries)
|
||||
SimpleDialogOption(
|
||||
onPressed: () => Navigator.of(context).pop(e.key),
|
||||
child: Text(e.value),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (visibility == null || !mounted) return;
|
||||
setState(() => _uploading = true);
|
||||
try {
|
||||
await api.uploadFile(widget.kcId, picked.name, picked.bytes, visibility);
|
||||
if (mounted) _reload();
|
||||
} catch (e) {
|
||||
if (mounted) toast(context, '$e');
|
||||
} finally {
|
||||
if (mounted) setState(() => _uploading = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Dateien (LT)')),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: _uploading ? null : _upload,
|
||||
icon: _uploading
|
||||
? const SizedBox(
|
||||
height: 18, width: 18, child: CircularProgressIndicator(strokeWidth: 2))
|
||||
: const Icon(Icons.upload_file),
|
||||
label: const Text('Hochladen'),
|
||||
),
|
||||
body: FutureBuilder<List<FileEntry>>(
|
||||
future: _files,
|
||||
builder: (context, snap) {
|
||||
if (snap.connectionState != ConnectionState.done) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (snap.hasError) return ErrorText('${snap.error}', onRetry: _reload);
|
||||
final files = snap.data!;
|
||||
if (files.isEmpty) {
|
||||
return const Center(child: Text('Noch keine Dateien.'));
|
||||
}
|
||||
return ListView(
|
||||
children: [
|
||||
for (final f in files)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.insert_drive_file_outlined),
|
||||
title: Text(f.filename),
|
||||
subtitle: Text(_visibilities[f.visibility] ?? f.visibility),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user