+ if (activeStore) {
+ return (
+
+ {isSoleStore && (
+
- Stores Registry
+ Store Console
- Local nodes registry, active manager assignments, and live dispatch and grocery delivery fulfillment statistics.
+ This merchant operates a single store. Add a branch to manage multiple outlets.
+ )}
+
setSelectedStore(null) : undefined}
+ tenantId={tenantId}
+ />
+
+ );
+ }
- {/* Filter control bar */}
-
- {/* Search input with search icon */}
-
-
-
-
- setStoresSearch(e.target.value)}
- className="w-full pl-10 pr-4 py-2 bg-zinc-50 border border-zinc-200 rounded-lg text-xs font-medium text-zinc-800 placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-[#662582]/20 focus:border-[#662582] transition-all"
- />
- {storesSearch && (
- setStoresSearch('')}
- className="absolute inset-y-0 right-0 pr-3 flex items-center text-zinc-400 hover:text-zinc-600"
- >
-
-
- )}
-
+ return (
+
- {/* Filter tabs */}
-
- setStoresFilter('ALL')}
- className={`flex-1 md:flex-none px-4 py-1.5 rounded-md text-xs font-bold transition-all ${
- storesFilter === 'ALL'
- ? 'bg-white text-[#662582] shadow-sm'
- : 'text-zinc-500 hover:text-zinc-800 hover:bg-white/40'
- }`}
- >
- All Nodes ({storesList.length})
-
- setStoresFilter('ACTIVE')}
- className={`flex-1 md:flex-none px-4 py-1.5 rounded-md text-xs font-bold transition-all ${
- storesFilter === 'ACTIVE'
- ? 'bg-white text-emerald-600 shadow-sm'
- : 'text-zinc-500 hover:text-emerald-600 hover:bg-white/40'
- }`}
- >
- Active ({storesList.filter(s => s.status.toLowerCase() === 'active').length})
-
- setStoresFilter('INACTIVE')}
- className={`flex-1 md:flex-none px-4 py-1.5 rounded-md text-xs font-bold transition-all ${
- storesFilter === 'INACTIVE'
- ? 'bg-white text-rose-600 shadow-sm'
- : 'text-zinc-500 hover:text-rose-650 hover:bg-white/40'
- }`}
- >
- Inactive ({storesList.filter(s => s.status.toLowerCase() !== 'active').length})
-
-
+
+ {/* ── Search & Filter Toolbar ── */}
+
+ {/* Search Input */}
+
+
+
-
- {/* Empty States */}
- {filteredStoresList.length === 0 && (
-
- {locationsQ.isLoading ? (
-
-
-
Loading live store locations…
-
- ) : (
-
No store locations found matching your filter criteria.
- )}
-
- )}
-
- {/* Immersive Background Blur Blobs */}
-
-
-
-
- {/* Store Cards Grid */}
-
- {filteredStoresList.map((st, i) => {
- const totalOrders = st.orders ?? st.deliveries ?? 0;
- const fulfillmentRate = totalOrders > 0 ? Math.min(100, Math.round((st.deliveries / totalOrders) * 100)) : 100;
-
- return (
-
setSelectedStore(st)}
- className={`group relative overflow-hidden bg-white/70 backdrop-blur-md border border-zinc-200/80 rounded-2xl shadow-sm hover:shadow-[0_20px_40px_rgba(88,28,135,0.12)] transition-all duration-500 cursor-pointer flex flex-col ${
- st.status.toLowerCase() === 'active'
- ? st.deliveries > 40
- ? 'hover:border-rose-300'
- : 'hover:border-emerald-300'
- : 'hover:border-amber-300'
- }`}
- >
- {/* Card Cover Image with Zoom effect */}
-
-
-
-
- {/* Status Badge */}
-
- 40
- ? 'text-rose-200 bg-rose-950/60 border-rose-500/30'
- : 'text-emerald-200 bg-emerald-950/60 border-emerald-500/30'
- : 'text-amber-200 bg-amber-950/60 border-amber-500/30'
- }`}>
-
- {st.status.toLowerCase() === 'active' && st.deliveries > 40 ? 'High Load' : st.status}
-
-
-
- {/* Zone & Title */}
-
-
{st.zone}
-
{st.name}
-
-
-
- {/* Card Content Area */}
-
- {/* Metrics Row & Progress Circle */}
-
-
-
-
Deliveries
-
{st.deliveries.toLocaleString()}
-
Dispatched Today
-
-
-
Total Orders
-
{totalOrders.toLocaleString()}
-
Incoming Volume
-
-
-
- {/* Circular Progress Ring */}
-
-
-
- 40
- ? 'stroke-rose-500'
- : 'stroke-emerald-500'
- : 'stroke-amber-500'
- }`}
- strokeWidth="3.5"
- fill="transparent"
- strokeDasharray="113"
- strokeDashoffset={113 - (113 * fulfillmentRate) / 100}
- strokeLinecap="round"
- />
-
-
- {fulfillmentRate}%
-
-
-
-
- {/* Live Sparkline Trend Histogram */}
-
-
-
Speed Index (Live Feed)
-
- Live
-
-
-
- {[30, 48, 25, 62, 54, 75, 42, 80, Math.min(95, Math.max(15, st.deliveries * 1.8))].map((val, idx) => (
-
-
40
- ? 'bg-rose-500/80 group-hover/bar:bg-rose-500'
- : 'bg-[#662582]/70 group-hover/bar:bg-[#662582]'
- : 'bg-amber-500/70 group-hover/bar:bg-amber-500'
- }`}
- />
-
- ))}
-
-
-
- {/* Lead Manager Profile block */}
-
-
-
- {st.staff.slice(0, 2).toUpperCase()}
-
-
- Node Lead
- {st.staff}
-
-
-
-
{
- e.stopPropagation();
- alert(`Routing communications channel directly to manager ${st.staff}...`);
- }}
- className="p-1.5 rounded-lg bg-white border border-zinc-200 hover:border-[#662582] hover:text-[#662582] text-zinc-500 transition-colors shadow-sm"
- title="Communicate with Node Lead"
- >
-
-
-
-
- {/* Card footer - enter console */}
-
-
{
- e.stopPropagation();
- const newStatus = st.status.toLowerCase() === 'active' ? 'Inactive' : 'Active';
- setStoresList(stores => stores.map(s => s.locationid === st.locationid ? { ...s, status: newStatus } : s));
- if (st.locationid) {
- updateLocation.mutate({ locationid: st.locationid, status: newStatus.toLowerCase() });
- }
- }}
- className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors focus:outline-none ${st.status.toLowerCase() === 'active' ? 'bg-emerald-500' : 'bg-zinc-300'}`}
- title={`Toggle store to ${st.status.toLowerCase() === 'active' ? 'Inactive' : 'Active'}`}
- >
-
-
-
-
-
Enter Terminal Console
-
-
-
-
-
- );
- })}
-
+
setStoresSearch(e.target.value)}
+ className="w-full pl-11 pr-12 py-3 bg-transparent border-none text-sm font-semibold text-slate-800 placeholder-slate-400 focus:outline-none focus:ring-0 transition-all duration-300"
+ />
+
+ {storesSearch && (
+ setStoresSearch('')}
+ className="p-1.5 rounded-lg text-slate-400 hover:text-slate-600 hover:bg-slate-100 transition-colors"
+ >
+
+
+ )}
- );
- }
+ {/* Vertical Divider (Hidden on Mobile) */}
+
+
+ {/* Filter Segmented Control */}
+
+ setStoresFilter('ALL')}
+ className={`flex-1 lg:flex-none px-5 py-2 rounded-lg text-xs font-bold transition-all duration-300 flex items-center justify-center gap-2 ${
+ storesFilter === 'ALL'
+ ? 'bg-white text-slate-800 shadow-[0_1px_3px_rgba(0,0,0,0.1)] ring-1 ring-slate-900/5'
+ : 'text-slate-500 hover:text-slate-700 hover:bg-white/50'
+ }`}
+ >
+ All Nodes
+
+ {storesList.length}
+
+
+ setStoresFilter('ACTIVE')}
+ className={`flex-1 lg:flex-none px-5 py-2 rounded-lg text-xs font-bold transition-all duration-300 flex items-center justify-center gap-2 ${
+ storesFilter === 'ACTIVE'
+ ? 'bg-white text-emerald-600 shadow-[0_1px_3px_rgba(0,0,0,0.1)] ring-1 ring-emerald-500/20'
+ : 'text-slate-500 hover:text-emerald-600 hover:bg-emerald-50/50'
+ }`}
+ >
+ Active
+
+ {storesList.filter(s => s.status.toLowerCase() === 'active').length}
+
+
+ setStoresFilter('INACTIVE')}
+ className={`flex-1 lg:flex-none px-5 py-2 rounded-lg text-xs font-bold transition-all duration-300 flex items-center justify-center gap-2 ${
+ storesFilter === 'INACTIVE'
+ ? 'bg-white text-slate-700 shadow-[0_1px_3px_rgba(0,0,0,0.1)] ring-1 ring-slate-900/5'
+ : 'text-slate-500 hover:text-slate-700 hover:bg-slate-200/50'
+ }`}
+ >
+ Inactive
+
+ {storesList.filter(s => s.status.toLowerCase() !== 'active').length}
+
+
+
+
+
+ {/* Empty States */}
+ {filteredStoresList.length === 0 && (
+
+ {locationsQ.isLoading ? (
+
+
+
Loading live store locations…
+
+ ) : (
+
No store locations found matching your filter criteria.
+ )}
+
+ )}
+
+ {/* Immersive Background Blur Blobs */}
+
+
+
+
+ {/* Store Cards Grid */}
+
+ {filteredStoresList.map((st, i) => {
+ const totalOrders = st.orders ?? st.deliveries ?? 0;
+ const fulfillmentRate = totalOrders > 0 ? Math.min(100, Math.round((st.deliveries / totalOrders) * 100)) : 100;
+
+ return (
+
setSelectedStore(st)}
+ className={`group relative overflow-hidden bg-white/70 backdrop-blur-md border border-zinc-200/80 rounded-2xl shadow-sm hover:shadow-[0_20px_40px_rgba(88,28,135,0.12)] transition-all duration-500 cursor-pointer flex flex-col ${
+ st.status.toLowerCase() === 'active'
+ ? st.deliveries > 40
+ ? 'hover:border-rose-300'
+ : 'hover:border-emerald-300'
+ : 'hover:border-amber-300'
+ }`}
+ >
+ {/* Card Cover Image with Zoom effect */}
+
+
+
+
+ {/* Status Badge */}
+
+ 40
+ ? 'text-rose-200 bg-rose-950/60 border-rose-500/30'
+ : 'text-emerald-200 bg-emerald-950/60 border-emerald-500/30'
+ : 'text-amber-200 bg-amber-950/60 border-amber-500/30'
+ }`}>
+
+ {st.status.toLowerCase() === 'active' && st.deliveries > 40 ? 'High Load' : st.status}
+
+
+
+ {/* Zone & Title */}
+
+
{st.zone}
+
{st.name}
+
+
+
+ {/* Card Content Area */}
+
+ {/* Metrics Row & Progress Circle */}
+
+
+
+
Deliveries
+
{st.deliveries.toLocaleString()}
+
Dispatched Today
+
+
+
Total Orders
+
{totalOrders.toLocaleString()}
+
Incoming Volume
+
+
+
+ {/* Circular Progress Ring */}
+
+
+
+ 40
+ ? 'stroke-rose-500'
+ : 'stroke-emerald-500'
+ : 'stroke-amber-500'
+ }`}
+ strokeWidth="3.5"
+ fill="transparent"
+ strokeDasharray="113"
+ strokeDashoffset={113 - (113 * fulfillmentRate) / 100}
+ strokeLinecap="round"
+ />
+
+
+ {fulfillmentRate}%
+
+
+
+
+ {/* Live Sparkline Trend Histogram */}
+
+
+
Speed Index (Live Feed)
+
+ Live
+
+
+
+ {[30, 48, 25, 62, 54, 75, 42, 80, Math.min(95, Math.max(15, st.deliveries * 1.8))].map((val, idx) => (
+
+
40
+ ? 'bg-rose-500/80 group-hover/bar:bg-rose-500'
+ : 'bg-[#662582]/70 group-hover/bar:bg-[#662582]'
+ : 'bg-amber-500/70 group-hover/bar:bg-amber-500'
+ }`}
+ />
+
+ ))}
+
+
+
+ {/* Lead Manager Profile block */}
+
+
+
+ {st.staff.slice(0, 2).toUpperCase()}
+
+
+ Node Lead
+ {st.staff}
+
+
+
+
{
+ e.stopPropagation();
+ alert(`Routing communications channel directly to manager ${st.staff}...`);
+ }}
+ className="p-1.5 rounded-lg bg-white border border-zinc-200 hover:border-[#662582] hover:text-[#662582] text-zinc-500 transition-colors shadow-sm"
+ title="Communicate with Node Lead"
+ >
+
+
+
+
+ {/* Card footer - enter console */}
+
+
{
+ e.stopPropagation();
+ const newStatus = st.status.toLowerCase() === 'active' ? 'Inactive' : 'Active';
+ setStoresList(stores => stores.map(s => s.locationid === st.locationid ? { ...s, status: newStatus } : s));
+ if (st.locationid) {
+ updateLocation.mutate({ locationid: st.locationid, status: newStatus.toLowerCase() });
+ }
+ }}
+ className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors focus:outline-none ${st.status.toLowerCase() === 'active' ? 'bg-emerald-500' : 'bg-zinc-300'}`}
+ title={`Toggle store to ${st.status.toLowerCase() === 'active' ? 'Inactive' : 'Active'}`}
+ >
+
+
+
+
+
Enter Terminal Console
+
+
+
+
+
+ );
+ })}
+
+
+
+ );
+ };
+
+ const renderSecondarySection = () => {
+ switch (currentSection) {
case 'settings':
return
;
@@ -626,7 +627,16 @@ export default function App() {
{/* Nav content routing */}
{currentSection === 'dashboard' && (
-
+ selectedStore ? (
+ renderStoresSection()
+ ) : (
+
+
+
+ {renderStoresSection()}
+
+
+ )
)}
{currentSection === 'inventory' && (
@@ -647,8 +657,8 @@ export default function App() {
)}
- {/* Handle alternative sections: Stores, Settings */}
- {['stores', 'settings'].includes(currentSection) &&
+ {/* Handle alternative sections: Settings */}
+ {['settings'].includes(currentSection) &&
renderSecondarySection()
}
@@ -787,6 +797,7 @@ export default function App() {
)}
+
);
}
diff --git a/src/components/BulkCartDrawer.tsx b/src/components/BulkCartDrawer.tsx
new file mode 100644
index 0000000..339a3f3
--- /dev/null
+++ b/src/components/BulkCartDrawer.tsx
@@ -0,0 +1,144 @@
+import React from 'react';
+import { ShoppingCart, X, Package, Trash2, Plus, ArrowRight, Layers } from 'lucide-react';
+import { useCompare } from '../contexts/CompareContext';
+import { useStoreCatalogue } from '../services/storeCatalogue';
+
+interface BulkCartDrawerProps {
+ isOpen: boolean;
+ onClose: () => void;
+ onRequestSamples: () => void;
+}
+
+export default function BulkCartDrawer({ isOpen, onClose, onRequestSamples }: BulkCartDrawerProps) {
+ const { selectedProducts, removeProduct, clearSelection, setIsComparing } = useCompare();
+ const storeCat = useStoreCatalogue();
+
+ if (!isOpen) return null;
+
+ const handleBulkAdd = () => {
+ let addedCount = 0;
+ selectedProducts.forEach(prod => {
+ if (prod.verified && !storeCat.has(prod.id)) {
+ storeCat.add({
+ productid: String(prod.id),
+ name: prod.name,
+ image: prod.image,
+ category: prod.category,
+ sku: prod.sku,
+ price: prod.price,
+ unit: 'All Outlets',
+ qty: 1
+ });
+ addedCount++;
+ }
+ });
+ alert(addedCount > 0 ? `Successfully added ${addedCount} product(s) to the store catalogue!` : 'Selected products are already in the catalogue or not verified.');
+ if (addedCount > 0) onClose();
+ };
+
+ const handleBulkRemove = () => {
+ let removedCount = 0;
+ selectedProducts.forEach(prod => {
+ if (storeCat.has(String(prod.id))) {
+ storeCat.remove(String(prod.id));
+ removedCount++;
+ }
+ });
+ alert(removedCount > 0 ? `Successfully removed ${removedCount} product(s) from the store catalogue!` : 'None of the selected products were in the catalogue.');
+ if (removedCount > 0) onClose();
+ };
+
+ return (
+
+ {/* Backdrop */}
+
+
+ {/* Drawer */}
+
+
+ {/* Header */}
+
+
+
+
+
+
+
Selection Cart
+
{selectedProducts.length} items ready for action
+
+
+
+ { clearSelection(); onClose(); }}
+ className="text-xs font-bold text-slate-500 hover:text-rose-500 px-3 py-2 rounded-lg hover:bg-rose-50 transition-colors"
+ >
+ Clear All
+
+
+
+
+
+
+
+ {/* Content (Scrollable List) */}
+
+ {selectedProducts.length === 0 ? (
+
+
+
Your cart is empty.
+
+ ) : (
+ selectedProducts.map((prod) => (
+
+
+
+
+
+
+ {prod.category}
+
+
{prod.name}
+
{prod.sku}
+
+
removeProduct(prod.id)}
+ className="absolute right-4 top-1/2 -translate-y-1/2 w-8 h-8 rounded-full bg-rose-50 text-rose-500 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-all hover:bg-rose-100 scale-90 group-hover:scale-100"
+ title="Remove from cart"
+ >
+
+
+
+ ))
+ )}
+
+
+ {/* Action Footer */}
+ {selectedProducts.length > 0 && (
+
+
+
+ Bulk Add
+
+
+
+ Bulk Remove
+
+
+
+ )}
+
+
+ );
+}
diff --git a/src/components/ComparisonModal.tsx b/src/components/ComparisonModal.tsx
new file mode 100644
index 0000000..29854f4
--- /dev/null
+++ b/src/components/ComparisonModal.tsx
@@ -0,0 +1,174 @@
+import React from 'react';
+import { createPortal } from 'react-dom';
+import { X, CheckCircle2, ArrowRight } from 'lucide-react';
+import { useCompare } from '../contexts/CompareContext';
+
+export default function ComparisonModal() {
+ const { selectedProducts, isComparing, setIsComparing, removeProduct, clearSelection, hideCompareBar } = useCompare();
+
+ if (selectedProducts.length === 0) return null;
+
+ return createPortal(
+ <>
+ {/* Sticky Comparison Bar */}
+ {!isComparing && !hideCompareBar && (
+
+
+
+ {selectedProducts.length}
+
+ items selected
+
+
+
+ {selectedProducts.map((p) => (
+
+ ))}
+
+
+
+
+ Clear
+
+
setIsComparing(true)}
+ className="bg-[#662582] hover:bg-purple-800 text-white px-5 py-2 rounded-full font-bold text-sm shadow-md transition-colors flex items-center gap-2"
+ >
+ Compare Now
+
+
+
+ )}
+
+ {/* Comparison Drawer */}
+ {isComparing && (
+
+
+
+ {/* Header */}
+
+
+
Product Comparison
+
+ {selectedProducts.length} items
+
+
+
+ {selectedProducts.length < 5 && (
+ setIsComparing(false)}
+ className="text-xs font-bold text-[#662582] hover:bg-purple-50 bg-white px-3 py-1.5 rounded-lg border border-purple-200 shadow-sm transition-colors flex items-center gap-1.5"
+ >
+ + Add Product
+
+ )}
+
+ Clear All
+
+ setIsComparing(false)}
+ className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-slate-200 text-slate-500 transition-colors"
+ >
+
+
+
+
+
+ {/* Comparison Table / Grid */}
+
+
+ {/* Left Column for labels */}
+
+
Category
+
SKU
+
Price
+
Units Sold
+
Status
+
+
+ {/* Product Columns */}
+ {selectedProducts.map((prod) => (
+
+
+ {/* Remove button overlay */}
+
removeProduct(prod.id)}
+ className="absolute -top-3 -right-3 w-7 h-7 bg-white border border-slate-200 rounded-full flex items-center justify-center text-slate-400 hover:text-rose-500 hover:border-rose-200 hover:bg-rose-50 shadow-sm z-20 opacity-0 group-hover:opacity-100 transition-all"
+ >
+
+
+
+ {/* Product Header Card */}
+
+
+
+
+
{prod.name}
+
+
+
+
+ {/* Metrics */}
+
+ {prod.category || '—'}
+
+
+
+ {prod.sku || '—'}
+
+
+
+
+ {prod.price > 0 ? `₹${prod.price.toLocaleString('en-IN')}` : '—'}
+
+
+
+
+
+ {prod.unitsSold != null ? prod.unitsSold.toLocaleString('en-IN') : '—'}
+
+
+
+
+ {prod.verified !== undefined ? (
+
+ {prod.verified ? (
+
+ Active
+
+ ) : (
+
+ Inspection
+
+ )}
+
+ ) : prod.closing !== undefined ? (
+
+ {prod.closing.toLocaleString('en-IN')} {prod.unit || 'Pc'}
+
+ ) : (
+
—
+ )}
+
+
+
+ ))}
+
+ {/* Empty placeholders removed */}
+
+
+
+
+
+
+ )}
+ >,
+ document.body
+ );
+}
diff --git a/src/components/DashboardView.tsx b/src/components/DashboardView.tsx
index 3be248e..215cce5 100644
--- a/src/components/DashboardView.tsx
+++ b/src/components/DashboardView.tsx
@@ -232,152 +232,7 @@ export default function DashboardView({ searchQuery, tenantId = FIESTA_TENANT_ID
})}
- {/* Order status + store locations */}
-
- {/* Store Node Status donut (live) */}
-
-
-
-
-
Store Outlet Status
-
Active share of all registered nodes.
-
-
-
-
- {/* soft glow behind the ring */}
-
-
-
-
-
-
-
-
-
-
-
-
- {activePct}%
- Active
-
-
-
-
-
- {statusRows.map((r) => (
-
-
-
- {r.label}
-
- {r.value.toLocaleString('en-IN')}
-
- ))}
-
- Total Nodes
- {totalStoresCount.toLocaleString('en-IN')}
-
-
-
-
- {/* Store locations (live) */}
-
-
-
-
-
-
- Store Locations
-
-
- {locationsQ.isLoading ? 'Loading…' : `${locations.length} Outlet${locations.length === 1 ? '' : 's'}`}
-
-
-
- {locationsQ.isLoading ? (
-
Loading store locations…
- ) : locations.length === 0 ? (
-
No store locations found for this tenant.
- ) : (
-
- {locations.map((loc, i) => {
- const sum = summaries.find((s) => s.locationid === Number(loc.locationid));
- const deliveries = sum?.delivered ?? 0;
- const orders = Math.max(sum?.delivered ?? 0, sum?.total ?? 0);
- const isActive = str(loc.status).toLowerCase() === 'active';
- const name = str(loc.locationname);
-
- return (
-
-
- {/* Outlet initial badge */}
-
- {name.slice(0, 2) || '—'}
-
-
-
{name}
-
-
- {str(loc.address) || `${str(loc.suburb)}, ${str(loc.city)}`}
-
- {str(loc.contactno) && (
-
-
- {str(loc.contactno)}
-
- )}
-
- {/* Node-specific Orders and Dispatches */}
-
-
- {orders} Orders
-
-
- {deliveries} Dispatched
-
- {orders > 0 && (
-
- {Math.round((deliveries / orders) * 100)}% Fulfilled
-
- )}
-
-
-
-
-
- {str(loc.status) || '—'}
-
-
- );
- })}
-
- )}
-
-
);
}
diff --git a/src/components/DeliveriesView.tsx b/src/components/DeliveriesView.tsx
index 61506c4..4f43d2b 100644
--- a/src/components/DeliveriesView.tsx
+++ b/src/components/DeliveriesView.tsx
@@ -109,7 +109,7 @@ export default function DeliveriesView({ searchQuery = '', locationid, tenantId
{ label: 'Total Deliveries', value: total.toLocaleString('en-IN'), color: '#6366f1', icon:
, badge: undefined },
{ label: 'Pending', value: (summary?.pending ?? 0).toLocaleString('en-IN'), color: '#f59e0b', icon:
, badge: pct(summary?.pending ?? 0) },
{ label: 'Delivered', value: (summary?.delivered ?? 0).toLocaleString('en-IN'), color: '#10b981', icon:
, badge: pct(summary?.delivered ?? 0) },
- { label: 'Cancelled', value: (summary?.cancelled ?? 0).toLocaleString('en-IN'), color: '#ef4444', icon:
, badge: pct(summary?.cancelled ?? 0) },
+ { label: 'Cancelled', value: (summary?.cancelled ?? 0).toLocaleString('en-IN'), color: '#f43f5e', icon:
, badge: pct(summary?.cancelled ?? 0) },
];
return (
diff --git a/src/components/DeliveryReportsView.tsx b/src/components/DeliveryReportsView.tsx
index bf8c27b..0d83cf1 100644
--- a/src/components/DeliveryReportsView.tsx
+++ b/src/components/DeliveryReportsView.tsx
@@ -16,11 +16,12 @@
import React, { useMemo, useState } from 'react';
import { TrendingUp, Clock, CheckCircle2, IndianRupee, Bike, Truck, Calendar, Store } from 'lucide-react';
import { useFiestaLocationSummary, useFiestaFleetSummary, useFiestaOrderSummary, useFiestaAllOrders, useFiestaRevenueSummary } from '../services/fiestaQueries';
-import { ResponsiveContainer, AreaChart, Area, XAxis, YAxis, Tooltip as RechartsTooltip, CartesianGrid } from 'recharts';
+import { ResponsiveContainer, AreaChart, Area, BarChart, Bar, PieChart, Pie, Cell, XAxis, YAxis, Tooltip as RechartsTooltip, CartesianGrid, Legend } from 'recharts';
import { FIESTA_TENANT_ID, num as fnum, str as fstr, ymd, type Row } from '../services/fiestaApi';
import {
GradientHeader, KpiStrip, Pill, StatusChip, MetricPill, FilterBar, TH_STYLE, TableShell,
DELIVERY_STATUS, statusColor, BRAND, BRAND_LIGHT, TEXT, TEXT_2, TEXT_3, BORDER, DIVIDER, SURFACE_ALT, tint, soft, edge, ring,
+ Skeleton, Tooltip
} from './consoleUi';
type ReportTab = 'orders-summary' | 'riders-summary';
@@ -50,35 +51,34 @@ export default function DeliveryReportsView({ searchQuery = '', tenantId = FIEST
return (
-
- {/* Tab nav */}
-
-
- {TABS.map((t) => {
- const Icon = t.icon;
- return (
-
- setTab(t.key)}> {t.label}
-
- );
- })}
-
-
- {/* Shared date range */}
+ {/* Tab nav & Date range combined */}
-
-
-
Period
- {presets.map((p) => (
-
{ setFromdate(p.from); setTodate(p.to); }}>{p.label}
- ))}
+
+
+ {TABS.map((t) => {
+ const Icon = t.icon;
+ return (
+
+ setTab(t.key)}> {t.label}
+
+ );
+ })}
-
-
setFromdate(e.target.value)} className="rounded-full outline-none font-semibold" style={{ padding: '6px 12px', border: `1.5px solid ${edge('#f59e0b')}`, background: tint('#f59e0b'), color: '#b45309' }} />
-
→
-
setTodate(e.target.value)} className="rounded-full outline-none font-semibold" style={{ padding: '6px 12px', border: `1.5px solid ${edge('#f59e0b')}`, background: tint('#f59e0b'), color: '#b45309' }} />
+
+
+
+
Period
+ {presets.map((p) => (
+
{ setFromdate(p.from); setTodate(p.to); }}>{p.label}
+ ))}
+
+
+ setFromdate(e.target.value)} className="rounded-full outline-none font-semibold w-28 sm:w-auto" style={{ padding: '6px 12px', border: `1.5px solid ${edge('#f59e0b')}`, background: tint('#f59e0b'), color: '#b45309' }} />
+ →
+ setTodate(e.target.value)} className="rounded-full outline-none font-semibold w-28 sm:w-auto" style={{ padding: '6px 12px', border: `1.5px solid ${edge('#f59e0b')}`, background: tint('#f59e0b'), color: '#b45309' }} />
+
@@ -143,35 +143,147 @@ function OrdersSummaryReport({ tenantId, locationid, fromdate, todate }: { tenan
const metricColor = metric === 'revenue' ? '#4f46e5' : '#0ea5e9';
+ // Computed Advanced Metrics
+ const aov = totals.total > 0 ? totalRevenue / totals.total : 0;
+ const cancelRate = totals.total > 0 ? (totals.cancelled / totals.total) * 100 : 0;
+
+ const hourlyData = useMemo(() => {
+ const hours = new Array(24).fill(0);
+ for (const r of (ordersQ.data ?? [])) {
+ const dateVal = fstr(r.orderdate) || fstr(r.deliverydate) || fstr(r.createdat);
+ if (!dateVal) continue;
+ const timePart = dateVal.split('T')[1];
+ if (timePart) {
+ // Adjust based on the actual timezone if needed, assuming UTC -> local roughly or just raw hour
+ const hour = parseInt(timePart.split(':')[0], 10);
+ if (!isNaN(hour) && hour >= 0 && hour <= 23) {
+ hours[hour] += 1;
+ }
+ }
+ }
+ // Combine hours into something more readable or just raw 24h
+ return hours.map((count, hour) => {
+ const ampm = hour >= 12 ? 'PM' : 'AM';
+ const h12 = hour % 12 || 12;
+ return { hourLabel: `${h12}${ampm}`, count };
+ });
+ }, [ordersQ.data]);
+
+ const statusData = [
+ { name: 'Delivered', value: totals.delivered, color: '#10b981' },
+ { name: 'Processing', value: totals.total - totals.delivered - totals.pending - totals.cancelled, color: '#6366f1' },
+ { name: 'Pending', value: totals.pending, color: '#f59e0b' },
+ { name: 'Cancelled', value: totals.cancelled, color: '#ef4444' },
+ ].filter(d => d.value > 0);
+
return (
-
-
setMetric('revenue')} className={`text-left p-5 bg-white rounded-2xl border transition-all duration-200 cursor-pointer flex items-center gap-4 ${metric === 'revenue' ? 'border-indigo-500 shadow-md ring-1 ring-indigo-500' : 'border-slate-200 shadow-sm hover:shadow-md'}`}>
-
-
-
Revenue
-
₹{totalRevenue.toLocaleString('en-IN')}
+
+ {/* Revenue */}
+
setMetric('revenue')} className={`rounded-xl p-3 flex flex-col justify-between transition-all duration-300 text-left cursor-pointer outline-none relative group border ${metric === 'revenue' ? 'border-indigo-200 bg-white shadow-[0_8px_24px_rgba(99,102,241,0.12)] ring-1 ring-indigo-500/20 z-10' : 'border-slate-200/70 bg-white hover:border-indigo-200 hover:shadow-[0_8px_24px_rgba(99,102,241,0.06)] opacity-70 hover:opacity-100'}`}>
+
+
+
+ ₹{totalRevenue.toLocaleString('en-IN')}
+
+
Total collected
-
setMetric('orders')} className={`text-left p-5 bg-white rounded-2xl border transition-all duration-200 cursor-pointer flex items-center gap-4 ${metric === 'orders' ? 'border-sky-500 shadow-md ring-1 ring-sky-500' : 'border-slate-200 shadow-sm hover:shadow-md'}`}>
-
-
-
Total Orders
-
{totals.total.toLocaleString('en-IN')}
+
+ {/* Orders */}
+
setMetric('orders')} className={`rounded-xl p-3 flex flex-col justify-between transition-all duration-300 text-left cursor-pointer outline-none relative group border ${metric === 'orders' ? 'border-sky-200 bg-white shadow-[0_8px_24px_rgba(14,165,233,0.12)] ring-1 ring-sky-500/20 z-10' : 'border-slate-200/70 bg-white hover:border-sky-200 hover:shadow-[0_8px_24px_rgba(14,165,233,0.06)] opacity-70 hover:opacity-100'}`}>
+
+
+ Total Orders
+
+
+
+
+
+
+
+ {totals.total.toLocaleString('en-IN')}
+
+
All statuses
-
-
-
-
Pending
-
{totals.pending.toLocaleString('en-IN')}
+
+ {/* AOV */}
+
+
+
+ Avg Order Value
+
+
+
+
+
+
+
+ ₹{Math.round(aov).toLocaleString('en-IN')}
+
+
Per order
-
-
-
-
Delivered
-
{totals.delivered.toLocaleString('en-IN')}
+
+ {/* Delivered */}
+
+
+
+
+ {totals.delivered.toLocaleString('en-IN')}
+
+
Successfully fulfilled
+
+
+
+ {/* Pending */}
+
+
+
+
+ {totals.pending.toLocaleString('en-IN')}
+
+
In progress
+
+
+
+ {/* Cancel Rate */}
+
+
+
+ Cancel Rate
+
+
10 ? 'bg-rose-100 text-rose-700' : 'bg-slate-50 text-slate-500'}`}>
+
+
+
+
+
10 ? 'text-rose-600' : 'text-slate-900'}`}>
+ {cancelRate.toFixed(1)}%
+
+
Cancelled orders
@@ -184,30 +296,43 @@ function OrdersSummaryReport({ tenantId, locationid, fromdate, todate }: { tenan
#
Outlet
Revenue
-
All
-
Created
-
Pending
-
Processing
-
Delivered
-
Cancelled
+
AOV
+
Orders
+
Fulfillment
+
Cancel Rate
- {q.isLoading || revenueQ.isLoading ? Loading outlet summary…
- : rows.length === 0 ? No outlet data available.
- : rows.map((r, i) => (
-
- {i + 1}
- {r.locationname || `Location ${r.locationid}`}
- ₹{(locationRevenueMap.get(fnum(r.locationid)) ?? 0).toLocaleString('en-IN')}
- {r.total.toLocaleString('en-IN')}
-
-
-
-
-
-
- ))}
+ {q.isLoading || revenueQ.isLoading ?
+ : rows.length === 0 ? No outlet data available.
+ : rows.map((r, i) => {
+ const rowRev = locationRevenueMap.get(fnum(r.locationid)) ?? 0;
+ const rowAov = r.total > 0 ? rowRev / r.total : 0;
+ const rowFulfill = r.total > 0 ? (r.delivered / r.total) * 100 : 0;
+ const rowCancel = r.total > 0 ? (r.cancelled / r.total) * 100 : 0;
+ return (
+
+ {i + 1}
+ {r.locationname || `Location ${r.locationid}`}
+ ₹{rowRev.toLocaleString('en-IN')}
+ ₹{Math.round(rowAov).toLocaleString('en-IN')}
+ {r.total.toLocaleString('en-IN')}
+
+
+
{rowFulfill.toFixed(0)}%
+
+
+
+
+ 10 ? 'bg-rose-100 text-rose-700' : 'bg-slate-100 text-slate-600'}`}>
+ {rowCancel.toFixed(1)}%
+
+
+
+ );
+ })}
@@ -222,58 +347,119 @@ function OrdersSummaryReport({ tenantId, locationid, fromdate, todate }: { tenan
)}
- {/* Trend Chart */}
-
-
-
-
Trend Analysis
+ {/* Insights Bento */}
+
+
+ {/* Trend Chart */}
+
+
+
+
Performance Trend
+
+
+ {ordersQ.isLoading ? (
+
+ ) : chartData.length === 0 ? (
+
No trend data for this period.
+ ) : (
+
+
+
+
+
+
+
+
+
+
+ metric === 'revenue' ? `₹${v.toLocaleString('en-IN')}` : v.toLocaleString('en-IN')} />
+ {
+ if (active && payload && payload.length) {
+ return (
+
+
{formatLabel(label)}
+
+
+
+ {metric}
+
+
+ {metric === 'revenue' ? `₹${payload[0].value.toLocaleString('en-IN')}` : payload[0].value.toLocaleString('en-IN')}
+
+
+
+ );
+ }
+ return null;
+ }}
+ />
+
+
+
+ )}
+
-
- {ordersQ.isLoading ? (
-
Loading trend data...
- ) : chartData.length === 0 ? (
-
No trend data for this period.
- ) : (
-
-
-
-
-
-
-
-
-
-
- metric === 'revenue' ? `₹${v.toLocaleString('en-IN')}` : v.toLocaleString('en-IN')} />
- {
- if (active && payload && payload.length) {
- return (
-
-
{formatLabel(label)}
-
-
-
- {metric}
-
-
- {metric === 'revenue' ? `₹${payload[0].value.toLocaleString('en-IN')}` : payload[0].value.toLocaleString('en-IN')}
-
-
-
- );
- }
- return null;
- }}
- />
-
-
-
- )}
+ {/* Breakdown Stack */}
+
+ {/* Status Breakdown */}
+
+
+
+
Order Status
+
+ {statusData.length > 0 ? (
+
+
+
+
+ {statusData.map((entry, index) => (
+ |
+ ))}
+
+ value.toLocaleString('en-IN')} contentStyle={{ borderRadius: '12px', border: '1px solid #e2e8f0', boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)' }} itemStyle={{ fontWeight: 'bold' }} />
+
+
+
+ ) : (
+
No data
+ )}
+
+ {statusData.map(s => (
+
+ {s.name} ({s.value})
+
+ ))}
+
+
+
+ {/* Hourly Heatmap (Bar) */}
+
+
+
+
Peak Hours
+
+
+ {ordersQ.isLoading ? (
+
+ ) : (
+
+
+
+
+
+
+
+
+
+ )}
+
+
+
);
@@ -301,7 +487,7 @@ function RidersSummaryReport({ fromdate, todate, tenantId, locationid }: { fromd
0 ? : undefined}>
- {q.isLoading ? Loading rider summary…
+ {q.isLoading ?
: q.isError ? Rider summary unavailable for this period.
: mapped.length === 0 ? No rider activity in this period.
: mapped.map((r, i) => (
diff --git a/src/components/InventoryView.tsx b/src/components/InventoryView.tsx
index e7b95b9..b79b2c4 100644
--- a/src/components/InventoryView.tsx
+++ b/src/components/InventoryView.tsx
@@ -28,18 +28,52 @@ import {
Server,
ChevronDown,
ChevronUp,
- CheckCircle
+ CheckCircle,
+ ShoppingCart
} from 'lucide-react';
import { ProductMatrixItem } from '../types';
-import {
- useFiestaTenantLocations,
- useFiestaStoresStock,
- useFiestaProductCategories,
-} from '../services/fiestaQueries';
+import { useFiestaProductCategories, useFiestaStoresStock, useFiestaTenantLocations } from '../services/fiestaQueries';
import { FIESTA_TENANT_ID, str as fstr } from '../services/fiestaApi';
import { stockRowToProduct, stockRowToInventory } from '../services/fiestaMappers';
import { useStoreCatalogue } from '../services/storeCatalogue';
+import BulkCartDrawer from './BulkCartDrawer';
import AwaitingApi from './AwaitingApi';
+import { SlideDrawer, Skeleton } from './consoleUi';
+import { useCompare } from '../contexts/CompareContext';
+import TrialBatchDrawer, { TrialProduct } from './TrialBatchDrawer';
+
+const MOCK_GLOBAL_CATALOG: ProductMatrixItem[] = [
+ {
+ id: 'gc-1', name: 'Aavin Pure Cow Milk 500ml', sku: 'GC-AAV-500', category: 'Dairy / Milk',
+ unitsSold: 0, revenue: 0, stockStatus: 'Healthy', trend: 'flat', exposure: 'All Outlets', verified: true, isNew: false,
+ image: 'https://images.unsplash.com/photo-1550583724-b2692b85b150?auto=format&fit=crop&q=80&w=200'
+ },
+ {
+ id: 'gc-2', name: 'Britannia Whole Wheat Bread', sku: 'GC-BRI-BREAD', category: 'Bakery / Bread',
+ unitsSold: 0, revenue: 0, stockStatus: 'Healthy', trend: 'flat', exposure: 'All Outlets', verified: true, isNew: false,
+ image: 'https://images.unsplash.com/photo-1509440159596-0249088772ff?auto=format&fit=crop&q=80&w=200'
+ },
+ {
+ id: 'gc-3', name: 'Heritage Farm Fresh Curd 400g', sku: 'GC-HER-CURD', category: 'Dairy / Curd',
+ unitsSold: 0, revenue: 0, stockStatus: 'Healthy', trend: 'flat', exposure: 'All Outlets', verified: true, isNew: false,
+ image: 'https://images.unsplash.com/photo-1588669527025-a131238ba4fb?auto=format&fit=crop&q=80&w=200'
+ },
+ {
+ id: 'gc-4', name: 'Tata Salt Vacuum Evaporated 1kg', sku: 'GC-TAT-SALT', category: 'Staples / Spices',
+ unitsSold: 0, revenue: 0, stockStatus: 'Healthy', trend: 'flat', exposure: 'All Outlets', verified: true, isNew: false,
+ image: 'https://images.unsplash.com/photo-1621245039234-f81d113daef4?auto=format&fit=crop&q=80&w=200'
+ },
+ {
+ id: 'gc-5', name: 'Sunfeast Dark Fantasy Choco Fills', sku: 'GC-SUN-CHOC', category: 'Snacks / Biscuits',
+ unitsSold: 0, revenue: 0, stockStatus: 'Healthy', trend: 'flat', exposure: 'All Outlets', verified: true, isNew: false,
+ image: 'https://images.unsplash.com/photo-1558961363-fa8fdf82db35?auto=format&fit=crop&q=80&w=200'
+ },
+ {
+ id: 'gc-6', name: 'Nandini GoodLife UHT Milk 500ml', sku: 'GC-NAN-UHT', category: 'Dairy / Milk',
+ unitsSold: 0, revenue: 0, stockStatus: 'Healthy', trend: 'flat', exposure: 'All Outlets', verified: true, isNew: true,
+ image: 'https://images.unsplash.com/photo-1563636619-e9143da7973b?auto=format&fit=crop&q=80&w=200'
+ },
+];
type StockRow = Record;
const rowId = (r: StockRow) => String(r.productid ?? '') || String(r.productname ?? '');
@@ -55,6 +89,10 @@ export default function InventoryView({
isCoimbatoreView,
tenantId = FIESTA_TENANT_ID
}: InventoryViewProps) {
+ const { selectedProducts, toggleProduct, setIsComparing, clearSelection } = useCompare();
+ const [trialProducts, setTrialProducts] = useState([]);
+ const [isCartOpen, setIsCartOpen] = useState(false);
+
// ── Live stock across every outlet (Fiesta) ───────────────────────────────
// This page is the admin's command surface. The GLOBAL CATALOG is the deduped
// union of products across all outlets the tenant owns (admin-only import adds
@@ -97,20 +135,7 @@ export default function InventoryView({
// Add 3 mock products with isNew: true
const mockProducts: ProductMatrixItem[] = [
- {
- id: 'mock-1',
- name: 'Organic Honey 500g',
- sku: 'GRO-HON-500G',
- unitsSold: 0,
- revenue: 0,
- stockStatus: 'Healthy',
- trend: 'flat',
- image: 'https://images.unsplash.com/photo-1587049352847-4d4b1240c5f2?auto=format&fit=crop&q=80&w=200',
- category: 'Groceries / Pantry',
- exposure: 'All Outlets',
- verified: true,
- isNew: true
- },
+
{
id: 'mock-2',
name: 'Premium Basmati Rice 5kg',
@@ -146,83 +171,13 @@ export default function InventoryView({
}, [allStoreRows, seeded]);
const [activeTab, setActiveTab] = useState<'catalog' | 'import_branding'>('catalog');
- const [selectedCategory, setSelectedCategory] = useState('ALL');
- // The store catalogue the admin curates from the global catalogue (shown to users).
+ const [selectedCategories, setSelectedCategories] = useState([]);
+ const [selectedAdminProduct, setSelectedAdminProduct] = useState(null);
+ const [localSearch, setLocalSearch] = useState('');
const storeCat = useStoreCatalogue();
- const [showAddProductModal, setShowAddProductModal] = useState(false);
- const [outletFilter, setOutletFilter] = useState<'all' | 'alerts'>('all');
- const [outletSearch, setOutletSearch] = useState('');
- // Regional Hub Stocks is read-only for admins — overrides remain empty (no restock actions).
- const [restockedOverrides] = useState>>({});
- const [expandedHubs, setExpandedHubs] = useState>({});
-
- // Memoize storesStock query results merged with simulated restock overrides
- const storesStockWithOverrides = useMemo(() => {
- return storesStock.map(store => {
- const overrides = restockedOverrides[store.locationid];
- if (!overrides) return store;
-
- const newRows = store.rows.map(row => {
- const sku = `SKU-${String(row.productid ?? '') || String(row.productname ?? '')}`;
- if (overrides[sku] !== undefined) {
- return {
- ...row,
- closing: overrides[sku],
- opening: Math.max(Number(row.opening || 0), overrides[sku])
- };
- }
- return row;
- });
-
- return {
- ...store,
- rows: newRows
- };
- });
- }, [storesStock, restockedOverrides]);
-
- // Memoized alerts analysis for all stores, using overridden data
- const storeAlertsData = useMemo(() => {
- let alertOutletsCount = 0;
- let criticalCount = 0;
- let lowStockCount = 0;
- const outletsWithAlerts: number[] = [];
-
- storesStockWithOverrides.forEach(store => {
- const items = store.rows.map(r => stockRowToInventory(r, store.locationname));
- const hasCritical = items.some(it => it.status === 'Critical');
- const hasLow = items.some(it => it.status === 'Low Stock');
-
- if (hasCritical || hasLow) {
- alertOutletsCount++;
- outletsWithAlerts.push(store.locationid);
- }
- criticalCount += items.filter(it => it.status === 'Critical').length;
- lowStockCount += items.filter(it => it.status === 'Low Stock').length;
- });
-
- return {
- alertOutletsCount,
- criticalCount,
- lowStockCount,
- outletsWithAlerts
- };
- }, [storesStockWithOverrides]);
-
- // CSV Textarea input
- const [csvText, setCsvText] = useState(
- "Name, SKU, Category, Price, InitialStock\nAmma Ghee Pure Butter, GHEE-AMMA-1L, Groceries / Oils, 640, 200\nBhavani Ponni Sona Rice, ST-SONA-25K, Staples / Rice, 1350, 150"
- );
-
- // Form state for individual adding
- const [newProduct, setNewProduct] = useState({
- name: '',
- sku: '',
- category: 'Staples / Rice',
- price: 150,
- initialStock: 250,
- image: 'https://images.unsplash.com/photo-1542838132-92c53300491e?auto=format&fit=crop&q=80&w=200'
- });
+ const [globalCatalogSearch, setGlobalCatalogSearch] = useState('');
+ const [globalCatalogPicks, setGlobalCatalogPicks] = useState>(new Set());
+ const [csvText, setCsvText] = useState('');
// Live product categories (for the Add-Product modal dropdown).
const productCategoriesQ = useFiestaProductCategories();
@@ -235,55 +190,73 @@ export default function InventoryView({
);
// Categories derived from the live catalog (falls back to ALL only).
- const categorySet = new Set();
- products.forEach((p) => categorySet.add(p.category));
- const categories: string[] = ['ALL', ...Array.from(categorySet)];
+ const categories = useMemo(() => {
+ const cats = Array.from(new Set(products.map((p) => p.category.split(' / ')[0])));
+ return cats.sort();
+ }, [products]);
// Filter criteria
- const filteredProducts = products.filter(p => {
- const matchesSearch = p.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
- p.sku.toLowerCase().includes(searchQuery.toLowerCase()) ||
- p.category.toLowerCase().includes(searchQuery.toLowerCase());
- const matchesCat = selectedCategory === 'ALL' || p.category.startsWith(selectedCategory.split(' / ')[0]);
- return matchesSearch && matchesCat;
- });
-
- const handleAddNewProduct = (e: React.FormEvent) => {
- e.preventDefault();
- if (!newProduct.name || !newProduct.sku) {
- alert('Kindly supply correct product specifications and catalogue SKU code.');
- return;
+ const filteredProducts = useMemo(() => {
+ let result = products;
+ if (searchQuery) {
+ const q = searchQuery.toLowerCase();
+ result = result.filter(
+ (p) =>
+ p.name.toLowerCase().includes(q) ||
+ p.sku.toLowerCase().includes(q) ||
+ p.category.toLowerCase().includes(q),
+ );
}
+ if (localSearch) {
+ const q = localSearch.toLowerCase();
+ result = result.filter(
+ (p) =>
+ p.name.toLowerCase().includes(q) ||
+ p.sku.toLowerCase().includes(q) ||
+ p.category.toLowerCase().includes(q),
+ );
+ }
+ if (selectedCategories.length > 0) {
+ result = result.filter((p) => selectedCategories.includes(p.category.split(' / ')[0]));
+ }
+ return result;
+ }, [products, searchQuery, localSearch, selectedCategories]);
- const createdProd: ProductMatrixItem = {
- id: String(products.length + 1),
- name: newProduct.name,
- sku: newProduct.sku,
- unitsSold: 0,
- revenue: 0,
- stockStatus: 'Healthy',
- trend: 'flat',
- image: newProduct.image,
- category: newProduct.category,
- exposure: 'All Outlets',
- verified: true,
- isNew: true
- };
-
- setProducts([createdProd, ...products]);
- setShowAddProductModal(false);
- alert(`Fresh product "${createdProd.name}" added to the Global Catalogue. It is now available to roll out to all outlets.`);
-
- setNewProduct({
- name: '',
- sku: '',
- category: 'Staples / Rice',
- price: 150,
- initialStock: 250,
- image: 'https://images.unsplash.com/photo-1542838132-92c53300491e?auto=format&fit=crop&q=80&w=200'
- });
+ const toggleCategory = (cat: string) => {
+ setSelectedCategories((prev) =>
+ prev.includes(cat) ? prev.filter((c) => c !== cat) : [...prev, cat],
+ );
};
+ const [globalSelectedCategories, setGlobalSelectedCategories] = useState([]);
+
+ const globalCategories = useMemo(() => {
+ const cats = Array.from(new Set(MOCK_GLOBAL_CATALOG.map((p) => p.category.split(' / ')[0])));
+ return cats.sort();
+ }, []);
+
+ const filteredGlobalProducts = useMemo(() => {
+ let result = MOCK_GLOBAL_CATALOG;
+ if (globalCatalogSearch) {
+ const q = globalCatalogSearch.toLowerCase();
+ result = result.filter(
+ (p) => p.name.toLowerCase().includes(q) || p.sku.toLowerCase().includes(q) || p.category.toLowerCase().includes(q)
+ );
+ }
+ if (globalSelectedCategories.length > 0) {
+ result = result.filter((p) => globalSelectedCategories.includes(p.category.split(' / ')[0]));
+ }
+ return result;
+ }, [globalCatalogSearch, globalSelectedCategories]);
+
+ const toggleGlobalCategory = (cat: string) => {
+ setGlobalSelectedCategories((prev) =>
+ prev.includes(cat) ? prev.filter((c) => c !== cat) : [...prev, cat],
+ );
+ };
+
+
+
const handleToggleProductExposure = (id: string) => {
setProducts(prev =>
prev.map(p => p.id === id ? { ...p, verified: !p.verified } : p)
@@ -344,360 +317,463 @@ export default function InventoryView({
{/* Header and Metrics */}
-
- {/* Small card metrics grid */}
-
- {/* Card 1: Total SKUs */}
-
-
-
Total SKUs
-
-
+ {activeTab === 'catalog' && (
+
+ {/* Small card metrics grid */}
+
+ {/* Card 1: Total SKUs */}
+
+
+
+
+ {products.length}
+
+
Master catalogue
-
-
- {products.length}
-
-
Master catalogue
-
-
- {/* Card 2: Synced Outlets */}
-
-
-
Active Outlets
-
-
+ {/* Card 2: Synced Outlets */}
+
+
+
+
+ {locations.length}
+
+
Synced locations
-
-
- {locations.length}
-
-
Synced locations
-
-
- {/* Card 3: Total On-Hand Volume */}
-
-
-
Total Stock
-
-
+ {/* Card 3: Total On-Hand Volume */}
+
+
+
+
+ {storesStock.reduce((total, store) => {
+ return total + (store.rows || []).reduce((subTotal, r) => {
+ const inv = stockRowToInventory(r, store.locationname);
+ return subTotal + (inv.stockLevel || 0);
+ }, 0);
+ }, 0).toLocaleString('en-IN')}
+
+
Units on hand
-
-
- {storesStockWithOverrides.reduce((total, store) => {
- return total + (store.rows || []).reduce((subTotal, r) => {
- const inv = stockRowToInventory(r, store.locationname);
- return subTotal + (inv.stockLevel || 0);
- }, 0);
- }, 0).toLocaleString('en-IN')}
-
-
Units on hand
-
-
- {/* Card 4: Catalog Health */}
-
-
-
Catalogue Sync Ratio
-
-
+ {/* Card 4: Catalog Health */}
+
+
+
Catalogue Sync Ratio
+
+
+
+
+
+
+ {products.length > 0 ? `${Math.round((products.filter(p => p.verified).length / products.length) * 100)}%` : '100%'}
+
+
Active Portfolio
-
-
-
- {products.length > 0 ? `${Math.round((products.filter(p => p.verified).length / products.length) * 100)}%` : '100%'}
-
-
Active Portfolio
-
+ )}
{activeTab === 'catalog' ? (
<>
- {/* Category filter + admin import actions */}
-
-
- {/* Themed Category Badges */}
-
- {categories.map((cat) => {
- const isSelected = selectedCategory === cat;
- let badgeTheme = '';
-
- if (cat === 'ALL') {
- badgeTheme = isSelected
- ? 'bg-indigo-600 border-indigo-600 text-white shadow-sm'
- : 'bg-white text-indigo-700 border-indigo-100 hover:bg-indigo-50';
- } else if (cat.startsWith('Staples')) {
- badgeTheme = isSelected
- ? 'bg-amber-600 border-amber-600 text-white shadow-sm'
- : 'bg-white text-amber-700 border-amber-100 hover:bg-amber-50';
- } else if (cat.startsWith('Groceries')) {
- badgeTheme = isSelected
- ? 'bg-emerald-600 border-emerald-600 text-white shadow-sm'
- : 'bg-white text-emerald-700 border-emerald-100 hover:bg-emerald-50';
- } else if (cat.startsWith('Beverages')) {
- badgeTheme = isSelected
- ? 'bg-sky-600 border-sky-600 text-white shadow-sm'
- : 'bg-white text-sky-700 border-sky-100 hover:bg-sky-50';
- } else {
- badgeTheme = isSelected
- ? 'bg-rose-600 border-rose-600 text-white shadow-sm'
- : 'bg-white text-rose-700 border-rose-100 hover:bg-rose-50';
- }
+
+ {/* ── Sticky Sidebar (Filters) ── */}
+
+
+
+
+ Filter Product
+
+
+
+ {/* Search Input */}
+
+ setLocalSearch(e.target.value)}
+ className="w-full pl-9 pr-4 py-2 bg-slate-50 border border-slate-200 rounded-xl text-xs text-slate-800 placeholder-slate-400 focus:outline-none focus:border-[#662582] focus:bg-white focus:ring-1 focus:ring-[#662582]/20 transition-all font-medium"
+ />
+
+ {localSearch && (
+ setLocalSearch('')}
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 bg-transparent border-none cursor-pointer"
+ >
+
+
+ )}
+
- return (
-
setSelectedCategory(cat)}
- className={`px-3 py-1.5 rounded-xl font-sans text-xs font-bold tracking-wide transition-all duration-200 border cursor-pointer outline-none ${badgeTheme}`}
+
+
+
+ Categories
+
+
+ {categories.map((cat) => (
+
+
+ toggleCategory(cat)}
+ className="peer appearance-none w-5 h-5 border-2 border-slate-300 rounded-lg checked:border-[#662582] checked:bg-[#662582] transition-colors cursor-pointer"
+ />
+
+
+
+ {cat}
+
+
+ ))}
+
+
+
+ {(selectedCategories.length > 0 || localSearch) && (
+ { setSelectedCategories([]); setLocalSearch(''); }}
+ className="w-full py-2 bg-slate-100 text-slate-600 rounded-xl text-[10px] font-bold uppercase tracking-wider hover:bg-slate-200 transition-colors cursor-pointer border-none mt-4"
>
- {cat === 'ALL'
- ? '🌐 All Items'
- : cat.replace('Groceries / ', '').replace('Staples / ', '').replace('Beverages / ', '').replace('Fresh Produce / ', '')}
+ Clear All Filters
- );
- })}
-
-
-
-
setActiveTab('import_branding')}
- className="bg-white text-[#662582] border border-purple-200 px-4 py-2 rounded-lg text-xs font-bold uppercase tracking-wider flex items-center justify-center gap-xs cursor-pointer hover:bg-[#faf5ff] transition shadow-sm"
- >
-
- Import SKUs
-
-
setShowAddProductModal(true)}
- className="bg-[#662582] text-white px-xl py-2 rounded-lg text-xs font-bold uppercase tracking-wider flex items-center justify-center gap-xs cursor-pointer hover:bg-purple-800 transition shadow-sm"
- >
-
- Add SKU
-
+ )}
+
- {/* Global Catalog — master assortment grid (full width) */}
-
-
+ {/* ── Main Content Area ── */}
+
+
-
- Global Catalogue Assortment
+
+ Product Catalogue Assortment
+
+
+ {storeCat.items.length} in store catalogue
+
+
+ {filteredProducts.length} item{filteredProducts.length === 1 ? '' : 's'} loaded
+
+
- Pick products & set quantities — selected items appear in every store's catalogue.
+
Pick products & set quantities — selected items appear in every store's catalogue.
-
-
- {storeCat.items.length} in store catalogue
-
-
- {filteredProducts.length} item{filteredProducts.length === 1 ? '' : 's'} loaded
-
+
+
+ {selectedProducts.length > 0 && (
+
{
+ setProducts(prev => prev.filter(p => !selectedProducts.some(sp => sp.id === p.id)));
+ clearSelection();
+ }}
+ className="flex items-center gap-1.5 bg-rose-50 hover:bg-rose-100 text-rose-600 border border-rose-200 px-3 py-1.5 rounded-lg text-xs font-bold transition-colors shadow-sm cursor-pointer animate-in zoom-in-95"
+ >
+ Remove Selected ({selectedProducts.length})
+
+ )}
+
setActiveTab('import_branding')}
+ className="flex items-center gap-1.5 bg-[#662582] hover:bg-purple-800 text-white px-3 py-1.5 rounded-lg text-xs font-bold transition-colors shadow-sm cursor-pointer"
+ >
+ Import Product
+
+ {filteredProducts.filter(p => p.isNew).length > 0 && (
+
+
+
Recently Added
+
+ )}
{storesLoading && products.length === 0 ? (
-
Synchronizing regional database...
+
+ {Array.from({ length: 10 }).map((_, i) => (
+
+ ))}
+
) : filteredProducts.length === 0 ? (
No catalogue products match your selection.
) : (
{/* Left Side: Normal Catalogue */}
-
+
{filteredProducts.filter(p => !p.isNew).map((prod) => (
-
-
- {/* Image zoom effect on hover */}
-
-
+
setSelectedAdminProduct(prod)} className="bg-white/80 backdrop-blur-md border border-[#e2e8f0] rounded-2xl flex flex-col shadow-sm hover:shadow-[0_12px_24px_rgba(99,102,241,0.06)] hover:border-[#662582]/40 hover:-translate-y-1 transition-all duration-300 relative group overflow-hidden cursor-pointer">
+ {/* Image Section - Top */}
+
+
+
+
+
+ {prod.category.split(' / ')[0]}
+
-
-
-
-
{prod.name}
-
-
- {prod.sku}
-
-
- {/* Categorized pill badge */}
-
- {prod.category.split(' / ')[0]}
+
+
+ {/* Content Section */}
+
+
+
+
{prod.name}
+
{prod.sku}
+
+
{
+ e.stopPropagation();
+ toggleProduct({
+ id: prod.id,
+ name: prod.name,
+ sku: prod.sku,
+ category: prod.category.split(' / ')[0],
+ price: prod.revenue / Math.max(1, prod.unitsSold),
+ image: prod.image,
+ unitsSold: prod.unitsSold,
+ verified: prod.verified
+ });
+ }}
+ >
+
+ String(p.id) === String(prod.id))}
+ readOnly
+ />
+
+
+
+
+
+
+
+ Units Sold
+ {prod.unitsSold.toLocaleString()}
+
+
+ Revenue
+ ₹{prod.revenue.toLocaleString()}
+
+
+
+
+
+
+
+ {prod.verified ? 'Active Portfolio' : 'Under Inspection'}
+
+
+ { e.stopPropagation(); handleToggleProductExposure(prod.id); }}
+ className="sr-only peer"
+ />
+
+
-
-
-
Units Sold
-
{prod.unitsSold.toLocaleString()}
+ {storeCat.has(prod.id) ? (
+
+ In Store Catalogue
+ { e.stopPropagation(); storeCat.remove(prod.id); }} title="Remove from store catalogue" className="flex items-center gap-1 px-2.5 py-1.5 rounded-lg bg-white text-rose-600 hover:bg-rose-50 shadow-sm border border-rose-100 font-bold text-[10px] cursor-pointer transition-colors whitespace-nowrap">
+ Remove
+
-
- Revenue
- ₹{prod.revenue.toLocaleString()}
-
-
+ ) : (
+
{ e.stopPropagation(); storeCat.add({ productid: prod.id, name: prod.name, image: prod.image, category: prod.category, sku: prod.sku, price: prod.unitsSold > 0 ? Math.round(prod.revenue / prod.unitsSold) : 0, unit: prod.exposure, qty: 1 }); }}
+ disabled={!prod.verified}
+ className={`w-full flex items-center justify-center gap-1.5 py-2.5 rounded-xl text-[11px] font-bold transition-all ${
+ prod.verified
+ ? 'bg-[#662582]/5 text-[#662582] hover:bg-[#662582] hover:text-white cursor-pointer shadow-sm border border-[#662582]/20'
+ : 'bg-zinc-50 text-zinc-400 cursor-not-allowed opacity-70 border border-zinc-200'
+ }`}
+ title={!prod.verified ? "Product must be Active to be added" : "Add to Store Catalogue"}
+ >
+ Add to Store Catalogue
+
+ )}
-
- {/* Exposure toggle row */}
-
-
-
- {prod.verified ? 'Active Portfolio' : 'Under Inspection'}
-
-
-
- handleToggleProductExposure(prod.id)}
- className="sr-only peer"
- />
-
-
-
-
- {/* Store-catalogue curation: pick the product to show to store users */}
- {storeCat.has(prod.id) ? (
-
- In Store Catalogue
- storeCat.remove(prod.id)} title="Remove from store catalogue" className="flex items-center gap-1 px-2 py-1 rounded-lg text-rose-500 hover:bg-rose-50 font-bold text-[10px] cursor-pointer transition-colors">
- Remove
-
-
- ) : (
-
storeCat.add({ productid: prod.id, name: prod.name, image: prod.image, category: prod.category, sku: prod.sku, price: prod.unitsSold > 0 ? Math.round(prod.revenue / prod.unitsSold) : 0, unit: prod.exposure, qty: 1 })}
- disabled={!prod.verified}
- className={`w-full flex items-center justify-center gap-1.5 pt-2.5 mt-1 border-t border-[#f1f5f9] text-[11px] font-bold transition-colors ${
- prod.verified
- ? 'text-[#662582] hover:text-purple-800 cursor-pointer'
- : 'text-zinc-400 cursor-not-allowed opacity-70'
- }`}
- title={!prod.verified ? "Product must be Active to be added" : "Add to Store Catalogue"}
- >
- Add to Store Catalogue
-
- )}
))}
- {/* Right Side: Newly Added Items */}
+ {/* Right Side: Recently Added Items */}
{filteredProducts.filter(p => p.isNew).length > 0 && (
-
-
-
-
Newly Added
-
+
{filteredProducts.filter(p => p.isNew).map((prod) => (
-
-
- {/* Image zoom effect on hover */}
-
-
+
setSelectedAdminProduct(prod)} className="bg-white/80 backdrop-blur-md border border-[#e2e8f0] rounded-2xl flex flex-col shadow-sm hover:shadow-[0_12px_24px_rgba(99,102,241,0.06)] hover:border-[#662582]/40 hover:-translate-y-1 transition-all duration-300 relative group overflow-hidden cursor-pointer">
+ {/* Image Section - Top */}
+
+
+
+
+
+ {prod.category.split(' / ')[0]}
+
-
-
-
-
{prod.name}
-
-
- {prod.sku}
-
-
- {/* Categorized pill badge */}
-
- {prod.category.split(' / ')[0]}
+
+
+ {/* Content Section */}
+
+
+
+
{prod.name}
+
{prod.sku}
+
+
{
+ e.stopPropagation();
+ toggleProduct({
+ id: prod.id,
+ name: prod.name,
+ sku: prod.sku,
+ category: prod.category.split(' / ')[0],
+ price: prod.revenue / Math.max(1, prod.unitsSold),
+ image: prod.image,
+ unitsSold: prod.unitsSold,
+ verified: prod.verified
+ });
+ }}
+ >
+
+ String(p.id) === String(prod.id))}
+ readOnly
+ />
+
+
+
+
+
+
+
+ Units Sold
+ {prod.unitsSold.toLocaleString()}
+
+
+ Revenue
+ ₹{prod.revenue.toLocaleString()}
+
+
+
+
+
+
+
+ {prod.verified ? 'Active Portfolio' : 'Under Inspection'}
+
+
+ handleToggleProductExposure(prod.id)}
+ className="sr-only peer"
+ />
+
+
-
-
-
Units Sold
-
{prod.unitsSold.toLocaleString()}
+ {storeCat.has(prod.id) ? (
+
+ In Store Catalogue
+ storeCat.remove(prod.id)} title="Remove from store catalogue" className="flex items-center gap-1 px-2.5 py-1.5 rounded-lg bg-white text-rose-600 hover:bg-rose-50 shadow-sm border border-rose-100 font-bold text-[10px] cursor-pointer transition-colors whitespace-nowrap">
+ Remove
+
-
- Revenue
- ₹{prod.revenue.toLocaleString()}
-
-
-
-
-
- {/* Exposure toggle row */}
-
-
-
- {prod.verified ? 'Active Portfolio' : 'Under Inspection'}
-
-
-
- handleToggleProductExposure(prod.id)}
- className="sr-only peer"
- />
-
-
-
-
- {/* Store-catalogue curation: pick the product to show to store users */}
- {storeCat.has(prod.id) ? (
-
-
In Store Catalogue
-
storeCat.remove(prod.id)} title="Remove from store catalogue" className="flex items-center gap-1 px-2 py-1 rounded-lg text-rose-500 hover:bg-rose-50 font-bold text-[10px] cursor-pointer transition-colors">
- Remove
+ ) : (
+ storeCat.add({ productid: prod.id, name: prod.name, image: prod.image, category: prod.category, sku: prod.sku, price: prod.unitsSold > 0 ? Math.round(prod.revenue / prod.unitsSold) : 0, unit: prod.exposure, qty: 1 })}
+ disabled={!prod.verified}
+ className={`w-full flex items-center justify-center gap-1.5 py-2.5 rounded-xl text-[11px] font-bold transition-all ${
+ prod.verified
+ ? 'bg-[#662582]/5 text-[#662582] hover:bg-[#662582] hover:text-white cursor-pointer shadow-sm border border-[#662582]/20'
+ : 'bg-zinc-50 text-zinc-400 cursor-not-allowed opacity-70 border border-zinc-200'
+ }`}
+ title={!prod.verified ? "Product must be Active to be added" : "Add to Store Catalogue"}
+ >
+ Add to Store Catalogue
+
+ )}
+ {
+ e.stopPropagation();
+ setTrialProducts([{
+ id: prod.id,
+ name: prod.name,
+ sku: prod.sku,
+ category: prod.category.split(' / ')[0],
+ price: prod.revenue / Math.max(1, prod.unitsSold),
+ image: prod.image
+ }]);
+ }}
+ className="w-full flex items-center justify-center gap-1.5 py-2 rounded-xl text-[11px] font-bold transition-all bg-indigo-50 text-indigo-700 hover:bg-indigo-600 hover:text-white cursor-pointer border border-indigo-100 hover:border-indigo-600"
+ >
+ Request Sample
- ) : (
-
storeCat.add({ productid: prod.id, name: prod.name, image: prod.image, category: prod.category, sku: prod.sku, price: prod.unitsSold > 0 ? Math.round(prod.revenue / prod.unitsSold) : 0, unit: prod.exposure, qty: 1 })}
- disabled={!prod.verified}
- className={`w-full flex items-center justify-center gap-1.5 pt-2.5 mt-1 border-t border-[#f1f5f9] text-[11px] font-bold transition-colors ${
- prod.verified
- ? 'text-[#662582] hover:text-purple-800 cursor-pointer'
- : 'text-zinc-400 cursor-not-allowed opacity-70'
- }`}
- title={!prod.verified ? "Product must be Active to be added" : "Add to Store Catalogue"}
- >
- Add to Store Catalogue
-
- )}
+
))}
@@ -706,503 +782,494 @@ export default function InventoryView({
)}
+
- {/* Store Stock — live per-outlet breakdown for every store under the admin */}
-
+ >
+ ) : (
+
+
+ {/* ── Sticky Sidebar (Filters) ── */}
+
- {/* Elegant Header Row */}
-
-
-
-
-
-
-
-
Regional Hub Stocks
-
-
- Live Sync
-
-
-
- Real-time inventory levels and capacity balance across {locations.length} regional outlets.
-
-
-
+
setActiveTab('catalog')}
+ className="flex items-center gap-1.5 bg-white border border-slate-200 text-slate-600 hover:bg-slate-50 hover:text-slate-900 px-4 py-2 rounded-xl text-sm font-bold transition-colors shadow-sm cursor-pointer"
+ >
+ ← Back to Product Catalogue
+
- {/* Controls: Search + Filters */}
-
- {/* Search */}
+
+
+
+ Filter Global Product
+
+
+
+ {/* Search Input */}
-
setOutletSearch(e.target.value)}
- className="pl-8 pr-7 py-1.5 bg-slate-50 border border-slate-200 rounded-full text-xs text-slate-800 placeholder-slate-400 focus:outline-none focus:border-purple-500 focus:bg-white focus:ring-1 focus:ring-purple-500/20 transition-all w-48 font-medium"
+ placeholder="Search global catalogue..."
+ value={globalCatalogSearch}
+ onChange={(e) => setGlobalCatalogSearch(e.target.value)}
+ className="w-full pl-9 pr-4 py-2 bg-slate-50 border border-slate-200 rounded-xl text-xs text-slate-800 placeholder-slate-400 focus:outline-none focus:border-[#662582] focus:bg-white focus:ring-1 focus:ring-[#662582]/20 transition-all font-medium"
/>
- {outletSearch && (
+
+ {globalCatalogSearch && (
setOutletSearch('')}
- className="absolute right-2.5 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 border-none bg-transparent cursor-pointer text-xs"
+ onClick={() => setGlobalCatalogSearch('')}
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 bg-transparent border-none cursor-pointer"
>
- ✕
+
)}
- {/* Filter buttons */}
-
-
setOutletFilter('all')}
- className={`px-3 py-1 rounded-full text-[10px] font-bold uppercase transition-all duration-200 cursor-pointer border-none ${
- outletFilter === 'all'
- ? 'bg-white text-purple-700 shadow-sm border border-purple-100/30'
- : 'text-slate-500 hover:text-slate-850 bg-transparent'
- }`}
- >
- All ({locations.length})
-
-
setOutletFilter('alerts')}
- className={`px-3 py-1 rounded-full text-[10px] font-bold uppercase transition-all duration-200 cursor-pointer border-none flex items-center gap-1 ${
- outletFilter === 'alerts'
- ? 'bg-white text-amber-700 shadow-sm border border-amber-100/30'
- : 'text-slate-500 hover:text-slate-850 bg-transparent'
- }`}
- >
- Alerts Only
- {storeAlertsData.alertOutletsCount > 0 && (
-
- {storeAlertsData.alertOutletsCount}
-
- )}
-
+
+
+
+ Categories
+
+
+ {globalCategories.map((cat) => (
+
+
+ toggleGlobalCategory(cat)}
+ className="peer appearance-none w-5 h-5 border-2 border-slate-300 rounded-lg checked:border-[#662582] checked:bg-[#662582] transition-colors cursor-pointer"
+ />
+
+
+
+ {cat}
+
+
+ ))}
+
+
+ {(globalSelectedCategories.length > 0 || globalCatalogSearch) && (
+
{ setGlobalSelectedCategories([]); setGlobalCatalogSearch(''); }}
+ className="w-full py-2 bg-slate-100 text-slate-600 rounded-xl text-[10px] font-bold uppercase tracking-wider hover:bg-slate-200 transition-colors cursor-pointer border-none mt-4"
+ >
+ Clear All Filters
+
+ )}
+
+
+
+
+ {/* ── Main Content Area ── */}
+
+
+
+
+ Super Admin Global Catalogue
+
+
+ {filteredGlobalProducts.length} items available
+
+
+
+
Select products from the global ledger to import them into your own catalogue.
+
+
+
+ {globalCatalogPicks.size > 0 && (
+
{
+ const newProds = MOCK_GLOBAL_CATALOG.filter(p => globalCatalogPicks.has(p.id) && !products.some(ext => ext.sku === p.sku));
+ if (newProds.length > 0) {
+ setProducts(prev => [...newProds, ...prev]);
+ setGlobalCatalogPicks(new Set());
+ setActiveTab('catalog');
+ }
+ }}
+ className="flex items-center gap-1.5 bg-[#662582] hover:bg-purple-800 text-white px-4 py-2 rounded-lg text-xs font-bold transition-colors shadow-sm cursor-pointer animate-in zoom-in-95"
+ >
+ Import Selected ({globalCatalogPicks.size})
+
+ )}
+ {filteredGlobalProducts.filter(p => p.isNew).length > 0 && (
+
+
+
Recently Added
+
+ )}
- {/* Quick Metrics Strip */}
-
- {[
- { label: 'Active Outlets', value: locations.length, icon: Server, chip: 'bg-purple-50 text-purple-650 ring-purple-100', value_cls: 'text-slate-900' },
- { label: 'Optimal Hubs', value: locations.length - storeAlertsData.alertOutletsCount, icon: CheckCircle, chip: 'bg-emerald-50 text-emerald-600 ring-emerald-100', value_cls: 'text-emerald-600' },
- { label: 'Low Stock Items', value: storeAlertsData.lowStockCount, icon: TrendingDown, chip: 'bg-amber-50 text-amber-600 ring-amber-100', value_cls: 'text-amber-600' },
- { label: 'Critical Alerts', value: storeAlertsData.criticalCount, icon: AlertTriangle, chip: 'bg-rose-50 text-rose-600 ring-rose-100', value_cls: 'text-rose-600' },
- ].map((m) => {
- const MIcon = m.icon;
- return (
-
-
-
-
-
- {m.label}
- {m.value}
-
-
- );
- })}
-
-
- {(() => {
- const filteredStores = storesStockWithOverrides.filter(store => {
- const matchesSearch = !outletSearch || store.locationname.toLowerCase().includes(outletSearch.toLowerCase());
- const matchesFilter = outletFilter === 'all' || storeAlertsData.outletsWithAlerts.includes(store.locationid);
- return matchesSearch && matchesFilter;
- });
-
- if (filteredStores.length === 0) {
- return (
-
- {outletFilter === 'alerts'
- ? '🎉 Outstanding! No outlets have critical or low stock alerts at this time.'
- : 'No outlets matched current search criteria.'}
-
- );
- }
-
- return (
-
- {filteredStores.map((store) => {
- const items = store.rows.map((r) => stockRowToInventory(r, store.locationname));
- const displayItems = items.filter((it) => !searchQuery || it.name.toLowerCase().includes(searchQuery.toLowerCase()));
- const totalUnits = items.reduce((a, it) => a + it.stockLevel, 0);
- const maxCapacity = items.reduce((a, it) => a + it.maxCapacity, 0);
- const capacityPct = Math.min(100, maxCapacity > 0 ? (totalUnits / maxCapacity) * 100 : 0);
+
+ {/* Left Side: Normal Catalogue */}
+
+
+ {filteredGlobalProducts.filter(p => !p.isNew).map((prod) => {
+ const isSelected = globalCatalogPicks.has(prod.id);
+ const isAlreadyInCatalog = products.some(p => p.sku === prod.sku);
- const totalItems = items.length;
- const optimalCount = items.filter(it => it.status === 'Optimal').length;
- const lowCount = items.filter(it => it.status === 'Low Stock').length;
- const criticalItemsCount = items.filter(it => it.status === 'Critical').length;
- const hasAlert = lowCount > 0 || criticalItemsCount > 0;
-
- const meta = locations.find((l) => l.locationid === store.locationid);
- const status = meta?.status ?? 'Active';
-
- const statusDotColor = hasAlert
- ? criticalItemsCount > 0 ? 'bg-rose-500' : 'bg-amber-500'
- : 'bg-emerald-500';
-
- const statusChip = hasAlert
- ? criticalItemsCount > 0 ? 'bg-rose-50 text-rose-600 ring-rose-100' : 'bg-amber-50 text-amber-600 ring-amber-100'
- : 'bg-emerald-50 text-emerald-600 ring-emerald-100';
-
- const optimalPct = totalItems > 0 ? (optimalCount / totalItems) * 100 : 0;
- const lowPct = totalItems > 0 ? (lowCount / totalItems) * 100 : 0;
- const criticalPct = totalItems > 0 ? (criticalItemsCount / totalItems) * 100 : 0;
-
- // Sort items: Critical first, then Low Stock, then Optimal
- const sortedItems = [...displayItems].sort((a, b) => {
- const severity: Record
= { 'Critical': 0, 'Low Stock': 1, 'Optimal': 2 };
- return (severity[a.status] ?? 2) - (severity[b.status] ?? 2);
- });
-
return (
-
-
- {/* Card Header */}
-
-
-
-
-
-
-
- {store.locationname}
-
-
- {totalItems} items · {totalUnits.toLocaleString('en-IN')} units
-
-
+
{
+ if (isAlreadyInCatalog) return;
+ setGlobalCatalogPicks(prev => {
+ const next = new Set(prev);
+ if (next.has(prod.id)) next.delete(prod.id);
+ else next.add(prod.id);
+ return next;
+ });
+ }}
+ >
+ {/* Image Section - Top */}
+
+
+
+
+
+ {prod.category.split(' / ')[0]}
+
-
0
- ? 'text-rose-600 bg-rose-50 border border-rose-100'
- : 'text-amber-700 bg-amber-50 border border-amber-100'
- : 'text-emerald-600 bg-emerald-50 border border-emerald-100'
- }`}>
-
- {hasAlert ? criticalItemsCount > 0 ? 'Critical' : 'Low Stock' : 'Optimal'}
-
+ {isAlreadyInCatalog && (
+
+ IN CATALOGUE
+
+ )}
- {/* Card Body */}
-
-
- {/* Segmented Stock Health Distribution */}
-
-
-
Stock Health
-
- {criticalItemsCount > 0 && {criticalItemsCount} crit }
- {lowCount > 0 && {lowCount} low }
- {optimalCount} ok
-
+ {/* Content Section */}
+
+
+
+
{prod.name}
+
{prod.sku}
-
- {criticalPct > 0 && (
-
- )}
- {lowPct > 0 && (
-
- )}
- {optimalPct > 0 && (
-
- )}
-
-
-
- {/* Capacity utilization indicator */}
-
-
- Capacity Utilised
- {Math.round(capacityPct)}%
-
-
-
85 ? 'bg-rose-500' : 'bg-gradient-to-r from-purple-500 to-indigo-500'
- }`}
- style={{ width: `${capacityPct}%` }}
- />
-
-
-
- {/* SKU lists */}
-
- {store.isLoading ? (
-
Syncing live balances…
- ) : store.isError ? (
-
Offline.
- ) : sortedItems.length === 0 ? (
-
No active stock items.
- ) : (
-
- {sortedItems.map((it, idx) => {
- const isLow = it.status !== 'Optimal';
-
- return (
-
-
-
-
- {it.name}
-
-
-
-
- {it.stockLevel}
-
-
- );
- })}
+ {!isAlreadyInCatalog && (
+
{
+ e.stopPropagation();
+ setGlobalCatalogPicks(prev => {
+ const next = new Set(prev);
+ if (next.has(prod.id)) next.delete(prod.id);
+ else next.add(prod.id);
+ return next;
+ });
+ }}
+ >
+
+
+
+
)}
-
+
+
+ Global Sales
+ {Math.floor(Math.random() * 5000) + 1000}
+
+
+ Rating
+ {(4.0 + Math.random()).toFixed(1)} ★
+
+
- {/* Card Footer — read-only status (admins cannot edit hub stock) */}
-
-
- {hasAlert ? `${criticalItemsCount + lowCount} items need attention` : 'All items optimal'}
-
+
+
+
+
+ {isAlreadyInCatalog ? 'Synced to Local' : 'Available for Import'}
+
+
- {hasAlert ? (
-
0 ? 'text-rose-600' : 'text-amber-700'
- }`}>
- {criticalItemsCount > 0 ? 'Critical' : 'Low Stock'}
-
- ) : (
-
- Stocked
-
- )}
+ {isAlreadyInCatalog ? (
+
+ Already in Catalogue
+
+ ) : (
+
{
+ e.stopPropagation();
+ setProducts(prev => [prod, ...prev]);
+ setActiveTab('catalog');
+ }}
+ className="w-full flex items-center justify-center gap-1.5 py-2.5 rounded-xl text-[11px] font-bold transition-all bg-[#662582]/5 text-[#662582] hover:bg-[#662582] hover:text-white cursor-pointer shadow-sm border border-[#662582]/20"
+ >
+ Import to Catalogue
+
+ )}
+
);
})}
- );
- })()}
-
- >
- ) : (
-
-
- {/* Left Column: Catalogue Import & Batch Console (7 Cols) */}
-
-
- {/* Fast Imports presets Card */}
-
-
-
-
Cooperative Catalogue Presets
-
-
+ {/* Right Side: Recently Added Items */}
+ {filteredGlobalProducts.filter(p => p.isNew).length > 0 && (
+
+
+ {filteredGlobalProducts.filter(p => p.isNew).map((prod) => {
+ const isSelected = globalCatalogPicks.has(prod.id);
+ const isAlreadyInCatalog = products.some(p => p.sku === prod.sku);
+
+ return (
+
{
+ if (isAlreadyInCatalog) return;
+ setGlobalCatalogPicks(prev => {
+ const next = new Set(prev);
+ if (next.has(prod.id)) next.delete(prod.id);
+ else next.add(prod.id);
+ return next;
+ });
+ }}
+ >
+ {/* Image Section - Top */}
+
+
+
+
+
+ {prod.category.split(' / ')[0]}
+
+
+ {isAlreadyInCatalog && (
+
+ IN CATALOGUE
+
+ )}
+
- {/* Custom CSV Parsing Box */}
-
-
-
-
Manual CSV Direct-Entry Console
-
+ {/* Content Section */}
+
+
+
+
{prod.name}
+
{prod.sku}
+
+ {!isAlreadyInCatalog && (
+
{
+ e.stopPropagation();
+ setGlobalCatalogPicks(prev => {
+ const next = new Set(prev);
+ if (next.has(prod.id)) next.delete(prod.id);
+ else next.add(prod.id);
+ return next;
+ });
+ }}
+ >
+
+
+
+
+
+ )}
+
-
-
- {/* Visual Editor Header */}
-
-
CSV_IMPORT_STREAM
-
+
+
+ Global Sales
+ {Math.floor(Math.random() * 5000) + 1000}
+
+
+ Rating
+ {(4.0 + Math.random()).toFixed(1)} ★
+
+
+
+
+
+
+
+ {isAlreadyInCatalog ? 'Synced to Local' : 'Available for Import'}
+
+
+
+ {isAlreadyInCatalog ? (
+
+ Already in Catalogue
+
+ ) : (
+
{
+ e.stopPropagation();
+ setProducts(prev => [prod, ...prev]);
+ setActiveTab('catalog');
+ }}
+ className="w-full flex items-center justify-center gap-1.5 py-2.5 rounded-xl text-[11px] font-bold transition-all bg-[#662582]/5 text-[#662582] hover:bg-[#662582] hover:text-white cursor-pointer shadow-sm border border-[#662582]/20"
+ >
+ Import to Catalogue
+
+ )}
+
+
+
+ );
+ })}
-
-
-
- Header row ignored on parse
-
- Parse CSV Data & Sync
-
-
-
+ )}
-
- {/* Realtime Import Logs list styled like terminal */}
-
-
-
-
- Live Sync Audit Stream
-
- COIMBATORE_ERP_V4
-
-
-
-
-
-
- {/* Right Column: Beautiful Interactive Brand Design Studio (5 Cols) */}
-
-
-
-
-
-
Packaging Branding Studio
-
-
-
-
-
-
-
)}
- {/* CREATE NEW PRODUCT MODAL PORTAL */}
- {showAddProductModal && (
-
{ if (e.target === e.currentTarget) setShowAddProductModal(false); }}
- >
-
-
-
-
- Introduce New Grocery Catalogue SKU
-
-
setShowAddProductModal(false)}
- className="p-1 hover:bg-zinc-200 rounded-full text-zinc-400 cursor-pointer transition-colors"
- >
-
-
+ {/* ── Slide Drawer ── */}
+
setSelectedAdminProduct(null)}
+ title="Administrative Product Details"
+ >
+ {selectedAdminProduct && (
+
+
+
+
+
+ {selectedAdminProduct.category}
+
+
+
+
+
+
{selectedAdminProduct.name}
+
{selectedAdminProduct.sku}
-
diff --git a/src/components/OrdersDeliveriesView.tsx b/src/components/OrdersDeliveriesView.tsx
index 3443b42..24447e5 100644
--- a/src/components/OrdersDeliveriesView.tsx
+++ b/src/components/OrdersDeliveriesView.tsx
@@ -140,10 +140,10 @@ export default function OrdersDeliveriesView({
const pct = (n: number) => (total > 0 ? Math.round((n / total) * 100) : 0);
const kpis = [
- { label: 'Created Orders', value: countFor('created').toLocaleString('en-IN'), color: '#475569', icon: , badge: `${pct(countFor('created'))}% of total` },
- { label: 'Pending Orders', value: countFor('pending').toLocaleString('en-IN'), color: '#9a6700', icon: , badge: `${pct(countFor('pending'))}% of total` },
- { label: 'Delivered Orders', value: countFor('delivered').toLocaleString('en-IN'), color: '#15803d', icon: , badge: `${pct(countFor('delivered'))}% of total` },
- { label: 'Cancelled Orders', value: countFor('cancelled').toLocaleString('en-IN'), color: '#b42318', icon: , badge: `${pct(countFor('cancelled'))}% of total` },
+ { label: 'Created Orders', value: countFor('created').toLocaleString('en-IN'), color: '#6366f1', icon: , badge: `${pct(countFor('created'))}% of total` },
+ { label: 'Pending Orders', value: countFor('pending').toLocaleString('en-IN'), color: '#f59e0b', icon: , badge: `${pct(countFor('pending'))}% of total` },
+ { label: 'Delivered Orders', value: countFor('delivered').toLocaleString('en-IN'), color: '#10b981', icon: , badge: `${pct(countFor('delivered'))}% of total` },
+ { label: 'Cancelled Orders', value: countFor('cancelled').toLocaleString('en-IN'), color: '#f43f5e', icon: , badge: `${pct(countFor('cancelled'))}% of total` },
];
// CSV export
diff --git a/src/components/OrdersView.tsx b/src/components/OrdersView.tsx
index c9c1e10..8d10a01 100644
--- a/src/components/OrdersView.tsx
+++ b/src/components/OrdersView.tsx
@@ -198,10 +198,10 @@ export default function OrdersView({ searchQuery = '', locationid, tenantId = FI
// Restrained, professional palette — deep muted tones (not neon) so the KPI
// strip reads as a serious business dashboard rather than a colourful one.
const kpis = [
- { label: 'Created Orders', value: (summary?.created ?? 0).toLocaleString('en-IN'), color: '#475569', icon: , badge: `${pct(summary?.created ?? 0)}% of total` },
- { label: 'Pending Orders', value: (summary?.pending ?? 0).toLocaleString('en-IN'), color: '#9a6700', icon: , badge: `${pct(summary?.pending ?? 0)}% of total` },
- { label: 'Delivered Orders', value: (summary?.delivered ?? 0).toLocaleString('en-IN'), color: '#15803d', icon: , badge: `${pct(summary?.delivered ?? 0)}% of total` },
- { label: 'Cancelled Orders', value: (summary?.cancelled ?? 0).toLocaleString('en-IN'), color: '#b42318', icon: , badge: `${pct(summary?.cancelled ?? 0)}% of total` },
+ { label: 'Created Orders', value: (summary?.created ?? 0).toLocaleString('en-IN'), color: '#6366f1', icon: , badge: `${pct(summary?.created ?? 0)}% of total` },
+ { label: 'Pending Orders', value: (summary?.pending ?? 0).toLocaleString('en-IN'), color: '#f59e0b', icon: , badge: `${pct(summary?.pending ?? 0)}% of total` },
+ { label: 'Delivered Orders', value: (summary?.delivered ?? 0).toLocaleString('en-IN'), color: '#10b981', icon: , badge: `${pct(summary?.delivered ?? 0)}% of total` },
+ { label: 'Cancelled Orders', value: (summary?.cancelled ?? 0).toLocaleString('en-IN'), color: '#f43f5e', icon: , badge: `${pct(summary?.cancelled ?? 0)}% of total` },
];
const setScope = (next: Partial<{ status: StatusKey; from: string; to: string }>) => {
diff --git a/src/components/ReportsView.tsx b/src/components/ReportsView.tsx
index 3eaaf24..15bc249 100644
--- a/src/components/ReportsView.tsx
+++ b/src/components/ReportsView.tsx
@@ -23,7 +23,8 @@ import {
MapPin,
Calendar,
Trophy,
- Store
+ Store,
+ CheckCircle2
} from 'lucide-react';
import { LeaderboardNode } from '../types';
import {
@@ -33,6 +34,7 @@ import {
useFiestaOrderSummary,
useFiestaRevenueSummary,
useFiestaTimeSeries,
+ useFiestaStoresTimeSeries,
} from '../services/fiestaQueries';
import { FIESTA_TENANT_ID, FIESTA_PRIMARY_LOCATION_ID, num as fnum, str as fstr, ymd } from '../services/fiestaApi';
import { stockRowToProduct } from '../services/fiestaMappers';
@@ -283,38 +285,48 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
- // Leaderboard — outlets ranked by total live orders.
+ // Monthly order distribution per outlet — includes all stores.
+ // Stores with no orders will simply show 0s across the board.
+ const insightRows = (() => {
+ return filteredLocations.map(loc => {
+ const locName = loc.locationname || `Location ${loc.locationid}`;
+ // Find the corresponding row from insightQ.data
+ const insightData = (insightQ.data ?? []).find(
+ r => (fstr(r.locationname) || `Location ${fstr(r.locationid)}`) === locName
+ );
+
+ return {
+ name: locName,
+ months: (insightData?.ordermonths ?? {}) as Record,
+ };
+ });
+ })();
+
+ // Leaderboard — outlets ranked by current month's live orders.
const leaderboard: LeaderboardNode[] = (() => {
- let rows = [...filteredLocations];
- rows = rows.sort((a, b) => b.total - a.total).slice(0, 4);
+ const currentMonthIdx = new Date().getMonth();
+ const currentMonthKey = MONTH_KEYS[currentMonthIdx];
+
+ let rows = filteredLocations.map(loc => {
+ const name = loc.locationname || `Location ${loc.locationid}`;
+ // Find the corresponding insight row for this store
+ const insightRow = insightRows.find(r => r.name === name);
+ const total = insightRow ? fnum(insightRow.months[currentMonthKey]) : 0;
+
+ return { name, total };
+ });
+
+ rows = rows.sort((a, b) => b.total - a.total);
const max = rows.length ? rows[0].total : 0;
return rows.map((r, i) => ({
rank: String(i + 1).padStart(2, '0'),
- name: r.locationname || `Location ${r.locationid}`,
+ name: r.name,
percentage: max > 0 ? Math.round((r.total / max) * 100) : 0,
- // Live order count drives the ranking/bar. No per-node revenue API yet, so the
- // label shows the real order count (not fabricated rupees) — revenue lands with [R1].
revenue: `${r.total.toLocaleString()} ord`,
}));
})();
const currentLeaderboard = leaderboard;
- // Monthly order distribution per outlet — live only (useFiestaOrderInsight
- // already covers all the tenant's locations). The Chennai/Bangalore stub rows
- // were removed; selecting those regions filters the live rows to none.
- const insightRows = (() => {
- let rows = (insightQ.data ?? []);
- if (selectedRegion === 'coimbatore') {
- rows = rows.filter(r => isCoimbatoreNode(fstr(r.locationname)));
- } else if (selectedRegion === 'chennai' || selectedRegion === 'bangalore') {
- rows = [];
- }
- return rows.map((r) => ({
- name: fstr(r.locationname) || `Location ${fstr(r.locationid)}`,
- months: (r.ordermonths ?? {}) as Record,
- }));
- })();
-
const heatmapMax = Math.max(
1,
...insightRows.flatMap((row) => MONTH_KEYS.map((k) => fnum(row.months[k]))),
@@ -426,15 +438,23 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
return ymd(d);
}, [chartTimeframe]);
- const tsQ = useFiestaTimeSeries({
+ const tsQ_all = useFiestaTimeSeries({
tenantid: tenantId,
granularity: tsGranularity,
fromdate: tsFromDate,
todate: todate,
});
+ const tsQ_stores = useFiestaStoresTimeSeries(
+ tenantId,
+ mappedLocations,
+ tsGranularity,
+ tsFromDate,
+ todate
+ );
+
const chartData = useMemo(() => {
- const data = tsQ.data || [];
+ const data = tsQ_all.data || [];
if (data.length === 0) return [{ label: 'No Data', value: 0 }];
return data.map((d: any) => {
@@ -461,49 +481,13 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
value: val
};
});
- }, [tsQ.data, chartMetric, tsGranularity]);
+ }, [tsQ_all.data, tsQ_stores.data, isAllRegions, chartMetric, tsGranularity]);
const maxChartVal = useMemo(() => Math.max(...chartData.map(d => d.value), 1), [chartData]);
return (
-
+
- {/* Immersive Background Blur Blobs */}
-
-
-
- {/* ── Immersive Analytics Banner (With Data Cover Image & Slate Gradient Overlay) ── */}
-
- {/* Cover Image Background & Decor (wrapped in overflow-hidden to keep rounded corner clip, while allowing dropdown overflow) */}
-
-
-
-
- {/* Background decorative glowing circles */}
-
-
-
-
- {/* Content Row */}
-
-
-
- Business Intelligence Center
-
- Live Core
-
-
-
-
-
-
-
-
-
{/* Primary KPI Row - 4 Key Tab buttons with Sparklines */}
@@ -519,91 +503,76 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
if (kpi.color === 'indigo') {
activeStyles = isActive
- ? 'border-indigo-500 bg-indigo-50/10 shadow-[0_12px_30px_rgba(99,102,241,0.15)] ring-1 ring-indigo-500 scale-102 z-10'
- : 'hover:border-indigo-200 border-[#e2e8f0] bg-white/70 hover:bg-white hover:shadow-[0_8px_20px_rgba(99,102,241,0.05)]';
+ ? 'border-indigo-200 bg-white shadow-[0_8px_24px_rgba(99,102,241,0.12)] ring-1 ring-indigo-500/20 z-10'
+ : 'border-slate-200/70 bg-white hover:border-indigo-200 hover:shadow-[0_8px_24px_rgba(99,102,241,0.06)] opacity-70 hover:opacity-100';
strokeColor = '#6366f1';
IconComponent = ShoppingBag;
- iconBg = 'bg-indigo-50 border border-indigo-100 shadow-[0_0_12px_rgba(99,102,241,0.2)]';
- iconTextColor = 'text-indigo-600';
+ iconBg = 'bg-indigo-50 text-indigo-600 ring-1 ring-indigo-100';
} else if (kpi.color === 'emerald') {
activeStyles = isActive
- ? 'border-emerald-500 bg-emerald-50/10 shadow-[0_12px_30px_rgba(16,185,129,0.15)] ring-1 ring-emerald-500 scale-102 z-10'
- : 'hover:border-emerald-200 border-[#e2e8f0] bg-white/70 hover:bg-white hover:shadow-[0_8px_20px_rgba(16,185,129,0.05)]';
+ ? 'border-emerald-200 bg-white shadow-[0_8px_24px_rgba(16,185,129,0.12)] ring-1 ring-emerald-500/20 z-10'
+ : 'border-slate-200/70 bg-white hover:border-emerald-200 hover:shadow-[0_8px_24px_rgba(16,185,129,0.06)] opacity-70 hover:opacity-100';
strokeColor = '#10b981';
IconComponent = Activity;
- iconBg = 'bg-emerald-50 border border-emerald-100 shadow-[0_0_12px_rgba(16,185,129,0.2)]';
- iconTextColor = 'text-emerald-600';
+ iconBg = 'bg-emerald-50 text-emerald-600 ring-1 ring-emerald-100';
} else if (kpi.color === 'rose') {
activeStyles = isActive
- ? 'border-rose-500 bg-rose-50/10 shadow-[0_12px_30px_rgba(244,63,94,0.15)] ring-1 ring-rose-500 scale-102 z-10'
- : 'hover:border-rose-200 border-[#e2e8f0] bg-white/70 hover:bg-white hover:shadow-[0_8px_20px_rgba(244,63,94,0.05)]';
+ ? 'border-rose-200 bg-white shadow-[0_8px_24px_rgba(244,63,94,0.12)] ring-1 ring-rose-500/20 z-10'
+ : 'border-slate-200/70 bg-white hover:border-rose-200 hover:shadow-[0_8px_24px_rgba(244,63,94,0.06)] opacity-70 hover:opacity-100';
strokeColor = '#f43f5e';
IconComponent = AlertTriangle;
- iconBg = 'bg-rose-50 border border-rose-100 shadow-[0_0_12px_rgba(244,63,94,0.2)]';
- iconTextColor = 'text-rose-600';
+ iconBg = 'bg-rose-50 text-rose-600 ring-1 ring-rose-100';
} else {
activeStyles = isActive
- ? 'border-sky-500 bg-sky-50/10 shadow-[0_12px_30px_rgba(14,165,233,0.15)] ring-1 ring-sky-500 scale-102 z-10'
- : 'hover:border-sky-200 border-[#e2e8f0] bg-white/70 hover:bg-white hover:shadow-[0_8px_20px_rgba(14,165,233,0.05)]';
+ ? 'border-sky-200 bg-white shadow-[0_8px_24px_rgba(14,165,233,0.12)] ring-1 ring-sky-500/20 z-10'
+ : 'border-slate-200/70 bg-white hover:border-sky-200 hover:shadow-[0_8px_24px_rgba(14,165,233,0.06)] opacity-70 hover:opacity-100';
strokeColor = '#0ea5e9';
IconComponent = Package;
- iconBg = 'bg-sky-50 border border-sky-100 shadow-[0_0_12px_rgba(14,165,233,0.2)]';
- iconTextColor = 'text-sky-600';
+ iconBg = 'bg-sky-50 text-sky-600 ring-1 ring-sky-100';
}
return (
setChartMetric(kpi.id)}
- className={`border backdrop-blur-md rounded-2xl p-3 flex flex-col justify-between hover:shadow-lg transition-all duration-300 hover:-translate-y-1.5 text-left cursor-pointer outline-none relative overflow-hidden group ${activeStyles}`}
+ className={`rounded-xl p-3 flex flex-col justify-between transition-all duration-300 text-left cursor-pointer outline-none relative group border ${activeStyles}`}
>
- {/* Subtle top glowing line when active */}
- {isActive && (
-
- )}
-
- {/* Header Row */}
-
-
- {kpi.title}
-
-
-
-
-
-
- {/* Main Metric Value and Trend Badge */}
- {kpi.awaiting ? (
-
- ) : (
-
-
- {kpi.value}
-
+
+
{kpi.title}
+
{kpi.trend && (
-
- {kpi.isPositive ? '▲' : '▼'}
+
+ {kpi.isPositive ? : }
{kpi.trend}
)}
+
+
+
- )}
-
- {/* Bottom Sparkline & Subtext segment */}
-
-
{kpi.status}
-
- {/* KPI Mini Sparkline Chart */}
-
+
+
+
+
+ {kpi.awaiting ? (
+
+ ) : (
+
+ {kpi.value}
+
+ )}
+
{kpi.status}
+
+
+
@@ -614,143 +583,33 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
})}
- {/* Main Interactive Charts & Insights Bento Segment */}
-
- {/* Interactive Main Graph Card — Full width (12 cols) */}
-
-
-
-
-
- Interactive Graph
-
-
- {chartMetric === 'orders' ? 'Total Orders Velocity Trend' :
- chartMetric === 'revenue' ? 'Revenue Expansion Trajectory' :
- chartMetric === 'cancelled' ? 'Order Cancellation Frequency' :
- 'Catalogue Active SKUs Growth'}
-
-
-
-
-
- {(['day', 'week', 'month'] as const).map((tf) => (
- setChartTimeframe(tf)}
- className={`px-3 py-1.5 rounded-lg text-[10px] font-bold uppercase tracking-wider transition-all ${
- chartTimeframe === tf
- ? 'bg-white text-purple-700 shadow-sm border border-purple-100'
- : 'text-slate-500 hover:text-slate-700 hover:bg-slate-200/50'
- }`}
- >
- {tf}
-
- ))}
-
-
-
-
- Live Sync
-
-
-
-
- {/* Plotted Area */}
-
-
-
-
-
-
-
-
- {
- const x = (i / (chartData.length - 1)) * 100;
- const y = 100 - ((d.value / maxChartVal) * 80);
- return `L ${x},${y} `;
- }).join('') + `L 100,100 Z`}
- fill="url(#chartGradient)"
- vectorEffect="non-scaling-stroke"
- />
- {
- const x = (i / (chartData.length - 1)) * 100;
- const y = 100 - ((d.value / maxChartVal) * 80);
- return `${i === 0 ? 'M' : 'L'} ${x},${y}`;
- }).join(' ')}
- fill="none"
- stroke={theme.activeLine}
- strokeWidth="2.5"
- strokeLinecap="round"
- strokeLinejoin="round"
- vectorEffect="non-scaling-stroke"
- className="drop-shadow-[0_4px_6px_rgba(168,85,247,0.4)]"
- />
-
-
-
- {chartData.map((d, i) => (
-
- {/* Hover vertical line */}
-
-
- {/* Invisible hit area */}
-
-
- {/* Tooltip */}
-
-
{d.label}
-
- {chartMetric === 'revenue' ? `₹${d.value.toLocaleString()}` : d.value.toLocaleString()}
-
- {/* Tooltip caret */}
-
-
-
- {/* Dot on line */}
-
-
- {/* X-axis label */}
-
{d.label}
-
- ))}
-
-
-
-
-
{/* Bento split maps (Heatmap grid & Leaderboard) */}
{/* Revenue Heatmap table - 8 Cols */}
-
+
-
-
+
+
Monthly Order Distribution
-
-
+
{selectedCell && (
-
+
-
+
-
+
{selectedCell.day} · {selectedCell.hour} : {selectedCell.val} orders
@@ -764,16 +623,16 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
)}
{insightRows.length === 0 ? (
-
+
{insightQ.isLoading ? 'Loading monthly order distribution…' : 'No order insight available for this region.'}
) : (
- Outlet Name
+ Outlet Name
{MONTH_LABELS.map((m) => (
-
+
{m}
))}
@@ -782,7 +641,7 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
{insightRows.map((row, idx) => (
-
+
{row.name}
{MONTH_KEYS.map((key, mIdx) => {
@@ -807,16 +666,16 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
{/* Leaderboard nodes bar list - 4 Cols */}
-
-
-
+
+
+
Top Performing Nodes
-
+
{currentLeaderboard.length === 0 ? (
-
+
No active nodes in this region.
) : (
@@ -825,34 +684,41 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
let badgeContent: React.ReactNode = node.rank;
if (index === 0) {
- badgeStyle = 'bg-gradient-to-tr from-amber-300 via-yellow-400 to-amber-500 text-amber-955 border-amber-200 shadow-md shadow-amber-500/20';
+ badgeStyle = 'bg-gradient-to-tr from-amber-300 via-yellow-400 to-amber-500 text-amber-950 shadow-md shadow-amber-500/20';
badgeContent = '🥇';
} else if (index === 1) {
- badgeStyle = 'bg-gradient-to-tr from-slate-200 via-zinc-300 to-slate-400 text-zinc-800 border-zinc-100 shadow-md shadow-zinc-400/10';
+ badgeStyle = 'bg-gradient-to-tr from-slate-200 via-slate-300 to-slate-400 text-slate-800 shadow-md shadow-slate-400/10';
badgeContent = '🥈';
} else if (index === 2) {
- badgeStyle = 'bg-gradient-to-tr from-amber-600 via-amber-700 to-orange-850 text-amber-50 border-amber-500 shadow-md shadow-amber-800/20';
+ badgeStyle = 'bg-gradient-to-tr from-amber-600 via-amber-700 to-orange-800 text-white shadow-md shadow-amber-800/20';
badgeContent = '🥉';
} else {
- badgeStyle = 'bg-slate-100 text-zinc-505 border-slate-200';
+ badgeStyle = 'bg-slate-100 text-slate-500 border border-slate-200';
}
return (
-
+
{/* Ranking circle avatar */}
-
+
{badgeContent}
-
-
{node.name}
-
{node.revenue}
+
+
+ {node.name}
+ {index < 3 && (
+
+ Verified
+
+ )}
+
+ {node.revenue}
-
+
@@ -997,13 +863,6 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
{filter}
))}
-
-
startExportSim('CSV')}
- className="px-3 py-1.5 bg-[#0f172a] text-white rounded-lg cursor-pointer hover:bg-zinc-805 border border-[#0f172a] transition-colors shadow-sm"
- >
- Export CSV
-
@@ -1016,15 +875,14 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
Product Name
SKU ID
Units Sold
-
Revenue
+
Overall Revenue
Stock Status
-
Trend index
{paginatedProducts.length === 0 ? (
-
+
No matching items matching stock filter criteria.
@@ -1076,33 +934,88 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
{prod.stockStatus}
-
-
- {prod.trend === 'up' ? (
-
- ) : prod.trend === 'down' ? (
-
- ) : (
-
- )}
-
-
{/* Expanded details row */}
- {isExpanded && (
-
-
-
- {/* Per-product stock & location breakdown has no live
- API ([R3]); the previously fabricated unit counts,
- hub split, bin code, audit date and barcode are
- replaced with the awaiting-backend placeholder. */}
-
-
-
-
- )}
+ {isExpanded && (() => {
+ const storeBreakdown = activeRegionStock.map(store => {
+ if (!store.rows) return null;
+ const rawProdId = prod.sku.replace('SKU-', '');
+ const row = store.rows.find(r =>
+ r.productid?.toString() === rawProdId ||
+ r.productname === prod.name
+ );
+ if (!row) return null;
+
+ const closing = Number(row.closing || 0);
+ const debit = Math.max(0, Number(row.debit || 0));
+ const retail = Number(row.retailprice) || Number(row.productcost) || 0;
+ const rev = debit * retail;
+
+ let status = 'Healthy';
+ if (closing <= 0) status = 'Critical';
+ else if (closing < 25) status = 'Critical';
+ else if (closing < 120) status = 'Low Stock';
+
+ return {
+ locationname: store.locationname || `Store ${store.locationid}`,
+ closing,
+ debit,
+ revenue: rev,
+ status
+ };
+ }).filter(Boolean);
+
+ return (
+
+
+
+
+
+ Store-by-Store Breakdown
+
+
+ {storeBreakdown.length === 0 ? (
+
No store data available for this product.
+ ) : (
+
+ {storeBreakdown.map((b: any, idx) => (
+
+
+
{b.locationname}
+
+ {b.status}
+
+
+
+
+
In Stock
+
{b.closing.toLocaleString()}
+
+
+
+
Units Sold
+
{b.debit.toLocaleString()}
+
+
+
+
Revenue
+
₹{b.revenue.toLocaleString()}
+
+
+
+ ))}
+
+ )}
+
+
+
+ );
+ })()}
);
})
diff --git a/src/components/SettingsView.tsx b/src/components/SettingsView.tsx
index a806fd8..be5b4e7 100644
--- a/src/components/SettingsView.tsx
+++ b/src/components/SettingsView.tsx
@@ -213,10 +213,7 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID, user }: Sett
{/* Header */}
-
Settings
-
- Manage your store profile, outlet hubs, delivery configurations, payments, and team preferences.
-
+
{tenantsQ.isLoading ? (
@@ -294,24 +291,12 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID, user }: Sett
Identity & Contacts
- {/* Logo / Identity Row */}
-
- {/* Visual Drag and Drop Logo Uploader Placeholder */}
-
-
+ {/* Identity Row */}
+
{user?.name || fstr(tenant?.tenantname) || 'Nearle Store'}
- — Set up your store logo, customer service email, and contact number. Official registration details are synced with your primary credentials.
+ — View your customer service email and contact number. Official registration details are synced with your primary credentials.
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx
index 221b808..3183b0d 100644
--- a/src/components/Sidebar.tsx
+++ b/src/components/Sidebar.tsx
@@ -34,7 +34,6 @@ export default function Sidebar({
// Navigation elements
const navItems = [
{ id: 'dashboard' as MainSection, label: 'Dashboard', icon: LayoutDashboard },
- { id: 'stores' as MainSection, label: 'Stores', icon: Store },
{ id: 'inventory' as MainSection, label: 'Products', icon: Layers },
{ id: 'reports' as MainSection, label: 'Reports', icon: TrendingUp },
{ id: 'settings' as MainSection, label: 'Settings', icon: Settings }
diff --git a/src/components/StoreCatalogView.tsx b/src/components/StoreCatalogView.tsx
index 19d2fea..60ee8eb 100644
--- a/src/components/StoreCatalogView.tsx
+++ b/src/components/StoreCatalogView.tsx
@@ -20,12 +20,14 @@
*/
import React, { useEffect, useMemo, useState } from 'react';
-import { Search, Boxes, Layers, Plus, Minus, Check, CheckCircle2, X, Store, PackageSearch } from 'lucide-react';
+import { Search, Boxes, Layers, Plus, Minus, Check, CheckCircle2, X, Store, PackageSearch, Activity } from 'lucide-react';
import { useFiestaStockStatement, FIESTA_TENANT_ID } from '../services/fiestaQueries';
import { num as fnum, str as fstr, type Row } from '../services/fiestaApi';
import { categoryName } from '../services/fiestaMappers';
import { useStoreCatalogue } from '../services/storeCatalogue';
import AwaitingApi from './AwaitingApi';
+import { SlideDrawer } from './consoleUi';
+import { useCompare } from '../contexts/CompareContext';
const PLACEHOLDER = 'https://images.unsplash.com/photo-1542838132-92c53300491e?auto=format&fit=crop&q=80&w=200';
@@ -52,10 +54,13 @@ function catBadgeClass(category: string): string {
}
export default function StoreCatalogView({ locationid, storeName = 'your store', tenantId = FIESTA_TENANT_ID }: StoreCatalogViewProps) {
+ const { selectedProducts, toggleProduct, setIsComparing } = useCompare();
const tenantid = tenantId;
const [view, setView] = useState<'catalogue' | 'inventory'>('catalogue');
const [search, setSearch] = useState('');
- const [category, setCategory] = useState('ALL');
+ const [selectedCategories, setSelectedCategories] = useState
([]);
+ const [stockHealthFilter, setStockHealthFilter] = useState([]);
+ const [selectedProduct, setSelectedProduct] = useState(null);
const [notice, setNotice] = useState(false);
// The admin-curated catalogue (what the user is allowed to pick from).
@@ -107,7 +112,7 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
const inventory = useMemo(
() =>
(stockQ.data ?? []).map((r: Row) => {
- const closing = fnum(r.closing);
+ const closing = fnum(r.closing) ?? 0;
return {
id: fstr(r.productid),
name: fstr(r.productname) || 'Unnamed product',
@@ -126,15 +131,33 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
return inventory.filter((it) => it.name.toLowerCase().includes(term) || it.category.toLowerCase().includes(term) || it.id.toLowerCase().includes(term));
}, [inventory, search]);
- const categories = useMemo(() => [...new Set(products.map((p) => p.category))].sort(), [products]);
+ const categories = useMemo(() => [...new Set(products.map((p) => p.category.split(' / ')[0]))].sort(), [products]);
+
const filtered = useMemo(() => {
const term = search.toLowerCase();
return products.filter((p) => {
- if (category !== 'ALL' && p.category !== category) return false;
+ const pCat = p.category.split(' / ')[0];
+ if (selectedCategories.length > 0 && !selectedCategories.includes(pCat)) return false;
if (!term) return true;
return p.name.toLowerCase().includes(term) || p.category.toLowerCase().includes(term) || p.id.toLowerCase().includes(term);
});
- }, [products, search, category]);
+ }, [products, search, selectedCategories]);
+
+ const toggleCategory = (cat: string) => {
+ setSelectedCategories(prev => prev.includes(cat) ? prev.filter(c => c !== cat) : [...prev, cat]);
+ };
+
+ const toggleStockHealth = (health: string) => {
+ setStockHealthFilter(prev => prev.includes(health) ? prev.filter(h => h !== health) : [...prev, health]);
+ };
+
+ const finalFilteredInventory = useMemo(() => {
+ return filteredInventory.filter(it => {
+ if (selectedCategories.length > 0 && !selectedCategories.includes(it.category.split(' / ')[0])) return false;
+ if (stockHealthFilter.length > 0 && !stockHealthFilter.includes(it.label)) return false;
+ return true;
+ });
+ }, [filteredInventory, selectedCategories, stockHealthFilter]);
// ── Integration point ──────────────────────────────────────────────────────────
// Replace with the real request/stock POST (selected productids + quantities),
@@ -143,13 +166,7 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
return (
- {/* Header */}
-
-
Product Catalogue
-
- Products your admin published for {storeName} — choose what you need and set quantities.
-
-
+
{/* Tabs */}
@@ -171,231 +188,417 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
- {/* Filters */}
-
-
-
-
setSearch(e.target.value)}
- className="w-full pl-9 pr-9 py-2.5 border border-[#e2e8f0] rounded-xl text-xs outline-none bg-[#f8fafc] focus:bg-white focus:ring-1 focus:ring-[#662582] transition-all"
- />
- {search && (
-
setSearch('')} className="absolute right-3 top-1/2 -translate-y-1/2 text-zinc-400 hover:text-zinc-600">
+
+ {/* Sticky Sidebar Filter */}
+
+
+
+ Search
+
+
+ setSearch(e.target.value)}
+ className="w-full pl-3 pr-8 py-2.5 border border-[#e2e8f0] rounded-xl text-xs font-semibold outline-none bg-slate-50 focus:bg-white focus:ring-2 focus:ring-purple-500/20 focus:border-purple-500 transition-all shadow-sm"
+ />
+ {search && (
+ setSearch('')} className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 cursor-pointer">
+
+
+ )}
+
+
+
+
+
+
+
+ Categories
+
+
+ {categories.map((c) => (
+
+ toggleCategory(c)}
+ className="sr-only"
+ />
+
+ {selectedCategories.includes(c) && }
+
+ {c}
+
+ ))}
+
+
+
+ {view === 'inventory' && (
+ <>
+
+
+
+ Stock Health
+
+
+ {['Healthy', 'Low', 'Critical', 'Out of stock'].map((h) => (
+
+ toggleStockHealth(h)}
+ className="sr-only"
+ />
+
+ {stockHealthFilter.includes(h) && }
+
+ {h}
+
+ ))}
+
+
+ >
+ )}
+
+ {(selectedCategories.length > 0 || stockHealthFilter.length > 0 || search) && (
+
{ setSelectedCategories([]); setStockHealthFilter([]); setSearch(''); }}
+ className="mt-2 w-full py-2 bg-slate-100 text-slate-600 rounded-xl text-[10px] font-bold uppercase tracking-wider hover:bg-slate-200 transition-colors cursor-pointer"
+ >
+ Clear All Filters
+
)}
- {view === 'catalogue' && categories.length > 0 && (
-
-
Filter
-
setCategory(e.target.value)}
- className="border border-[#e2e8f0] rounded-lg px-3 py-2 text-xs font-semibold text-zinc-700 bg-[#f8fafc] outline-none focus:ring-1 focus:ring-[#662582] cursor-pointer"
- >
- All categories
- {categories.map((c) => {c} )}
-
+
+ {/* Product Grid Area */}
+
+ {/* Mobile Search Input (Visible only on mobile) */}
+
- )}
-
- {view === 'catalogue' ? `${filtered.length} products` : `${inventory.length} stocked`}
+
+ {/* Results Summary & Toolbar */}
+
+
+
+ {view === 'catalogue' ? 'Global Catalogue' : 'My Inventory'}
+
+
+ Showing {view === 'catalogue' ? filtered.length : finalFilteredInventory.length} results
+
+
+ {/* Can add sorting dropdown here if needed */}
+
+
+ {/* ── Browse Catalogue ── */}
+ {view === 'catalogue' && (
+ products.length === 0 ? (
+
}
+ title="No products published yet"
+ sub="Your admin hasn't added any products to the catalogue. Once they do, they'll appear here automatically for you to select."
+ />
+ ) : filtered.length === 0 ? (
+
}
+ title="No products match your search"
+ sub="Try a different keyword or clear the filters to see the full catalogue."
+ />
+ ) : (
+
+ {filtered.map((p) => {
+ const stocked = inStore.has(p.id);
+ const picked = picks[p.id] != null;
+ return (
+
setSelectedProduct(p)} className="bg-white border border-slate-200 rounded-2xl flex flex-col shadow-sm hover:shadow-xl hover:border-purple-300 hover:-translate-y-1.5 transition-all duration-300 relative group cursor-pointer">
+ {/* Image Bento Block */}
+
+
+
+
+ {stocked && (
+
+
+ In Store
+
+ )}
+
+
+ {p.category.split(' / ')[0]}
+
+
+
+
+
+ {/* Details Bento Block */}
+
+
+
+
{
+ e.stopPropagation();
+ toggleProduct({
+ id: p.id,
+ name: p.name,
+ sku: p.sku,
+ category: p.category.split(' / ')[0],
+ price: p.price,
+ image: p.image,
+ unit: p.unit
+ });
+ }}
+ >
+
+ String(sp.id) === String(p.id))}
+ readOnly
+ />
+
+
+
+
+
+
+
+ Price
+ {p.price > 0 ? `₹${p.price.toLocaleString('en-IN')}` : '—'}
+
+
+ Unit
+ {p.unit || 'Pc'}
+
+
+
+ {/* Action Bento Block */}
+
+ {picked ? (
+
+
+
+
{ e.stopPropagation(); setPickQty(p.id, picks[p.id] - 1); }} className="w-6 h-6 rounded-md bg-slate-50 hover:bg-slate-100 text-slate-600 font-bold flex items-center justify-center transition-colors">
+
{picks[p.id]}
+
{ e.stopPropagation(); setPickQty(p.id, picks[p.id] + 1); }} className="w-6 h-6 rounded-md bg-slate-50 hover:bg-slate-100 text-slate-600 font-bold flex items-center justify-center transition-colors">
+
+
{ e.stopPropagation(); togglePick(p.id); }} title="Remove" className="w-7 h-7 rounded-lg text-rose-500 hover:bg-rose-50 hover:text-rose-600 bg-white border border-rose-100 flex items-center justify-center transition-colors shadow-sm ml-1">
+
+ ) : (
+
{ e.stopPropagation(); togglePick(p.id); }}
+ className="w-full flex items-center justify-center gap-2 py-2.5 rounded-xl text-[11px] font-bold transition-all bg-white text-purple-700 hover:bg-purple-600 hover:text-white hover:shadow-md border border-purple-200 hover:border-purple-600"
+ >
+ Add to Store
+
+ )}
+
+
+
+ );
+ })}
+
+ )
+ )}
+
+ {/* ── My Store Inventory ── */}
+ {view === 'inventory' && (
+ stockQ.isLoading ? (
+
} title="Loading your stock…" sub="Fetching the latest stock levels for your store." />
+ ) : !locationid ? (
+
} title="No store linked yet" sub="Your account isn't linked to a store outlet, so there's no inventory to show." />
+ ) : inventory.length === 0 ? (
+
} title="No products stocked yet" sub="Add products from the catalogue and they'll appear here with live stock levels." />
+ ) : finalFilteredInventory.length === 0 ? (
+
}
+ title="No stock matches your filters"
+ sub="Try a different keyword or clear the sidebar filters to find an item in your store."
+ action={
+
{ setSearch(''); setSelectedCategories([]); setStockHealthFilter([]); }} className="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl text-xs font-bold text-white bg-[#662582] hover:bg-purple-800 transition shadow-sm cursor-pointer">
+ Clear filters
+
+ }
+ />
+ ) : (
+
+ {finalFilteredInventory.map((it, i) => (
+
setSelectedProduct(it)} className="bg-white border border-slate-200 rounded-2xl flex flex-col shadow-sm hover:shadow-xl hover:border-purple-300 hover:-translate-y-1.5 transition-all duration-300 relative group cursor-pointer">
+ {/* Image Bento Block */}
+
+
+
+
+
+
+ {it.label}
+
+
+
+ {it.category.split(' / ')[0]}
+
+
+
+
+
+ {/* Details Bento Block */}
+
+
+
+
{
+ e.stopPropagation();
+ toggleProduct({
+ id: it.id,
+ name: it.name,
+ sku: it.sku,
+ category: it.category.split(' / ')[0],
+ price: it.price,
+ image: it.image,
+ closing: it.closing,
+ label: it.label,
+ color: it.color,
+ unit: it.unit
+ });
+ }}
+ >
+
+ String(sp.id) === String(it.id))}
+ readOnly
+ />
+
+
+
+
+
+
+ Live Stock
+ {(it.closing ?? 0).toLocaleString('en-IN')}
+
+
+
+ ))}
+
+ )
+ )}
- {/* ── Browse Catalogue ── */}
- {view === 'catalogue' && (
- products.length === 0 ? (
-
}
- title="No products published yet"
- sub="Your admin hasn't added any products to the catalogue. Once they do, they'll appear here automatically for you to select."
- />
- ) : filtered.length === 0 ? (
-
}
- title="No products match your search"
- sub="Try a different keyword or clear the filters to see the full catalogue."
- action={
-
{ setSearch(''); setCategory('ALL'); }}
- className="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl text-xs font-bold text-white bg-[#662582] hover:bg-purple-800 transition shadow-sm cursor-pointer"
- >
- Clear filters
-
- }
- />
- ) : (
-
- {filtered.map((p) => {
- const stocked = inStore.has(p.id);
- const picked = picks[p.id] != null;
- return (
-
-
- {/* Thumbnail with hover zoom */}
-
-
- {stocked && (
-
- )}
-
-
-
-
-
{p.name}
- {p.sku}
-
- {/* Category pill badge */}
-
- {p.category.split(' / ')[0]}
-
-
-
-
-
- Price
- {p.price > 0 ? `₹${p.price.toLocaleString('en-IN')}` : '—'}
-
-
-
-
-
- {/* Stocked-status row (mirrors the admin card's status line) */}
-
-
-
- {stocked ? 'In Your Store' : 'Not stocked yet'}
-
- {p.unit && {p.unit} }
-
-
- {/* Pick action: quantity stepper when selected, else add button */}
- {picked ? (
-
-
Selected
-
-
setPickQty(p.id, picks[p.id] - 1)} className="w-6 h-6 rounded-lg border border-[#e2e8f0] text-zinc-500 hover:bg-zinc-50 font-bold cursor-pointer leading-none flex items-center justify-center">
-
{picks[p.id]}
-
setPickQty(p.id, picks[p.id] + 1)} className="w-6 h-6 rounded-lg border border-[#e2e8f0] text-zinc-500 hover:bg-zinc-50 font-bold cursor-pointer leading-none flex items-center justify-center">
-
togglePick(p.id)} title="Remove" className="ml-1 w-6 h-6 rounded-lg text-rose-500 hover:bg-rose-50 flex items-center justify-center cursor-pointer">
-
-
- ) : (
-
togglePick(p.id)}
- className="w-full flex items-center justify-center gap-1.5 pt-2.5 mt-1 border-t border-[#f1f5f9] text-[11px] font-bold text-[#662582] hover:text-purple-800 cursor-pointer"
- >
- Add to Store
-
- )}
-
- );
- })}
-
- )
- )}
-
- {/* ── My Store Inventory ── (card grid — same design as Browse Catalogue) */}
- {view === 'inventory' && (
- stockQ.isLoading ? (
-
} title="Loading your stock…" sub="Fetching the latest stock levels for your store." />
- ) : !locationid ? (
-
} title="No store linked yet" sub="Your account isn't linked to a store outlet, so there's no inventory to show." />
- ) : inventory.length === 0 ? (
-
} title="No products stocked yet" sub="Add products from the catalogue and they'll appear here with live stock levels." />
- ) : filteredInventory.length === 0 ? (
-
}
- title="No stock matches your search"
- sub="Try a different keyword to find an item in your store."
- action={
-
setSearch('')} className="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl text-xs font-bold text-white bg-[#662582] hover:bg-purple-800 transition shadow-sm cursor-pointer">
- Clear search
-
- }
- />
- ) : (
-
- {filteredInventory.map((it, i) => (
-
-
- {/* Thumbnail with status corner dot */}
-
-
-
-
-
-
-
-
{it.name}
- {it.sku}
-
-
- {it.category.split(' / ')[0]}
-
-
-
-
-
- In Stock
- {it.closing.toLocaleString('en-IN')}
-
-
- Status
- {it.label}
-
-
-
-
-
- {/* Status line (mirrors the catalogue card's footer) */}
-
-
-
- {it.label}
-
- {it.category.split(' / ')[0]}
-
-
- ))}
-
- )
- )}
-
- {/* ── Selection action bar ── */}
+ {/* ── Selection FAB ── */}
{view === 'catalogue' && pickCount > 0 && (
-
-
- {notice ? (
-
-
- {pickCount} product{pickCount > 1 ? 's' : ''} requested for {storeName}
- { setPicks({}); setNotice(false); }} className="text-[11px] font-semibold text-purple-200 hover:text-white cursor-pointer">Clear
-
-
+
+ {notice ? (
+
+
+ {pickCount} product{pickCount > 1 ? 's' : ''} requested
+ { setPicks({}); setNotice(false); }} className="text-[11px] font-semibold text-purple-200 hover:text-white cursor-pointer">Clear
- ) : (
-
-
-
-
-
{pickCount} product{pickCount > 1 ? 's' : ''} · {Object.values(picks).reduce((a: number, b: number) => a + b, 0)} units
-
Selected for {storeName}
-
-
-
- setPicks({})} className="px-3 py-2 rounded-xl text-[11px] font-bold text-purple-200 hover:text-white hover:bg-white/10 transition cursor-pointer">Clear
-
- Request for Store
-
-
+
+
+ ) : (
+
+
+ {pickCount}
- )}
-
+
+ Save Picks
+ {Object.values(picks).reduce((a: number, b: number) => a + b, 0)} items total
+
+
+
+
+
+ )}
)}
+
+ {/* ADD/EDIT PRODUCT MODAL (simulated) */}
+
setSelectedProduct(null)}
+ title={view === 'catalogue' ? 'Catalogue Product Details' : 'Store Inventory Status'}
+ >
+ {selectedProduct && (
+
+
+
+
+
+ {selectedProduct.category.split(' / ')[0]}
+
+
+
+
+
+
{selectedProduct.name}
+
{selectedProduct.sku}
+
+
+
+
+ Pricing
+
+ {selectedProduct.price > 0 ? `₹${selectedProduct.price.toLocaleString('en-IN')}` : '—'}
+
+
+
+ Unit
+ {selectedProduct.unit || 'Piece'}
+
+
+
+ {selectedProduct.closing !== undefined && (
+
+
+
+
+
+
+ Live Stock Level
+
+ {selectedProduct.label}
+
+
+
+
{selectedProduct.closing.toLocaleString('en-IN')}
+
+ )}
+
+
+ This is a shared product available in the Global Catalogue. Make sure to keep adequate stock to prevent customer order cancellations due to unavailability.
+
+
+ )}
+
);
}
@@ -416,28 +619,11 @@ function CenterState({ icon, title, sub, action }: { icon: React.ReactNode; titl
-
{title}
- {sub &&
{sub}
}
+
{title}
+ {sub &&
{sub}
}
{action &&
{action}
}
- {/* Ghost preview cards — hint at what will appear here */}
-
- {[0, 1, 2].map((i) => (
-
- ))}
-
-
-
- Syncs automatically
-
);
diff --git a/src/components/StoreDetailView.tsx b/src/components/StoreDetailView.tsx
index badba9b..032133c 100644
--- a/src/components/StoreDetailView.tsx
+++ b/src/components/StoreDetailView.tsx
@@ -389,7 +389,7 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
className="flex items-center gap-xs text-xs font-bold text-[#662582] hover:text-[#4c1d95] bg-purple-50 hover:bg-purple-100/80 px-xl py-2 rounded-lg transition-all shadow-sm border border-purple-100 cursor-pointer"
>
-
Back to Registry
+
Back to Dashboard
)}
diff --git a/src/components/TrialBatchDrawer.tsx b/src/components/TrialBatchDrawer.tsx
new file mode 100644
index 0000000..cf1f1b1
--- /dev/null
+++ b/src/components/TrialBatchDrawer.tsx
@@ -0,0 +1,234 @@
+import React, { useState, useEffect } from 'react';
+import { Package, MapPin, Truck, ChevronDown, Check, X } from 'lucide-react';
+import { useFiestaTenantLocations } from '../services/fiestaQueries';
+import { str as fstr } from '../services/fiestaApi';
+
+export interface TrialProduct {
+ id: string | number;
+ name: string;
+ sku: string;
+ category: string;
+ price: number;
+ image: string;
+}
+
+interface TrialBatchDrawerProps {
+ isOpen: boolean;
+ onClose: () => void;
+ product: TrialProduct | null;
+ tenantId?: number;
+}
+
+export default function TrialBatchDrawer({ isOpen, onClose, product, tenantId }: TrialBatchDrawerProps) {
+ const [qty, setQty] = useState(5);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [isSuccess, setIsSuccess] = useState(false);
+ const [selectedLocationIds, setSelectedLocationIds] = useState
([]);
+
+ const locationsQ = useFiestaTenantLocations(tenantId);
+ const stores = locationsQ.data || [];
+
+ // Reset state when drawer opens with a new product
+ useEffect(() => {
+ if (isOpen) {
+ setQty(5);
+ setIsSubmitting(false);
+ setIsSuccess(false);
+ if (stores.length > 0 && selectedLocationIds.length === 0) {
+ setSelectedLocationIds([String(stores[0].locationid)]);
+ }
+ }
+ }, [isOpen, product, stores]);
+
+ if (!isOpen || !product) return null;
+
+ const sampleDiscount = 0.5; // 50% discount for sample
+ const baseCost = qty * (product.price || 0) * Math.max(1, selectedLocationIds.length);
+ const discountedCost = baseCost * (1 - sampleDiscount);
+
+ const handleSubmit = () => {
+ if (selectedLocationIds.length === 0) {
+ alert("Please select at least one store delivery hub.");
+ return;
+ }
+ setIsSubmitting(true);
+ // Simulate API call
+ setTimeout(() => {
+ setIsSubmitting(false);
+ setIsSuccess(true);
+ // Auto close after showing success
+ setTimeout(() => {
+ onClose();
+ }, 2000);
+ }, 1500);
+ };
+
+ return (
+
+ {/* Backdrop */}
+
+
+ {/* Drawer */}
+
+
+ {/* Header */}
+
+
+ {/* Scrollable Content */}
+
+
+ {/* Product Preview Card */}
+
+
+
+
+
+
+ {product.category}
+
+
{product.name}
+
{product.sku}
+
+
+
+ {/* Form */}
+
+
+ {/* Quantity Selector */}
+
+
Sample Quantity
+
+ {[1, 5, 10].map((amount) => (
+ setQty(amount)}
+ className={`flex-1 py-2.5 rounded-xl font-bold text-sm transition-all border ${
+ qty === amount
+ ? 'bg-indigo-50 border-indigo-200 text-indigo-700 ring-1 ring-indigo-500/20'
+ : 'bg-white border-slate-200 text-slate-600 hover:border-indigo-300'
+ }`}
+ >
+ {amount} units
+
+ ))}
+
+
+
+ {/* Delivery Locations (Multi-Select) */}
+
+
Delivery Hubs (Select Multiple)
+
+
+ {locationsQ.isLoading ? (
+
Loading stores...
+ ) : stores.length > 0 ? (
+ stores.map((store) => {
+ const sid = String(store.locationid);
+ const isSelected = selectedLocationIds.includes(sid);
+ return (
+
{
+ if (isSelected) {
+ setSelectedLocationIds(selectedLocationIds.filter(id => id !== sid));
+ } else {
+ setSelectedLocationIds([...selectedLocationIds, sid]);
+ }
+ }}
+ className={`flex items-center gap-3 p-3 rounded-xl border cursor-pointer transition-all ${
+ isSelected
+ ? 'bg-indigo-50 border-indigo-200 shadow-sm'
+ : 'bg-white border-slate-200 hover:border-indigo-300'
+ }`}
+ >
+
+ {isSelected && }
+
+
+
+ {fstr(store.locationname)}
+
+
+ {fstr(store.suburb) || fstr(store.city) || 'Main Outlet'}
+
+
+
+ );
+ })
+ ) : (
+
No stores found
+ )}
+
+
+
+
+
+
+
+ {/* Footer actions */}
+
+
+
+
+ Base Price ({qty} × ₹{product.price || 0})
+ ₹{baseCost.toLocaleString('en-IN')}
+
+
+ Sample Discount (50%)
+ -₹{(baseCost * sampleDiscount).toLocaleString('en-IN')}
+
+
+ Total Est. Cost
+ ₹{discountedCost.toLocaleString('en-IN')}
+
+
+
+
+ {isSubmitting ? (
+
+
+ Processing...
+
+ ) : isSuccess ? (
+
+
+ Request Sent
+
+ ) : (
+ <>
+ Request Trial Batch
+ >
+ )}
+
+
+
+
+
+ );
+}
diff --git a/src/components/UserStorePage.tsx b/src/components/UserStorePage.tsx
index 284e757..1e12813 100644
--- a/src/components/UserStorePage.tsx
+++ b/src/components/UserStorePage.tsx
@@ -34,6 +34,7 @@ import DispatchHubView from './DispatchHubView';
import DeliveryReportsView from './DeliveryReportsView';
import StoreQRView from './StoreQRView';
import UserStoreSidebar, { type UserNavItem } from './UserStoreSidebar';
+import ComparisonModal from './ComparisonModal';
interface UserStorePageProps {
/** Returns to the login screen. */
@@ -352,6 +353,8 @@ export default function UserStorePage({ onLogout, user }: UserStorePageProps) {
,
document.body,
)}
+
+
);
}
diff --git a/src/components/UsersPanel.tsx b/src/components/UsersPanel.tsx
index cb30402..ce4ffee 100644
--- a/src/components/UsersPanel.tsx
+++ b/src/components/UsersPanel.tsx
@@ -29,7 +29,7 @@ import {
Bike,
Clock
} from 'lucide-react';
-import { useFiestaUsers, useFiestaCreateUser, useFiestaRiderShifts, useFiestaTenantLocations } from '../services/fiestaQueries';
+import { useFiestaUsers, useFiestaCreateUser, useFiestaUpdateUser, useFiestaRiderShifts, useFiestaTenantLocations } from '../services/fiestaQueries';
import { useAppRoles } from '../services/queries';
import { FIESTA_TENANT_ID, str as fstr, num as fnum, roleName } from '../services/fiestaApi';
import AddressAutocomplete, { type AddressResult } from './AddressAutocomplete';
@@ -76,8 +76,30 @@ const FALLBACK_ROLE_CHOICES = [
export default function UsersPanel({ tenantId = FIESTA_TENANT_ID, defaultNewUserRole = 4 }: UsersPanelProps) {
const usersQ = useFiestaUsers({ tenantid: tenantId, pagesize: 100 });
const createUserMut = useFiestaCreateUser();
+ const updateUserMut = useFiestaUpdateUser();
const rolesQ = useAppRoles();
+ const [assignUserId, setAssignUserId] = useState
(null);
+ const [assignLocationId, setAssignLocationId] = useState(0);
+ const [assignRoleId, setAssignRoleId] = useState(4);
+
+ const handleToggleAssign = async (userid: number, currentRoleId: number) => {
+ try {
+ if (currentRoleId <= 0) {
+ // Open the assign modal
+ setAssignUserId(userid);
+ if (storeOptions.length === 1) {
+ setAssignLocationId(storeOptions[0].locationid);
+ }
+ } else {
+ // Unassign immediately. We use -1 because the Go backend ignores 0 (zero value) during updates.
+ await updateUserMut.mutateAsync({ userid, roleid: -1, locationid: -1, applocationid: -1 });
+ }
+ } catch (err) {
+ alert(`Could not update user role: ${err instanceof Error ? err.message : 'Unknown error'}`);
+ }
+ };
+
// Selectable roles for the Add User modal — driven by the live app-roles API,
// matched to local icon/desc styling by roleid; falls back to the static list.
// Selectable roles for the Add User modal - limited to Staff and Rider.
@@ -130,7 +152,10 @@ export default function UsersPanel({ tenantId = FIESTA_TENANT_ID, defaultNewUser
if (!newUser.locationid && storeOptions.length === 1) {
setNewUser((u) => ({ ...u, locationid: storeOptions[0].locationid, applocationid: storeOptions[0].applocationid }));
}
- }, [storeOptions.length, newUser.locationid]);
+ if (!assignLocationId && storeOptions.length === 1) {
+ setAssignLocationId(storeOptions[0].locationid);
+ }
+ }, [storeOptions.length, newUser.locationid, assignLocationId]);
// Rider-shift picker — shown only when a rider role is selected (parity with the
// merchant_web create form). Shifts come from the live partners/getridershifts.
@@ -189,6 +214,7 @@ export default function UsersPanel({ tenantId = FIESTA_TENANT_ID, defaultNewUser
const roleOptions = React.useMemo(() => {
const roles = new Set(users.map((u) => u.role));
+ roles.add('Admin');
roles.add('Rider');
return Array.from(roles);
}, [users]);
@@ -280,25 +306,25 @@ export default function UsersPanel({ tenantId = FIESTA_TENANT_ID, defaultNewUser
{/* Search & Filter Utility Bar */}
-
-
-
-
-
+
+
+
+
+
setSearch(e.target.value)}
- className="w-full pl-3 pr-12 py-3 bg-transparent border-none text-sm font-semibold text-slate-800 placeholder-slate-400 focus:outline-none outline-none"
+ className="w-full pl-3 pr-10 py-2.5 bg-transparent border-none text-sm font-semibold text-slate-800 placeholder-slate-400 focus:outline-none outline-none"
/>
{search && (
setSearch('')}
- className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-700 hover:bg-slate-200/50 active:scale-95 transition-all p-1.5 rounded-lg flex items-center justify-center"
+ className="absolute right-1 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-700 active:scale-95 transition-all p-1.5 flex items-center justify-center"
>
-
+
)}
@@ -333,7 +359,7 @@ export default function UsersPanel({ tenantId = FIESTA_TENANT_ID, defaultNewUser
{/* Directory Grid */}
-
+
{filteredUsers.length === 0 ? (
{usersQ.isLoading ? 'Loading team list…' : 'No matches found in team list.'}
@@ -342,62 +368,77 @@ export default function UsersPanel({ tenantId = FIESTA_TENANT_ID, defaultNewUser
filteredUsers.map((u) => {
const roleInfo = ROLE_THEMES[u.roleid] || { bg: 'bg-slate-55', text: 'text-slate-700', border: 'border-slate-100', label: u.role };
return (
-
+
{/* Background aura gradient effect on hover */}
-
+
-
+
{/* User Avatar with status indicator ring */}
-
{u.name}
-
{u.email}
+
{u.email}
{/* Metadata fields */}
-
-
-
+
+
-
-
+
+
{u.location}
{u.shift && u.shift !== '—' && (
-
-
Shift
-
{u.shift}
+
+ Shift
+ {u.shift}
)}
-
-
- {u.roleid === 1 && }
- {u.roleid === 2 && }
- {u.roleid === 3 && }
- {u.roleid === 4 && }
- {u.roleid === 5 && }
- {u.roleid === 6 && }
- {roleInfo.label}
-
+
+
+
+ {u.roleid === 1 && }
+ {u.roleid === 2 && }
+ {u.roleid === 3 && }
+ {u.roleid === 4 && }
+ {u.roleid === 5 && }
+ {u.roleid === 6 && }
+ {roleInfo.label}
+
+ {u.roleid !== 1 && (
+ handleToggleAssign(u.userid, u.roleid)}
+ disabled={updateUserMut.isPending}
+ className={`px-2 py-0.5 rounded-[8px] text-[9px] font-extrabold uppercase transition-colors cursor-pointer border ${
+ u.roleid <= 0
+ ? 'bg-emerald-50 text-emerald-700 border-emerald-200 hover:bg-emerald-100'
+ : 'bg-slate-50 text-slate-500 border-slate-200 hover:bg-slate-100 hover:text-slate-700'
+ } ${updateUserMut.isPending ? 'opacity-50 cursor-not-allowed' : ''}`}
+ >
+ {u.roleid <= 0 ? 'Assign' : 'Unassign'}
+
+ )}
+
-
ID: #{u.userid}
+
#{u.userid}
);
@@ -615,6 +656,99 @@ export default function UsersPanel({ tenantId = FIESTA_TENANT_ID, defaultNewUser
)}
+
+ {/* ASSIGN USER MODAL */}
+ {assignUserId !== null && (
+
{ if (e.target === e.currentTarget) setAssignUserId(null); }}
+ >
+
+
+
+
+
+
+ Assign Team Member
+
+
setAssignUserId(null)}
+ className="p-1.5 hover:bg-slate-100 rounded-lg text-slate-400 hover:text-slate-600 cursor-pointer transition-colors"
+ >
+
+
+
+
+
+
+
+ )}
);
}
diff --git a/src/components/consoleUi.tsx b/src/components/consoleUi.tsx
index 2cfa47c..ec05660 100644
--- a/src/components/consoleUi.tsx
+++ b/src/components/consoleUi.tsx
@@ -15,7 +15,8 @@
* source's MUI `sx`) while layout/spacing use Tailwind.
*/
-import React from 'react';
+import React, { useEffect } from 'react';
+import { X, Info } from 'lucide-react';
// ── Design tokens ────────────────────────────────────────────────────────────────
export const BRAND = '#662582';
@@ -141,32 +142,28 @@ export function KpiStrip({ items, loading }: { items: KpiItem[]; loading?: boole
{ if (!it.active && it.onClick) e.currentTarget.style.boxShadow = SHADOW_MD; }}
- onMouseLeave={(e) => { if (!it.active) e.currentTarget.style.boxShadow = 'none'; }}
+ className={`rounded-xl p-3 flex flex-col justify-between transition-all duration-300 text-left relative group border bg-white ${it.active ? 'border-indigo-500 opacity-100 shadow-[0_8px_24px_rgba(99,102,241,0.06)]' : 'border-slate-200/70 opacity-70 hover:opacity-100'} ${it.onClick ? 'cursor-pointer' : ''}`}
>
-
-
-
-
- {it.label}
-
-
- {loading ? '—' : it.value}
-
- {it.badge && (
-
- {it.badge}
-
- )}
-
-
- {it.icon}
+
+
+ {it.label}
+
+ {React.cloneElement(it.icon as React.ReactElement, { className: 'w-3.5 h-3.5' })}
+
+
+
+
+ {loading ? '—' : it.value}
+
+ {it.badge && (
+
+ {it.badge}
+
+ )}
))}
@@ -328,3 +325,91 @@ export function TableShell({ head, children, minWidth, footer }: { head: string[
);
}
+
+// ── Spinny UX Components ─────────────────────────────────────────────────────────
+
+/**
+ * Beautiful Skeleton Loader for empty/loading states.
+ * Uses a pulsing animation to match shape before data arrives.
+ */
+export function Skeleton({ className = '', style }: { className?: string; style?: React.CSSProperties }) {
+ return (
+
+ );
+}
+
+/**
+ * Interactive Tooltip (Jargon Buster).
+ * Wraps children and shows a sleek dark explanation box on hover.
+ */
+export function Tooltip({ children, content, position = 'top' }: { children: React.ReactNode; content: React.ReactNode; position?: 'top' | 'bottom' }) {
+ return (
+
+ );
+}
+
+/**
+ * "Quick Look" Slide-out Drawer.
+ * Slides in from the right edge of the screen.
+ */
+export function SlideDrawer({
+ isOpen,
+ onClose,
+ title,
+ children
+}: {
+ isOpen: boolean;
+ onClose: () => void;
+ title: string;
+ children: React.ReactNode;
+}) {
+
+ // Prevent body scroll when open
+ useEffect(() => {
+ if (isOpen) document.body.style.overflow = 'hidden';
+ else document.body.style.overflow = '';
+ return () => { document.body.style.overflow = ''; };
+ }, [isOpen]);
+
+ if (!isOpen) return null;
+
+ return (
+
+ {/* Backdrop */}
+
+
+ {/* Drawer Panel */}
+
+ {/* Header */}
+
+
{title}
+
+
+
+
+
+ {/* Scrollable Content */}
+
+ {children}
+
+
+
+ );
+}
diff --git a/src/contexts/CompareContext.tsx b/src/contexts/CompareContext.tsx
new file mode 100644
index 0000000..7bc6ce5
--- /dev/null
+++ b/src/contexts/CompareContext.tsx
@@ -0,0 +1,84 @@
+import React, { createContext, useContext, useState, ReactNode } from 'react';
+
+export interface CompareProduct {
+ id: string | number;
+ name: string;
+ sku: string;
+ category: string;
+ price: number;
+ image: string;
+ unitsSold?: number;
+ // Admin-specific
+ verified?: boolean;
+ // User-specific
+ closing?: number;
+ unit?: string;
+ color?: string;
+ label?: string;
+}
+
+interface CompareContextType {
+ selectedProducts: CompareProduct[];
+ toggleProduct: (product: CompareProduct) => void;
+ removeProduct: (id: string | number) => void;
+ clearSelection: () => void;
+ isComparing: boolean;
+ setIsComparing: (val: boolean) => void;
+ hideCompareBar: boolean;
+ setHideCompareBar: (val: boolean) => void;
+}
+
+const CompareContext = createContext
(undefined);
+
+export function CompareProvider({ children }: { children: ReactNode }) {
+ const [selectedProducts, setSelectedProducts] = useState([]);
+ const [isComparing, setIsComparing] = useState(false);
+ const [hideCompareBar, setHideCompareBar] = useState(false);
+
+ const toggleProduct = (product: CompareProduct) => {
+ setSelectedProducts((prev) => {
+ const exists = prev.find((p) => String(p.id) === String(product.id));
+ if (exists) {
+ return prev.filter((p) => String(p.id) !== String(product.id));
+ }
+ if (prev.length >= 5) {
+ return prev; // Max 5 items
+ }
+ return [...prev, product];
+ });
+ };
+
+ const removeProduct = (id: string | number) => {
+ setSelectedProducts((prev) => prev.filter((p) => String(p.id) !== String(id)));
+ };
+
+ const clearSelection = () => {
+ setSelectedProducts([]);
+ setIsComparing(false);
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useCompare() {
+ const context = useContext(CompareContext);
+ if (!context) {
+ throw new Error('useCompare must be used within a CompareProvider');
+ }
+ return context;
+}
diff --git a/src/index.css b/src/index.css
index e68d054..1ddf05f 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1,8 +1,8 @@
-@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap');
+@import url('https://fonts.googleapis.com/css2?family=Jost:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;700&display=swap');
@import "tailwindcss";
@theme {
- --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
+ --font-sans: "Jost", ui-sans-serif, system-ui, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, monospace;
--spacing-xs: 4px;
diff --git a/src/main.tsx b/src/main.tsx
index 8a8c3ea..b288aec 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -3,6 +3,7 @@ import {createRoot} from 'react-dom/client';
import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
import App from './App.tsx';
import './index.css';
+import { CompareProvider } from './contexts/CompareContext.tsx';
// How often every page silently re-syncs with the backend. Orders/deliveries
// statuses change out-of-band (riders accept/pick/deliver, customers place
@@ -35,7 +36,9 @@ const queryClient = new QueryClient({
createRoot(document.getElementById('root')!).render(
-
+
+
+
,
);
diff --git a/src/services/fiestaApi.ts b/src/services/fiestaApi.ts
index 28b7d93..edfe607 100644
--- a/src/services/fiestaApi.ts
+++ b/src/services/fiestaApi.ts
@@ -809,6 +809,7 @@ export async function getProductSubcategories(opts: {
/** Best-effort role label from the numeric roleid (roles aren't fully resolvable for every config). */
export function roleName(roleid: number): string {
const map: Record = {
+ [-1]: 'Unassigned',
0: 'Unassigned',
1: 'Owner',
2: 'Manager',
@@ -914,6 +915,9 @@ export interface UpdateUserInput {
state?: string;
postcode?: string;
status?: string;
+ roleid?: number | null;
+ locationid?: number | null;
+ applocationid?: number | null;
}
/** PUT /users/update — update an existing web staff user. */