feat: Wahl-Phasen, Verantwortliche-Invites, Auth fixes, GRUPPE chat #1

Merged
linus merged 28 commits from feat/backend-phases-0-6 into main 2026-09-12 11:26:16 +00:00
3 changed files with 50 additions and 0 deletions
Showing only changes of commit d8ff49480d - Show all commits
+7
View File
@@ -0,0 +1,7 @@
import { IsBoolean, IsOptional } from 'class-validator';
export class UpdateWahlDto {
@IsOptional()
@IsBoolean()
isOpen?: boolean;
}
+16
View File
@@ -3,6 +3,7 @@ import {
Controller, Controller,
Get, Get,
Param, Param,
Patch,
Post, Post,
Query, Query,
Req, Req,
@@ -14,6 +15,7 @@ import { Response } from 'express';
import { WahlService } from './wahl.service'; import { WahlService } from './wahl.service';
import { ZuteilungService } from './zuteilung.service'; import { ZuteilungService } from './zuteilung.service';
import { CreateWahlDto } from './dto/create-wahl.dto'; import { CreateWahlDto } from './dto/create-wahl.dto';
import { UpdateWahlDto } from './dto/update-wahl.dto';
import { CreateWorkshopDto } from './dto/create-workshop.dto'; import { CreateWorkshopDto } from './dto/create-workshop.dto';
import { SubmitTeilnehmerDto } from './dto/submit-teilnehmer.dto'; import { SubmitTeilnehmerDto } from './dto/submit-teilnehmer.dto';
import { CreateForceZuteilungDto } from './dto/create-force-zuteilung.dto'; import { CreateForceZuteilungDto } from './dto/create-force-zuteilung.dto';
@@ -45,6 +47,20 @@ export class WahlController {
return this.wahl.listWahlen(kcId); 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') @Post(':wahlId/workshops')
@UseGuards(AuthGuard(['authentik', 'team']), RolesGuard) @UseGuards(AuthGuard(['authentik', 'team']), RolesGuard)
@Roles(Role.LEITUNGSTEAM) @Roles(Role.LEITUNGSTEAM)
+27
View File
@@ -22,6 +22,33 @@ export class WahlService {
return this.prisma.wahl.findMany({ where: { kcId } }); 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 /// 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). /// workshops and the guest's own current priorities (null if not submitted).
async guestOverview(kcId: string, guestAccountId: string) { async guestOverview(kcId: string, guestAccountId: string) {