datewise/app/routes/app.runsheet.tsx
metatroncubeswdev 54734e8938 feat(billing): switch to Shopify managed pricing; show chosen time on thank-you page
App Store review 1.2.2 ("fails to register the plan"): follow Offerlyst's approved
managed-pricing setup. The plan is read from the Partner API on every app load,
shops without a plan (new install or reinstall) go to Shopify's plan selection
page, and app_subscriptions/update keeps it in sync. Billing API code removed.

Reviewer's checkout video: Shopify's checkout shows pickup by location name, so
the delivery label rename never reached the shopper. New checkout-time extension
shows the chosen date and time on the thank-you page (all plans) and next to
pickup/shipping options (Plus). Checkout display copy now matches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 16:52:00 -05:00

115 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { LoaderFunctionArgs } from "react-router";
import { Form, useLoaderData } from "react-router";
import prisma from "../db.server";
import { buildRunSheet, toCsv } from "../engine/runsheet";
import { localParts } from "../engine/time";
import { adminContext } from "../lib/admin.server";
import { logAccess } from "../lib/audit.server";
import { niceDate, slotLabel } from "../lib/format";
import { rowToBooking } from "../services/reminders.server";
export const loader = async ({ request }: LoaderFunctionArgs) => {
const { cfg, features, actor } = await adminContext(request);
const sp = new URL(request.url).searchParams;
const today = localParts(Date.now(), cfg.shop.timezone).date;
const date = sp.get("date") || today;
const delivering = cfg.locations.filter((l) => l.methods.delivery?.enabled);
const loc = delivering.find((l) => l.id === sp.get("location")) ?? delivering[0];
if (!loc) return { empty: true as const };
logAccess(cfg.shop.id, actor, "runsheet.view", `${loc.id} ${date}`);
const rows = await prisma.booking.findMany({ where: { shopId: cfg.shop.id, locationId: loc.id, date, method: "delivery", status: "confirmed" } });
const sheet = buildRunSheet(rows.map(rowToBooking), loc, date);
const csv = toCsv(
sheet.stops.map((s) => ({
stop: s.seq,
window: `${s.booking.slotStart}-${s.booking.slotEnd}`,
order: s.booking.orderName ?? "",
customer: s.booking.customer?.name ?? "",
phone: s.booking.customer?.phone ?? "",
address: [s.booking.address?.line1, s.booking.address?.city, s.booking.address?.postal].filter(Boolean).join(", "),
items: (s.booking.items ?? []).map((i) => `${i.quantity}x ${i.title}`).join("; "),
leg_km: s.legKm?.toFixed(1) ?? "",
})),
);
if (sp.get("format") === "csv" && features.runSheets)
return new Response(csv, {
headers: { "Content-Type": "text/csv; charset=utf-8", "Content-Disposition": `attachment; filename="run-sheet-${date}.csv"` },
}) as never;
return {
empty: false as const,
allowed: features.runSheets,
date,
locationId: loc.id,
locations: delivering.map((l) => ({ id: l.id, name: l.name })),
sheet: { ...sheet, stops: sheet.stops.map((s) => ({ seq: s.seq, legKm: s.legKm, window: s.window, b: s.booking })) },
csvHref: `/app/runsheet?date=${date}&location=${loc.id}&format=csv`,
};
};
export default function RunSheet() {
const d = useLoaderData<typeof loader>();
if (d.empty)
return (
<s-page heading="Driver run sheet">
<s-section><s-paragraph>Turn on local delivery for a location to build run sheets.</s-paragraph></s-section>
</s-page>
);
const { sheet } = d;
return (
<s-page heading={`Run sheet · ${niceDate(d.date, true)}`}>
{d.allowed && <s-button slot="secondary-actions" href={d.csvHref} target="_blank">Download CSV</s-button>}
{!d.allowed && <s-banner tone="info" heading="Growth plan feature">Run sheets and route links. <s-link href="/app/plans">See plans</s-link></s-banner>}
<s-section>
<Form method="get">
<s-stack direction="inline" gap="base" alignItems="end">
<s-date-field name="date" label="Date" value={d.date} />
<s-select name="location" label="From" value={d.locationId}>
{d.locations.map((l) => <s-option key={l.id} value={l.id}>{l.name}</s-option>)}
</s-select>
<s-button type="submit">Show</s-button>
</s-stack>
</Form>
</s-section>
<s-section heading={`${sheet.stops.length} stops · about ${sheet.totalKm ?? 0} km`}>
<s-stack gap="base">
<s-paragraph>Stops are grouped by promised window, then ordered nearest-first. Distances are straight-line estimates; open a leg in Google Maps for turn-by-turn directions.</s-paragraph>
{d.allowed && (
<s-stack direction="inline" gap="base">
{sheet.mapsUrls.map((u, i) => <s-button key={u} href={u} target="_blank">Open leg {i + 1} in Google Maps</s-button>)}
</s-stack>
)}
{sheet.unlocated.length > 0 && (
<s-banner tone="warning">{sheet.unlocated.length} order(s) have no map position (checkout didn't geocode the address): {sheet.unlocated.map((b) => b.orderName).join(", ")}. Add them by hand.</s-banner>
)}
</s-stack>
</s-section>
<s-section padding="none">
<s-table>
<s-table-header-row>
<s-table-header>#</s-table-header>
<s-table-header>Window</s-table-header>
<s-table-header listSlot="primary">Order</s-table-header>
<s-table-header>Customer</s-table-header>
<s-table-header>Address</s-table-header>
<s-table-header format="numeric">Leg</s-table-header>
<s-table-header>Items</s-table-header>
</s-table-header-row>
<s-table-body>
{sheet.stops.map((s) => (
<s-table-row key={s.b.id}>
<s-table-cell>{s.seq}</s-table-cell>
<s-table-cell>{slotLabel(s.b.slotStart, s.b.slotEnd)}</s-table-cell>
<s-table-cell><s-link href={`/app/bookings/${s.b.id}`}>{s.b.orderName}</s-link></s-table-cell>
<s-table-cell>{[s.b.customer?.name, s.b.customer?.phone].filter(Boolean).join(" · ")}</s-table-cell>
<s-table-cell>{[s.b.address?.line1, s.b.address?.postal].filter(Boolean).join(", ")}</s-table-cell>
<s-table-cell>{s.legKm != null ? `${s.legKm.toFixed(1)} km` : "—"}</s-table-cell>
<s-table-cell>{(s.b.items ?? []).map((i) => `${i.quantity}× ${i.title}`).join(", ")}</s-table-cell>
</s-table-row>
))}
</s-table-body>
</s-table>
</s-section>
</s-page>
);
}