import { useState } from "react"; import { data, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node"; import { Form, useLoaderData, useNavigation } from "@remix-run/react"; import { Page, Card, BlockStack, InlineStack, Text, Button, Select, TextField, ChoiceList, IndexTable } 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 { getShopTier } from "../services/billing.server"; import { tierAtLeast } from "../lib/billing-plans"; import { UpsellState } from "../components/UpsellState"; // Product/collection/vendor/type/tag-scoped delivery rules (PRODUCT_STRATEGY.md // §2 — previously unimplemented; see product-rules.server.ts). Tier-gated at // Growth, same as Zones/Rates, since scoping rules is an "advanced operator" // feature, not core parity every merchant needs on day one. const SCOPE_TYPES = [ { label: "Product", value: "product" }, { label: "Collection", value: "collection" }, { label: "Vendor", value: "vendor" }, { label: "Product type", value: "type" }, { label: "Tag", value: "tag" }, ]; const METHOD_OPTIONS: Array<{ label: string; value: Method }> = [ { label: "Shipping", value: "SHIPPING" }, { label: "Local delivery", value: "LOCAL_DELIVERY" }, { label: "Pickup", value: "PICKUP" }, ]; const SCOPE_LABELS: Record = { product: "Product ID", collection: "Collection ID", vendor: "Vendor", type: "Product type", tag: "Tag", }; export const loader = async ({ request }: LoaderFunctionArgs) => { const { session } = await authenticate.admin(request); const tier = await getShopTier(session.shop); if (!tierAtLeast(tier, "growth")) { return { gated: true as const, tier }; } const [rules, locations] = await Promise.all([ db.productRule.findMany({ where: { shopDomain: session.shop }, orderBy: { createdAt: "asc" } }), db.location.findMany({ where: { shopDomain: session.shop }, orderBy: { createdAt: "asc" } }), ]); return { gated: false as const, rules, locations }; }; export const action = async ({ request }: ActionFunctionArgs) => { const { session } = await authenticate.admin(request); const tier = await getShopTier(session.shop); if (!tierAtLeast(tier, "growth")) { return data({ errors: { scopeValue: "Product rules need the Growth plan or higher." } }, { status: 403 }); } const formData = await request.formData(); const intent = formData.get("intent"); if (intent === "delete") { const id = String(formData.get("id") || ""); await db.productRule.deleteMany({ where: { id, shopDomain: session.shop } }); return data({ ok: true }); } const scopeType = String(formData.get("scopeType") || "product"); const scopeValue = String(formData.get("scopeValue") || "").trim(); const allowedMethods = formData.getAll("allowedMethods").map(String) as Method[]; const allowedLocationIds = formData.getAll("allowedLocationIds").map(String); const leadTimeMinRaw = String(formData.get("leadTimeMin") || ""); const errors: Record = {}; if (!scopeValue) errors.scopeValue = "This field is required"; if (Object.keys(errors).length > 0) { return data({ errors }); } await db.productRule.create({ data: { shopDomain: session.shop, scopeType, scopeValue, allowedMethods, allowedLocationIds, leadTimeMin: leadTimeMinRaw ? Number(leadTimeMinRaw) : null, }, }); return data({ ok: true }); }; export default function ProductRulesIndex() { const loaderData = useLoaderData(); const navigation = useNavigation(); const isSubmitting = navigation.state === "submitting"; if (loaderData.gated) { return ( ); } const { rules, locations } = loaderData; const locationNameById = new Map(locations.map((l) => [l.id, l.name])); return ( {rules.length === 0 ? (
No product rules yet. Without one, every product uses the same lead time and is available via any method/location your slots allow.
) : ( {rules.map((rule, index) => ( {SCOPE_LABELS[rule.scopeType] ?? rule.scopeType}: {rule.scopeValue} {rule.leadTimeMin ? `${rule.leadTimeMin} min` : "—"} {rule.allowedMethods.length > 0 ? rule.allowedMethods.join(", ") : "Any"} {rule.allowedLocationIds.length > 0 ? rule.allowedLocationIds.map((id) => locationNameById.get(id) ?? id).join(", ") : "Any"}
))}
)}
); } function AddRuleForm({ locations, isSubmitting, }: { locations: Array<{ id: string; name: string }>; isSubmitting: boolean; }) { const [scopeType, setScopeType] = useState("vendor"); const [scopeValue, setScopeValue] = useState(""); const [leadTimeMin, setLeadTimeMin] = useState(""); const [allowedMethods, setAllowedMethods] = useState([]); const [allowedLocationIds, setAllowedLocationIds] = useState([]); return (
{allowedMethods.map((m) => ( ))} {allowedLocationIds.map((id) => ( ))} Add a product rule