|
|
|
|
@@ -4,6 +4,7 @@
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import React, { useState, useEffect, useMemo } from 'react';
|
|
|
|
|
import { useNavigate } from 'react-router-dom';
|
|
|
|
|
import {
|
|
|
|
|
Layers,
|
|
|
|
|
Search,
|
|
|
|
|
@@ -43,12 +44,9 @@ import {
|
|
|
|
|
useFiestaProductCategories,
|
|
|
|
|
useFiestaUpdateStockRequest,
|
|
|
|
|
useFiestaGetStockRequests,
|
|
|
|
|
useFiestaGlobalBrands,
|
|
|
|
|
useFiestaGlobalCategories,
|
|
|
|
|
useFiestaGlobalProducts,
|
|
|
|
|
} from '../services/fiestaQueries';
|
|
|
|
|
import { FIESTA_TENANT_ID, str as fstr } from '../services/fiestaApi';
|
|
|
|
|
import { stockRowToProduct, stockRowToInventory, globalRowToProduct } from '../services/fiestaMappers';
|
|
|
|
|
import { stockRowToProduct, stockRowToInventory } from '../services/fiestaMappers';
|
|
|
|
|
import { useStoreCatalogue } from '../services/storeCatalogue';
|
|
|
|
|
import BulkCartDrawer from './BulkCartDrawer';
|
|
|
|
|
import AwaitingApi from './AwaitingApi';
|
|
|
|
|
@@ -75,8 +73,9 @@ export default function InventoryView({
|
|
|
|
|
tenantId = FIESTA_TENANT_ID,
|
|
|
|
|
isSidebarOpen = false
|
|
|
|
|
}: InventoryViewProps) {
|
|
|
|
|
const { selectedProducts, toggleProduct, setIsComparing, clearSelection, setHideCompareBar } = useCompare();
|
|
|
|
|
const { setHideCompareBar } = useCompare();
|
|
|
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
|
|
|
const navigate = useNavigate();
|
|
|
|
|
|
|
|
|
|
// ── Live stock across every outlet (Fiesta) ───────────────────────────────
|
|
|
|
|
// This page is the admin's command surface. The GLOBAL CATALOG is the deduped
|
|
|
|
|
@@ -109,39 +108,12 @@ export default function InventoryView({
|
|
|
|
|
|
|
|
|
|
const allStoreRows = storesStock.flatMap((s) => s.rows);
|
|
|
|
|
|
|
|
|
|
const globalBrandsQ = useFiestaGlobalBrands();
|
|
|
|
|
const [globalCatalogSearch, setGlobalCatalogSearch] = useState('');
|
|
|
|
|
const [debouncedGlobalSearch, setDebouncedGlobalSearch] = useState('');
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const handler = setTimeout(() => {
|
|
|
|
|
setDebouncedGlobalSearch(globalCatalogSearch);
|
|
|
|
|
}, 500);
|
|
|
|
|
return () => clearTimeout(handler);
|
|
|
|
|
}, [globalCatalogSearch]);
|
|
|
|
|
|
|
|
|
|
const [globalSelectedBrand, setGlobalSelectedBrand] = useState<string>('');
|
|
|
|
|
|
|
|
|
|
const globalCategoriesQ = useFiestaGlobalCategories(globalSelectedBrand);
|
|
|
|
|
const [globalSelectedCategories, setGlobalSelectedCategories] = useState<string[]>([]);
|
|
|
|
|
|
|
|
|
|
const globalProductsQ = useFiestaGlobalProducts({
|
|
|
|
|
brand: globalSelectedBrand,
|
|
|
|
|
keyword: debouncedGlobalSearch,
|
|
|
|
|
pagesize: 500
|
|
|
|
|
});
|
|
|
|
|
const liveMasterCatalog = useMemo(() => globalProductsQ.data ?? [], [globalProductsQ.data]);
|
|
|
|
|
const masterCatalogProducts = useMemo(() => liveMasterCatalog.map(globalRowToProduct), [liveMasterCatalog]);
|
|
|
|
|
|
|
|
|
|
const [activeTab, setActiveTab] = useState<'catalog' | 'import_branding' | 'requests'>('catalog');
|
|
|
|
|
const [activeTab, setActiveTab] = useState<'catalog' | 'requests'>('catalog');
|
|
|
|
|
const [isLocalSidebarOpen, setIsLocalSidebarOpen] = useState(true);
|
|
|
|
|
const [isGlobalSidebarOpen, setIsGlobalSidebarOpen] = useState(true);
|
|
|
|
|
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
|
|
|
|
|
const [hoveredAdminProduct, setHoveredAdminProduct] = useState<ProductMatrixItem | null>(null);
|
|
|
|
|
const [localSearch, setLocalSearch] = useState('');
|
|
|
|
|
const storeCat = useStoreCatalogue();
|
|
|
|
|
const [globalCatalogPicks, setGlobalCatalogPicks] = useState<Set<string>>(new Set());
|
|
|
|
|
const [csvText, setCsvText] = useState('');
|
|
|
|
|
const [importPrice, setImportPrice] = useState<string>('');
|
|
|
|
|
const [isSettingPrice, setIsSettingPrice] = useState(false);
|
|
|
|
|
const [addingPriceProdId, setAddingPriceProdId] = useState<string | null>(null);
|
|
|
|
|
@@ -323,87 +295,10 @@ export default function InventoryView({
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Note: globalSelectedCategories is declared at the top now!
|
|
|
|
|
|
|
|
|
|
const globalCategories = useMemo(() => {
|
|
|
|
|
if (!globalCategoriesQ.data) return [];
|
|
|
|
|
// The new API returns an array of strings for categories
|
|
|
|
|
return (globalCategoriesQ.data as unknown as string[]).filter(Boolean);
|
|
|
|
|
}, [globalCategoriesQ.data]);
|
|
|
|
|
|
|
|
|
|
const toggleGlobalCategory = (cat: string) => {
|
|
|
|
|
setGlobalSelectedCategories((prev) =>
|
|
|
|
|
prev.includes(cat) ? [] : [cat], // Only allow one at a time for API compatibility, or filter client-side
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const filteredGlobalProducts = useMemo(() => {
|
|
|
|
|
let result = masterCatalogProducts;
|
|
|
|
|
// We already pass keyword to API, but can double filter client-side just in case
|
|
|
|
|
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;
|
|
|
|
|
}, [masterCatalogProducts, globalCatalogSearch, globalSelectedCategories]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const handleToggleProductExposure = (id: string) => {
|
|
|
|
|
// Left empty for now, as products are managed via storeCat
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Custom Raw CSV import
|
|
|
|
|
const handleCSVImport = () => {
|
|
|
|
|
const lines = csvText.split('\n').map(l => l.trim()).filter(l => l.length > 0 && !l.startsWith('Name'));
|
|
|
|
|
if (lines.length === 0) {
|
|
|
|
|
alert('CSV sequence contains no importable entries.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let parsedCount = 0;
|
|
|
|
|
const newProds: ProductMatrixItem[] = [];
|
|
|
|
|
|
|
|
|
|
lines.forEach(line => {
|
|
|
|
|
const parts = line.split(',').map(p => p.trim());
|
|
|
|
|
if (parts.length >= 2) {
|
|
|
|
|
const name = parts[0];
|
|
|
|
|
const sku = parts[1];
|
|
|
|
|
const category = parts[2] || 'Staples / Rice';
|
|
|
|
|
|
|
|
|
|
if (!products.some(p => p.sku === sku)) {
|
|
|
|
|
newProds.push({
|
|
|
|
|
id: String(products.length + newProds.length + 1),
|
|
|
|
|
name,
|
|
|
|
|
sku,
|
|
|
|
|
unitsSold: 0,
|
|
|
|
|
revenue: 0,
|
|
|
|
|
stockStatus: 'Healthy',
|
|
|
|
|
trend: 'flat',
|
|
|
|
|
image: 'https://images.unsplash.com/photo-1542838132-92c53300491e?auto=format&fit=crop&q=80&w=200',
|
|
|
|
|
category,
|
|
|
|
|
exposure: 'All Outlets',
|
|
|
|
|
verified: true,
|
|
|
|
|
isNew: true
|
|
|
|
|
});
|
|
|
|
|
parsedCount++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (parsedCount > 0) {
|
|
|
|
|
newProds.forEach(p => storeCat.add({ productid: p.id, name: p.name, sku: p.sku, category: p.category, price: p.price || 0, image: p.image, unit: p.exposure, qty: 100, status: 'Active' }));
|
|
|
|
|
alert(`Synchronized ${parsedCount} regional products into Catalogue database successfully!`);
|
|
|
|
|
} else {
|
|
|
|
|
alert('All the specified SKU codes are already active in the catalogue ledger.');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
const flattenedRequests = useMemo(() => {
|
|
|
|
|
return storeRequests
|
|
|
|
|
.filter(r => requestStoreFilter === 'All Stores' || r.locationname === requestStoreFilter)
|
|
|
|
|
@@ -412,21 +307,7 @@ export default function InventoryView({
|
|
|
|
|
.filter(([_, pick]) => requestStatusFilter === 'All Statuses' || String((pick as any).status).toLowerCase() === requestStatusFilter.toLowerCase())
|
|
|
|
|
.map(([requestId, pick]) => {
|
|
|
|
|
const productId = (pick as any).productid;
|
|
|
|
|
let product = products.find(p => String(p.id) === String(productId));
|
|
|
|
|
if (!product) {
|
|
|
|
|
const liveMatch = liveMasterCatalog.find((r: any) => String(r.productid) === String(productId));
|
|
|
|
|
if (liveMatch) {
|
|
|
|
|
product = {
|
|
|
|
|
id: String(liveMatch.productid),
|
|
|
|
|
name: String(liveMatch.productname || 'Unknown'),
|
|
|
|
|
sku: String(liveMatch.sku || `SKU-${liveMatch.productid}`),
|
|
|
|
|
image: String(liveMatch.productimage || 'https://images.unsplash.com/photo-1542838132-92c53300491e?auto=format&fit=crop&q=80&w=200'),
|
|
|
|
|
category: String(liveMatch.categoryname || 'Uncategorized'),
|
|
|
|
|
} as any;
|
|
|
|
|
} else {
|
|
|
|
|
product = masterCatalogProducts.find(p => String(p.id) === String(productId)) as any;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const product = products.find(p => String(p.id) === String(productId));
|
|
|
|
|
return {
|
|
|
|
|
locationid: store.locationid,
|
|
|
|
|
locationname: store.locationname,
|
|
|
|
|
@@ -438,7 +319,7 @@ export default function InventoryView({
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
.sort((a, b) => new Date(b.pickData.requestedAt).getTime() - new Date(a.pickData.requestedAt).getTime());
|
|
|
|
|
}, [storeRequests, products, requestStoreFilter, requestStatusFilter, liveMasterCatalog]);
|
|
|
|
|
}, [storeRequests, products, requestStoreFilter, requestStatusFilter]);
|
|
|
|
|
|
|
|
|
|
const requestingStores = useMemo(() => {
|
|
|
|
|
return Array.from(new Set(locations.map(l => l.locationname))).sort();
|
|
|
|
|
@@ -632,7 +513,7 @@ export default function InventoryView({
|
|
|
|
|
)}
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setActiveTab('import_branding')}
|
|
|
|
|
onClick={() => navigate('/admin/catalogue')}
|
|
|
|
|
className="flex items-center gap-1.5 bg-[#662582] hover:bg-purple-800 text-white px-2.5 py-1.5 rounded-lg text-[10px] font-bold transition-colors shadow-sm cursor-pointer h-[32px] whitespace-nowrap"
|
|
|
|
|
>
|
|
|
|
|
<Plus size={12} /> Import Product
|
|
|
|
|
@@ -786,443 +667,6 @@ export default function InventoryView({
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
) : activeTab === 'import_branding' ? (
|
|
|
|
|
<div className="flex flex-col xl:flex-row gap-6 mt-2 animate-in fade-in duration-300 flex-1 min-h-0 overflow-hidden pb-4">
|
|
|
|
|
|
|
|
|
|
{/* ── Sticky Sidebar (Filters) ── */}
|
|
|
|
|
{isGlobalSidebarOpen && (
|
|
|
|
|
<div className="w-full xl:w-64 shrink-0 flex flex-col gap-5 pr-2 pb-2">
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setActiveTab('catalog')}
|
|
|
|
|
className="flex items-center gap-1.5 bg-white border border-slate-200 text-slate-600 hover:bg-slate-50 hover:text-slate-900 px-4 py-2 rounded-xl text-sm font-bold transition-colors shadow-sm cursor-pointer shrink-0"
|
|
|
|
|
>
|
|
|
|
|
← Back to Product Catalogue
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<div className="bg-white border border-slate-200 shadow-sm overflow-hidden flex-1 min-h-0 flex flex-col">
|
|
|
|
|
<div className="p-4 bg-slate-50 border-b border-slate-100 shrink-0">
|
|
|
|
|
<h3 className="font-bold text-sm text-slate-800 flex items-center gap-2">
|
|
|
|
|
<Search size={16} className="text-[#662582]" /> Filter Global Product
|
|
|
|
|
</h3>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="p-4 space-y-5 overflow-y-auto custom-scrollbar">
|
|
|
|
|
<div>
|
|
|
|
|
<h3 className="text-[11px] font-extrabold text-slate-400 uppercase tracking-widest mb-3 flex items-center gap-1.5">
|
|
|
|
|
<Award size={14} className="text-[#662582]" /> Brands
|
|
|
|
|
</h3>
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
{globalBrandsQ.isLoading ? (
|
|
|
|
|
<span className="text-xs text-slate-400 ml-2">Loading brands...</span>
|
|
|
|
|
) : (globalBrandsQ.data || []).map((b) => {
|
|
|
|
|
const brandName = String(b.brand || b.name || b.brandname || b.id || 'Unknown');
|
|
|
|
|
return (
|
|
|
|
|
<label key={brandName} className="flex items-center gap-3 p-2 rounded-xl hover:bg-slate-50 cursor-pointer group transition-colors">
|
|
|
|
|
<div className="relative flex items-center justify-center">
|
|
|
|
|
<input
|
|
|
|
|
type="radio"
|
|
|
|
|
name="globalBrand"
|
|
|
|
|
checked={globalSelectedBrand === brandName}
|
|
|
|
|
onChange={() => {
|
|
|
|
|
setGlobalSelectedBrand(brandName);
|
|
|
|
|
setGlobalSelectedCategories([]);
|
|
|
|
|
}}
|
|
|
|
|
className="peer appearance-none w-5 h-5 border-2 border-slate-300 rounded-full checked:border-[#662582] checked:bg-[#662582] transition-colors cursor-pointer"
|
|
|
|
|
/>
|
|
|
|
|
<div className="absolute w-2 h-2 rounded-full bg-white opacity-0 peer-checked:opacity-100 pointer-events-none" />
|
|
|
|
|
</div>
|
|
|
|
|
<span className={`text-xs font-semibold select-none transition-colors ${globalSelectedBrand === brandName ? 'text-[#662582]' : 'text-slate-600 group-hover:text-slate-900'}`}>
|
|
|
|
|
{brandName.charAt(0).toUpperCase() + brandName.slice(1)}
|
|
|
|
|
{b.productCount ? ` (${b.productCount})` : ''}
|
|
|
|
|
</span>
|
|
|
|
|
</label>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{globalSelectedBrand && (
|
|
|
|
|
<>
|
|
|
|
|
<div className="border-t border-slate-100"></div>
|
|
|
|
|
<div>
|
|
|
|
|
<h3 className="text-[11px] font-extrabold text-slate-400 uppercase tracking-widest mb-3 flex items-center gap-1.5">
|
|
|
|
|
<Layers size={14} className="text-[#662582]" /> Categories
|
|
|
|
|
</h3>
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
{globalCategoriesQ.isLoading ? (
|
|
|
|
|
<span className="text-xs text-slate-400 ml-2">Loading categories...</span>
|
|
|
|
|
) : globalCategories.map((cat) => (
|
|
|
|
|
<label key={cat} className="flex items-center gap-3 p-2 rounded-xl hover:bg-slate-50 cursor-pointer group transition-colors">
|
|
|
|
|
<div className="relative flex items-center justify-center">
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
checked={globalSelectedCategories.includes(cat)}
|
|
|
|
|
onChange={() => toggleGlobalCategory(cat)}
|
|
|
|
|
className="peer appearance-none w-5 h-5 border-2 border-slate-300 rounded-lg checked:border-[#662582] checked:bg-[#662582] transition-colors cursor-pointer"
|
|
|
|
|
/>
|
|
|
|
|
<Check size={12} className="absolute text-white opacity-0 peer-checked:opacity-100 pointer-events-none" strokeWidth={3} />
|
|
|
|
|
</div>
|
|
|
|
|
<span className={`text-xs font-semibold select-none transition-colors ${globalSelectedCategories.includes(cat) ? 'text-[#662582]' : 'text-slate-600 group-hover:text-slate-900'}`}>
|
|
|
|
|
{cat}
|
|
|
|
|
</span>
|
|
|
|
|
</label>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{(globalSelectedCategories.length > 0 || globalCatalogSearch) && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => { setGlobalSelectedCategories([]); setGlobalCatalogSearch(''); }}
|
|
|
|
|
className="w-full py-2 bg-slate-100 text-slate-600 rounded-xl text-[10px] font-bold uppercase tracking-wider hover:bg-slate-200 transition-colors cursor-pointer border-none mt-4"
|
|
|
|
|
>
|
|
|
|
|
Clear All Filters
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* ── Main Content Area ── */}
|
|
|
|
|
<div className="flex-1 min-w-0 overflow-y-auto custom-scrollbar bg-white/40 backdrop-blur-md border border-[#e2e8f0] p-5 shadow-sm flex flex-col">
|
|
|
|
|
|
|
|
|
|
{/* Global Catalogue Header with Search and Filter */}
|
|
|
|
|
<div className="flex flex-col xl:flex-row xl:items-start justify-between mb-5 shrink-0 gap-4">
|
|
|
|
|
<div>
|
|
|
|
|
<h3 className="font-sans font-bold text-sm text-[#0f172a] flex items-center gap-1.5 flex-wrap">
|
|
|
|
|
{!isGlobalSidebarOpen && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setActiveTab('catalog')}
|
|
|
|
|
className="p-1 -ml-1 mr-0.5 hover:bg-slate-100 rounded-lg text-slate-500 hover:text-slate-900 transition-colors cursor-pointer"
|
|
|
|
|
title="Back to Product Catalogue"
|
|
|
|
|
>
|
|
|
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="m15 18-6-6 6-6"/></svg>
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
<Sparkles size={15} className="text-amber-500 animate-pulse" /> Global Catalogue
|
|
|
|
|
<div className="flex items-center gap-2 ml-2">
|
|
|
|
|
<span className="text-[10px] text-[#662582] font-bold bg-purple-50 px-2 py-0.5 rounded-lg border border-purple-100/50">
|
|
|
|
|
{filteredGlobalProducts.length} items available
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</h3>
|
|
|
|
|
<p className="text-[10px] text-zinc-400 font-medium mt-1">Select products from the global ledger to import them into your own catalogue.</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex flex-wrap items-center gap-3 w-full xl:w-auto">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setIsGlobalSidebarOpen(!isGlobalSidebarOpen)}
|
|
|
|
|
className="flex items-center gap-2 bg-white border border-slate-200 px-3 py-2 rounded-xl text-xs font-bold hover:bg-slate-50 transition-colors text-slate-700 shadow-sm cursor-pointer h-[36px]"
|
|
|
|
|
>
|
|
|
|
|
<Filter size={14} /> Filter
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<div className="relative flex-1 xl:w-64 h-[36px]">
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="Search global catalogue..."
|
|
|
|
|
value={globalCatalogSearch}
|
|
|
|
|
onChange={(e) => setGlobalCatalogSearch(e.target.value)}
|
|
|
|
|
className="w-full h-full pl-9 pr-4 py-2 bg-white border border-slate-200 rounded-xl text-xs text-slate-800 placeholder-slate-400 focus:outline-none focus:border-[#662582] transition-all shadow-sm font-medium"
|
|
|
|
|
/>
|
|
|
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 w-4 h-4" />
|
|
|
|
|
{globalCatalogSearch && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setGlobalCatalogSearch('')}
|
|
|
|
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 bg-transparent border-none cursor-pointer"
|
|
|
|
|
>
|
|
|
|
|
<X size={12} />
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{globalCatalogPicks.size > 0 && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => {
|
|
|
|
|
const newProds = masterCatalogProducts.filter(p => globalCatalogPicks.has(p.id) && !products.some(ext => ext.sku === p.sku));
|
|
|
|
|
if (newProds.length > 0) {
|
|
|
|
|
newProds.forEach(p => storeCat.add({ productid: String(p.id), name: p.name, sku: p.sku, category: p.category, price: 0, image: p.image, unit: 'unit', qty: 100, status: 'Active' }));
|
|
|
|
|
setGlobalCatalogPicks(new Set());
|
|
|
|
|
setActiveTab('catalog');
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
className="flex items-center gap-1.5 bg-[#662582] hover:bg-purple-800 text-white px-4 py-2 rounded-lg text-xs font-bold transition-colors shadow-sm cursor-pointer animate-in zoom-in-95"
|
|
|
|
|
>
|
|
|
|
|
<UploadCloud size={14} /> Import Selected ({globalCatalogPicks.size})
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
{filteredGlobalProducts.filter(p => p.isNew).length > 0 && (
|
|
|
|
|
<div className="hidden xl:flex items-center gap-2 bg-[#f8fafc] px-3 py-1.5 rounded-lg border border-purple-100/50 mt-1">
|
|
|
|
|
<Sparkles size={14} className="text-purple-600" />
|
|
|
|
|
<h4 className="text-[10px] font-bold text-purple-900 uppercase tracking-widest">Recently Added</h4>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex flex-col xl:flex-row gap-6">
|
|
|
|
|
{/* Left Side: Normal Catalogue */}
|
|
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
|
{!globalSelectedBrand ? (
|
|
|
|
|
<div className="flex flex-col items-center justify-center min-h-[400px] text-center px-4 gap-4">
|
|
|
|
|
<h3 className="text-xl font-bold text-slate-800">Select a Brand</h3>
|
|
|
|
|
<div className="w-16 h-1 bg-slate-200 rounded-full"></div>
|
|
|
|
|
<div className="flex items-center justify-center gap-2">
|
|
|
|
|
<span className="text-slate-500 text-sm whitespace-nowrap">
|
|
|
|
|
Please select a brand from the sidebar on the left to view and import products from the global catalogue.
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : globalProductsQ.isLoading ? (
|
|
|
|
|
<div className="flex flex-col items-center justify-center min-h-[400px] text-center px-4">
|
|
|
|
|
<div className="w-8 h-8 border-4 border-[#662582] border-t-transparent rounded-full animate-spin mb-4" />
|
|
|
|
|
<h3 className="text-sm font-bold text-slate-800">Loading products...</h3>
|
|
|
|
|
</div>
|
|
|
|
|
) : filteredGlobalProducts.filter(p => !p.isNew).length === 0 ? (
|
|
|
|
|
<div className="flex flex-col items-center justify-center min-h-[400px] text-center px-4">
|
|
|
|
|
<Search size={48} className="text-slate-200 mb-4" />
|
|
|
|
|
<h3 className="text-lg font-bold text-slate-800 mb-2">No Products Found</h3>
|
|
|
|
|
<p className="text-slate-500 text-sm max-w-sm">
|
|
|
|
|
We couldn't find any products matching your search or category filters for this brand.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className={`grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-2 ${!isGlobalSidebarOpen ? (!isSidebarOpen ? 'xl:grid-cols-5 2xl:grid-cols-6' : 'xl:grid-cols-4 2xl:grid-cols-5') : (!isSidebarOpen ? 'xl:grid-cols-4 2xl:grid-cols-5' : 'xl:grid-cols-3 2xl:grid-cols-4')} gap-3 pb-8`}>
|
|
|
|
|
{filteredGlobalProducts.filter(p => !p.isNew).map((prod) => {
|
|
|
|
|
const isSelected = globalCatalogPicks.has(prod.id);
|
|
|
|
|
const isAlreadyInCatalog = products.some(p => p.sku === prod.sku);
|
|
|
|
|
|
|
|
|
|
const wholesalePrice = prod.unitsSold > 0 ? Math.max(1, Math.round(prod.revenue / prod.unitsSold)) : Math.floor(Math.random() * 50) + 20;
|
|
|
|
|
const mrp = Math.round(wholesalePrice * 1.35);
|
|
|
|
|
const profit = mrp - wholesalePrice;
|
|
|
|
|
const profitMargin = Math.round((profit / mrp) * 100);
|
|
|
|
|
const globalSales = Math.floor(Math.random() * 5000) + 1000;
|
|
|
|
|
const rating = Number((4.0 + Math.random()).toFixed(1));
|
|
|
|
|
|
|
|
|
|
const premiumTagSeed = (prod.name.length + prod.sku.length) % 3;
|
|
|
|
|
const premiumTag = premiumTagSeed === 0 ? 'Great Deal' : premiumTagSeed === 1 ? 'High Margin' : 'Trending';
|
|
|
|
|
const tagColor = premiumTag === 'Great Deal' ? 'bg-emerald-100 text-emerald-800 border-emerald-200' :
|
|
|
|
|
premiumTag === 'High Margin' ? 'bg-amber-100 text-amber-800 border-amber-200' :
|
|
|
|
|
'bg-indigo-100 text-indigo-800 border-indigo-200';
|
|
|
|
|
const tagIcon = premiumTag === 'Great Deal' ? <CheckCircle size={10} /> :
|
|
|
|
|
premiumTag === 'High Margin' ? <Sparkles size={10} /> :
|
|
|
|
|
<TrendingUp size={10} />;
|
|
|
|
|
const sparklineColor = premiumTag === 'Great Deal' ? 'text-emerald-500' :
|
|
|
|
|
premiumTag === 'High Margin' ? 'text-amber-500' :
|
|
|
|
|
'text-indigo-500';
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={prod.id}
|
|
|
|
|
onClick={() => setSelectedAdminProduct(prod)}
|
|
|
|
|
className={`cursor-pointer bg-white/80 backdrop-blur-md border border-[#e2e8f0] rounded-2xl flex flex-col min-h-[340px] shadow-sm transition-all duration-300 relative group overflow-hidden ${
|
|
|
|
|
isSelected ? 'border-[#662582] shadow-[0_0_0_2px_#662582]' : 'hover:shadow-[0_12px_24px_rgba(99,102,241,0.06)] hover:border-[#662582]/40 hover:-translate-y-1'
|
|
|
|
|
} ${isAlreadyInCatalog ? 'opacity-60 grayscale-[50%]' : ''}`}
|
|
|
|
|
>
|
|
|
|
|
{/* Image Section - Top */}
|
|
|
|
|
<div className="w-full h-48 bg-white p-4 relative overflow-hidden shrink-0 border-b border-slate-100 flex items-center justify-center">
|
|
|
|
|
<img
|
|
|
|
|
src={prod.image}
|
|
|
|
|
alt={prod.name}
|
|
|
|
|
referrerPolicy="no-referrer"
|
|
|
|
|
className="w-full h-full object-contain mix-blend-multiply group-hover:scale-110 transition-transform duration-700 ease-in-out"
|
|
|
|
|
/>
|
|
|
|
|
<div className="absolute inset-0 bg-gradient-to-t from-slate-900/60 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
|
|
|
|
<div className="absolute top-2 left-2 flex items-center gap-2">
|
|
|
|
|
<span className={`px-1.5 py-0.5 rounded text-[8px] font-extrabold uppercase shadow-sm ${
|
|
|
|
|
prod.category.startsWith('Staples') ? 'bg-amber-100 text-amber-800' :
|
|
|
|
|
prod.category.startsWith('Groceries') ? 'bg-emerald-100 text-emerald-800' :
|
|
|
|
|
prod.category.startsWith('Beverages') ? 'bg-sky-100 text-sky-800' :
|
|
|
|
|
'bg-rose-100 text-rose-800'
|
|
|
|
|
}`}>
|
|
|
|
|
{prod.category.split(' / ')[0]}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
{isAlreadyInCatalog && (
|
|
|
|
|
<div className="absolute top-2 right-2 z-10 bg-emerald-100 text-emerald-800 px-2 py-0.5 rounded-md text-[9px] font-black tracking-widest shadow-sm flex items-center gap-1 border border-emerald-200 backdrop-blur-md">
|
|
|
|
|
<CheckCircle size={10} /> IN CATALOGUE
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Content Section */}
|
|
|
|
|
<div className="p-3 flex flex-col flex-1 gap-2">
|
|
|
|
|
<div className="flex items-start justify-between gap-2">
|
|
|
|
|
<div className="grow shrink-0 min-w-0 pb-2">
|
|
|
|
|
<h4 className="font-bold text-[#0f172a] text-xs leading-snug group-hover:text-[#662582] transition-colors line-clamp-2">{prod.name}</h4>
|
|
|
|
|
<p className="text-[10px] text-zinc-500 font-bold font-mono tracking-tight mt-1 mb-2">{prod.sku}</p>
|
|
|
|
|
<span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-[9px] font-black uppercase tracking-widest shadow-sm border ${tagColor}`}>
|
|
|
|
|
{tagIcon} {premiumTag}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex flex-col gap-2 p-2 bg-indigo-50/50 rounded-xl border border-indigo-100/50 mt-auto">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<span className="text-[9px] text-slate-500 uppercase tracking-wider font-extrabold">Price Range</span>
|
|
|
|
|
<span className="font-black text-indigo-900 font-mono text-xs">{prod.priceRange || 'N/A'}</span>
|
|
|
|
|
</div>
|
|
|
|
|
{prod.providers && prod.providers.length > 0 && (
|
|
|
|
|
<div className="flex flex-wrap gap-1">
|
|
|
|
|
{prod.providers.slice(0, 3).map(provider => (
|
|
|
|
|
<span key={provider} className="text-[8px] bg-white border border-indigo-100 text-indigo-600 px-1.5 py-0.5 rounded-md font-bold uppercase tracking-wider">
|
|
|
|
|
{provider}
|
|
|
|
|
</span>
|
|
|
|
|
))}
|
|
|
|
|
{prod.providers.length > 3 && (
|
|
|
|
|
<span className="text-[8px] text-indigo-400 font-bold self-center">+{prod.providers.length - 3} more</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2 pt-1 border-t border-slate-100">
|
|
|
|
|
<div className="flex justify-between items-center select-none">
|
|
|
|
|
<span className={`inline-flex items-center gap-1.5 text-[10px] font-bold tracking-tight ${isAlreadyInCatalog ? 'text-emerald-600' : 'text-zinc-500'}`}>
|
|
|
|
|
<span className={`w-1.5 h-1.5 rounded-full ${isAlreadyInCatalog ? 'bg-emerald-500 animate-pulse' : 'bg-zinc-300'}`} />
|
|
|
|
|
{isAlreadyInCatalog ? 'Synced to Local' : 'Available for Import'}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{isAlreadyInCatalog ? (
|
|
|
|
|
<div className="flex flex-wrap items-center justify-center gap-2 p-2 bg-emerald-50 rounded-xl border border-emerald-100/50">
|
|
|
|
|
<span className="inline-flex items-center gap-1.5 text-[10px] font-bold text-emerald-700 whitespace-nowrap"><CheckCircle size={13} /> Already in Catalogue</span>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<button
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
storeCat.add({ productid: String(prod.id), name: prod.name, sku: prod.sku, category: prod.category, price: 0, image: prod.image, unit: 'unit', qty: 100, status: 'Active' });
|
|
|
|
|
setActiveTab('catalog');
|
|
|
|
|
}}
|
|
|
|
|
className="w-full flex items-center justify-center gap-1.5 py-2.5 rounded-xl text-[11px] font-bold transition-all bg-[#662582]/5 text-[#662582] hover:bg-[#662582] hover:text-white cursor-pointer shadow-sm border border-[#662582]/20"
|
|
|
|
|
>
|
|
|
|
|
<Plus size={14} /> Import to Catalogue
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Right Side: Recently Added Items */}
|
|
|
|
|
{filteredGlobalProducts.filter(p => p.isNew).length > 0 && (
|
|
|
|
|
<div className="w-full xl:w-72 shrink-0 xl:border-l xl:border-purple-100/50 xl:pl-6 relative">
|
|
|
|
|
<div className="flex flex-col gap-md">
|
|
|
|
|
{filteredGlobalProducts.filter(p => p.isNew).map((prod) => {
|
|
|
|
|
const isAlreadyInCatalog = products.some(p => p.sku === prod.sku);
|
|
|
|
|
const rating = Number((4.0 + Math.random()).toFixed(1));
|
|
|
|
|
|
|
|
|
|
const tagIcon = <Sparkles size={10} />;
|
|
|
|
|
const tagColor = 'bg-indigo-100 text-indigo-800 border-indigo-200';
|
|
|
|
|
const premiumTag = prod.brand || 'Global';
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={prod.id}
|
|
|
|
|
onClick={() => setSelectedAdminProduct(prod)}
|
|
|
|
|
className={`bg-white/80 backdrop-blur-md border border-[#e2e8f0] rounded-2xl flex flex-col min-h-[340px] shadow-sm transition-all duration-300 relative group overflow-hidden hover:shadow-[0_12px_24px_rgba(99,102,241,0.06)] hover:border-[#662582]/40 hover:-translate-y-1 ${isAlreadyInCatalog ? 'opacity-60 grayscale-[50%]' : ''}`}
|
|
|
|
|
>
|
|
|
|
|
{/* Image Section - Top */}
|
|
|
|
|
<div className="w-full h-48 bg-white p-4 relative overflow-hidden shrink-0 border-b border-slate-100 flex items-center justify-center">
|
|
|
|
|
<img
|
|
|
|
|
src={prod.image}
|
|
|
|
|
alt={prod.name}
|
|
|
|
|
referrerPolicy="no-referrer"
|
|
|
|
|
className="w-full h-full object-contain mix-blend-multiply group-hover:scale-110 transition-transform duration-700 ease-in-out"
|
|
|
|
|
/>
|
|
|
|
|
<div className="absolute inset-0 bg-gradient-to-t from-slate-900/60 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
|
|
|
|
<div className="absolute top-2 left-2 flex items-center gap-2">
|
|
|
|
|
<span className={`px-1.5 py-0.5 rounded text-[8px] font-extrabold uppercase shadow-sm ${
|
|
|
|
|
prod.category.startsWith('Staples') ? 'bg-amber-100 text-amber-800' :
|
|
|
|
|
prod.category.startsWith('Groceries') ? 'bg-emerald-100 text-emerald-800' :
|
|
|
|
|
prod.category.startsWith('Beverages') ? 'bg-sky-100 text-sky-800' :
|
|
|
|
|
'bg-rose-100 text-rose-800'
|
|
|
|
|
}`}>
|
|
|
|
|
{prod.category.split(' / ')[0]}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
{isAlreadyInCatalog && (
|
|
|
|
|
<div className="absolute top-2 right-2 z-10 bg-emerald-100 text-emerald-800 px-2 py-0.5 rounded-md text-[9px] font-black tracking-widest shadow-sm flex items-center gap-1 border border-emerald-200 backdrop-blur-md">
|
|
|
|
|
<CheckCircle size={10} /> IN CATALOGUE
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Content Section */}
|
|
|
|
|
<div className="p-3 flex flex-col flex-1 gap-2">
|
|
|
|
|
<div className="flex items-start justify-between gap-2">
|
|
|
|
|
<div className="grow shrink-0 min-w-0 pb-2">
|
|
|
|
|
<h4 className="font-bold text-[#0f172a] text-xs leading-snug group-hover:text-[#662582] transition-colors line-clamp-2">{prod.name}</h4>
|
|
|
|
|
<p className="text-[10px] text-zinc-500 font-bold font-mono tracking-tight mt-1 mb-2">{prod.sku}</p>
|
|
|
|
|
<span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-[9px] font-black uppercase tracking-widest shadow-sm border ${tagColor}`}>
|
|
|
|
|
{tagIcon} {premiumTag}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex flex-col gap-2 p-2 bg-indigo-50/50 rounded-xl border border-indigo-100/50 mt-auto">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<span className="text-[9px] text-slate-500 uppercase tracking-wider font-extrabold">Price Range</span>
|
|
|
|
|
<span className="font-black text-indigo-900 font-mono text-xs">{prod.priceRange || 'N/A'}</span>
|
|
|
|
|
</div>
|
|
|
|
|
{prod.providers && prod.providers.length > 0 && (
|
|
|
|
|
<div className="flex flex-wrap gap-1">
|
|
|
|
|
{prod.providers.slice(0, 3).map(provider => (
|
|
|
|
|
<span key={provider} className="text-[8px] bg-white border border-indigo-100 text-indigo-600 px-1.5 py-0.5 rounded-md font-bold uppercase tracking-wider">
|
|
|
|
|
{provider}
|
|
|
|
|
</span>
|
|
|
|
|
))}
|
|
|
|
|
{prod.providers.length > 3 && (
|
|
|
|
|
<span className="text-[8px] text-indigo-400 font-bold self-center">+{prod.providers.length - 3} more</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2 pt-1 border-t border-slate-100">
|
|
|
|
|
<div className="flex justify-between items-center select-none">
|
|
|
|
|
<span className={`inline-flex items-center gap-1.5 text-[10px] font-bold tracking-tight ${isAlreadyInCatalog ? 'text-emerald-600' : 'text-zinc-500'}`}>
|
|
|
|
|
<span className={`w-1.5 h-1.5 rounded-full ${isAlreadyInCatalog ? 'bg-emerald-500 animate-pulse' : 'bg-zinc-300'}`} />
|
|
|
|
|
{isAlreadyInCatalog ? 'Synced to Local' : 'Available for Import'}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{isAlreadyInCatalog ? (
|
|
|
|
|
<div className="flex flex-wrap items-center justify-center gap-2 p-2 bg-emerald-50 rounded-xl border border-emerald-100/50">
|
|
|
|
|
<span className="inline-flex items-center gap-1.5 text-[10px] font-bold text-emerald-700 whitespace-nowrap"><CheckCircle size={13} /> Already in Catalogue</span>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<button
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
storeCat.add({ productid: String(prod.id), name: prod.name, sku: prod.sku, category: prod.category, price: 0, image: prod.image, unit: 'unit', qty: 100, status: 'Active' });
|
|
|
|
|
setActiveTab('catalog');
|
|
|
|
|
}}
|
|
|
|
|
className="w-full flex items-center justify-center gap-1.5 py-2.5 rounded-xl text-[11px] font-bold transition-all bg-[#662582]/5 text-[#662582] hover:bg-[#662582] hover:text-white cursor-pointer shadow-sm border border-[#662582]/20"
|
|
|
|
|
>
|
|
|
|
|
<Plus size={14} /> Import to Catalogue
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : activeTab === 'requests' ? (
|
|
|
|
|
<div className="flex-1 overflow-y-auto pr-2 custom-scrollbar animate-in fade-in duration-300">
|
|
|
|
|
<div className="flex flex-col lg:flex-row lg:justify-between lg:items-end gap-4 mb-2">
|
|
|
|
|
@@ -1475,14 +919,7 @@ export default function InventoryView({
|
|
|
|
|
|
|
|
|
|
{/* Action Area */}
|
|
|
|
|
<div className="space-y-3 pt-4 border-t border-slate-100">
|
|
|
|
|
{isAlreadyInCatalog ? (
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
{activeTab === 'import_branding' && (
|
|
|
|
|
<div className="flex items-center justify-center gap-2 p-4 bg-emerald-50 rounded-xl border border-emerald-200">
|
|
|
|
|
<CheckCircle size={18} className="text-emerald-600" />
|
|
|
|
|
<span className="text-sm font-semibold text-emerald-800">Synced to Local Catalogue</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{storeCat.has(selectedAdminProduct.id) ? (
|
|
|
|
|
<button onClick={() => storeCat.remove(selectedAdminProduct.id)} className="w-full flex items-center justify-center gap-2 py-3.5 rounded-xl bg-white text-rose-600 border border-slate-200 font-semibold text-sm transition-colors hover:bg-slate-50">
|
|
|
|
|
<X size={18} strokeWidth={2} /> Remove from Store Catalogue
|
|
|
|
|
@@ -1521,26 +958,8 @@ export default function InventoryView({
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="flex gap-3">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => {
|
|
|
|
|
storeCat.add({ productid: selectedAdminProduct.id, name: selectedAdminProduct.name, sku: selectedAdminProduct.sku, category: selectedAdminProduct.category, price: Number(importPrice) || 0, image: selectedAdminProduct.image, unit: selectedAdminProduct.exposure, qty: 100, status: 'Active' });
|
|
|
|
|
setSelectedAdminProduct(null);
|
|
|
|
|
setActiveTab('catalog');
|
|
|
|
|
}}
|
|
|
|
|
className="flex-1 flex items-center justify-center gap-2 py-3.5 rounded-xl text-sm font-semibold transition-colors bg-[#662582] text-white hover:bg-[#531e6a]"
|
|
|
|
|
>
|
|
|
|
|
<Plus size={18} strokeWidth={2} /> Import Local
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<p className="text-sm text-slate-500 leading-relaxed mt-2 border-t border-slate-100 pt-5">
|
|
|
|
|
This product is part of the Global Catalogue. Importing it will add it to your local inventory where you can manage pricing and availability.
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
<div className="mt-2">
|
|
|
|
|
<h4 className="text-[10px] font-black uppercase tracking-widest text-slate-400 mb-2">Retail Packaging Info</h4>
|
|
|
|
|
<FMCGHoverOverlay productId={selectedAdminProduct.id} category={selectedAdminProduct.category} productName={selectedAdminProduct.name} product={selectedAdminProduct} />
|
|
|
|
|
@@ -1644,37 +1063,6 @@ export default function InventoryView({
|
|
|
|
|
})()}
|
|
|
|
|
</SlideDrawer>
|
|
|
|
|
|
|
|
|
|
{/* ── Floating Import Button ── */}
|
|
|
|
|
{activeTab === 'import_branding' && selectedProducts.length > 0 && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => {
|
|
|
|
|
selectedProducts.forEach(sp => {
|
|
|
|
|
if (!storeCat.has(sp.id)) {
|
|
|
|
|
storeCat.add({
|
|
|
|
|
productid: sp.id,
|
|
|
|
|
name: sp.name,
|
|
|
|
|
sku: sp.sku,
|
|
|
|
|
category: sp.category,
|
|
|
|
|
price: sp.price || 0,
|
|
|
|
|
image: sp.image,
|
|
|
|
|
unit: 'unit',
|
|
|
|
|
qty: 100,
|
|
|
|
|
status: 'Draft'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
clearSelection();
|
|
|
|
|
setActiveTab('catalog');
|
|
|
|
|
}}
|
|
|
|
|
className="fixed bottom-8 right-8 z-[100] px-6 h-14 bg-[#662582] text-white rounded-full shadow-[0_10px_25px_rgba(102,37,130,0.5)] flex items-center justify-center gap-2.5 hover:scale-105 hover:bg-purple-800 transition-all duration-300 animate-in slide-in-from-bottom-10 group"
|
|
|
|
|
>
|
|
|
|
|
<Plus size={20} />
|
|
|
|
|
<span className="font-bold text-sm">Import {selectedProducts.length} Product{selectedProducts.length > 1 ? 's' : ''}</span>
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|