diff --git a/estore/src/app/admin/page.tsx b/estore/src/app/admin/page.tsx index 4fc0c09..c6ebfac 100644 --- a/estore/src/app/admin/page.tsx +++ b/estore/src/app/admin/page.tsx @@ -756,17 +756,25 @@ function DeliveryRatesEditor() { // ─── Orders Panel ───────────────────────────────────────────────────────────── interface AdminOrder { - id: string - state: string - createdAt: string - customerName: string - customerPhone: string - address: string | null - type: 'delivery' | 'pickup' - slotISO: string | null - lineItems: { name: string; quantity: string; note: string | null }[] - totalCents: number | null - version: number + id: string + state: string + fulfillmentState: string + createdAt: string + customerName: string + customerPhone: string + address: string | null + type: 'delivery' | 'pickup' + slotISO: string | null + lineItems: { name: string; quantity: string; note: string | null }[] + totalCents: number | null + version: number +} + +const FULFILLMENT_LABELS: Record = { + PROPOSED: { label: 'New', color: '#888' }, + RESERVED: { label: 'In progress', color: '#d48806' }, + PREPARED: { label: 'Ready', color: '#1677ff' }, + COMPLETED:{ label: 'Complete', color: '#389e0d' }, } function fmtSlot(iso: string): string { @@ -778,11 +786,11 @@ function fmtSlot(iso: string): string { } function OrdersPanel() { - const [orders, setOrders] = useState([]) - const [loading, setLoading] = useState(true) - const [error, setError] = useState('') - const [completing, setCompleting] = useState(null) - const [completeMsg, setCompleteMsg] = useState>({}) + const [orders, setOrders] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState('') + const [updating, setUpdating] = useState(null) + const [updateMsg, setUpdateMsg] = useState>({}) const load = useCallback(async () => { setLoading(true) @@ -801,22 +809,29 @@ function OrdersPanel() { useEffect(() => { load() }, [load]) - async function handleComplete(orderId: string) { - if (!confirm('Mark this order as complete in Square?')) return - setCompleting(orderId) + async function handleSetState(orderId: string, fulfillmentState: string) { + setUpdating(orderId) + setUpdateMsg((prev) => ({ ...prev, [orderId]: '' })) try { - const res = await fetch(`${BASE}/api/admin/orders/${orderId}/complete`, { method: 'POST' }) + const res = await fetch(`${BASE}/api/admin/orders/${orderId}/status`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ fulfillmentState }), + }) const data = await res.json() if (res.ok) { - setOrders((prev) => prev.filter((o) => o.id !== orderId)) - setCompleteMsg((prev) => ({ ...prev, [orderId]: 'Done' })) + if (fulfillmentState === 'COMPLETED') { + setOrders((prev) => prev.filter((o) => o.id !== orderId)) + } else { + setOrders((prev) => prev.map((o) => o.id === orderId ? { ...o, fulfillmentState } : o)) + } } else { - setCompleteMsg((prev) => ({ ...prev, [orderId]: data.error ?? 'Failed' })) + setUpdateMsg((prev) => ({ ...prev, [orderId]: data.error ?? 'Failed' })) } } catch { - setCompleteMsg((prev) => ({ ...prev, [orderId]: 'Network error' })) + setUpdateMsg((prev) => ({ ...prev, [orderId]: 'Network error' })) } finally { - setCompleting(null) + setUpdating(null) } } @@ -883,16 +898,51 @@ function OrdersPanel() { {order.totalCents != null && ( ${(order.totalCents / 100).toFixed(2)} )} - - {completeMsg[order.id] && ( - {completeMsg[order.id]} + + {/* Status badge */} + {(() => { + const s = FULFILLMENT_LABELS[order.fulfillmentState] ?? { label: order.fulfillmentState, color: '#888' } + return ( + + ● {s.label} + + ) + })()} + + {/* Action buttons — next logical state(s) only */} +
+ {order.fulfillmentState === 'PROPOSED' && ( + + )} + {(order.fulfillmentState === 'PROPOSED' || order.fulfillmentState === 'RESERVED') && ( + + )} + +
+ + {updateMsg[order.id] && ( + {updateMsg[order.id]} )} {new Date(order.createdAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} diff --git a/estore/src/app/api/admin/orders/[orderId]/status/route.ts b/estore/src/app/api/admin/orders/[orderId]/status/route.ts new file mode 100644 index 0000000..b779408 --- /dev/null +++ b/estore/src/app/api/admin/orders/[orderId]/status/route.ts @@ -0,0 +1,50 @@ +import { NextRequest, NextResponse } from 'next/server' +import { Client, Environment } from 'square' + +type FulfillmentState = 'RESERVED' | 'PREPARED' | 'COMPLETED' + +function getClient() { + return new Client({ + accessToken: process.env.SQUARE_ACCESS_TOKEN!, + environment: process.env.SQUARE_ENVIRONMENT === 'production' + ? Environment.Production + : Environment.Sandbox, + }) +} + +export async function POST( + req: NextRequest, + { params }: { params: { orderId: string } } +) { + try { + const { orderId } = params + const { fulfillmentState } = await req.json() as { fulfillmentState: FulfillmentState } + + if (!['RESERVED', 'PREPARED', 'COMPLETED'].includes(fulfillmentState)) { + return NextResponse.json({ error: 'Invalid state' }, { status: 400 }) + } + + const client = getClient() + const { result: { order } } = await client.ordersApi.retrieveOrder(orderId) + if (!order) return NextResponse.json({ error: 'Order not found' }, { status: 404 }) + + await client.ordersApi.updateOrder(orderId, { + order: { + locationId: order.locationId!, + version: order.version, + // Only close the order when fulfillment is fully complete + ...(fulfillmentState === 'COMPLETED' ? { state: 'COMPLETED' } : {}), + fulfillments: (order.fulfillments ?? []).map((f) => ({ + uid: f.uid, + state: fulfillmentState, + })), + }, + idempotencyKey: `status-${orderId}-${fulfillmentState}`, + }) + + return NextResponse.json({ ok: true }) + } catch (err) { + console.error('[admin/orders/status]', err) + return NextResponse.json({ error: 'Failed to update order' }, { status: 500 }) + } +} diff --git a/estore/src/app/api/admin/orders/route.ts b/estore/src/app/api/admin/orders/route.ts index fec2fcb..6b1dfaa 100644 --- a/estore/src/app/api/admin/orders/route.ts +++ b/estore/src/app/api/admin/orders/route.ts @@ -43,6 +43,7 @@ export async function GET() { address: delivery?.recipient?.address?.addressLine1 ?? null, type: type === 'DELIVERY' ? 'delivery' : 'pickup', slotISO, + fulfillmentState: fulfillment?.state ?? 'PROPOSED', lineItems: (o.lineItems ?? []).map((li) => ({ name: li.name ?? '', quantity: li.quantity ?? '1',