Files
KC-APP-Server/src/wahl/wahl.controller.ts
T
linusandClaude Sonnet 5 a29024407f feat(backend): guest-facing Wahl overview endpoint + dev seed
GET /api/wahl/guest/overview (guest JWT) returns the open Wahlen for the
guest's KC, each with its workshops and the guest's own current priorities
(null if not yet submitted) — everything the client needs to render the
Wahl form without any LT-only endpoint. Verified end to end against a local
Postgres (guest login -> overview -> submit -> re-fetch).

prisma/seed-dev.js: minimal dev fixture (one KC "DEV123" + Gemeinde + open
Wahl with three workshops).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 08:45:11 +02:00

117 lines
3.8 KiB
TypeScript

import {
Body,
Controller,
Get,
Param,
Post,
Query,
Req,
Res,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Response } from 'express';
import { WahlService } from './wahl.service';
import { ZuteilungService } from './zuteilung.service';
import { CreateWahlDto } from './dto/create-wahl.dto';
import { CreateWorkshopDto } from './dto/create-workshop.dto';
import { SubmitTeilnehmerDto } from './dto/submit-teilnehmer.dto';
import { CreateForceZuteilungDto } from './dto/create-force-zuteilung.dto';
import { Roles } from '../common/roles.decorator';
import { RolesGuard } from '../common/roles.guard';
import { Role } from '../common/role.enum';
import { GuestAuthenticatedRequest } from '../auth/authenticated-request';
@Controller('wahl')
export class WahlController {
constructor(
private readonly wahl: WahlService,
private readonly zuteilung: ZuteilungService,
) {}
/// Wahl-/Workshop-/Force-Zuteilung-Verwaltung ist ausschließlich Sache des
/// Leitungsteams (global über alle KCs, siehe RolesGuard).
@Post()
@UseGuards(AuthGuard('authentik'), RolesGuard)
@Roles(Role.LEITUNGSTEAM)
createWahl(@Body() dto: CreateWahlDto) {
return this.wahl.createWahl(dto.kcId, dto.name, dto.datumsSchluessel, dto.teil);
}
@Get()
@UseGuards(AuthGuard('authentik'), RolesGuard)
@Roles(Role.LEITUNGSTEAM)
listWahlen(@Query('kcId') kcId: string) {
return this.wahl.listWahlen(kcId);
}
@Post(':wahlId/workshops')
@UseGuards(AuthGuard('authentik'), RolesGuard)
@Roles(Role.LEITUNGSTEAM)
createWorkshop(@Param('wahlId') wahlId: string, @Body() dto: CreateWorkshopDto) {
return this.wahl.createWorkshop(wahlId, dto.name, dto.kapazitaet, dto.minTeilnehmer);
}
@Get(':wahlId/workshops')
@UseGuards(AuthGuard('authentik'), RolesGuard)
@Roles(Role.LEITUNGSTEAM)
listWorkshops(@Param('wahlId') wahlId: string) {
return this.wahl.listWorkshops(wahlId);
}
@Post(':wahlId/force-zuteilung')
@UseGuards(AuthGuard('authentik'), RolesGuard)
@Roles(Role.LEITUNGSTEAM)
createForceZuteilung(
@Param('wahlId') wahlId: string,
@Body() dto: CreateForceZuteilungDto,
) {
return this.wahl.createForceZuteilung(wahlId, dto.teilnehmerId, dto.workshopId);
}
/// Everything a guest needs to fill in the Wahl: open Wahlen for their KC,
/// each with its workshops and the guest's own current priorities (if any).
@Get('guest/overview')
@UseGuards(AuthGuard('guest'))
guestOverview(@Req() req: GuestAuthenticatedRequest) {
const guest = req.user!;
return this.wahl.guestOverview(guest.kcId, guest.guestId);
}
/// Guests submit their own workshop preferences (guest JWT, not Authentik).
@Post(':wahlId/teilnehmer')
@UseGuards(AuthGuard('guest'))
submitTeilnehmer(
@Param('wahlId') wahlId: string,
@Body() dto: SubmitTeilnehmerDto,
@Req() req: GuestAuthenticatedRequest,
) {
const guest = req.user!;
return this.wahl.submitTeilnehmer(wahlId, guest.guestId, guest.kcId, dto.prioritaeten);
}
@Post(':wahlId/zuteilung/run')
@UseGuards(AuthGuard('authentik'), RolesGuard)
@Roles(Role.LEITUNGSTEAM)
runZuteilung(@Param('wahlId') wahlId: string) {
return this.zuteilung.run(wahlId);
}
@Get(':wahlId/zuteilung')
@UseGuards(AuthGuard('authentik'), RolesGuard)
@Roles(Role.LEITUNGSTEAM)
getZuteilung(@Param('wahlId') wahlId: string) {
return this.zuteilung.getResults(wahlId);
}
@Get(':wahlId/zuteilung/csv')
@UseGuards(AuthGuard('authentik'), RolesGuard)
@Roles(Role.LEITUNGSTEAM)
async exportCsv(@Param('wahlId') wahlId: string, @Res() res: Response) {
const csv = await this.zuteilung.exportCsv(wahlId);
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
res.setHeader('Content-Disposition', `attachment; filename="zuteilung-${wahlId}.csv"`);
res.send(csv);
}
}