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
+16
View File
@@ -3,6 +3,7 @@ import {
Controller,
Get,
Param,
Patch,
Post,
Query,
Req,
@@ -14,6 +15,7 @@ import { Response } from 'express';
import { WahlService } from './wahl.service';
import { ZuteilungService } from './zuteilung.service';
import { CreateWahlDto } from './dto/create-wahl.dto';
import { UpdateWahlDto } from './dto/update-wahl.dto';
import { CreateWorkshopDto } from './dto/create-workshop.dto';
import { SubmitTeilnehmerDto } from './dto/submit-teilnehmer.dto';
import { CreateForceZuteilungDto } from './dto/create-force-zuteilung.dto';
@@ -45,6 +47,20 @@ export class WahlController {
return this.wahl.listWahlen(kcId);
}
@Patch(':wahlId')
@UseGuards(AuthGuard(['authentik', 'team']), RolesGuard)
@Roles(Role.LEITUNGSTEAM)
updateWahl(@Param('wahlId') wahlId: string, @Body() dto: UpdateWahlDto) {
return this.wahl.updateWahl(wahlId, { isOpen: dto.isOpen });
}
@Get(':wahlId/teilnehmer')
@UseGuards(AuthGuard(['authentik', 'team']), RolesGuard)
@Roles(Role.LEITUNGSTEAM)
listTeilnehmer(@Param('wahlId') wahlId: string) {
return this.wahl.listTeilnehmer(wahlId);
}
@Post(':wahlId/workshops')
@UseGuards(AuthGuard(['authentik', 'team']), RolesGuard)
@Roles(Role.LEITUNGSTEAM)