diff --git a/src/components/admin/logistics/lunch-event-config.tsx b/src/components/admin/logistics/lunch-event-config.tsx
new file mode 100644
index 0000000..50019a9
--- /dev/null
+++ b/src/components/admin/logistics/lunch-event-config.tsx
@@ -0,0 +1,246 @@
+'use client'
+
+import { useState } from 'react'
+import type { LunchEvent } from '@prisma/client'
+import { trpc } from '@/lib/trpc/client'
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/components/ui/card'
+import { Input } from '@/components/ui/input'
+import { Label } from '@/components/ui/label'
+import { Switch } from '@/components/ui/switch'
+import { Textarea } from '@/components/ui/textarea'
+import { Badge } from '@/components/ui/badge'
+import { X } from 'lucide-react'
+import { toast } from 'sonner'
+
+function toLocalDateTimeInputValue(d: Date | null | undefined): string {
+ if (!d) return ''
+ // datetime-local expects "YYYY-MM-DDTHH:mm" in local time.
+ const pad = (n: number) => String(n).padStart(2, '0')
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(
+ d.getHours(),
+ )}:${pad(d.getMinutes())}`
+}
+
+export function LunchEventConfig({
+ programId,
+ event,
+}: {
+ programId: string
+ event: LunchEvent
+}) {
+ const utils = trpc.useUtils()
+ const update = trpc.lunch.updateEvent.useMutation({
+ onSuccess: () => {
+ utils.lunch.getEvent.invalidate({ programId })
+ utils.lunch.getEventForMember.invalidate({ programId })
+ },
+ onError: (e) => toast.error(e.message),
+ })
+ const [extraInput, setExtraInput] = useState('')
+
+ const eventAt = event.eventAt ? new Date(event.eventAt) : null
+ const endAt = event.endAt ? new Date(event.endAt) : null
+
+ return (
+
+
+ Event configuration
+ Per-edition lunch settings.
+
+
+ {/* enabled */}
+
+
+
+
+ When off, attendees see no banner or picker; admins still see this tab.
+
+
+
update.mutate({ programId, enabled: v })}
+ disabled={update.isPending}
+ />
+
+
+ {/* eventAt */}
+
+
+ {
+ const v = e.target.value
+ update.mutate({ programId, eventAt: v ? new Date(v) : null })
+ }}
+ disabled={update.isPending}
+ className="max-w-sm"
+ />
+
+
+ {/* endAt */}
+
+
+ {
+ const v = e.target.value
+ update.mutate({ programId, endAt: v ? new Date(v) : null })
+ }}
+ disabled={update.isPending}
+ className="max-w-sm"
+ />
+
+
+ {/* venue */}
+
+
+
+ update.mutate({ programId, venue: e.target.value || null })
+ }
+ disabled={update.isPending}
+ />
+
+
+ {/* notes */}
+
+
+
+
+ {/* changeCutoffHours */}
+
+
+
{
+ const n = Number(e.target.value)
+ if (Number.isFinite(n) && n !== event.changeCutoffHours) {
+ update.mutate({ programId, changeCutoffHours: n })
+ }
+ }}
+ disabled={update.isPending}
+ className="max-w-[12rem]"
+ />
+
+ After this many hours before the event, attendees and team leads can
+ no longer change their picks. Admins always can.
+
+
+
+ {/* reminderHoursBeforeDeadline */}
+
+
+ {
+ const v = e.target.value
+ update.mutate({
+ programId,
+ reminderHoursBeforeDeadline: v === '' ? null : Number(v),
+ })
+ }}
+ disabled={update.isPending}
+ className="max-w-[12rem]"
+ />
+
+
+ {/* cronEnabled */}
+
+
+
+
+ When on, the platform automatically emails the manifest when the
+ change deadline passes.
+
+
+
update.mutate({ programId, cronEnabled: v })}
+ disabled={update.isPending}
+ />
+
+
+ {/* extraRecipients */}
+
+
+
+ All edition admins receive the recap automatically. Add additional
+ email addresses here (e.g. caterer, event manager).
+
+
+ {event.extraRecipients.map((email) => (
+
+ {email}
+
+
+ ))}
+
+
setExtraInput(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' && extraInput.trim()) {
+ e.preventDefault()
+ const next = [
+ ...event.extraRecipients,
+ extraInput.trim(),
+ ]
+ update.mutate({ programId, extraRecipients: next })
+ setExtraInput('')
+ }
+ }}
+ className="max-w-sm"
+ />
+
+
+
+ )
+}
diff --git a/src/components/admin/logistics/lunch-tab.tsx b/src/components/admin/logistics/lunch-tab.tsx
index 72de5cf..0ae2e07 100644
--- a/src/components/admin/logistics/lunch-tab.tsx
+++ b/src/components/admin/logistics/lunch-tab.tsx
@@ -1,35 +1,18 @@
'use client'
import { trpc } from '@/lib/trpc/client'
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Skeleton } from '@/components/ui/skeleton'
+import { LunchEventConfig } from './lunch-event-config'
export function LunchTab({ programId }: { programId: string }) {
const { data: event, isLoading } = trpc.lunch.getEvent.useQuery({ programId })
if (isLoading || !event) {
return
}
- if (!event.enabled) {
- return (
-
-
- Lunch is disabled
-
-
-
- Toggle Lunch on from the Event configuration card to begin setup.
-
- {/* Event config card mounts in Task 14, replacing this stub. */}
-
-
- )
- }
return (
- {/* Cards mount in Tasks 14-18. */}
-
- Lunch tab — cards land in upcoming tasks.
-
+
+ {/* Other cards mount in Tasks 15-18 once event.enabled. */}
)
}