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-vue@latest add https://uipkge.dev/r/vue/store-pickup-curbside.json$npx shadcn-vue@latest add https://uipkge.dev/r/vue/store-pickup-curbside.json$yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/store-pickup-curbside.json$bunx shadcn-vue@latest add https://uipkge.dev/r/vue/store-pickup-curbside.jsonNamed registry:
npx shadcn-vue@latest add @uipkge/store-pickup-curbsideInstalls to:app/components/blocks/Variants
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
storeName | string | 'Pasadena Flagship Store #02' | optional |
storeCode | string | 'STORE-CA-02' | optional |
initialModeFilter | 'all''curbside''in_store' | 'all' | optional |
class | HTMLAttributes['class'] | — | optional |
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)
app/components/blocks/StorePickupCurbside.vue30.8 kB
<script setup lang="ts"> import type { HTMLAttributes } from 'vue' import { computed, ref } from 'vue' import { Bell, Car, Check, CheckCircle2, Clock, Eye, Layers, MapPin, MessageSquare, MoreHorizontal, Navigation, Package, PackageCheck, Phone, Plus, RefreshCw, Search, Send, Store, X, } from 'lucide-vue-next' 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.vue' import StorePickupModals from './StorePickupModals.vue' 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 } interface Props { storeName?: string storeCode?: string initialModeFilter?: 'all' | 'curbside' | 'in_store' class?: HTMLAttributes['class'] } const props = withDefaults(defineProps<Props>(), { storeName: 'Pasadena Flagship Store #02', storeCode: 'STORE-CA-02', initialModeFilter: 'all', }) // State const orders = ref<PickupOrder[]>(INITIAL_ORDERS) const searchQuery = ref('') const selectedModeFilter = ref<'all' | 'curbside' | 'in_store'>(props.initialModeFilter) const selectedStatusFilter = ref<'all' | 'arrived' | 'en_route' | 'ready_staging' | 'completed'>('all') const lastRefreshed = ref('Just now') const isLiveSyncing = ref(true) // Modals State const checkInModalOpen = ref(false) const inspectModalOpen = ref(false) const notifyModalOpen = ref(false) const newOrderModalOpen = ref(false) const selectedOrder = ref<PickupOrder | null>(null) const actionFeedbackToast = ref<string | null>(null) // Computed const readyInStagingCount = computed(() => orders.value.filter((o) => o.status !== 'completed').length) const arrivedCustomersCount = computed(() => orders.value.filter((o) => o.status === 'arrived').length) const enRouteCount = computed(() => orders.value.filter((o) => o.status === 'en_route').length) const completedCount = computed(() => orders.value.filter((o) => o.status === 'completed').length + 32) const filteredOrders = computed(() => { return orders.value.filter((order) => { const matchesSearch = searchQuery.value.trim() === '' || order.customerName.toLowerCase().includes(searchQuery.value.toLowerCase()) || order.orderNumber.toLowerCase().includes(searchQuery.value.toLowerCase()) || order.customerPhone.includes(searchQuery.value) || order.stagingLocation.bin.toLowerCase().includes(searchQuery.value.toLowerCase()) || (order.vehicleDetails?.makeModel.toLowerCase().includes(searchQuery.value.toLowerCase()) ?? false) || (order.vehicleDetails?.plate.toLowerCase().includes(searchQuery.value.toLowerCase()) ?? false) || order.items.some((i) => i.name.toLowerCase().includes(searchQuery.value.toLowerCase())) const matchesMode = selectedModeFilter.value === 'all' || order.pickupMode === selectedModeFilter.value const matchesStatus = selectedStatusFilter.value === 'all' || order.status === selectedStatusFilter.value return matchesSearch && matchesMode && matchesStatus }) }) function triggerToast(msg: string) { actionFeedbackToast.value = msg setTimeout(() => { if (actionFeedbackToast.value === msg) { actionFeedbackToast.value = null } }, 3500) } function handleInspect(order: PickupOrder) { selectedOrder.value = order inspectModalOpen.value = true } function handleNotify(order: PickupOrder) { selectedOrder.value = order notifyModalOpen.value = true } function handleSendNotification(channel: 'sms' | 'push') { if (!selectedOrder.value) return notifyModalOpen.value = false triggerToast(`Sent arrival notification to ${selectedOrder.value?.customerName} via ${channel.toUpperCase()}`) } function handleMarkDelivered(orderId: string) { const target = orders.value.find((o) => o.id === orderId) if (target) { target.status = 'completed' target.statusLabel = 'Delivered & Handed Over' triggerToast(`Order #${target.orderNumber} successfully delivered to ${target.customerName}!`) if (inspectModalOpen.value) { inspectModalOpen.value = false } } } function handleConfirmArrival(payload: { orderId: string spot: string vehicleModel: string plate: string hazards: boolean notes: string }) { const order = orders.value.find((o) => o.id === payload.orderId) if (order) { order.status = 'arrived' order.pickupMode = 'curbside' order.spotNumber = parseInt(payload.spot, 10) || 4 order.waitMinutes = 1 order.statusLabel = 'Customer Arrived · Waiting 1m' order.vehicleDetails = { makeModel: payload.vehicleModel || 'Vehicle Unspecified', plate: payload.plate || 'N/A', hazardsFlashing: payload.hazards, } order.arrivedAt = 'Just now' checkInModalOpen.value = false triggerToast(`Customer ${order.customerName} checked into Spot #${order.spotNumber}! Runner alerted.`) } } function 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', } orders.value.unshift(newOrder) newOrderModalOpen.value = false triggerToast(`New Order #${newOrder.orderNumber} staged in ${newOrder.stagingLocation.bin}!`) } function handleRefresh() { lastRefreshed.value = 'Just now' triggerToast('Refreshed pickup staging queue & curbside telemetry') } </script> <template> <div :class="cn('text-foreground w-full space-y-6', props.class)"> <!-- Live Toast Notification Banner --> <div v-if="actionFeedbackToast" class="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 class="flex items-center gap-2"> <CheckCircle2 class="text-success size-4 shrink-0" /> <span>{{ actionFeedbackToast }}</span> </div> <button aria-label="Dismiss notification" type="button" class="text-muted-foreground hover:text-foreground inline-flex size-4 items-center justify-center" @click="actionFeedbackToast = null" > <X class="size-3.5" /> </button> </div> <!-- Header Section --> <div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between"> <div> <div class="flex flex-wrap items-center gap-2.5"> <div class="bg-primary/10 text-primary border-primary/20 flex size-8 items-center justify-center rounded-lg border" > <Car class="size-4" /> </div> <h1 class="text-xl font-bold tracking-tight md:text-2xl">Store Pickup & Curbside Coordination</h1> <Badge wrap variant="outline" class="gap-1.5 font-medium"> <span class="bg-success size-1.5 animate-pulse rounded-full" /> <span class="text-xs">Live Staging Radar</span> </Badge> </div> <div class="text-muted-foreground mt-1.5 flex flex-wrap items-center gap-2 text-xs"> <span class="text-foreground flex items-center gap-1 font-medium"> <MapPin class="text-muted-foreground size-3.5" /> {{ props.storeName }} </span> <span class="font-mono text-xs">{{ props.storeCode }}</span> <span class="text-foreground font-medium tabular-nums"> {{ readyInStagingCount }} Ready · {{ enRouteCount }} En Route · {{ completedCount }} Completed </span> </div> </div> <div class="flex flex-wrap items-center gap-2"> <Button variant="outline" size="sm" class="h-9 gap-1.5 text-xs shadow-xs" @click="checkInModalOpen = true"> <Car class="text-warning size-3.5" /> <span>Customer "I'm Here" Check-In</span> </Button> <Button size="sm" class="h-9 gap-1.5 text-xs shadow-xs" @click="newOrderModalOpen = true"> <Plus class="size-3.5" /> <span>New Pickup Order</span> </Button> </div> </div> <!-- 4 Fulfillment Health Cards --> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> <!-- Card 1: Ready in Staging --> <Card class="border-border shadow-xs"> <CardHeader class="flex flex-row flex-wrap items-center justify-between gap-2 pb-2"> <CardTitle class="text-muted-foreground text-xs font-semibold tracking-wider uppercase"> Ready for Customer </CardTitle> <div class="bg-muted text-muted-foreground flex size-7 items-center justify-center rounded-md"> <PackageCheck class="text-foreground size-4" /> </div> </CardHeader> <CardContent class="space-y-1.5"> <div class="text-2xl font-bold tracking-tight tabular-nums">8 Orders in Staging</div> <div class="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" class="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 class="border-warning/30 bg-warning/5 shadow-xs"> <CardHeader class="flex flex-row flex-wrap items-center justify-between gap-2 pb-2"> <CardTitle class="text-warning text-warning min-w-0 text-xs font-semibold tracking-wider break-words uppercase" > Customers in Parking Bays </CardTitle> <div class="bg-warning/15 text-warning flex size-7 items-center justify-center rounded-md"> <Car class="text-warning size-4" /> </div> </CardHeader> <CardContent class="space-y-1.5"> <div class="text-foreground dark:text-foreground text-2xl font-bold tracking-tight tabular-nums"> {{ arrivedCustomersCount }} Customers Arrived </div> <div class="text-muted-foreground flex flex-wrap items-center justify-between gap-x-2 gap-y-1 text-xs"> <span class="text-warning/80 text-warning/80">Bay #4 (2m) · Bay #2 (4m)</span> <Badge wrap variant="warning" class="h-5 gap-1 px-1.5 text-xs font-medium"> <span class="bg-warning bg-warning size-1.5 rounded-full" /> Action Required </Badge> </div> </CardContent> </Card> <!-- Card 3: Avg Fulfillment Time --> <Card class="border-border shadow-xs"> <CardHeader class="flex flex-row flex-wrap items-center justify-between gap-2 pb-2"> <CardTitle class="text-muted-foreground text-xs font-semibold tracking-wider uppercase"> Avg Fulfillment Time </CardTitle> <div class="bg-muted text-muted-foreground flex size-7 items-center justify-center rounded-md"> <Clock class="text-foreground size-4" /> </div> </CardHeader> <CardContent class="space-y-1.5"> <div class="text-2xl font-bold tracking-tight tabular-nums">1m 45s</div> <div class="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" class="h-5 px-1.5 text-xs font-medium"> Fastest: 52s </Badge> </div> </CardContent> </Card> <!-- Card 4: Today's Completed Pickups --> <Card class="border-border shadow-xs"> <CardHeader class="flex flex-row flex-wrap items-center justify-between gap-2 pb-2"> <CardTitle class="text-muted-foreground text-xs font-semibold tracking-wider uppercase"> Today's Pickups </CardTitle> <div class="bg-muted text-muted-foreground flex size-7 items-center justify-center rounded-md"> <CheckCircle2 class="text-foreground size-4" /> </div> </CardHeader> <CardContent class="space-y-1.5"> <div class="text-2xl font-bold tracking-tight tabular-nums">{{ completedCount }} Orders</div> <div class="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" class="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 class="border-border shadow-xs"> <CardHeader class="space-y-4 pb-4"> <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div> <CardTitle class="text-base font-semibold">Active Pickup Orders Board</CardTitle> <CardDescription class="text-xs"> Live queue of customers arriving curbside and checking in at in-store fulfillment stations. </CardDescription> </div> <div class="flex items-center gap-2"> <Button variant="outline" size="sm" class="h-8 gap-1.5 text-xs shadow-xs" @click="handleRefresh"> <RefreshCw class="text-muted-foreground size-3" /> <span>Refresh</span> </Button> </div> </div> <!-- Filter & Search Controls --> <div class="flex flex-col gap-2.5 sm:flex-row sm:items-center sm:justify-between"> <div class="relative max-w-sm flex-1"> <Search class="text-muted-foreground absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2" /> <Input v-model="searchQuery" placeholder="Search customer, order #, spot, or items..." class="h-8 pl-8 text-xs" /> <button aria-label="Clear search" v-if="searchQuery" type="button" class="text-muted-foreground hover:text-foreground absolute top-1/2 right-2.5 -translate-y-1/2" @click="searchQuery = ''" > <X class="size-3" /> </button> </div> <div class="flex flex-wrap items-center gap-2"> <div class="flex items-center gap-1.5"> <span class="text-muted-foreground text-xs">Mode:</span> <Select v-model="selectedModeFilter"> <SelectTrigger class="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 class="flex items-center gap-1.5"> <span class="text-muted-foreground text-xs">Status:</span> <Select v-model="selectedStatusFilter"> <SelectTrigger class="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 class="p-0"> <div class="overflow-x-auto"> <Table> <TableHeader class="bg-muted/40"> <TableRow> <TableHead class="w-[200px] text-xs font-semibold">Customer & Contact</TableHead> <TableHead class="w-[180px] text-xs font-semibold">Order & Items</TableHead> <TableHead class="w-[220px] text-xs font-semibold">Pickup Mode & Bay</TableHead> <TableHead class="w-[200px] text-xs font-semibold">Customer Status</TableHead> <TableHead class="w-[160px] text-xs font-semibold">Staging Location</TableHead> <TableHead class="w-[180px] text-right text-xs font-semibold">Fulfillment Actions</TableHead> </TableRow> </TableHeader> <TableBody> <TableRow v-for="order in filteredOrders" :key="order.id" :class=" cn( 'transition-colors duration-150', order.status === 'arrived' && 'bg-warning/[0.04] bg-warning/[0.08]', ) " > <!-- Customer Name & Phone --> <TableCell class="py-3"> <div class="flex items-center gap-2.5"> <Avatar class="border-border size-8 rounded-full border"> <AvatarFallback class="bg-muted text-xs font-medium"> {{ order.customerInitials }} </AvatarFallback> </Avatar> <div class="space-y-0.5"> <div class="text-foreground text-xs leading-tight font-semibold"> {{ order.customerName }} </div> <div class="text-muted-foreground flex items-center gap-1 font-mono text-xs tabular-nums"> <Phone class="text-muted-foreground/70 size-3" /> {{ order.customerPhone }} </div> </div> </div> </TableCell> <!-- Order Number & Items Summary --> <TableCell class="py-3"> <div class="space-y-1"> <div class="flex items-center gap-1.5"> <span class="text-foreground font-mono text-xs font-bold"> #{{ order.orderNumber }} </span> <span class="text-muted-foreground text-xs tabular-nums"> (${{ order.orderTotal.toFixed(2) }}) </span> </div> <div class="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 class="py-3"> <div class="space-y-1"> <template v-if="order.pickupMode === 'curbside'"> <Badge wrap variant="outline" class="border-success/30 bg-success/10 text-success gap-1 font-medium" > <Car class="text-success size-3 shrink-0" /> <span>Curbside Spot #{{ order.spotNumber }}</span> </Badge> <div v-if="order.vehicleDetails" class="text-muted-foreground flex items-center gap-1 text-xs"> <span class="max-w-[180px] truncate">{{ order.vehicleDetails.makeModel }}</span> <span class="text-foreground font-mono font-medium">· {{ order.vehicleDetails.plate }}</span> </div> </template> <template v-else> <Badge wrap variant="outline" class="border-info/30 bg-info/10 text-info gap-1 font-medium"> <Store class="text-info size-3 shrink-0" /> <span>In-Store Desk</span> </Badge> <div class="text-muted-foreground text-xs"> {{ order.pickupDeskBay || 'Front Pickup Counter' }} </div> </template> </div> </TableCell> <!-- Customer Status --> <TableCell class="py-3"> <div class="space-y-1"> <template v-if="order.status === 'arrived'"> <Badge wrap variant="warning" class="gap-1.5 px-2 py-0.5 font-medium"> <span class="bg-warning size-1.5 rounded-full" /> <Clock class="size-3" /> <span class="tabular-nums">Customer Arrived · Waiting {{ order.waitMinutes || 2 }}m</span> </Badge> <div class="text-muted-foreground flex items-center gap-1 text-xs"> <span>Check-in: {{ order.arrivedAt || '11:40 AM' }}</span> <span v-if="order.vehicleDetails?.hazardsFlashing" class="text-warning font-medium" >· Hazards On</span > </div> </template> <template v-else-if="order.status === 'en_route'"> <Badge wrap variant="info" class="gap-1.5 px-2 py-0.5 font-medium"> <Navigation class="text-info size-3" /> <span class="tabular-nums">En Route · {{ order.etaMinutes }} min ETA</span> </Badge> <div class="text-muted-foreground text-xs"> <span class="tabular-nums">{{ order.distanceMiles }} miles away</span> </div> </template> <template v-else-if="order.status === 'ready_staging'"> <Badge wrap variant="secondary" class="gap-1.5 px-2 py-0.5 font-medium"> <Package class="size-3" /> <span>Ready in Staging</span> </Badge> <div class="text-muted-foreground text-xs">Staged at {{ order.readyAt }}</div> </template> <template v-else> <Badge wrap variant="success" class="gap-1.5 px-2 py-0.5 font-medium"> <CheckCircle2 class="size-3" /> <span>Delivered</span> </Badge> <div class="text-muted-foreground text-xs">Completed Handoff</div> </template> </div> </TableCell> <!-- Staging Location --> <TableCell class="py-3"> <div class="space-y-0.5"> <div class="text-foreground flex items-center gap-1.5 text-xs font-semibold"> <Layers class="text-muted-foreground size-3.5" /> <span>{{ order.stagingLocation.zone }}</span> <span class="text-primary font-mono font-bold">{{ order.stagingLocation.bin }}</span> </div> <div v-if="order.stagingLocation.lockerPin" class="text-muted-foreground font-mono text-xs"> PIN Code: <span class="text-foreground font-bold">{{ order.stagingLocation.lockerPin }}</span> </div> <div v-else class="text-muted-foreground text-xs capitalize"> Type: {{ order.stagingLocation.type }} staging </div> </div> </TableCell> <!-- Fulfillment Actions --> <TableCell class="py-3 text-right"> <div class="flex items-center justify-end gap-1.5"> <!-- Primary Mark Delivered Action (For Arrived Orders) --> <Button v-if="order.status === 'arrived'" size="sm" class="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" @click="handleMarkDelivered(order.id)" > <Check class="size-3.5" /> <span>Mark Delivered</span> </Button> <!-- Notify Customer Action --> <Button v-else-if="order.status !== 'completed'" variant="outline" size="sm" class="h-8 gap-1 px-2.5 text-xs shadow-xs" @click="handleNotify(order)" > <Bell class="text-muted-foreground size-3" /> <span>Notify</span> </Button> <Button v-else variant="outline" size="sm" disabled class="h-8 text-xs opacity-60"> <CheckCircle2 class="text-success size-3" /> <span>Fulfilled</span> </Button> <!-- Dropdown for Inspect & Details --> <DropdownMenu> <DropdownMenuTrigger as-child> <Button variant="ghost" size="icon" class="text-muted-foreground hover:text-foreground size-8"> <MoreHorizontal class="size-4" /> <span class="sr-only">Actions</span> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end" class="w-48 text-xs"> <DropdownMenuLabel>Order #{{ order.orderNumber }}</DropdownMenuLabel> <DropdownMenuSeparator /> <DropdownMenuItem class="cursor-pointer gap-2" @click="handleInspect(order)"> <Eye class="text-muted-foreground size-3.5" /> <span>Inspect Order Details</span> </DropdownMenuItem> <DropdownMenuItem class="cursor-pointer gap-2" @click="handleNotify(order)"> <MessageSquare class="text-muted-foreground size-3.5" /> <span>Send SMS / Push Alert</span> </DropdownMenuItem> <DropdownMenuSeparator /> <DropdownMenuItem v-if="order.status !== 'completed'" class="text-success cursor-pointer gap-2 font-medium" @click="handleMarkDelivered(order.id)" > <CheckCircle2 class="size-3.5" /> <span>Complete Handoff</span> </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </div> </TableCell> </TableRow> <TableRow v-if="filteredOrders.length === 0"> <TableCell colspan="6" class="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 class="border-border text-muted-foreground flex items-center justify-between border-t px-6 py-3 text-xs" > <div> Showing <span class="text-foreground font-medium tabular-nums">{{ filteredOrders.length }}</span> of <span class="text-foreground font-medium tabular-nums">{{ orders.length }}</span> active pickup orders </div> <div class="flex items-center gap-3"> <div class="flex items-center gap-1.5"> <span class="bg-success size-2 rounded-full" /> <span>Curbside Bays (Spots 1-6)</span> </div> <div class="flex items-center gap-1.5"> <span class="bg-info size-2 rounded-full" /> <span>In-Store Pickup Desk</span> </div> </div> </CardFooter> </Card> <!-- Modal 1: Inspect Order Details Dialog --> <OrderDetailDialog v-model:open="inspectModalOpen" :selected-order="selectedOrder" @mark-delivered="handleMarkDelivered" /> <!-- Action Modals (Check-In, Notify, New Order) --> <StorePickupModals v-model:check-in-modal-open="checkInModalOpen" v-model:notify-modal-open="notifyModalOpen" v-model:new-order-modal-open="newOrderModalOpen" :orders="orders" :selected-order="selectedOrder" :store-name="props.storeName" @confirm-arrival="handleConfirmArrival" @send-notification="handleSendNotification" @create-order="handleCreateNewOrder" /> </div> </template>app/components/blocks/OrderDetailDialog.vue6.4 kB
<script setup lang="ts"> import { Car, Check, CheckCircle2, Layers } from 'lucide-vue-next' 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' interface Props { open: boolean selectedOrder: PickupOrder | null } defineProps<Props>() defineEmits<{ 'update:open': [value: boolean] 'mark-delivered': [orderId: string] }>() </script> <template> <Dialog :open="open" @update:open="$emit('update:open', $event)"> <DialogContent class="sm:max-w-lg"> <DialogHeader v-if="selectedOrder"> <div class="flex items-start justify-between"> <div> <div class="flex items-center gap-2"> <DialogTitle class="text-base font-bold"> Order #{{ selectedOrder.orderNumber }} </DialogTitle> <Badge :variant="selectedOrder.pickupMode === 'curbside' ? 'success' : 'info'" class="text-xs font-medium whitespace-normal" > {{ selectedOrder.pickupMode === 'curbside' ? `Curbside Bay #${selectedOrder.spotNumber}` : 'In-Store Desk' }} </Badge> </div> <DialogDescription class="mt-1 text-xs"> Placed at {{ selectedOrder.placedAt }} · Staged at {{ selectedOrder.readyAt }} </DialogDescription> </div> </div> </DialogHeader> <div v-if="selectedOrder" class="space-y-4 py-2 text-xs"> <!-- Customer & Contact Strip --> <div class="border-border bg-muted/30 flex items-center justify-between rounded-lg border p-3"> <div class="flex items-center gap-2.5"> <Avatar class="border-border size-9 border"> <AvatarFallback class="text-xs font-bold">{{ selectedOrder.customerInitials }}</AvatarFallback> </Avatar> <div> <div class="text-foreground text-xs font-semibold">{{ selectedOrder.customerName }}</div> <div class="text-muted-foreground font-mono text-xs">{{ selectedOrder.customerPhone }}</div> </div> </div> <div class="text-right"> <div class="text-muted-foreground text-xs">Pickup Verification PIN</div> <div class="text-primary font-mono text-sm font-bold tracking-widest"> {{ selectedOrder.verificationCode }} </div> </div> </div> <!-- Staging & Bay Location Details --> <div class="grid grid-cols-1 gap-3 sm:grid-cols-2"> <div class="border-border space-y-1 rounded-md border p-2.5"> <div class="text-muted-foreground flex items-center gap-1 text-xs font-medium"> <Layers class="size-3.5" /> <span>Staging Bin Location</span> </div> <div class="text-foreground text-xs font-bold"> {{ selectedOrder.stagingLocation.zone }} · <span class="text-primary font-mono">{{ selectedOrder.stagingLocation.bin }}</span> </div> <div class="text-muted-foreground text-xs capitalize"> {{ selectedOrder.stagingLocation.type }} unit storage </div> </div> <div class="border-border space-y-1 rounded-md border p-2.5"> <div class="text-muted-foreground flex items-center gap-1 text-xs font-medium"> <Car class="size-3.5" /> <span>Vehicle & Bay</span> </div> <template v-if="selectedOrder.pickupMode === 'curbside' && selectedOrder.vehicleDetails"> <div class="text-foreground truncate text-xs font-bold"> Spot #{{ selectedOrder.spotNumber }} · {{ selectedOrder.vehicleDetails.makeModel }} </div> <div class="text-muted-foreground font-mono text-xs">Plate: {{ selectedOrder.vehicleDetails.plate }}</div> </template> <template v-else> <div class="text-foreground text-xs font-bold"> {{ selectedOrder.pickupDeskBay || 'Pickup Counter' }} </div> <div class="text-muted-foreground text-xs"> Locker PIN: {{ selectedOrder.stagingLocation.lockerPin || 'N/A' }} </div> </template> </div> </div> <!-- Staged Items Checklist --> <div class="space-y-2"> <div class="text-foreground flex items-center justify-between text-xs font-medium"> <span>Order Items Checklist ({{ selectedOrder.items.length }} items)</span> <span class="text-muted-foreground">Total: ${{ selectedOrder.orderTotal.toFixed(2) }}</span> </div> <div class="border-border max-h-40 space-y-1.5 overflow-y-auto rounded-md border p-2"> <div v-for="item in selectedOrder.items" :key="item.id" class="bg-muted/40 flex items-center justify-between rounded p-2 text-xs" > <div class="flex items-center gap-2"> <CheckCircle2 class="text-success size-3.5 shrink-0" /> <div> <div class="text-foreground font-medium">{{ item.name }}</div> <div class="text-muted-foreground font-mono text-xs"> SKU: {{ item.sku }} · Qty: {{ item.quantity }} </div> </div> </div> <div class="text-foreground font-mono text-xs font-medium"> ${{ (item.price * item.quantity).toFixed(2) }} </div> </div> </div> </div> </div> <DialogFooter class="gap-2 sm:gap-0"> <Button variant="outline" size="sm" class="text-xs" @click="$emit('update:open', false)"> Close </Button> <Button v-if="selectedOrder?.status !== 'completed'" size="sm" class="bg-success hover:bg-success/90 gap-1.5 text-xs text-white" @click="selectedOrder && $emit('mark-delivered', selectedOrder.id)" > <Check class="size-3.5" /> <span>Mark Delivered & Close</span> </Button> </DialogFooter> </DialogContent> </Dialog> </template>app/components/blocks/StorePickupModals.vue14.9 kB
<script setup lang="ts"> import { ref } from 'vue' import { Bell, Car, CheckCircle2, MessageSquare, Send } from 'lucide-vue-next' 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 Props { checkInModalOpen: boolean notifyModalOpen: boolean newOrderModalOpen: boolean orders: PickupOrder[] selectedOrder: PickupOrder | null storeName: string } const props = defineProps<Props>() const emit = defineEmits<{ 'update:checkInModalOpen': [value: boolean] 'update:notifyModalOpen': [value: boolean] 'update:newOrderModalOpen': [value: boolean] 'confirm-arrival': [ payload: { orderId: string spot: string vehicleModel: string plate: string hazards: boolean notes: string }, ] 'send-notification': [channel: 'sms' | 'push'] 'create-order': [ payload: { customerName: string customerPhone: string pickupMode: PickupMode spotNumber: string stagingAisle: string stagingBin: string itemSummary: string orderTotal: string }, ] }>() // Check-in state const checkInOrderId = ref('order-3') const checkInSpot = ref('4') const checkInVehicleModel = ref('Silver Tesla Model 3') const checkInPlate = ref('7XYZ892') const checkInHazards = ref(true) const checkInNotes = ref('Parked in bay 4, trunk unlocked for contactless placement.') // Notify state const notifyChannel = ref<'sms' | 'push'>('sms') const notifyMessageSent = ref(false) // New Order state const newCustomerName = ref('') const newCustomerPhone = ref('') const newPickupMode = ref<PickupMode>('curbside') const newSpotNumber = ref('3') const newStagingAisle = ref('Aisle 01') const newStagingBin = ref('Bin A-12') const newItemSummary = ref('') const newOrderTotal = ref('120.00') function handleCheckInSubmit() { emit('confirm-arrival', { orderId: checkInOrderId.value, spot: checkInSpot.value, vehicleModel: checkInVehicleModel.value, plate: checkInPlate.value, hazards: checkInHazards.value, notes: checkInNotes.value, }) } function handleSendNotification() { if (!props.selectedOrder) return notifyMessageSent.value = true setTimeout(() => { emit('send-notification', notifyChannel.value) notifyMessageSent.value = false }, 1000) } function handleCreateOrder() { if (!newCustomerName.value) return emit('create-order', { customerName: newCustomerName.value, customerPhone: newCustomerPhone.value, pickupMode: newPickupMode.value, spotNumber: newSpotNumber.value, stagingAisle: newStagingAisle.value, stagingBin: newStagingBin.value, itemSummary: newItemSummary.value, orderTotal: newOrderTotal.value, }) newCustomerName.value = '' newCustomerPhone.value = '' newItemSummary.value = '' } </script> <template> <div> <!-- Modal 1: Customer "I'm Here" Parking Spot Check-in Dialog --> <Dialog :open="checkInModalOpen" @update:open="$emit('update:checkInModalOpen', $event)"> <DialogContent class="sm:max-w-md"> <DialogHeader> <div class="flex items-center gap-2"> <div class="border-warning/20 bg-warning/10 text-warning flex size-8 items-center justify-center rounded-lg border" > <Car class="size-4" /> </div> <div> <DialogTitle class="text-base font-bold">Customer Parking Arrival Check-In</DialogTitle> <DialogDescription class="text-xs"> Check in customer arriving at the curbside pickup bay or simulate customer mobile app check-in. </DialogDescription> </div> </div> </DialogHeader> <div class="space-y-4 py-2 text-xs"> <!-- Select Order to Check In --> <div class="space-y-1.5"> <label class="text-foreground font-medium">Select Customer Order</label> <Select v-model="checkInOrderId"> <SelectTrigger class="h-9 text-xs"> <SelectValue placeholder="Select active order" /> </SelectTrigger> <SelectContent> <SelectItem v-for="o in orders.filter((x) => x.status !== 'completed')" :key="o.id" :value="o.id"> {{ o.customerName }} · #{{ o.orderNumber }} ({{ o.items.length }} items) </SelectItem> </SelectContent> </Select> </div> <!-- Select Parking Spot --> <div class="space-y-1.5"> <label class="text-foreground font-medium">Curbside Parking Bay</label> <div class="grid grid-cols-6 gap-1.5"> <button v-for="spot in ['1', '2', '3', '4', '5', '6']" :key="spot" type="button" :class=" 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', ) " @click="checkInSpot = spot" > <span>Bay</span> <span class="text-sm font-bold">#{{ spot }}</span> </button> </div> </div> <!-- Vehicle Information --> <div class="grid grid-cols-1 gap-3 sm:grid-cols-2"> <div class="space-y-1.5"> <label class="text-foreground font-medium">Vehicle Make & Model</label> <Input v-model="checkInVehicleModel" placeholder="e.g. Silver Tesla Model 3" class="h-8 text-xs" /> </div> <div class="space-y-1.5"> <label class="text-foreground font-medium">License Plate</label> <Input v-model="checkInPlate" placeholder="e.g. 7XYZ892" class="h-8 font-mono text-xs uppercase" /> </div> </div> <!-- Arrival Notes --> <div class="space-y-1.5"> <label class="text-foreground font-medium">Arrival Instructions & Bay Notes</label> <Input v-model="checkInNotes" placeholder="e.g. Trunk unlocked, hazard lights on" class="h-8 text-xs" /> </div> <!-- Hazard Lights Checkbox Simulation --> <div class="border-border bg-muted/40 flex items-center gap-2 rounded-md border p-2.5"> <input id="hazards-checkbox" v-model="checkInHazards" type="checkbox" class="border-border text-primary focus:ring-primary size-4 rounded" /> <label for="hazards-checkbox" class="text-foreground cursor-pointer text-xs"> Vehicle hazard lights are actively flashing for runner identification </label> </div> </div> <DialogFooter class="gap-2 sm:gap-0"> <DialogClose as-child> <Button variant="outline" size="sm" class="text-xs">Cancel</Button> </DialogClose> <Button size="sm" class="bg-success hover:bg-success/90 gap-1.5 text-xs text-white" @click="handleCheckInSubmit" > <CheckCircle2 class="size-3.5" /> <span>Confirm Arrival & Alert Runner</span> </Button> </DialogFooter> </DialogContent> </Dialog> <!-- Modal 2: Notify Customer Dialog --> <Dialog :open="notifyModalOpen" @update:open="$emit('update:notifyModalOpen', $event)"> <DialogContent class="sm:max-w-md"> <DialogHeader v-if="selectedOrder"> <DialogTitle class="text-base font-bold">Notify Customer</DialogTitle> <DialogDescription class="text-xs"> Send real-time pickup status alert to {{ selectedOrder.customerName }} ({{ selectedOrder.customerPhone }}). </DialogDescription> </DialogHeader> <div v-if="selectedOrder" class="space-y-4 py-2 text-xs"> <div class="space-y-1.5"> <label class="text-foreground font-medium">Notification Channel</label> <div class="grid grid-cols-1 gap-2 sm:grid-cols-2"> <button type="button" :class=" 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', ) " @click="notifyChannel = 'sms'" > <MessageSquare class="size-3.5" /> <span>SMS Text Message</span> </button> <button type="button" :class=" 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', ) " @click="notifyChannel = 'push'" > <Bell class="size-3.5" /> <span>Mobile App Push</span> </button> </div> </div> <div class="border-border bg-muted/40 space-y-1.5 rounded-md border p-3 text-xs"> <div class="text-foreground flex items-center justify-between font-medium"> <span>Message Preview</span> <span class="text-muted-foreground font-mono">160 chars</span> </div> <p class="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 class="gap-2 sm:gap-0"> <DialogClose as-child> <Button variant="outline" size="sm" class="text-xs">Cancel</Button> </DialogClose> <Button size="sm" class="gap-1.5 text-xs" :disabled="notifyMessageSent" @click="handleSendNotification"> <Send class="size-3.5" /> <span>{{ notifyMessageSent ? 'Notification Sent!' : 'Send Alert' }}</span> </Button> </DialogFooter> </DialogContent> </Dialog> <!-- Modal 3: New Pickup Order Staging Dialog --> <Dialog :open="newOrderModalOpen" @update:open="$emit('update:newOrderModalOpen', $event)"> <DialogContent class="sm:max-w-md"> <DialogHeader> <DialogTitle class="text-base font-bold">Stage New Pickup Order</DialogTitle> <DialogDescription class="text-xs"> Register an incoming online BOPIS order into retail staging shelves or lockers. </DialogDescription> </DialogHeader> <div class="space-y-3.5 py-2 text-xs"> <div class="grid grid-cols-1 gap-3 sm:grid-cols-2"> <div class="space-y-1.5"> <label class="text-foreground font-medium">Customer Name</label> <Input v-model="newCustomerName" placeholder="e.g. Alex Morgan" class="h-8 text-xs" /> </div> <div class="space-y-1.5"> <label class="text-foreground font-medium">Customer Phone</label> <Input v-model="newCustomerPhone" placeholder="e.g. (415) 555-0199" class="h-8 font-mono text-xs" /> </div> </div> <div class="grid grid-cols-1 gap-3 sm:grid-cols-2"> <div class="space-y-1.5"> <label class="text-foreground font-medium">Pickup Mode</label> <Select v-model="newPickupMode"> <SelectTrigger class="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 class="space-y-1.5"> <label class="text-foreground font-medium">Preferred Bay / Desk</label> <Select v-model="newSpotNumber"> <SelectTrigger class="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 class="grid grid-cols-1 gap-3 sm:grid-cols-2"> <div class="space-y-1.5"> <label class="text-foreground font-medium">Staging Aisle</label> <Input v-model="newStagingAisle" placeholder="e.g. Aisle 01" class="h-8 text-xs" /> </div> <div class="space-y-1.5"> <label class="text-foreground font-medium">Staging Bin</label> <Input v-model="newStagingBin" placeholder="e.g. Bin B-05" class="h-8 text-xs" /> </div> </div> <div class="space-y-1.5"> <label class="text-foreground font-medium">Items Summary</label> <Input v-model="newItemSummary" placeholder="e.g. Noise Cancelling Earbuds, Travel Case" class="h-8 text-xs" /> </div> <div class="space-y-1.5"> <label class="text-foreground font-medium">Order Total ($)</label> <Input v-model="newOrderTotal" placeholder="120.00" class="h-8 font-mono text-xs" /> </div> </div> <DialogFooter class="gap-2 sm:gap-0"> <DialogClose as-child> <Button variant="outline" size="sm" class="text-xs">Cancel</Button> </DialogClose> <Button size="sm" class="text-xs" :disabled="!newCustomerName" @click="handleCreateOrder"> <span>Stage & Assign Bin</span> </Button> </DialogFooter> </DialogContent> </Dialog> </div> </template>app/components/blocks/store-pickup-types.ts1.2 kB
import type { HTMLAttributes } from 'vue' 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' class?: HTMLAttributes['class'] }app/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/vue/store-pickup-curbside.json