606 lines
30 KiB
JavaScript
606 lines
30 KiB
JavaScript
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>
|
|
);
|
|
}
|