feat: client monorepo (Flutter app) + web fallback redesign #1

Merged
linus merged 37 commits from feat/backend-phases-0-6 into main 2026-09-12 11:27:27 +00:00
3 changed files with 78 additions and 0 deletions
Showing only changes of commit 912461751a - Show all commits
+35
View File
@@ -0,0 +1,35 @@
/* Minimal dev seed: one KC + Gemeinde + open Wahl with workshops.
Run: node prisma/seed-dev.js (backend/.env must point at the dev DB) */
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
async function main() {
const kc = await prisma.kc.upsert({
where: { inviteCode: 'DEV123' },
update: {},
create: { name: 'KC Dev 2026', inviteCode: 'DEV123' },
});
const gem = await prisma.gemeinde.upsert({
where: { kcId_name: { kcId: kc.id, name: 'Mustergemeinde' } },
update: {},
create: { kcId: kc.id, name: 'Mustergemeinde' },
});
let wahl = await prisma.wahl.findFirst({ where: { kcId: kc.id } });
if (!wahl) {
wahl = await prisma.wahl.create({
data: { kcId: kc.id, name: 'Samstag Teil 1', datumsSchluessel: '2026-06-13', teil: '1' },
});
await prisma.workshop.createMany({
data: [
{ wahlId: wahl.id, name: 'Töpfern', kapazitaet: 12, minTeilnehmer: 4 },
{ wahlId: wahl.id, name: 'Fußball', kapazitaet: 20, minTeilnehmer: 6 },
{ wahlId: wahl.id, name: 'Bandworkshop', kapazitaet: 8, minTeilnehmer: 3 },
],
});
}
console.log('KC', kc.id, 'invite', kc.inviteCode);
console.log('Gemeinde', gem.id);
console.log('Wahl', wahl.id);
}
main().finally(() => prisma.$disconnect());
+9
View File
@@ -69,6 +69,15 @@ export class WahlController {
return this.wahl.createForceZuteilung(wahlId, dto.teilnehmerId, dto.workshopId); 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). /// Guests submit their own workshop preferences (guest JWT, not Authentik).
@Post(':wahlId/teilnehmer') @Post(':wahlId/teilnehmer')
@UseGuards(AuthGuard('guest')) @UseGuards(AuthGuard('guest'))
+34
View File
@@ -22,6 +22,40 @@ export class WahlService {
return this.prisma.wahl.findMany({ where: { kcId } }); return this.prisma.wahl.findMany({ where: { kcId } });
} }
/// 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) {
const [kc, wahlen] = await Promise.all([
this.prisma.kc.findUnique({ where: { id: kcId }, select: { id: true, name: true } }),
this.prisma.wahl.findMany({
where: { kcId, isOpen: true },
orderBy: { createdAt: 'asc' },
include: {
workshops: {
select: { id: true, name: true, kapazitaet: true },
orderBy: { name: 'asc' },
},
teilnehmer: {
where: { guestAccountId },
select: { prioritaeten: true },
},
},
}),
]);
return {
kc,
wahlen: wahlen.map((w) => ({
id: w.id,
name: w.name,
datumsSchluessel: w.datumsSchluessel,
teil: w.teil,
workshops: w.workshops,
meinePrioritaeten: (w.teilnehmer[0]?.prioritaeten as string[] | undefined) ?? null,
})),
};
}
async createWorkshop( async createWorkshop(
wahlId: string, wahlId: string,
name: string, name: string,