/** * @license * SPDX-License-Identifier: Apache-2.0 */ import React, { useState, useEffect, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import { Layers, Search, Plus, RefreshCw, AlertTriangle, TrendingUp, Sparkles, Check, Package, ChevronRight, TrendingDown, Trash2, PackageCheck, ShieldCheck, Tag, UploadCloud, FileSpreadsheet, Palette, Info, X, Server, ChevronDown, ChevronUp, CheckCircle, ShoppingCart, Inbox, Store, Activity, Award, Filter, Box, ArrowLeft } from 'lucide-react'; import { ProductMatrixItem } from '../types'; import { useFiestaTenantLocations, useFiestaStoresStock, useFiestaProductCategories, useFiestaUpdateStockRequest, useFiestaGetStockRequests, } from '../services/fiestaQueries'; import { FIESTA_TENANT_ID, FIESTA_PRIMARY_LOCATION_ID, str as fstr } from '../services/fiestaApi'; import { stockRowToProduct, stockRowToInventory } from '../services/fiestaMappers'; import { useStoreCatalogue, isPublishedItem } from '../services/storeCatalogue'; import BulkCartDrawer from './BulkCartDrawer'; import AwaitingApi from './AwaitingApi'; import { SlideDrawer, Skeleton, TH_STYLE, SURFACE_ALT, TEXT, TEXT_2, TEXT_3, BORDER, BRAND, tint, edge, StatusChip } from './consoleUi'; import FMCGHoverOverlay from './FMCGHoverOverlay'; import CatalogueBrowser from './CatalogueBrowser'; import OfflineSalesUpload from './OfflineSalesUpload'; import { useCompare } from '../contexts/CompareContext'; type StockRow = Record; const rowId = (r: StockRow) => String(r.productid ?? '') || String(r.productname ?? ''); interface InventoryViewProps { searchQuery: string; isCoimbatoreView: boolean; tenantId?: number; isSidebarOpen?: boolean; } export default function InventoryView({ searchQuery, isCoimbatoreView, tenantId = FIESTA_TENANT_ID, isSidebarOpen = false }: InventoryViewProps) { const { setHideCompareBar } = useCompare(); const [searchTerm, setSearchTerm] = useState(''); const [showCatalogueModal, setShowCatalogueModal] = useState(false); const [showOfflineSales, setShowOfflineSales] = useState(false); const navigate = useNavigate(); // ── 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 // to it); the STORE STOCK section shows each outlet's live stock so the admin // can see all the stores under them at a glance. const locationsQ = useFiestaTenantLocations(tenantId); const locations = useMemo( () => (locationsQ.data ?? []).map((l) => ({ locationid: Number(l.locationid), locationname: fstr(l.locationname) || `Outlet ${fstr(l.locationid)}`, status: fstr(l.status) || 'Active', })), [locationsQ.data], ); // The admin catalogue (imports, curation) is scoped to this tenant's own // primary/hub outlet — not the hardcoded Fiesta defaults, so onboarded // tenants other than Fiesta see their own imports reflected here. const primaryLocationId = locations[0]?.locationid ?? FIESTA_PRIMARY_LOCATION_ID; const storesStock = useFiestaStoresStock( tenantId, locations.map(({ locationid, locationname }) => ({ locationid, locationname })), ); const storesLoading = locationsQ.isLoading || storesStock.some((s) => s.isLoading); const storesError = locationsQ.isError || (storesStock.length > 0 && storesStock.every((s) => s.isError)); // Global catalog = deduped union of every outlet's products, plus anything the // admin adds/imports in-session. Computes live from storesStock and storeCat.items. const [selectedAdminProduct, setSelectedAdminProduct] = useState(null); const [selectedRequest, setSelectedRequest] = useState(null); const allStoreRows = storesStock.flatMap((s) => s.rows); const [activeTab, setActiveTab] = useState<'catalog' | 'requests' | 'global_catalogue'>('catalog'); const [isLocalSidebarOpen, setIsLocalSidebarOpen] = useState(false); const [selectedCategories, setSelectedCategories] = useState([]); const [hoveredAdminProduct, setHoveredAdminProduct] = useState(null); const [localSearch, setLocalSearch] = useState(''); const storeCat = useStoreCatalogue(tenantId, primaryLocationId); const [importPrice, setImportPrice] = useState(''); const [isSettingPrice, setIsSettingPrice] = useState(false); const [addingPriceProdId, setAddingPriceProdId] = useState(null); const [cardImportPrice, setCardImportPrice] = useState(''); const selectedProductId = selectedAdminProduct?.id; useEffect(() => { if (selectedAdminProduct) { const storeItem = storeCat.items.find(i => i.productid === selectedAdminProduct.id); const existingPrice = storeItem?.price || selectedAdminProduct.price; setImportPrice( existingPrice && existingPrice > 0 ? String(existingPrice) : (selectedAdminProduct.unitsSold > 0 ? String(Math.round(selectedAdminProduct.revenue / selectedAdminProduct.unitsSold)) : '') ); setIsSettingPrice(false); } else { setImportPrice(''); setIsSettingPrice(false); } }, [selectedProductId]); const products = useMemo(() => { const byId = new Map(); allStoreRows.forEach((r) => { const id = rowId(r); if (id && !byId.has(id)) byId.set(id, stockRowToProduct(r)); }); // Ensure all items published to the store catalogue or imported as drafts are preserved storeCat.items.forEach((item) => { const id = String(item.productid); if (!byId.has(id)) { byId.set(id, { id: id, name: item.name, sku: item.sku || `SKU-${item.productid}`, image: item.image, category: item.category, price: item.price, unitsSold: 0, revenue: 0, stockStatus: 'Healthy', trend: 'flat', exposure: 'All Outlets', // "Fully published with price" — tested on the price itself, not on // status. Status is not a publish flag: the backend overwrites our // 'Active' with an availability value, which used to flip published // products back to unverified. See storeCatalogue.isPublishedItem. verified: item.price > 0, }); } else { // If it exists but we have curated price in storeCat, we could apply it here if needed const existing = byId.get(id)!; existing.price = item.price || existing.price; existing.verified = existing.verified || item.price > 0; } }); return Array.from(byId.values()); }, [allStoreRows, storeCat.items]); const [requestStoreFilter, setRequestStoreFilter] = useState('All Stores'); const [requestStatusFilter, setRequestStatusFilter] = useState('All Statuses'); const [requestDate, setRequestDate] = useState(() => new Date().toISOString().split('T')[0]); // Fetch store stock requests const stockRequestsQ = useFiestaGetStockRequests({ tenantid: tenantId, pagesize: 1000, date: requestDate }); const [storeRequests, setStoreRequests] = useState<{ locationid: number, locationname: string, picks: Record }[]>([]); useEffect(() => { if (activeTab === 'requests' && stockRequestsQ.data) { const reqsMap: Record = {}; stockRequestsQ.data.forEach((req: any) => { if (!reqsMap[req.locationid]) { const loc = locations.find(l => String(l.locationid) === String(req.locationid)); reqsMap[req.locationid] = { locationid: req.locationid, locationname: req.locationname || (loc ? loc.locationname : `Outlet #${req.locationid}`), picks: {} }; } // Key by requestid so multiple requests for the same product are all shown if (!reqsMap[req.locationid].picks[req.requestid]) { reqsMap[req.locationid].picks[req.requestid] = { qty: req.qty, status: req.status, requestedAt: req.created, resolvedAt: req.updated, productid: req.productid, productname: req.productname, productimage: req.productimage, requestid: req.requestid }; } }); setStoreRequests(Object.values(reqsMap).filter(r => Object.keys(r.picks).length > 0)); } }, [activeTab, locations, stockRequestsQ.data]); useEffect(() => { if (activeTab === 'requests') { stockRequestsQ.refetch(); } }, [activeTab, stockRequestsQ]); const updateStockRequestMutation = useFiestaUpdateStockRequest(); const updateProductRequestStatus = (locationid: number, requestid: number, productid: string, status: 'Approved' | 'Rejected' | 'Pending') => { updateStockRequestMutation.mutate({ tenantid: tenantId, locationid, productid: Number(productid), requestid, status }); // Optimistic UI update setStoreRequests(prev => prev.map(r => { if (r.locationid === locationid && r.picks[requestid]) { return { ...r, picks: { ...r.picks, [requestid]: { ...r.picks[requestid], status, resolvedAt: new Date().toISOString() } } }; } return r; })); }; // Hide compare bar everywhere for now useEffect(() => { setHideCompareBar(true); }, [activeTab, setHideCompareBar]); // Live product categories (for the Add-Product modal dropdown). const productCategoriesQ = useFiestaProductCategories(); const productCategoryNames = useMemo( () => (productCategoriesQ.data ?? []) .map((c) => fstr(c.categoryname)) .filter((name): name is string => Boolean(name)), [productCategoriesQ.data], ); // Categories derived from the live catalog (falls back to ALL only). const categories = useMemo(() => { const cats = Array.from(new Set(products.map((p) => p.category.split(' / ')[0]))); return cats.sort(); }, [products]); // Filter criteria 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 toggleCategory = (cat: string) => { setSelectedCategories((prev) => prev.includes(cat) ? [] : [cat], ); }; const handleToggleProductExposure = (id: string) => { // Left empty for now, as products are managed via storeCat }; const flattenedRequests = useMemo(() => { return storeRequests .filter(r => requestStoreFilter === 'All Stores' || r.locationname === requestStoreFilter) .flatMap(store => { return Object.entries(store.picks) .filter(([_, pick]) => requestStatusFilter === 'All Statuses' || String((pick as any).status).toLowerCase() === requestStatusFilter.toLowerCase()) .map(([requestId, pick]) => { const productId = (pick as any).productid; const product = products.find(p => String(p.id) === String(productId)); return { locationid: store.locationid, locationname: store.locationname, productid: productId, requestid: Number(requestId), product, pickData: pick }; }); }) .sort((a, b) => new Date(b.pickData.requestedAt).getTime() - new Date(a.pickData.requestedAt).getTime()); }, [storeRequests, products, requestStoreFilter, requestStatusFilter]); const requestingStores = useMemo(() => { return Array.from(new Set(locations.map(l => l.locationname))).sort(); }, [locations]); return (
{/* Immersive Background Blur Blobs */}
{/* Header and Metrics */} {activeTab === 'catalog' && (
{/* Small card metrics grid */}
{/* Card 1: Total SKUs */}
Total SKUs

{products.length}

Master catalogue

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

{locations.length}

Synced locations

{/* 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

{/* Card 4: Catalog Health */}
Catalogue Sync Ratio

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

Active Portfolio

)} {activeTab === 'catalog' ? ( <>
{isLocalSidebarOpen && (

Filter Product

Categories

{categories.map((cat) => ( ))}
{(selectedCategories.length > 0 || localSearch) && ( )}
)} {/* ── Main Content Area ── */}

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.

setLocalSearch(e.target.value)} className="w-full h-full pl-8 pr-3 py-1.5 bg-white border border-slate-200 rounded-lg text-[10px] text-slate-800 placeholder-slate-400 focus:outline-none focus:border-[#662582] transition-all shadow-sm font-medium" /> {localSearch && ( )}
{storesLoading && products.length === 0 ? (
{Array.from({ length: 10 }).map((_, i) => (
))}
) : filteredProducts.length === 0 ? (
No catalogue products match your selection.
) : (
{/* Left Side: Normal Catalogue */}
{filteredProducts.map((prod) => { // Presence in the store catalogue IS publication — the row // stays until the admin removes it. Don't gate on status; // the backend rewrites it to 'available'/'outofstock'. const isPublished = isPublishedItem(storeCat.items.find(i => i.productid === prod.id)); return (
setSelectedAdminProduct(prod)} className="bg-white/80 backdrop-blur-md border border-[#e2e8f0] rounded-none 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.isSample && (
SAMPLE
)}
{/* Content Section */}

{prod.name}

{prod.sku}

Sold: {prod.unitsSold.toLocaleString()} ₹{prod.revenue.toLocaleString()}
{isPublished ? ( ) : addingPriceProdId === prod.id ? (
e.stopPropagation()}> setCardImportPrice(e.target.value)} placeholder="e.g. 50" className="px-2 py-1 border border-slate-200 rounded-none text-[10px] font-bold focus:outline-none focus:border-[#662582]" autoFocus />
) : ( )}
); })}
)}
) : activeTab === 'requests' ? (

Pending Stock Requests

Store Branch Inventory Requests {flattenedRequests.length} Total
setRequestDate(e.target.value)} className="px-3 py-2 border border-slate-200 rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-[#662582] focus:border-[#662582] text-sm font-semibold text-slate-700 bg-white hover:border-[#662582]/40 hover:shadow-md transition-all" />
{flattenedRequests.length === 0 ? (

No Pending Requests

You're all caught up! All store stock requests have been processed and there is currently no pending action required.

) : (
{['Requested At', 'Store', 'Product', 'Qty', 'Status', 'Resolved At', ''].map((h, i) => ( ))} {flattenedRequests.map((req, idx) => { const isApproved = req.pickData.status === 'Approved'; const isRejected = req.pickData.status === 'Rejected'; const isPending = req.pickData.status === 'Pending'; const isCancelled = req.pickData.status === 'Cancelled'; const isReceived = req.pickData.status === 'Received'; return ( setSelectedRequest(req)} > ); })}
{h}
{req.pickData.requestedAt ? new Date(req.pickData.requestedAt).toLocaleDateString('en-IN', { day: 'numeric', month: 'short' }) : '—'} {req.pickData.requestedAt ? new Date(req.pickData.requestedAt).toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit' }) : ''}
{req.locationname}
{req.product?.name}

{req.product?.name}

{req.product?.sku}

{req.pickData.qty || '—'} {req.pickData.status || '—'} {req.pickData.resolvedAt ? new Date(req.pickData.resolvedAt).toLocaleString('en-IN', { dateStyle: 'medium', timeStyle: 'short' }) : '—'} {isPending ? (
) : ( Processed )}
)}
) : activeTab === 'global_catalogue' ? (
setActiveTab('catalog')} />
) : null} {/* ── Slide Drawer ── */} setSelectedAdminProduct(null)} title="Administrative Product Details" > {selectedAdminProduct && (() => { const isAlreadyInCatalog = products.some(p => p.sku === selectedAdminProduct.sku); return (
{/* Clean Image Container */}
{selectedAdminProduct.name}
{/* Title & Basics */}

SKU: {selectedAdminProduct.sku}

{selectedAdminProduct.name}

{String(selectedAdminProduct.category || '').split(' / ')[0]} {isAlreadyInCatalog && ( In Catalogue )}
{/* Real Price and Providers */}
Price Range
{selectedAdminProduct.priceRange || 'N/A'}
{selectedAdminProduct.providers && selectedAdminProduct.providers.length > 0 && (
Available Providers
{selectedAdminProduct.providers.map(provider => ( {provider} ))}
)}
{/* Selling Price & Store Catalogue Action Area */}

Store Selling Price (₹)

{storeCat.has(selectedAdminProduct.id) ? 'Currently active in store catalogue' : 'Set selling price to publish in store catalogue'}

{storeCat.has(selectedAdminProduct.id) && ( Active )}
e.stopPropagation()}>
setImportPrice(e.target.value)} onClick={(e) => e.stopPropagation()} placeholder="e.g. 50" className="w-full pl-7 pr-3 py-2.5 border border-slate-200 rounded-xl text-sm font-bold text-slate-900 focus:outline-none focus:ring-2 focus:ring-[#662582]/20 focus:border-[#662582] bg-white transition-all shadow-xs" />
{storeCat.has(selectedAdminProduct.id) && ( )}

Retail Packaging Info

); })()}
{/* ── Slide Drawer for Stock Requests ── */} setSelectedRequest(null)} title="Stock Request Details" > {selectedRequest && (() => { const req = selectedRequest; const isPending = req.pickData.status === 'Pending'; const isApproved = req.pickData.status === 'Approved'; const isRejected = req.pickData.status === 'Rejected'; const isCancelled = req.pickData.status === 'Cancelled'; const prod = req.product; return (
{prod ? (
{prod.name}
{String(prod.category || '').split(' / ')[0]}
) : (
Product Image Not Found
)}

{prod?.name || 'Unknown Product'}

{prod?.sku || `SKU-${req.productid}`}

Requesting Store {req.locationname}
Requested Qty {req.pickData.qty} Units
Status {req.pickData.status}
{/* Action Area */}

Process Request

{isPending ? (
) : (
This request has been {req.pickData.status.toLowerCase()}.
)}
{prod && (

Retail Packaging Info

)}
); })()}
{/* Offline (counter) sales import. No locationId is passed: the admin gets one workbook covering every branch, and each row's own locationid routes its sale to the right store. This view operates on the tenant's first outlet elsewhere, which would have been the wrong store to credit for most of these sales. */} {showOfflineSales && ( setShowOfflineSales(false)} /> )}
); }