datewise/app/routes/app.diagnostics.tsx
metatroncubeswdev e56182e0e3 chore: initial Datewise app (rebranded from SlotSure prototype)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:06:49 -04:00

106 lines
6.1 KiB
TypeScript

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<typeof loader>();
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 (
<s-page heading="Why is a time unavailable?">
<s-section>
<Form method="get">
<input type="hidden" name="variants" value={variants.map((v) => v.id).join(",")} />
<s-stack gap="base">
<s-grid gridTemplateColumns="repeat(auto-fit, minmax(180px, 1fr))" gap="base" alignItems="end">
<s-select name="method" label="Method" value={d.method}>
<s-option value="delivery">Delivery</s-option>
<s-option value="pickup">Pickup</s-option>
<s-option value="shipping">Shipping</s-option>
</s-select>
<s-select name="location" label="Location" value={d.locationId}>
{d.locations.map((l) => <s-option key={l.id} value={l.id}>{l.name}</s-option>)}
</s-select>
<s-date-field name="date" label="Date" value={d.date} />
</s-grid>
<s-stack direction="inline" gap="base" alignItems="center">
<s-button onClick={pick}>Choose products for the test cart</s-button>
<s-text color="subdued">{variants.length ? variants.map((v) => v.title).join(", ") : "Empty cart (only hours, closures and capacity apply)"}</s-text>
</s-stack>
<s-button type="submit" variant="primary">Check</s-button>
</s-stack>
</Form>
</s-section>
{r && (
<s-section heading={niceDate(d.date, true)}>
<s-stack gap="base">
<s-paragraph>
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}` : ""}.
</s-paragraph>
{r.blockers.map((b, i) => <s-banner key={i} tone="critical" heading={b.code}>{b.message}{b.ruleId ? ` (rule ${b.ruleId})` : ""}</s-banner>)}
{!r.blockers.length && !r.day && <s-banner tone="warning" heading="BEYOND_HORIZON">This date is outside the {r.horizon}-day booking window, or in the past.</s-banner>}
{r.day?.reasons.map((x, i) => <s-banner key={i} tone="warning" heading={x.code}>{x.message}{x.ruleId ? ` (rule ${x.ruleId})` : ""}</s-banner>)}
{r.day?.slots.map((s) => (
<s-box key={s.id} padding="small" border="base" borderRadius="base" background={s.available ? "base" : "subdued"}>
<s-stack direction="inline" gap="base" alignItems="center">
<s-badge tone={s.available ? "success" : "critical"}>{s.available ? "Available" : "Blocked"}</s-badge>
<s-text type="strong">{r.dateOnly ? "Dispatch" : slotLabel(s.start, s.end)}</s-text>
<s-text color="subdued">{s.available ? (s.remaining == null ? "no limit" : `${s.remaining} left`) : s.reasons.map((x) => `${x.code}: ${x.message}`).join("; ")}</s-text>
</s-stack>
</s-box>
))}
</s-stack>
</s-section>
)}
</s-page>
);
}