udpates on the ui changesand api integration
This commit is contained in:
@@ -12,7 +12,6 @@ import {
|
||||
UserCheck,
|
||||
MapPin,
|
||||
TrendingUp,
|
||||
Plus,
|
||||
ChevronRight,
|
||||
Package,
|
||||
ArrowRight,
|
||||
@@ -28,9 +27,11 @@ 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;
|
||||
@@ -43,7 +44,6 @@ interface DeliveryExecutive {
|
||||
name: string;
|
||||
phone: string;
|
||||
status: 'Active Duty' | 'Idle' | 'Offline';
|
||||
rating: number;
|
||||
completedToday: number;
|
||||
currentZone: string;
|
||||
avatar: string;
|
||||
@@ -61,7 +61,6 @@ function riderRowToExecutive(row: Record<string, unknown>, idx: number): Deliver
|
||||
name: fstr(row.fullname) || `${fstr(row.firstname)} ${fstr(row.lastname)}`.trim() || 'Rider',
|
||||
phone: fstr(row.contactno) || '—',
|
||||
status: fstr(row.starttime) ? 'Active Duty' : 'Idle',
|
||||
rating: 4.7,
|
||||
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],
|
||||
@@ -141,76 +140,20 @@ export default function OrdersDeliveriesView({ searchQuery = '', isCoimbatoreVie
|
||||
const activeDispatchCount = storeOrders.filter(o => o.status === 'OUT_FOR_DELIVERY').length;
|
||||
const completedDeliveriesCount = storeOrders.filter(o => o.status === 'DELIVERED').length;
|
||||
|
||||
const MOCK_NAMES = ['Aravind Swamy', 'Karthik Raja', 'Priya Mani', 'Meera Jasmine', 'Sanjay Dutt', 'Divya Spandana', 'Vijay Sethupathi', 'Nayan Thara'];
|
||||
const MOCK_STREETS = ['Avarampalayam Rd', 'DB Road', 'Cross Cut Road', 'Avinashi Road', 'Trichy Road', 'NSR Road', 'Sathy Road', 'Marudhamalai Road'];
|
||||
const MOCK_ITEMS = [
|
||||
{ name: 'Tata Salt Premium Iodized 1kg', price: 28 },
|
||||
{ name: 'Gold Winner Sunflower Oil 1L', price: 145 },
|
||||
{ name: 'Britannia Marie Gold Biscuit 250g', price: 35 },
|
||||
{ name: 'MTR Sambar Powder 200g', price: 85 },
|
||||
{ name: 'Aavin Salted Butter 500g', price: 260 },
|
||||
{ name: 'Ponni Boiled Rice 5kg', price: 380 },
|
||||
{ name: 'Fresh Ooty Carrots 500g', price: 45 },
|
||||
{ name: 'Nescafe Classic Coffee 100g', price: 185 },
|
||||
];
|
||||
|
||||
const handleCreateMockOrder = () => {
|
||||
const randomName = MOCK_NAMES[Math.floor(Math.random() * MOCK_NAMES.length)];
|
||||
const randomStreet = MOCK_STREETS[Math.floor(Math.random() * MOCK_STREETS.length)];
|
||||
const numItems = Math.floor(Math.random() * 3) + 1; // 1 to 3 items
|
||||
const selectedItems = [];
|
||||
let amount = 0;
|
||||
for (let k = 0; k < numItems; k++) {
|
||||
const it = MOCK_ITEMS[Math.floor(Math.random() * MOCK_ITEMS.length)];
|
||||
const qty = Math.floor(Math.random() * 2) + 1;
|
||||
selectedItems.push({ name: it.name, quantity: qty, price: it.price });
|
||||
amount += it.price * qty;
|
||||
}
|
||||
const newId = `ORD-${Math.floor(100000 + Math.random() * 900000)}`;
|
||||
const newOrder: CustomerOrder = {
|
||||
id: newId,
|
||||
customerName: randomName,
|
||||
phone: `9${Math.floor(100000000 + Math.random() * 900000000)}`,
|
||||
address: `${Math.floor(10 + Math.random() * 190)}, ${randomStreet}, Coimbatore`,
|
||||
items: selectedItems,
|
||||
amount,
|
||||
time: new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }),
|
||||
status: 'PROCESSING',
|
||||
assignedRider: 'Pending Assignment',
|
||||
hub: locationid ? `Outlet Node #${locationid}` : 'Coimbatore Hub',
|
||||
locationid: locationid ?? 1097,
|
||||
// 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,
|
||||
};
|
||||
setOrders(prev => [newOrder, ...prev]);
|
||||
setSelectedOrder(newOrder);
|
||||
};
|
||||
|
||||
const handleUpdateStatus = (newStatus: CustomerOrder['status']) => {
|
||||
if (!selectedOrder) return;
|
||||
setOrders(prev => prev.map(o => {
|
||||
if (o.id === selectedOrder.id) {
|
||||
const updated = { ...o, status: newStatus };
|
||||
setSelectedOrder(updated);
|
||||
return updated;
|
||||
}
|
||||
return o;
|
||||
}));
|
||||
};
|
||||
|
||||
const handleAssignRider = (riderName: string) => {
|
||||
if (!selectedOrder) return;
|
||||
setOrders(prev => prev.map(o => {
|
||||
if (o.id === selectedOrder.id) {
|
||||
const updated = {
|
||||
...o,
|
||||
assignedRider: riderName,
|
||||
status: o.status === 'PROCESSING' ? 'CONFIRMED' : o.status
|
||||
};
|
||||
setSelectedOrder(updated);
|
||||
return updated;
|
||||
}
|
||||
return o;
|
||||
}));
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-lg animate-in fade-in duration-500">
|
||||
@@ -348,10 +291,10 @@ export default function OrdersDeliveriesView({ searchQuery = '', isCoimbatoreVie
|
||||
<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 space-y-md">
|
||||
<div className="bg-white border border-[#e2e8f0] rounded-xl shadow-sm overflow-hidden flex flex-col justify-between">
|
||||
<div>
|
||||
<div className="p-md border-b border-[#e2e8f0] bg-[#f8fafc] flex flex-col gap-md">
|
||||
<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]">
|
||||
@@ -359,14 +302,6 @@ export default function OrdersDeliveriesView({ searchQuery = '', isCoimbatoreVie
|
||||
</h4>
|
||||
<p className="text-[10px] text-zinc-400 font-medium mt-0.5">Interactive list of customer purchases made via client app</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleCreateMockOrder}
|
||||
className="bg-[#581c87] text-white px-3 py-1.5 rounded-lg text-[10px] font-bold uppercase tracking-wider flex items-center justify-center gap-1 cursor-pointer hover:bg-purple-800 transition shadow-sm"
|
||||
>
|
||||
<Plus size={12} />
|
||||
Create Simulated Order
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row items-center gap-sm w-full">
|
||||
@@ -401,11 +336,11 @@ export default function OrdersDeliveriesView({ searchQuery = '', isCoimbatoreVie
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Order item rows */}
|
||||
<div className="divide-y divide-[#f1f5f9] max-h-[480px] overflow-y-auto">
|
||||
{/* 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 place a mock delivery item.
|
||||
No orders matching status filter found. Try another query or adjust the date range.
|
||||
</div>
|
||||
) : (
|
||||
filteredOrdersList.map(order => (
|
||||
@@ -448,41 +383,6 @@ export default function OrdersDeliveriesView({ searchQuery = '', isCoimbatoreVie
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Delivery Executives Fleet Section */}
|
||||
<div className="bg-white border border-[#e2e8f0] p-md rounded-xl shadow-sm">
|
||||
<span className="text-[10px] font-sans font-bold text-zinc-400 uppercase tracking-widest block pb-xs mb-md border-b border-[#f1f5f9]">
|
||||
Coimbatore Delivery Executive Fleet status
|
||||
</span>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-sm">
|
||||
{executives.map((ex) => (
|
||||
<div key={ex.id} className="p-sm border border-[#e2e8f0]/80 rounded-xl bg-[#f8fafc]/40 flex justify-between items-center">
|
||||
<div className="flex items-center gap-sm">
|
||||
<img
|
||||
src={ex.avatar}
|
||||
alt={ex.name}
|
||||
referrerPolicy="no-referrer"
|
||||
className="w-10 h-10 rounded-full object-cover border border-zinc-200 shrink-0"
|
||||
/>
|
||||
<div>
|
||||
<p className="font-semibold text-zinc-800">{ex.name}</p>
|
||||
<p className="text-[10px] text-zinc-400 font-medium">Zone: <strong>{ex.currentZone}</strong> • Rated ★{ex.rating}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-right">
|
||||
<span className={`px-1.5 py-0.5 rounded text-[9px] font-bold uppercase inline-block ${
|
||||
ex.status === 'Active Duty' ? 'bg-sky-50 text-sky-600 border border-sky-100' : ex.status === 'Idle' ? 'bg-emerald-50 text-emerald-600 border border-emerald-100' : 'bg-zinc-100 text-zinc-400'
|
||||
}`}>
|
||||
{ex.status}
|
||||
</span>
|
||||
<p className="text-[10px] text-zinc-500 font-semibold mt-1">Completed: {ex.completedToday}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right column — Order Details, shown parallel to the orders feed */}
|
||||
@@ -513,19 +413,24 @@ export default function OrdersDeliveriesView({ searchQuery = '', isCoimbatoreVie
|
||||
<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">
|
||||
{selectedOrder.items.length === 0 && (
|
||||
{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>
|
||||
)}
|
||||
{selectedOrder.items.map((item, idx) => (
|
||||
{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.price * Number(item.quantity))}</span>
|
||||
<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]">
|
||||
@@ -535,167 +440,13 @@ export default function OrdersDeliveriesView({ searchQuery = '', isCoimbatoreVie
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Interactive Status advancement controls */}
|
||||
<div className="pt-xs space-y-sm">
|
||||
<span className="text-[9px] font-sans font-bold text-zinc-400 uppercase tracking-widest block pb-xs border-b border-[#f1f5f9]">OPERATIONAL CONTROL</span>
|
||||
{selectedOrder.status === 'PROCESSING' && (
|
||||
<button
|
||||
onClick={() => handleUpdateStatus('CONFIRMED')}
|
||||
className="w-full bg-amber-500 hover:bg-amber-600 text-white font-bold text-[11px] py-2 rounded-xl transition duration-150 shadow-sm flex items-center justify-center gap-1 cursor-pointer border-none"
|
||||
>
|
||||
<Check size={14} /> Pack & Bag Order
|
||||
</button>
|
||||
)}
|
||||
{selectedOrder.status === 'CONFIRMED' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (selectedOrder.assignedRider === 'Pending Assignment') {
|
||||
alert('Please assign a delivery partner from the fleet roster first.');
|
||||
return;
|
||||
}
|
||||
handleUpdateStatus('OUT_FOR_DELIVERY');
|
||||
}}
|
||||
className="w-full bg-purple-600 hover:bg-purple-700 text-white font-bold text-[11px] py-2 rounded-xl transition duration-150 shadow-sm flex items-center justify-center gap-1 cursor-pointer border-none"
|
||||
>
|
||||
<Truck size={14} /> Dispatch Rider
|
||||
</button>
|
||||
)}
|
||||
{selectedOrder.status === 'OUT_FOR_DELIVERY' && (
|
||||
<button
|
||||
onClick={() => handleUpdateStatus('DELIVERED')}
|
||||
className="w-full bg-emerald-600 hover:bg-emerald-700 text-white font-bold text-[11px] py-2 rounded-xl transition duration-150 shadow-sm flex items-center justify-center gap-1 cursor-pointer border-none"
|
||||
>
|
||||
<CheckCircle2 size={14} /> Verify Delivery Handover
|
||||
</button>
|
||||
)}
|
||||
{selectedOrder.status === 'DELIVERED' && (
|
||||
<div className="bg-emerald-50 border border-emerald-250 text-emerald-800 font-bold text-[10px] py-2.5 rounded-xl text-center flex items-center justify-center gap-1 select-none">
|
||||
<CheckCircle2 size={13} className="text-emerald-600" /> Order Completed Successfully
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Active Rider Assignment (only if not delivered) */}
|
||||
{selectedOrder.status !== 'DELIVERED' && (
|
||||
<div className="space-y-sm pt-xs">
|
||||
<div className="flex justify-between items-center border-b border-[#f1f5f9] pb-xs">
|
||||
<span className="text-[9px] font-sans font-bold text-zinc-400 uppercase tracking-widest">ASSIGN DELIVERY EXECUTIVE</span>
|
||||
<span className="text-[9px] text-[#581c87] font-bold">Fleet Roster</span>
|
||||
</div>
|
||||
<div className="space-y-1.5 max-h-[140px] overflow-y-auto pr-1">
|
||||
{executives.length === 0 ? (
|
||||
<p className="text-[10px] text-zinc-405">No riders currently available.</p>
|
||||
) : (
|
||||
executives.map(ex => {
|
||||
const isAssigned = selectedOrder.assignedRider === ex.name;
|
||||
return (
|
||||
<button
|
||||
key={ex.id}
|
||||
type="button"
|
||||
onClick={() => handleAssignRider(ex.name)}
|
||||
className={`w-full p-2 border rounded-xl flex items-center justify-between text-left transition-all cursor-pointer ${
|
||||
isAssigned
|
||||
? 'bg-purple-50 border-[#581c87] text-[#581c87] font-semibold'
|
||||
: 'bg-[#f8fafc]/50 hover:bg-zinc-55 border-[#e2e8f0] text-zinc-700'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<img src={ex.avatar} alt={ex.name} referrerPolicy="no-referrer" className="w-6 h-6 rounded-full object-cover border border-zinc-200" />
|
||||
<div>
|
||||
<p className="text-[10px] font-bold leading-tight">{ex.name}</p>
|
||||
<p className="text-[9px] text-zinc-450 leading-none">{ex.currentZone} • ★{ex.rating}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className={`text-[8px] font-bold uppercase px-1.5 py-0.5 rounded ${
|
||||
isAssigned
|
||||
? 'bg-[#581c87] text-white'
|
||||
: 'bg-zinc-200 text-zinc-650'
|
||||
}`}>
|
||||
{isAssigned ? 'Assigned' : 'Assign'}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Simulated GPS map tracking path */}
|
||||
{/* 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>
|
||||
<div className="relative overflow-hidden rounded-xl border border-zinc-200 bg-zinc-950 p-4 h-40 text-white flex flex-col justify-between font-sans shadow-inner select-none">
|
||||
{/* Grid background lines */}
|
||||
<div className="absolute inset-0 opacity-10 bg-[linear-gradient(to_right,#808080_1px,transparent_1px),linear-gradient(to_bottom,#808080_1px,transparent_1px)] bg-[size:12px_18px]" />
|
||||
|
||||
<svg className="absolute inset-0 w-full h-full" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="route-grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stopColor="#c084fc" />
|
||||
<stop offset="100%" stopColor="#818cf8" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
{/* Route path line */}
|
||||
<path
|
||||
d="M 30 110 C 60 70, 110 110, 160 40"
|
||||
fill="none"
|
||||
stroke="#1e293b"
|
||||
strokeWidth="4"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M 30 110 C 60 70, 110 110, 160 40"
|
||||
fill="none"
|
||||
stroke="url(#route-grad)"
|
||||
strokeWidth="4"
|
||||
strokeLinecap="round"
|
||||
strokeDasharray="200"
|
||||
strokeDashoffset="200"
|
||||
style={{
|
||||
animation: 'dash 6s linear infinite'
|
||||
}}
|
||||
/>
|
||||
{/* Hub Marker */}
|
||||
<circle cx="30" cy="110" r="5" fill="#c084fc" className="animate-pulse" />
|
||||
<circle cx="30" cy="110" r="3" fill="#a855f7" />
|
||||
{/* Destination Marker */}
|
||||
<circle cx="160" cy="40" r="5" fill="#f43f5e" className="animate-ping" />
|
||||
<circle cx="160" cy="40" r="3" fill="#e11d48" />
|
||||
</svg>
|
||||
|
||||
<style dangerouslySetInnerHTML={{__html: `
|
||||
@keyframes dash {
|
||||
to {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
`}} />
|
||||
|
||||
{/* Map overlays */}
|
||||
<div className="z-10 flex justify-between items-start">
|
||||
<div className="bg-zinc-900/90 backdrop-blur-md px-2 py-0.5 rounded border border-zinc-800 text-[8px] font-bold text-zinc-300">
|
||||
GPS ACTIVE: IN TRANSIT
|
||||
</div>
|
||||
<div className="bg-zinc-900/90 backdrop-blur-md px-2 py-0.5 rounded border border-zinc-800 text-[8px] font-bold text-[#c084fc] flex items-center gap-1">
|
||||
<span className="w-1 h-1 rounded-full bg-purple-500 animate-ping" />
|
||||
ETA 9 MINS
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="z-10 bg-zinc-900/95 backdrop-blur-md p-2 rounded-lg border border-zinc-800 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-[8px] text-zinc-400 font-bold uppercase tracking-wider">Executive</p>
|
||||
<p className="text-[10px] font-bold text-white leading-tight">{selectedOrder.assignedRider}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-[8px] text-zinc-400 font-bold uppercase tracking-wider">Distance</p>
|
||||
<p className="text-[10px] font-bold text-[#c084fc] font-mono leading-tight">1.2 km left</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AwaitingApi label="Live rider GPS & ETA" api="[R9]" compact />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user