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:
@@ -183,6 +183,26 @@ class WorkshopAdmin {
|
||||
);
|
||||
}
|
||||
|
||||
class TeilnehmerRow {
|
||||
TeilnehmerRow({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.prioritaeten,
|
||||
required this.forcedWorkshopId,
|
||||
});
|
||||
final String id;
|
||||
final String name;
|
||||
final List<String> prioritaeten;
|
||||
final String? forcedWorkshopId;
|
||||
factory TeilnehmerRow.fromJson(Map<String, dynamic> j) => TeilnehmerRow(
|
||||
id: j['id'] as String,
|
||||
name: j['name'] as String? ?? '',
|
||||
prioritaeten:
|
||||
(j['prioritaeten'] as List<dynamic>? ?? []).map((e) => e as String).toList(),
|
||||
forcedWorkshopId: j['forcedWorkshopId'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
class ZuteilungRow {
|
||||
ZuteilungRow({
|
||||
required this.name,
|
||||
@@ -393,6 +413,15 @@ class Api {
|
||||
return _decode(res);
|
||||
}
|
||||
|
||||
Future<dynamic> _patch(String path, Object? body) async {
|
||||
final res = await _client.patch(
|
||||
Uri.parse('$kApiBase$path'),
|
||||
headers: _headers,
|
||||
body: body == null ? null : jsonEncode(body),
|
||||
);
|
||||
return _decode(res);
|
||||
}
|
||||
|
||||
dynamic _decode(http.Response res) {
|
||||
final text = res.body.isEmpty ? '{}' : res.body;
|
||||
dynamic parsed;
|
||||
@@ -547,6 +576,42 @@ class Api {
|
||||
return list.map((e) => ZuteilungRow.fromJson(e as Map<String, dynamic>)).toList();
|
||||
}
|
||||
|
||||
Future<void> setWahlOpen(String wahlId, bool isOpen) =>
|
||||
_patch('/wahl/$wahlId', {'isOpen': isOpen});
|
||||
|
||||
Future<List<TeilnehmerRow>> wahlTeilnehmer(String wahlId) async {
|
||||
final list = await _get('/wahl/$wahlId/teilnehmer') as List<dynamic>;
|
||||
return list.map((e) => TeilnehmerRow.fromJson(e as Map<String, dynamic>)).toList();
|
||||
}
|
||||
|
||||
Future<void> forceZuteilung(String wahlId, String teilnehmerId, String workshopId) =>
|
||||
_post('/wahl/$wahlId/force-zuteilung', {
|
||||
'teilnehmerId': teilnehmerId,
|
||||
'workshopId': workshopId,
|
||||
});
|
||||
|
||||
Future<String> zuteilungCsv(String wahlId) async {
|
||||
final res = await _get('/wahl/$wahlId/zuteilung/csv');
|
||||
return res is String ? res : res.toString();
|
||||
}
|
||||
|
||||
Future<void> uploadFile(
|
||||
String kcId,
|
||||
String filename,
|
||||
List<int> bytes,
|
||||
String visibility,
|
||||
) async {
|
||||
final req = http.MultipartRequest('POST', Uri.parse('$kApiBase/files/$kcId'))
|
||||
..fields['visibility'] = visibility
|
||||
..files.add(http.MultipartFile.fromBytes('file', bytes, filename: filename));
|
||||
if (token != null) req.headers['Authorization'] = 'Bearer $token';
|
||||
final streamed = await req.send();
|
||||
final res = await http.Response.fromStream(streamed);
|
||||
if (res.statusCode < 200 || res.statusCode >= 300) {
|
||||
_decode(res); // throws ApiException with the server message
|
||||
}
|
||||
}
|
||||
|
||||
// --- Teamer administration (LT or the responsible Verantwortliche/r) ---
|
||||
Future<List<TeamerAccount>> teamerFor(String gemeindeId) async {
|
||||
final list = await _get('/gemeinde/$gemeindeId/teamer') as List<dynamic>;
|
||||
|
||||
Reference in New Issue
Block a user