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:
@@ -4,6 +4,8 @@ import OrderEditForm from "@/components/admin/OrderEditForm";
|
||||
import OrderPaymentSection from "@/components/admin/OrderPaymentSection";
|
||||
import OrderPickupAction from "@/components/admin/OrderPickupAction";
|
||||
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
||||
import AdminBadge from "@/components/admin/design-system/AdminBadge";
|
||||
import { PageHeader } from "@/components/admin/design-system";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { redirect } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
@@ -34,16 +36,31 @@ export default async function OrderDetailPage({ params }: OrderDetailPageProps)
|
||||
|
||||
if (!order) {
|
||||
return (
|
||||
<main className="min-h-screen px-6 py-10">
|
||||
<main
|
||||
className="min-h-screen px-6 py-10"
|
||||
style={{ backgroundColor: "var(--admin-bg)" }}
|
||||
>
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<Link
|
||||
href="/admin/orders"
|
||||
className="inline-flex items-center gap-2 text-sm text-stone-500 hover:text-stone-700"
|
||||
className="inline-flex items-center gap-2 text-sm transition-colors"
|
||||
style={{ color: "var(--admin-text-muted)" }}
|
||||
>
|
||||
← Back to Orders
|
||||
</Link>
|
||||
<div className="mt-8 rounded-2xl border border-red-200 bg-red-50 p-8 text-center">
|
||||
<p className="text-lg font-semibold text-red-700">Order not found</p>
|
||||
<div
|
||||
className="mt-8 rounded-2xl border p-8 text-center"
|
||||
style={{
|
||||
borderColor: "var(--admin-danger-soft)",
|
||||
backgroundColor: "var(--admin-danger-soft)",
|
||||
}}
|
||||
>
|
||||
<p
|
||||
className="text-lg font-semibold"
|
||||
style={{ color: "var(--admin-danger)" }}
|
||||
>
|
||||
Order not found
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@@ -67,46 +84,66 @@ export default async function OrderDetailPage({ params }: OrderDetailPageProps)
|
||||
const total = subtotal + taxAmount - discount_amount;
|
||||
|
||||
return (
|
||||
<main className="min-h-screen px-6 py-8">
|
||||
<main
|
||||
className="min-h-screen px-6 py-8"
|
||||
style={{ backgroundColor: "var(--admin-bg)" }}
|
||||
>
|
||||
<div className="mx-auto max-w-4xl space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Back link */}
|
||||
<Link
|
||||
href="/admin/orders"
|
||||
className="flex h-9 w-9 items-center justify-center rounded-xl border border-stone-200 bg-white text-stone-500 hover:bg-stone-50 transition-colors"
|
||||
className="inline-flex items-center gap-2 text-xs font-semibold transition-colors"
|
||||
style={{ color: "var(--admin-text-muted)" }}
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
Back to Orders
|
||||
</Link>
|
||||
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-stone-950 tracking-tight">Order Details</h1>
|
||||
<p className="text-sm text-stone-500">{formatDate(order.created_at)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className={`rounded-full px-3 py-1 text-xs font-bold ${
|
||||
order.pickup_complete
|
||||
? "bg-green-50 text-green-700 border border-green-200"
|
||||
: "bg-amber-50 text-amber-700 border border-amber-200"
|
||||
}`}>
|
||||
{order.pickup_complete ? "✓ Picked Up" : "⏳ Pending"}
|
||||
</span>
|
||||
<p className="ha-eyebrow mb-2">
|
||||
Order #{order.id.slice(0, 8).toUpperCase()} · {formatDate(order.created_at)}
|
||||
</p>
|
||||
<PageHeader
|
||||
title={order.customer_name}
|
||||
subtitle={`${formatCurrency(total)} · ${order.pickup_complete ? "Picked Up" : "Awaiting Pickup"}${order.stops ? ` · ${order.stops.city}, ${order.stops.state}` : ""}`}
|
||||
actions={
|
||||
<div className="flex items-center gap-2">
|
||||
<AdminBadge tone={order.pickup_complete ? "success" : "warning"} dot>
|
||||
{order.pickup_complete ? "Picked Up" : "Pending"}
|
||||
</AdminBadge>
|
||||
{order.payment_processor && (
|
||||
<span className="rounded-full bg-violet-50 px-2.5 py-0.5 text-xs font-semibold text-violet-700 border border-violet-200">
|
||||
{order.payment_processor}
|
||||
</span>
|
||||
<AdminBadge tone="info">{order.payment_processor}</AdminBadge>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Customer info */}
|
||||
<div className="rounded-2xl border border-stone-200 bg-white p-6 shadow-sm">
|
||||
<div
|
||||
className="rounded-2xl border p-6 shadow-sm"
|
||||
style={{
|
||||
borderColor: "var(--admin-border)",
|
||||
backgroundColor: "var(--admin-card-bg)",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-stone-400">Customer</p>
|
||||
<h2 className="mt-1 text-2xl font-bold text-stone-950">{order.customer_name}</h2>
|
||||
<p
|
||||
className="text-xs font-semibold uppercase tracking-wider"
|
||||
style={{ color: "var(--admin-text-muted)" }}
|
||||
>
|
||||
Customer
|
||||
</p>
|
||||
<h2
|
||||
className="mt-1 text-2xl font-bold"
|
||||
style={{ color: "var(--admin-text-primary)" }}
|
||||
>
|
||||
{order.customer_name}
|
||||
</h2>
|
||||
</div>
|
||||
<OrderPickupAction
|
||||
orderId={order.id}
|
||||
@@ -118,14 +155,34 @@ export default async function OrderDetailPage({ params }: OrderDetailPageProps)
|
||||
<div className="mt-5 grid grid-cols-2 gap-4">
|
||||
{order.customer_phone && (
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-stone-400">Phone</p>
|
||||
<p className="mt-0.5 text-sm font-medium text-stone-700">{order.customer_phone}</p>
|
||||
<p
|
||||
className="text-xs font-semibold"
|
||||
style={{ color: "var(--admin-text-muted)" }}
|
||||
>
|
||||
Phone
|
||||
</p>
|
||||
<p
|
||||
className="mt-0.5 text-sm font-medium"
|
||||
style={{ color: "var(--admin-text-primary)" }}
|
||||
>
|
||||
{order.customer_phone}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{order.customer_email && (
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-stone-400">Email</p>
|
||||
<p className="mt-0.5 text-sm font-medium text-stone-700">{order.customer_email}</p>
|
||||
<p
|
||||
className="text-xs font-semibold"
|
||||
style={{ color: "var(--admin-text-muted)" }}
|
||||
>
|
||||
Email
|
||||
</p>
|
||||
<p
|
||||
className="mt-0.5 text-sm font-medium"
|
||||
style={{ color: "var(--admin-text-primary)" }}
|
||||
>
|
||||
{order.customer_email}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -133,64 +190,133 @@ export default async function OrderDetailPage({ params }: OrderDetailPageProps)
|
||||
|
||||
{/* Stop info */}
|
||||
{order.stops && (
|
||||
<div className="rounded-2xl border border-stone-200 bg-white p-6 shadow-sm">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-stone-400">Pickup Location</p>
|
||||
<p className="mt-1 text-lg font-bold text-stone-950">
|
||||
<div
|
||||
className="rounded-2xl border p-6 shadow-sm"
|
||||
style={{
|
||||
borderColor: "var(--admin-border)",
|
||||
backgroundColor: "var(--admin-card-bg)",
|
||||
}}
|
||||
>
|
||||
<p
|
||||
className="text-xs font-semibold uppercase tracking-wider"
|
||||
style={{ color: "var(--admin-text-muted)" }}
|
||||
>
|
||||
Pickup Location
|
||||
</p>
|
||||
<p
|
||||
className="mt-1 text-lg font-bold"
|
||||
style={{ color: "var(--admin-text-primary)" }}
|
||||
>
|
||||
{order.stops.city}, {order.stops.state}
|
||||
</p>
|
||||
<p className="mt-0.5 text-sm text-stone-500">{formatDate(order.stops.date)}</p>
|
||||
<p
|
||||
className="mt-0.5 text-sm"
|
||||
style={{ color: "var(--admin-text-muted)" }}
|
||||
>
|
||||
{formatDate(order.stops.date)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Order items */}
|
||||
<div className="rounded-2xl border border-stone-200 bg-white p-6 shadow-sm">
|
||||
<h3 className="text-lg font-bold text-stone-950">Order Items</h3>
|
||||
<div
|
||||
className="rounded-2xl border p-6 shadow-sm"
|
||||
style={{
|
||||
borderColor: "var(--admin-border)",
|
||||
backgroundColor: "var(--admin-card-bg)",
|
||||
}}
|
||||
>
|
||||
<h3
|
||||
className="text-lg font-bold"
|
||||
style={{ color: "var(--admin-text-primary)" }}
|
||||
>
|
||||
Order Items
|
||||
</h3>
|
||||
|
||||
{order.order_items && order.order_items.length > 0 ? (
|
||||
<div className="mt-4 divide-y divide-stone-100">
|
||||
<div
|
||||
className="mt-4 divide-y"
|
||||
style={{ borderColor: "var(--admin-border-light)" }}
|
||||
>
|
||||
{order.order_items.map((item: OrderItem) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex items-center justify-between py-3"
|
||||
>
|
||||
<div>
|
||||
<p className="font-medium text-stone-900">
|
||||
<p
|
||||
className="font-medium"
|
||||
style={{ color: "var(--admin-text-primary)" }}
|
||||
>
|
||||
{item.products?.name ?? "Unknown Product"}
|
||||
</p>
|
||||
<p className="text-xs text-stone-400">Qty: {item.quantity} × {formatCurrency(Number(item.price))}</p>
|
||||
<p
|
||||
className="text-xs"
|
||||
style={{ color: "var(--admin-text-muted)" }}
|
||||
>
|
||||
Qty: {item.quantity} × {formatCurrency(Number(item.price))}
|
||||
</p>
|
||||
</div>
|
||||
<p className="font-semibold text-stone-900">
|
||||
<p
|
||||
className="font-semibold"
|
||||
style={{ color: "var(--admin-text-primary)" }}
|
||||
>
|
||||
{formatCurrency(Number(item.price) * item.quantity)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="mt-4 text-sm text-stone-400">No items found</p>
|
||||
<p
|
||||
className="mt-4 text-sm"
|
||||
style={{ color: "var(--admin-text-muted)" }}
|
||||
>
|
||||
No items found
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Totals */}
|
||||
<div className="mt-6 border-t border-stone-100 pt-6 space-y-2">
|
||||
<div
|
||||
className="mt-6 border-t pt-6 space-y-2"
|
||||
style={{ borderColor: "var(--admin-border-light)" }}
|
||||
>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-stone-500">Subtotal</span>
|
||||
<span className="text-stone-900">{formatCurrency(subtotal)}</span>
|
||||
<span style={{ color: "var(--admin-text-muted)" }}>Subtotal</span>
|
||||
<span style={{ color: "var(--admin-text-primary)" }}>
|
||||
{formatCurrency(subtotal)}
|
||||
</span>
|
||||
</div>
|
||||
{taxAmount > 0 && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-stone-500">Tax</span>
|
||||
<span className="text-stone-900">{formatCurrency(taxAmount)}</span>
|
||||
<span style={{ color: "var(--admin-text-muted)" }}>Tax</span>
|
||||
<span style={{ color: "var(--admin-text-primary)" }}>
|
||||
{formatCurrency(taxAmount)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{discount_amount > 0 && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-stone-500">Discount</span>
|
||||
<span className="text-red-600">-{formatCurrency(discount_amount)}</span>
|
||||
<span style={{ color: "var(--admin-text-muted)" }}>Discount</span>
|
||||
<span style={{ color: "var(--admin-danger)" }}>
|
||||
-{formatCurrency(discount_amount)}
|
||||
</span>
|
||||
{order.discount_reason && (
|
||||
<span className="text-xs text-stone-400 ml-2">({order.discount_reason})</span>
|
||||
<span
|
||||
className="text-xs ml-2"
|
||||
style={{ color: "var(--admin-text-muted)" }}
|
||||
>
|
||||
({order.discount_reason})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between text-lg font-bold text-stone-950 pt-2 border-t border-stone-200">
|
||||
<div
|
||||
className="flex justify-between text-lg font-bold pt-2 border-t"
|
||||
style={{
|
||||
color: "var(--admin-text-primary)",
|
||||
borderColor: "var(--admin-border)",
|
||||
}}
|
||||
>
|
||||
<span>Total</span>
|
||||
<span>{formatCurrency(total)}</span>
|
||||
</div>
|
||||
@@ -198,9 +324,25 @@ export default async function OrderDetailPage({ params }: OrderDetailPageProps)
|
||||
</div>
|
||||
|
||||
{/* Payment & Refunds */}
|
||||
<div className="rounded-2xl border border-stone-200 bg-white p-6 shadow-sm">
|
||||
<h3 className="text-lg font-bold text-stone-950">Payment & Refunds</h3>
|
||||
<p className="mt-1 text-sm text-stone-500">Record payment details and manage refunds</p>
|
||||
<div
|
||||
className="rounded-2xl border p-6 shadow-sm"
|
||||
style={{
|
||||
borderColor: "var(--admin-border)",
|
||||
backgroundColor: "var(--admin-card-bg)",
|
||||
}}
|
||||
>
|
||||
<h3
|
||||
className="text-lg font-bold"
|
||||
style={{ color: "var(--admin-text-primary)" }}
|
||||
>
|
||||
Payment & Refunds
|
||||
</h3>
|
||||
<p
|
||||
className="mt-1 text-sm"
|
||||
style={{ color: "var(--admin-text-muted)" }}
|
||||
>
|
||||
Record payment details and manage refunds
|
||||
</p>
|
||||
|
||||
<div className="mt-5">
|
||||
<OrderPaymentSection
|
||||
@@ -218,9 +360,25 @@ export default async function OrderDetailPage({ params }: OrderDetailPageProps)
|
||||
</div>
|
||||
|
||||
{/* Edit form */}
|
||||
<div className="rounded-2xl border border-stone-200 bg-white p-6 shadow-sm">
|
||||
<h3 className="text-lg font-bold text-stone-950">Edit Order</h3>
|
||||
<p className="mt-1 text-sm text-stone-500">Update customer details, pricing, and status</p>
|
||||
<div
|
||||
className="rounded-2xl border p-6 shadow-sm"
|
||||
style={{
|
||||
borderColor: "var(--admin-border)",
|
||||
backgroundColor: "var(--admin-card-bg)",
|
||||
}}
|
||||
>
|
||||
<h3
|
||||
className="text-lg font-bold"
|
||||
style={{ color: "var(--admin-text-primary)" }}
|
||||
>
|
||||
Edit Order
|
||||
</h3>
|
||||
<p
|
||||
className="mt-1 text-sm"
|
||||
style={{ color: "var(--admin-text-muted)" }}
|
||||
>
|
||||
Update customer details, pricing, and status
|
||||
</p>
|
||||
|
||||
<div className="mt-5">
|
||||
<OrderEditForm order={order as unknown as Parameters<typeof OrderEditForm>[0]["order"]} brandId={brandId} />
|
||||
@@ -229,9 +387,25 @@ export default async function OrderDetailPage({ params }: OrderDetailPageProps)
|
||||
|
||||
{/* Internal notes */}
|
||||
{order.internal_notes && (
|
||||
<div className="rounded-2xl border border-amber-200 bg-amber-50 p-6">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-amber-600">Internal Notes</p>
|
||||
<p className="mt-1 text-sm text-stone-700">{order.internal_notes}</p>
|
||||
<div
|
||||
className="rounded-2xl border p-6"
|
||||
style={{
|
||||
borderColor: "var(--admin-warning-soft)",
|
||||
backgroundColor: "var(--admin-warning-soft)",
|
||||
}}
|
||||
>
|
||||
<p
|
||||
className="text-xs font-semibold uppercase tracking-wider"
|
||||
style={{ color: "var(--admin-warning)" }}
|
||||
>
|
||||
Internal Notes
|
||||
</p>
|
||||
<p
|
||||
className="mt-1 text-sm"
|
||||
style={{ color: "var(--admin-text-primary)" }}
|
||||
>
|
||||
{order.internal_notes}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -65,11 +65,12 @@ export default async function AdminOrdersPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[var(--admin-bg)]">
|
||||
<div className="min-h-screen" style={{ backgroundColor: "var(--admin-bg)" }}>
|
||||
<div className="px-4 sm:px-6 md:px-8 pt-4 sm:pt-6">
|
||||
<p className="ha-eyebrow mb-2">Operations</p>
|
||||
<PageHeader
|
||||
title="Orders"
|
||||
subtitle="Manage customer orders and pickup status"
|
||||
subtitle="View, filter, and manage customer orders."
|
||||
icon={
|
||||
<svg className="h-5 w-5 sm:h-6 sm:w-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z"/>
|
||||
@@ -82,7 +83,13 @@ export default async function AdminOrdersPage() {
|
||||
|
||||
{/* Content */}
|
||||
<div className="px-4 sm:px-6 md:px-8 py-4 sm:py-6">
|
||||
<div className="rounded-2xl border border-[var(--admin-border)] bg-white overflow-hidden">
|
||||
<div
|
||||
className="rounded-2xl border overflow-hidden"
|
||||
style={{
|
||||
borderColor: "var(--admin-border)",
|
||||
backgroundColor: "var(--admin-card-bg)",
|
||||
}}
|
||||
>
|
||||
<AdminOrdersPanel
|
||||
initialOrders={brandOrders}
|
||||
initialStops={brandStops}
|
||||
|
||||
@@ -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,
|
||||
@@ -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>
|
||||
|
||||
@@ -198,8 +198,22 @@ export default function OrderEditForm({ order, brandId }: OrderEditFormProps) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{error && (
|
||||
<div className="rounded-xl bg-red-50 border border-red-200 p-4 text-sm text-red-600 flex items-start gap-3">
|
||||
<svg className="h-5 w-5 shrink-0 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<div
|
||||
className="rounded-xl border p-4 text-sm flex items-start gap-3"
|
||||
style={{
|
||||
borderColor: "var(--admin-danger-soft)",
|
||||
backgroundColor: "var(--admin-danger-soft)",
|
||||
color: "var(--admin-danger)",
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
className="h-5 w-5 shrink-0"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
style={{ color: "var(--admin-danger)" }}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
{error}
|
||||
@@ -208,11 +222,17 @@ export default function OrderEditForm({ order, brandId }: OrderEditFormProps) {
|
||||
|
||||
{/* Order items */}
|
||||
<div>
|
||||
<p className="mb-3 text-sm font-semibold text-stone-700">
|
||||
<p className="ha-field-label mb-3">
|
||||
Order Items ({visibleItems.length})
|
||||
</p>
|
||||
{visibleItems.length === 0 ? (
|
||||
<p className="rounded-xl border border-dashed border-stone-300 p-4 text-center text-sm text-stone-400">
|
||||
<p
|
||||
className="rounded-xl border border-dashed p-4 text-center text-sm"
|
||||
style={{
|
||||
borderColor: "var(--admin-border-strong)",
|
||||
color: "var(--admin-text-muted)",
|
||||
}}
|
||||
>
|
||||
No items
|
||||
</p>
|
||||
) : (
|
||||
@@ -220,13 +240,23 @@ export default function OrderEditForm({ order, brandId }: OrderEditFormProps) {
|
||||
{visibleItems.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="rounded-xl border border-stone-200 bg-white p-4"
|
||||
className="rounded-xl border p-4"
|
||||
style={{
|
||||
borderColor: "var(--admin-border)",
|
||||
backgroundColor: "var(--admin-card-bg)",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<p className="font-medium text-stone-900">{item.productName}</p>
|
||||
<p
|
||||
className="font-medium"
|
||||
style={{ color: "var(--admin-text-primary)" }}
|
||||
>
|
||||
{item.productName}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => removeItem(item.id)}
|
||||
className="shrink-0 rounded-lg px-2 py-1 text-xs font-medium text-red-600 hover:bg-red-50"
|
||||
className="shrink-0 rounded-lg px-2 py-1 text-xs font-medium"
|
||||
style={{ color: "var(--admin-danger)" }}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
@@ -234,7 +264,7 @@ export default function OrderEditForm({ order, brandId }: OrderEditFormProps) {
|
||||
|
||||
<div className="mt-3 grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-stone-500 mb-1">Qty</label>
|
||||
<label className="ha-field-label mb-1">Qty</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
@@ -242,13 +272,18 @@ export default function OrderEditForm({ order, brandId }: OrderEditFormProps) {
|
||||
onChange={(e) =>
|
||||
updateItem(item.id, "quantity", Number(e.target.value))
|
||||
}
|
||||
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-2 text-sm outline-none focus:border-[var(--admin-accent)] focus:ring-2 focus:ring-[var(--admin-accent)]/20"
|
||||
className="ha-field-input ha-field-input-mono"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-stone-500 mb-1">Price</label>
|
||||
<label className="ha-field-label mb-1">Price</label>
|
||||
<div className="relative">
|
||||
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-stone-400 text-sm">$</span>
|
||||
<span
|
||||
className="absolute left-3 top-1/2 -translate-y-1/2 text-sm"
|
||||
style={{ color: "var(--admin-text-muted)" }}
|
||||
>
|
||||
$
|
||||
</span>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
@@ -257,13 +292,16 @@ export default function OrderEditForm({ order, brandId }: OrderEditFormProps) {
|
||||
onChange={(e) =>
|
||||
updateItem(item.id, "price", Number(e.target.value))
|
||||
}
|
||||
className="w-full rounded-xl border border-stone-200 bg-stone-50 pl-8 pr-3 py-2 text-sm outline-none focus:border-[var(--admin-accent)] focus:ring-2 focus:ring-[var(--admin-accent)]/20"
|
||||
className="ha-field-input ha-field-input-mono pl-8 pr-3"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="mt-2 text-right text-sm font-semibold text-stone-900">
|
||||
<p
|
||||
className="mt-2 text-right text-sm font-semibold"
|
||||
style={{ color: "var(--admin-text-primary)" }}
|
||||
>
|
||||
{formatCurrency(Number(item.price) * item.quantity)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -280,48 +318,71 @@ export default function OrderEditForm({ order, brandId }: OrderEditFormProps) {
|
||||
step="0.01"
|
||||
value={subtotal}
|
||||
readOnly
|
||||
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-4 py-2.5 text-sm text-stone-500"
|
||||
className="ha-field-input ha-field-input-mono"
|
||||
style={{ color: "var(--admin-text-muted)" }}
|
||||
/>
|
||||
</AdminInput>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-stone-500 mb-1.5">Discount Amount</label>
|
||||
<label className="ha-field-label mb-1.5">Discount Amount</label>
|
||||
<div className="relative">
|
||||
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-stone-400 text-sm">$</span>
|
||||
<span
|
||||
className="absolute left-3 top-1/2 -translate-y-1/2 text-sm"
|
||||
style={{ color: "var(--admin-text-muted)" }}
|
||||
>
|
||||
$
|
||||
</span>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
value={discount_amount}
|
||||
onChange={(e) => setDiscount_amount(Number(e.target.value))}
|
||||
className={`w-full rounded-xl border px-8 pr-3 py-2.5 text-sm outline-none focus:ring-2 focus:ring-[var(--admin-accent)]/20 ${
|
||||
className="ha-field-input ha-field-input-mono pl-8 pr-3"
|
||||
style={
|
||||
fieldErrors.discount_amount
|
||||
? "border-red-400 bg-red-50"
|
||||
: "border-stone-200 bg-white focus:border-[var(--admin-accent)]"
|
||||
}`}
|
||||
? { borderColor: "var(--admin-danger)", backgroundColor: "var(--admin-danger-soft)" }
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{fieldErrors.discount_amount && (
|
||||
<p className="mt-1 text-xs text-red-500">{fieldErrors.discount_amount}</p>
|
||||
<p className="mt-1 text-xs" style={{ color: "var(--admin-danger)" }}>
|
||||
{fieldErrors.discount_amount}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-stone-500 mb-1.5">Discount Reason</label>
|
||||
<label className="ha-field-label mb-1.5">Discount Reason</label>
|
||||
<input
|
||||
type="text"
|
||||
value={discount_reason}
|
||||
onChange={(e) => setDiscount_reason(e.target.value)}
|
||||
placeholder="Optional"
|
||||
className="w-full rounded-xl border border-stone-200 bg-white px-3 py-2.5 text-sm outline-none focus:border-[var(--admin-accent)] focus:ring-2 focus:ring-[var(--admin-accent)]/20"
|
||||
className="ha-field-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-stone-200 bg-stone-50 p-4">
|
||||
<div
|
||||
className="rounded-xl border p-4"
|
||||
style={{
|
||||
borderColor: "var(--admin-border)",
|
||||
backgroundColor: "var(--admin-bg-subtle)",
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm font-medium text-stone-600">Total</span>
|
||||
<span className="text-xl font-bold text-stone-900">
|
||||
<span
|
||||
className="text-sm font-medium"
|
||||
style={{ color: "var(--admin-text-secondary)" }}
|
||||
>
|
||||
Total
|
||||
</span>
|
||||
<span
|
||||
className="text-xl font-bold"
|
||||
style={{ color: "var(--admin-text-primary)" }}
|
||||
>
|
||||
{formatCurrency(subtotal + Number(order.tax_amount ?? 0) - discount_amount)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -331,8 +392,9 @@ export default function OrderEditForm({ order, brandId }: OrderEditFormProps) {
|
||||
{/* Customer fields */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-stone-500 mb-1.5">
|
||||
Name <span className="text-red-500">*</span>
|
||||
<label className="ha-field-label mb-1.5">
|
||||
<span>Name</span>
|
||||
<span className="ha-field-label-required">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -345,14 +407,17 @@ export default function OrderEditForm({ order, brandId }: OrderEditFormProps) {
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
className={`w-full rounded-xl border px-3 py-2.5 text-sm outline-none focus:ring-2 focus:ring-[var(--admin-accent)]/20 ${
|
||||
className="ha-field-input"
|
||||
style={
|
||||
fieldErrors.customer_name
|
||||
? "border-red-400 bg-red-50"
|
||||
: "border-stone-200 bg-white focus:border-[var(--admin-accent)]"
|
||||
}`}
|
||||
? { borderColor: "var(--admin-danger)", backgroundColor: "var(--admin-danger-soft)" }
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
{fieldErrors.customer_name && (
|
||||
<p className="mt-1 text-xs text-red-500">{fieldErrors.customer_name}</p>
|
||||
<p className="mt-1 text-xs" style={{ color: "var(--admin-danger)" }}>
|
||||
{fieldErrors.customer_name}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
@@ -376,17 +441,15 @@ export default function OrderEditForm({ order, brandId }: OrderEditFormProps) {
|
||||
{/* Status & pickup */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-2 block text-xs font-semibold text-stone-500">Status</label>
|
||||
<div className="flex gap-2">
|
||||
<label className="ha-field-label mb-2">Status</label>
|
||||
<div className="ha-segment">
|
||||
{["pending", "confirmed", "cancelled"].map((s) => (
|
||||
<button
|
||||
key={s}
|
||||
type="button"
|
||||
onClick={() => setStatus(s)}
|
||||
className={`flex-1 rounded-xl px-4 py-2.5 text-sm font-semibold capitalize transition-colors ${
|
||||
status === s
|
||||
? "bg-[var(--admin-accent)] text-white"
|
||||
: "bg-white text-stone-600 border border-stone-200 hover:bg-stone-50"
|
||||
className={`ha-segment-btn capitalize ${
|
||||
status === s ? "ha-segment-btn--active" : ""
|
||||
}`}
|
||||
>
|
||||
{s}
|
||||
@@ -396,15 +459,24 @@ export default function OrderEditForm({ order, brandId }: OrderEditFormProps) {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-2 block text-xs font-semibold text-stone-500">Pickup</label>
|
||||
<label className="ha-field-label mb-2">Pickup</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPickup_complete((v) => !v)}
|
||||
className={`w-full rounded-xl px-4 py-2.5 text-sm font-semibold transition-colors ${
|
||||
className="ha-segment-btn w-full justify-center"
|
||||
style={
|
||||
pickup_complete
|
||||
? "bg-green-100 text-green-700 border border-green-200"
|
||||
: "bg-amber-100 text-amber-700 border border-amber-200"
|
||||
}`}
|
||||
? {
|
||||
backgroundColor: "var(--admin-primary-soft)",
|
||||
color: "var(--admin-primary)",
|
||||
border: "1px solid var(--admin-primary)",
|
||||
}
|
||||
: {
|
||||
backgroundColor: "var(--admin-warning-soft)",
|
||||
color: "var(--admin-warning)",
|
||||
border: "1px solid var(--admin-warning)",
|
||||
}
|
||||
}
|
||||
>
|
||||
{pickup_complete ? "✓ Picked Up" : "○ Not Picked Up"}
|
||||
</button>
|
||||
@@ -428,6 +500,7 @@ export default function OrderEditForm({ order, brandId }: OrderEditFormProps) {
|
||||
variant="primary"
|
||||
fullWidth
|
||||
size="lg"
|
||||
className="ha-btn-primary"
|
||||
>
|
||||
{saving ? "Saving..." : "Save Changes"}
|
||||
</AdminButton>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import AdminBadge from "./design-system/AdminBadge";
|
||||
import { toggleOrderPickupComplete } from "@/actions/orders";
|
||||
|
||||
type Order = {
|
||||
@@ -15,6 +16,16 @@ type Order = {
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
type OrderStatusTone = "warning" | "info" | "success" | "danger" | "neutral";
|
||||
|
||||
const statusToTone: Record<string, OrderStatusTone> = {
|
||||
pending: "warning",
|
||||
confirmed: "info",
|
||||
paid: "success",
|
||||
cancelled: "danger",
|
||||
completed: "neutral",
|
||||
};
|
||||
|
||||
export default function OrderTableBody({ orders }: { orders: Order[] }) {
|
||||
const [pickupToggles, setPickupToggles] = useState<Record<string, boolean>>(
|
||||
() => Object.fromEntries(orders.map((o) => [o.id, o.pickup_complete]))
|
||||
@@ -33,68 +44,78 @@ export default function OrderTableBody({ orders }: { orders: Order[] }) {
|
||||
}
|
||||
}
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
pending: "bg-yellow-100 text-yellow-700",
|
||||
confirmed: "bg-blue-900/40 text-blue-700",
|
||||
paid: "bg-green-900/40 text-green-400",
|
||||
cancelled: "bg-red-900/40 text-red-400",
|
||||
completed: "bg-zinc-950 text-zinc-400",
|
||||
};
|
||||
|
||||
return (
|
||||
<tbody className="divide-y divide-slate-200">
|
||||
<tbody style={{ borderColor: "var(--admin-border)" }}>
|
||||
{error && (
|
||||
<tr>
|
||||
<td colSpan={6} className="px-3 py-2 text-sm text-red-400">
|
||||
<td
|
||||
colSpan={6}
|
||||
className="px-3 py-2 text-sm"
|
||||
style={{ color: "var(--admin-danger)" }}
|
||||
>
|
||||
{error}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{orders.map((order) => (
|
||||
<tr key={order.id} className="hover:bg-zinc-800">
|
||||
<tr
|
||||
key={order.id}
|
||||
className="transition-colors"
|
||||
style={{ borderTop: "1px solid var(--admin-border-light)" }}
|
||||
>
|
||||
<td className="px-3 py-2">
|
||||
<span className="font-mono text-sm text-zinc-500">
|
||||
<span
|
||||
className="font-mono text-sm"
|
||||
style={{ color: "var(--admin-text-muted)" }}
|
||||
>
|
||||
{order.id.slice(0, 8)}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td className="px-3 py-2">
|
||||
<div className="font-medium text-zinc-100">
|
||||
<div
|
||||
className="font-medium"
|
||||
style={{ color: "var(--admin-text-primary)" }}
|
||||
>
|
||||
{order.customer_name}
|
||||
</div>
|
||||
<div className="text-sm text-zinc-500">
|
||||
<div
|
||||
className="text-sm"
|
||||
style={{ color: "var(--admin-text-muted)" }}
|
||||
>
|
||||
{order.customer_email}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td className="px-3 py-2">
|
||||
<span
|
||||
className={`rounded-full px-2 py-0.5 text-xs font-medium ${
|
||||
statusColors[order.status] ?? "bg-zinc-950 text-zinc-400"
|
||||
}`}
|
||||
>
|
||||
<AdminBadge tone={statusToTone[order.status] ?? "neutral"}>
|
||||
{order.status ?? "pending"}
|
||||
</span>
|
||||
</AdminBadge>
|
||||
</td>
|
||||
|
||||
<td className="px-3 py-2 font-semibold text-zinc-100">
|
||||
<td
|
||||
className="px-3 py-2 font-semibold"
|
||||
style={{ color: "var(--admin-text-primary)" }}
|
||||
>
|
||||
${Number(order.subtotal).toFixed(2)}
|
||||
</td>
|
||||
|
||||
<td className="px-3 py-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => togglePickup(order.id, pickupToggles[order.id])}
|
||||
className={`rounded-full px-2 py-0.5 text-xs font-medium ${
|
||||
pickupToggles[order.id]
|
||||
? "bg-green-900/40 text-green-400"
|
||||
: "bg-zinc-950 text-zinc-500"
|
||||
}`}
|
||||
className="transition-opacity hover:opacity-80"
|
||||
>
|
||||
<AdminBadge tone={pickupToggles[order.id] ? "success" : "neutral"} dot>
|
||||
{pickupToggles[order.id] ? "Picked up" : "Pending"}
|
||||
</AdminBadge>
|
||||
</button>
|
||||
</td>
|
||||
|
||||
<td className="px-3 py-2 text-sm text-zinc-500">
|
||||
<td
|
||||
className="px-3 py-2 text-sm"
|
||||
style={{ color: "var(--admin-text-muted)" }}
|
||||
>
|
||||
{formatDate(new Date(order.created_at))}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
Reference in New Issue
Block a user