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:
2026-09-10 10:19:35 +02:00
co-authored by Claude Sonnet 5
parent 73ff55643f
commit a4ae7549ae
10 changed files with 384 additions and 4 deletions
+41
View File
@@ -1,3 +1,6 @@
import 'dart:async';
import 'dart:js_interop';
import 'package:web/web.dart' as web;
/// Per-tab storage for the PKCE verifier + CSRF state (cleared when the tab
@@ -20,3 +23,41 @@ Map<String, String> currentQueryParameters() =>
void clearQuery() {
web.window.history.replaceState(null, '', '/');
}
/// Opens the OS file picker and reads the chosen file's bytes.
Future<({String name, List<int> bytes})?> pickFile() {
final completer = Completer<({String name, List<int> bytes})?>();
final input = web.HTMLInputElement()..type = 'file';
input.onchange = ((web.Event _) {
final files = input.files;
if (files == null || files.length == 0) {
completer.complete(null);
return;
}
final file = files.item(0)!;
final reader = web.FileReader();
reader.onload = ((web.Event _) {
final buffer = (reader.result as JSArrayBuffer).toDart;
completer.complete((name: file.name, bytes: buffer.asUint8List()));
}).toJS;
reader.onerror = ((web.Event _) => completer.complete(null)).toJS;
reader.readAsArrayBuffer(file);
}).toJS;
input.click();
return completer.future;
}
/// Triggers a browser download of an in-memory string (e.g. the CSV export).
void downloadText(
String filename,
String content, {
String mime = 'text/csv;charset=utf-8',
}) {
final blob = web.Blob([content.toJS].toJS, web.BlobPropertyBag(type: mime));
final url = web.URL.createObjectURL(blob);
web.HTMLAnchorElement()
..href = url
..download = filename
..click();
web.URL.revokeObjectURL(url);
}