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 Vue ->

Installation

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

Variants

Loading interactive previews…

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)

  • 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 &lt; 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 StorePickupCurbside
    
  • components/blocks/OrderDetailDialog.tsx7.3 kB
  • components/blocks/StorePickupModals.tsx17.8 kB
  • components/blocks/store-pickup-types.ts1.1 kB
  • components/blocks/store-pickup-data.ts5.6 kB

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