-
handleRoleSelect('admin')}
- className={`flex items-center justify-center gap-2 py-3 px-4 rounded-lg text-xs font-bold transition-all ${
- selectedRole === 'admin'
- ? 'bg-amber-500 text-slate-950 shadow-md scale-[1.02]'
- : 'text-slate-400 hover:text-white hover:bg-slate-800/50'
- }`}
- >
- Admin Account
-
-
-
handleRoleSelect('user')}
- className={`flex items-center justify-center gap-2 py-3 px-4 rounded-lg text-xs font-bold transition-all ${
- selectedRole === 'user'
- ? 'bg-blue-500 text-white shadow-md scale-[1.02]'
- : 'text-slate-400 hover:text-white hover:bg-slate-800/50'
- }`}
- >
- User Account
-
+
+ {/* Single account banner - replaces the old Admin/User tab pair */}
+
+ Administrator Sign-In
{error && (
-
+
{error}
)}
@@ -97,31 +79,31 @@ export function LoginPage() {
{/* Login Form */}
@@ -139,7 +121,7 @@ export function LoginPage() {
{/* Footer info */}
- Powered by pgvector RAG Model & Multi-Store Nutrition Intelligence Engine
+ Powered by pgvector RAG Model & Multi-Store Nutrition Intelligence Engine
diff --git a/src/pages/NutritionAnalyticsPage.jsx b/src/pages/NutritionAnalyticsPage.jsx
deleted file mode 100644
index aa7ab4e..0000000
--- a/src/pages/NutritionAnalyticsPage.jsx
+++ /dev/null
@@ -1,215 +0,0 @@
-import { useEffect, useRef, useState } from 'react';
-import {
- BarChart, Bar, PieChart, Pie, Cell, XAxis, YAxis,
- Tooltip, ResponsiveContainer, CartesianGrid,
-} from 'recharts';
-import {
- Apple, Award, Beef, Droplet, Flame, HeartPulse, Play, RefreshCw, Wheat, UploadCloud,
-} from 'lucide-react';
-import { api } from '../api/client';
-import { Spinner, EmptyState } from '../components/Atoms';
-import { NavigationHeader } from '../components/NavigationHeader';
-import { UploadExcelModal } from '../components/UploadExcelModal';
-
-const PALETTE = ['#e2a33d', '#3f8556', '#8b2e3c', '#2c3e63', '#c2872a'];
-
-export function NutritionAnalyticsPage() {
- const [dashboard, setDashboard] = useState(null);
- const [loading, setLoading] = useState(true);
- const [job, setJob] = useState(null);
- const [isUploadOpen, setIsUploadOpen] = useState(false);
- const pollRef = useRef(null);
-
- const load = () => {
- setLoading(true);
- api.getNutritionAnalyticsDashboard(8).then(setDashboard).catch(() => setDashboard(null)).finally(() => setLoading(false));
- };
-
- useEffect(() => { load(); }, []);
-
- useEffect(() => {
- if (!job || job.status === 'done' || job.status === 'failed') {
- if (pollRef.current) clearInterval(pollRef.current);
- if (job?.status === 'done') load();
- return;
- }
- pollRef.current = setInterval(() => {
- api.getNutritionIntelligenceJob(job.job_id).then(setJob).catch(() => {});
- }, 2000);
- return () => clearInterval(pollRef.current);
- }, [job]);
-
- const runEnrich = () => api.enrichNutrition({}).then(setJob);
- const runTrain = () => api.trainNutritionModels().then(setJob);
-
- const distribution = dashboard?.distribution;
- const enrichmentCounts = distribution?.enrichment_status || {};
- const totalTracked = Object.values(enrichmentCounts).reduce((a, b) => a + b, 0);
- const hasData = totalTracked > 0;
-
- const distributionData = distribution?.health_score_distribution
- ? Object.entries(distribution.health_score_distribution).map(([bucket, count]) => ({ bucket, count }))
- : [];
-
- return (
-
-
-
-
-
-
-
Nutrition data pipeline
-
- {hasData
- ? `${enrichmentCounts.verified || 0} verified · ${enrichmentCounts.partial || 0} partial · ${enrichmentCounts.unavailable || 0} unavailable of ${totalTracked} tracked products`
- : 'No products enriched yet - run enrichment or upload Excel/CSV data.'}
-
- {job && (
-
- {job.kind === 'enrich' ? 'Enrichment' : 'Training'} {job.status}
- {job.total ? ` (${job.processed}/${job.total})` : ''}
- {job.detail ? ` - ${job.detail}` : ''}
-
- )}
-
-
-
setIsUploadOpen(true)}
- className="flex items-center gap-1.5 rounded-full bg-amber-600 px-3.5 py-1.5 text-xs font-semibold text-white transition hover:bg-amber-700 shadow-xs active:scale-95"
- >
-
- Upload Nutrition (Excel / CSV)
-
-
-
- Auto Enrich OFF
-
-
-
- Train Similarity
-
-
-
-
- {loading ? (
-
- ) : !hasData ? (
-
- ) : (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {distributionData.length > 0 && (
-
-
-
-
- e.bucket}>
- {distributionData.map((_, i) => | )}
-
-
-
-
-
-
- )}
-
-
-
-
-
-
-
- >
- )}
-
-
-
setIsUploadOpen(false)}
- tabType="nutrition"
- tabTitle="Nutritional Intelligence Facts"
- onSuccess={() => load()}
- />
-
- );
-}
-
-function ChartCard({ title, icon: Icon, children }) {
- return (
-
-
- {title}
-
- {children}
-
- );
-}
-
-function Leaderboard({ title, icon: Icon, items = [], field, unit }) {
- return (
-
-
- {title}
-
- {items.length === 0 ? (
-
No data yet.
- ) : (
-
- {items.map((p, i) => (
-
-
- #{i + 1}
- {p.product_name || p.image_id}
- {p.brand}
-
-
- {p[field] != null ? `${Number(p[field]).toFixed(1)} ${unit}` : '–'}
-
-
- ))}
-
- )}
-
- );
-}
diff --git a/src/pages/StoresPage.jsx b/src/pages/StoresPage.jsx
deleted file mode 100644
index 713f1c3..0000000
--- a/src/pages/StoresPage.jsx
+++ /dev/null
@@ -1,228 +0,0 @@
-import { useEffect, useState, useCallback } from 'react';
-import {
- Store as StoreIcon, TrendingUp, IndianRupee, PackageCheck, PackageX,
- PackageSearch, BarChart3, Search, UploadCloud,
-} from 'lucide-react';
-import { api } from '../api/client';
-import { Spinner, EmptyState } from '../components/Atoms';
-import { NavigationHeader } from '../components/NavigationHeader';
-import { UploadExcelModal } from '../components/UploadExcelModal';
-
-const STOCK_STATUS_STYLES = {
- 'In Stock': 'bg-leaf-100 text-leaf-600',
- 'Low Stock': 'bg-amber-100 text-amber-600',
- 'Out of Stock': 'bg-maroon-100 text-maroon-600',
- 'Overstocked': 'bg-ink-100 text-ink-700',
-};
-
-export function StoresPage() {
- const [stores, setStores] = useState([]);
- const [storesLoading, setStoresLoading] = useState(true);
- const [selectedStore, setSelectedStore] = useState(null);
-
- const [products, setProducts] = useState([]);
- const [productsLoading, setProductsLoading] = useState(false);
- const [dashboard, setDashboard] = useState(null);
- const [dashboardLoading, setDashboardLoading] = useState(false);
- const [discounts, setDiscounts] = useState({}); // keyed by brand||image_id
-
- const [search, setSearch] = useState('');
- const [inStockOnly, setInStockOnly] = useState(false);
- const [isUploadOpen, setIsUploadOpen] = useState(false);
-
- useEffect(() => {
- api.getStores()
- .then((res) => {
- setStores(res);
- if (res.length > 0) setSelectedStore(res[0].store_id);
- })
- .catch(() => setStores([]))
- .finally(() => setStoresLoading(false));
- }, []);
-
- const loadStoreData = useCallback((storeId) => {
- if (!storeId) return;
- setProductsLoading(true);
- setDashboardLoading(true);
- api.getStoreProducts(storeId, { in_stock_only: inStockOnly })
- .then(setProducts)
- .catch(() => setProducts([]))
- .finally(() => setProductsLoading(false));
- api.getStoreAnalytics(storeId)
- .then(setDashboard)
- .catch(() => setDashboard(null))
- .finally(() => setDashboardLoading(false));
- api.getStoreDiscounts(storeId)
- .then((rows) => {
- const map = {};
- rows.forEach((r) => { map[`${r.brand}||${r.image_id}`] = r; });
- setDiscounts(map);
- })
- .catch(() => setDiscounts({}));
- }, [inStockOnly]);
-
- useEffect(() => { loadStoreData(selectedStore); }, [selectedStore, loadStoreData]);
-
- const filteredProducts = products.filter((p) =>
- !search || (p.title || '').toLowerCase().includes(search.toLowerCase())
- );
-
- return (
-
-
-
- {storesLoading ? (
-
- ) : stores.length === 0 ? (
-
-
-
- ) : (
-
-
- {stores.map((s) => (
- setSelectedStore(s.store_id)}
- className={`flex shrink-0 items-center gap-2 rounded-full px-4 py-1.5 text-sm font-medium transition ${
- selectedStore === s.store_id ? 'bg-ink-900 text-paper-100' : 'bg-ink-100 text-ink-700 hover:bg-ink-100/70'
- }`}
- >
-
- {s.store_name}
- {s.tier}
-
- ))}
-
-
-
- {dashboardLoading ? (
-
- ) : dashboard ? (
-
- ) : null}
-
-
-
Products in this store
-
-
-
- {productsLoading ? (
-
- ) : filteredProducts.length === 0 ? (
-
- ) : (
-
-
-
-
- Product
- Category
- Stock
- Status
- MRP
- Selling Price
- Discount
- Margin
-
-
-
- {filteredProducts.map((p) => {
- const d = discounts[`${p.brand}||${p.image_id}`];
- return (
-
-
- {p.title}
- {p.brand}{p.fssai_license ? ` · FSSAI: ${p.fssai_license}` : ''}
-
- {p.category}
- {p.available_stock}
-
-
- {p.stock_status}
-
-
- ₹{p.mrp}
- ₹{p.selling_price}
-
- {d && d.discount_pct > 0 ? (
-
- -{d.discount_pct}% → ₹{d.final_price}
-
- ) : (
- —
- )}
-
- {p.gross_profit_pct}%
-
- );
- })}
-
-
-
- )}
-
-
- )}
-
-
setIsUploadOpen(false)}
- tabType="stores"
- tabTitle="Store Inventory & Pricing"
- onSuccess={() => loadStoreData(selectedStore)}
- />
-
- );
-}
-
-function KpiRow({ dashboard }) {
- const { sales, profit, inventory } = dashboard;
- const TONE_TEXT = { ink: 'text-ink-600', leaf: 'text-leaf-600', maroon: 'text-maroon-600' };
- const cards = [
- { icon: IndianRupee, label: 'Revenue', value: `₹${Number(sales.revenue).toLocaleString('en-IN')}`, tone: 'ink' },
- { icon: TrendingUp, label: 'Profit', value: `₹${Number(profit.total_profit).toLocaleString('en-IN')}`, sub: `${profit.gross_profit_pct}% GP`, tone: 'leaf' },
- { icon: BarChart3, label: 'Orders', value: sales.total_sales.toLocaleString('en-IN'), sub: `₹${sales.average_basket_value} avg basket`, tone: 'ink' },
- { icon: PackageCheck, label: 'In Stock', value: inventory.in_stock, sub: `${inventory.total_products} products total`, tone: 'leaf' },
- { icon: PackageX, label: 'Out of Stock', value: inventory.out_of_stock, sub: `${inventory.low_stock} running low`, tone: 'maroon' },
- ];
- return (
-
- {cards.map((c) => (
-
-
- {c.label}
-
-
{c.value}
- {c.sub &&
{c.sub}
}
-
- ))}
-
- );
-}
diff --git a/src/pages/UserPage.jsx b/src/pages/UserPage.jsx
index 8ed3a68..19696a9 100644
--- a/src/pages/UserPage.jsx
+++ b/src/pages/UserPage.jsx
@@ -1,121 +1,15 @@
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, AlertTriangle
+ RefreshCw, Search, FileSpreadsheet, Download, LayoutGrid,
+ AlertTriangle
} 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
@@ -145,46 +39,6 @@ export function UserPage() {
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);
@@ -210,7 +64,6 @@ export function UserPage() {
const handlePresetLionDates = () => {
setEntryMode('single');
- switchTab('upload');
setBrand('Lion Dates');
setProductName('Lion Dates 450g');
setCategory('Health Foods');
@@ -245,32 +98,6 @@ export function UserPage() {
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);
@@ -303,26 +130,6 @@ export function UserPage() {
fetchBrandCatalog(firstBrand);
}
- 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);
@@ -347,90 +154,6 @@ export function UserPage() {
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]);
-
// A spreadsheet import can genuinely half-succeed, and that must not be
// painted the same green as a clean one - the whole complaint was a success
// panel over rows that never landed.
@@ -443,12 +166,12 @@ export function UserPage() {
- {/* Top View Selector Tabs: [ Add / Upload Records ] vs [ View Product Cards ] */}
+ {/* Workspace banner */}
@@ -457,584 +180,419 @@ export function UserPage() {
User Product Management
- Single/Batch Product Uploads with Dual Persistence & Store Product Cards
+ Single/Batch Product Uploads with Dual Persistence
-
-
switchTab('upload')}
- className={`flex items-center gap-2 px-4 py-2 rounded-lg text-xs font-bold transition-all cursor-pointer ${
- activeTab === 'upload'
- ? 'bg-blue-600 text-white shadow-md'
- : 'text-slate-400 hover:text-white hover:bg-slate-800/60'
- }`}
- >
- ➕ Add / Upload Records
-
-
-
switchTab('cards')}
- className={`flex items-center gap-2 px-4 py-2 rounded-lg text-xs font-bold transition-all cursor-pointer ${
- activeTab === 'cards'
- ? 'bg-amber-500 text-slate-950 shadow-md font-bold'
- : 'text-slate-400 hover:text-white hover:bg-slate-800/60'
- }`}
- >
- 🎴 Product Catalog Cards ({allCardProducts.length})
-
-
{/* ========================================================================= */}
{/* 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.
-
-
-
-
-
- Download Sample CSV
-
-
-
- Preset: Add Lion Dates 450g
-
-
+
+
+
+
+ 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 */}
-
-
-
-
setEntryMode('single')}
- className={`flex items-center gap-1.5 px-3.5 py-1.5 rounded-lg text-xs font-bold transition cursor-pointer ${
- entryMode === 'single'
- ? 'bg-blue-600 text-white shadow-xs'
- : 'text-slate-600 hover:text-ink-950'
- }`}
- >
- Single Entry Form
-
+
+
+ Download Sample CSV
+
- setEntryMode('batch')}
- className={`flex items-center gap-1.5 px-3.5 py-1.5 rounded-lg text-xs font-bold transition cursor-pointer ${
- entryMode === 'batch'
- ? 'bg-blue-600 text-white shadow-xs'
- : 'text-slate-600 hover:text-ink-950'
- }`}
- >
- Batch CSV / Excel Upload
-
-
+
+ Preset: Add Lion Dates 450g
+
+
+
-
- Auto-Enrich & Dual Persistence
-
-
-
- {/* SINGLE PRODUCT FORM */}
- {entryMode === 'single' && (
-
- {singleError && (
-
- {singleError}
-
- )}
-
-
-
- )}
-
- {/* BATCH CSV/EXCEL UPLOAD */}
- {entryMode === 'batch' && (
-
- {batchError && (
-
- {batchError}
-
- )}
-
-
-
-
-
Upload Product Spreadsheet (.csv, .xlsx, .xls)
-
- Supports multiple rows containing Brand Name, Product Name, Category, Price, Barcode, HSN Code.
-
-
-
setBatchFile(e.target.files?.[0] || null)}
- className="hidden"
- id="batch-file-input"
- />
-
- Select CSV or Excel File
-
-
- {batchFile && (
-
-
- {batchFile.name} ({(batchFile.size / 1024).toFixed(1)} KB)
-
- )}
-
-
-
- {uploadingBatch ? : }
- {uploadingBatch ? 'Processing & Enriching Batch Products...' : 'Upload & Sync Batch Records'}
-
-
-
- )}
-
-
- {/* Right Panel: Results & Quick Brand Inspection */}
-
- {singleResult && (
-
-
-
-
Product Record Created & Synced
-
-
{singleResult.message}
-
-
{singleResult.product.product_name}
-
Brand: {singleResult.product.brand}
- {singleResult.product.barcode && (
-
Barcode: {singleResult.product.barcode}
- )}
-
-
-
switchTab('cards')}
- className="w-full flex items-center justify-center gap-2 py-2 px-3 rounded-xl bg-emerald-600 text-white font-bold text-xs shadow-sm hover:bg-emerald-700 transition cursor-pointer"
- >
- View as Product Card
-
-
- )}
-
- {batchResult && (
-
+ {/* Left Form: Single vs Batch Upload Tabs */}
+
+
+
+
setEntryMode('single')}
+ className={`flex items-center gap-1.5 px-3.5 py-1.5 rounded-lg text-xs font-bold transition cursor-pointer ${
+ entryMode === 'single'
+ ? 'bg-blue-600 text-white shadow-xs'
+ : 'text-slate-600 hover:text-ink-950'
}`}
>
-
- {batchHadErrors ?
:
}
-
- {batchHadErrors ? 'Batch Partially Imported' : 'Batch Products Saved to Database'}
-
-
+ Single Entry Form
+
-
{batchResult.message}
-
- {/* Counts, so "success" is never a claim without a number
- behind it. added_count is the number of rows verified
- present in the database, not the number attempted. */}
-
-
-
{batchResult.added_count ?? 0}
-
Saved
-
-
-
{batchResult.error_count ?? 0}
-
Failed
-
-
-
{batchResult.skipped_blank_rows ?? 0}
-
Blank Rows
-
-
-
- {Array.isArray(batchResult.warnings) && batchResult.warnings.length > 0 && (
-
- {batchResult.warnings.map((w, idx) => (
-
-
- {w}
-
- ))}
-
- )}
-
- {Array.isArray(batchResult.errors) && batchResult.errors.length > 0 && (
-
-
Rows that were not saved
- {batchResult.errors.map((e, idx) => (
-
- {e.row ? `Row ${e.row}: ` : ''}
- {e.product_name ? `${e.product_name} - ` : ''}
- {e.error}
-
- ))}
- {batchResult.errors_truncated > 0 && (
-
- +{batchResult.errors_truncated} more not shown
-
- )}
-
- )}
-
- {(batchResult.added_count ?? 0) > 0 && (
-
switchTab('cards')}
- className="w-full flex items-center justify-center gap-2 py-2 px-3 rounded-xl bg-emerald-600 text-white font-bold text-xs shadow-sm hover:bg-emerald-700 transition cursor-pointer"
- >
- View Batch Items as Product Cards
-
- )}
-
- )}
-
- {!singleResult && !batchResult && (
-
-
-
Dual Storage Persistence
-
- Every product submitted (single or batch CSV/Excel) is automatically parsed, assigned high-resolution images, and synced into PostgreSQL brand tables and JSON catalog seed files.
-
-
- )}
-
- {/* Quick Brand Catalog Search */}
-
-
-
- Inspect Brand Products
-
- {brandProducts.length} items
-
-
-
- setBrandSearch(e.target.value)}
- placeholder="Enter brand to inspect (e.g. Lion Dates)"
- className="flex-1 rounded-xl border border-ink-900/15 bg-white p-2 text-xs font-medium text-ink-950 focus:outline-none"
- />
- fetchBrandCatalog(brandSearch)}
- className="px-3 py-1.5 bg-ink-900 text-white text-xs font-semibold rounded-xl hover:bg-ink-950 cursor-pointer"
- >
- Fetch
-
-
-
- {loadingBrand ? (
-
Loading catalog items...
- ) : (
-
- {brandProducts.map((p, idx) => (
-
setSelectedProduct(p)}
- className="cursor-pointer flex items-center justify-between gap-2 p-2.5 rounded-xl border border-slate-200 bg-white hover:border-amber-500/60 transition shadow-xs"
- >
-
- {p.image_url ? (
-
- ) : (
-
No img
- )}
-
-
{p.product_name}
-
- {p.barcode ? `Barcode: ${p.barcode}` : p.product_sku ? `SKU: ${p.product_sku}` : 'Standard Record'}
-
-
-
- {p.final_selling_price && (
-
₹{p.final_selling_price}
- )}
-
- ))}
-
- )}
-
-
-
-
- )}
-
- {/* ========================================================================= */}
- {/* VIEW 2: PRODUCT CATALOG CARDS (STORE-A & USER UPLOADED CARDS) */}
- {/* ========================================================================= */}
- {activeTab === 'cards' && (
-
- {/* Header Toolbar */}
-
-
-
-
- User Product Catalog Cards
-
-
- Displaying sample store records (e.g. Store-A Gandhipuram ) alongside your uploaded product records as rich cards.
-
-
-
- {/* Store Selector Dropdown */}
-
-
- Sample Store:
- setSelectedStoreId(e.target.value)}
- className="bg-white border border-slate-300 rounded-lg px-3 py-1 text-xs font-bold text-slate-800 focus:outline-none"
+ setEntryMode('batch')}
+ className={`flex items-center gap-1.5 px-3.5 py-1.5 rounded-lg text-xs font-bold transition cursor-pointer ${
+ entryMode === 'batch'
+ ? 'bg-blue-600 text-white shadow-xs'
+ : 'text-slate-600 hover:text-ink-950'
+ }`}
>
- {STORES_SAMPLE.map((s) => (
-
- {s.store_name} ({s.tier})
-
- ))}
-
-
-
-
- {/* Filter & Search Bar */}
-
- {/* Brand Filter Pills */}
-
-
- Brands:
-
- {uniqueBrandsList.slice(0, 8).map((b) => (
- setSelectedBrandFilter(b)}
- className={`px-3 py-1 rounded-full text-xs font-bold transition-all cursor-pointer ${
- selectedBrandFilter === b
- ? 'bg-ink-950 text-white shadow-xs scale-105'
- : 'bg-slate-200/70 text-slate-700 hover:bg-slate-300'
- }`}
- >
- {b}
-
- ))}
+ Batch CSV / Excel Upload
+
- {/* Search Bar */}
-
-
- setCardSearchQuery(e.target.value)}
- placeholder="Search by product, brand or barcode..."
- className="w-full rounded-xl border border-slate-300 bg-white py-1.5 pl-8 pr-3 text-xs text-ink-950 placeholder-slate-400 focus:outline-none focus:border-amber-500"
- />
-
+
+ Auto-Enrich & Dual Persistence
+
-
- {/* Product Cards Grid */}
- {loadingCards ? (
-
- Loading store and uploaded product cards...
-
- ) : filteredCards.length === 0 ? (
-
-
-
No product cards match your filter
-
Try resetting the brand filter or search term
-
{ setSelectedBrandFilter('All'); setCardSearchQuery(''); }}
- className="mt-3 px-4 py-1.5 rounded-xl bg-slate-900 text-white text-xs font-bold cursor-pointer"
- >
- Reset Filters
-
-
- ) : (
-
-
-
- Showing {filteredCards.length} product cards ({selectedStoreId} + Uploads)
-
- {uploadedCards.length > 0 && (
-
- {uploadedCards.length} Newly Uploaded Items Added
-
+ {/* SINGLE PRODUCT FORM */}
+ {entryMode === 'single' && (
+
+ {singleError && (
+
+ {singleError}
+
)}
-
-
- {filteredCards.map((prod, idx) => (
-
- {prod.is_user_uploaded && (
-
- NEWLY UPLOADED
+
+
+
+
+
+
+
+
+ Custom Image URL (Optional)
+ 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"
+ />
+
+
+
+ Description (Optional)
+ setDescription(e.target.value)}
+ placeholder="Enter detailed description or leave empty for AI auto-description..."
+ 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"
+ />
+
+
+
+ {submittingSingle ? : }
+ {submittingSingle ? 'Syncing to Database & JSON Catalog...' : 'Save Record to DB Table & JSON File'}
+
+
+
+ )}
+
+ {/* BATCH CSV/EXCEL UPLOAD */}
+ {entryMode === 'batch' && (
+
+ {batchError && (
+
+ {batchError}
+
+ )}
+
+
+
+
+
Upload Product Spreadsheet (.csv, .xlsx, .xls)
+
+ Supports multiple rows containing Brand Name, Product Name, Category, Price, Barcode, HSN Code.
+
+
+
setBatchFile(e.target.files?.[0] || null)}
+ className="hidden"
+ id="batch-file-input"
+ />
+
+ Select CSV or Excel File
+
+
+ {batchFile && (
+
+
+ {batchFile.name} ({(batchFile.size / 1024).toFixed(1)} KB)
)}
-
setSelectedProduct(prod)} />
- ))}
+
+
+ {uploadingBatch ? : }
+ {uploadingBatch ? 'Processing & Enriching Batch Products...' : 'Upload & Sync Batch Records'}
+
+
+ )}
+
+
+ {/* Right Panel: Results & Quick Brand Inspection */}
+
+ {singleResult && (
+
+
+
+
Product Record Created & Synced
+
+
{singleResult.message}
+
+
{singleResult.product.product_name}
+
Brand: {singleResult.product.brand}
+ {singleResult.product.barcode && (
+
Barcode: {singleResult.product.barcode}
+ )}
+
+
+ )}
+
+ {batchResult && (
+
+
+ {batchHadErrors ?
:
}
+
+ {batchHadErrors ? 'Batch Partially Imported' : 'Batch Products Saved to Database'}
+
+
+
+
{batchResult.message}
+
+ {/* Counts, so "success" is never a claim without a number
+ behind it. added_count is the number of rows verified
+ present in the database, not the number attempted. */}
+
+
+
{batchResult.added_count ?? 0}
+
Saved
+
+
+
{batchResult.error_count ?? 0}
+
Failed
+
+
+
{batchResult.skipped_blank_rows ?? 0}
+
Blank Rows
+
+
+
+ {Array.isArray(batchResult.warnings) && batchResult.warnings.length > 0 && (
+
+ {batchResult.warnings.map((w, idx) => (
+
+
+ {w}
+
+ ))}
+
+ )}
+
+ {Array.isArray(batchResult.errors) && batchResult.errors.length > 0 && (
+
+
Rows that were not saved
+ {batchResult.errors.map((e, idx) => (
+
+ {e.row ? `Row ${e.row}: ` : ''}
+ {e.product_name ? `${e.product_name} - ` : ''}
+ {e.error}
+
+ ))}
+ {batchResult.errors_truncated > 0 && (
+
+ +{batchResult.errors_truncated} more not shown
+
+ )}
+
+ )}
+
+ )}
+
+ {!singleResult && !batchResult && (
+
+
+
Dual Storage Persistence
+
+ Every product submitted (single or batch CSV/Excel) is automatically parsed, assigned high-resolution images, and synced into PostgreSQL brand tables and JSON catalog seed files.
+
+
+ )}
+
+ {/* Quick Brand Catalog Search */}
+
+
+
+ Inspect Brand Products
+
+ {brandProducts.length} items
+
+
+
+ setBrandSearch(e.target.value)}
+ placeholder="Enter brand to inspect (e.g. Lion Dates)"
+ className="flex-1 rounded-xl border border-ink-900/15 bg-white p-2 text-xs font-medium text-ink-950 focus:outline-none"
+ />
+ fetchBrandCatalog(brandSearch)}
+ className="px-3 py-1.5 bg-ink-900 text-white text-xs font-semibold rounded-xl hover:bg-ink-950 cursor-pointer"
+ >
+ Fetch
+
+
+
+ {loadingBrand ? (
+
Loading catalog items...
+ ) : (
+
+ {brandProducts.map((p, idx) => (
+
setSelectedProduct(p)}
+ className="cursor-pointer flex items-center justify-between gap-2 p-2.5 rounded-xl border border-slate-200 bg-white hover:border-amber-500/60 transition shadow-xs"
+ >
+
+ {p.image_url ? (
+
+ ) : (
+
No img
+ )}
+
+
{p.product_name}
+
+ {p.barcode ? `Barcode: ${p.barcode}` : p.product_sku ? `SKU: ${p.product_sku}` : 'Standard Record'}
+
+
+
+ {p.final_selling_price && (
+
₹{p.final_selling_price}
+ )}
+
+ ))}
+
+ )}
- )}
+
- )}
+
{/* Interactive Product Details Modal */}