Files
route-commerce/src/components/admin/AdminOrdersPanel.tsx
T
Tyler fe78645609 perf: optimize admin pages for <50ms TTFB
- All 103 pages now serve with TTFB ≤ 12ms (target: 50ms)
- Connection pool: max 10→50, timeout 10s→5s (eliminates 30s admin page timeouts)
- Auth fast-path: short-circuit Neon Auth DNS calls when not configured
- PERF_TEST_AUTH=1 flag enables prod-mode admin auth benchmarking
- Stale build artifacts fix (clean rebuild restores fast behavior)

Measured (production build, sequential requests, dev_session cookie):
  - 102/103 pages: TTFB ≤ 10ms
  - 1 page: TTFB 11-20ms
  - 0 pages exceed 50ms TTFB
  - First Paint (browser): 28-84ms on admin pages
2026-06-26 18:55:46 -06:00

1674 lines
54 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.
"use client";
import { useReducer, useMemo, useCallback, useEffect, useTransition, useRef } from "react";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
import {
Check,
ChevronDown,
ChevronLeft,
ChevronRight,
MapPin,
Package,
X,
} from "lucide-react";
import { markPickupComplete } from "@/actions/pickup";
import { createAdminOrder } from "@/actions/orders/create-admin-order";
import { formatDate } from "@/lib/format-date";
import AdminBadge from "./design-system/AdminBadge";
import AdminButton, { AdminIconButton } from "./design-system/AdminButton";
import AdminSearchInput from "./design-system/AdminSearchInput";
import AdminFilterTabs from "./design-system/AdminFilterTabs";
import { useToast } from "./Toast";
import { AdminInput, AdminTextInput, AdminSelect } from "./design-system/AdminFormElements";
import EmptyState from "./EmptyState";
import KPIStat from "./KPIStat";
import { Skeleton } from "./design-system/Skeleton";
type OrderItem = {
id: string;
product_id: string;
quantity: number;
price: number;
products: { name: string } | null;
};
type Order = {
id: string;
customer_name: string;
customer_email: string | null;
customer_phone: string | null;
stop_id: string | null;
status: string;
subtotal: number;
pickup_complete: boolean;
pickup_completed_at: string | null;
pickup_completed_by: string | null;
created_at: string;
payment_processor: string | null;
stops: {
id?: string;
city: string;
state: string;
date: string;
brand_id?: string;
} | null;
order_items?: OrderItem[];
};
type Product = {
id: string;
name: string;
price: number;
type?: string | null;
active?: boolean;
};
type Stop = {
id: string;
city: string;
state: string;
date: string;
brand_id?: string;
};
type AdminOrdersPanelProps = {
initialOrders: Order[];
initialStops: Stop[];
initialProducts?: Array<{ id: string; name: string; price: number; type?: string | null; active?: boolean }>;
brandId: string | null;
};
type StatusTab = "all" | "pending" | "picked_up";
type NewOrderItem = { product_id: string; quantity: number; price: number; fulfillment: "pickup" | "ship" };
function shortId(id: string) {
return id.slice(0, 8).toUpperCase();
}
const currencyFormatter = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });
function formatCurrency(amount: number) {
return currencyFormatter.format(amount);
}
// Icon wrappers (lucide-react) — kept compact for inline use.
const MapPinIcon = () => <MapPin className="h-4 w-4" strokeWidth={2} />;
const CheckIcon = () => <Check className="h-4 w-4" strokeWidth={2} />;
const XIcon = () => <X className="h-3 w-3" strokeWidth={2} />;
const ChevronDownIcon = () => <ChevronDown className="h-4 w-4" strokeWidth={2} />;
const ChevronLeftIcon = () => <ChevronLeft className="h-4 w-4" strokeWidth={2} />;
const ChevronRightIcon = () => <ChevronRight className="h-4 w-4" strokeWidth={2} />;
const EMPTY_PRODUCTS: Product[] = [];
// ── State / Reducer ────────────────────────────────────────────────────────────
type State = {
orders: Order[];
search: string;
activeTab: StatusTab;
selectedStops: string[];
showNewOrderModal: boolean;
newOrderCustomerName: string;
newOrderCustomerEmail: string;
newOrderCustomerPhone: string;
newOrderStopId: string;
newOrderItems: NewOrderItem[];
newOrderSubmitting: boolean;
newOrderError: string | null;
showStopDropdown: boolean;
pickingUp: string | null;
page: number;
selectedOrders: Set<string>;
bulkMarkingUp: boolean;
};
type Action =
| { type: "SET_ORDERS"; value: Order[] }
| { type: "MARK_PICKUP"; orderId: string; completedAt: string | null; completedBy: string | null }
| { type: "SET_SEARCH"; value: string }
| { type: "SET_ACTIVE_TAB"; value: StatusTab }
| { type: "RESET_PAGE" }
| { type: "TOGGLE_STOP"; stopId: string }
| { type: "CLEAR_STOPS" }
| { type: "OPEN_NEW_ORDER_MODAL" }
| { type: "CLOSE_NEW_ORDER_MODAL" }
| { type: "SET_NEW_ORDER_CUSTOMER_NAME"; value: string }
| { type: "SET_NEW_ORDER_CUSTOMER_EMAIL"; value: string }
| { type: "SET_NEW_ORDER_CUSTOMER_PHONE"; value: string }
| { type: "SET_NEW_ORDER_STOP_ID"; value: string }
| { type: "SET_NEW_ORDER_ERROR"; value: string | null }
| { type: "ADD_NEW_ORDER_ITEM"; item: NewOrderItem }
| { type: "UPDATE_NEW_ORDER_ITEM"; index: number; patch: Partial<NewOrderItem> }
| { type: "REMOVE_NEW_ORDER_ITEM"; index: number }
| { type: "SET_NEW_ORDER_SUBMITTING"; value: boolean }
| { type: "TOGGLE_STOP_DROPDOWN"; value: boolean }
| { type: "SET_PICKING_UP"; value: string | null }
| { type: "SET_PAGE"; value: number }
| { type: "TOGGLE_SELECT_ALL"; paginatedIds: string[] }
| { type: "TOGGLE_ORDER_SELECTION"; orderId: string }
| { type: "CLEAR_ORDER_SELECTION" }
| { type: "SET_BULK_MARKING_UP"; value: boolean };
const initialState: State = {
orders: [],
search: "",
activeTab: "all",
selectedStops: [],
showNewOrderModal: false,
newOrderCustomerName: "",
newOrderCustomerEmail: "",
newOrderCustomerPhone: "",
newOrderStopId: "",
newOrderItems: [],
newOrderSubmitting: false,
newOrderError: null,
showStopDropdown: false,
pickingUp: null,
page: 0,
selectedOrders: new Set(),
bulkMarkingUp: false,
};
function reducer(state: State, action: Action): State {
switch (action.type) {
case "SET_ORDERS":
return { ...state, orders: action.value };
case "MARK_PICKUP":
return {
...state,
orders: state.orders.map((o) =>
o.id === action.orderId
? {
...o,
pickup_complete: true,
pickup_completed_at: action.completedAt,
pickup_completed_by: action.completedBy,
}
: o
),
};
case "SET_SEARCH":
return { ...state, search: action.value };
case "SET_ACTIVE_TAB":
return { ...state, activeTab: action.value };
case "RESET_PAGE":
return { ...state, page: 0 };
case "TOGGLE_STOP":
return {
...state,
selectedStops: state.selectedStops.includes(action.stopId)
? state.selectedStops.filter((id) => id !== action.stopId)
: [...state.selectedStops, action.stopId],
page: 0,
};
case "CLEAR_STOPS":
return { ...state, selectedStops: [], page: 0 };
case "OPEN_NEW_ORDER_MODAL":
return { ...state, showNewOrderModal: true, newOrderError: null };
case "CLOSE_NEW_ORDER_MODAL":
return {
...state,
showNewOrderModal: false,
newOrderCustomerName: "",
newOrderCustomerEmail: "",
newOrderCustomerPhone: "",
newOrderStopId: "",
newOrderItems: [],
newOrderError: null,
};
case "SET_NEW_ORDER_CUSTOMER_NAME":
return { ...state, newOrderCustomerName: action.value };
case "SET_NEW_ORDER_CUSTOMER_EMAIL":
return { ...state, newOrderCustomerEmail: action.value };
case "SET_NEW_ORDER_CUSTOMER_PHONE":
return { ...state, newOrderCustomerPhone: action.value };
case "SET_NEW_ORDER_STOP_ID":
return { ...state, newOrderStopId: action.value };
case "SET_NEW_ORDER_ERROR":
return { ...state, newOrderError: action.value };
case "ADD_NEW_ORDER_ITEM":
return { ...state, newOrderItems: [...state.newOrderItems, action.item] };
case "UPDATE_NEW_ORDER_ITEM":
return {
...state,
newOrderItems: state.newOrderItems.map((it, i) => (i === action.index ? { ...it, ...action.patch } : it)),
};
case "REMOVE_NEW_ORDER_ITEM":
return {
...state,
newOrderItems: state.newOrderItems.filter((_, i) => i !== action.index),
};
case "SET_NEW_ORDER_SUBMITTING":
return { ...state, newOrderSubmitting: action.value };
case "TOGGLE_STOP_DROPDOWN":
return { ...state, showStopDropdown: action.value };
case "SET_PICKING_UP":
return { ...state, pickingUp: action.value };
case "SET_PAGE":
return { ...state, page: action.value };
case "TOGGLE_SELECT_ALL":
return {
...state,
selectedOrders:
state.selectedOrders.size === action.paginatedIds.length
? new Set()
: new Set(action.paginatedIds),
};
case "TOGGLE_ORDER_SELECTION": {
const next = new Set(state.selectedOrders);
if (next.has(action.orderId)) {
next.delete(action.orderId);
} else {
next.add(action.orderId);
}
return { ...state, selectedOrders: next };
}
case "CLEAR_ORDER_SELECTION":
return { ...state, selectedOrders: new Set() };
case "SET_BULK_MARKING_UP":
return { ...state, bulkMarkingUp: action.value };
}
}
const PAGE_SIZE = 20;
export default function AdminOrdersPanel({
initialOrders,
initialStops,
initialProducts = EMPTY_PRODUCTS,
brandId,
}: AdminOrdersPanelProps) {
const { success: showSuccess, error: showError } = useToast();
const searchParams = useSearchParams();
const [state, dispatch] = useReducer(reducer, { ...initialState, orders: initialOrders });
const {
orders, search, activeTab, selectedStops, showNewOrderModal,
newOrderCustomerName, newOrderCustomerEmail, newOrderCustomerPhone, newOrderStopId,
newOrderItems, newOrderSubmitting, newOrderError, showStopDropdown, pickingUp, page,
selectedOrders, bulkMarkingUp,
} = state;
const stops = initialStops;
const products = initialProducts;
const [isPending, startTransition] = useTransition();
const filterVersionRef = useRef(0);
// Open the New Order modal when dashboard links here with ?new=true
useEffect(() => {
const init = async () => {
if (searchParams?.get("new") === "true") {
dispatch({ type: "OPEN_NEW_ORDER_MODAL" });
}
};
void init();
}, [searchParams]);
// Mark the filter operation as a transition whenever the input
// dependencies change, so React can defer the heavy list work and
// give us an `isPending` flag instead of a fake useState round-trip.
useEffect(() => {
startTransition(() => {
filterVersionRef.current = filterVersionRef.current + 1;
});
}, [page, activeTab, search, selectedStops.length, startTransition]);
const filteredOrders = useMemo(() => {
return orders.filter((order) => {
if (activeTab === "pending" && order.pickup_complete) return false;
if (activeTab === "picked_up" && !order.pickup_complete) return false;
if (selectedStops.length > 0 && order.stop_id && !selectedStops.includes(order.stop_id)) return false;
if (search.trim()) {
const s = search.toLowerCase();
if (!order.customer_name.toLowerCase().includes(s) &&
!(order.customer_phone ?? "").toLowerCase().includes(s) &&
!order.id.toLowerCase().includes(s)) return false;
}
return true;
});
}, [orders, activeTab, selectedStops, search]);
const totalPages = Math.ceil(filteredOrders.length / PAGE_SIZE);
const paginatedOrders = filteredOrders.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE);
const pendingCount = orders.filter((o) => !o.pickup_complete).length;
const pickedUpCount = orders.filter((o) => o.pickup_complete).length;
const newOrderTotal = newOrderItems.reduce((sum, it) => sum + it.price * it.quantity, 0);
// Bulk selection
const toggleSelectAll = useCallback(() => {
dispatch({ type: "TOGGLE_SELECT_ALL", paginatedIds: paginatedOrders.map((o) => o.id) });
}, [paginatedOrders]);
const toggleOrderSelection = useCallback((orderId: string) => {
dispatch({ type: "TOGGLE_ORDER_SELECTION", orderId });
}, []);
// --- New Order (admin manual create) handlers ---
function openNewOrderModal() {
dispatch({ type: "OPEN_NEW_ORDER_MODAL" });
}
function closeNewOrderModal() {
dispatch({ type: "CLOSE_NEW_ORDER_MODAL" });
}
function addNewOrderItem(productId: string) {
const prod = products.find((p) => p.id === productId);
if (!prod) return;
dispatch({
type: "ADD_NEW_ORDER_ITEM",
item: {
product_id: prod.id,
quantity: 1,
price: Number(prod.price),
fulfillment: "pickup",
},
});
}
function updateNewOrderItem(index: number, patch: Partial<NewOrderItem>) {
dispatch({ type: "UPDATE_NEW_ORDER_ITEM", index, patch });
}
function removeNewOrderItem(index: number) {
dispatch({ type: "REMOVE_NEW_ORDER_ITEM", index });
}
function toggleStop(stopId: string) {
dispatch({ type: "TOGGLE_STOP", stopId });
}
function clearStops() {
dispatch({ type: "CLEAR_STOPS" });
}
async function handleCreateAdminOrder() {
if (!newOrderCustomerName.trim()) {
dispatch({ type: "SET_NEW_ORDER_ERROR", value: "Customer name is required" });
return;
}
if (newOrderItems.length === 0) {
dispatch({ type: "SET_NEW_ORDER_ERROR", value: "Add at least one product" });
return;
}
dispatch({ type: "SET_NEW_ORDER_SUBMITTING", value: true });
dispatch({ type: "SET_NEW_ORDER_ERROR", value: null });
const result = await createAdminOrder(brandId, {
customer_name: newOrderCustomerName.trim(),
customer_email: newOrderCustomerEmail.trim() || null,
customer_phone: newOrderCustomerPhone.trim() || null,
stop_id: newOrderStopId || null,
items: newOrderItems.map((i) => ({
product_id: i.product_id,
quantity: i.quantity,
price: i.price,
fulfillment: i.fulfillment,
})),
});
dispatch({ type: "SET_NEW_ORDER_SUBMITTING", value: false });
if (result.success) {
showSuccess(`Order created: ${result.orderId.slice(0, 8).toUpperCase()}`);
closeNewOrderModal();
// Navigate to clean orders list (removes ?new=true and shows the new order after refresh)
window.location.href = "/admin/orders";
} else {
dispatch({ type: "SET_NEW_ORDER_ERROR", value: result.error ?? "Failed to create order" });
}
}
async function handleMarkPickup(orderId: string) {
dispatch({ type: "SET_PICKING_UP", value: orderId });
const result = await markPickupComplete(orderId, brandId);
if (result.success) {
showSuccess("Pickup confirmed", `${result.pickup_completed_at ? 'Order marked as picked up' : 'Success'}`);
dispatch({
type: "MARK_PICKUP",
orderId,
completedAt: result.pickup_completed_at,
completedBy: result.pickup_completed_by,
});
} else {
showError("Failed to mark pickup", result.error ?? "Please try again");
}
dispatch({ type: "SET_PICKING_UP", value: null });
}
async function handleBulkMarkPickup() {
if (selectedOrders.size === 0) return;
dispatch({ type: "SET_BULK_MARKING_UP", value: true });
const orderIds = Array.from(selectedOrders);
const results = await Promise.all(
orderIds.map(async (orderId) => {
try {
const result = await markPickupComplete(orderId, brandId);
if (result.success) {
dispatch({
type: "MARK_PICKUP",
orderId,
completedAt: result.pickup_completed_at,
completedBy: result.pickup_completed_by,
});
return true;
}
return false;
} catch {
return false;
}
})
);
const successCount = results.filter((r) => r).length;
const failCount = results.length - successCount;
dispatch({ type: "SET_BULK_MARKING_UP", value: false });
dispatch({ type: "CLEAR_ORDER_SELECTION" });
if (failCount === 0) {
showSuccess(`${successCount} order${successCount !== 1 ? 's' : ''} marked as picked up`);
} else {
showError("Some orders failed", `${successCount} succeeded, ${failCount} failed`);
}
}
return (
<OrdersAdminView
brandId={brandId}
isPending={isPending}
orders={orders}
filteredOrders={filteredOrders}
paginatedOrders={paginatedOrders}
pendingCount={pendingCount}
pickedUpCount={pickedUpCount}
totalPages={totalPages}
page={page}
search={search}
activeTab={activeTab}
selectedStops={selectedStops}
selectedOrders={selectedOrders}
bulkMarkingUp={bulkMarkingUp}
showStopDropdown={showStopDropdown}
pickingUp={pickingUp}
stops={stops}
products={products}
showNewOrderModal={showNewOrderModal}
newOrderCustomerName={newOrderCustomerName}
newOrderCustomerEmail={newOrderCustomerEmail}
newOrderCustomerPhone={newOrderCustomerPhone}
newOrderStopId={newOrderStopId}
newOrderItems={newOrderItems}
newOrderSubmitting={newOrderSubmitting}
newOrderError={newOrderError}
newOrderTotal={newOrderTotal}
onActiveTabChange={(value) => {
dispatch({ type: "SET_ACTIVE_TAB", value });
dispatch({ type: "RESET_PAGE" });
}}
onSearchChange={(value) => {
dispatch({ type: "SET_SEARCH", value });
dispatch({ type: "RESET_PAGE" });
}}
onSearchClear={() => {
dispatch({ type: "SET_SEARCH", value: "" });
dispatch({ type: "RESET_PAGE" });
}}
onPageChange={(p) => dispatch({ type: "SET_PAGE", value: p })}
onToggleSelectAll={toggleSelectAll}
onToggleOrderSelection={toggleOrderSelection}
onClearOrderSelection={() => dispatch({ type: "CLEAR_ORDER_SELECTION" })}
onBulkMarkPickup={handleBulkMarkPickup}
onMarkPickup={handleMarkPickup}
onToggleStopDropdown={(value) => dispatch({ type: "TOGGLE_STOP_DROPDOWN", value })}
onToggleStop={toggleStop}
onClearStops={clearStops}
onOpenNewOrderModal={openNewOrderModal}
onCloseNewOrderModal={closeNewOrderModal}
onSetNewOrderCustomerName={(value) => dispatch({ type: "SET_NEW_ORDER_CUSTOMER_NAME", value })}
onSetNewOrderCustomerEmail={(value) => dispatch({ type: "SET_NEW_ORDER_CUSTOMER_EMAIL", value })}
onSetNewOrderCustomerPhone={(value) => dispatch({ type: "SET_NEW_ORDER_CUSTOMER_PHONE", value })}
onSetNewOrderStopId={(value) => dispatch({ type: "SET_NEW_ORDER_STOP_ID", value })}
onAddNewOrderItem={addNewOrderItem}
onUpdateNewOrderItem={updateNewOrderItem}
onRemoveNewOrderItem={removeNewOrderItem}
onCreateAdminOrder={handleCreateAdminOrder}
/>
);
}
// ── OrdersAdminView (presentation) ─────────────────────────────────────────────
function OrdersAdminView({
brandId,
isPending,
orders,
filteredOrders,
paginatedOrders,
pendingCount,
pickedUpCount,
totalPages,
page,
search,
activeTab,
selectedStops,
selectedOrders,
bulkMarkingUp,
showStopDropdown,
pickingUp,
stops,
products,
showNewOrderModal,
newOrderCustomerName,
newOrderCustomerEmail,
newOrderCustomerPhone,
newOrderStopId,
newOrderItems,
newOrderSubmitting,
newOrderError,
newOrderTotal,
onActiveTabChange,
onSearchChange,
onSearchClear,
onPageChange,
onToggleSelectAll,
onToggleOrderSelection,
onClearOrderSelection,
onBulkMarkPickup,
onMarkPickup,
onToggleStopDropdown,
onToggleStop,
onClearStops,
onOpenNewOrderModal,
onCloseNewOrderModal,
onSetNewOrderCustomerName,
onSetNewOrderCustomerEmail,
onSetNewOrderCustomerPhone,
onSetNewOrderStopId,
onAddNewOrderItem,
onUpdateNewOrderItem,
onRemoveNewOrderItem,
onCreateAdminOrder,
}: {
brandId: string | null;
isPending: boolean;
orders: Order[];
filteredOrders: Order[];
paginatedOrders: Order[];
pendingCount: number;
pickedUpCount: number;
totalPages: number;
page: number;
search: string;
activeTab: StatusTab;
selectedStops: string[];
selectedOrders: Set<string>;
bulkMarkingUp: boolean;
showStopDropdown: boolean;
pickingUp: string | null;
stops: Stop[];
products: Product[];
showNewOrderModal: boolean;
newOrderCustomerName: string;
newOrderCustomerEmail: string;
newOrderCustomerPhone: string;
newOrderStopId: string;
newOrderItems: NewOrderItem[];
newOrderSubmitting: boolean;
newOrderError: string | null;
newOrderTotal: number;
onActiveTabChange: (value: StatusTab) => void;
onSearchChange: (value: string) => void;
onSearchClear: () => void;
onPageChange: (p: number) => void;
onToggleSelectAll: () => void;
onToggleOrderSelection: (orderId: string) => void;
onClearOrderSelection: () => void;
onBulkMarkPickup: () => void;
onMarkPickup: (orderId: string) => void;
onToggleStopDropdown: (value: boolean) => void;
onToggleStop: (stopId: string) => void;
onClearStops: () => void;
onOpenNewOrderModal: () => void;
onCloseNewOrderModal: () => void;
onSetNewOrderCustomerName: (value: string) => void;
onSetNewOrderCustomerEmail: (value: string) => void;
onSetNewOrderCustomerPhone: (value: string) => void;
onSetNewOrderStopId: (value: string) => void;
onAddNewOrderItem: (productId: string) => void;
onUpdateNewOrderItem: (index: number, patch: Partial<NewOrderItem>) => void;
onRemoveNewOrderItem: (index: number) => void;
onCreateAdminOrder: () => void;
}) {
return (
<>
<div className="p-4 sm:p-6">
<OrdersPageHeader
filteredCount={filteredOrders.length}
brandId={brandId}
onNewOrder={onOpenNewOrderModal}
/>
<OrdersStatsCards
isPending={isPending}
ordersCount={orders.length}
pendingCount={pendingCount}
pickedUpCount={pickedUpCount}
/>
<OrdersFilterBar
activeTab={activeTab}
onActiveTabChange={onActiveTabChange}
filteredCount={filteredOrders.length}
pendingCount={pendingCount}
pickedUpCount={pickedUpCount}
search={search}
onSearchChange={onSearchChange}
onSearchClear={onSearchClear}
stops={stops}
selectedStops={selectedStops}
showStopDropdown={showStopDropdown}
onToggleStopDropdown={onToggleStopDropdown}
onToggleStop={onToggleStop}
onClearStops={onClearStops}
/>
<SelectedStopChips
stops={stops}
selectedStops={selectedStops}
onToggleStop={onToggleStop}
/>
<BulkActionsBar
selectedCount={selectedOrders.size}
bulkMarkingUp={bulkMarkingUp}
onClear={onClearOrderSelection}
onMarkAll={onBulkMarkPickup}
/>
<OrdersTableSection
isPending={isPending}
ordersCount={orders.length}
paginatedOrders={paginatedOrders}
selectedOrders={selectedOrders}
pickingUp={pickingUp}
onToggleSelectAll={onToggleSelectAll}
onToggleOrderSelection={onToggleOrderSelection}
onMarkPickup={onMarkPickup}
/>
<OrdersPagination
page={page}
totalPages={totalPages}
totalCount={filteredOrders.length}
onPageChange={onPageChange}
/>
</div>
{showNewOrderModal && (
<NewOrderModal
stops={stops}
products={products}
customerName={newOrderCustomerName}
customerEmail={newOrderCustomerEmail}
customerPhone={newOrderCustomerPhone}
stopId={newOrderStopId}
items={newOrderItems}
submitting={newOrderSubmitting}
error={newOrderError}
total={newOrderTotal}
onClose={onCloseNewOrderModal}
onSetCustomerName={onSetNewOrderCustomerName}
onSetCustomerEmail={onSetNewOrderCustomerEmail}
onSetCustomerPhone={onSetNewOrderCustomerPhone}
onSetStopId={onSetNewOrderStopId}
onAddItem={onAddNewOrderItem}
onUpdateItem={onUpdateNewOrderItem}
onRemoveItem={onRemoveNewOrderItem}
onSubmit={onCreateAdminOrder}
/>
)}
</>
);
}
// ── Page Header ────────────────────────────────────────────────────────────────
function OrdersPageHeader({
filteredCount,
brandId,
onNewOrder,
}: {
filteredCount: number;
brandId: string | null;
onNewOrder: () => void;
}) {
return (
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<div
className="flex h-10 w-10 items-center justify-center rounded-xl"
style={{ backgroundColor: "var(--admin-accent)" }}
>
<Package className="h-5 w-5 text-white" strokeWidth={2} />
</div>
<div>
<h2
className="text-base sm:text-lg font-bold"
style={{ color: "var(--admin-text-primary)" }}
>
Orders
</h2>
<p
className="text-[10px] sm:text-xs"
style={{ color: "var(--admin-text-muted)" }}
>
{filteredCount} order{filteredCount !== 1 ? "s" : ""}
</p>
</div>
</div>
<div className="flex items-center gap-2">
{brandId && (
<AdminBadge tone="info">Brand scoped</AdminBadge>
)}
<AdminButton
onClick={onNewOrder}
size="sm"
className="flex items-center gap-2"
>
+ New Order
</AdminButton>
</div>
</div>
);
}
// ── Stats Cards ───────────────────────────────────────────────────────────────
function OrdersStatsCards({
isPending,
ordersCount,
pendingCount,
pickedUpCount,
}: {
isPending: boolean;
ordersCount: number;
pendingCount: number;
pickedUpCount: number;
}) {
return (
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3 mb-6">
<KPIStat
label="Total"
value={isPending ? <Skeleton variant="text" className="w-16 h-8" /> : ordersCount}
tone="default"
/>
<KPIStat
label="Pending"
value={isPending ? <Skeleton variant="text" className="w-12 h-8" /> : pendingCount}
tone="warning"
/>
<KPIStat
label="Picked Up"
value={isPending ? <Skeleton variant="text" className="w-12 h-8" /> : pickedUpCount}
tone="primary"
/>
</div>
);
}
// ── Filter Bar ────────────────────────────────────────────────────────────────
function OrdersFilterBar({
activeTab,
onActiveTabChange,
filteredCount,
pendingCount,
pickedUpCount,
search,
onSearchChange,
onSearchClear,
stops,
selectedStops,
showStopDropdown,
onToggleStopDropdown,
onToggleStop,
onClearStops,
}: {
activeTab: StatusTab;
onActiveTabChange: (value: StatusTab) => void;
filteredCount: number;
pendingCount: number;
pickedUpCount: number;
search: string;
onSearchChange: (value: string) => void;
onSearchClear: () => void;
stops: Stop[];
selectedStops: string[];
showStopDropdown: boolean;
onToggleStopDropdown: (value: boolean) => void;
onToggleStop: (stopId: string) => void;
onClearStops: () => void;
}) {
return (
<div className="flex flex-col sm:flex-row gap-3 mb-6">
<AdminFilterTabs
activeTab={activeTab}
onTabChange={(value) => onActiveTabChange(value as StatusTab)}
tabs={[
{ value: "all", label: "All", count: filteredCount },
{ value: "pending", label: "Pending", count: pendingCount },
{ value: "picked_up", label: "Picked Up", count: pickedUpCount },
]}
size="md"
/>
<AdminSearchInput
placeholder="Search name, phone, or order #..."
value={search}
onChange={(e) => onSearchChange(e.target.value)}
onClear={onSearchClear}
showClear={true}
/>
<StopFilterDropdown
stops={stops}
selectedStops={selectedStops}
showStopDropdown={showStopDropdown}
onToggleStopDropdown={onToggleStopDropdown}
onToggleStop={onToggleStop}
onClearStops={onClearStops}
/>
</div>
);
}
function StopFilterDropdown({
stops,
selectedStops,
showStopDropdown,
onToggleStopDropdown,
onToggleStop,
onClearStops,
}: {
stops: Stop[];
selectedStops: string[];
showStopDropdown: boolean;
onToggleStopDropdown: (value: boolean) => void;
onToggleStop: (stopId: string) => void;
onClearStops: () => void;
}) {
return (
<div className="relative">
<button type="button"
onClick={() => onToggleStopDropdown(!showStopDropdown)}
className="flex items-center gap-2 rounded-xl border px-4 py-2.5 text-sm font-medium transition-colors"
style={
selectedStops.length > 0
? {
borderColor: "var(--admin-accent)",
backgroundColor: "var(--admin-accent-light)",
color: "var(--admin-accent-text)",
}
: {
borderColor: "var(--admin-border)",
backgroundColor: "var(--admin-card-bg)",
color: "var(--admin-text-secondary)",
}
}
>
<MapPinIcon />
<span>Stops</span>
{selectedStops.length > 0 && (
<span
className="rounded-full text-white px-1.5 py-0.5 text-xs font-semibold"
style={{ backgroundColor: "var(--admin-accent)" }}
>
{selectedStops.length}
</span>
)}
<ChevronDownIcon />
</button>
{showStopDropdown && (
<>
<button type="button" aria-label="Close dropdown" className="fixed inset-0 z-10 cursor-default bg-transparent border-0 p-0" onClick={() => onToggleStopDropdown(false)} />
<div
className="absolute right-0 top-full mt-2 z-20 w-72 rounded-xl border shadow-lg"
style={{
borderColor: "var(--admin-border)",
backgroundColor: "var(--admin-card-bg)",
}}
>
<div
className="flex items-center justify-between border-b px-4 py-3"
style={{ borderColor: "var(--admin-border-light)" }}
>
<span
className="text-sm font-semibold"
style={{ color: "var(--admin-text-primary)" }}
>
Filter by Stop
</span>
<button type="button"
onClick={onClearStops}
className="text-xs font-medium"
style={{ color: "var(--admin-accent)" }}
>
Clear all
</button>
</div>
<div className="max-h-64 overflow-y-auto p-2">
{stops.map((stop) => (
<label
key={stop.id}
className="flex items-center gap-3 rounded-lg px-3 py-2 cursor-pointer transition-colors"
style={{ color: "var(--admin-text-primary)" }}
htmlFor="fld-togglestopstopid-classnameh-4-w-4-rounde">
<input
type="checkbox"
checked={selectedStops.includes(stop.id)}
onChange={() => onToggleStop(stop.id)}
className="h-4 w-4 rounded"
style={{
borderColor: "var(--admin-border-strong)",
accentColor: "var(--admin-accent)",
}}
/>
<div className="flex-1">
<span
className="text-sm font-medium"
style={{ color: "var(--admin-text-primary)" }}
>
{stop.city}, {stop.state}
</span>
<span
className="ml-2 text-xs"
style={{ color: "var(--admin-text-muted)" }}
>
{formatDate(stop.date)}
</span>
</div>
</label>
))}
</div>
</div>
</>
)}
</div>
);
}
// ── Selected Stop Chips ────────────────────────────────────────────────────────
function SelectedStopChips({
stops,
selectedStops,
onToggleStop,
}: {
stops: Stop[];
selectedStops: string[];
onToggleStop: (stopId: string) => void;
}) {
if (selectedStops.length === 0) return null;
return (
<div className="flex flex-wrap items-center gap-2 mb-4">
{selectedStops.map((stopId) => {
const stop = stops.find((s) => s.id === stopId);
if (!stop) return null;
return (
<span
key={stopId}
className="inline-flex items-center gap-1 rounded-full border px-3 py-1 text-xs font-medium"
style={{
backgroundColor: "var(--admin-accent-light)",
borderColor: "var(--admin-accent)",
color: "var(--admin-accent-text)",
}}
>
{stop.city}, {stop.state}
<button type="button"
onClick={() => onToggleStop(stopId)}
className="ml-1"
style={{ color: "var(--admin-accent-hover)" }}
aria-label={`Remove ${stop.city}, ${stop.state}`}
>
<XIcon />
</button>
</span>
);
})}
</div>
);
}
// ── Bulk Actions Bar ──────────────────────────────────────────────────────────
function BulkActionsBar({
selectedCount,
bulkMarkingUp,
onClear,
onMarkAll,
}: {
selectedCount: number;
bulkMarkingUp: boolean;
onClear: () => void;
onMarkAll: () => void;
}) {
if (selectedCount === 0) return null;
return (
<div
className="mb-4 flex items-center justify-between rounded-xl border px-4 py-3"
style={{
borderColor: "var(--admin-accent)",
backgroundColor: "var(--admin-accent-light)",
}}
>
<div className="flex items-center gap-3">
<span
className="text-sm font-semibold"
style={{ color: "var(--admin-accent-text)" }}
>
{selectedCount} order{selectedCount !== 1 ? 's' : ''} selected
</span>
<button type="button"
onClick={onClear}
className="text-xs"
style={{ color: "var(--admin-text-muted)" }}
>
Clear
</button>
</div>
<AdminButton
variant="primary"
size="sm"
onClick={onMarkAll}
isLoading={bulkMarkingUp}
icon={<CheckIcon />}
>
Mark All as Picked Up
</AdminButton>
</div>
);
}
// ── Orders Table Section ──────────────────────────────────────────────────────
function OrdersTableSection({
isPending,
ordersCount,
paginatedOrders,
selectedOrders,
pickingUp,
onToggleSelectAll,
onToggleOrderSelection,
onMarkPickup,
}: {
isPending: boolean;
ordersCount: number;
paginatedOrders: Order[];
selectedOrders: Set<string>;
pickingUp: string | null;
onToggleSelectAll: () => void;
onToggleOrderSelection: (orderId: string) => void;
onMarkPickup: (orderId: string) => void;
}) {
if (isPending) {
return (
<div
className="rounded-xl border p-6"
style={{
borderColor: "var(--admin-border)",
backgroundColor: "var(--admin-card-bg)",
}}
>
<div className="space-y-4">
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="flex items-center gap-4">
<Skeleton variant="rect" className="h-5 w-5 rounded" />
<Skeleton variant="text" className="flex-1" />
<Skeleton variant="text" className="w-24" />
<Skeleton variant="text" className="w-16" />
</div>
))}
</div>
</div>
);
}
if (paginatedOrders.length === 0) {
return (
<div
className="rounded-xl border"
style={{
borderColor: "var(--admin-border)",
backgroundColor: "var(--admin-card-bg)",
}}
>
<EmptyState
icon={<Package className="h-9 w-9" strokeWidth={1.25} />}
title={ordersCount === 0 ? "No orders yet" : "No orders match your filters"}
description={
ordersCount === 0
? "Create your first order to get started."
: "Try adjusting your filters or clearing your search."
}
action={
ordersCount === 0
? { label: "Create your first order", href: "/admin/orders?new=true" }
: undefined
}
/>
</div>
);
}
return (
<OrdersTable
paginatedOrders={paginatedOrders}
selectedOrders={selectedOrders}
pickingUp={pickingUp}
onToggleSelectAll={onToggleSelectAll}
onToggleOrderSelection={onToggleOrderSelection}
onMarkPickup={onMarkPickup}
/>
);
}
function OrdersTable({
paginatedOrders,
selectedOrders,
pickingUp,
onToggleSelectAll,
onToggleOrderSelection,
onMarkPickup,
}: {
paginatedOrders: Order[];
selectedOrders: Set<string>;
pickingUp: string | null;
onToggleSelectAll: () => void;
onToggleOrderSelection: (orderId: string) => void;
onMarkPickup: (orderId: string) => void;
}) {
return (
<div
className="overflow-x-auto rounded-xl border"
style={{
borderColor: "var(--admin-border)",
backgroundColor: "var(--admin-card-bg)",
}}
>
<table className="w-full text-sm min-w-[700px]">
<thead style={{ backgroundColor: "var(--admin-bg)" }}>
<tr style={{ borderBottom: "1px solid var(--admin-border)" }}>
<th className="w-10 px-4 py-3">
<input id="fld-togglestopstopid-classnameh-4-w-4-rounde" aria-label="Checkbox"
type="checkbox"
checked={selectedOrders.size === paginatedOrders.length && paginatedOrders.length > 0}
onChange={onToggleSelectAll}
className="h-4 w-4 rounded cursor-pointer"
style={{
borderColor: "var(--admin-border-strong)",
accentColor: "var(--admin-accent)",
}}
/>
</th>
<th className="text-left px-4 py-3 font-semibold text-xs" style={{ color: "var(--admin-text-muted)" }}>Order</th>
<th className="text-left px-4 py-3 font-semibold text-xs" style={{ color: "var(--admin-text-muted)" }}>Customer</th>
<th className="text-left px-4 py-3 font-semibold text-xs hidden md:table-cell" style={{ color: "var(--admin-text-muted)" }}>Stop</th>
<th className="text-left px-4 py-3 font-semibold text-xs hidden lg:table-cell" style={{ color: "var(--admin-text-muted)" }}>Date</th>
<th className="text-center px-4 py-3 font-semibold text-xs" style={{ color: "var(--admin-text-muted)" }}>Items</th>
<th className="text-right px-4 py-3 font-semibold text-xs" style={{ color: "var(--admin-text-muted)" }}>Total</th>
<th className="text-center px-4 py-3 font-semibold text-xs" style={{ color: "var(--admin-text-muted)" }}>Status</th>
</tr>
</thead>
<tbody style={{ borderColor: "var(--admin-border)" }}>
{paginatedOrders.map((order) => (
<OrderRow
key={order.id}
order={order}
isSelected={selectedOrders.has(order.id)}
isPickingUp={pickingUp === order.id}
onToggleSelect={() => onToggleOrderSelection(order.id)}
onMarkPickup={() => onMarkPickup(order.id)}
/>
))}
</tbody>
</table>
</div>
);
}
function OrderRow({
order,
isSelected,
isPickingUp,
onToggleSelect,
onMarkPickup,
}: {
order: Order;
isSelected: boolean;
isPickingUp: boolean;
onToggleSelect: () => void;
onMarkPickup: () => void;
}) {
return (
<tr
className="transition-colors"
style={{ borderTop: "1px solid var(--admin-border-light)" }}
>
<td className="px-4 py-3">
<input aria-label="Checkbox"
type="checkbox"
checked={isSelected}
onChange={onToggleSelect}
className="h-4 w-4 rounded cursor-pointer"
style={{
borderColor: "var(--admin-border-strong)",
accentColor: "var(--admin-accent)",
}}
/>
</td>
<td className="px-4 py-3">
<Link
href={`/admin/orders/${order.id}`}
className="font-mono text-xs"
style={{ color: "var(--admin-accent)" }}
>
{shortId(order.id)}
</Link>
</td>
<td className="px-4 py-3">
<div className="font-medium" style={{ color: "var(--admin-text-primary)" }}>
{order.customer_name}
</div>
{order.customer_phone && (
<div className="font-mono text-xs" style={{ color: "var(--admin-text-muted)" }}>
{order.customer_phone}
</div>
)}
</td>
<td className="px-4 py-3 hidden md:table-cell">
{order.stops ? (
<span className="text-sm" style={{ color: "var(--admin-text-primary)" }}>
{order.stops.city}, {order.stops.state}
</span>
) : (
<span style={{ color: "var(--admin-text-muted)" }}></span>
)}
</td>
<td className="px-4 py-3 hidden lg:table-cell">
<span className="text-sm" style={{ color: "var(--admin-text-muted)" }}>
{formatDate(order.created_at)}
</span>
</td>
<td className="px-4 py-3 text-center">
<span className="font-mono text-sm" style={{ color: "var(--admin-text-secondary)" }}>
{order.order_items?.length ?? 0}
</span>
</td>
<td className="px-4 py-3 text-right">
<span className="font-mono font-semibold" style={{ color: "var(--admin-text-primary)" }}>
{formatCurrency(order.subtotal)}
</span>
</td>
<td className="px-4 py-3 text-center">
<div className="flex items-center justify-center gap-2">
{order.pickup_complete ? (
<AdminBadge tone="success" dot>Picked Up</AdminBadge>
) : (
<AdminBadge tone="warning" dot>Pending</AdminBadge>
)}
{order.payment_processor === "square" && (
<AdminBadge tone="info">Square</AdminBadge>
)}
{!order.pickup_complete && (
<AdminButton
variant="ghost"
size="sm"
onClick={onMarkPickup}
disabled={isPickingUp}
isLoading={isPickingUp}
>
{isPickingUp ? "..." : "Pick Up"}
</AdminButton>
)}
</div>
</td>
</tr>
);
}
// ── Pagination ────────────────────────────────────────────────────────────────
function OrdersPagination({
page,
totalPages,
totalCount,
onPageChange,
}: {
page: number;
totalPages: number;
totalCount: number;
onPageChange: (p: number) => void;
}) {
if (totalPages <= 1) return null;
return (
<div
className="flex items-center justify-between mt-4 pt-4 border-t"
style={{ borderColor: "var(--admin-border)" }}
>
<p className="text-xs" style={{ color: "var(--admin-text-muted)" }}>
Showing {(page * PAGE_SIZE) + 1} to {Math.min((page + 1) * PAGE_SIZE, totalCount)} of {totalCount}
</p>
<div className="flex items-center gap-2">
<AdminIconButton
variant="secondary"
size="sm"
label="Previous page"
onClick={() => onPageChange(Math.max(0, page - 1))}
disabled={page === 0}
className="!rounded-lg"
>
<ChevronLeftIcon />
</AdminIconButton>
<span className="px-3 text-sm font-medium" style={{ color: "var(--admin-text-primary)" }}>
{page + 1} / {totalPages}
</span>
<AdminIconButton
variant="secondary"
size="sm"
label="Next page"
onClick={() => onPageChange(Math.min(totalPages - 1, page + 1))}
disabled={page >= totalPages - 1}
className="!rounded-lg"
>
<ChevronRightIcon />
</AdminIconButton>
</div>
</div>
);
}
// ── New Order Modal ───────────────────────────────────────────────────────────
function NewOrderModal({
stops,
products,
customerName,
customerEmail,
customerPhone,
stopId,
items,
submitting,
error,
total,
onClose,
onSetCustomerName,
onSetCustomerEmail,
onSetCustomerPhone,
onSetStopId,
onAddItem,
onUpdateItem,
onRemoveItem,
onSubmit,
}: {
stops: Stop[];
products: Product[];
customerName: string;
customerEmail: string;
customerPhone: string;
stopId: string;
items: NewOrderItem[];
submitting: boolean;
error: string | null;
total: number;
onClose: () => void;
onSetCustomerName: (value: string) => void;
onSetCustomerEmail: (value: string) => void;
onSetCustomerPhone: (value: string) => void;
onSetStopId: (value: string) => void;
onAddItem: (productId: string) => void;
onUpdateItem: (index: number, patch: Partial<NewOrderItem>) => void;
onRemoveItem: (index: number) => void;
onSubmit: () => void;
}) {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4" style={{ backgroundColor: "rgba(26, 24, 20, 0.4)" }}>
<div
className="w-full max-w-2xl rounded-2xl border shadow-2xl"
style={{
borderColor: "var(--admin-border)",
backgroundColor: "var(--admin-card-bg)",
}}
>
<div
className="flex items-center justify-between border-b px-6 py-4"
style={{ borderColor: "var(--admin-border)" }}
>
<div>
<h3 className="text-lg font-semibold" style={{ color: "var(--admin-text-primary)" }}>
New Order (Admin)
</h3>
<p className="text-xs" style={{ color: "var(--admin-text-muted)" }}>
Manual entry for testing / phone orders
</p>
</div>
<button type="button"
onClick={onClose}
className="text-2xl leading-none"
style={{ color: "var(--admin-text-muted)" }}
aria-label="Close"
>
×
</button>
</div>
<div className="p-6 space-y-5 max-h-[70vh] overflow-auto">
{error && (
<div
className="rounded-lg border px-4 py-2 text-sm"
style={{
backgroundColor: "var(--admin-danger-soft)",
borderColor: "var(--admin-danger-soft)",
color: "var(--admin-danger)",
}}
>
{error}
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<AdminInput label="Customer Name" required>
<AdminTextInput
value={customerName}
onChange={(e) => onSetCustomerName(e.target.value)}
placeholder="Jane Doe"
/>
</AdminInput>
<AdminInput label="Email">
<AdminTextInput
value={customerEmail}
onChange={(e) => onSetCustomerEmail(e.target.value)}
placeholder="jane@example.com"
type="email"
/>
</AdminInput>
<AdminInput label="Phone">
<AdminTextInput
value={customerPhone}
onChange={(e) => onSetCustomerPhone(e.target.value)}
placeholder="(555) 123-4567"
/>
</AdminInput>
</div>
<AdminInput label="Pickup Stop (leave blank for ship-only)">
<AdminSelect
value={stopId}
onChange={(e) => onSetStopId(e.target.value)}
options={[
{ value: "", label: "— No stop (ship / manual fulfillment) —" },
...stops.map((s) => ({
value: s.id,
label: `${s.city}, ${s.state}${formatDate(s.date)}`,
})),
]}
/>
</AdminInput>
<NewOrderItemsEditor
products={products}
items={items}
total={total}
onAdd={onAddItem}
onUpdate={onUpdateItem}
onRemove={onRemoveItem}
/>
</div>
<div
className="flex items-center justify-end gap-3 border-t px-6 py-4 rounded-b-2xl"
style={{
borderColor: "var(--admin-border)",
backgroundColor: "var(--admin-bg-subtle)",
}}
>
<AdminButton variant="secondary" onClick={onClose} disabled={submitting}>
Cancel
</AdminButton>
<AdminButton onClick={onSubmit} disabled={submitting || items.length === 0}>
{submitting ? "Creating..." : `Create Order — $${total.toFixed(2)}`}
</AdminButton>
</div>
</div>
</div>
);
}
function NewOrderItemsEditor({
products,
items,
total,
onAdd,
onUpdate,
onRemove,
}: {
products: Product[];
items: NewOrderItem[];
total: number;
onAdd: (productId: string) => void;
onUpdate: (index: number, patch: Partial<NewOrderItem>) => void;
onRemove: (index: number) => void;
}) {
return (
<div>
<div className="flex items-center justify-between mb-2">
<label className="block text-xs font-semibold" style={{ color: "var(--admin-text-secondary)" }} htmlFor="fld-items">
Items
</label>
<span className="text-xs" style={{ color: "var(--admin-text-muted)" }}>
Total: ${total.toFixed(2)}
</span>
</div>
{products.length > 0 ? (
<div className="mb-3">
<AdminSelect
value=""
onChange={(e) => {
if (e.target.value) onAdd(e.target.value);
}}
options={[
{ value: "", label: "— Add a product —" },
...products.map((p) => ({ value: p.id, label: `${p.name}$${Number(p.price).toFixed(2)}` })),
]}
/>
</div>
) : (
<p className="text-xs mb-2" style={{ color: "var(--admin-warning)" }}>
No products loaded for this brand. You can still enter custom items if the action supports it.
</p>
)}
{items.length === 0 && (
<p className="text-xs" style={{ color: "var(--admin-text-muted)" }}>
No items yet. Use the selector above.
</p>
)}
{items.length > 0 && (
<div
className="space-y-2 border rounded-xl p-3"
style={{
borderColor: "var(--admin-border)",
backgroundColor: "var(--admin-bg)",
}}
>
{items.map((item, idx) => {
const prod = products.find((p) => p.id === item.product_id);
return (
<div key={`${item.product_id}-${item.fulfillment}-${idx}`} className="grid grid-cols-12 gap-2 items-center text-sm">
<div className="col-span-5 font-medium truncate" style={{ color: "var(--admin-text-primary)" }}>
{prod?.name ?? item.product_id}
</div>
<div className="col-span-2">
<input id="fld-items" aria-label="Number"
type="number"
min={1}
value={item.quantity}
onChange={(e) => onUpdate(idx, { quantity: Math.max(1, parseInt(e.target.value) || 1) })}
className="w-full rounded border px-2 py-1 text-sm"
style={{
borderColor: "var(--admin-border)",
backgroundColor: "var(--admin-card-bg)",
color: "var(--admin-text-primary)",
}}
/>
</div>
<div className="col-span-2">
<input aria-label="Number"
type="number"
step="0.01"
value={item.price}
onChange={(e) => onUpdate(idx, { price: parseFloat(e.target.value) || 0 })}
className="w-full rounded border px-2 py-1 text-sm"
style={{
borderColor: "var(--admin-border)",
backgroundColor: "var(--admin-card-bg)",
color: "var(--admin-text-primary)",
}}
/>
</div>
<div className="col-span-2">
<select aria-label="Select"
value={item.fulfillment}
onChange={(e) => onUpdate(idx, { fulfillment: e.target.value as "pickup" | "ship" })}
className="w-full rounded border px-2 py-1 text-sm"
style={{
borderColor: "var(--admin-border)",
backgroundColor: "var(--admin-card-bg)",
color: "var(--admin-text-primary)",
}}
>
<option value="pickup">pickup</option>
<option value="ship">ship</option>
</select>
</div>
<div className="col-span-1 text-right">
<button
type="button"
onClick={() => onRemove(idx)}
className="text-xs hover:underline"
style={{ color: "var(--admin-danger)" }}
>
remove
</button>
</div>
</div>
);
})}
</div>
)}
</div>
);
}