505 lines
25 KiB
TypeScript
505 lines
25 KiB
TypeScript
/**
|
|
* @license
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
ShoppingBag,
|
|
Truck,
|
|
CheckCircle2,
|
|
Clock,
|
|
UserCheck,
|
|
MapPin,
|
|
TrendingUp,
|
|
ChevronRight,
|
|
Package,
|
|
ArrowRight,
|
|
AlertCircle,
|
|
Clock4,
|
|
Search,
|
|
Check,
|
|
Calendar,
|
|
X
|
|
} from 'lucide-react';
|
|
import { CustomerOrder } from '../types';
|
|
import {
|
|
useFiestaDeliveries,
|
|
useFiestaDeliverySummary,
|
|
useFiestaRiders,
|
|
useFiestaOrderDetails,
|
|
} from '../services/fiestaQueries';
|
|
import { FIESTA_TENANT_ID, num as fnum, str as fstr, ymd } from '../services/fiestaApi';
|
|
import { deliveryRowToOrder } from '../services/fiestaMappers';
|
|
import AwaitingApi from './AwaitingApi';
|
|
|
|
interface OrdersDeliveriesViewProps {
|
|
searchQuery?: string;
|
|
isCoimbatoreView?: boolean;
|
|
locationid?: number;
|
|
}
|
|
|
|
interface DeliveryExecutive {
|
|
id: string;
|
|
name: string;
|
|
phone: string;
|
|
status: 'Active Duty' | 'Idle' | 'Offline';
|
|
completedToday: number;
|
|
currentZone: string;
|
|
avatar: string;
|
|
}
|
|
|
|
const RIDER_AVATARS = [
|
|
'https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&w=150&q=80',
|
|
'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=150&q=80',
|
|
'https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=150&q=80',
|
|
];
|
|
|
|
function riderRowToExecutive(row: Record<string, unknown>, idx: number): DeliveryExecutive {
|
|
return {
|
|
id: `DE-${fstr(row.userid) || idx}`,
|
|
name: fstr(row.fullname) || `${fstr(row.firstname)} ${fstr(row.lastname)}`.trim() || 'Rider',
|
|
phone: fstr(row.contactno) || '—',
|
|
status: fstr(row.starttime) ? 'Active Duty' : 'Idle',
|
|
completedToday: fnum(row.completed) || fnum(row.deliverycount),
|
|
currentZone: fstr(row.city) || fstr(row.vehiclename) || fstr(row.vehicleno) || 'Coimbatore',
|
|
avatar: RIDER_AVATARS[idx % RIDER_AVATARS.length],
|
|
};
|
|
}
|
|
|
|
export default function OrdersDeliveriesView({ searchQuery = '', isCoimbatoreView = false, locationid }: OrdersDeliveriesViewProps) {
|
|
// ── Live deliveries / fleet (Fiesta) ──────────────────────────────────────
|
|
// Order feed + dispatch controls run off the live deliveries board; the KPI
|
|
// strip uses the delivery summary; the fleet panel uses the active riders.
|
|
// A date-range filter lets the user view orders/deliveries day-wise.
|
|
const today = new Date();
|
|
const monthStart = new Date(today.getFullYear(), today.getMonth(), 1);
|
|
const [fromdate, setFromdate] = useState<string>(ymd(monthStart));
|
|
const [todate, setTodate] = useState<string>(ymd(today));
|
|
|
|
// Quick-range presets (computed off the current day; no Date.now in render path).
|
|
const dayOffset = (n: number) => {
|
|
const d = new Date();
|
|
d.setDate(d.getDate() - n);
|
|
return ymd(d);
|
|
};
|
|
const presets: Array<{ key: string; label: string; from: string; to: string }> = [
|
|
{ key: 'today', label: 'Today', from: ymd(today), to: ymd(today) },
|
|
{ key: 'yesterday', label: 'Yesterday', from: dayOffset(1), to: dayOffset(1) },
|
|
{ key: '7d', label: 'Last 7 Days', from: dayOffset(6), to: ymd(today) },
|
|
{ key: '30d', label: 'Last 30 Days', from: dayOffset(29), to: ymd(today) },
|
|
{ key: 'month', label: 'This Month', from: ymd(monthStart), to: ymd(today) },
|
|
];
|
|
const activePreset = presets.find((p) => p.from === fromdate && p.to === todate)?.key ?? 'custom';
|
|
|
|
const deliveriesQ = useFiestaDeliveries({ tenantid: FIESTA_TENANT_ID, fromdate, todate });
|
|
const summaryQ = useFiestaDeliverySummary({ tenantid: FIESTA_TENANT_ID, fromdate, todate });
|
|
const ridersQ = useFiestaRiders({ tenantid: FIESTA_TENANT_ID });
|
|
|
|
const [orders, setOrders] = useState<CustomerOrder[]>([]);
|
|
const [executives, setExecutives] = useState<DeliveryExecutive[]>([]);
|
|
const [selectedOrder, setSelectedOrder] = useState<CustomerOrder | null>(null);
|
|
const [filterStatus, setFilterStatus] = useState<string>('ALL');
|
|
const [localSearch, setLocalSearch] = useState('');
|
|
|
|
// Seed local state once live data arrives so existing dispatch/create handlers
|
|
// continue to mutate in-session.
|
|
useEffect(() => {
|
|
if (deliveriesQ.data) {
|
|
const mapped = deliveriesQ.data.map(deliveryRowToOrder);
|
|
setOrders(mapped);
|
|
// Keep the current selection only if it's still in the new range; otherwise
|
|
// fall back to the first order so the detail panel stays in sync.
|
|
setSelectedOrder((prev) =>
|
|
(prev && mapped.some((o) => o.id === prev.id)) ? prev : mapped[0] ?? null,
|
|
);
|
|
}
|
|
}, [deliveriesQ.data]);
|
|
|
|
useEffect(() => {
|
|
if (ridersQ.data) setExecutives(ridersQ.data.map(riderRowToExecutive));
|
|
}, [ridersQ.data]);
|
|
|
|
const summary = summaryQ.data;
|
|
|
|
// Local filtered list of orders
|
|
const storeOrders = locationid ? orders.filter(o => o.locationid === locationid) : orders;
|
|
|
|
const filteredOrdersList = storeOrders.filter(o => {
|
|
const term = (localSearch || searchQuery).toLowerCase();
|
|
const matchesSearch = o.id.toLowerCase().includes(term) ||
|
|
o.customerName.toLowerCase().includes(term) ||
|
|
o.address.toLowerCase().includes(term);
|
|
const matchesFilter = filterStatus === 'ALL' || o.status === filterStatus;
|
|
return matchesSearch && matchesFilter;
|
|
});
|
|
|
|
// Calculate dynamic stats for metrics cards based on filtered storeOrders
|
|
const totalDeliveriesCount = storeOrders.length;
|
|
const pendingFulfillmentCount = storeOrders.filter(o => o.status === 'PROCESSING' || o.status === 'CONFIRMED').length;
|
|
const activeDispatchCount = storeOrders.filter(o => o.status === 'OUT_FOR_DELIVERY').length;
|
|
const completedDeliveriesCount = storeOrders.filter(o => o.status === 'DELIVERED').length;
|
|
|
|
// Live line-item details for the currently selected order. The deliveries board
|
|
// only carries an itemCount; the actual basket lines come from this endpoint.
|
|
const orderDetailsQ = useFiestaOrderDetails(selectedOrder?.id ?? null);
|
|
const orderItems = (orderDetailsQ.data ?? []).map((row) => {
|
|
const quantity = fnum(row.quantity) || fnum(row.qty);
|
|
const price = fnum(row.price) || fnum(row.unitprice);
|
|
const lineTotal = fnum(row.amount) || price * quantity;
|
|
return {
|
|
name: fstr(row.productname) || fstr(row.itemname) || 'Item',
|
|
quantity,
|
|
price,
|
|
lineTotal,
|
|
};
|
|
});
|
|
|
|
return (
|
|
<div className="space-y-lg animate-in fade-in duration-500">
|
|
|
|
{/* View Header with Statistics Overview */}
|
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-md border-b border-[#e2e8f0] pb-xl">
|
|
<div>
|
|
<h1 className="font-sans font-bold text-2xl tracking-tight text-[#0f172a]">
|
|
Orders & Delivery Operations
|
|
</h1>
|
|
<p className="text-zinc-500 font-sans text-xs mt-1">
|
|
Real-time tracking of app orders, dispatch queues, and active delivery partners across Coimbatore regional sub-hubs.
|
|
</p>
|
|
<div className="mt-1.5">
|
|
{deliveriesQ.isLoading ? (
|
|
<span className="inline-flex items-center gap-1 text-[10px] font-semibold text-zinc-400 uppercase tracking-wide">
|
|
<span className="w-1.5 h-1.5 rounded-full bg-zinc-300 animate-pulse" /> Loading live deliveries…
|
|
</span>
|
|
) : deliveriesQ.isError ? (
|
|
<span className="inline-flex items-center gap-1 text-[10px] font-semibold text-rose-600 uppercase tracking-wide">
|
|
<span className="w-1.5 h-1.5 rounded-full bg-rose-500" /> Live data unavailable
|
|
</span>
|
|
) : (
|
|
<span className="inline-flex items-center gap-1 text-[10px] font-semibold text-emerald-600 uppercase tracking-wide">
|
|
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500" /> Live · {orders.length} deliveries · {executives.length} riders
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Top Level Delivery Performance Indicators */}
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-gutter font-sans">
|
|
|
|
<div className="bg-white border border-[#e2e8f0] p-md rounded-xl flex items-center gap-md shadow-sm">
|
|
<div className="p-2 bg-purple-50 text-[#581c87] rounded-lg">
|
|
<ShoppingBag size={20} />
|
|
</div>
|
|
<div>
|
|
<p className="text-[10px] text-zinc-400 uppercase tracking-wider font-bold">Deliveries in Range</p>
|
|
<p className="font-sans font-bold text-lg text-zinc-800">{totalDeliveriesCount.toLocaleString('en-IN')} total</p>
|
|
<p className="text-[10px] text-emerald-600 font-semibold mt-0.5">{fromdate === todate ? fromdate : `${fromdate} → ${todate}`}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white border border-[#e2e8f0] p-md rounded-xl flex items-center gap-md shadow-sm">
|
|
<div className="p-2 bg-amber-50 text-amber-600 rounded-lg">
|
|
<Clock size={20} />
|
|
</div>
|
|
<div>
|
|
<p className="text-[10px] text-zinc-400 uppercase tracking-wider font-bold">Pending Fulfilment</p>
|
|
<p className="font-sans font-bold text-lg text-zinc-800">
|
|
{pendingFulfillmentCount + activeDispatchCount} active
|
|
</p>
|
|
<p className="text-[10px] text-amber-600 font-semibold mt-0.5">Awaiting dispatch / in transit</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white border border-[#e2e8f0] p-md rounded-xl flex items-center gap-md shadow-sm">
|
|
<div className="p-2 bg-emerald-50 text-emerald-600 rounded-lg">
|
|
<Truck size={20} />
|
|
</div>
|
|
<div>
|
|
<p className="text-[10px] text-zinc-400 uppercase tracking-wider font-bold">Successful Deliveries</p>
|
|
<p className="font-sans font-bold text-lg text-zinc-800">
|
|
{completedDeliveriesCount} done
|
|
</p>
|
|
<p className="text-[10px] text-[#581c87] font-semibold mt-0.5">{locationid ? 'At this location' : 'Across all locations'}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white border border-[#e2e8f0] p-md rounded-xl flex items-center gap-md shadow-sm">
|
|
<div className="p-2 bg-purple-50 text-purple-600 rounded-lg">
|
|
<UserCheck size={20} />
|
|
</div>
|
|
<div>
|
|
<p className="text-[10px] text-zinc-400 uppercase tracking-wider font-bold">Active Delivery Fleet</p>
|
|
<p className="font-sans font-bold text-lg text-zinc-800">
|
|
{executives.filter(e => e.status !== 'Offline').length} partners
|
|
</p>
|
|
<p className="text-[10px] text-purple-600 font-semibold mt-0.5">{executives.length} riders registered</p>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
{/* Day-wise date filter — drives the live deliveries + summary queries */}
|
|
<div className="bg-white border border-[#e2e8f0] rounded-xl p-md shadow-sm flex flex-col lg:flex-row lg:items-center justify-between gap-md">
|
|
<div className="flex items-center gap-sm flex-wrap">
|
|
<span className="flex items-center gap-1.5 text-[10px] font-bold text-zinc-400 uppercase tracking-widest pr-1">
|
|
<Calendar size={13} className="text-[#581c87]" /> View
|
|
</span>
|
|
{presets.map((p) => (
|
|
<button
|
|
key={p.key}
|
|
onClick={() => { setFromdate(p.from); setTodate(p.to); }}
|
|
className={`px-3 py-1.5 rounded-lg text-[11px] font-bold transition-all border cursor-pointer ${
|
|
activePreset === p.key
|
|
? 'bg-[#581c87] text-white border-[#581c87] shadow-sm'
|
|
: 'bg-white text-zinc-600 border-[#e2e8f0] hover:bg-zinc-50'
|
|
}`}
|
|
>
|
|
{p.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<div className="flex items-center gap-sm text-xs">
|
|
<div className="flex items-center gap-1.5">
|
|
<label className="text-[10px] font-bold text-zinc-400 uppercase tracking-wider">From</label>
|
|
<input
|
|
type="date"
|
|
value={fromdate}
|
|
max={todate}
|
|
onChange={(e) => setFromdate(e.target.value)}
|
|
className="border border-[#e2e8f0] rounded-lg px-2 py-1.5 text-zinc-700 font-medium outline-none focus:ring-1 focus:ring-[#581c87] cursor-pointer"
|
|
/>
|
|
</div>
|
|
<span className="text-zinc-300">→</span>
|
|
<div className="flex items-center gap-1.5">
|
|
<label className="text-[10px] font-bold text-zinc-400 uppercase tracking-wider">To</label>
|
|
<input
|
|
type="date"
|
|
value={todate}
|
|
min={fromdate}
|
|
max={ymd(today)}
|
|
onChange={(e) => setTodate(e.target.value)}
|
|
className="border border-[#e2e8f0] rounded-lg px-2 py-1.5 text-zinc-700 font-medium outline-none focus:ring-1 focus:ring-[#581c87] cursor-pointer"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Main interactive segment splits */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-gutter text-xs font-sans">
|
|
|
|
{/* Left List of Customer App Orders */}
|
|
<div className="lg:col-span-2 flex">
|
|
<div className="bg-white border border-[#e2e8f0] rounded-xl shadow-sm overflow-hidden flex flex-col h-full w-full min-h-[32rem]">
|
|
<div className="flex flex-col flex-1 min-h-0">
|
|
<div className="p-md border-b border-[#e2e8f0] bg-[#f8fafc] flex flex-col gap-md shrink-0">
|
|
<div className="flex flex-col sm:flex-row justify-between sm:items-center gap-sm">
|
|
<div>
|
|
<h4 className="font-sans font-bold text-sm text-[#0f172a]">
|
|
Customer Orders Feed ({filteredOrdersList.length})
|
|
</h4>
|
|
<p className="text-[10px] text-zinc-400 font-medium mt-0.5">Interactive list of customer purchases made via client app</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col sm:flex-row items-center gap-sm w-full">
|
|
{/* Local Search Input */}
|
|
<div className="relative w-full sm:max-w-xs">
|
|
<Search size={13} className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-400" />
|
|
<input
|
|
type="text"
|
|
placeholder="Search orders by customer, street, ID..."
|
|
value={localSearch}
|
|
onChange={(e) => setLocalSearch(e.target.value)}
|
|
className="w-full pl-8 pr-4 py-1.5 border border-[#e2e8f0] rounded-lg text-[11px] outline-none bg-white focus:ring-1 focus:ring-[#581c87] transition-all"
|
|
/>
|
|
</div>
|
|
|
|
{/* Filter Status buttons */}
|
|
<div className="flex gap-1 overflow-x-auto w-full sm:w-auto">
|
|
{['ALL', 'PROCESSING', 'CONFIRMED', 'OUT_FOR_DELIVERY', 'DELIVERED'].map((st) => (
|
|
<button
|
|
key={st}
|
|
onClick={() => setFilterStatus(st)}
|
|
className={`px-2 py-1.5 rounded text-[9px] font-bold uppercase transition-all border outline-none cursor-pointer whitespace-nowrap ${
|
|
filterStatus === st
|
|
? 'bg-[#581c87] text-white border-[#581c87] shadow-sm'
|
|
: 'bg-white text-zinc-500 border-[#e2e8f0] hover:bg-zinc-50'
|
|
}`}
|
|
>
|
|
{st.replace(/_/g, ' ')}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Order item rows — flex-fills the column so the feed matches the Order Details card height */}
|
|
<div className="divide-y divide-[#f1f5f9] flex-1 min-h-0 overflow-y-auto">
|
|
{filteredOrdersList.length === 0 ? (
|
|
<div className="p-xl text-center text-zinc-400 font-medium">
|
|
No orders matching status filter found. Try another query or adjust the date range.
|
|
</div>
|
|
) : (
|
|
filteredOrdersList.map(order => (
|
|
<div
|
|
key={order.id}
|
|
onClick={() => setSelectedOrder(order)}
|
|
className={`p-md flex items-center justify-between hover:bg-zinc-50 border-l-4 transition-all cursor-pointer ${
|
|
selectedOrder?.id === order.id ? 'bg-[#faf5ff]/50 border-[#581c87]' : 'border-transparent'
|
|
}`}
|
|
>
|
|
<div className="space-y-1">
|
|
<div className="flex items-center gap-sm">
|
|
<span className="font-bold text-zinc-700">{order.customerName}</span>
|
|
<span className="text-[10px] text-zinc-400">• {order.time}</span>
|
|
</div>
|
|
<p className="text-zinc-500 truncate max-w-[24rem]">{order.address}</p>
|
|
<div className="flex gap-sm py-1 items-center">
|
|
<span className="bg-[#f1f5f9] px-1.5 py-0.5 rounded text-[9px] font-bold text-zinc-500 uppercase">{order.hub}</span>
|
|
<span className="text-[9px] text-[#581c87] font-bold">{order.itemCount ?? order.items.length} Items</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="text-right space-y-1">
|
|
<p className="font-bold font-mono text-sm text-[#0f172a]">₹{order.amount.toLocaleString()}</p>
|
|
<span className={`px-2 py-0.5 rounded text-[9px] font-bold tracking-wider inline-block uppercase ${
|
|
order.status === 'DELIVERED'
|
|
? 'bg-emerald-50 text-emerald-600 border border-emerald-100'
|
|
: order.status === 'OUT_FOR_DELIVERY'
|
|
? 'bg-purple-50 text-purple-700 border border-purple-100'
|
|
: order.status === 'CONFIRMED'
|
|
? 'bg-amber-50 text-amber-600 border border-amber-100 animate-pulse'
|
|
: 'bg-zinc-100 text-zinc-650 border border-zinc-200'
|
|
}`}>
|
|
{order.status.replace(/_/g, ' ')}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right column — Order Details, shown parallel to the orders feed */}
|
|
<div className="lg:col-span-1 space-y-md">
|
|
{selectedOrder ? (
|
|
<div className="bg-white border border-[#e2e8f0] rounded-xl p-md shadow-sm space-y-md animate-in zoom-in-95 duration-150">
|
|
<span className="text-[10px] font-sans font-bold text-zinc-400 uppercase tracking-widest block pb-xs border-b border-[#f1f5f9]">
|
|
Order Details: {selectedOrder.id}
|
|
</span>
|
|
|
|
{/* Customer summary */}
|
|
<div className="p-sm bg-[#f8fafc] rounded-lg border border-[#e2e8f0]/50 space-y-xs">
|
|
<div className="flex justify-between font-semibold">
|
|
<span>Customer Name</span>
|
|
<span className="text-zinc-700">{selectedOrder.customerName}</span>
|
|
</div>
|
|
<div className="flex justify-between font-semibold">
|
|
<span>Contact info</span>
|
|
<span className="text-zinc-600 font-mono">{selectedOrder.phone}</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-[10px] text-zinc-400 font-bold uppercase block mt-1">Delivery Address</span>
|
|
<p className="text-zinc-700 mt-0.5 leading-relaxed font-medium">{selectedOrder.address}</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Category items description list */}
|
|
<div>
|
|
<span className="text-[10px] text-zinc-400 font-bold uppercase tracking-wide block mb-sm">Ordered Grocery basket Items:</span>
|
|
<div className="divide-y divide-[#f1f5f9] bg-zinc-50/50 p-2.5 rounded-lg border border-[#e2e8f0]/40">
|
|
{orderDetailsQ.isLoading && (
|
|
<div className="py-2 flex items-center gap-1.5 text-[10px] text-zinc-400 font-medium">
|
|
<span className="w-1.5 h-1.5 rounded-full bg-zinc-300 animate-pulse" /> Loading order line items…
|
|
</div>
|
|
)}
|
|
{!orderDetailsQ.isLoading && orderItems.length === 0 && (
|
|
<div className="py-2 flex justify-between items-center text-xs text-zinc-500">
|
|
<span className="font-medium">{selectedOrder.itemCount ?? 0} line item(s)</span>
|
|
<span className="text-[10px] text-zinc-400">Detail lines not loaded on board view</span>
|
|
</div>
|
|
)}
|
|
{orderItems.map((item, idx) => (
|
|
<div key={idx} className="py-2 flex justify-between items-center text-xs">
|
|
<div>
|
|
<p className="font-bold text-[#0f172a]">{item.name}</p>
|
|
<p className="text-[10px] text-zinc-400">Qty: {item.quantity} x ₹{item.price}</p>
|
|
</div>
|
|
<span className="font-bold font-mono text-zinc-700">₹{item.lineTotal}</span>
|
|
</div>
|
|
))}
|
|
<div className="pt-2 flex justify-between items-center font-bold text-sm text-[#581c87] border-t border-dashed border-[#e2e8f0]">
|
|
<span>Grand Total Invoice</span>
|
|
<span className="font-mono">₹{selectedOrder.amount.toLocaleString()}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Live GPS route tracker — no rider-telemetry/GPS API yet */}
|
|
{selectedOrder.status === 'OUT_FOR_DELIVERY' && (
|
|
<div className="space-y-xs pt-xs">
|
|
<span className="text-[9px] font-sans font-bold text-zinc-400 uppercase tracking-widest block">
|
|
LIVE GPS ROUTE TRACKER
|
|
</span>
|
|
<AwaitingApi label="Live rider GPS & ETA" api="[R9]" compact />
|
|
</div>
|
|
)}
|
|
|
|
{/* Delivery tracking visual roadmap layout */}
|
|
<div className="bg-zinc-50 border border-[#e2e8f0]/60 rounded-xl p-md">
|
|
<span className="text-[9px] font-sans font-bold text-zinc-400 uppercase tracking-widest block pb-xs mb-sm border-b border-[#f1f5f9]">
|
|
Live Dispatch Timeline Tracker
|
|
</span>
|
|
|
|
<div className="space-y-xs pt-1 relative text-[11px]">
|
|
<div className="flex gap-md items-start relative group">
|
|
<span className="text-emerald-500 mt-0.5"><CheckCircle2 size={12} /></span>
|
|
<div>
|
|
<h5 className="font-semibold text-zinc-800">Order Received ({selectedOrder.time})</h5>
|
|
<p className="text-[10px] text-zinc-400">Placed via customer app cart checkout successfully.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex gap-md items-start pt-3">
|
|
<span className={['CONFIRMED', 'OUT_FOR_DELIVERY', 'DELIVERED'].includes(selectedOrder.status) ? 'text-emerald-500 mt-0.5' : 'text-zinc-300 mt-0.5'}><CheckCircle2 size={12} /></span>
|
|
<div>
|
|
<h5 className="font-semibold text-zinc-800">Assortment Packaged & Bagged</h5>
|
|
<p className="text-[10px] text-zinc-400">Verified fresh produce items in-stock levels.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex gap-md items-start pt-3">
|
|
<span className={['OUT_FOR_DELIVERY', 'DELIVERED'].includes(selectedOrder.status) ? 'text-emerald-500 mt-0.5' : 'text-zinc-300 mt-0.5'}><CheckCircle2 size={12} /></span>
|
|
<div>
|
|
<h5 className="font-semibold text-zinc-800">Out for Delivery</h5>
|
|
<p className="text-[10px] text-zinc-400">Dispatched with executive partner on bike route.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex gap-md items-start pt-3">
|
|
<span className={selectedOrder.status === 'DELIVERED' ? 'text-emerald-500 mt-0.5' : 'text-zinc-300 mt-0.5'}><CheckCircle2 size={12} /></span>
|
|
<div>
|
|
<h5 className="font-semibold text-zinc-800">Handover Verified</h5>
|
|
<p className="text-[10px] text-zinc-400">Delivered directly to door step location.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="p-xl bg-white border border-[#e2e8f0] rounded-xl text-center text-zinc-400 font-medium">
|
|
Select any customer order from the feed to view its details.
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|