import { data, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node"; import { Form, useLoaderData, useNavigation, useSearchParams } from "@remix-run/react"; import { Page, Card, BlockStack, InlineStack, Text, Button, Select, TextField, IndexTable, Badge, Box, } from "@shopify/polaris"; import { TitleBar } from "@shopify/app-bridge-react"; import { DateTime } from "luxon"; import type { Method } from "@prisma/client"; import { authenticate } from "../shopify.server"; import db from "../db.server"; import { bucketByLocalDate, revenueByMethod, upcomingFulfillments, utilizationByDateLocation, type BookingSummary, } from "../services/dashboard.server"; import { formatPriceLabel } from "../lib/currency"; const METHODS: Method[] = ["SHIPPING", "LOCAL_DELIVERY", "PICKUP"]; const STATUSES = ["confirmed", "fulfilled", "no_show", "cancelled"]; export const loader = async ({ request }: LoaderFunctionArgs) => { const { session } = await authenticate.admin(request); const url = new URL(request.url); const locationIdFilter = url.searchParams.get("locationId") || undefined; const methodFilter = (url.searchParams.get("method") as Method | null) || undefined; const statusFilter = url.searchParams.get("status") || undefined; const startDate = url.searchParams.get("startDate") || DateTime.now().toISODate()!; const endDate = url.searchParams.get("endDate") || DateTime.now().plus({ days: 7 }).toISODate()!; const rangeStart = DateTime.fromISO(startDate, { zone: "utc" }).toJSDate(); const rangeEnd = DateTime.fromISO(endDate, { zone: "utc" }).plus({ days: 1 }).toJSDate(); const [locations, bookings, slotTemplates] = await Promise.all([ db.location.findMany({ where: { shopDomain: session.shop }, orderBy: { createdAt: "asc" } }), db.booking.findMany({ where: { shopDomain: session.shop, locationId: locationIdFilter, method: methodFilter, status: statusFilter, slotStart: { gte: rangeStart, lt: rangeEnd }, }, include: { location: true }, orderBy: { slotStart: "asc" }, }), db.slotTemplate.findMany({ where: { shopDomain: session.shop, locationId: locationIdFilter, method: methodFilter }, }), ]); const summaries: BookingSummary[] = bookings.map((b) => ({ id: b.id, orderName: b.orderName, locationId: b.locationId, locationName: b.location.name, method: b.method, slotStart: b.slotStart, slotEnd: b.slotEnd, timezone: b.location.timezone, status: b.status, totalPriceCents: b.totalPriceCents, customerEmail: b.customerEmail, })); const byDate = bucketByLocalDate(summaries); const revenue = revenueByMethod(summaries); const utilization = utilizationByDateLocation( summaries, slotTemplates.map((t) => ({ locationId: t.locationId, method: t.method, weekday: t.weekday, capacity: t.capacity })), ); const upcoming = upcomingFulfillments(summaries); return { locations, filters: { locationId: locationIdFilter ?? "", method: methodFilter ?? "", status: statusFilter ?? "", startDate, endDate }, byDate, revenue, utilization, upcoming, totalBookings: summaries.length, }; }; export const action = async ({ request }: ActionFunctionArgs) => { const { session } = await authenticate.admin(request); const formData = await request.formData(); const bookingId = String(formData.get("bookingId") || ""); const status = String(formData.get("status") || ""); if (!bookingId || !STATUSES.includes(status)) { return data({ error: "Invalid request" }, { status: 400 }); } await db.booking.updateMany({ where: { id: bookingId, shopDomain: session.shop }, data: { status } }); return data({ ok: true }); }; export default function Dashboard() { const { locations, filters, byDate, revenue, utilization, upcoming, totalBookings } = useLoaderData(); const [searchParams, setSearchParams] = useSearchParams(); const navigation = useNavigation(); const isSubmitting = navigation.state === "submitting"; const setFilter = (key: string, value: string) => { const next = new URLSearchParams(searchParams); if (value) next.set(key, value); else next.delete(key); setSearchParams(next); }; const dates = Object.keys(byDate).sort(); const exportQuery = new URLSearchParams(searchParams).toString(); return ( Filters setFilter("startDate", v)} /> setFilter("endDate", v)} /> ({ label: m.replace("_", " "), value: m }))]} value={filters.method} onChange={(v) => setFilter("method", v)} />
); })} )}
All bookings by day {dates.length === 0 ? ( No bookings in this range. ) : ( dates.map((date) => ( {date} {byDate[date].map((b) => { const local = DateTime.fromISO(b.slotStart, { zone: "utc" }).setZone(b.timezone); return ( {local.toFormat("h:mm a")} · {b.orderName ?? "—"} · {b.method.replace("_", " ")} · {b.locationName} {b.status} ); })} )) )}
); }