UIPackage
Menu

Framework

Change language

Boilerplate repo

Store Pickup Curbside

blockcommerce

Buy 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.

Also available for React ->

Installation

$npx shadcn-vue@latest add https://uipkge.dev/r/vue/store-pickup-curbside.json
Named registry:npx shadcn-vue@latest add @uipkge/store-pickup-curbsideInstalls to:app/components/blocks/

Variants

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
storeNamestring'Pasadena Flagship Store #02'optional
storeCodestring'STORE-CA-02'optional
initialModeFilter
'all''curbside''in_store'
'all'optional
classHTMLAttributes['class']optional

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

OrderItem
interface OrderItem {
  id: string
  name: string
  sku: string
  quantity: number
  price: number
  staged: boolean
  category: string
}
PickupOrder
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
}

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 &lt; 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
  • app/components/blocks/StorePickupModals.vue14.9 kB
  • app/components/blocks/store-pickup-types.ts1.2 kB
  • app/components/blocks/store-pickup-data.ts5.6 kB

Raw manifest:https://uipkge.dev/r/vue/store-pickup-curbside.json