From 87070044050337ffe59155faff5b2e0852a9d78c Mon Sep 17 00:00:00 2001 From: abhishek Date: Sat, 20 Jun 2026 18:45:25 +0530 Subject: [PATCH] changes on global catalogue --- src/App.tsx | 605 ++++---- src/components/BulkCartDrawer.tsx | 144 ++ src/components/ComparisonModal.tsx | 174 +++ src/components/DashboardView.tsx | 145 -- src/components/DeliveriesView.tsx | 2 +- src/components/DeliveryReportsView.tsx | 422 ++++-- src/components/InventoryView.tsx | 1827 ++++++++++++----------- src/components/OrdersDeliveriesView.tsx | 8 +- src/components/OrdersView.tsx | 8 +- src/components/ReportsView.tsx | 493 +++--- src/components/SettingsView.tsx | 23 +- src/components/Sidebar.tsx | 1 - src/components/StoreCatalogView.tsx | 678 ++++++--- src/components/StoreDetailView.tsx | 2 +- src/components/TrialBatchDrawer.tsx | 234 +++ src/components/UserStorePage.tsx | 3 + src/components/UsersPanel.tsx | 206 ++- src/components/consoleUi.tsx | 135 +- src/contexts/CompareContext.tsx | 84 ++ src/index.css | 4 +- src/main.tsx | 5 +- src/services/fiestaApi.ts | 4 + 22 files changed, 3137 insertions(+), 2070 deletions(-) create mode 100644 src/components/BulkCartDrawer.tsx create mode 100644 src/components/ComparisonModal.tsx create mode 100644 src/components/TrialBatchDrawer.tsx create mode 100644 src/contexts/CompareContext.tsx diff --git a/src/App.tsx b/src/App.tsx index 8051983..33eea8b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -50,6 +50,7 @@ import StoreDetailView from './components/StoreDetailView'; import LoginView from './components/LoginView'; import UserStorePage from './components/UserStorePage'; import AwaitingApi from './components/AwaitingApi'; +import ComparisonModal from './components/ComparisonModal'; import type { AuthUser } from './services/auth'; import ragulStoreCover from './assets/images/store_front_view_1780299351800.png'; @@ -249,314 +250,314 @@ export default function App() { const handleLogout = () => setAuthUser(null); // Define secondary sections (Stores, Logistics, Staffing, Settings) within main body - const renderSecondarySection = () => { - switch (currentSection) { - case 'stores': { - // A single-store merchant has no branches, so skip the one-card registry - // and open that store directly. Add a branch (2+ stores) and the grid - // returns automatically. Clicking a card in multi-store mode also opens - // the console, with a Back-to-registry button. - const isSoleStore = !selectedStore && storesList.length === 1; - const activeStore = selectedStore ?? (isSoleStore ? storesList[0] : null); + const renderStoresSection = () => { + const isSoleStore = !selectedStore && storesList.length === 1; + const activeStore = selectedStore ?? (isSoleStore ? storesList[0] : null); - if (activeStore) { - return ( -
- {isSoleStore && ( -
-
-

- Store Console -

-

- This merchant operates a single store. Add a branch to manage multiple outlets. -

-
-
- )} - setSelectedStore(null) : undefined} - tenantId={tenantId} - /> -
- ); - } - - return ( -
- {/* Simple and elegant premium header */} -
+ 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 && ( - - )} -
+ return ( +
- {/* Filter tabs */} -
- - - -
+ + {/* ── 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 */} -
- {st.name} -
- - {/* 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} -
-
- - -
- - {/* Card footer - enter console */} -
- - -
- 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 && ( + + )}
- ); - } + {/* Vertical Divider (Hidden on Mobile) */} +
+ + {/* Filter Segmented Control */} +
+ + + +
+
+ + {/* 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 */} +
+ {st.name} +
+ + {/* 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} +
+
+ + +
+ + {/* Card footer - enter console */} +
+ + +
+ 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

+
+
+
+ + +
+
+ + {/* Content (Scrollable List) */} +
+ {selectedProducts.length === 0 ? ( +
+ +

Your cart is empty.

+
+ ) : ( + selectedProducts.map((prod) => ( +
+
+ {prod.name} +
+
+ + {prod.category} + +

{prod.name}

+

{prod.sku}

+
+ +
+ )) + )} +
+ + {/* Action Footer */} + {selectedProducts.length > 0 && ( +
+
+ + + +
+
+ )} +
+
+ ); +} 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) => ( + {p.name} + ))} +
+ +
+ + +
+
+ )} + + {/* Comparison Drawer */} + {isComparing && ( +
+
+ + {/* Header */} +
+
+

Product Comparison

+ + {selectedProducts.length} items + +
+
+ {selectedProducts.length < 5 && ( + + )} + + +
+
+ + {/* Comparison Table / Grid */} +
+
+ {/* Left Column for labels */} +
+
Category
+
SKU
+
Price
+
Units Sold
+
Status
+
+ + {/* Product Columns */} + {selectedProducts.map((prod) => ( +
+ + {/* Remove button overlay */} + + + {/* Product Header Card */} +
+
+ {prod.name} +
+

{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 (
-
- - -
-
-
-

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 */} +
+
+ + Delivered + +
+ +
+
+
+

+ {totals.delivered.toLocaleString('en-IN')} +

+

Successfully fulfilled

+
+
+ + {/* Pending */} +
+
+ + 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 */} +
+
+ Total SKUs +
+ +
+
+
+

+ {products.length} +

+

Master catalogue

-
-

- {products.length} -

-

Master catalogue

-
-
- {/* Card 2: Synced Outlets */} -
-
- Active Outlets -
- + {/* Card 2: Synced Outlets */} +
+
+ Active Outlets +
+ +
+
+
+

+ {locations.length} +

+

Synced locations

-
-

- {locations.length} -

-

Synced locations

-
-
- {/* Card 3: Total On-Hand Volume */} -
-
- Total Stock -
- + {/* Card 3: Total On-Hand Volume */} +
+
+ Total Stock +
+ +
+
+
+

+ {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 && ( + + )} +
- return ( - - ); - })} -
- -
- - + )} +
- {/* 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 && ( + + )} + + {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 */} -
- {prod.name} +
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.name} +
+
+ + {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 + }); + }} + > + +
+
+ +
+
+ Units Sold + {prod.unitsSold.toLocaleString()} +
+
+ Revenue + ₹{prod.revenue.toLocaleString()} +
+
+ +
+
+ + + {prod.verified ? 'Active Portfolio' : 'Under Inspection'} + +
-
-
- Units Sold - {prod.unitsSold.toLocaleString()} + {storeCat.has(prod.id) ? ( +
+ In Store Catalogue +
-
- Revenue - ₹{prod.revenue.toLocaleString()} -
-
+ ) : ( + + )}
- - {/* Exposure toggle row */} -
- - - {prod.verified ? 'Active Portfolio' : 'Under Inspection'} - - - -
- - {/* Store-catalogue curation: pick the product to show to store users */} - {storeCat.has(prod.id) ? ( -
- In 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 */} -
- {prod.name} +
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.name} +
+
+ + {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 + }); + }} + > + +
+
+ +
+
+ Units Sold + {prod.unitsSold.toLocaleString()} +
+
+ Revenue + ₹{prod.revenue.toLocaleString()} +
+
+ +
+
+ + + {prod.verified ? 'Active Portfolio' : 'Under Inspection'} + +
-
-
- Units Sold - {prod.unitsSold.toLocaleString()} + {storeCat.has(prod.id) ? ( +
+ In Store Catalogue +
-
- Revenue - ₹{prod.revenue.toLocaleString()} -
-
-
-
- - {/* Exposure toggle row */} -
- - - {prod.verified ? 'Active Portfolio' : 'Under Inspection'} - - - -
- - {/* Store-catalogue curation: pick the product to show to store users */} - {storeCat.has(prod.id) ? ( -
- In 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. -

-
-
+ - {/* 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 && ( )}
- {/* Filter buttons */} -
- - +
+
+

+ Categories +

+
+ {globalCategories.map((cat) => ( + + ))} +
+ + {(globalSelectedCategories.length > 0 || globalCatalogSearch) && ( + + )} +
+
+
+ + {/* ── 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 && ( + + )} + {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.name} +
+
+ + {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 +
+ ) : ( + + )} +
); })}
- ); - })()} -
- - ) : ( -
- - {/* 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.name} +
+
+ + {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 +
+ ) : ( + + )} +
+
+
+ ); + })}
-