import { useState } from "react"; import type { LoaderFunctionArgs } from "react-router"; import { Form, useLoaderData } from "react-router"; import { computeAvailability } from "../engine/availability"; import { evaluateRules } from "../engine/rules"; import { addDays, localParts } from "../engine/time"; import type { Method } from "../engine/types"; import { adminContext } from "../lib/admin.server"; import { niceDate, slotLabel } from "../lib/format"; import { enrichCart } from "../services/catalog.server"; import { reservations } from "../services/storefront.server"; /** * "Why is this slot unavailable?" — runs the exact checkout engine for a sample cart, * location, method and date and shows every reason with the rule that caused it. */ export const loader = async ({ request }: LoaderFunctionArgs) => { const { cfg, admin } = await adminContext(request); const sp = new URL(request.url).searchParams; const today = localParts(Date.now(), cfg.shop.timezone).date; const method = (sp.get("method") as Method) || "delivery"; const loc = cfg.locations.find((l) => l.id === sp.get("location")) ?? cfg.locations[0]; const date = sp.get("date") || addDays(today, 1); const variants = (sp.get("variants") ?? "").split(",").filter(Boolean); const base = { locations: cfg.locations.map((l) => ({ id: l.id, name: l.name })), method, locationId: loc?.id ?? "", date, variants }; if (!loc) return { ...base, result: null }; const s2a = Object.fromEntries(cfg.locations.filter((l) => l.shopifyLocationId).map((l) => [l.shopifyLocationId!, l.id])); const { cart } = await enrichCart(admin, cfg.shop.domain, variants.map((v) => ({ variantId: v, quantity: 1 })), s2a); const usage = await reservations.usage(cfg.shop.id, addDays(today, -1), addDays(today, 370), new Date()); const cons = evaluateRules(cart, cfg.rules, cfg.settings.mixedPrepPolicy); const av = computeAvailability({ location: loc, method, cart, rules: cfg.rules, peakDays: cfg.peakDays, usage, settings: cfg.settings, now: Date.now(), constraints: cons }); const d = av.dates.find((x) => x.date === date); return { ...base, result: { items: cart.items.map((i) => i.title), matched: cons.matched, prep: av.prepMinutes, prepDriver: cons.prepDriver ?? null, blockers: av.blockers, horizon: loc.methods[method]?.horizonDays ?? null, day: d ? { reasons: d.reasons, peak: d.peak, slots: d.slots.map((s) => ({ id: s.id, start: s.start, end: s.end, available: s.available, remaining: s.remaining, reasons: s.reasons })) } : null, dateOnly: av.dateOnly, }, }; }; export default function Diagnostics() { const d = useLoaderData(); const [variants, setVariants] = useState<{ id: string; title: string }[]>(d.variants.map((id) => ({ id, title: id.split("/").pop()! }))); const pick = async () => { const sel = (await shopify.resourcePicker({ type: "variant", multiple: true, action: "select" })) as unknown as { id: string; displayName?: string; title?: string }[] | undefined; if (sel) setVariants(sel.map((v) => ({ id: v.id, title: v.displayName ?? v.title ?? v.id }))); }; const r = d.result; return (
v.id).join(",")} /> Delivery Pickup Shipping {d.locations.map((l) => {l.name})} Choose products for the test cart {variants.length ? variants.map((v) => v.title).join(", ") : "Empty cart (only hours, closures and capacity apply)"} Check
{r && ( Rules matched: {r.matched.length ? r.matched.map((m) => `${m.ruleName} (${m.itemTitle})`).join("; ") : "none"}. Preparation used: {Math.round(r.prep / 6) / 10} h {r.prepDriver ? `, set by ${r.prepDriver.itemTitle}` : ""}. {r.blockers.map((b, i) => {b.message}{b.ruleId ? ` (rule ${b.ruleId})` : ""})} {!r.blockers.length && !r.day && This date is outside the {r.horizon}-day booking window, or in the past.} {r.day?.reasons.map((x, i) => {x.message}{x.ruleId ? ` (rule ${x.ruleId})` : ""})} {r.day?.slots.map((s) => ( {s.available ? "Available" : "Blocked"} {r.dateOnly ? "Dispatch" : slotLabel(s.start, s.end)} {s.available ? (s.remaining == null ? "no limit" : `${s.remaining} left`) : s.reasons.map((x) => `${x.code}: ${x.message}`).join("; ")} ))} )}
); }