feat(backend): guest-facing Wahl result endpoint

GET /api/wahl/guest/results (guest JWT) returns, per Wahl the guest took
part in, their assignment: status PENDING (algorithm not run yet) /
ASSIGNED (workshopName + wunschRang) / UNASSIGNED (no capacity left).
Verified both PENDING and ASSIGNED paths against local Postgres.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-10 08:57:05 +02:00
co-authored by Claude Sonnet 5
parent 138d782ca3
commit 0b588fa4b7
2 changed files with 44 additions and 0 deletions
+8
View File
@@ -78,6 +78,14 @@ export class WahlController {
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'))
+36
View File
@@ -56,6 +56,42 @@ export class WahlService {
};
}
/// Guest-facing result view: for every Wahl in the guest's KC where they
/// took part, their assignment (workshop name + wish rank), or a pending
/// marker if the algorithm has not run for them yet.
async guestResults(kcId: string, guestAccountId: string) {
const teilnahmen = await this.prisma.teilnehmer.findMany({
where: { guestAccountId, wahl: { kcId } },
orderBy: { wahl: { createdAt: 'asc' } },
select: {
wahl: { select: { id: true, name: true, datumsSchluessel: true, teil: true } },
zuteilung: { select: { workshopId: true, wunschRang: true, isForced: true } },
},
});
const workshopIds = teilnahmen
.map((t) => t.zuteilung?.workshopId)
.filter((id): id is string => !!id);
const workshops = workshopIds.length
? await this.prisma.workshop.findMany({
where: { id: { in: workshopIds } },
select: { id: true, name: true },
})
: [];
const nameById = new Map(workshops.map((w) => [w.id, w.name]));
return teilnahmen.map((t) => {
const z = t.zuteilung;
return {
wahl: t.wahl,
status: !z ? 'PENDING' : z.workshopId ? 'ASSIGNED' : 'UNASSIGNED',
workshopName: z?.workshopId ? (nameById.get(z.workshopId) ?? null) : null,
wunschRang: z?.wunschRang ?? null,
isForced: z?.isForced ?? false,
};
});
}
async createWorkshop(
wahlId: string,
name: string,