import type { LoaderFunctionArgs } from "react-router"; import { useLoaderData } from "react-router"; import prisma from "../db.server"; import { capsFor } from "../engine/capacity"; import { hoursFor } from "../engine/availability"; import { addDays, localParts } from "../engine/time"; import { METHODS } from "../engine/types"; import { adminContext } from "../lib/admin.server"; import { METHOD_LABEL, niceDate } from "../lib/format"; export const loader = async ({ request }: LoaderFunctionArgs) => { const { cfg, features } = await adminContext(request); const shopId = cfg.shop.id; const today = localParts(Date.now(), cfg.shop.timezone).date; const end = addDays(today, 13); const [todayCount, weekCount, review, waiting, counters, monthCount] = await Promise.all([ prisma.booking.count({ where: { shopId, date: today, status: "confirmed" } }), prisma.booking.count({ where: { shopId, date: { gt: today, lte: addDays(today, 7) }, status: "confirmed" } }), prisma.booking.count({ where: { shopId, OR: [{ status: "needs_review" }, { status: "confirmed", reviewReason: { not: null } }] } }), prisma.waitlistEntry.count({ where: { shopId, offeredAt: null, date: { gte: today } } }), prisma.capacityCounter.findMany({ where: { shopId, slot: "*", date: { gte: today, lte: end } } }), prisma.booking.count({ where: { shopId, createdAt: { gte: new Date(`${today.slice(0, 7)}-01T00:00:00Z`) } } }), ]); const rows = cfg.locations .filter((l) => l.enabled) .flatMap((l) => METHODS.filter((m) => l.methods[m]?.enabled && m !== "shipping").map((m) => ({ key: `${l.id}|${m}`, label: `${l.name} · ${METHOD_LABEL[m]}`, days: Array.from({ length: 14 }, (_, i) => { const date = addDays(today, i); const open = hoursFor(l, m, date).hours.length > 0; const cap = capsFor(l, m, date, "00:00-00:00", features.peakDays ? cfg.peakDays : []).dayCap; const used = counters.find((c) => c.locationId === l.id && c.method === m && c.date === date)?.count ?? 0; const peak = cfg.peakDays.find((p) => p.dates.includes(date) && (!p.locationIds?.length || p.locationIds.includes(l.id))); return { date, open, cap, used, peak: peak?.name ?? null }; }), })), ); const peaks = cfg.peakDays .flatMap((p) => p.dates.filter((d) => d >= today).map((d) => ({ name: p.name, date: d, cutoff: p.cutoff }))) .sort((a, b) => (a.date < b.date ? -1 : 1)) .slice(0, 3); const setup = { anyLocation: cfg.locations.some((l) => l.enabled), feeProduct: !!cfg.shop.feeProductId, }; return { today, todayCount, weekCount, review, waiting, rows, peaks, setup, plan: cfg.shop.plan, monthCount, monthlyLimit: features.monthlyBookings, themeEditor: `https://${cfg.shop.domain}/admin/themes/current/editor?context=apps&activateAppId=${process.env.SHOPIFY_API_KEY}/datewise-embed`, }; }; export default function Dashboard() { const d = useLoaderData(); const overLimit = d.monthlyLimit != null && d.monthCount >= d.monthlyLimit; return ( View bookings {!d.setup.anyLocation && ( 1. Review hours and turn on a location in Locations & hours. 2. Add the Datewise block to your cart page and switch on the app embed in the theme editor. 3. Turn on the checkout guard in Settings. )} {overLimit && ( New orders are still recorded, so no customer is turned away. Upgrade to keep reminders and capacity planning running. )} 0} /> {d.rows.length === 0 && No pickup or delivery locations are switched on yet.} {d.rows.map((r) => ( {r.label}
{r.days.map((x) => { const pct = x.cap ? Math.min(100, Math.round((100 * x.used) / x.cap)) : 0; const full = x.cap != null && x.used >= x.cap; return (
{x.open &&
}
{x.open ? (x.cap != null ? `${x.used}/${x.cap}` : x.used) : "—"} {x.date.slice(8)}
); })}
))} {d.peaks.length === 0 ? ( No peak days set. Add Diwali, Mother's Day or Valentine's in Rush control to cap orders, close earlier and open a waitlist. ) : ( {d.peaks.map((p) => ( {p.name}, {niceDate(p.date, true)} {p.cutoff ? `. Orders close ${p.cutoff.daysBefore} day(s) before at ${p.cutoff.time}.` : "."} ))} )} ); } function Kpi({ label, value, href, alert }: { label: string; value: number; href?: string; alert?: boolean }) { const body = ( {label} {value} ); return href ? {body} : body; }