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>
103 lines
3.1 KiB
Dart
103 lines
3.1 KiB
Dart
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),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|