import { useState } from "react"; 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, EmptyState, } from "@shopify/polaris"; import { TitleBar } from "@shopify/app-bridge-react"; import type { Method } from "@prisma/client"; import { authenticate } from "../shopify.server"; import db from "../db.server"; import { writeCheckoutSnapshot } from "../services/checkout-snapshot.server"; const WEEKDAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; const METHODS: Method[] = ["SHIPPING", "LOCAL_DELIVERY", "PICKUP"]; function minutesToTime(min: number): string { const h = Math.floor(min / 60) .toString() .padStart(2, "0"); const m = (min % 60).toString().padStart(2, "0"); return `${h}:${m}`; } function timeToMinutes(time: string): number { const [h, m] = time.split(":").map(Number); return h * 60 + m; } export const loader = async ({ request }: LoaderFunctionArgs) => { const { session } = await authenticate.admin(request); const url = new URL(request.url); const locationId = url.searchParams.get("locationId"); const locations = await db.location.findMany({ where: { shopDomain: session.shop }, orderBy: { createdAt: "asc" }, }); const activeLocationId = locationId || locations[0]?.id || null; const slotTemplates = activeLocationId ? await db.slotTemplate.findMany({ where: { shopDomain: session.shop, locationId: activeLocationId }, orderBy: [{ weekday: "asc" }, { startMin: "asc" }], }) : []; return { locations, activeLocationId, slotTemplates }; }; export const action = async ({ request }: ActionFunctionArgs) => { const { session, admin } = await authenticate.admin(request); const formData = await request.formData(); const intent = formData.get("intent"); const locationId = String(formData.get("locationId") || ""); if (intent === "delete") { const id = String(formData.get("id") || ""); await db.slotTemplate.deleteMany({ where: { id, shopDomain: session.shop } }); await writeCheckoutSnapshot(admin, session.shop); return data({ ok: true }); } const method = formData.get("method") as Method; const weekday = Number(formData.get("weekday")); const startMin = timeToMinutes(String(formData.get("startTime"))); const endMin = timeToMinutes(String(formData.get("endTime"))); const capacity = Number(formData.get("capacity")); const cutoffMin = Number(formData.get("cutoffMin") || 0); const leadTimeMin = Number(formData.get("leadTimeMin") || 0); const transitMinDaysRaw = String(formData.get("transitMinDays") || ""); const transitMaxDaysRaw = String(formData.get("transitMaxDays") || ""); const errors: Record = {}; if (!locationId) errors.locationId = "Choose a location"; if (startMin >= endMin) errors.endTime = "End time must be after start time"; if (!capacity || capacity < 1) errors.capacity = "Capacity must be at least 1"; if (transitMinDaysRaw && transitMaxDaysRaw && Number(transitMinDaysRaw) > Number(transitMaxDaysRaw)) { errors.transitMaxDays = "Max transit days must be at least the min"; } if (Object.keys(errors).length > 0) { return data({ errors }); } await db.slotTemplate.create({ data: { shopDomain: session.shop, locationId, method, weekday, startMin, endMin, capacity, cutoffMin, leadTimeMin, // Only meaningful for SHIPPING (PRODUCT_STRATEGY.md §2 "date ranges") — // stored regardless of method since a merchant might switch a // template's method later, but only ever read for SHIPPING. transitMinDays: transitMinDaysRaw ? Number(transitMinDaysRaw) : null, transitMaxDays: transitMaxDaysRaw ? Number(transitMaxDaysRaw) : null, }, }); await writeCheckoutSnapshot(admin, session.shop); return data({ ok: true }); }; export default function SlotsIndex() { const { locations, activeLocationId, slotTemplates } = useLoaderData(); const [, setSearchParams] = useSearchParams(); const navigation = useNavigation(); const isSubmitting = navigation.state === "submitting"; if (locations.length === 0) { return ( Weekly slot templates belong to a location. ); } return ( ))} )} ); } function AddSlotForm({ locationId, isSubmitting }: { locationId: string; isSubmitting: boolean }) { const [startTime, setStartTime] = useState("09:00"); const [endTime, setEndTime] = useState("17:00"); const [capacity, setCapacity] = useState("10"); const [cutoffMin, setCutoffMin] = useState("60"); const [leadTimeMin, setLeadTimeMin] = useState("0"); const [transitMinDays, setTransitMinDays] = useState(""); const [transitMaxDays, setTransitMaxDays] = useState(""); return (
Add a slot template ({ label: m.replace("_", " "), value: m }))} />
); }