frontend changes with drop elements

This commit is contained in:
sriram
2026-08-18 18:39:20 +05:30
parent 39ae51e291
commit 8bb53d19dd
4 changed files with 10 additions and 636 deletions

View File

@@ -18,7 +18,6 @@ import { LoginPage } from './pages/LoginPage';
* sees, so deferring it would only add a round trip before the form appears.
*/
const HomePage = lazy(() => import('./pages/HomePage').then((m) => ({ default: m.HomePage })));
const UserPage = lazy(() => import('./pages/UserPage').then((m) => ({ default: m.UserPage })));
const AdminPage = lazy(() => import('./pages/AdminPage').then((m) => ({ default: m.AdminPage })));
const McpPage = lazy(() => import('./pages/McpPage').then((m) => ({ default: m.McpPage })));
@@ -48,10 +47,11 @@ function ProtectedRoute({ children, allowedRoles }) {
const effectiveRole = role || (user.role === 'store' ? 'user' : user.role);
if (allowedRoles && !allowedRoles.includes(effectiveRole)) {
if (window.location.pathname === '/user') {
return children;
}
return <Navigate to="/user" replace />;
// Every remaining route is admin-only, so there is nowhere else in the app
// to send a non-admin. This can still be reached by a token minted before
// the `user` account was disabled - those stay valid for their 12h TTL -
// so it has to terminate somewhere public rather than redirect in a loop.
return <Navigate to="/login" replace />;
}
return children;
@@ -78,16 +78,6 @@ export default function App() {
}
/>
{/* User Workspace Route */}
<Route
path="/user"
element={
<ProtectedRoute allowedRoles={['admin', 'user', 'store']}>
<UserPage />
</ProtectedRoute>
}
/>
{/* Admin Panel Route - RESTRICTED TO ADMIN ONLY */}
<Route
path="/admin"

View File

@@ -43,11 +43,11 @@ export function NavigationHeader({
{/* Left Section: Home/Back Button + Page Title */}
<div className="flex items-center gap-3">
<Link
to={role === 'admin' ? '/' : '/user'}
to="/"
onClick={() => {
if (onResetHome) onResetHome();
}}
title={role === 'admin' ? 'Navigate to Catalog' : 'Navigate to User Workspace'}
title="Navigate to Catalog"
className="group flex items-center gap-2 rounded-full bg-gradient-to-r from-amber-500 to-amber-600 hover:from-amber-600 hover:to-amber-700 text-white px-4 py-2 text-xs font-semibold shadow-sm transition-all duration-200 active:scale-95 hover:shadow-md"
>
<Home className="h-4 w-4 transition-transform group-hover:-translate-x-0.5" />
@@ -107,16 +107,6 @@ export function NavigationHeader({
</Link>
)}
{/* User Upload tab */}
<Link
to="/user"
className={`flex items-center gap-1.5 rounded-full px-3.5 py-1.5 transition-all ${
path === '/user' ? 'bg-ink-950 text-paper-100 shadow-sm font-semibold' : 'text-slate-600 hover:text-ink-950 hover:bg-white/60'
}`}
>
<UserCheck className="h-3.5 w-3.5 text-blue-400" /> User Upload
</Link>
{/* MCP tab ONLY visible to Admin role */}
{role === 'admin' && (
<Link

View File

@@ -35,10 +35,9 @@ export function LoginPage() {
try {
const user = await login(username, password);
setLoading(false);
// Route by the role the SERVER assigned. Only `admin` can sign in now,
// but a token minted for any other role still lands somewhere valid.
if (user.role === 'admin') navigate('/admin');
else navigate('/user');
// Only `admin` can sign in, and every route is admin-only, so there is
// one destination. Sending a non-admin to /login instead would loop.
navigate(user.role === 'admin' ? '/admin' : '/login');
} catch (err) {
setLoading(false);
// Surface the server's own message: it distinguishes bad credentials

View File

@@ -1,605 +0,0 @@
import React, { useState, useEffect } from 'react';
import { api } from '../api/client';
import { NavigationHeader } from '../components/NavigationHeader';
import { ProductModal } from '../components/ProductModal';
import { ErrorBoundary } from '../components/ErrorBoundary';
import {
PlusCircle, Upload, CheckCircle2, Database, Sparkles,
RefreshCw, Search, FileSpreadsheet, Download, LayoutGrid,
AlertTriangle
} from 'lucide-react';
export function UserPage() {
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);
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');
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);
// 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)) {
// Re-read the brand from the API rather than trusting the response, so
// the panel below is showing what the database actually holds. A card
// built purely from the upload response is what made a failed import
// look successful.
const firstBrand = data.added_products[0]?.brand;
if (firstBrand) {
setBrandSearch(firstBrand);
fetchBrandCatalog(firstBrand);
}
}
} 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);
};
// 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.
const batchHadErrors = Boolean(
batchResult && ((batchResult.error_count ?? 0) > 0 || batchResult.status === 'partial')
);
return (
<ErrorBoundary>
<div className="min-h-screen bg-paper-100 font-sans text-ink-950">
<NavigationHeader
title="User Product Workspace"
subtitle="Add and batch-upload product records with DB & JSON sync"
icon={PlusCircle}
/>
<main className="max-w-7xl mx-auto px-4 py-6 space-y-6">
{/* Workspace banner */}
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 bg-slate-900 text-white rounded-2xl p-4 shadow-lg border border-slate-800">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-blue-500/20 text-blue-400 border border-blue-500/30">
<LayoutGrid className="h-5 w-5" />
</div>
<div>
<h2 className="text-lg font-bold font-display">User Product Management</h2>
<p className="text-xs text-slate-300">
Single/Batch Product Uploads with Dual Persistence
</p>
</div>
</div>
</div>
{/* ========================================================================= */}
{/* VIEW 1: PRODUCT ENTRY & BATCH UPLOAD */}
{/* ========================================================================= */}
<div className="space-y-6">
<div className="rounded-2xl border border-blue-500/20 bg-gradient-to-r from-blue-950 to-slate-900 text-white p-6 shadow-md flex flex-wrap items-center justify-between gap-4">
<div>
<span className="inline-flex items-center gap-1 rounded-full bg-blue-500/20 px-3 py-1 text-xs font-bold text-blue-300 border border-blue-400/30 mb-2">
<Database className="h-3.5 w-3.5" /> Database & Seed JSON Auto-Sync
</span>
<h3 className="text-xl font-bold font-display">Upload & Add New Product Records</h3>
<p className="text-sm text-blue-200 mt-1 max-w-2xl">
Add new product variants (e.g. <strong>"Lion Dates 450g"</strong>) via single form or batch CSV/Excel upload. Product details & images sync directly into PostgreSQL database tables and JSON seed catalog files.
</p>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={downloadSampleCSV}
className="flex items-center gap-1.5 rounded-xl bg-white/10 hover:bg-white/20 border border-white/20 text-white font-semibold px-3.5 py-2 text-xs transition cursor-pointer"
>
<Download className="h-3.5 w-3.5" /> Download Sample CSV
</button>
<button
type="button"
onClick={handlePresetLionDates}
className="flex items-center gap-2 rounded-xl bg-amber-500 hover:bg-amber-400 text-slate-950 font-bold px-4 py-2 text-xs shadow-md transition active:scale-95 cursor-pointer"
>
<Sparkles className="h-4 w-4" /> Preset: Add Lion Dates 450g
</button>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
{/* Left Form: Single vs Batch Upload Tabs */}
<div className="lg:col-span-7 rounded-2xl border border-ink-900/10 bg-paper-50 p-6 shadow-sm">
<div className="flex items-center justify-between border-b border-ink-900/10 pb-4 mb-6">
<div className="flex items-center gap-2 p-1 rounded-xl bg-slate-200/80">
<button
type="button"
onClick={() => 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'
}`}
>
<PlusCircle className="h-4 w-4" /> Single Entry Form
</button>
<button
type="button"
onClick={() => 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'
}`}
>
<FileSpreadsheet className="h-4 w-4" /> Batch CSV / Excel Upload
</button>
</div>
<span className="text-[11px] font-semibold text-slate-500 bg-slate-100 px-2.5 py-1 rounded-full">
Auto-Enrich & Dual Persistence
</span>
</div>
{/* SINGLE PRODUCT FORM */}
{entryMode === 'single' && (
<div>
{singleError && (
<div className="mb-4 rounded-xl bg-rose-500/10 border border-rose-500/30 p-3.5 text-xs text-rose-700">
{singleError}
</div>
)}
<form onSubmit={handleSingleSubmit} className="space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-semibold text-ink-800 mb-1">Brand Name *</label>
<input
type="text"
required
value={brand}
onChange={(e) => 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"
/>
</div>
<div>
<label className="block text-xs font-semibold text-ink-800 mb-1">Product Name / Variant *</label>
<input
type="text"
required
value={productName}
onChange={(e) => 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"
/>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div>
<label className="block text-xs font-semibold text-ink-800 mb-1">Category</label>
<input
type="text"
value={category}
onChange={(e) => 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"
/>
</div>
<div>
<label className="block text-xs font-semibold text-ink-800 mb-1">Price Range</label>
<input
type="text"
value={priceRange}
onChange={(e) => 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"
/>
</div>
<div>
<label className="block text-xs font-semibold text-ink-800 mb-1">Final Price (₹)</label>
<input
type="number"
step="0.01"
value={finalPrice}
onChange={(e) => 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"
/>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-semibold text-ink-800 mb-1">Barcode (GTIN/EAN)</label>
<input
type="text"
value={barcode}
onChange={(e) => 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"
/>
</div>
<div>
<label className="block text-xs font-semibold text-ink-800 mb-1">HSN Code</label>
<input
type="text"
value={hsnCode}
onChange={(e) => 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"
/>
</div>
</div>
<div>
<label className="block text-xs font-semibold text-ink-800 mb-1">Custom Image URL (Optional)</label>
<input
type="url"
value={imageUrl}
onChange={(e) => 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"
/>
</div>
<div>
<label className="block text-xs font-semibold text-ink-800 mb-1">Description (Optional)</label>
<textarea
rows={3}
value={description}
onChange={(e) => 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"
/>
</div>
<button
type="submit"
disabled={submittingSingle}
className="w-full flex items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white py-3 text-xs font-bold shadow-md transition active:scale-98 disabled:opacity-50 cursor-pointer"
>
{submittingSingle ? <RefreshCw className="h-4 w-4 animate-spin" /> : <PlusCircle className="h-4 w-4" />}
{submittingSingle ? 'Syncing to Database & JSON Catalog...' : 'Save Record to DB Table & JSON File'}
</button>
</form>
</div>
)}
{/* BATCH CSV/EXCEL UPLOAD */}
{entryMode === 'batch' && (
<div>
{batchError && (
<div className="mb-4 rounded-xl bg-rose-500/10 border border-rose-500/30 p-3.5 text-xs text-rose-700">
{batchError}
</div>
)}
<form onSubmit={handleBatchSubmit} className="space-y-4">
<div className="border-2 border-dashed border-blue-500/30 rounded-2xl bg-blue-50/50 p-6 text-center hover:bg-blue-50 transition">
<FileSpreadsheet className="h-10 w-10 text-blue-600 mx-auto mb-2" />
<h4 className="font-bold text-sm text-ink-950">Upload Product Spreadsheet (.csv, .xlsx, .xls)</h4>
<p className="text-xs text-slate-500 mt-1 mb-4">
Supports multiple rows containing Brand Name, Product Name, Category, Price, Barcode, HSN Code.
</p>
<input
type="file"
accept=".csv, .xlsx, .xls"
onChange={(e) => setBatchFile(e.target.files?.[0] || null)}
className="hidden"
id="batch-file-input"
/>
<label
htmlFor="batch-file-input"
className="inline-flex items-center gap-2 px-4 py-2 rounded-xl bg-blue-600 hover:bg-blue-700 text-white font-bold text-xs cursor-pointer shadow-sm transition"
>
<Upload className="h-4 w-4" /> Select CSV or Excel File
</label>
{batchFile && (
<div className="mt-4 p-3 rounded-xl bg-white border border-blue-200 inline-flex items-center gap-2 text-xs font-bold text-ink-950">
<CheckCircle2 className="h-4 w-4 text-emerald-600" />
{batchFile.name} ({(batchFile.size / 1024).toFixed(1)} KB)
</div>
)}
</div>
<button
type="submit"
disabled={!batchFile || uploadingBatch}
className="w-full flex items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white py-3 text-xs font-bold shadow-md transition active:scale-98 disabled:opacity-50 cursor-pointer"
>
{uploadingBatch ? <RefreshCw className="h-4 w-4 animate-spin" /> : <Upload className="h-4 w-4" />}
{uploadingBatch ? 'Processing & Enriching Batch Products...' : 'Upload & Sync Batch Records'}
</button>
</form>
</div>
)}
</div>
{/* Right Panel: Results & Quick Brand Inspection */}
<div className="lg:col-span-5 space-y-6">
{singleResult && (
<div className="rounded-2xl border border-emerald-500/30 bg-emerald-50/70 p-6 shadow-sm space-y-3">
<div className="flex items-center gap-2 text-emerald-700">
<CheckCircle2 className="h-5 w-5" />
<h4 className="font-bold text-sm">Product Record Created & Synced</h4>
</div>
<p className="text-xs text-slate-700">{singleResult.message}</p>
<div className="p-3 rounded-xl bg-white border border-emerald-200 text-xs space-y-1">
<p className="font-bold text-ink-950">{singleResult.product.product_name}</p>
<p className="text-[11px] text-slate-500 font-mono">Brand: {singleResult.product.brand}</p>
{singleResult.product.barcode && (
<p className="text-[11px] text-slate-600 font-mono">Barcode: {singleResult.product.barcode}</p>
)}
</div>
</div>
)}
{batchResult && (
<div
className={`rounded-2xl border p-6 shadow-sm space-y-3 ${
batchHadErrors
? 'border-amber-500/40 bg-amber-50/70'
: 'border-emerald-500/30 bg-emerald-50/70'
}`}
>
<div className={`flex items-center gap-2 ${batchHadErrors ? 'text-amber-700' : 'text-emerald-700'}`}>
{batchHadErrors ? <AlertTriangle className="h-5 w-5" /> : <CheckCircle2 className="h-5 w-5" />}
<h4 className="font-bold text-sm">
{batchHadErrors ? 'Batch Partially Imported' : 'Batch Products Saved to Database'}
</h4>
</div>
<p className="text-xs text-slate-700">{batchResult.message}</p>
{/* 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. */}
<div className="grid grid-cols-3 gap-2 text-center">
<div className="rounded-xl bg-white border border-emerald-200 py-2">
<p className="text-base font-extrabold text-emerald-700">{batchResult.added_count ?? 0}</p>
<p className="text-[10px] font-semibold text-slate-500 uppercase">Saved</p>
</div>
<div className="rounded-xl bg-white border border-rose-200 py-2">
<p className="text-base font-extrabold text-rose-700">{batchResult.error_count ?? 0}</p>
<p className="text-[10px] font-semibold text-slate-500 uppercase">Failed</p>
</div>
<div className="rounded-xl bg-white border border-slate-200 py-2">
<p className="text-base font-extrabold text-slate-700">{batchResult.skipped_blank_rows ?? 0}</p>
<p className="text-[10px] font-semibold text-slate-500 uppercase">Blank Rows</p>
</div>
</div>
{Array.isArray(batchResult.warnings) && batchResult.warnings.length > 0 && (
<ul className="space-y-1">
{batchResult.warnings.map((w, idx) => (
<li key={idx} className="flex items-start gap-1.5 text-[11px] text-amber-800">
<AlertTriangle className="h-3 w-3 shrink-0 mt-0.5" />
<span>{w}</span>
</li>
))}
</ul>
)}
{Array.isArray(batchResult.errors) && batchResult.errors.length > 0 && (
<div className="rounded-xl bg-white border border-rose-200 p-3 space-y-1 max-h-40 overflow-y-auto no-scrollbar">
<p className="text-[11px] font-bold text-rose-800">Rows that were not saved</p>
{batchResult.errors.map((e, idx) => (
<p key={idx} className="text-[11px] text-slate-600 font-mono">
{e.row ? `Row ${e.row}: ` : ''}
{e.product_name ? `${e.product_name} - ` : ''}
{e.error}
</p>
))}
{batchResult.errors_truncated > 0 && (
<p className="text-[11px] text-slate-400 italic">
+{batchResult.errors_truncated} more not shown
</p>
)}
</div>
)}
</div>
)}
{!singleResult && !batchResult && (
<div className="rounded-2xl border border-ink-900/10 bg-paper-50 p-6 shadow-sm text-center">
<Database className="h-10 w-10 text-blue-500 mx-auto mb-3" />
<h4 className="font-bold text-sm text-ink-950">Dual Storage Persistence</h4>
<p className="text-xs text-slate-500 mt-1">
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.
</p>
</div>
)}
{/* Quick Brand Catalog Search */}
<div className="rounded-2xl border border-ink-900/10 bg-paper-50 p-6 shadow-sm space-y-4">
<div className="flex items-center justify-between">
<h4 className="font-bold text-sm text-ink-950 flex items-center gap-2">
<Search className="h-4 w-4 text-amber-500" /> Inspect Brand Products
</h4>
<span className="text-[11px] font-mono text-slate-400">{brandProducts.length} items</span>
</div>
<div className="flex gap-2">
<input
type="text"
value={brandSearch}
onChange={(e) => 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"
/>
<button
type="button"
onClick={() => 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
</button>
</div>
{loadingBrand ? (
<div className="text-center py-4 text-xs text-slate-400">Loading catalog items...</div>
) : (
<div className="max-h-64 overflow-y-auto space-y-2 pr-1 no-scrollbar">
{brandProducts.map((p, idx) => (
<div
key={idx}
onClick={() => 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"
>
<div className="flex items-center gap-2.5 overflow-hidden">
{p.image_url ? (
<img src={p.image_url} alt="" className="h-9 w-9 object-contain bg-slate-50 rounded" />
) : (
<div className="h-9 w-9 rounded bg-slate-100 flex items-center justify-center text-[10px] text-slate-400">No img</div>
)}
<div className="truncate">
<p className="text-xs font-bold text-ink-950 truncate">{p.product_name}</p>
<p className="text-[10px] text-slate-400 font-mono">
{p.barcode ? `Barcode: ${p.barcode}` : p.product_sku ? `SKU: ${p.product_sku}` : 'Standard Record'}
</p>
</div>
</div>
{p.final_selling_price && (
<span className="font-mono text-xs font-bold text-leaf-600 shrink-0">₹{p.final_selling_price}</span>
)}
</div>
))}
</div>
)}
</div>
</div>
</div>
</div>
</main>
{/* Interactive Product Details Modal */}
{selectedProduct && (
<ProductModal product={selectedProduct} onClose={() => setSelectedProduct(null)} />
)}
</div>
</ErrorBoundary>
);
}