diff --git a/prisma/migrations/20260429002325_add_lunch_event/migration.sql b/prisma/migrations/20260429002325_add_lunch_event/migration.sql new file mode 100644 index 0000000..bf4242e --- /dev/null +++ b/prisma/migrations/20260429002325_add_lunch_event/migration.sql @@ -0,0 +1,109 @@ +-- CreateEnum +CREATE TYPE "DietaryTag" AS ENUM ('VEGETARIAN', 'VEGAN', 'GLUTEN_FREE', 'PESCATARIAN'); + +-- CreateEnum +CREATE TYPE "Allergen" AS ENUM ('GLUTEN', 'CRUSTACEANS', 'EGGS', 'FISH', 'PEANUTS', 'SOYBEANS', 'MILK', 'TREE_NUTS', 'CELERY', 'MUSTARD', 'SESAME', 'SULPHITES', 'LUPIN', 'MOLLUSCS'); + +-- CreateTable +CREATE TABLE "LunchEvent" ( + "id" TEXT NOT NULL, + "programId" TEXT NOT NULL, + "enabled" BOOLEAN NOT NULL DEFAULT false, + "eventAt" TIMESTAMP(3), + "endAt" TIMESTAMP(3), + "venue" TEXT, + "notes" TEXT, + "changeCutoffHours" INTEGER NOT NULL DEFAULT 48, + "reminderHoursBeforeDeadline" INTEGER, + "cronEnabled" BOOLEAN NOT NULL DEFAULT true, + "extraRecipients" TEXT[] DEFAULT ARRAY[]::TEXT[], + "reminderSentAt" TIMESTAMP(3), + "recapSentAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "LunchEvent_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Dish" ( + "id" TEXT NOT NULL, + "lunchEventId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "sortOrder" INTEGER NOT NULL DEFAULT 0, + "dietaryTags" "DietaryTag"[], + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Dish_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "MemberLunchPick" ( + "id" TEXT NOT NULL, + "attendingMemberId" TEXT NOT NULL, + "dishId" TEXT, + "allergens" "Allergen"[] DEFAULT ARRAY[]::"Allergen"[], + "allergenOther" TEXT, + "pickedAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "MemberLunchPick_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ExternalAttendee" ( + "id" TEXT NOT NULL, + "lunchEventId" TEXT NOT NULL, + "projectId" TEXT, + "name" TEXT NOT NULL, + "email" TEXT, + "roleNote" TEXT, + "dishId" TEXT, + "allergens" "Allergen"[] DEFAULT ARRAY[]::"Allergen"[], + "allergenOther" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "ExternalAttendee_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "LunchEvent_programId_key" ON "LunchEvent"("programId"); + +-- CreateIndex +CREATE INDEX "Dish_lunchEventId_idx" ON "Dish"("lunchEventId"); + +-- CreateIndex +CREATE UNIQUE INDEX "MemberLunchPick_attendingMemberId_key" ON "MemberLunchPick"("attendingMemberId"); + +-- CreateIndex +CREATE INDEX "MemberLunchPick_dishId_idx" ON "MemberLunchPick"("dishId"); + +-- CreateIndex +CREATE INDEX "ExternalAttendee_lunchEventId_idx" ON "ExternalAttendee"("lunchEventId"); + +-- CreateIndex +CREATE INDEX "ExternalAttendee_projectId_idx" ON "ExternalAttendee"("projectId"); + +-- AddForeignKey +ALTER TABLE "LunchEvent" ADD CONSTRAINT "LunchEvent_programId_fkey" FOREIGN KEY ("programId") REFERENCES "Program"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Dish" ADD CONSTRAINT "Dish_lunchEventId_fkey" FOREIGN KEY ("lunchEventId") REFERENCES "LunchEvent"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MemberLunchPick" ADD CONSTRAINT "MemberLunchPick_attendingMemberId_fkey" FOREIGN KEY ("attendingMemberId") REFERENCES "AttendingMember"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MemberLunchPick" ADD CONSTRAINT "MemberLunchPick_dishId_fkey" FOREIGN KEY ("dishId") REFERENCES "Dish"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ExternalAttendee" ADD CONSTRAINT "ExternalAttendee_lunchEventId_fkey" FOREIGN KEY ("lunchEventId") REFERENCES "LunchEvent"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ExternalAttendee" ADD CONSTRAINT "ExternalAttendee_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ExternalAttendee" ADD CONSTRAINT "ExternalAttendee_dishId_fkey" FOREIGN KEY ("dishId") REFERENCES "Dish"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index bfd3561..f8c4eca 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -507,6 +507,7 @@ model Program { finalistSlotQuotas FinalistSlotQuota[] waitlistEntries WaitlistEntry[] hotel Hotel? + lunchEvent LunchEvent? @@unique([name, year]) @@index([status]) @@ -650,8 +651,9 @@ model Project { notificationLogs NotificationLog[] // Grand-finale logistics - waitlistEntry WaitlistEntry? - finalistConfirmation FinalistConfirmation? + waitlistEntry WaitlistEntry? + finalistConfirmation FinalistConfirmation? + externalLunchAttendees ExternalAttendee[] @@index([programId]) @@index([status]) @@ -2731,6 +2733,7 @@ model AttendingMember { user User @relation(fields: [userId], references: [id], onDelete: Cascade) flightDetail FlightDetail? visaApplication VisaApplication? + lunchPick MemberLunchPick? @@unique([confirmationId, userId]) @@index([userId]) @@ -2809,3 +2812,110 @@ model VisaApplication { @@index([status]) } + +// ───────────────────────────────────────────────────────────────────────────── +// Grand-finale lunch event (PR 6) +// Single configurable lunch event per edition. Each attending member has a +// 1:1 MemberLunchPick (auto-created via lunch-pick-sync). External attendees +// can be standalone or attached to a finalist project. Allergens use the +// EU 14 regulated list; dishes carry dietary tags. +// ───────────────────────────────────────────────────────────────────────────── + +enum DietaryTag { + VEGETARIAN + VEGAN + GLUTEN_FREE + PESCATARIAN +} + +enum Allergen { + GLUTEN + CRUSTACEANS + EGGS + FISH + PEANUTS + SOYBEANS + MILK + TREE_NUTS + CELERY + MUSTARD + SESAME + SULPHITES + LUPIN + MOLLUSCS +} + +model LunchEvent { + id String @id @default(cuid()) + programId String @unique + enabled Boolean @default(false) + eventAt DateTime? + endAt DateTime? + venue String? + notes String? @db.Text + changeCutoffHours Int @default(48) + reminderHoursBeforeDeadline Int? + cronEnabled Boolean @default(true) + extraRecipients String[] @default([]) + reminderSentAt DateTime? + recapSentAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + program Program @relation(fields: [programId], references: [id], onDelete: Cascade) + dishes Dish[] + externalAttendees ExternalAttendee[] +} + +model Dish { + id String @id @default(cuid()) + lunchEventId String + name String + sortOrder Int @default(0) + dietaryTags DietaryTag[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + lunchEvent LunchEvent @relation(fields: [lunchEventId], references: [id], onDelete: Cascade) + memberPicks MemberLunchPick[] + externals ExternalAttendee[] + + @@index([lunchEventId]) +} + +model MemberLunchPick { + id String @id @default(cuid()) + attendingMemberId String @unique + dishId String? + allergens Allergen[] @default([]) + allergenOther String? + pickedAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + attendingMember AttendingMember @relation(fields: [attendingMemberId], references: [id], onDelete: Cascade) + dish Dish? @relation(fields: [dishId], references: [id], onDelete: SetNull) + + @@index([dishId]) +} + +model ExternalAttendee { + id String @id @default(cuid()) + lunchEventId String + projectId String? + name String + email String? + roleNote String? + dishId String? + allergens Allergen[] @default([]) + allergenOther String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + lunchEvent LunchEvent @relation(fields: [lunchEventId], references: [id], onDelete: Cascade) + project Project? @relation(fields: [projectId], references: [id], onDelete: SetNull) + dish Dish? @relation(fields: [dishId], references: [id], onDelete: SetNull) + + @@index([lunchEventId]) + @@index([projectId]) +}