diff --git a/src/app/admin/bookings/GenericBookingsList.tsx b/src/app/admin/bookings/GenericBookingsList.tsx index 57e8823..4edf095 100644 --- a/src/app/admin/bookings/GenericBookingsList.tsx +++ b/src/app/admin/bookings/GenericBookingsList.tsx @@ -86,7 +86,7 @@ export function GenericBookingsList({
- {item.name} + {item.name} {renderExtra?.(item)}
diff --git a/src/app/admin/bookings/page.tsx b/src/app/admin/bookings/page.tsx index 1814b19..aed3058 100644 --- a/src/app/admin/bookings/page.tsx +++ b/src/app/admin/bookings/page.tsx @@ -568,17 +568,31 @@ function DashboardSummary({ onNavigate }: { onNavigate: (tab: Tab) => void }) { const [counts, setCounts] = useState(null); useEffect(() => { + const today = new Date().toISOString().split("T")[0]; + const tomorrow = new Date(Date.now() + 86400000).toISOString().split("T")[0]; + Promise.all([ adminFetch("/api/admin/group-bookings").then((r) => r.json()), - adminFetch("/api/admin/mc-registrations").then((r) => r.json()), - adminFetch("/api/admin/open-day").then((r) => r.json()).then(async (events: { id: number }[]) => { - if (events.length === 0) return []; - return adminFetch(`/api/admin/open-day/bookings?eventId=${events[0].id}`).then((r) => r.json()); + // Fetch MC registrations + section data to filter out archived + Promise.all([ + adminFetch("/api/admin/mc-registrations").then((r) => r.json()), + adminFetch("/api/admin/sections/masterClasses").then((r) => r.json()), + ]).then(([regs, mcData]: [{ status: string; masterClassTitle: string }[], { items?: { title: string; slots: { date: string }[] }[] }]) => { + // Build set of upcoming MC titles + const upcomingTitles = new Set(); + for (const mc of mcData.items || []) { + if (mc.slots?.some((s) => s.date >= today)) upcomingTitles.add(mc.title); + } + return regs.filter((r) => upcomingTitles.has(r.masterClassTitle)); + }), + // Fetch Open Day — only upcoming events + adminFetch("/api/admin/open-day").then((r) => r.json()).then(async (events: { id: number; date: string }[]) => { + const active = events.find((e) => e.date >= today); + if (!active) return []; + return adminFetch(`/api/admin/open-day/bookings?eventId=${active.id}`).then((r) => r.json()); }), adminFetch("/api/admin/reminders").then((r) => r.json()).catch(() => []), ]).then(([gb, mc, od, rem]: [{ status: string }[], { status: string }[], { status: string }[], { eventDate: string }[]]) => { - const today = new Date().toISOString().split("T")[0]; - const tomorrow = new Date(Date.now() + 86400000).toISOString().split("T")[0]; setCounts({ classesNew: gb.filter((b) => b.status === "new").length, classesContacted: gb.filter((b) => b.status === "contacted").length, diff --git a/src/app/api/admin/group-bookings/route.ts b/src/app/api/admin/group-bookings/route.ts index 01da868..d6cd7e2 100644 --- a/src/app/api/admin/group-bookings/route.ts +++ b/src/app/api/admin/group-bookings/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import { getGroupBookings, addGroupBooking, toggleGroupBookingNotification, deleteGroupBooking, setGroupBookingStatus, updateBookingNotes } from "@/lib/db"; import type { BookingStatus } from "@/lib/db"; +import { sanitizeText } from "@/lib/validation"; export async function GET() { const bookings = getGroupBookings(); @@ -35,7 +36,7 @@ export async function PUT(request: NextRequest) { if (body.action === "set-notes") { const { id, notes } = body; if (!id) return NextResponse.json({ error: "id is required" }, { status: 400 }); - updateBookingNotes("group_bookings", id, notes ?? ""); + updateBookingNotes("group_bookings", id, sanitizeText(notes, 1000) ?? ""); return NextResponse.json({ ok: true }); } return NextResponse.json({ error: "Unknown action" }, { status: 400 }); diff --git a/src/app/api/admin/mc-registrations/route.ts b/src/app/api/admin/mc-registrations/route.ts index bab8b79..453b2b4 100644 --- a/src/app/api/admin/mc-registrations/route.ts +++ b/src/app/api/admin/mc-registrations/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { getMcRegistrations, getAllMcRegistrations, addMcRegistration, updateMcRegistration, toggleMcNotification, deleteMcRegistration, setMcRegistrationStatus, updateBookingNotes } from "@/lib/db"; +import { sanitizeText } from "@/lib/validation"; export async function GET(request: NextRequest) { const title = request.nextUrl.searchParams.get("title"); @@ -44,7 +45,7 @@ export async function PUT(request: NextRequest) { if (body.action === "set-notes") { const { id, notes } = body; if (!id) return NextResponse.json({ error: "id is required" }, { status: 400 }); - updateBookingNotes("mc_registrations", id, notes ?? ""); + updateBookingNotes("mc_registrations", id, sanitizeText(notes, 1000) ?? ""); return NextResponse.json({ ok: true }); } diff --git a/src/app/api/admin/open-day/bookings/route.ts b/src/app/api/admin/open-day/bookings/route.ts index 1c81d55..2c0a55c 100644 --- a/src/app/api/admin/open-day/bookings/route.ts +++ b/src/app/api/admin/open-day/bookings/route.ts @@ -6,6 +6,7 @@ import { setOpenDayBookingStatus, updateBookingNotes, } from "@/lib/db"; +import { sanitizeText } from "@/lib/validation"; export async function GET(request: NextRequest) { const eventIdStr = request.nextUrl.searchParams.get("eventId"); @@ -30,7 +31,7 @@ export async function PUT(request: NextRequest) { if (body.action === "set-notes") { const { id, notes } = body; if (!id) return NextResponse.json({ error: "id is required" }, { status: 400 }); - updateBookingNotes("open_day_bookings", id, notes ?? ""); + updateBookingNotes("open_day_bookings", id, sanitizeText(notes, 1000) ?? ""); return NextResponse.json({ ok: true }); } if (body.action === "toggle-notify") { diff --git a/src/lib/validation.ts b/src/lib/validation.ts index 8fb2e4f..c46afc2 100644 --- a/src/lib/validation.ts +++ b/src/lib/validation.ts @@ -2,9 +2,13 @@ * Shared input sanitization for public registration endpoints. */ +function stripHtml(str: string): string { + return str.replace(/<[^>]*>/g, ""); +} + export function sanitizeName(name: unknown): string | null { if (!name || typeof name !== "string") return null; - const clean = name.trim().slice(0, 100); + const clean = stripHtml(name).trim().slice(0, 100); return clean || null; } @@ -22,6 +26,6 @@ export function sanitizeHandle(value: unknown): string | undefined { export function sanitizeText(value: unknown, maxLength: number = 200): string | undefined { if (!value || typeof value !== "string") return undefined; - const clean = value.trim().slice(0, maxLength); + const clean = stripHtml(value).trim().slice(0, maxLength); return clean || undefined; }