feat(admin): apply design system to Orders list/detail/edit/table

- Add PageHeader + Operations eyebrow on list page
- Detail page: PageHeader with order-id/date eyebrow, customer title,
  total/status subtitle, AdminBadge tone for pickup + payment processor
- AdminOrdersPanel: KPIStat for stat cards, EmptyState with Create your
  first order CTA, AdminBadge tone for status pills, lucide-react icons,
  all hardcoded Tailwind colors replaced with var(--admin-*) tokens
- OrderTableBody: AdminBadge tone for status + pickup pills, all hardcoded
  colors replaced with tokens
- OrderEditForm: ha-field-label + ha-field-input / ha-field-textarea classes
  for form fields, ha-segment control for status buttons, semantic pickup
  toggle with primary/warning tokens, all hardcoded colors replaced

Behavior preserved end-to-end. Type-check clean.
This commit is contained in:
Tyler
2026-06-17 00:34:58 -06:00
parent 685a1269a4
commit 467f7e63fd
5 changed files with 849 additions and 292 deletions
+427 -145
View File
@@ -3,11 +3,31 @@
import { useState, useMemo, useCallback, useEffect } 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, type AdminCreateOrderItem } from "@/actions/orders/create-admin-order";
import { formatDate } from "@/lib/format-date";
import AdminBadge from "./design-system/AdminBadge";
import { AdminButton, AdminSearchInput, AdminFilterTabs, AdminIconButton, useToast, AdminInput, AdminTextInput, AdminSelect } from "./design-system";
import {
AdminButton,
AdminSearchInput,
AdminFilterTabs,
AdminIconButton,
useToast,
AdminInput,
AdminTextInput,
AdminSelect,
EmptyState,
KPIStat,
} from "./design-system";
import { Skeleton } from "./design-system";
type OrderItem = {
@@ -66,55 +86,13 @@ function formatCurrency(amount: number) {
return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(amount);
}
// Icons
const Icons = {
mapPin: (className: string) => (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0z"/>
<path d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1 1 15 0z"/>
</svg>
),
check: (className: string) => (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="20 6 9 17 4 12"/>
</svg>
),
x: (className: string) => (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
),
chevronDown: (className: string) => (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="m6 9 6 6 6-6"/>
</svg>
),
chevronLeft: (className: string) => (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="m15 18-6-6 6-6"/>
</svg>
),
chevronRight: (className: string) => (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="m9 18 6-6-6-6"/>
</svg>
),
package: (className: string) => (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M16.5 9.4 7.55 4.24"/>
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
<polyline points="3.29 7 12 12 20.71 7"/>
<line x1="12" y1="22" x2="12" y2="12"/>
</svg>
),
selectAll: (className: string) => (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
<polyline points="9 11 12 14 22 4"/>
</svg>
),
};
// 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} />;
export default function AdminOrdersPanel({
initialOrders,
@@ -318,11 +296,11 @@ export default function AdminOrdersPanel({
async function handleBulkMarkPickup() {
if (selectedOrders.size === 0) return;
setBulkMarkingUp(true);
let successCount = 0;
let failCount = 0;
for (const orderId of selectedOrders) {
const result = await markPickupComplete(orderId, brandId);
if (result.success) {
@@ -338,10 +316,10 @@ export default function AdminOrdersPanel({
failCount++;
}
}
setBulkMarkingUp(false);
setSelectedOrders(new Set());
if (failCount === 0) {
showSuccess(`${successCount} order${successCount !== 1 ? 's' : ''} marked as picked up`);
} else {
@@ -355,12 +333,23 @@ export default function AdminOrdersPanel({
{/* Header */}
<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 bg-[var(--admin-accent)]">
{Icons.package("h-5 w-5 text-white")}
<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 text-[var(--admin-text-primary)]">Orders</h2>
<p className="text-[10px] sm:text-xs text-[var(--admin-text-muted)]">
<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)" }}
>
{filteredOrders.length} order{filteredOrders.length !== 1 ? "s" : ""}
</p>
</div>
@@ -368,7 +357,7 @@ export default function AdminOrdersPanel({
<div className="flex items-center gap-2">
{brandId && (
<AdminBadge variant="info">Brand scoped</AdminBadge>
<AdminBadge tone="info">Brand scoped</AdminBadge>
)}
<AdminButton
onClick={openNewOrderModal}
@@ -382,18 +371,21 @@ export default function AdminOrdersPanel({
{/* Stats Cards */}
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3 mb-6">
<div className="bg-white rounded-xl border border-[var(--admin-border)] p-4">
<p className="text-[10px] sm:text-xs text-[var(--admin-text-muted)] font-medium">Total</p>
<p className="text-lg sm:text-xl md:text-2xl font-bold text-[var(--admin-text-primary)] mt-1">{isLoading ? <Skeleton variant="text" className="w-16 h-8" /> : orders.length}</p>
</div>
<div className="bg-white rounded-xl border border-[var(--admin-border)] p-4">
<p className="text-[10px] sm:text-xs text-[var(--admin-text-muted)] font-medium">Pending</p>
<p className="text-lg sm:text-xl md:text-2xl font-bold text-amber-600 mt-1">{isLoading ? <Skeleton variant="text" className="w-12 h-8" /> : pendingCount}</p>
</div>
<div className="bg-white rounded-xl border border-[var(--admin-border)] p-4">
<p className="text-[10px] sm:text-xs text-[var(--admin-text-muted)] font-medium">Picked Up</p>
<p className="text-lg sm:text-xl md:text-2xl font-bold text-[var(--admin-accent)] mt-1">{isLoading ? <Skeleton variant="text" className="w-12 h-8" /> : pickedUpCount}</p>
</div>
<KPIStat
label="Total"
value={isLoading ? <Skeleton variant="text" className="w-16 h-8" /> : orders.length}
tone="default"
/>
<KPIStat
label="Pending"
value={isLoading ? <Skeleton variant="text" className="w-12 h-8" /> : pendingCount}
tone="warning"
/>
<KPIStat
label="Picked Up"
value={isLoading ? <Skeleton variant="text" className="w-12 h-8" /> : pickedUpCount}
tone="primary"
/>
</div>
{/* Filters */}
@@ -423,40 +415,92 @@ export default function AdminOrdersPanel({
<div className="relative">
<button
onClick={() => setShowStopDropdown(!showStopDropdown)}
className={`flex items-center gap-2 rounded-xl border px-4 py-2.5 text-sm font-medium transition-colors ${
className="flex items-center gap-2 rounded-xl border px-4 py-2.5 text-sm font-medium transition-colors"
style={
selectedStops.length > 0
? "border-[var(--admin-accent)] bg-[var(--admin-accent-light)] text-[var(--admin-accent-text)]"
: "border-[var(--admin-border)] bg-white text-stone-600 hover:bg-stone-50"
}`}
? {
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)",
}
}
>
{Icons.mapPin("h-4 w-4")}
<MapPinIcon />
<span>Stops</span>
{selectedStops.length > 0 && (
<span className="rounded-full bg-[var(--admin-accent)] text-white px-1.5 py-0.5 text-xs font-semibold">{selectedStops.length}</span>
<span
className="rounded-full text-white px-1.5 py-0.5 text-xs font-semibold"
style={{ backgroundColor: "var(--admin-accent)" }}
>
{selectedStops.length}
</span>
)}
{Icons.chevronDown("h-4 w-4")}
<ChevronDownIcon />
</button>
{showStopDropdown && (
<>
<div className="fixed inset-0 z-10" onClick={() => setShowStopDropdown(false)} />
<div className="absolute right-0 top-full mt-2 z-20 w-72 rounded-xl border border-[var(--admin-border)] bg-white shadow-lg">
<div className="flex items-center justify-between border-b border-stone-100 px-4 py-3">
<span className="text-sm font-semibold text-stone-700">Filter by Stop</span>
<button onClick={clearStops} className="text-xs text-[var(--admin-accent)] hover:text-[var(--admin-accent-hover)] font-medium">Clear all</button>
<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
onClick={clearStops}
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 hover:bg-stone-50 cursor-pointer">
<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)" }}
>
<input
type="checkbox"
checked={selectedStops.includes(stop.id)}
onChange={() => toggleStop(stop.id)}
className="h-4 w-4 rounded border-stone-300 text-[var(--admin-accent)] focus:ring-[var(--admin-accent)]"
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 text-stone-700">{stop.city}, {stop.state}</span>
<span className="ml-2 text-xs text-stone-400">{formatDate(stop.date)}</span>
<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>
))}
@@ -474,10 +518,23 @@ export default function AdminOrdersPanel({
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 bg-[var(--admin-accent-light)] border border-[var(--admin-accent)] px-3 py-1 text-xs font-medium text-[var(--admin-accent-text)]">
<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 onClick={() => toggleStop(stopId)} className="ml-1 hover:text-[var(--admin-accent-hover)]">
{Icons.x("h-3 w-3")}
<button
onClick={() => toggleStop(stopId)}
className="ml-1"
style={{ color: "var(--admin-accent-hover)" }}
aria-label={`Remove ${stop.city}, ${stop.state}`}
>
<XIcon />
</button>
</span>
);
@@ -487,14 +544,24 @@ export default function AdminOrdersPanel({
{/* Bulk actions bar */}
{selectedOrders.size > 0 && (
<div className="mb-4 flex items-center justify-between rounded-xl border border-[var(--admin-accent)] bg-[var(--admin-accent-light)] px-4 py-3">
<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 text-[var(--admin-accent-text)]">
<span
className="text-sm font-semibold"
style={{ color: "var(--admin-accent-text)" }}
>
{selectedOrders.size} order{selectedOrders.size !== 1 ? 's' : ''} selected
</span>
<button
onClick={() => setSelectedOrders(new Set())}
className="text-xs text-[var(--admin-text-muted)] hover:text-[var(--admin-text-secondary)]"
className="text-xs"
style={{ color: "var(--admin-text-muted)" }}
>
Clear
</button>
@@ -504,7 +571,7 @@ export default function AdminOrdersPanel({
size="sm"
onClick={handleBulkMarkPickup}
isLoading={bulkMarkingUp}
icon={Icons.check("h-4 w-4")}
icon={<CheckIcon />}
>
Mark All as Picked Up
</AdminButton>
@@ -513,7 +580,13 @@ export default function AdminOrdersPanel({
{/* Orders Table */}
{isLoading ? (
<div className="rounded-xl border border-[var(--admin-border)] bg-white p-6">
<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">
@@ -526,80 +599,184 @@ export default function AdminOrdersPanel({
</div>
</div>
) : paginatedOrders.length === 0 ? (
<div className="text-center py-12 rounded-xl border border-[var(--admin-border)] bg-white">
<div className="flex h-16 w-16 mx-auto items-center justify-center rounded-full bg-stone-100 mb-4">
{Icons.package("h-8 w-8 text-stone-400")}
</div>
<p className="text-sm font-medium text-stone-600">No orders found</p>
<p className="text-xs text-stone-400 mt-1">Try adjusting your filters</p>
<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={orders.length === 0 ? "No orders yet" : "No orders match your filters"}
description={
orders.length === 0
? "Create your first order to get started."
: "Try adjusting your filters or clearing your search."
}
action={
orders.length === 0
? { label: "Create your first order", href: "/admin/orders?new=true" }
: undefined
}
/>
</div>
) : (
<div className="overflow-x-auto rounded-xl border border-[var(--admin-border)] bg-white">
<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 className="bg-stone-50">
<tr className="border-b border-[var(--admin-border)]">
<thead style={{ backgroundColor: "var(--admin-bg)" }}>
<tr style={{ borderBottom: "1px solid var(--admin-border)" }}>
<th className="w-10 px-4 py-3">
<input
type="checkbox"
checked={selectedOrders.size === paginatedOrders.length && paginatedOrders.length > 0}
onChange={toggleSelectAll}
className="h-4 w-4 rounded border-stone-300 text-[var(--admin-accent)] focus:ring-[var(--admin-accent)] cursor-pointer"
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-[var(--admin-text-muted)] text-xs">Order</th>
<th className="text-left px-4 py-3 font-semibold text-[var(--admin-text-muted)] text-xs">Customer</th>
<th className="text-left px-4 py-3 font-semibold text-[var(--admin-text-muted)] text-xs hidden md:table-cell">Stop</th>
<th className="text-left px-4 py-3 font-semibold text-[var(--admin-text-muted)] text-xs hidden lg:table-cell">Date</th>
<th className="text-center px-4 py-3 font-semibold text-[var(--admin-text-muted)] text-xs">Items</th>
<th className="text-right px-4 py-3 font-semibold text-[var(--admin-text-muted)] text-xs">Total</th>
<th className="text-center px-4 py-3 font-semibold text-[var(--admin-text-muted)] text-xs">Status</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 className="divide-y divide-[var(--admin-border)]">
<tbody style={{ borderColor: "var(--admin-border)" }}>
{paginatedOrders.map((order) => (
<tr key={order.id} className="hover:bg-stone-50 transition-colors">
<tr
key={order.id}
className="transition-colors"
style={{ borderTop: "1px solid var(--admin-border-light)" }}
>
<td className="px-4 py-3">
<input
type="checkbox"
checked={selectedOrders.has(order.id)}
onChange={() => toggleOrderSelection(order.id)}
className="h-4 w-4 rounded border-stone-300 text-[var(--admin-accent)] focus:ring-[var(--admin-accent)] cursor-pointer"
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 text-[var(--admin-accent)] hover:text-[var(--admin-accent-hover)]">
<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 text-[var(--admin-text-primary)]">{order.customer_name}</div>
{order.customer_phone && <div className="font-mono text-xs text-stone-400">{order.customer_phone}</div>}
<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 text-[var(--admin-text-primary)]">{order.stops.city}, {order.stops.state}</span>
<span
className="text-sm"
style={{ color: "var(--admin-text-primary)" }}
>
{order.stops.city}, {order.stops.state}
</span>
) : (
<span className="text-stone-300"></span>
<span style={{ color: "var(--admin-text-muted)" }}></span>
)}
</td>
<td className="px-4 py-3 hidden lg:table-cell">
<span className="text-sm text-stone-500">{formatDate(order.created_at)}</span>
<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 text-stone-600">{order.order_items?.length ?? 0}</span>
<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 text-[var(--admin-text-primary)]">{formatCurrency(order.subtotal)}</span>
<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 variant="success" dot>Picked Up</AdminBadge>
<AdminBadge tone="success" dot>Picked Up</AdminBadge>
) : (
<AdminBadge variant="warning" dot>Pending</AdminBadge>
<AdminBadge tone="warning" dot>Pending</AdminBadge>
)}
{order.payment_processor === "square" && (
<AdminBadge variant="info">Square</AdminBadge>
<AdminBadge tone="info">Square</AdminBadge>
)}
{!order.pickup_complete && (
<AdminButton
@@ -623,8 +800,14 @@ export default function AdminOrdersPanel({
{/* Pagination */}
{totalPages > 1 && (
<div className="flex items-center justify-between mt-4 pt-4 border-t border-[var(--admin-border)]">
<p className="text-xs text-[var(--admin-text-muted)]">
<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, filteredOrders.length)} of {filteredOrders.length}
</p>
<div className="flex items-center gap-2">
@@ -636,9 +819,14 @@ export default function AdminOrdersPanel({
disabled={page === 0}
className="!rounded-lg"
>
{Icons.chevronLeft("h-4 w-4")}
<ChevronLeftIcon />
</AdminIconButton>
<span className="px-3 text-sm font-medium text-stone-700">{page + 1} / {totalPages}</span>
<span
className="px-3 text-sm font-medium"
style={{ color: "var(--admin-text-primary)" }}
>
{page + 1} / {totalPages}
</span>
<AdminIconButton
variant="secondary"
size="sm"
@@ -647,7 +835,7 @@ export default function AdminOrdersPanel({
disabled={page >= totalPages - 1}
className="!rounded-lg"
>
{Icons.chevronRight("h-4 w-4")}
<ChevronRightIcon />
</AdminIconButton>
</div>
</div>
@@ -656,19 +844,54 @@ export default function AdminOrdersPanel({
{/* New Order Modal (admin manual entry) - sibling to the main panel content */}
{showNewOrderModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4">
<div className="w-full max-w-2xl rounded-2xl border border-[var(--admin-border)] bg-white shadow-2xl">
<div className="flex items-center justify-between border-b border-[var(--admin-border)] px-6 py-4">
<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 text-[var(--admin-text-primary)]">New Order (Admin)</h3>
<p className="text-xs text-[var(--admin-text-muted)]">Manual entry for testing / phone orders</p>
<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 onClick={closeNewOrderModal} className="text-2xl leading-none text-stone-400 hover:text-stone-600">×</button>
<button
onClick={closeNewOrderModal}
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">
{newOrderError && (
<div className="rounded-lg bg-red-50 border border-red-200 px-4 py-2 text-sm text-red-700">{newOrderError}</div>
<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)",
}}
>
{newOrderError}
</div>
)}
{/* Customer */}
@@ -715,8 +938,18 @@ export default function AdminOrdersPanel({
{/* Items */}
<div>
<div className="flex items-center justify-between mb-2">
<label className="block text-xs font-semibold text-[var(--admin-text-secondary)]">Items</label>
<span className="text-xs text-[var(--admin-text-muted)]">Total: ${newOrderTotal.toFixed(2)}</span>
<label
className="block text-xs font-semibold"
style={{ color: "var(--admin-text-secondary)" }}
>
Items
</label>
<span
className="text-xs"
style={{ color: "var(--admin-text-muted)" }}
>
Total: ${newOrderTotal.toFixed(2)}
</span>
</div>
{products.length > 0 ? (
@@ -733,20 +966,41 @@ export default function AdminOrdersPanel({
/>
</div>
) : (
<p className="text-xs text-amber-600 mb-2">No products loaded for this brand. You can still enter custom items if the action supports it.</p>
<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>
)}
{newOrderItems.length === 0 && (
<p className="text-xs text-[var(--admin-text-muted)]">No items yet. Use the selector above.</p>
<p
className="text-xs"
style={{ color: "var(--admin-text-muted)" }}
>
No items yet. Use the selector above.
</p>
)}
{newOrderItems.length > 0 && (
<div className="space-y-2 border border-[var(--admin-border)] rounded-xl p-3 bg-[var(--admin-bg)]">
<div
className="space-y-2 border rounded-xl p-3"
style={{
borderColor: "var(--admin-border)",
backgroundColor: "var(--admin-bg)",
}}
>
{newOrderItems.map((item, idx) => {
const prod = products.find((p) => p.id === item.product_id);
return (
<div key={idx} className="grid grid-cols-12 gap-2 items-center text-sm">
<div className="col-span-5 font-medium truncate">{prod?.name ?? item.product_id}</div>
<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
type="number"
@@ -754,6 +1008,11 @@ export default function AdminOrdersPanel({
value={item.quantity}
onChange={(e) => updateNewOrderItem(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">
@@ -763,6 +1022,11 @@ export default function AdminOrdersPanel({
value={item.price}
onChange={(e) => updateNewOrderItem(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">
@@ -770,13 +1034,25 @@ export default function AdminOrdersPanel({
value={item.fulfillment}
onChange={(e) => updateNewOrderItem(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={() => removeNewOrderItem(idx)} className="text-red-500 text-xs hover:underline">remove</button>
<button
type="button"
onClick={() => removeNewOrderItem(idx)}
className="text-xs hover:underline"
style={{ color: "var(--admin-danger)" }}
>
remove
</button>
</div>
</div>
);
@@ -786,7 +1062,13 @@ export default function AdminOrdersPanel({
</div>
</div>
<div className="flex items-center justify-end gap-3 border-t border-[var(--admin-border)] px-6 py-4 bg-stone-50 rounded-b-2xl">
<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={closeNewOrderModal} disabled={newOrderSubmitting}>
Cancel
</AdminButton>