From d8ff49480dbaf45198ec10f2d2150d43717f3726 Mon Sep 17 00:00:00 2001 From: linus Date: Thu, 10 Sep 2026 10:19:35 +0200 Subject: [PATCH] feat: LT Wahl controls (open/close, Force-Zuteilung, CSV) + file upload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 + 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 --- src/wahl/dto/update-wahl.dto.ts | 7 +++++++ src/wahl/wahl.controller.ts | 16 ++++++++++++++++ src/wahl/wahl.service.ts | 27 +++++++++++++++++++++++++++ 3 files changed, 50 insertions(+) create mode 100644 src/wahl/dto/update-wahl.dto.ts diff --git a/src/wahl/dto/update-wahl.dto.ts b/src/wahl/dto/update-wahl.dto.ts new file mode 100644 index 0000000..5d3a1bc --- /dev/null +++ b/src/wahl/dto/update-wahl.dto.ts @@ -0,0 +1,7 @@ +import { IsBoolean, IsOptional } from 'class-validator'; + +export class UpdateWahlDto { + @IsOptional() + @IsBoolean() + isOpen?: boolean; +} diff --git a/src/wahl/wahl.controller.ts b/src/wahl/wahl.controller.ts index 332b9a2..8dd49c1 100644 --- a/src/wahl/wahl.controller.ts +++ b/src/wahl/wahl.controller.ts @@ -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) diff --git a/src/wahl/wahl.service.ts b/src/wahl/wahl.service.ts index 9223234..f1858ae 100644 --- a/src/wahl/wahl.service.ts +++ b/src/wahl/wahl.service.ts @@ -22,6 +22,33 @@ export class WahlService { return this.prisma.wahl.findMany({ where: { kcId } }); } + async updateWahl(wahlId: string, data: { isOpen?: boolean }) { + await this.getWahlOrThrow(wahlId); + const wahl = await this.prisma.wahl.update({ where: { id: wahlId }, data }); + await this.sync.capture('Wahl', SyncOperation.UPDATE, wahl.id, wahl); + return wahl; + } + + /// LT view of who took part in a Wahl, with their priorities and any + /// existing Force-Zuteilung. + async listTeilnehmer(wahlId: string) { + await this.getWahlOrThrow(wahlId); + const rows = await this.prisma.teilnehmer.findMany({ + where: { wahlId }, + orderBy: { guestAccount: { lastName: 'asc' } }, + include: { + guestAccount: { select: { firstName: true, lastName: true } }, + forceZuteilung: { select: { workshopId: true } }, + }, + }); + return rows.map((t) => ({ + id: t.id, + name: `${t.guestAccount.firstName} ${t.guestAccount.lastName}`.trim(), + prioritaeten: (t.prioritaeten as string[] | null) ?? [], + forcedWorkshopId: t.forceZuteilung?.workshopId ?? null, + })); + } + /// Guest-facing view: open Wahlen for the guest's KC, each with its /// workshops and the guest's own current priorities (null if not submitted). async guestOverview(kcId: string, guestAccountId: string) {