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>
141 lines
4.7 KiB
TypeScript
141 lines
4.7 KiB
TypeScript
import {
|
|
Body,
|
|
Controller,
|
|
Get,
|
|
Param,
|
|
Patch,
|
|
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 { 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';
|
|
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', 'team']), RolesGuard)
|
|
@Roles(Role.LEITUNGSTEAM)
|
|
createWahl(@Body() dto: CreateWahlDto) {
|
|
return this.wahl.createWahl(dto.kcId, dto.name, dto.datumsSchluessel, dto.teil);
|
|
}
|
|
|
|
@Get()
|
|
@UseGuards(AuthGuard(['authentik', 'team']), RolesGuard)
|
|
@Roles(Role.LEITUNGSTEAM)
|
|
listWahlen(@Query('kcId') kcId: string) {
|
|
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)
|
|
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', 'team']), RolesGuard)
|
|
@Roles(Role.LEITUNGSTEAM)
|
|
listWorkshops(@Param('wahlId') wahlId: string) {
|
|
return this.wahl.listWorkshops(wahlId);
|
|
}
|
|
|
|
@Post(':wahlId/force-zuteilung')
|
|
@UseGuards(AuthGuard(['authentik', 'team']), 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);
|
|
}
|
|
|
|
/// The guest's own assignment result per Wahl they took part in.
|
|
@Get('guest/results')
|
|
@UseGuards(AuthGuard('guest'))
|
|
guestResults(@Req() req: GuestAuthenticatedRequest) {
|
|
const guest = req.user!;
|
|
return this.wahl.guestResults(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', 'team']), RolesGuard)
|
|
@Roles(Role.LEITUNGSTEAM)
|
|
runZuteilung(@Param('wahlId') wahlId: string) {
|
|
return this.zuteilung.run(wahlId);
|
|
}
|
|
|
|
@Get(':wahlId/zuteilung')
|
|
@UseGuards(AuthGuard(['authentik', 'team']), RolesGuard)
|
|
@Roles(Role.LEITUNGSTEAM)
|
|
getZuteilung(@Param('wahlId') wahlId: string) {
|
|
return this.zuteilung.getResults(wahlId);
|
|
}
|
|
|
|
@Get(':wahlId/zuteilung/csv')
|
|
@UseGuards(AuthGuard(['authentik', 'team']), 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);
|
|
}
|
|
}
|