diff --git a/src/App.tsx b/src/App.tsx index f51c333..8395b33 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -595,26 +595,22 @@ export default function App() { ? (summaryQ.data?.tenantname ? `${summaryQ.data.tenantname} Admin` : 'Admin Console') : currentSection === 'inventory' ? 'Products' - : currentSection === 'catalogue' - ? 'Global Catalogue' - : currentSection === 'dispatch' - ? 'Console' - : currentSection.charAt(0).toUpperCase() + currentSection.slice(1), + : currentSection === 'dispatch' + ? 'Console' + : currentSection.charAt(0).toUpperCase() + currentSection.slice(1), icon: currentSection === 'dashboard' ? LayoutDashboard : currentSection === 'inventory' ? Layers - : currentSection === 'catalogue' - ? Box - : currentSection === 'stores' - ? Store - : currentSection === 'reports' - ? TrendingUp - : currentSection === 'settings' - ? Settings - : currentSection === 'dispatch' - ? Truck - : undefined + : currentSection === 'stores' + ? Store + : currentSection === 'reports' + ? TrendingUp + : currentSection === 'settings' + ? Settings + : currentSection === 'dispatch' + ? Truck + : undefined }} /> @@ -657,12 +653,7 @@ export default function App() { /> } /> - - } /> + } /> - {/* Header section */} -
-
- System Operations -

Admin Onboarding Console

-

- Provision new tenants, register store outlet branches, and onboard delivery fleet riders into the system. -

-
-
{/* Tabs Layout */}
diff --git a/src/components/CatalogueBrowser.tsx b/src/components/CatalogueBrowser.tsx index 3af58cb..d317073 100644 --- a/src/components/CatalogueBrowser.tsx +++ b/src/components/CatalogueBrowser.tsx @@ -1,26 +1,31 @@ import React, { useState } from 'react'; -import { Search, CheckCircle2, DownloadCloud, Box, PackageOpen } from 'lucide-react'; +import { Search, CheckCircle2, DownloadCloud, Box, PackageOpen, Filter, Layers, Check, Package, X, ArrowLeft } from 'lucide-react'; import { useCatalogueProducts, useCatalogueBrands, useImportedCatalogueRefs, - useImportCatalogueProduct + useImportCatalogueProduct, + useTenantCategories } from '../hooks/useCatalogueImport'; import { CatalogueProduct, ImportCatalogueProductRequest } from '../services/catalogueApi'; +import { useStoreCatalogue } from '../services/storeCatalogue'; import ImportProductModal from './ImportProductModal'; interface CatalogueBrowserProps { tenantid: number; locationid: number; + onClose?: () => void; } -export default function CatalogueBrowser({ tenantid, locationid }: CatalogueBrowserProps) { +export default function CatalogueBrowser({ tenantid, locationid, onClose }: CatalogueBrowserProps) { const [brand, setBrand] = useState(undefined); const [keyword, setKeyword] = useState(''); const [debouncedKeyword, setDebouncedKeyword] = useState(''); const [importingProduct, setImportingProduct] = useState(null); + const [isLocalSidebarOpen, setIsLocalSidebarOpen] = useState(true); + // Debounce search React.useEffect(() => { const handler = setTimeout(() => { @@ -36,6 +41,8 @@ export default function CatalogueBrowser({ tenantid, locationid }: CatalogueBrow const { data: brandsData = [], isLoading: isLoadingBrands } = useCatalogueBrands(); const { data: importedRefs = new Set() } = useImportedCatalogueRefs(tenantid, brand); + const { data: tenantCategories = [] } = useTenantCategories(tenantid); + const storeCat = useStoreCatalogue(); const importProductMutation = useImportCatalogueProduct(tenantid, locationid); @@ -51,133 +58,207 @@ export default function CatalogueBrowser({ tenantid, locationid }: CatalogueBrow }; return ( -
+
- {/* Top Filters */} -
-
-
-

- - Global Catalogue -

-

- Browse {total > 0 ? total : ''} global FMCG products and import them to your store. -

-
- -
- - setKeyword(e.target.value)} - className="w-full pl-10 pr-4 py-2 bg-slate-50 border border-slate-200 rounded-full text-sm focus:outline-none focus:ring-2 focus:ring-purple-500/20 focus:border-purple-500 focus:bg-white transition-all" - /> -
-
- - {/* Brands Chip Row */} -
-

Filter by Brand

-
- - {isLoadingBrands ? ( - Loading brands... - ) : ( - brandsData.map(b => ( - - )) - )} -
-
-
- - {/* Product Grid */} -
- {isLoadingProducts ? ( -
-
- Loading catalogue... -
- ) : products.length === 0 ? ( -
- -

No products found in the catalogue.

-
- ) : ( -
- {products.map(p => { - const isImported = importedRefs.has(`${p.brand}:${p.id}`); - - return ( -
- {isImported && ( -
- - Imported -
- )} - -
- {p.images && p.images.length > 0 ? ( - {p.product_name} - ) : ( - - )} +
+ {/* Left Side: Filters */} + {isLocalSidebarOpen && ( +
+
+
+

+ Filter Brands +

+
+
+
+

+ Brands +

+ {isLoadingBrands ? ( + Loading brands... + ) : ( +
+ {brandsData.map((b) => ( + + ))}
+ )} +
+ + {(brand || keyword) && ( + + )} +
+
+
+ )} + + {/* Right Side: Main Content Area */} +
+
+
+ {onClose && ( + + )} + +
+

+ Global Product Catalogue +
+ + {total} product{total === 1 ? '' : 's'} available + +
+

+

Browse global FMCG products and import them into your local merchant catalogue.

+
+
+ +
+
+ + setKeyword(e.target.value)} + className="bg-transparent border-none outline-none text-xs font-semibold text-slate-700 ml-2 w-full placeholder:text-slate-400 placeholder:font-medium" + /> + {keyword && ( + + )} +
+
+
+ + {isLoadingProducts ? ( +
+
+ Loading catalogue... +
+ ) : products.length === 0 ? ( +
+ No products match your selection. +
+ ) : ( +
+
+ {products.map(p => { + const isImported = storeCat.has(String(p.id)); -
-
{p.brand.replace('brand_', '')}
-

- {p.product_name} -

- -
-
- {p.size || 'N/A'} - {p.price_range || 'Price N/A'} + return ( +
setImportingProduct(p)} + 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" + > +
+ {p.images && p.images.length > 0 ? ( + {p.product_name} + ) : ( + + )} +
- {!isImported && ( - - )} +
+
+

+ {p.product_name} +

+

ID: {p.id}

+
+ +
+
+ {p.size || 'N/A'} + {p.price_range || 'N/A'} +
+ + {isImported ? ( + + ) : ( + + )} +
+
-
-
- ); - })} -
- )} + ); + })} +
+
+ )} +
{importingProduct && ( @@ -187,13 +268,14 @@ export default function CatalogueBrowser({ tenantid, locationid }: CatalogueBrow locationid={locationid} onClose={() => setImportingProduct(null)} onImport={handleImportSubmit} + isImported={storeCat.has(String(importingProduct.id))} /> )} {importProductMutation.isPending && (
-
Importing product...
+
Importing product...
)}
diff --git a/src/components/ImportProductModal.tsx b/src/components/ImportProductModal.tsx index 88b2251..452c2b5 100644 --- a/src/components/ImportProductModal.tsx +++ b/src/components/ImportProductModal.tsx @@ -1,7 +1,9 @@ import React, { useState } from 'react'; -import { X, Save, AlertCircle } from 'lucide-react'; +import { X, Save, AlertCircle, CheckCircle, DownloadCloud, Plus, Box } from 'lucide-react'; import { CatalogueProduct, ImportCatalogueProductRequest } from '../services/catalogueApi'; import { useProductSubcategories, useTenantCategories } from '../hooks/useCatalogueImport'; +import { SlideDrawer } from './consoleUi'; +import FMCGHoverOverlay from './FMCGHoverOverlay'; interface ImportProductModalProps { product: CatalogueProduct; @@ -9,6 +11,7 @@ interface ImportProductModalProps { locationid: number; onClose: () => void; onImport: (item: ImportCatalogueProductRequest) => void; + isImported?: boolean; } export default function ImportProductModal({ @@ -16,7 +19,8 @@ export default function ImportProductModal({ tenantid, locationid, onClose, - onImport + onImport, + isImported = false }: ImportProductModalProps) { const [categoryId, setCategoryId] = useState(''); const [subcategoryId, setSubcategoryId] = useState(''); @@ -25,14 +29,10 @@ export default function ImportProductModal({ const [taxPercent, setTaxPercent] = useState('0'); const [quantity, setQuantity] = useState('1'); - // Categories this tenant's own products actually use — the global - // categories list is missing categoryids that are nonetheless in real use. + // Categories this tenant's own products actually use const { data: categories = [], isLoading: isLoadingCategories } = useTenantCategories(tenantid); - // Subcategories are scoped to the selected category, so a product can't be - // tagged with a subcategory that doesn't actually belong to its category. - // Optional: many subcategories are unowned/shared and a product can be - // imported without one (falls back to "Uncategorized" in subcategory views). + // Subcategories scoped to selected category const { data: subcategories = [], isLoading: isLoadingSubcats } = useProductSubcategories( tenantid, categoryId ? Number(categoryId) : undefined, @@ -62,151 +62,114 @@ export default function ImportProductModal({ }; return ( -
-
+ +
- {/* Header */} -
-
-

Import Product

-

{product.product_name} ({product.brand})

+ {/* Clean Image Container */} +
+ {product.images && product.images.length > 0 ? ( + {product.product_name} + ) : ( + + )} + {isImported && ( +
+ + In Store Catalogue +
+ )} +
+ + {/* Title & Basics */} +
+

ID: {product.id}

+

{product.product_name}

+
+ + {product.brand.replace('brand_', '')} +
-
- {/* Body */} -
-
- -
- -

You need to map this catalogue product to your store's categories and set your own pricing.

+ {/* Price Range Container */} +
+
+ Price Range +
+ {product.price_range || 'N/A'}
- -
-
- - {isLoadingCategories ? ( -
Loading...
- ) : ( - - )} -
-
- - {!categoryId ? ( -
Select a category first
- ) : isLoadingSubcats ? ( -
Loading...
- ) : ( - - )} -
-
- -
-
- -
- - setRetailPrice(e.target.value)} - className="w-full pl-7 pr-3 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-emerald-500/20 focus:border-emerald-500 transition-colors" - placeholder="0.00" - required - /> -
-
-
- -
- - setProductCost(e.target.value)} - className="w-full pl-7 pr-3 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-emerald-500/20 focus:border-emerald-500 transition-colors" - placeholder="0.00" - required - /> -
-
-
- -
-
- -
- setTaxPercent(e.target.value)} - className="w-full pr-8 pl-3 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500/20 focus:border-purple-500 transition-colors" - placeholder="0" - /> - % -
-
-
- - setQuantity(e.target.value)} - className="w-full px-3 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500/20 focus:border-purple-500 transition-colors" - required - /> -
-
- - +
+ {product.size && ( + + Size: {product.size} + + )}
- {/* Footer */} -
- - + {/* Import Action Container */} +
+ {isImported ? ( +
+ +
+
Imported
+

This product is available in your Admin Catalogue. Set pricing and store parameters in the Admin Catalogue.

+
+
+ ) : ( +
+
+

+ Import Product +

+ Global FMCG +
+

+ Import this product to your Admin Catalogue. Selling price and outlet parameters will be managed directly in your Admin Catalogue. +

+ +
+ )}
+ + {/* Retail Packaging Details */} +
+

Retail Packaging Info

+ +
+
-
+ ); } diff --git a/src/components/InventoryView.tsx b/src/components/InventoryView.tsx index 50f560a..93ba709 100644 --- a/src/components/InventoryView.tsx +++ b/src/components/InventoryView.tsx @@ -35,7 +35,9 @@ import { Store, Activity, Award, - Filter + Filter, + Box, + ArrowLeft } from 'lucide-react'; import { ProductMatrixItem } from '../types'; import { @@ -45,13 +47,14 @@ import { useFiestaUpdateStockRequest, useFiestaGetStockRequests, } from '../services/fiestaQueries'; -import { FIESTA_TENANT_ID, str as fstr } from '../services/fiestaApi'; +import { FIESTA_TENANT_ID, FIESTA_PRIMARY_LOCATION_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, 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 { useCompare } from '../contexts/CompareContext'; @@ -75,6 +78,7 @@ export default function InventoryView({ }: InventoryViewProps) { const { setHideCompareBar } = useCompare(); const [searchTerm, setSearchTerm] = useState(''); + const [showCatalogueModal, setShowCatalogueModal] = useState(false); const navigate = useNavigate(); // ── Live stock across every outlet (Fiesta) ─────────────────────────────── @@ -108,7 +112,7 @@ export default function InventoryView({ const allStoreRows = storesStock.flatMap((s) => s.rows); - const [activeTab, setActiveTab] = useState<'catalog' | 'requests'>('catalog'); + const [activeTab, setActiveTab] = useState<'catalog' | 'requests' | 'global_catalogue'>('catalog'); const [isLocalSidebarOpen, setIsLocalSidebarOpen] = useState(true); const [selectedCategories, setSelectedCategories] = useState([]); const [hoveredAdminProduct, setHoveredAdminProduct] = useState(null); @@ -119,19 +123,22 @@ export default function InventoryView({ 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( - selectedAdminProduct.price - ? String(selectedAdminProduct.price) - : String(selectedAdminProduct.unitsSold > 0 ? Math.round(selectedAdminProduct.revenue / selectedAdminProduct.unitsSold) : 0) + existingPrice && existingPrice > 0 + ? String(existingPrice) + : (selectedAdminProduct.unitsSold > 0 ? String(Math.round(selectedAdminProduct.revenue / selectedAdminProduct.unitsSold)) : '') ); setIsSettingPrice(false); } else { setImportPrice(''); setIsSettingPrice(false); } - }, [selectedAdminProduct]); + }, [selectedProductId]); const products = useMemo(() => { const byId = new Map(); @@ -513,7 +520,7 @@ export default function InventoryView({ )} - )} {/* Content Section */}
-
-
-

{prod.name}

-

{prod.sku}

-
+
+

{prod.name}

+

{prod.sku}

-
-
- Units Sold - {prod.unitsSold.toLocaleString()} +
+
+ Sold: {prod.unitsSold.toLocaleString()} + ₹{prod.revenue.toLocaleString()}
-
- Revenue - ₹{prod.revenue.toLocaleString()} -
-
-
- {storeCat.has(prod.id) && storeCat.items.find(i => i.productid === prod.id)?.status === 'Active' ? ( -
- In Store Catalogue - -
+ {isPublished ? ( + ) : addingPriceProdId === prod.id ? ( -
e.stopPropagation()}> - +
e.stopPropagation()}> + setCardImportPrice(e.target.value)} placeholder="e.g. 50" - className="px-2 py-1.5 border border-slate-200 rounded-md text-[11px] font-bold focus:outline-none focus:border-[#662582]" + className="px-2 py-1 border border-slate-200 rounded-none text-[10px] font-bold focus:outline-none focus:border-[#662582]" autoFocus /> -
- +
+ @@ -650,16 +629,17 @@ export default function InventoryView({ setCardImportPrice(String(prod.price || (prod.unitsSold > 0 ? Math.round(prod.revenue / prod.unitsSold) : 0))); setAddingPriceProdId(prod.id); }} - 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" + className="w-full bg-[#f8fafc] hover:bg-[#662582] text-slate-600 hover:text-white border border-slate-200 hover:border-[#662582] px-3 py-2 rounded-none text-[10px] font-bold uppercase tracking-wider transition-all shadow-sm flex items-center justify-center gap-1.5 cursor-pointer" title="Add to Store Catalogue" > - Add to Store Catalogue + Add to Store )}
- ))} + ); + })}
@@ -859,6 +839,14 @@ export default function InventoryView({
)}
+ ) : activeTab === 'global_catalogue' ? ( +
+ setActiveTab('catalog')} + /> +
) : null} {/* ── Slide Drawer ── */} @@ -917,47 +905,71 @@ export default function InventoryView({ )}
- {/* Action Area */} -
-
- {storeCat.has(selectedAdminProduct.id) ? ( - - ) : ( -
-
- - setImportPrice(e.target.value)} - placeholder="e.g. 50" - className="px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:border-[#662582]" - /> -
- -
- )} - {!storeCat.has(selectedAdminProduct.id) && ( - - )} + {/* 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) && ( + + )} +
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index d5992fd..3225a21 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -38,7 +38,6 @@ export default function Sidebar({ const navItems = [ { id: 'dashboard' as MainSection, label: 'Dashboard', icon: LayoutDashboard }, { id: 'inventory' as MainSection, label: 'Products', icon: Layers }, - { id: 'catalogue' as MainSection, label: 'Global Catalogue', icon: Box }, { id: 'reports' as MainSection, label: 'Reports', icon: TrendingUp }, { id: 'dispatch' as MainSection, label: 'Console', icon: Truck }, { id: 'settings' as MainSection, label: 'Settings', icon: Settings } diff --git a/src/components/StoreCatalogView.tsx b/src/components/StoreCatalogView.tsx index 6f86396..45cf272 100644 --- a/src/components/StoreCatalogView.tsx +++ b/src/components/StoreCatalogView.tsx @@ -441,85 +441,59 @@ export default function StoreCatalogView({ locationid, storeName = 'your store', const pick = picks[p.id]; const picked = pick != null && pick.status !== 'Cancelled' && pick.status !== 'Approved'; return ( -
setSelectedProduct(p)} className="cursor-pointer 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"> - {/* Image Bento Block */} -
- {p.name} -
- - {stocked && ( -
- - In Store -
- )} -
- - {p.category.split(' / ')[0]} - -
+
setSelectedProduct(p)} 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 */} +
+ {p.name} +
- {/* Details Bento Block */} + {/* Content Section */}
-
-
-

{p.name}

-

{p.sku}

-
+
+

{p.name}

+

{p.sku}

-
-
- Price - {p.price > 0 ? `₹${p.price.toLocaleString('en-IN')}` : '—'} +
+
+ Unit: {p.unit || 'Pc'} + {p.price > 0 ? `₹${p.price.toLocaleString('en-IN')}` : '—'}
-
- Unit - {p.unit || 'Pc'} -
-
- {/* Action Bento Block */} -
{picked ? ( -
- - {picks[p.id].status === 'Approved' ? : - picks[p.id].status === 'Rejected' ? : + {picks[p.id].status === 'Approved' ? : + picks[p.id].status === 'Rejected' ? :
} {picks[p.id].status === 'Pending' ? 'Requested' : picks[p.id].status} ({picks[p.id].qty})
{picks[p.id].status === 'Pending' && ( - + )} - +
) : ( )}
@@ -668,40 +642,41 @@ export default function StoreCatalogView({ locationid, storeName = 'your store', ) : (
{finalFilteredInventory.map((it, i) => ( -
setSelectedProduct(it)} className="cursor-pointer 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"> - {/* Image Bento Block */} -
- {it.name} -
+
setSelectedProduct(it)} 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 */} +
+ {it.name} +
-
- +
+ {it.label}
- + {it.category.split(' / ')[0]}
- {/* Details Bento Block */} + {/* Content Section */}
-
-
-

{it.name}

-

{it.sku}

-
+
+

{it.name}

+

{it.sku}

-
- Live Stock - {(it.closing ?? 0).toLocaleString('en-IN')} +
+
+ Unit: {it.unit || 'Pc'} + Stock: {(it.closing ?? 0).toLocaleString('en-IN')} +
+ +
diff --git a/src/components/SuperAdminPage.tsx b/src/components/SuperAdminPage.tsx index 8d073e5..2ab7ee7 100644 --- a/src/components/SuperAdminPage.tsx +++ b/src/components/SuperAdminPage.tsx @@ -11,36 +11,66 @@ * scoped to a single tenant's own Settings page. */ -import React from 'react'; -import { Routes, Route, Navigate } from 'react-router-dom'; +import React, { useState } from 'react'; +import { Routes, Route, Navigate, useLocation } from 'react-router-dom'; import Header from './Header'; import AdminConsole from './AdminConsole'; +import CatalogueBrowser from './CatalogueBrowser'; +import SuperAdminSidebar, { type SuperAdminNavItem } from './SuperAdminSidebar'; +import { UserPlus, Box } from 'lucide-react'; import type { AuthUser } from '../services/auth'; +import { FIESTA_TENANT_ID, FIESTA_PRIMARY_LOCATION_ID } from '../services/fiestaApi'; interface SuperAdminPageProps { user: AuthUser; onLogout: () => void; } +const NAV_ITEMS: SuperAdminNavItem[] = [ + { id: 'onboarding', label: 'Onboard Tenant', icon: UserPlus }, + { id: 'catalogue', label: 'Global Catalogue', icon: Box }, +]; + export default function SuperAdminPage({ user, onLogout }: SuperAdminPageProps) { + const [sidebarOpen, setSidebarOpen] = useState(true); + const location = useLocation(); const profile = { name: user.name, role: 'Super Admin', email: user.email }; + const tenantId = user.tenantid || FIESTA_TENANT_ID; + + const currentPath = location.pathname.split('/').pop(); + const currentItem = NAV_ITEMS.find(item => item.id === currentPath) || NAV_ITEMS[0]; return ( -
+
{}} + isSidebarOpen={sidebarOpen} + onToggleSidebar={() => setSidebarOpen(!sidebarOpen)} onHelpClick={() => {}} onLogoutClick={onLogout} profile={profile} + storeContext={{ + storeName: currentItem.label, + icon: currentItem.icon + }} /> - {/* Header is fixed (h-16); pt-16 keeps content from rendering under it. */} -
- - } /> - } /> - -
+ +
+ setSidebarOpen(false)} + /> + +
+
+ + } /> + } /> + } /> + +
+
+
); } diff --git a/src/components/SuperAdminSidebar.tsx b/src/components/SuperAdminSidebar.tsx new file mode 100644 index 0000000..af50da4 --- /dev/null +++ b/src/components/SuperAdminSidebar.tsx @@ -0,0 +1,73 @@ +/** + * @license + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { type LucideIcon } from 'lucide-react'; +import { NavLink } from 'react-router-dom'; + +/** One entry in the super admin sidebar. */ +export interface SuperAdminNavItem { + id: string; + label: string; + icon: LucideIcon; +} + +interface SuperAdminSidebarProps { + items: SuperAdminNavItem[]; + /** Collapsed → icon-only rail; expanded → labels visible. */ + isOpen: boolean; + onClose: () => void; +} + +/** + * The Super Admin page's left rail. Visually identical to the admin `Sidebar`. + */ +export default function SuperAdminSidebar({ items, isOpen, onClose }: SuperAdminSidebarProps) { + return ( + <> + {/* Mobile Overlay */} + {isOpen && ( +
+ )} + + + + ); +} diff --git a/src/services/catalogueApi.ts b/src/services/catalogueApi.ts index 5805c46..d5ac890 100644 --- a/src/services/catalogueApi.ts +++ b/src/services/catalogueApi.ts @@ -77,17 +77,52 @@ export async function getImportedCatalogueRefs(tenantid: number, brand?: string) } export async function importCatalogueProducts(items: ImportCatalogueProductRequest[]) { - const res = await fetch(`${API_BASE}/products/importcatalogueproduct`, { + try { + const res = await fetch(`${API_BASE}/products/importcatalogueproduct`, { + method: "POST", + headers: { "Content-Type": "application/json", "Accept": "application/json" }, + body: JSON.stringify(items), + }); + if (res.ok) { + const json = await res.json(); + if (json && typeof json === 'object' && 'status' in json && json.status === false) { + console.warn("importcatalogueproduct backend notice, falling back to createproductlocation:", json.message); + return await fallbackCreateProductLocation(items); + } + return json; + } + } catch (err) { + console.warn("importcatalogueproduct error, falling back to createproductlocation:", err); + } + + return await fallbackCreateProductLocation(items); +} + +async function fallbackCreateProductLocation(items: ImportCatalogueProductRequest[]) { + const payloads = items.map(item => ({ + tenantid: item.tenantid, + locationid: item.locationid, + productid: item.catalogueid, + quantity: item.quantity || 1, + stocktype: item.stocktype || "in", + status: item.status || "Active", + retailprice: item.retailprice || 0, + productcost: item.productcost || 0, + taxpercent: item.taxpercent || 0 + })); + + const res = await fetch(`${API_BASE}/products/createproductlocation`, { method: "POST", headers: { "Content-Type": "application/json", "Accept": "application/json" }, - body: JSON.stringify(items), + body: JSON.stringify(payloads), }); - if (!res.ok) throw new Error("Failed to import products"); - const json = await res.json(); - if (json && typeof json === 'object' && 'status' in json && !json.status) { - throw new Error(json.message || "Failed to import products"); + + if (!res.ok) { + let errText = ""; + try { errText = await res.text(); } catch (_) {} + throw new Error(errText || `Failed to add product to catalogue (${res.status})`); } - return json; + return await res.json(); } export async function removeFromStoreCatalogue(tenantid: number, locationid: number, productid: number) { diff --git a/src/services/fiestaQueries.ts b/src/services/fiestaQueries.ts index 2f56546..5c8736e 100644 --- a/src/services/fiestaQueries.ts +++ b/src/services/fiestaQueries.ts @@ -941,14 +941,9 @@ export function useFiestaCreateProductLocation() { return useMutation({ mutationFn: (input: CreateProductLocationInput) => createProductLocation(input), onSuccess: (_, variables) => { - // Invalidate the store catalogue so it fetches the new item (prefix match) - qc.invalidateQueries({ - queryKey: ['fiesta', 'productLocations'] - }); - // Also invalidate productStocks so the Admin Catalogue UI updates - qc.invalidateQueries({ - queryKey: ['fiesta', 'productStocks'] - }); + qc.invalidateQueries({ queryKey: ['catalogue', 'imported'] }); + qc.invalidateQueries({ queryKey: ['fiesta', 'productLocations'] }); + qc.invalidateQueries({ queryKey: ['fiesta', 'productStocks'] }); }, onError: (error: any) => { alert(`Error adding product: ${error.message}`); @@ -961,9 +956,9 @@ export function useFiestaDeleteProductLocation() { return useMutation({ mutationFn: (input: DeleteProductLocationInput) => deleteProductLocation(input), onSuccess: (_, variables) => { - qc.invalidateQueries({ - queryKey: ['fiesta', 'productLocations'] - }); + qc.invalidateQueries({ queryKey: ['catalogue', 'imported'] }); + qc.invalidateQueries({ queryKey: ['fiesta', 'productLocations'] }); + qc.invalidateQueries({ queryKey: ['fiesta', 'productStocks'] }); }, }); }