"use client"; import React, { Suspense, useEffect, useMemo, useState } from "react"; import { useAuth } from "@/hooks/useAuth"; import { useRouter, useSearchParams } from "next/navigation"; import { apiFetch } from "@/lib/api"; import { useDismissingState } from "@/hooks/useDismissingState"; import { Ticket } from "lucide-react"; // Format a Date (or date-like input) to the value expected by // This returns local time (browser timezone) as YYYY-MM-DDTHH:mm function toLocalDateTimeInputValue(input: string | number | Date | null | undefined): string { if (input == null) return ""; const d = new Date(input); if (isNaN(d.getTime())) return ""; const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, "0"); const day = String(d.getDate()).padStart(2, "0"); const hh = String(d.getHours()).padStart(2, "0"); const mm = String(d.getMinutes()).padStart(2, "0"); return `${y}-${m}-${day}T${hh}:${mm}`; } function EarlyBirdTiersEditor({ option, onSave }: { option: any; onSave: (tiers: { deadline: string; price: number; order?: number }[]) => void }) { const [rows, setRows] = React.useState<{ id?: string; deadline: string; price: string; order?: number }[]>([]); const [open, setOpen] = React.useState(false); const [saving, setSaving] = React.useState(false); React.useEffect(() => { const tiers = Array.isArray(option?.earlyBirdTiers) ? option.earlyBirdTiers : []; const normalized = tiers .slice() .sort((a: any, b: any) => new Date(a.deadline).getTime() - new Date(b.deadline).getTime() || (a.order || 0) - (b.order || 0)) .map((t: any, i: number) => ({ id: t.id, deadline: toLocalDateTimeInputValue(t.deadline), price: String(t.price ?? ''), order: typeof t.order === 'number' ? t.order : i })); setRows(normalized); }, [option?.id, option?.earlyBirdTiers]); const addRow = () => { setRows((r) => [...r, { deadline: '', price: '', order: (r.length || 0) }]); }; const removeRow = (idx: number) => { const copy = rows.slice(); copy.splice(idx, 1); setRows(copy); }; const updateRow = (idx: number, patch: Partial<{ deadline: string; price: string; order?: number }>) => { const copy = rows.slice(); copy[idx] = { ...copy[idx], ...patch } as any; setRows(copy); }; const save = async () => { setSaving(true); try { const tiers = rows .filter((r) => !!r.deadline && String(r.price).trim() !== '') .map((r, i) => ({ deadline: new Date(r.deadline).toISOString(), price: parseFloat(r.price), order: typeof r.order === 'number' ? r.order : i })) .filter((t) => t.price >= 0 && !isNaN(new Date(t.deadline).getTime())); onSave(tiers); } finally { setSaving(false); } }; return (
{open && (
{rows.length === 0 ? (
No tiers yet. Add deadlines and prices for early-bird discounts.
) : ( )}
)}
); } function EventOptionsContent() { const { user, loading, token } = useAuth(); const router = useRouter(); const search = useSearchParams(); const canView = useMemo(() => { const role = user?.role; return role === "admin" || role === "supervisor"; }, [user]); useEffect(() => { if (loading) return; if (!user) router.replace("/login"); }, [user, loading, router]); const [events, setEvents] = useState([]); const [selectedEventId, setSelectedEventId] = useState(""); const [options, setOptions] = useState([]); const [loadingEv, setLoadingEv] = useState(false); const [error, setError] = useDismissingState(null); const [info, setInfo] = useDismissingState(null); const loadEvents = async () => { if (!token) return; try { setLoadingEv(true); const evs = await apiFetch("/api/events/all", { authToken: token }); setEvents(evs || []); } catch (e) { // ignore } finally { setLoadingEv(false); } }; useEffect(() => { loadEvents(); }, [token]); // Preselect event from query (?eventId=) useEffect(() => { const q = search?.get("eventId"); if (!q) return; // if events already loaded, ensure it exists then set; otherwise, set directly and let options hook handle setSelectedEventId(q); }, [search]); useEffect(() => { const ev = events.find(e => e.id === selectedEventId); if (ev) { setOptions(ev.options || ev.eventOptions || []); } else { setOptions([]); } }, [selectedEventId, events]); const [newOpt, setNewOpt] = useState({ name: "", price: "", isMainTicket: false }); const createOption = async () => { if (!token || !selectedEventId) return; setError(null); setInfo(null); if (!newOpt.name) { setError("Option name is required"); return; } const priceNum = parseFloat(newOpt.price || "0"); try { await apiFetch(`/api/events/${encodeURIComponent(selectedEventId)}/options`, { method: "POST", authToken: token, body: { name: newOpt.name, price: priceNum || 0, isMainTicket: !!newOpt.isMainTicket } }); setNewOpt({ name: "", price: "", isMainTicket: false }); setInfo("Option created"); await loadEvents(); } catch (e: any) { setError(e?.message || "Failed to create option"); } }; const updateOption = async (opt: any, patch: any) => { if (!token) return; setError(null); setInfo(null); try { await apiFetch(`/api/events/options/${encodeURIComponent(opt.id)}`, { method: "PUT", authToken: token, body: patch }); setInfo("Option updated"); await loadEvents(); } catch (e: any) { setError(e?.message || "Failed to update option"); } }; const deleteOption = async (opt: any) => { if (!token) return; setError(null); setInfo(null); try { await apiFetch(`/api/events/options/${encodeURIComponent(opt.id)}`, { method: "DELETE", authToken: token, }); setInfo("Option deleted"); await loadEvents(); } catch (e: any) { setError(e?.message || "Failed to delete option"); } }; return (

Event options

{!canView && (
You need supervisor or admin access to use this page.
)} {error &&
{error}
} {info &&
{info}
}
Select event
{selectedEventId && (
{(() => { const ev = events.find(e => e.id === selectedEventId); if (!ev) return null; return <>
{new Date(ev.startDate).toLocaleString()} - {new Date(ev.endDate).toLocaleString()}
; })()}
)}
{selectedEventId && (
Options
{options.length === 0 ? (
No options yet.
) : (
    {options.map(opt => (
  • {opt.name}
    Price: R {(opt.price || 0).toFixed(2)} {opt.isMainTicket ? "• Main ticket" : ""}
    { const v = e.target.value.trim(); if (v && v !== opt.name) updateOption(opt, { name: v }); }} /> { const v = parseFloat(e.target.value || '0'); if (!isNaN(v) && v !== opt.price) updateOption(opt, { price: v }); }} /> {user?.role === 'admin' && ( )}
    {/* Early Bird Tiers Editor */} updateOption(opt, { earlyBirdTiers: tiers })} />
  • ))}
)}
Create new option
setNewOpt({ ...newOpt, name: e.target.value })} /> setNewOpt({ ...newOpt, price: e.target.value })} />
)}
); } export default function EventOptionsPage() { return ( Loading...}> ); }