import type { ActionFunctionArgs, LoaderFunctionArgs } from "react-router"; import { Form, useActionData, useLoaderData, useNavigation } from "react-router"; import prisma from "../db.server"; import { localParts } from "../engine/time"; import { METHODS } from "../engine/types"; import { adminContext, formBool, formInt, formList, formMoney, formString } from "../lib/admin.server"; import { METHOD_LABEL, money, niceDate } from "../lib/format"; import { bumpConfig } from "../services/config.server"; export const loader = async ({ request }: LoaderFunctionArgs) => { const { cfg, features } = await adminContext(request); const today = localParts(Date.now(), cfg.shop.timezone).date; const counters = await prisma.capacityCounter.findMany({ where: { shopId: cfg.shop.id, slot: "*", date: { in: cfg.peakDays.flatMap((p) => p.dates) } }, }); const waitlist = await prisma.waitlistEntry.findMany({ where: { shopId: cfg.shop.id, date: { gte: today } }, orderBy: { createdAt: "asc" }, take: 200 }); return { peaks: cfg.peakDays.map((p) => ({ ...p, booked: p.dates.map((d) => ({ d, n: counters.filter((c) => c.date === d && (!p.methods?.length || p.methods.includes(c.method as "pickup"))).reduce((s, c) => s + c.count, 0) })), })), waitlist: waitlist.map((w) => ({ ...w, createdAt: w.createdAt.toISOString(), offeredAt: w.offeredAt?.toISOString() ?? null })), locations: cfg.locations.map((l) => ({ id: l.id, name: l.name })), allowed: features.peakDays, currency: cfg.shop.currency, }; }; export const action = async ({ request }: ActionFunctionArgs) => { const { cfg, features } = await adminContext(request); if (!features.peakDays) return { ok: false, message: "Rush control is on the Growth plan" }; const fd = await request.formData(); const intent = formString(fd, "intent"); if (intent.startsWith("delete:")) { await prisma.peakDay.deleteMany({ where: { id: intent.split(":")[1], shopId: cfg.shop.id } }); await bumpConfig(cfg.shop.id, cfg.shop.domain); return { ok: true, message: "Peak day removed" }; } const dates = formList(fd, "dates").filter((d) => /^\d{4}-\d{2}-\d{2}$/.test(d)); if (!dates.length) return { ok: false, message: "Add at least one date (YYYY-MM-DD)" }; const data = { name: formString(fd, "name") || "Peak day", dates, methods: METHODS.filter((m) => formBool(fd, `m_${m}`)), locationIds: cfg.locations.filter((l) => formBool(fd, `loc_${l.id}`)).map((l) => l.id), dailyCapacity: formInt(fd, "dailyCapacity"), slotCapacity: formInt(fd, "slotCapacity"), extraPrepMinutes: Math.round((Number(formString(fd, "extraPrepHours")) || 0) * 60), cutoffDaysBefore: formInt(fd, "cutoffDays"), cutoffTime: formString(fd, "cutoffTime") || null, surcharge: formMoney(fd, "surcharge") ?? 0, waitlist: formBool(fd, "waitlist") && features.waitlist, message: formString(fd, "message") || null, }; const id = formString(fd, "id"); if (id) await prisma.peakDay.updateMany({ where: { id, shopId: cfg.shop.id }, data }); else await prisma.peakDay.create({ data: { ...data, shopId: cfg.shop.id } }); await bumpConfig(cfg.shop.id, cfg.shop.domain); return { ok: true, message: `${data.name} saved` }; }; type PeakView = ReturnType>["peaks"][number]; export default function Peak() { const { peaks, waitlist, locations, allowed, currency } = useLoaderData(); const result = useActionData(); const busy = useNavigation().state !== "idle"; return ( {result && {result.message}} {!allowed && Separate limits, earlier cutoffs and waitlists for festival days. See plans} On festival days demand jumps and mistakes are expensive. Set a separate order limit, close orders earlier, add a peak-handling fee, and let customers join a waitlist instead of leaving when you're full. When an order is cancelled, the first person waiting is offered the place for a limited time. {peaks.map((p) => ( {p.booked.map((b) => ( = p.dailyCapacity ? "critical" : "info"}> {`${niceDate(b.d)}: ${b.n}${p.dailyCapacity != null ? ` / ${p.dailyCapacity}` : ""} booked`} ))}
Delete {p.name}
))} Customer Wants Joined Status {waitlist.map((w) => ( {w.name ?? w.email ?? w.phone} {`${niceDate(w.date)} · ${METHOD_LABEL[w.method]} · ${w.slot === "*" ? "any time" : w.slot}`} {new Date(w.createdAt).toLocaleString("en-CA")} {w.offeredAt ? "Offered a place" : "Waiting"} ))} {waitlist.length === 0 && Nobody is waiting.}
); } function PeakForm({ p, locations, currency, busy }: { p?: PeakView; locations: { id: string; name: string }[]; currency: string; busy: boolean }) { return (
{p && } Methods (none = all): {METHODS.map((m) => )} Locations (none = all): {locations.map((l) => )} {p?.surcharge ? Current peak fee {money(p.surcharge, currency)} : null} {p ? "Save changes" : "Add peak day"}
); }