Store Pickup Curbside
blockcommerceBuy Online Pick Up in Store (BOPIS) & curbside pickup coordination board with customer arrival parking check-in, real-time staging bin tracking, runner fulfillment dispatch, and SLA monitoring.
Installation
$pnpm dlx shadcn@latest add https://uipkge.dev/r/react/store-pickup-curbside.json$npx shadcn@latest add https://uipkge.dev/r/react/store-pickup-curbside.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/store-pickup-curbside.json$bunx shadcn@latest add https://uipkge.dev/r/react/store-pickup-curbside.jsonNamed registry:
npx shadcn@latest add @uipkge-react/store-pickup-curbsideInstalls to:components/blocks/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
OrderIteminterface OrderItem {
id: string
name: string
sku: string
quantity: number
price: number
staged: boolean
category: string
}PickupOrderinterface PickupOrder {
id: string
orderNumber: string
customerName: string
customerPhone: string
customerInitials: string
avatar?: string
pickupMode: PickupMode
spotNumber?: number
vehicleDetails?: {
makeModel: string
plate: string
hazardsFlashing?: boolean
}
pickupDeskBay?: string
status: OrderStatus
statusLabel: string
etaMinutes?: number
distanceMiles?: number
waitMinutes?: number
stagingLocation: {
zone: string
bin: string
lockerPin?: string
type: 'shelf' | 'locker' | 'bulky'
}
items: OrderItem[]
orderTotal: number
placedAt: string
readyAt: string
arrivedAt?: string
verificationCode: string
assignedRunner?: string
}npm dependencies
Files installed (5)
components/blocks/StorePickupCurbside.tsx34.6 kB
'use client' import * as React from 'react' import { Bell, Car, Check, CheckCircle2, Clock, Eye, Layers, MapPin, MessageSquare, MoreHorizontal, Navigation, Package, PackageCheck, Phone, Plus, RefreshCw, Search, Send, Store, X, } from 'lucide-react' import { cn } from '@/lib/utils' import { Avatar, AvatarFallback } from '@/components/ui/avatar' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { Input } from '@/components/ui/input' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' import { OrderDetailDialog } from './OrderDetailDialog' import { StorePickupModals } from './StorePickupModals' import type { OrderItem, OrderStatus, PickupMode, PickupOrder, StorePickupCurbsideProps } from './store-pickup-types' import { INITIAL_ORDERS } from './store-pickup-data' export type { OrderItem, OrderStatus, PickupMode, PickupOrder, StorePickupCurbsideProps } export function StorePickupCurbside({ storeName = 'Pasadena Flagship Store #02', storeCode = 'STORE-CA-02', initialModeFilter = 'all', className, }: StorePickupCurbsideProps) { const [orders, setOrders] = React.useState<PickupOrder[]>(INITIAL_ORDERS) const [searchQuery, setSearchQuery] = React.useState('') const [selectedModeFilter, setSelectedModeFilter] = React.useState<'all' | 'curbside' | 'in_store'>(initialModeFilter) const [selectedStatusFilter, setSelectedStatusFilter] = React.useState< 'all' | 'arrived' | 'en_route' | 'ready_staging' | 'completed' >('all') // Modals const [checkInModalOpen, setCheckInModalOpen] = React.useState(false) const [inspectModalOpen, setInspectModalOpen] = React.useState(false) const [notifyModalOpen, setNotifyModalOpen] = React.useState(false) const [newOrderModalOpen, setNewOrderModalOpen] = React.useState(false) const [selectedOrder, setSelectedOrder] = React.useState<PickupOrder | null>(null) const [actionFeedbackToast, setActionFeedbackToast] = React.useState<string | null>(null) // Toast feedback helper const triggerToast = React.useCallback((msg: string) => { setActionFeedbackToast(msg) setTimeout(() => { setActionFeedbackToast((current) => (current === msg ? null : current)) }, 3500) }, []) // KPI calculations const readyInStagingCount = React.useMemo(() => orders.filter((o) => o.status !== 'completed').length, [orders]) const arrivedCustomersCount = React.useMemo(() => orders.filter((o) => o.status === 'arrived').length, [orders]) const enRouteCount = React.useMemo(() => orders.filter((o) => o.status === 'en_route').length, [orders]) const completedCount = React.useMemo(() => orders.filter((o) => o.status === 'completed').length + 32, [orders]) // Filtered orders list const filteredOrders = React.useMemo(() => { return orders.filter((order) => { const query = searchQuery.trim().toLowerCase() const matchesSearch = query === '' || order.customerName.toLowerCase().includes(query) || order.orderNumber.toLowerCase().includes(query) || order.customerPhone.includes(query) || order.stagingLocation.bin.toLowerCase().includes(query) || (order.vehicleDetails?.makeModel.toLowerCase().includes(query) ?? false) || (order.vehicleDetails?.plate.toLowerCase().includes(query) ?? false) || order.items.some((i) => i.name.toLowerCase().includes(query)) const matchesMode = selectedModeFilter === 'all' || order.pickupMode === selectedModeFilter const matchesStatus = selectedStatusFilter === 'all' || order.status === selectedStatusFilter return matchesSearch && matchesMode && matchesStatus }) }, [orders, searchQuery, selectedModeFilter, selectedStatusFilter]) const handleInspect = (order: PickupOrder) => { setSelectedOrder(order) setInspectModalOpen(true) } const handleNotify = (order: PickupOrder) => { setSelectedOrder(order) setNotifyModalOpen(true) } const handleSendNotification = (channel: 'sms' | 'push') => { if (!selectedOrder) return setNotifyModalOpen(false) triggerToast(`Sent arrival notification to ${selectedOrder.customerName} via ${channel.toUpperCase()}`) } const handleMarkDelivered = (orderId: string) => { setOrders((prev) => prev.map((o) => o.id === orderId ? { ...o, status: 'completed', statusLabel: 'Delivered & Handed Over', } : o, ), ) const target = orders.find((o) => o.id === orderId) if (target) { triggerToast(`Order #${target.orderNumber} successfully delivered to ${target.customerName}!`) } setInspectModalOpen(false) } const handleConfirmArrival = (payload: { orderId: string spot: string vehicleModel: string plate: string hazards: boolean notes: string }) => { const target = orders.find((o) => o.id === payload.orderId) if (target) { setOrders((prev) => prev.map((o) => o.id === payload.orderId ? { ...o, status: 'arrived', pickupMode: 'curbside', spotNumber: parseInt(payload.spot, 10) || 4, waitMinutes: 1, statusLabel: 'Customer Arrived · Waiting 1m', vehicleDetails: { makeModel: payload.vehicleModel || 'Vehicle Unspecified', plate: payload.plate || 'N/A', hazardsFlashing: payload.hazards, }, arrivedAt: 'Just now', } : o, ), ) setCheckInModalOpen(false) triggerToast(`Customer ${target.customerName} checked into Spot #${payload.spot}! Runner alerted.`) } } const handleCreateNewOrder = (payload: { customerName: string customerPhone: string pickupMode: PickupMode spotNumber: string stagingAisle: string stagingBin: string itemSummary: string orderTotal: string }) => { const idNum = Math.floor(10000 + Math.random() * 90000) const newOrder: PickupOrder = { id: `order-${Date.now()}`, orderNumber: `BOPIS-${idNum}`, customerName: payload.customerName, customerPhone: payload.customerPhone || '(415) 555-0100', customerInitials: payload.customerName .split(' ') .map((n) => n[0]) .join('') .toUpperCase() .slice(0, 2), pickupMode: payload.pickupMode, spotNumber: payload.pickupMode === 'curbside' ? parseInt(payload.spotNumber, 10) || 3 : undefined, pickupDeskBay: payload.pickupMode === 'in_store' ? 'Pickup Desk Bay 02' : undefined, vehicleDetails: payload.pickupMode === 'curbside' ? { makeModel: 'Customer Vehicle', plate: 'PENDING', hazardsFlashing: false, } : undefined, status: 'ready_staging', statusLabel: `Ready in ${payload.stagingAisle}`, stagingLocation: { zone: payload.stagingAisle, bin: payload.stagingBin, type: payload.pickupMode === 'in_store' ? 'locker' : 'shelf', }, items: [ { id: `item-${Date.now()}`, name: payload.itemSummary || 'Selected Retail Merchandise', sku: `SKU-${Math.floor(100 + Math.random() * 900)}`, quantity: 1, price: parseFloat(payload.orderTotal) || 99.0, staged: true, category: 'General', }, ], orderTotal: parseFloat(payload.orderTotal) || 99.0, placedAt: '11:45 AM', readyAt: 'Just now', verificationCode: `${Math.floor(1000 + Math.random() * 9000)}`, assignedRunner: 'Pending Handoff', } setOrders((prev) => [newOrder, ...prev]) setNewOrderModalOpen(false) triggerToast(`New Order #${newOrder.orderNumber} staged in ${newOrder.stagingLocation.bin}!`) } const handleRefresh = () => { triggerToast('Refreshed pickup staging queue & curbside telemetry') } return ( <div className={cn('text-foreground w-full space-y-6', className)}> {/* Live Toast Notification Banner */} {actionFeedbackToast && ( <div className="border-success/30 bg-success/10 text-success flex items-center justify-between rounded-md border px-4 py-2.5 text-xs font-medium shadow-xs transition-colors duration-200"> <div className="flex items-center gap-2"> <CheckCircle2 className="text-success size-4 shrink-0" /> <span>{actionFeedbackToast}</span> </div> <button aria-label="Dismiss notification" type="button" className="text-muted-foreground hover:text-foreground inline-flex size-4 items-center justify-center" onClick={() => setActionFeedbackToast(null)} > <X className="size-3.5" /> </button> </div> )} {/* Header Section */} <div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between"> <div> <div className="flex flex-wrap items-center gap-2.5"> <div className="bg-primary/10 text-primary border-primary/20 flex size-8 items-center justify-center rounded-lg border"> <Car className="size-4" /> </div> <h1 className="text-xl font-bold tracking-tight md:text-2xl">Store Pickup & Curbside Coordination</h1> <Badge wrap variant="outline" className="gap-1.5 font-medium"> <span className="bg-success size-1.5 animate-pulse rounded-full" /> <span className="text-xs">Live Staging Radar</span> </Badge> </div> <div className="text-muted-foreground mt-1.5 flex flex-wrap items-center gap-2 text-xs"> <span className="text-foreground flex items-center gap-1 font-medium"> <MapPin className="text-muted-foreground size-3.5" /> {storeName} </span> <span className="font-mono text-xs">{storeCode}</span> <span className="text-foreground font-medium tabular-nums"> {readyInStagingCount} Ready · {enRouteCount} En Route · {completedCount} Completed </span> </div> </div> <div className="flex flex-wrap items-center gap-2"> <Button variant="outline" size="sm" className="h-9 gap-1.5 text-xs shadow-xs" onClick={() => setCheckInModalOpen(true)} > <Car className="text-warning size-3.5" /> <span>Customer "I'm Here" Check-In</span> </Button> <Button size="sm" className="h-9 gap-1.5 text-xs shadow-xs" onClick={() => setNewOrderModalOpen(true)}> <Plus className="size-3.5" /> <span>New Pickup Order</span> </Button> </div> </div> {/* 4 Fulfillment Health Cards */} <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> {/* Card 1: Ready in Staging */} <Card className="border-border shadow-xs"> <CardHeader className="flex flex-row flex-wrap items-center justify-between gap-2 pb-2"> <CardTitle className="text-muted-foreground text-xs font-semibold tracking-wider uppercase"> Ready for Customer </CardTitle> <div className="bg-muted text-muted-foreground flex size-7 items-center justify-center rounded-md"> <PackageCheck className="text-foreground size-4" /> </div> </CardHeader> <CardContent className="space-y-1.5"> <div className="text-2xl font-bold tracking-tight tabular-nums">8 Orders in Staging</div> <div className="text-muted-foreground flex flex-wrap items-center justify-between gap-x-2 gap-y-1 text-xs"> <span>6 Bins · 2 Secured Lockers</span> <Badge wrap variant="success" className="h-5 px-1.5 text-xs font-medium"> 100% Pick Accuracy </Badge> </div> </CardContent> </Card> {/* Card 2: Arrived in Curbside Bay (Amber Alert) */} <Card className="border-warning/30 bg-warning/5 shadow-xs"> <CardHeader className="flex flex-row flex-wrap items-center justify-between gap-2 pb-2"> <CardTitle className="text-warning text-warning min-w-0 text-xs font-semibold tracking-wider break-words uppercase"> Customers in Parking Bays </CardTitle> <div className="bg-warning/15 text-warning flex size-7 items-center justify-center rounded-md"> <Car className="text-warning size-4" /> </div> </CardHeader> <CardContent className="space-y-1.5"> <div className="text-foreground dark:text-foreground text-2xl font-bold tracking-tight tabular-nums"> {arrivedCustomersCount} Customers Arrived </div> <div className="text-muted-foreground flex flex-wrap items-center justify-between gap-x-2 gap-y-1 text-xs"> <span className="text-warning/80 text-warning/80">Bay #4 (2m) · Bay #2 (4m)</span> <Badge wrap variant="warning" className="h-5 gap-1 px-1.5 text-xs font-medium"> <span className="bg-warning bg-warning size-1.5 rounded-full" /> Action Required </Badge> </div> </CardContent> </Card> {/* Card 3: Avg Fulfillment Time */} <Card className="border-border shadow-xs"> <CardHeader className="flex flex-row flex-wrap items-center justify-between gap-2 pb-2"> <CardTitle className="text-muted-foreground text-xs font-semibold tracking-wider uppercase"> Avg Fulfillment Time </CardTitle> <div className="bg-muted text-muted-foreground flex size-7 items-center justify-center rounded-md"> <Clock className="text-foreground size-4" /> </div> </CardHeader> <CardContent className="space-y-1.5"> <div className="text-2xl font-bold tracking-tight tabular-nums">1m 45s</div> <div className="text-muted-foreground flex flex-wrap items-center justify-between gap-x-2 gap-y-1 text-xs"> <span>Goal < 3m 00s (-18s vs target)</span> <Badge wrap variant="info" className="h-5 px-1.5 text-xs font-medium"> Fastest: 52s </Badge> </div> </CardContent> </Card> {/* Card 4: Today's Completed Pickups */} <Card className="border-border shadow-xs"> <CardHeader className="flex flex-row flex-wrap items-center justify-between gap-2 pb-2"> <CardTitle className="text-muted-foreground text-xs font-semibold tracking-wider uppercase"> Today's Pickups </CardTitle> <div className="bg-muted text-muted-foreground flex size-7 items-center justify-center rounded-md"> <CheckCircle2 className="text-foreground size-4" /> </div> </CardHeader> <CardContent className="space-y-1.5"> <div className="text-2xl font-bold tracking-tight tabular-nums">{completedCount} Orders</div> <div className="text-muted-foreground flex flex-wrap items-center justify-between gap-x-2 gap-y-1 text-xs"> <span>$5,240 Fulfilled Volume</span> <Badge wrap variant="success" className="h-5 px-1.5 text-xs font-medium"> 97.2% On-Time SLA </Badge> </div> </CardContent> </Card> </div> {/* Active Pickup Orders Board / Table Card */} <Card className="border-border shadow-xs"> <CardHeader className="space-y-4 pb-4"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div> <CardTitle className="text-base font-semibold">Active Pickup Orders Board</CardTitle> <CardDescription className="text-xs"> Live queue of customers arriving curbside and checking in at in-store fulfillment stations. </CardDescription> </div> <div className="flex items-center gap-2"> <Button variant="outline" size="sm" className="h-8 gap-1.5 text-xs shadow-xs" onClick={handleRefresh}> <RefreshCw className="text-muted-foreground size-3" /> <span>Refresh</span> </Button> </div> </div> {/* Filter & Search Controls */} <div className="flex flex-col gap-2.5 sm:flex-row sm:items-center sm:justify-between"> <div className="relative max-w-sm flex-1"> <Search className="text-muted-foreground absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2" /> <Input value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} placeholder="Search customer, order #, spot, or items..." className="h-8 pl-8 text-xs" /> {searchQuery && ( <button type="button" className="text-muted-foreground hover:text-foreground absolute top-1/2 right-2.5 -translate-y-1/2" aria-label="Clear search" onClick={() => setSearchQuery('')} > <X className="size-3" /> </button> )} </div> <div className="flex flex-wrap items-center gap-2"> <div className="flex items-center gap-1.5"> <span className="text-muted-foreground text-xs">Mode:</span> <Select value={selectedModeFilter} onValueChange={(val: 'all' | 'curbside' | 'in_store') => setSelectedModeFilter(val)} > <SelectTrigger className="h-8 w-32 text-xs"> <SelectValue placeholder="Pickup Mode" /> </SelectTrigger> <SelectContent> <SelectItem value="all">All Modes</SelectItem> <SelectItem value="curbside">Curbside Bay</SelectItem> <SelectItem value="in_store">In-Store Desk</SelectItem> </SelectContent> </Select> </div> <div className="flex items-center gap-1.5"> <span className="text-muted-foreground text-xs">Status:</span> <Select value={selectedStatusFilter} onValueChange={(val: 'all' | 'arrived' | 'en_route' | 'ready_staging' | 'completed') => setSelectedStatusFilter(val) } > <SelectTrigger className="h-8 w-36 text-xs"> <SelectValue placeholder="Status" /> </SelectTrigger> <SelectContent> <SelectItem value="all">All Statuses</SelectItem> <SelectItem value="arrived">Customer Arrived</SelectItem> <SelectItem value="en_route">En Route</SelectItem> <SelectItem value="ready_staging">Ready in Staging</SelectItem> <SelectItem value="completed">Completed</SelectItem> </SelectContent> </Select> </div> </div> </div> </CardHeader> <CardContent className="p-0"> <div className="overflow-x-auto"> <Table> <TableHeader className="bg-muted/40"> <TableRow> <TableHead className="w-[200px] text-xs font-semibold">Customer & Contact</TableHead> <TableHead className="w-[180px] text-xs font-semibold">Order & Items</TableHead> <TableHead className="w-[220px] text-xs font-semibold">Pickup Mode & Bay</TableHead> <TableHead className="w-[200px] text-xs font-semibold">Customer Status</TableHead> <TableHead className="w-[160px] text-xs font-semibold">Staging Location</TableHead> <TableHead className="w-[180px] text-right text-xs font-semibold">Fulfillment Actions</TableHead> </TableRow> </TableHeader> <TableBody> {filteredOrders.map((order) => ( <TableRow key={order.id} className={cn( 'transition-colors duration-150', order.status === 'arrived' && 'bg-warning/[0.04] bg-warning/[0.08]', )} > {/* Customer Name & Phone */} <TableCell className="py-3"> <div className="flex items-center gap-2.5"> <Avatar className="border-border size-8 rounded-full border"> <AvatarFallback className="bg-muted text-xs font-medium"> {order.customerInitials} </AvatarFallback> </Avatar> <div className="space-y-0.5"> <div className="text-foreground text-xs leading-tight font-semibold"> {order.customerName} </div> <div className="text-muted-foreground flex items-center gap-1 font-mono text-xs tabular-nums"> <Phone className="text-muted-foreground/70 size-3" /> {order.customerPhone} </div> </div> </div> </TableCell> {/* Order Number & Items Summary */} <TableCell className="py-3"> <div className="space-y-1"> <div className="flex items-center gap-1.5"> <span className="text-foreground font-mono text-xs font-bold">#{order.orderNumber}</span> <span className="text-muted-foreground text-xs tabular-nums"> (${order.orderTotal.toFixed(2)}) </span> </div> <div className="text-muted-foreground max-w-[170px] min-w-0 truncate text-xs" title={order.items.map((i) => `${i.quantity}x ${i.name}`).join(', ')} > {order.items.length} {order.items.length === 1 ? 'item' : 'items'}:{' '} {order.items.map((i) => i.name).join(', ')} </div> </div> </TableCell> {/* Pickup Mode Badge */} <TableCell className="py-3"> <div className="space-y-1"> {order.pickupMode === 'curbside' ? ( <> <Badge wrap variant="outline" className="border-success/30 bg-success/10 text-success gap-1 font-medium" > <Car className="text-success size-3 shrink-0" /> <span>Curbside Spot #{order.spotNumber}</span> </Badge> {order.vehicleDetails && ( <div className="text-muted-foreground flex items-center gap-1 text-xs"> <span className="max-w-[180px] truncate">{order.vehicleDetails.makeModel}</span> <span className="text-foreground font-mono font-medium"> · {order.vehicleDetails.plate} </span> </div> )} </> ) : ( <> <Badge wrap variant="outline" className="border-info/30 bg-info/10 text-info gap-1 font-medium" > <Store className="text-info size-3 shrink-0" /> <span>In-Store Desk</span> </Badge> <div className="text-muted-foreground text-xs"> {order.pickupDeskBay || 'Front Pickup Counter'} </div> </> )} </div> </TableCell> {/* Customer Status */} <TableCell className="py-3"> <div className="space-y-1"> {order.status === 'arrived' ? ( <> <Badge wrap variant="warning" className="gap-1.5 px-2 py-0.5 font-medium"> <span className="bg-warning size-1.5 rounded-full" /> <Clock className="size-3" /> <span className="tabular-nums">Customer Arrived · Waiting {order.waitMinutes || 2}m</span> </Badge> <div className="text-muted-foreground flex items-center gap-1 text-xs"> <span>Check-in: {order.arrivedAt || '11:40 AM'}</span> {order.vehicleDetails?.hazardsFlashing && ( <span className="text-warning font-medium">· Hazards On</span> )} </div> </> ) : order.status === 'en_route' ? ( <> <Badge wrap variant="info" className="gap-1.5 px-2 py-0.5 font-medium"> <Navigation className="text-info size-3" /> <span className="tabular-nums">En Route · {order.etaMinutes} min ETA</span> </Badge> <div className="text-muted-foreground text-xs"> <span className="tabular-nums">{order.distanceMiles} miles away</span> </div> </> ) : order.status === 'ready_staging' ? ( <> <Badge wrap variant="secondary" className="gap-1.5 px-2 py-0.5 font-medium"> <Package className="size-3" /> <span>Ready in Staging</span> </Badge> <div className="text-muted-foreground text-xs">Staged at {order.readyAt}</div> </> ) : ( <> <Badge wrap variant="success" className="gap-1.5 px-2 py-0.5 font-medium"> <CheckCircle2 className="size-3" /> <span>Delivered</span> </Badge> <div className="text-muted-foreground text-xs">Completed Handoff</div> </> )} </div> </TableCell> {/* Staging Location */} <TableCell className="py-3"> <div className="space-y-0.5"> <div className="text-foreground flex items-center gap-1.5 text-xs font-semibold"> <Layers className="text-muted-foreground size-3.5" /> <span>{order.stagingLocation.zone}</span> <span className="text-primary font-mono font-bold">{order.stagingLocation.bin}</span> </div> {order.stagingLocation.lockerPin ? ( <div className="text-muted-foreground font-mono text-xs"> PIN Code:{' '} <span className="text-foreground font-bold">{order.stagingLocation.lockerPin}</span> </div> ) : ( <div className="text-muted-foreground text-xs capitalize"> Type: {order.stagingLocation.type} staging </div> )} </div> </TableCell> {/* Fulfillment Actions */} <TableCell className="py-3 text-right"> <div className="flex items-center justify-end gap-1.5"> {order.status === 'arrived' ? ( <Button size="sm" className="bg-success hover:bg-success/90 bg-success dark:hover:bg-success/90 h-8 gap-1 px-2.5 text-xs font-medium text-white shadow-xs" onClick={() => handleMarkDelivered(order.id)} > <Check className="size-3.5" /> <span>Mark Delivered</span> </Button> ) : order.status !== 'completed' ? ( <Button variant="outline" size="sm" className="h-8 gap-1 px-2.5 text-xs shadow-xs" onClick={() => handleNotify(order)} > <Bell className="text-muted-foreground size-3" /> <span>Notify</span> </Button> ) : ( <Button variant="outline" size="sm" disabled className="h-8 text-xs opacity-60"> <CheckCircle2 className="text-success size-3" /> <span>Fulfilled</span> </Button> )} <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="ghost" size="icon" className="text-muted-foreground hover:text-foreground size-8" > <MoreHorizontal className="size-4" /> <span className="sr-only">Actions</span> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end" className="w-48 text-xs"> <DropdownMenuLabel>Order #{order.orderNumber}</DropdownMenuLabel> <DropdownMenuSeparator /> <DropdownMenuItem className="cursor-pointer gap-2" onClick={() => handleInspect(order)}> <Eye className="text-muted-foreground size-3.5" /> <span>Inspect Order Details</span> </DropdownMenuItem> <DropdownMenuItem className="cursor-pointer gap-2" onClick={() => handleNotify(order)}> <MessageSquare className="text-muted-foreground size-3.5" /> <span>Send SMS / Push Alert</span> </DropdownMenuItem> <DropdownMenuSeparator /> {order.status !== 'completed' && ( <DropdownMenuItem className="text-success cursor-pointer gap-2 font-medium" onClick={() => handleMarkDelivered(order.id)} > <CheckCircle2 className="size-3.5" /> <span>Complete Handoff</span> </DropdownMenuItem> )} </DropdownMenuContent> </DropdownMenu> </div> </TableCell> </TableRow> ))} {filteredOrders.length === 0 && ( <TableRow> <TableCell colSpan={6} className="text-muted-foreground h-32 text-center text-xs"> No pickup orders match your search or filter criteria. </TableCell> </TableRow> )} </TableBody> </Table> </div> </CardContent> <CardFooter className="border-border text-muted-foreground flex items-center justify-between border-t px-6 py-3 text-xs"> <div> Showing <span className="text-foreground font-medium tabular-nums">{filteredOrders.length}</span> of{' '} <span className="text-foreground font-medium tabular-nums">{orders.length}</span> active pickup orders </div> <div className="flex items-center gap-3"> <div className="flex items-center gap-1.5"> <span className="bg-success size-2 rounded-full" /> <span>Curbside Bays (Spots 1-6)</span> </div> <div className="flex items-center gap-1.5"> <span className="bg-info size-2 rounded-full" /> <span>In-Store Pickup Desk</span> </div> </div> </CardFooter> </Card> {/* Modal 1: Inspect Order Details Dialog */} <OrderDetailDialog open={inspectModalOpen} onOpenChange={setInspectModalOpen} selectedOrder={selectedOrder} onMarkDelivered={handleMarkDelivered} /> {/* Action Modals (Check-In, Notify, New Order) */} <StorePickupModals checkInModalOpen={checkInModalOpen} onCheckInModalOpenChange={setCheckInModalOpen} notifyModalOpen={notifyModalOpen} onNotifyModalOpenChange={setNotifyModalOpen} newOrderModalOpen={newOrderModalOpen} onNewOrderModalOpenChange={setNewOrderModalOpen} orders={orders} selectedOrder={selectedOrder} storeName={storeName} onConfirmArrival={handleConfirmArrival} onSendNotification={handleSendNotification} onCreateOrder={handleCreateNewOrder} /> </div> ) } export default StorePickupCurbsidecomponents/blocks/OrderDetailDialog.tsx7.3 kB
'use client' import { Car, Check, CheckCircle2, Layers } from 'lucide-react' import { Avatar, AvatarFallback } from '@/components/ui/avatar' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import type { PickupOrder } from './store-pickup-types' export interface OrderDetailDialogProps { open: boolean onOpenChange: (open: boolean) => void selectedOrder: PickupOrder | null onMarkDelivered: (orderId: string) => void } export function OrderDetailDialog({ open, onOpenChange, selectedOrder, onMarkDelivered }: OrderDetailDialogProps) { return ( <Dialog open={open} onOpenChange={onOpenChange}> <DialogContent className="sm:max-w-lg"> {selectedOrder && ( <> <DialogHeader> <div className="flex items-start justify-between"> <div> <div className="flex items-center gap-2"> <DialogTitle className="text-base font-bold">Order #{selectedOrder.orderNumber}</DialogTitle> <Badge wrap variant={selectedOrder.pickupMode === 'curbside' ? 'success' : 'info'} className="text-xs font-medium" > {selectedOrder.pickupMode === 'curbside' ? `Curbside Bay #${selectedOrder.spotNumber}` : 'In-Store Desk'} </Badge> </div> <DialogDescription className="mt-1 text-xs"> Placed at {selectedOrder.placedAt} · Staged at {selectedOrder.readyAt} </DialogDescription> </div> </div> </DialogHeader> <div className="space-y-4 py-2 text-xs"> {/* Customer & Contact Strip */} <div className="border-border bg-muted/30 flex items-center justify-between rounded-lg border p-3"> <div className="flex items-center gap-2.5"> <Avatar className="border-border size-9 border"> <AvatarFallback className="text-xs font-bold">{selectedOrder.customerInitials}</AvatarFallback> </Avatar> <div> <div className="text-foreground text-xs font-semibold">{selectedOrder.customerName}</div> <div className="text-muted-foreground font-mono text-xs">{selectedOrder.customerPhone}</div> </div> </div> <div className="text-right"> <div className="text-muted-foreground text-xs">Pickup Verification PIN</div> <div className="text-primary font-mono text-sm font-bold tracking-widest"> {selectedOrder.verificationCode} </div> </div> </div> {/* Staging & Bay Location Details */} <div className="grid grid-cols-1 gap-3 sm:grid-cols-2"> <div className="border-border space-y-1 rounded-md border p-2.5"> <div className="text-muted-foreground flex items-center gap-1 text-xs font-medium"> <Layers className="size-3.5" /> <span>Staging Bin Location</span> </div> <div className="text-foreground text-xs font-bold"> {selectedOrder.stagingLocation.zone} ·{' '} <span className="text-primary font-mono">{selectedOrder.stagingLocation.bin}</span> </div> <div className="text-muted-foreground text-xs capitalize"> {selectedOrder.stagingLocation.type} unit storage </div> </div> <div className="border-border space-y-1 rounded-md border p-2.5"> <div className="text-muted-foreground flex items-center gap-1 text-xs font-medium"> <Car className="size-3.5" /> <span>Vehicle & Bay</span> </div> {selectedOrder.pickupMode === 'curbside' && selectedOrder.vehicleDetails ? ( <> <div className="text-foreground truncate text-xs font-bold"> Spot #{selectedOrder.spotNumber} · {selectedOrder.vehicleDetails.makeModel} </div> <div className="text-muted-foreground font-mono text-xs"> Plate: {selectedOrder.vehicleDetails.plate} </div> </> ) : ( <> <div className="text-foreground text-xs font-bold"> {selectedOrder.pickupDeskBay || 'Pickup Counter'} </div> <div className="text-muted-foreground text-xs"> Locker PIN: {selectedOrder.stagingLocation.lockerPin || 'N/A'} </div> </> )} </div> </div> {/* Staged Items Checklist */} <div className="space-y-2"> <div className="text-foreground flex items-center justify-between text-xs font-medium"> <span>Order Items Checklist ({selectedOrder.items.length} items)</span> <span className="text-muted-foreground">Total: ${selectedOrder.orderTotal.toFixed(2)}</span> </div> <div className="border-border max-h-40 space-y-1.5 overflow-y-auto rounded-md border p-2"> {selectedOrder.items.map((item) => ( <div key={item.id} className="bg-muted/40 flex items-center justify-between rounded p-2 text-xs"> <div className="flex items-center gap-2"> <CheckCircle2 className="text-success size-3.5 shrink-0" /> <div> <div className="text-foreground font-medium">{item.name}</div> <div className="text-muted-foreground font-mono text-xs"> SKU: {item.sku} · Qty: {item.quantity} </div> </div> </div> <div className="text-foreground font-mono text-xs font-medium"> ${(item.price * item.quantity).toFixed(2)} </div> </div> ))} </div> </div> </div> <DialogFooter className="gap-2 sm:gap-0"> <Button variant="outline" size="sm" className="text-xs" onClick={() => onOpenChange(false)}> Close </Button> {selectedOrder.status !== 'completed' && ( <Button size="sm" className="bg-success hover:bg-success/90 gap-1.5 text-xs text-white" onClick={() => onMarkDelivered(selectedOrder.id)} > <Check className="size-3.5" /> <span>Mark Delivered & Close</span> </Button> )} </DialogFooter> </> )} </DialogContent> </Dialog> ) }components/blocks/StorePickupModals.tsx17.8 kB
'use client' import * as React from 'react' import { Bell, Car, CheckCircle2, MessageSquare, Send } from 'lucide-react' import { cn } from '@/lib/utils' import { Button } from '@/components/ui/button' import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { Input } from '@/components/ui/input' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import type { PickupMode, PickupOrder } from './store-pickup-types' interface StorePickupModalsProps { checkInModalOpen: boolean onCheckInModalOpenChange: (open: boolean) => void notifyModalOpen: boolean onNotifyModalOpenChange: (open: boolean) => void newOrderModalOpen: boolean onNewOrderModalOpenChange: (open: boolean) => void orders: PickupOrder[] selectedOrder: PickupOrder | null storeName: string onConfirmArrival: (payload: { orderId: string spot: string vehicleModel: string plate: string hazards: boolean notes: string }) => void onSendNotification: (channel: 'sms' | 'push') => void onCreateOrder: (payload: { customerName: string customerPhone: string pickupMode: PickupMode spotNumber: string stagingAisle: string stagingBin: string itemSummary: string orderTotal: string }) => void } export function StorePickupModals({ checkInModalOpen, onCheckInModalOpenChange, notifyModalOpen, onNotifyModalOpenChange, newOrderModalOpen, onNewOrderModalOpenChange, orders, selectedOrder, storeName, onConfirmArrival, onSendNotification, onCreateOrder, }: StorePickupModalsProps) { // Check-in state const [checkInOrderId, setCheckInOrderId] = React.useState('order-3') const [checkInSpot, setCheckInSpot] = React.useState('4') const [checkInVehicleModel, setCheckInVehicleModel] = React.useState('Silver Tesla Model 3') const [checkInPlate, setCheckInPlate] = React.useState('7XYZ892') const [checkInHazards, setCheckInHazards] = React.useState(true) const [checkInNotes, setCheckInNotes] = React.useState('Parked in bay 4, trunk unlocked for contactless placement.') // Notify state const [notifyChannel, setNotifyChannel] = React.useState<'sms' | 'push'>('sms') const [notifyMessageSent, setNotifyMessageSent] = React.useState(false) // New Order state const [newCustomerName, setNewCustomerName] = React.useState('') const [newCustomerPhone, setNewCustomerPhone] = React.useState('') const [newPickupMode, setNewPickupMode] = React.useState<PickupMode>('curbside') const [newSpotNumber, setNewSpotNumber] = React.useState('3') const [newStagingAisle, setNewStagingAisle] = React.useState('Aisle 01') const [newStagingBin, setNewStagingBin] = React.useState('Bin A-12') const [newItemSummary, setNewItemSummary] = React.useState('') const [newOrderTotal, setNewOrderTotal] = React.useState('120.00') const handleCheckInSubmit = () => { onConfirmArrival({ orderId: checkInOrderId, spot: checkInSpot, vehicleModel: checkInVehicleModel, plate: checkInPlate, hazards: checkInHazards, notes: checkInNotes, }) } const handleSendNotification = () => { if (!selectedOrder) return setNotifyMessageSent(true) setTimeout(() => { onSendNotification(notifyChannel) setNotifyMessageSent(false) }, 1000) } const handleCreateOrder = () => { if (!newCustomerName) return onCreateOrder({ customerName: newCustomerName, customerPhone: newCustomerPhone, pickupMode: newPickupMode, spotNumber: newSpotNumber, stagingAisle: newStagingAisle, stagingBin: newStagingBin, itemSummary: newItemSummary, orderTotal: newOrderTotal, }) setNewCustomerName('') setNewCustomerPhone('') setNewItemSummary('') } return ( <> {/* Modal 1: Customer "I'm Here" Parking Spot Check-in Dialog */} <Dialog open={checkInModalOpen} onOpenChange={onCheckInModalOpenChange}> <DialogContent className="sm:max-w-md"> <DialogHeader> <div className="flex items-center gap-2"> <div className="border-warning/20 bg-warning/10 text-warning flex size-8 items-center justify-center rounded-lg border"> <Car className="size-4" /> </div> <div> <DialogTitle className="text-base font-bold">Customer Parking Arrival Check-In</DialogTitle> <DialogDescription className="text-xs"> Check in customer arriving at the curbside pickup bay or simulate customer mobile app check-in. </DialogDescription> </div> </div> </DialogHeader> <div className="space-y-4 py-2 text-xs"> {/* Select Order to Check In */} <div className="space-y-1.5"> <label className="text-foreground font-medium">Select Customer Order</label> <Select value={checkInOrderId} onValueChange={setCheckInOrderId}> <SelectTrigger className="h-9 text-xs"> <SelectValue placeholder="Select active order" /> </SelectTrigger> <SelectContent> {orders .filter((x) => x.status !== 'completed') .map((o) => ( <SelectItem key={o.id} value={o.id}> {o.customerName} · #{o.orderNumber} ({o.items.length} items) </SelectItem> ))} </SelectContent> </Select> </div> {/* Select Parking Spot */} <div className="space-y-1.5"> <label className="text-foreground font-medium">Curbside Parking Bay</label> <div className="grid grid-cols-6 gap-1.5"> {['1', '2', '3', '4', '5', '6'].map((spot) => ( <button key={spot} type="button" className={cn( 'flex flex-col items-center justify-center rounded-md border py-2 text-xs font-semibold transition-colors duration-150', checkInSpot === spot ? 'border-success bg-success/10 text-success ring-success/30 text-success ring-2' : 'border-border bg-card text-muted-foreground hover:border-foreground/20 hover:text-foreground', )} onClick={() => setCheckInSpot(spot)} > <span>Bay</span> <span className="text-sm font-bold">#{spot}</span> </button> ))} </div> </div> {/* Vehicle Information */} <div className="grid grid-cols-1 gap-3 sm:grid-cols-2"> <div className="space-y-1.5"> <label className="text-foreground font-medium">Vehicle Make & Model</label> <Input value={checkInVehicleModel} onChange={(e) => setCheckInVehicleModel(e.target.value)} placeholder="e.g. Silver Tesla Model 3" className="h-8 text-xs" /> </div> <div className="space-y-1.5"> <label className="text-foreground font-medium">License Plate</label> <Input value={checkInPlate} onChange={(e) => setCheckInPlate(e.target.value)} placeholder="e.g. 7XYZ892" className="h-8 font-mono text-xs uppercase" /> </div> </div> {/* Arrival Notes */} <div className="space-y-1.5"> <label className="text-foreground font-medium">Arrival Instructions & Bay Notes</label> <Input value={checkInNotes} onChange={(e) => setCheckInNotes(e.target.value)} placeholder="e.g. Trunk unlocked, hazard lights on" className="h-8 text-xs" /> </div> {/* Hazard Lights Checkbox Simulation */} <div className="border-border bg-muted/40 flex items-center gap-2 rounded-md border p-2.5"> <input id="hazards-checkbox" checked={checkInHazards} onChange={(e) => setCheckInHazards(e.target.checked)} type="checkbox" className="border-border text-primary focus:ring-primary size-4 rounded" /> <label htmlFor="hazards-checkbox" className="text-foreground cursor-pointer text-xs"> Vehicle hazard lights are actively flashing for runner identification </label> </div> </div> <DialogFooter className="gap-2 sm:gap-0"> <DialogClose asChild> <Button variant="outline" size="sm" className="text-xs"> Cancel </Button> </DialogClose> <Button size="sm" className="bg-success hover:bg-success/90 gap-1.5 text-xs text-white" onClick={handleCheckInSubmit} > <CheckCircle2 className="size-3.5" /> <span>Confirm Arrival & Alert Runner</span> </Button> </DialogFooter> </DialogContent> </Dialog> {/* Modal 2: Notify Customer Dialog */} <Dialog open={notifyModalOpen} onOpenChange={onNotifyModalOpenChange}> <DialogContent className="sm:max-w-md"> {selectedOrder && ( <DialogHeader> <DialogTitle className="text-base font-bold">Notify Customer</DialogTitle> <DialogDescription className="text-xs"> Send real-time pickup status alert to {selectedOrder.customerName} ({selectedOrder.customerPhone}). </DialogDescription> </DialogHeader> )} {selectedOrder && ( <div className="space-y-4 py-2 text-xs"> <div className="space-y-1.5"> <label className="text-foreground font-medium">Notification Channel</label> <div className="grid grid-cols-1 gap-2 sm:grid-cols-2"> <button type="button" className={cn( 'flex items-center justify-center gap-1.5 rounded-md border py-2 text-xs font-semibold transition-colors', notifyChannel === 'sms' ? 'border-primary bg-primary/10 text-primary' : 'border-border bg-card text-muted-foreground', )} onClick={() => setNotifyChannel('sms')} > <MessageSquare className="size-3.5" /> <span>SMS Text Message</span> </button> <button type="button" className={cn( 'flex items-center justify-center gap-1.5 rounded-md border py-2 text-xs font-semibold transition-colors', notifyChannel === 'push' ? 'border-primary bg-primary/10 text-primary' : 'border-border bg-card text-muted-foreground', )} onClick={() => setNotifyChannel('push')} > <Bell className="size-3.5" /> <span>Mobile App Push</span> </button> </div> </div> <div className="border-border bg-muted/40 space-y-1.5 rounded-md border p-3 text-xs"> <div className="text-foreground flex items-center justify-between font-medium"> <span>Message Preview</span> <span className="text-muted-foreground font-mono">160 chars</span> </div> <p className="text-muted-foreground text-xs leading-relaxed"> "Hi {selectedOrder.customerName}, your Order #{selectedOrder.orderNumber} is ready at {storeName}! Pull into Curbside Spot #{selectedOrder.spotNumber || '1-6'} and tap 'I'm Here' for runner delivery." </p> </div> </div> )} <DialogFooter className="gap-2 sm:gap-0"> <DialogClose asChild> <Button variant="outline" size="sm" className="text-xs"> Cancel </Button> </DialogClose> <Button size="sm" className="gap-1.5 text-xs" disabled={notifyMessageSent} onClick={handleSendNotification}> <Send className="size-3.5" /> <span>{notifyMessageSent ? 'Notification Sent!' : 'Send Alert'}</span> </Button> </DialogFooter> </DialogContent> </Dialog> {/* Modal 3: New Pickup Order Staging Dialog */} <Dialog open={newOrderModalOpen} onOpenChange={onNewOrderModalOpenChange}> <DialogContent className="sm:max-w-md"> <DialogHeader> <DialogTitle className="text-base font-bold">Stage New Pickup Order</DialogTitle> <DialogDescription className="text-xs"> Register an incoming online BOPIS order into retail staging shelves or lockers. </DialogDescription> </DialogHeader> <div className="space-y-3.5 py-2 text-xs"> <div className="grid grid-cols-1 gap-3 sm:grid-cols-2"> <div className="space-y-1.5"> <label className="text-foreground font-medium">Customer Name</label> <Input value={newCustomerName} onChange={(e) => setNewCustomerName(e.target.value)} placeholder="e.g. Alex Morgan" className="h-8 text-xs" /> </div> <div className="space-y-1.5"> <label className="text-foreground font-medium">Customer Phone</label> <Input value={newCustomerPhone} onChange={(e) => setNewCustomerPhone(e.target.value)} placeholder="e.g. (415) 555-0199" className="h-8 font-mono text-xs" /> </div> </div> <div className="grid grid-cols-1 gap-3 sm:grid-cols-2"> <div className="space-y-1.5"> <label className="text-foreground font-medium">Pickup Mode</label> <Select value={newPickupMode} onValueChange={(val) => setNewPickupMode(val as PickupMode)}> <SelectTrigger className="h-8 text-xs"> <SelectValue /> </SelectTrigger> <SelectContent> <SelectItem value="curbside">Curbside Bay</SelectItem> <SelectItem value="in_store">In-Store Desk</SelectItem> </SelectContent> </Select> </div> <div className="space-y-1.5"> <label className="text-foreground font-medium">Preferred Bay / Desk</label> <Select value={newSpotNumber} onValueChange={setNewSpotNumber}> <SelectTrigger className="h-8 text-xs"> <SelectValue /> </SelectTrigger> <SelectContent> <SelectItem value="1">Bay #1</SelectItem> <SelectItem value="2">Bay #2</SelectItem> <SelectItem value="3">Bay #3</SelectItem> <SelectItem value="4">Bay #4</SelectItem> <SelectItem value="5">Bay #5</SelectItem> <SelectItem value="6">Bay #6</SelectItem> </SelectContent> </Select> </div> </div> <div className="grid grid-cols-1 gap-3 sm:grid-cols-2"> <div className="space-y-1.5"> <label className="text-foreground font-medium">Staging Aisle</label> <Input value={newStagingAisle} onChange={(e) => setNewStagingAisle(e.target.value)} placeholder="e.g. Aisle 01" className="h-8 text-xs" /> </div> <div className="space-y-1.5"> <label className="text-foreground font-medium">Staging Bin</label> <Input value={newStagingBin} onChange={(e) => setNewStagingBin(e.target.value)} placeholder="e.g. Bin B-05" className="h-8 text-xs" /> </div> </div> <div className="space-y-1.5"> <label className="text-foreground font-medium">Items Summary</label> <Input value={newItemSummary} onChange={(e) => setNewItemSummary(e.target.value)} placeholder="e.g. Noise Cancelling Earbuds, Travel Case" className="h-8 text-xs" /> </div> <div className="space-y-1.5"> <label className="text-foreground font-medium">Order Total ($)</label> <Input value={newOrderTotal} onChange={(e) => setNewOrderTotal(e.target.value)} placeholder="120.00" className="h-8 font-mono text-xs" /> </div> </div> <DialogFooter className="gap-2 sm:gap-0"> <DialogClose asChild> <Button variant="outline" size="sm" className="text-xs"> Cancel </Button> </DialogClose> <Button size="sm" className="text-xs" disabled={!newCustomerName} onClick={handleCreateOrder}> <span>Stage & Assign Bin</span> </Button> </DialogFooter> </DialogContent> </Dialog> </> ) }components/blocks/store-pickup-types.ts1.1 kB
export type PickupMode = 'curbside' | 'in_store' export type OrderStatus = 'arrived' | 'en_route' | 'ready_staging' | 'completed' export interface OrderItem { id: string name: string sku: string quantity: number price: number staged: boolean category: string } export interface PickupOrder { id: string orderNumber: string customerName: string customerPhone: string customerInitials: string avatar?: string pickupMode: PickupMode spotNumber?: number vehicleDetails?: { makeModel: string plate: string hazardsFlashing?: boolean } pickupDeskBay?: string status: OrderStatus statusLabel: string etaMinutes?: number distanceMiles?: number waitMinutes?: number stagingLocation: { zone: string bin: string lockerPin?: string type: 'shelf' | 'locker' | 'bulky' } items: OrderItem[] orderTotal: number placedAt: string readyAt: string arrivedAt?: string verificationCode: string assignedRunner?: string } export interface StorePickupCurbsideProps { storeName?: string storeCode?: string initialModeFilter?: 'all' | 'curbside' | 'in_store' className?: string }components/blocks/store-pickup-data.ts5.6 kB
import type { PickupOrder } from './store-pickup-types' export const INITIAL_ORDERS: PickupOrder[] = [ { id: 'order-1', orderNumber: 'BOPIS-84920', customerName: 'Elena Rostova', customerPhone: '(415) 555-0192', customerInitials: 'ER', pickupMode: 'curbside', spotNumber: 4, vehicleDetails: { makeModel: 'Silver Tesla Model 3', plate: '7XYZ892', hazardsFlashing: true, }, status: 'arrived', statusLabel: 'Customer Arrived · Waiting 2m', waitMinutes: 2, stagingLocation: { zone: 'Aisle 02', bin: 'Bin B-14', type: 'shelf', }, items: [ { id: 'item-1', name: 'Studio Pro Wireless Headphones', sku: 'AUD-902-BLK', quantity: 1, price: 249.0, staged: true, category: 'Audio', }, { id: 'item-2', name: 'Carbon Runner Wind Jacket', sku: 'APP-410-MED', quantity: 1, price: 100.0, staged: true, category: 'Apparel', }, ], orderTotal: 349.0, placedAt: '11:20 AM', readyAt: '11:35 AM', arrivedAt: '11:42 AM', verificationCode: '4920', assignedRunner: 'Jordan Miller', }, { id: 'order-2', orderNumber: 'BOPIS-84919', customerName: 'David Chen', customerPhone: '(415) 555-0841', customerInitials: 'DC', pickupMode: 'curbside', spotNumber: 2, vehicleDetails: { makeModel: 'Midnight Blue Rivian R1T', plate: '8ABC104', hazardsFlashing: true, }, status: 'arrived', statusLabel: 'Customer Arrived · Waiting 4m', waitMinutes: 4, stagingLocation: { zone: 'Aisle 01', bin: 'Bin A-08', type: 'bulky', }, items: [ { id: 'item-3', name: '4K Ultra HD Smart Monitor 27"', sku: 'DIS-270-4K', quantity: 1, price: 380.0, staged: true, category: 'Displays', }, { id: 'item-4', name: 'Ergonomic Desk Stand', sku: 'ACC-110-SLV', quantity: 1, price: 110.0, staged: true, category: 'Accessories', }, { id: 'item-5', name: '100W USB-C Thunderbolt Dock', sku: 'DOK-090-BLK', quantity: 1, price: 90.0, staged: true, category: 'Power', }, ], orderTotal: 580.0, placedAt: '10:45 AM', readyAt: '11:10 AM', arrivedAt: '11:40 AM', verificationCode: '8419', assignedRunner: 'Marcus Lee', }, { id: 'order-3', orderNumber: 'BOPIS-84918', customerName: 'Marcus Vance', customerPhone: '(415) 555-0329', customerInitials: 'MV', pickupMode: 'in_store', pickupDeskBay: 'Pickup Desk Bay 01', status: 'en_route', statusLabel: 'En Route · 6 min ETA', etaMinutes: 6, distanceMiles: 1.4, stagingLocation: { zone: 'Locker #12', bin: 'Bin L-12', lockerPin: '8491', type: 'locker', }, items: [ { id: 'item-6', name: 'Pro Max Mirrorless Camera Kit', sku: 'CAM-800-PRO', quantity: 1, price: 1299.0, staged: true, category: 'Photography', }, ], orderTotal: 1299.0, placedAt: '11:05 AM', readyAt: '11:28 AM', verificationCode: '9182', assignedRunner: 'Staging Auto-Lock', }, { id: 'order-4', orderNumber: 'BOPIS-84917', customerName: 'Aisha Patel', customerPhone: '(415) 555-0782', customerInitials: 'AP', pickupMode: 'in_store', pickupDeskBay: 'Pickup Desk Bay 03', status: 'ready_staging', statusLabel: 'Ready in Staging Locker #04', stagingLocation: { zone: 'Aisle 03', bin: 'Bin C-02', lockerPin: '3719', type: 'locker', }, items: [ { id: 'item-7', name: 'Organic Single-Origin Espresso Blend', sku: 'COF-012-ESP', quantity: 2, price: 38.0, staged: true, category: 'Beverage', }, { id: 'item-8', name: 'Precision Gooseneck Pour-Over Kettle', sku: 'KET-044-SS', quantity: 1, price: 74.5, staged: true, category: 'Kitchen', }, { id: 'item-9', name: 'Digital Barista Gram Scale', sku: 'SCL-019-DIG', quantity: 1, price: 30.0, staged: true, category: 'Kitchen', }, ], orderTotal: 142.5, placedAt: '10:15 AM', readyAt: '10:50 AM', verificationCode: '1742', assignedRunner: 'Desk Associate', }, { id: 'order-5', orderNumber: 'BOPIS-84916', customerName: 'Sarah Jenkins', customerPhone: '(415) 555-0453', customerInitials: 'SJ', pickupMode: 'curbside', spotNumber: 1, vehicleDetails: { makeModel: 'White Toyota RAV4', plate: '6MNO451', hazardsFlashing: false, }, status: 'en_route', statusLabel: 'En Route · 12 min ETA', etaMinutes: 12, distanceMiles: 3.8, stagingLocation: { zone: 'Aisle 02', bin: 'Bin B-03', type: 'shelf', }, items: [ { id: 'item-10', name: 'Ultra Hydration 20L Trail Backpack', sku: 'OUT-220-BLU', quantity: 1, price: 130.0, staged: true, category: 'Outdoors', }, { id: 'item-11', name: 'Carbon Composite Trekking Poles (Pair)', sku: 'OUT-771-CAR', quantity: 1, price: 80.0, staged: true, category: 'Outdoors', }, ], orderTotal: 210.0, placedAt: '11:15 AM', readyAt: '11:38 AM', verificationCode: '9160', assignedRunner: 'Unassigned', }, ]
Raw manifest:https://uipkge.dev/r/react/store-pickup-curbside.json