diff --git a/src/App.tsx b/src/App.tsx index 84fe447..8621b94 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -38,7 +38,6 @@ import { FIESTA_TENANT_ID, str as fstr } from './services/fiestaApi'; import Sidebar from './components/Sidebar'; import Header from './components/Header'; import DashboardView from './components/DashboardView'; -import OperationsView from './components/OperationsView'; import ReportsView from './components/ReportsView'; import InventoryView from './components/InventoryView'; import SettingsView from './components/SettingsView'; @@ -519,12 +518,12 @@ export default function App() { /> )} - {currentSection === 'operations' && ( - - )} - {currentSection === 'reports' && ( - + )} {/* Handle alternative sections: Stores, Settings */} diff --git a/src/components/Header.tsx b/src/components/Header.tsx index b0b14c1..c5337aa 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -75,41 +75,6 @@ export default function Header({ > - - {/* Global Actions Bar */} diff --git a/src/components/InventoryView.tsx b/src/components/InventoryView.tsx index 9652aa6..db86b21 100644 --- a/src/components/InventoryView.tsx +++ b/src/components/InventoryView.tsx @@ -26,7 +26,12 @@ import { Palette, ShoppingBag, Info, - X + X, + Server, + ChevronDown, + ChevronUp, + RotateCw, + CheckCircle } from 'lucide-react'; import { ProductMatrixItem, ImportLog } from '../types'; import { initialImportLogs } from '../data'; @@ -91,6 +96,132 @@ export default function InventoryView({ const [activeTab, setActiveTab] = useState<'catalog' | 'import_branding'>('catalog'); const [selectedCategory, setSelectedCategory] = useState('ALL'); const [showAddProductModal, setShowAddProductModal] = useState(false); + const [outletFilter, setOutletFilter] = useState<'all' | 'alerts'>('all'); + const [outletSearch, setOutletSearch] = useState(''); + const [restockedOverrides, setRestockedOverrides] = useState>>({}); + const [loadingOutlets, setLoadingOutlets] = 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]); + + // Simulated restock dispatch handler + const handleRestockOutlet = (locationId: number, storeRows: any[], locationName: string) => { + setLoadingOutlets(prev => ({ ...prev, [locationId]: true })); + + setTimeout(() => { + setRestockedOverrides(prev => { + const currentOverrides = prev[locationId] || {}; + const newOverrides = { ...currentOverrides }; + + storeRows.forEach(row => { + const sku = `SKU-${String(row.productid ?? '') || String(row.productname ?? '')}`; + newOverrides[sku] = 200; // Restock to safe optimal level (>= 120) + }); + + return { + ...prev, + [locationId]: newOverrides + }; + }); + + setLoadingOutlets(prev => ({ ...prev, [locationId]: false })); + + const timestamp = new Date().toLocaleTimeString(); + setImportLogs(prev => [ + { + id: String(Date.now()), + timestamp, + file: 'SUPPLY_CHAIN_API', + status: 'SUCCESS', + count: storeRows.length, + note: `AUTO-RESTOCK: Dispatched emergency shipment to ${locationName}. Synchronized ${storeRows.length} SKUs to 200 units.` + }, + ...prev + ]); + }, 1500); + }; + + // Simulated single SKU restock handler + const handleRestockSKU = (locationId: number, row: any, locationName: string) => { + if (!row) return; + const sku = `SKU-${String(row.productid ?? '') || String(row.productname ?? '')}`; + const prodName = String(row.productname || 'Unnamed product'); + + setRestockedOverrides(prev => { + const currentOverrides = prev[locationId] || {}; + return { + ...prev, + [locationId]: { + ...currentOverrides, + [sku]: 200 + } + }; + }); + + const timestamp = new Date().toLocaleTimeString(); + setImportLogs(prev => [ + { + id: String(Date.now()), + timestamp, + file: 'SUPPLY_CHAIN_API', + status: 'SUCCESS', + count: 1, + note: `SKU-RESTOCK: Restocked ${prodName} (${sku}) at ${locationName} to 200 units.` + }, + ...prev + ]); + }; // CSV Textarea input const [csvText, setCsvText] = useState( @@ -166,6 +297,12 @@ export default function InventoryView({ }); }; + const handleToggleProductExposure = (id: string) => { + setProducts(prev => + prev.map(p => p.id === id ? { ...p, verified: !p.verified } : p) + ); + }; + // Custom Raw CSV import const handleCSVImport = () => { const lines = csvText.split('\n').map(l => l.trim()).filter(l => l.length > 0 && !l.startsWith('Name')); @@ -278,157 +415,289 @@ export default function InventoryView({ ]; return ( -
+
- {/* Dynamic Navigation Toolbar header */} -
-
-

- - Product Catalog · Global Assortment -

-

- The master product catalog for all your outlets. Import products into the global catalog and monitor live stock across every store under you. -

-
- {storesLoading ? ( - - Loading live stock across outlets… + {/* Immersive Background Blur Blobs */} +
+
+ + {/* ── Immersive Analytics Banner (With Catalog Cover Image & Slate Gradient Overlay) ── */} +
+ {/* Cover Image Background & Decor */} +
+ Catalog Command Center Banner +
+ + {/* Background decorative glowing circles */} +
+
+
+ + {/* Content Row */} +
+
+

+ + Product Catalog Command Center + + Global Sync - ) : storesError ? ( - - Live data unavailable - - ) : ( - - Live · {locations.length} outlet{locations.length === 1 ? '' : 's'} · {products.length} catalog SKUs - - )} +

+

+ Master catalog registry with regional assortment presets, brand styling studio, and live stock synchronization feeds. +

+
+ + {/* Navigation Tab pills styled like a modern control unit */} +
+ + +
-
- + {/* Small card metrics grid - 4 dynamic columns inside the banner */} +
- + {/* Card 1: Total SKUs */} +
+
+ Total SKUs +
+ +
+
+
+

+ {products.length} +

+

Master catalog

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

+ {locations.length} +

+

Synced locations

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

+ {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 */} +
+
+ Catalog Sync Ratio +
+ +
+
+
+

+ {products.length > 0 ? `${Math.round((products.filter(p => p.verified).length / products.length) * 100)}%` : '100%'} +

+

Active Portfolio

+
+
+
{activeTab === 'catalog' ? ( <> - {/* Admin access banner */} -
-
- -
-

Global Catalog — Admin access

-

- As an admin you can import products into the global catalog. Store managers see it read-only. The stock below is live across every outlet under you. -

-
-
- - Admin - -
- {/* Category filter + admin import actions */} -
+
+ + {/* Themed Category Badges */}
- {categories.map((cat) => ( - - ))} + {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'; + } + + return ( + + ); + })}
{/* Global Catalog — master assortment grid (full width) */} -
-
-

Global Product Catalog

- - {filteredProducts.length} item{filteredProducts.length === 1 ? '' : 's'} +
+
+

+ Global Catalog Assortment +

+ + {filteredProducts.length} item{filteredProducts.length === 1 ? '' : 's'} loaded
-

- Master assortment available to roll out to every outlet — imported by the admin and synced to the customer booking apps. -

{storesLoading && products.length === 0 ? ( -
Loading global catalog…
+
Synchronizing regional database...
) : filteredProducts.length === 0 ? ( -
No catalog products match your search or category.
+
No catalog products match your selection.
) : ( -
+
{filteredProducts.map((prod) => ( -
-
- {prod.name} +
+
+ {/* Image zoom effect on hover */} +
+ {prod.name} +
+
+
+
+

{prod.name}

+ {prod.sku} +
+ + {/* Categorized pill badge */} + + {prod.category.split(' / ')[0]} + +
+ +
+
+ Units Sold + {prod.unitsSold.toLocaleString()} +
+
+ Revenue + ₹{prod.revenue.toLocaleString()} +
+
+
-
-
-
-

{prod.name}

- {prod.sku} -
- - {prod.category.split(' / ')[0]} - -
-
-
- Sold (Units) - {prod.unitsSold.toLocaleString()} -
-
- Total revenue - ₹{prod.revenue.toLocaleString()} -
-
+ + {/* Exposure toggle row */} +
+ + + {prod.verified ? 'Active Portfolio' : 'Under Inspection'} + + +
))} @@ -438,129 +707,326 @@ export default function InventoryView({ {/* Store Stock — live per-outlet breakdown for every store under the admin */}
-
-
-

- Store Stock · All Outlets Under You -

-

Live on-hand balances for each store you manage.

+ + {/* Elegant Header Row */} +
+
+
+

+ Regional Hub Stocks +

+ + + Live Sync + +
+

+ Real-time inventory levels and capacity balance across {locations.length} regional outlets. +

+
+ + {/* Controls: Search + Filters */} +
+ {/* Search */} +
+ + 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" + /> + {outletSearch && ( + + )} +
+ + {/* Filter buttons */} +
+ + +
- - {locations.length} store{locations.length === 1 ? '' : 's'} -
- {locations.length === 0 ? ( -
- {locationsQ.isLoading ? 'Loading outlets…' : 'No outlets found under this tenant.'} + {/* Quick Metrics Strip */} +
+
+ Active Outlets + {locations.length}
- ) : ( -
- {storesStock.map((store) => { - const items = store.rows - .map((r) => stockRowToInventory(r, store.locationname)) - .filter((it) => !searchQuery || it.name.toLowerCase().includes(searchQuery.toLowerCase())); - const totalUnits = items.reduce((a, it) => a + it.stockLevel, 0); - const lowCount = items.filter((it) => it.status !== 'Optimal').length; - const meta = locations.find((l) => l.locationid === store.locationid); - const status = meta?.status ?? 'Active'; - return ( -
-
-
+
+ Optimal Hubs + {locations.length - storeAlertsData.alertOutletsCount} +
+
+ Low Stock Items + {storeAlertsData.lowStockCount} +
+
+ Critical Alerts + {storeAlertsData.criticalCount} +
+
+ + {(() => { + 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); + + 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 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 ( +
+ + {/* Loading Overlay */} + {loadingOutlets[store.locationid] && ( +
+
+
+
+
+
+
+

Replenishing Hub...

+

Dispatching supply batch to {store.locationname}

+
+
+
+ )} + + {/* Card Header (Clean & borderless) */} +
-

{store.locationname}

-

- {store.isLoading ? 'Syncing…' : `${items.length} SKUs · ${totalUnits.toLocaleString('en-IN')} units on hand`} +

+ + {store.locationname} +

+

+ {totalItems} items · {totalUnits.toLocaleString('en-IN')} units

- 0 + ? 'text-rose-600 bg-rose-50' + : 'text-amber-700 bg-amber-50' + : 'text-emerald-600 bg-emerald-50' }`}> - {status} + {hasAlert ? criticalItemsCount > 0 ? 'Critical' : 'Low Stock' : 'Optimal'}
- {lowCount > 0 && !store.isLoading && ( -

- {lowCount} low / critical SKU{lowCount === 1 ? '' : 's'} -

- )} -
-
- {store.isLoading ? ( -
Loading store stock…
- ) : store.isError ? ( -
Couldn't load this store's stock.
- ) : items.length === 0 ? ( -
No stock items{searchQuery ? ' match your search' : ''}.
- ) : ( - items.map((it, idx) => { - const pct = Math.min(100, (it.stockLevel / it.maxCapacity) * 100); - return ( -
-
-

{it.name}

- {it.stockLevel.toLocaleString('en-IN')} -
-
-
-
-
- - {it.status} - -
+ {/* Card Body */} +
+ + {/* Segmented Stock Health Distribution */} +
+ {criticalPct > 0 && ( +
+ )} + {lowPct > 0 && ( +
+ )} + {optimalPct > 0 && ( +
+ )} +
+ + {/* Capacity utilization indicator */} +
+
+ Capacity Utilised + {Math.round(capacityPct)}% +
+
+
85 ? 'bg-rose-500' : 'bg-purple-650' + }`} + 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 rawRow = store.rows.find(r => `SKU-${String(r.productid ?? '') || String(r.productname ?? '')}` === it.sku); + const isLow = it.status !== 'Optimal'; + + return ( +
+
+ + + {it.name} + +
+ +
+ + {it.stockLevel} + + {isLow && ( + + )} +
+
+ ); + })}
- ); - }) - )} + )} +
+ +
+ + {/* Card Footer actions */} +
+ + {hasAlert ? `${criticalItemsCount + lowCount} items need attention` : 'All items optimal'} + + + {hasAlert ? ( + + ) : ( + + Stocked + + )} +
-
- ); - })} -
- )} + ); + })} +
+ ); + })()}
) : ( -
+
- {/* Left Column: Catalogue Import & Batch Console */} -
+ {/* Left Column: Catalogue Import & Batch Console (7 Cols) */} +
{/* Fast Imports presets Card */} -
+
- -

Tamil Nadu Region Catalog Presets

+ +

Cooperative Catalog Presets

-

- Instantly import bulk verified grocers, spices and diary products catalogs from local Coimbatore farms & cooperatives. -

{/* Preset 1 */} -
-
-

Nilgiris Dairy Fresh Pack

-

3 High-Margin Butter & Cheese SKU

+
+
+ Cooperative Dairy +

Nilgiris Dairy Fresh Pack

+

3 High-Margin Butter & Cheese SKUs

-
+
CBE-COOP-04 @@ -568,16 +1034,17 @@ export default function InventoryView({
{/* Preset 2 */} -
-
-

Coimbatore Heritage Grains

-

3 Premium Boiled Rice & Oils

+
+
+ Agricultural Feed +

Coimbatore Heritage Grains

+

3 Premium Boiled Rice & Oils

-
+
TAMIL-AGRI-09 @@ -588,27 +1055,31 @@ export default function InventoryView({
{/* Custom CSV Parsing Box */} -
+

Manual CSV Direct-Entry Console

-

- Paste comma-separated rows here (Name, SKU, Category, Price, InitialStock) to bulk register catalog elements. -

-