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 { niceDate, slotLabel } from "../lib/format"; import { rowToBooking } from "../services/reminders.server"; export const loader = async ({ request }: LoaderFunctionArgs) => { const { cfg, features } = 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 }; 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(); if (d.empty) return ( Turn on local delivery for a location to build run sheets. ); const { sheet } = d; return ( {d.allowed && Download CSV} {!d.allowed && Run sheets and route links. See plans}
{d.locations.map((l) => {l.name})} Show
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. {d.allowed && ( {sheet.mapsUrls.map((u, i) => Open leg {i + 1} in Google Maps)} )} {sheet.unlocated.length > 0 && ( {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. )} # Window Order Customer Address Leg Items {sheet.stops.map((s) => ( {s.seq} {slotLabel(s.b.slotStart, s.b.slotEnd)} {s.b.orderName} {[s.b.customer?.name, s.b.customer?.phone].filter(Boolean).join(" · ")} {[s.b.address?.line1, s.b.address?.postal].filter(Boolean).join(", ")} {s.legKm != null ? `${s.legKm.toFixed(1)} km` : "—"} {(s.b.items ?? []).map((i) => `${i.quantity}× ${i.title}`).join(", ")} ))}
); }