feat(backend): Wahl-Phasen, Verantwortliche-Invites, Auth fixes
- New VerantwortlicheInvite model: LT-issued invites so a person can register as Gemeinde Verantwortliche(r) for a specific Gemeinde, skipping the self-registration approval step. - Wahl/Workshop/Teilnehmer gain phase support (phasenAnzahl, beschreibung), mirroring the WP plugin's multi-phase elections. Teilnehmer unique constraint now scoped per phase. - Auth: team login + guest auth adjustments, spec coverage. - sync.service.ts: register VerantwortlicheInvite as a synced model. - wahl.service.ts: submitTeilnehmer updated for the new phase-scoped unique key. - client: login/home screen rework, new theme.dart, FCM web tweaks. - .gitignore: ignore .DS_Store. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+43
@@ -0,0 +1,43 @@
|
||||
-- DropIndex
|
||||
DROP INDEX "Teilnehmer_wahlId_guestAccountId_key";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Wahl" ADD COLUMN "beschreibung" TEXT,
|
||||
ADD COLUMN "phasenAnzahl" INTEGER NOT NULL DEFAULT 1;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Workshop" ADD COLUMN "beschreibung" TEXT,
|
||||
ADD COLUMN "phase" INTEGER NOT NULL DEFAULT 1;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Teilnehmer" ADD COLUMN "phase" INTEGER NOT NULL DEFAULT 1;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "VerantwortlicheInvite" (
|
||||
"id" TEXT NOT NULL,
|
||||
"kcId" TEXT NOT NULL,
|
||||
"gemeindeId" TEXT NOT NULL,
|
||||
"token" TEXT NOT NULL,
|
||||
"email" TEXT,
|
||||
"maxUses" INTEGER,
|
||||
"usedCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"expiresAt" TIMESTAMP(3),
|
||||
"revokedAt" TIMESTAMP(3),
|
||||
"createdByUserId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "VerantwortlicheInvite_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "VerantwortlicheInvite_token_key" ON "VerantwortlicheInvite"("token");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Teilnehmer_wahlId_guestAccountId_phase_key" ON "Teilnehmer"("wahlId", "guestAccountId", "phase");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "VerantwortlicheInvite" ADD CONSTRAINT "VerantwortlicheInvite_kcId_fkey" FOREIGN KEY ("kcId") REFERENCES "Kc"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "VerantwortlicheInvite" ADD CONSTRAINT "VerantwortlicheInvite_gemeindeId_fkey" FOREIGN KEY ("gemeindeId") REFERENCES "Gemeinde"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
@@ -24,6 +24,7 @@ model Kc {
|
||||
guests GuestAccount[]
|
||||
localUsers User[]
|
||||
teamerInvites TeamerInvite[]
|
||||
verantwortlicheInvites VerantwortlicheInvite[]
|
||||
}
|
||||
|
||||
/// A local congregation/community participating in one Kc.
|
||||
@@ -37,6 +38,7 @@ model Gemeinde {
|
||||
memberships Membership[]
|
||||
guests GuestAccount[]
|
||||
teamerInvites TeamerInvite[]
|
||||
verantwortlicheInvites VerantwortlicheInvite[]
|
||||
|
||||
@@unique([kcId, name])
|
||||
}
|
||||
@@ -152,13 +154,42 @@ model TeamerInvite {
|
||||
gemeinde Gemeinde @relation(fields: [gemeindeId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
/// Invitation issued by a Leitungsteam member so a person can register as
|
||||
/// Gemeinde Verantwortliche/r for a specific Gemeinde via their
|
||||
/// Konfi-Castle-ID (Authentik) — skips the self-registration approval step
|
||||
/// since a Leitungsteam member is vouching for them directly. A group link
|
||||
/// leaves `email` null and may be redeemed up to `maxUses` times (null =
|
||||
/// unlimited); a personal invite pins `email` and defaults to a single use.
|
||||
model VerantwortlicheInvite {
|
||||
id String @id @default(cuid())
|
||||
kcId String
|
||||
gemeindeId String
|
||||
token String @unique
|
||||
email String?
|
||||
maxUses Int?
|
||||
usedCount Int @default(0)
|
||||
expiresAt DateTime?
|
||||
revokedAt DateTime?
|
||||
createdByUserId String
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
kc Kc @relation(fields: [kcId], references: [id], onDelete: Cascade)
|
||||
gemeinde Gemeinde @relation(fields: [gemeindeId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
/// A workshop election, scoped to a Kc; name carries a date key + "Teil".
|
||||
/// `phasenAnzahl` mirrors the WP plugin's `anzahl_einheiten`: a Wahl can run
|
||||
/// several independent phases (e.g. morning/afternoon), each with its own
|
||||
/// workshops, its own guest submission, and its own assignment run — a guest
|
||||
/// submits once per phase, not once for the whole Wahl.
|
||||
model Wahl {
|
||||
id String @id @default(cuid())
|
||||
kcId String
|
||||
name String
|
||||
datumsSchluessel String
|
||||
teil String
|
||||
beschreibung String?
|
||||
phasenAnzahl Int @default(1)
|
||||
isOpen Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@ -168,10 +199,14 @@ model Wahl {
|
||||
forceZuteilungen ForceZuteilung[]
|
||||
}
|
||||
|
||||
/// A workshop offered in one phase of a Wahl. `phase` is 1-based and must be
|
||||
/// <= the owning Wahl's `phasenAnzahl`.
|
||||
model Workshop {
|
||||
id String @id @default(cuid())
|
||||
wahlId String
|
||||
phase Int @default(1)
|
||||
name String
|
||||
beschreibung String?
|
||||
kapazitaet Int
|
||||
minTeilnehmer Int @default(0)
|
||||
|
||||
@@ -180,10 +215,13 @@ model Workshop {
|
||||
forceZuteilungen ForceZuteilung[]
|
||||
}
|
||||
|
||||
/// A participant's submitted choices for a Wahl.
|
||||
/// A participant's submitted choices for one phase of a Wahl. A guest submits
|
||||
/// separately per phase (matching the WP plugin), so the same guest can have
|
||||
/// one row per (wahlId, phase).
|
||||
model Teilnehmer {
|
||||
id String @id @default(cuid())
|
||||
wahlId String
|
||||
phase Int @default(1)
|
||||
guestAccountId String
|
||||
prioritaeten Json
|
||||
createdAt DateTime @default(now())
|
||||
@@ -193,7 +231,7 @@ model Teilnehmer {
|
||||
zuteilung Zuteilung?
|
||||
forceZuteilung ForceZuteilung?
|
||||
|
||||
@@unique([wahlId, guestAccountId])
|
||||
@@unique([wahlId, guestAccountId, phase])
|
||||
}
|
||||
|
||||
/// Manual override set by LT before running the assignment algorithm; takes precedence.
|
||||
|
||||
Reference in New Issue
Block a user