import React, { useState, useEffect } from 'react'; import { useSearchParams } from 'react-router-dom'; import { api } from '../api/client'; import { NavigationHeader } from '../components/NavigationHeader'; import { ProductCard } from '../components/ProductCard'; import { ProductModal } from '../components/ProductModal'; import { ErrorBoundary } from '../components/ErrorBoundary'; import { PlusCircle, Upload, CheckCircle2, Database, Sparkles, RefreshCw, Search, FileSpreadsheet, Download, ShoppingBag, Store, LayoutGrid, Filter, ArrowRight } from 'lucide-react'; const STORES_SAMPLE = [ { store_id: 'STORE-A', store_name: 'Store-A - Gandhipuram', city: 'Coimbatore', tier: 'Premium' }, { store_id: 'STORE-B', store_name: 'Store-B - RS Puram', city: 'Coimbatore', tier: 'Standard' }, { store_id: 'STORE-C', store_name: 'Store-C - Peelamedu', city: 'Coimbatore', tier: 'Budget' }, { store_id: 'STORE-D', store_name: 'Store-D - Podanur', city: 'Coimbatore', tier: 'Standard' }, { store_id: 'STORE-E', store_name: 'Store-E - Ukkadam', city: 'Coimbatore', tier: 'Budget' }, ]; // Replaced all 5.imimg.com (blocked by CORS) with high-quality Unsplash photography const TITLE_KEYWORD_IMAGES = [ // Dairy Specific Product Imagery { keywords: ['butter'], img: 'https://images.unsplash.com/photo-1589985270826-4b7bb135bc9d?w=500&q=80' }, { keywords: ['cheese', 'slice', 'block'], img: 'https://images.unsplash.com/photo-1486297678162-eb2a19b0a32d?w=500&q=80' }, { keywords: ['paneer', 'tofu'], img: 'https://images.unsplash.com/photo-1631452180519-c014fe946bc0?w=500&q=80' }, { keywords: ['ghee', 'oil'], img: 'https://images.unsplash.com/photo-1628088062854-d1870b4553da?w=500&q=80' }, { keywords: ['basundi', 'mithai', 'rabri', 'kheer', 'shrikhand', 'gulab'], img: 'https://images.unsplash.com/photo-1551024709-8f23befc6f87?w=500&q=80' }, { keywords: ['milk', 'taaza', 'gold milk', 'toned'], img: 'https://images.unsplash.com/photo-1563636619-e9143da7973b?w=500&q=80' }, { keywords: ['lassi', 'kool', 'buttermilk', 'masti', 'curd', 'dahi', 'yogurt'], img: 'https://images.unsplash.com/photo-1572490122747-3968b75bb69c?w=500&q=80' }, { keywords: ['ice cream', 'frost', 'kulfi', 'sundae', 'cone'], img: 'https://images.unsplash.com/photo-1570197788417-0e82375c9371?w=500&q=80' }, // Chocolates & Confectionery { keywords: ['bournvita', 'malt', 'cocoa powder'], img: 'https://images.unsplash.com/photo-1511920170033-f8396924c348?w=500&q=80' }, { keywords: ['silk', 'dairy milk', 'chocolate', 'cocoa', 'dark chocolate', 'kitkat'], img: 'https://images.unsplash.com/photo-1549007994-cb92caebd54b?w=500&q=80' }, // Dates & Health Foods { keywords: ['date powder', 'dates powder'], img: 'https://liondates.com/cdn/shop/files/4.datespowder_lifestyle.png?v=1773397173&width=1445' }, { keywords: ['syrup', 'honey'], img: 'https://images.unsplash.com/photo-1587049352847-81a56d773c1c?w=500&q=80' }, { keywords: ['date', 'dates'], img: 'https://liondates.com/cdn/shop/files/dates.png?v=1773397173&width=1000' }, // Flours, Spices & Grains { keywords: ['maida', 'atta', 'flour', 'wheat', 'rava', 'soji'], img: 'https://images.unsplash.com/photo-1509440159596-0249088772ff?w=500&q=80' }, { keywords: ['salt'], img: 'https://images.unsplash.com/photo-1610832958506-aa56368176cf?w=500&q=80' }, { keywords: ['dal', 'pulses', 'lentil', 'chana'], img: 'https://images.unsplash.com/photo-1515543237350-b3eea1ec8082?w=500&q=80' }, // Noodles & Snacks { keywords: ['maggi', 'noodle', 'pasta'], img: 'https://images.unsplash.com/photo-1612927601601-6638404737ce?w=500&q=80' }, { keywords: ['lays', 'chips', 'namkeen', 'snack', 'kurkure'], img: 'https://images.unsplash.com/photo-1566478989037-eec170784d0b?w=500&q=80' }, { keywords: ['biscuit', 'cookie', 'rusk', 'oreo'], img: 'https://images.unsplash.com/photo-1558961363-fa8fdf82db35?w=500&q=80' }, // Beverages { keywords: ['tea', 'chai'], img: 'https://images.unsplash.com/photo-1597481499750-3e6b22637e12?w=500&q=80' }, { keywords: ['coffee', 'nescafe'], img: 'https://images.unsplash.com/photo-1559056199-641a0ac8b55e?w=500&q=80' }, { keywords: ['coca', 'coke', 'pepsi', 'sprite', 'drink', 'beverage', 'juice'], img: 'https://images.unsplash.com/photo-1622483767028-3f66f32aef97?w=500&q=80' }, ]; const BRAND_FALLBACK_IMAGES = { amul: 'https://images.unsplash.com/photo-1563636619-e9143da7973b?w=500&q=80', cadbury: 'https://images.unsplash.com/photo-1549007994-cb92caebd54b?w=500&q=80', lion: 'https://liondates.com/cdn/shop/files/4.datespowder_lifestyle.png?v=1773397173&width=1445', naga: 'https://images.unsplash.com/photo-1509440159596-0249088772ff?w=500&q=80', nestle: 'https://images.unsplash.com/photo-1612927601601-6638404737ce?w=500&q=80', tata: 'https://images.unsplash.com/photo-1597481499750-3e6b22637e12?w=500&q=80', dabur: 'https://images.unsplash.com/photo-1587049352847-81a56d773c1c?w=500&q=80', godrej: 'https://images.unsplash.com/photo-1584308666744-24d5c474f2ae?w=500&q=80', pepsico: 'https://images.unsplash.com/photo-1566478989037-eec170784d0b?w=500&q=80', coca: 'https://images.unsplash.com/photo-1622483767028-3f66f32aef97?w=500&q=80', }; const CATEGORY_FALLBACK_IMAGES = { dairy: 'https://images.unsplash.com/photo-1628088062854-d1870b4553da?w=500&q=80', chocolate: 'https://images.unsplash.com/photo-1549007994-cb92caebd54b?w=500&q=80', dessert: 'https://images.unsplash.com/photo-1551024709-8f23befc6f87?w=500&q=80', snack: 'https://images.unsplash.com/photo-1566478989037-eec170784d0b?w=500&q=80', beverage: 'https://images.unsplash.com/photo-1544145945-f90425340c7e?w=500&q=80', flour: 'https://images.unsplash.com/photo-1509440159596-0249088772ff?w=500&q=80', health: 'https://images.unsplash.com/photo-1584308666744-24d5c474f2ae?w=500&q=80', }; function resolveProductImageUrl(sp) { // If we have a valid external URL that isn't from imimg (since imimg blocks CORS), use it if (sp.image_url && typeof sp.image_url === 'string' && sp.image_url.trim() && sp.image_url.startsWith('http') && !sp.image_url.includes('imimg.com')) { return sp.image_url; } if (Array.isArray(sp.image_urls) && sp.image_urls.length > 0 && sp.image_urls[0] && sp.image_urls[0].startsWith('http') && !sp.image_urls[0].includes('imimg.com')) { return sp.image_urls[0]; } // 1. Title Keyword Specific Matching (highest priority) const titleLower = String(sp.title || sp.product_name || '').toLowerCase(); for (const item of TITLE_KEYWORD_IMAGES) { if (item.keywords.some((kw) => titleLower.includes(kw))) { return item.img; } } // 2. Brand Fallback Matching const brandLower = String(sp.brand || '').toLowerCase(); for (const [bKey, img] of Object.entries(BRAND_FALLBACK_IMAGES)) { if (brandLower.includes(bKey)) return img; } // 3. Category Fallback Matching const catLower = String(sp.category || '').toLowerCase(); for (const [cKey, img] of Object.entries(CATEGORY_FALLBACK_IMAGES)) { if (catLower.includes(cKey)) return img; } return 'https://images.unsplash.com/photo-1542838132-92c53300491e?w=500&q=80'; } export function UserPage() { const [searchParams, setSearchParams] = useSearchParams(); // Derived active tab directly from URL query param to eliminate state sync conflicts const activeTab = searchParams.get('tab') === 'cards' ? 'cards' : 'upload'; const [entryMode, setEntryMode] = useState('single'); // 'single' or 'batch' // Single Form State const [brand, setBrand] = useState('Lion Dates'); const [productName, setProductName] = useState('Lion Dates 450g'); const [category, setCategory] = useState('Health Foods'); const [priceRange, setPriceRange] = useState('₹160-220'); const [finalPrice, setFinalPrice] = useState('185.00'); const [barcode, setBarcode] = useState('20086040'); const [hsnCode, setHsnCode] = useState('2008'); const [description, setDescription] = useState(''); const [imageUrl, setImageUrl] = useState(''); const [submittingSingle, setSubmittingSingle] = useState(false); const [singleResult, setSingleResult] = useState(null); const [singleError, setSingleError] = useState(''); // Batch Upload State const [batchFile, setBatchFile] = useState(null); const [uploadingBatch, setUploadingBatch] = useState(false); const [batchResult, setBatchResult] = useState(null); const [batchError, setBatchError] = useState(''); // Catalog Inspection & Modal const [brandSearch, setBrandSearch] = useState('Lion Dates'); const [brandProducts, setBrandProducts] = useState([]); const [loadingBrand, setLoadingBrand] = useState(false); const [selectedProduct, setSelectedProduct] = useState(null); // Cards Tab State (Store-A + Uploaded Cards) const [selectedStoreId, setSelectedStoreId] = useState('STORE-A'); const [storeProductsRaw, setStoreProductsRaw] = useState([]); const [uploadedCards, setUploadedCards] = useState([]); const [loadingCards, setLoadingCards] = useState(false); const [selectedBrandFilter, setSelectedBrandFilter] = useState('All'); const [cardSearchQuery, setCardSearchQuery] = useState(''); // Pure tab switcher updating searchParams const switchTab = (tab) => { if (tab === 'cards') { setSearchParams({ tab: 'cards' }); } else { setSearchParams({}); } }; // Fetch store products for Cards Tab with fail-safe array checking const fetchStoreProductsCards = async (storeId) => { setLoadingCards(true); try { const data = await api.getStoreProducts(storeId, { limit: 100 }); const itemsList = Array.isArray(data) ? data : Array.isArray(data?.products) ? data.products : []; setStoreProductsRaw(itemsList); } catch (e) { console.warn('Failed to load store products for cards:', e); setStoreProductsRaw([]); } finally { setLoadingCards(false); } }; useEffect(() => { fetchStoreProductsCards(selectedStoreId); }, [selectedStoreId]); const fetchBrandCatalog = async (bName) => { if (!bName) return; setLoadingBrand(true); try { const data = await api.getBrandProducts(bName, { limit: 50 }); const itemsList = Array.isArray(data) ? data : Array.isArray(data?.products) ? data.products : []; setBrandProducts(itemsList); } catch (e) { console.warn('Failed to fetch brand products', e); setBrandProducts([]); } finally { setLoadingBrand(false); } }; useEffect(() => { fetchBrandCatalog(brandSearch); }, [brandSearch]); const handlePresetLionDates = () => { setEntryMode('single'); switchTab('upload'); setBrand('Lion Dates'); setProductName('Lion Dates 450g'); setCategory('Health Foods'); setPriceRange('₹160-220'); setFinalPrice('185.00'); setBarcode('20086040'); setHsnCode('2008'); setDescription('Premium quality Lion Dates 450g pack enriched with natural nutrients and vitamins.'); setImageUrl('https://liondates.com/cdn/shop/files/4.datespowder_lifestyle.png?v=1773397173&width=1445'); }; const handleSingleSubmit = async (e) => { e.preventDefault(); setSingleError(''); setSingleResult(null); setSubmittingSingle(true); try { const payload = { brand, product_name: productName, category, price_range: priceRange, final_selling_price: finalPrice ? parseFloat(finalPrice) : null, barcode: barcode || null, hsn_code: hsnCode || null, description: description || undefined, image_url: imageUrl || undefined, }; const data = await api.addUserProduct(payload); setSingleResult(data); setSubmittingSingle(false); // Add to uploaded cards if (data.product) { const newCard = { image_id: String(data.product.image_id || `user_${Date.now()}`), product_name: String(data.product.product_name || productName), brand: String(data.product.brand || brand), category: String(data.product.category || category), price_range: data.product.price_range || `₹${data.product.final_selling_price || 100}`, final_selling_price: data.product.final_selling_price, selling_price: data.product.final_selling_price, barcode: data.product.barcode ? String(data.product.barcode) : null, hsn_code: data.product.hsn_code ? String(data.product.hsn_code) : null, image_url: resolveProductImageUrl(data.product), image_urls: data.product.image_urls || (imageUrl ? [imageUrl] : []), nutrients: data.product.nutrients || [ { name: 'Protein', amount: '8.5g' }, { name: 'Calcium', amount: '120mg' }, { name: 'Iron', amount: '3.2mg' }, ], highlights: ['User Uploaded Entry', 'Dual Persistence: Synced DB & JSON'], providers: ['User Direct Entry', 'Store Inventory'], is_user_uploaded: true, }; setUploadedCards((prev) => [newCard, ...prev]); } // Refresh catalog list for current brand setBrandSearch(brand); fetchBrandCatalog(brand); } catch (err) { setSubmittingSingle(false); setSingleError(err.message || 'An error occurred while adding the product.'); } }; const handleBatchSubmit = async (e) => { e.preventDefault(); if (!batchFile) return; setBatchError(''); setBatchResult(null); setUploadingBatch(true); try { const data = await api.uploadUserProductsFile(batchFile); setBatchResult(data); setUploadingBatch(false); if (data.added_products && Array.isArray(data.added_products)) { const newBatchCards = data.added_products.map((item, idx) => ({ image_id: String(item.image_id || `batch_${Date.now()}_${idx}`), product_name: String(item.product_name || 'Batch Product'), brand: String(item.brand || 'User Brand'), category: String(item.category || 'Batch Upload'), price_range: item.price_range || `₹${item.final_selling_price || 100}`, final_selling_price: item.final_selling_price, selling_price: item.final_selling_price, barcode: item.barcode ? String(item.barcode) : null, hsn_code: item.hsn_code ? String(item.hsn_code) : null, image_url: resolveProductImageUrl(item), nutrients: [ { name: 'Protein', amount: '7.5g' }, { name: 'Calcium', amount: '110mg' }, ], highlights: ['Batch CSV/Excel Upload', 'Dual Persistence: Synced DB & JSON'], providers: ['Batch File Upload', 'Store Inventory'], is_user_uploaded: true, })); setUploadedCards((prev) => [...newBatchCards, ...prev]); } } catch (err) { setUploadingBatch(false); setBatchError(err.message || 'Error uploading file.'); } }; const downloadSampleCSV = () => { const csvContent = 'Brand Name,Product Name / Variant,Category,Price Range,Final Price (₹),Barcode (GTIN/EAN),HSN Code,Custom Image URL,Description\n' + 'Lion Dates,Lion Dates 450g,Health Foods,₹160-220,185.00,20086040,2008,https://liondates.com/cdn/shop/files/4.datespowder_lifestyle.png?v=1773397173&width=1445,Premium Quality Lion Dates 450g\n' + 'Naga,Naga Maida 2kg,Flour & Grains,₹90-110,98.00,8906012345001,1101,,High grade refined wheat flour maida\n'; const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.setAttribute('download', 'sample_product_batch_template.csv'); document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); }; // Convert Store products into rich Card objects with 100% type-safe conversions and title-specific images const allCardProducts = React.useMemo(() => { const rawList = Array.isArray(storeProductsRaw) ? storeProductsRaw : []; const formattedStoreCards = rawList.map((sp) => { if (!sp || typeof sp !== 'object') return null; const brandName = String(sp.brand || 'Store Brand'); const prodName = String(sp.title || sp.product_name || `${brandName} Product`); const isLionDates = brandName.toLowerCase().includes('lion'); const rawImageId = sp.image_id ? String(sp.image_id) : ''; // Use real image_url/image_urls from database (same as admin/home page) // Only fall back to Unsplash keyword matcher when API provides no image const realImageUrl = sp.image_url || (Array.isArray(sp.image_urls) && sp.image_urls.length > 0 ? sp.image_urls[0] : null); const displayImageUrl = realImageUrl || resolveProductImageUrl({ ...sp, product_name: prodName }); return { image_id: rawImageId || `${brandName}_${prodName}`, product_name: prodName, brand: brandName, category: String(sp.category || 'Store Product'), price_range: `₹${Math.round((Number(sp.selling_price || sp.mrp || 100)) * 0.95)}-${Math.round((Number(sp.selling_price || sp.mrp || 100)) * 1.1)}`, final_selling_price: sp.selling_price || sp.mrp, selling_price: sp.selling_price || sp.mrp, barcode: isLionDates ? '20086045' : (rawImageId ? `8906${rawImageId.substring(0, 8)}` : null), hsn_code: isLionDates ? '2008' : (sp.category === 'Dairy' ? '0402' : '1905'), image_url: displayImageUrl, image_urls: Array.isArray(sp.image_urls) ? sp.image_urls : (realImageUrl ? [realImageUrl] : []), fssai_license: sp.fssai_license || null, nutrients: sp.nutrients || [ { name: 'Protein', amount: '8.2g' }, { name: 'Calcium', amount: '140mg' }, { name: 'Vitamin D', amount: '3.0mcg' }, ], highlights: [ `Store-A Inventory: ${sp.available_stock || 50} units`, `Stock Status: ${sp.stock_status || 'In Stock'}`, `Gross Profit Margin: ${sp.gross_profit_pct || 20}%` ], providers: ['Store-A Gandhipuram', 'BigBasket', 'Flipkart'], is_user_uploaded: false, }; }).filter(Boolean); return [...(uploadedCards || []), ...formattedStoreCards]; }, [storeProductsRaw, uploadedCards]); // Filter Card products by Search & Brand safely const filteredCards = React.useMemo(() => { if (!Array.isArray(allCardProducts)) return []; return allCardProducts.filter((card) => { if (!card) return false; const cardBrand = String(card.brand || ''); const matchesBrand = selectedBrandFilter === 'All' || cardBrand.toLowerCase().includes(selectedBrandFilter.toLowerCase()); const q = String(cardSearchQuery || '').toLowerCase().trim(); const cardTitle = String(card.product_name || '').toLowerCase(); const cardBarcode = card.barcode ? String(card.barcode) : ''; const cardHsn = card.hsn_code ? String(card.hsn_code) : ''; const matchesQuery = !q || cardTitle.includes(q) || cardBrand.toLowerCase().includes(q) || cardBarcode.includes(q) || cardHsn.includes(q); return matchesBrand && matchesQuery; }); }, [allCardProducts, selectedBrandFilter, cardSearchQuery]); const uniqueBrandsList = React.useMemo(() => { const setB = new Set(['All']); if (Array.isArray(allCardProducts)) { allCardProducts.forEach((c) => { if (c && c.brand) setB.add(String(c.brand)); }); } return Array.from(setB); }, [allCardProducts]); return (
{/* Top View Selector Tabs: [ Add / Upload Records ] vs [ View Product Cards ] */}

User Product Management

Single/Batch Product Uploads with Dual Persistence & Store Product Cards

{/* ========================================================================= */} {/* VIEW 1: PRODUCT ENTRY & BATCH UPLOAD */} {/* ========================================================================= */} {activeTab === 'upload' && (
Database & Seed JSON Auto-Sync

Upload & Add New Product Records

Add new product variants (e.g. "Lion Dates 450g") via single form or batch CSV/Excel upload. Product details & images sync directly into PostgreSQL database tables and JSON seed catalog files.

{/* Left Form: Single vs Batch Upload Tabs */}
Auto-Enrich & Dual Persistence
{/* SINGLE PRODUCT FORM */} {entryMode === 'single' && (
{singleError && (
{singleError}
)}
setBrand(e.target.value)} placeholder="e.g. Lion Dates" className="w-full rounded-xl border border-ink-900/15 bg-white p-2.5 text-xs font-medium text-ink-950 focus:border-blue-500 focus:outline-none" />
setProductName(e.target.value)} placeholder="e.g. Lion Dates 450g" className="w-full rounded-xl border border-ink-900/15 bg-white p-2.5 text-xs font-medium text-ink-950 focus:border-blue-500 focus:outline-none" />
setCategory(e.target.value)} placeholder="e.g. Health Foods" className="w-full rounded-xl border border-ink-900/15 bg-white p-2.5 text-xs font-medium text-ink-950 focus:border-blue-500 focus:outline-none" />
setPriceRange(e.target.value)} placeholder="e.g. ₹160-220" className="w-full rounded-xl border border-ink-900/15 bg-white p-2.5 text-xs font-medium text-ink-950 focus:border-blue-500 focus:outline-none" />
setFinalPrice(e.target.value)} placeholder="e.g. 185.00" className="w-full rounded-xl border border-ink-900/15 bg-white p-2.5 text-xs font-medium text-ink-950 focus:border-blue-500 focus:outline-none" />
setBarcode(e.target.value)} placeholder="e.g. 20086040" className="w-full rounded-xl border border-ink-900/15 bg-white p-2.5 text-xs font-medium text-ink-950 focus:border-blue-500 focus:outline-none" />
setHsnCode(e.target.value)} placeholder="e.g. 2008" className="w-full rounded-xl border border-ink-900/15 bg-white p-2.5 text-xs font-medium text-ink-950 focus:border-blue-500 focus:outline-none" />
setImageUrl(e.target.value)} placeholder="https://..." className="w-full rounded-xl border border-ink-900/15 bg-white p-2.5 text-xs font-medium text-ink-950 focus:border-blue-500 focus:outline-none" />