feat: add SellerContext provider and inventory and bulk upload dashboard pages
All checks were successful
Beta CI/CD Pipeline / build-and-test (push) Successful in 1m30s
Beta CI/CD Pipeline / deploy-beta (push) Successful in 42s

This commit is contained in:
vickytechkey 2026-10-04 16:19:16 +05:30
parent a196780135
commit 2874bf94df
3 changed files with 1023 additions and 324 deletions

View file

@ -168,6 +168,7 @@ export const SellerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
compare_at_price: parseFloat(p.compare_at_price) || undefined,
package_weight: p.package_weight || 1.0,
shipping_profile: p.shipping_profile || 'Standard Fragile',
uploadSource: p.upload_source || (p.sku?.startsWith('TRDX-') || p.sku?.includes('CSV') || p.sku?.startsWith('BATCH-') ? 'bulk' : 'individual'),
salesCount: 0
})));

View file

@ -8,12 +8,27 @@ import {
FileText,
Clock,
RefreshCw,
Archive,
Info
Info,
Package,
Layers,
Search,
Filter,
Plus,
Minus,
Award,
Edit3,
Trash2,
Tag,
ShieldCheck,
Check,
X,
Sparkles,
ExternalLink,
ChevronRight,
ArrowUpRight
} from 'lucide-react';
import { api } from '../../services/api';
import { useSeller } from '../../context/SellerContext';
import { formatISTDateTime } from '../../utils/date';
interface BatchLog {
id: string;
@ -26,8 +41,23 @@ interface BatchLog {
errorSummary?: string;
}
const HEALTH_CONFIG: Record<string, { label: string; bg: string; color: string; dot: string }> = {
Critical: { label: 'Critical', bg: '#FEE2E2', color: '#991B1B', dot: '#DC2626' },
Low: { label: 'Low Buffer', bg: '#FFF3E0', color: '#C2410C', dot: '#EA580C' },
Adequate: { label: 'Adequate', bg: '#FEF9C3', color: '#854D0E', dot: '#CA8A04' },
Healthy: { label: 'Healthy', bg: '#E6F4EA', color: '#166534', dot: '#16A34A' },
};
function healthFromStock(stock: number): string {
if (stock === 0) return 'Critical';
if (stock <= 4) return 'Low';
if (stock <= 15) return 'Adequate';
return 'Healthy';
}
export const BulkUploadPage: React.FC = () => {
const { fetchData } = useSeller();
const { inventory, updateStock, deleteProduct, fetchData } = useSeller();
const [isUploading, setIsUploading] = useState(false);
const [uploadSuccess, setUploadSuccess] = useState<string | null>(null);
const [uploadSummary, setUploadSummary] = useState<{ total: number; created: number; updated: number; skipped: number; errors: string[] } | null>(null);
@ -38,6 +68,31 @@ export const BulkUploadPage: React.FC = () => {
const [batches, setBatches] = useState<BatchLog[]>([]);
const [isLoadingLogs, setIsLoadingLogs] = useState(false);
// Filters for items
const [activeTab, setActiveTab] = useState<'all_items' | 'bulk_logs'>('all_items');
const [searchTerm, setSearchTerm] = useState('');
const [sourceFilter, setSourceFilter] = useState<'all' | 'bulk' | 'individual'>('all');
const [categoryFilter, setCategoryFilter] = useState('All');
const [stockFilter, setStockFilter] = useState<'all' | 'in_stock' | 'low_stock'>('all');
// Quick Edit Modal state
const [editingProduct, setEditingProduct] = useState<any | null>(null);
const [editPrice, setEditPrice] = useState<number | ''>('');
const [editStock, setEditStock] = useState<number | ''>('');
const [isSavingEdit, setIsSavingEdit] = useState(false);
const [stockValues, setStockValues] = useState<Record<string, number>>({});
useEffect(() => {
setStockValues(prev => {
const next = { ...prev };
inventory.forEach(item => {
next[item.id] = item.stock;
});
return next;
});
}, [inventory]);
const triggerCsvDownload = (csvString: string, filename = 'Validation_Errors.csv') => {
const blob = new Blob([csvString], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
@ -82,18 +137,7 @@ export const BulkUploadPage: React.FC = () => {
errorSummary: l.error_summary
})));
} else {
// Fallback demo log if no history exists yet
setBatches([
{
id: 'BATCH-2026-DEFAULT',
fileName: 'Craft_Standard_Catalog_Template.csv',
date: formatISTDateTime(),
totalRows: 3,
successfulRows: 3,
failedRows: 0,
status: 'Completed'
}
]);
setBatches([]);
}
} catch (err) {
console.warn('Could not fetch bulk upload logs, fallback to local state:', err);
@ -104,6 +148,7 @@ export const BulkUploadPage: React.FC = () => {
useEffect(() => {
loadBatchLogs();
fetchData();
}, []);
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
@ -175,30 +220,217 @@ export const BulkUploadPage: React.FC = () => {
document.body.removeChild(link);
};
// Classify item source: Bulk vs Individual
const getItemSourceInfo = (item: any) => {
const isBulk = item.uploadSource === 'bulk' ||
item.sku?.startsWith('TRDX-') ||
item.sku?.startsWith('BATCH-') ||
item.sku?.includes('CSV');
if (isBulk) {
return {
type: 'bulk',
label: 'Bulk CSV Batch',
icon: FileSpreadsheet,
bg: '#EFF6FF',
color: '#1D4ED8',
border: '#BFDBFE'
};
}
return {
type: 'individual',
label: 'Individual Form',
icon: Plus,
bg: '#F0FDF4',
color: '#15803D',
border: '#BBF7D0'
};
};
const handleStepStock = (id: string, delta: number) => {
setStockValues(prev => {
const current = prev[id] ?? (inventory.find(i => i.id === id)?.stock ?? 0);
const next = Math.max(0, current + delta);
updateStock(id, next);
return { ...prev, [id]: next };
});
};
const handleDeleteItem = async (id: string, title: string) => {
if (window.confirm(`Are you sure you want to remove "${title}" from your catalog?`)) {
try {
await deleteProduct(id);
await fetchData();
} catch (err: any) {
alert(err.message || 'Failed to delete product');
}
}
};
const handleOpenEdit = (item: any) => {
setEditingProduct(item);
setEditPrice(item.price);
setEditStock(stockValues[item.id] ?? item.stock);
};
const handleSaveEditModal = async () => {
if (!editingProduct) return;
setIsSavingEdit(true);
try {
const updatedPrice = editPrice !== '' ? Number(editPrice) : editingProduct.price;
const updatedStock = editStock !== '' ? Number(editStock) : (stockValues[editingProduct.id] ?? editingProduct.stock);
if (editingProduct.id && !String(editingProduct.id).startsWith('prod-')) {
await api.patchProduct(editingProduct.id, {
price: updatedPrice,
stock: updatedStock
});
}
await updateStock(editingProduct.id, updatedStock);
await fetchData();
setEditingProduct(null);
} catch (err: any) {
alert(err.message || 'Could not update product details');
} finally {
setIsSavingEdit(false);
}
};
// Filtered inventory list
const categoriesList = ['All', ...Array.from(new Set(inventory.map(i => i.category || 'General')))];
const filteredInventory = inventory.filter(item => {
const q = searchTerm.toLowerCase();
const matchesSearch = !q ||
item.title.toLowerCase().includes(q) ||
item.sku.toLowerCase().includes(q) ||
item.category.toLowerCase().includes(q);
const source = getItemSourceInfo(item).type;
const matchesSource = sourceFilter === 'all' ? true : sourceFilter === source;
const matchesCategory = categoryFilter === 'All' || item.category === categoryFilter;
const currentStock = stockValues[item.id] ?? item.stock;
const matchesStock = stockFilter === 'all' ? true : stockFilter === 'in_stock' ? currentStock > 0 : currentStock === 0;
return matchesSearch && matchesSource && matchesCategory && matchesStock;
});
const bulkCount = inventory.filter(i => getItemSourceInfo(i).type === 'bulk').length;
const individualCount = inventory.filter(i => getItemSourceInfo(i).type === 'individual').length;
const totalCatalogValue = inventory.reduce((acc, i) => acc + ((stockValues[i.id] ?? i.stock) * i.price), 0);
const lowStockCount = inventory.filter(i => (stockValues[i.id] ?? i.stock) <= 4).length;
return (
<div className="bulk-upload-page" style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
{/* Top Header */}
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', justifyContent: 'space-between', gap: '1rem' }}>
<div>
<h1 style={{ fontSize: 'clamp(1.25rem, 2.5vw, 1.55rem)', fontWeight: 800, color: '#111827', letterSpacing: '-0.02em', lineHeight: 1.2 }}>
Bulk Catalog &amp; Batch Management
Bulk &amp; Account Catalog Registry
</h1>
<p style={{ fontSize: '0.84rem', color: '#6B7280', marginTop: '4px' }}>
Add or update dozens of handcrafted SKUs at once using our official spreadsheets or image-packed ZIP archives.
Manage all products uploaded from your account — both bulk CSV/ZIP batch uploads and individual craft listings.
</p>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<button
type="button"
onClick={() => { fetchData(); loadBatchLogs(); }}
style={{
backgroundColor: '#FFFFFF',
border: '1px solid #D1D5DB',
borderRadius: '10px',
padding: '8px 14px',
fontSize: '0.78rem',
fontWeight: 600,
color: '#374151',
display: 'inline-flex',
alignItems: 'center',
gap: '6px',
cursor: 'pointer',
boxShadow: '0 1px 2px rgba(0,0,0,0.03)'
}}
>
<RefreshCw size={14} className={isLoadingLogs ? 'is-loading' : ''} />
<span>Sync Account Data</span>
</button>
</div>
</div>
{/* Summary KPI Cards */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: '1rem' }}>
<div style={{ backgroundColor: '#FFFFFF', border: '1px solid #E5E7EB', borderRadius: '16px', padding: '1.25rem', boxShadow: '0 1px 3px rgba(0,0,0,0.02)' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px' }}>
<span style={{ fontSize: '0.75rem', fontWeight: 700, color: '#6B7280', textTransform: 'uppercase', letterSpacing: '0.04em' }}>Total Account SKUs</span>
<div style={{ width: '32px', height: '32px', borderRadius: '8px', backgroundColor: '#FFF0F2', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Package size={16} color="#701A29" />
</div>
</div>
<div style={{ fontSize: '1.6rem', fontWeight: 800, color: '#111827' }}>{inventory.length}</div>
<div style={{ fontSize: '0.72rem', color: '#6B7280', marginTop: '2px', display: 'flex', alignItems: 'center', gap: '6px' }}>
<span style={{ color: '#16A34A', fontWeight: 700 }}>✓ All Active</span>
<span>· Ready for discovery</span>
</div>
</div>
<div style={{ backgroundColor: '#FFFFFF', border: '1px solid #E5E7EB', borderRadius: '16px', padding: '1.25rem', boxShadow: '0 1px 3px rgba(0,0,0,0.02)' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px' }}>
<span style={{ fontSize: '0.75rem', fontWeight: 700, color: '#6B7280', textTransform: 'uppercase', letterSpacing: '0.04em' }}>Bulk Batch Uploads</span>
<div style={{ width: '32px', height: '32px', borderRadius: '8px', backgroundColor: '#EFF6FF', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<FileSpreadsheet size={16} color="#2563EB" />
</div>
</div>
<div style={{ fontSize: '1.6rem', fontWeight: 800, color: '#111827' }}>{bulkCount} <span style={{ fontSize: '0.82rem', color: '#6B7280', fontWeight: 500 }}>SKUs</span></div>
<div style={{ fontSize: '0.72rem', color: '#2563EB', marginTop: '2px', fontWeight: 600 }}>
{batches.length} Batch Log Records
</div>
</div>
<div style={{ backgroundColor: '#FFFFFF', border: '1px solid #E5E7EB', borderRadius: '16px', padding: '1.25rem', boxShadow: '0 1px 3px rgba(0,0,0,0.02)' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px' }}>
<span style={{ fontSize: '0.75rem', fontWeight: 700, color: '#6B7280', textTransform: 'uppercase', letterSpacing: '0.04em' }}>Individual Uploads</span>
<div style={{ width: '32px', height: '32px', borderRadius: '8px', backgroundColor: '#F0FDF4', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Plus size={16} color="#16A34A" />
</div>
</div>
<div style={{ fontSize: '1.6rem', fontWeight: 800, color: '#111827' }}>{individualCount} <span style={{ fontSize: '0.82rem', color: '#6B7280', fontWeight: 500 }}>SKUs</span></div>
<div style={{ fontSize: '0.72rem', color: '#16A34A', marginTop: '2px', fontWeight: 600 }}>
Direct craft form entry
</div>
</div>
<div style={{ backgroundColor: '#FFFFFF', border: '1px solid #E5E7EB', borderRadius: '16px', padding: '1.25rem', boxShadow: '0 1px 3px rgba(0,0,0,0.02)' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px' }}>
<span style={{ fontSize: '0.75rem', fontWeight: 700, color: '#6B7280', textTransform: 'uppercase', letterSpacing: '0.04em' }}>Catalog Value</span>
<div style={{ width: '32px', height: '32px', borderRadius: '8px', backgroundColor: '#FEF3C7', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Tag size={16} color="#D97706" />
</div>
</div>
<div style={{ fontSize: '1.6rem', fontWeight: 800, color: '#111827' }}>₹{totalCatalogValue.toLocaleString('en-IN')}</div>
<div style={{ fontSize: '0.72rem', color: lowStockCount > 0 ? '#DC2626' : '#6B7280', marginTop: '2px', fontWeight: lowStockCount > 0 ? 700 : 400 }}>
{lowStockCount > 0 ? `⚠ ${lowStockCount} items low stock` : '✓ Stock levels healthy'}
</div>
</div>
</div>
{/* File Upload Zone (Drop Zone & Download Template Cards) */}
<div className="columns is-multiline is-variable is-5 mb-0">
{/* Left: File Upload Zone */}
<div className="column is-7-desktop is-12-tablet">
<div style={{ backgroundColor: '#FFFFFF', border: '1px solid #E5E7EB', borderRadius: '18px', padding: '2rem', height: '100%', display: 'flex', flexDirection: 'column', justifyContent: 'space-between' }}>
<div style={{ backgroundColor: '#FFFFFF', border: '1px solid #E5E7EB', borderRadius: '18px', padding: '1.75rem', height: '100%', display: 'flex', flexDirection: 'column', justifyContent: 'space-between' }}>
<div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px' }}>
<h2 style={{ fontSize: '1.15rem', fontWeight: 800, color: '#111827' }}>Upload Batch File</h2>
<h2 style={{ fontSize: '1.1rem', fontWeight: 800, color: '#111827' }}>Upload Bulk Batch File</h2>
<span style={{ fontSize: '0.72rem', backgroundColor: '#F3F4F6', color: '#4B5563', padding: '3px 8px', borderRadius: '6px', fontWeight: 600 }}>
.CSV · .ZIP
</span>
</div>
<p style={{ fontSize: '0.78rem', color: '#6B7280', marginBottom: '1.25rem' }}>
Supported formats: <strong>.csv</strong> (catalog list) or <strong>.zip</strong> (catalog + image folders). Max 15 MB.
Supported formats: <strong>.csv</strong> (catalog list) or <strong>.zip</strong> (catalog + product image folders).
</p>
<input
@ -215,7 +447,7 @@ export const BulkUploadPage: React.FC = () => {
border: '2px dashed #701A29',
borderRadius: '16px',
backgroundColor: '#FFF9F9',
padding: '2.5rem 1.5rem',
padding: '2.2rem 1.25rem',
textAlign: 'center',
cursor: 'pointer',
transition: 'all 0.2s ease'
@ -223,14 +455,14 @@ export const BulkUploadPage: React.FC = () => {
onMouseOver={e => e.currentTarget.style.backgroundColor = '#FFF0F2'}
onMouseOut={e => e.currentTarget.style.backgroundColor = '#FFF9F9'}
>
<div style={{ width: '48px', height: '48px', borderRadius: '50%', backgroundColor: '#FEE2E2', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 12px' }}>
<UploadCloud size={24} color="#701A29" />
<div style={{ width: '44px', height: '44px', borderRadius: '50%', backgroundColor: '#FEE2E2', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 10px' }}>
<UploadCloud size={22} color="#701A29" />
</div>
<div style={{ fontWeight: 800, fontSize: '0.95rem', color: '#111827', marginBottom: '4px' }}>
{isUploading ? 'Validating & Ingesting SKUs to Database...' : 'Click to Browse or Drop Batch File Here'}
<div style={{ fontWeight: 800, fontSize: '0.92rem', color: '#111827', marginBottom: '4px' }}>
{isUploading ? 'Validating & Ingesting SKUs...' : 'Click to Browse or Drop Batch File Here'}
</div>
<p style={{ fontSize: '0.75rem', color: '#6B7280', marginBottom: '1rem', maxWidth: '380px', margin: '0 auto 1rem auto' }}>
Columns are automatically verified for GST rates, craft origin, prices, and stock inventory.
<p style={{ fontSize: '0.74rem', color: '#6B7280', marginBottom: '1rem', maxWidth: '380px', margin: '0 auto 1rem auto' }}>
Automatic validation for GST rates, craft origin, price, stock, and GI certification tags.
</p>
<button
type="button"
@ -328,13 +560,13 @@ export const BulkUploadPage: React.FC = () => {
</div>
</div>
{/* Right: Template Download and Instructions */}
{/* Right: Template Download & Direct Upload Options */}
<div className="column is-5-desktop is-12-tablet">
<div style={{ backgroundColor: '#FFFFFF', border: '1px solid #E5E7EB', borderRadius: '18px', padding: '1.75rem', height: '100%', display: 'flex', flexDirection: 'column', justifyContent: 'space-between' }}>
<div>
<h3 style={{ fontSize: '1.15rem', fontWeight: 800, color: '#111827', marginBottom: '4px' }}>Official Catalog Template</h3>
<h3 style={{ fontSize: '1.1rem', fontWeight: 800, color: '#111827', marginBottom: '4px' }}>Official Catalog Template</h3>
<p style={{ fontSize: '0.78rem', color: '#6B7280', marginBottom: '1.25rem' }}>
Use our standardized template to avoid inventory sync delays or incorrect GI tagging.
Download our standardized CSV template for batch product imports.
</p>
<div style={{ background: '#FAF8F5', borderRadius: '14px', border: '1px solid #E5E7EB', padding: '14px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '1.25rem' }}>
@ -370,53 +602,386 @@ export const BulkUploadPage: React.FC = () => {
</div>
<div style={{ backgroundColor: '#F9FAFB', borderRadius: '12px', padding: '12px 16px', fontSize: '0.75rem', color: '#4B5563', lineHeight: 1.6 }}>
<div style={{ fontWeight: 700, color: '#111827', marginBottom: '4px' }}>Spreadsheet Guidelines:</div>
<div style={{ fontWeight: 700, color: '#111827', marginBottom: '4px' }}>Batch Standards:</div>
<ul style={{ paddingLeft: '1.2rem', listStyleType: 'disc' }}>
<li><strong>Title &amp; SKU</strong>: Unique identifier per variant.</li>
<li><strong>Price &amp; MRP</strong>: Pure numeric values in INR.</li>
<li><strong>Price &amp; MRP</strong>: Numeric values in INR.</li>
<li><strong>GI_Tagged</strong>: Set to <code>Yes</code> or <code>No</code>.</li>
<li><strong>ZIP Ingestion</strong>: Name your image files to match the SKU (e.g. <code>TRDX-001.jpg</code>).</li>
<li><strong>ZIP Ingestion</strong>: Name image files matching SKU (e.g. <code>TRDX-001.jpg</code>).</li>
</ul>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginTop: '1rem', fontSize: '0.72rem', color: '#6B7280' }}>
<Info size={14} color="#701A29" />
<span>Need help? Check Heritage Cataloging standards in Settings.</span>
</div>
<span>Have a single craft item to add? Use the direct add product form.</span>
</div>
</div>
</div>
{/* Batch Processing History Table */}
<div style={{ backgroundColor: '#FFFFFF', border: '1px solid #E5E7EB', borderRadius: '18px', overflow: 'hidden', boxShadow: '0 1px 3px rgba(0,0,0,0.02)' }}>
<div style={{ padding: '16px 20px', borderBottom: '1px solid #F3F4F6', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div>
<h3 style={{ fontSize: '0.96rem', fontWeight: 800, color: '#111827' }}>Recent Batch History &amp; Upload Logs</h3>
<p style={{ fontSize: '0.75rem', color: '#6B7280', marginTop: '2px' }}>Track live processing results from your bulk uploads</p>
</div>
{/* Navigation Tabs for All Account Items & Bulk Logs */}
<div style={{ backgroundColor: '#FFFFFF', border: '1px solid #E5E7EB', borderRadius: '18px', overflow: 'hidden', boxShadow: '0 1px 3px rgba(0,0,0,0.02)' }}>
{/* Tab Header */}
<div style={{ padding: '14px 20px', borderBottom: '1px solid #E5E7EB', display: 'flex', flexWrap: 'wrap', alignItems: 'center', justifyContent: 'space-between', gap: '1rem', backgroundColor: '#FAF9F6' }}>
<div style={{ display: 'flex', gap: '8px' }}>
<button
type="button"
onClick={loadBatchLogs}
disabled={isLoadingLogs}
onClick={() => setActiveTab('all_items')}
style={{
backgroundColor: '#FFFFFF',
border: '1px solid #E5E7EB',
backgroundColor: activeTab === 'all_items' ? '#701A29' : '#FFFFFF',
color: activeTab === 'all_items' ? '#FFFFFF' : '#374151',
border: activeTab === 'all_items' ? 'none' : '1px solid #D1D5DB',
borderRadius: '9999px',
padding: '8px 18px',
fontSize: '0.8rem',
fontWeight: 700,
cursor: 'pointer',
display: 'inline-flex',
alignItems: 'center',
gap: '8px',
transition: 'all 0.2s ease',
boxShadow: activeTab === 'all_items' ? '0 2px 4px rgba(112,26,41,0.2)' : 'none'
}}
>
<Package size={15} />
<span>All Uploaded Items ({inventory.length})</span>
</button>
<button
type="button"
onClick={() => setActiveTab('bulk_logs')}
style={{
backgroundColor: activeTab === 'bulk_logs' ? '#701A29' : '#FFFFFF',
color: activeTab === 'bulk_logs' ? '#FFFFFF' : '#374151',
border: activeTab === 'bulk_logs' ? 'none' : '1px solid #D1D5DB',
borderRadius: '9999px',
padding: '8px 18px',
fontSize: '0.8rem',
fontWeight: 700,
cursor: 'pointer',
display: 'inline-flex',
alignItems: 'center',
gap: '8px',
transition: 'all 0.2s ease',
boxShadow: activeTab === 'bulk_logs' ? '0 2px 4px rgba(112,26,41,0.2)' : 'none'
}}
>
<FileSpreadsheet size={15} />
<span>Bulk Batch History ({batches.length})</span>
</button>
</div>
<div style={{ fontSize: '0.78rem', color: '#6B7280', fontWeight: 500 }}>
{activeTab === 'all_items' ? `Showing ${filteredInventory.length} of ${inventory.length} total products` : `Showing ${batches.length} batch upload sessions`}
</div>
</div>
{/* Tab 1: All Uploaded Items (Registry with Bulk & Individual details) */}
{activeTab === 'all_items' && (
<div>
{/* Filter & Search Toolbar */}
<div style={{ padding: '16px 20px', borderBottom: '1px solid #F3F4F6', display: 'flex', flexWrap: 'wrap', alignItems: 'center', justifyContent: 'space-between', gap: '1rem', backgroundColor: '#FFFFFF' }}>
{/* Search input */}
<div style={{ position: 'relative', minWidth: '260px', flex: '1 1 300px' }}>
<Search size={16} color="#9CA3AF" style={{ position: 'absolute', left: '12px', top: '50%', transform: 'translateY(-50%)' }} />
<input
type="text"
placeholder="Search by SKU, Product Title, or Craft Category..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
style={{
width: '100%',
padding: '8px 12px 8px 36px',
fontSize: '0.8rem',
borderRadius: '8px',
padding: '6px 12px',
fontSize: '0.75rem',
fontWeight: 600,
color: '#374151',
border: '1px solid #D1D5DB',
outline: 'none'
}}
/>
</div>
{/* Filters */}
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: '10px' }}>
{/* Source Filter */}
<div style={{ display: 'flex', alignItems: 'center', gap: '4px', fontSize: '0.78rem' }}>
<span style={{ color: '#6B7280', fontWeight: 600 }}>Source:</span>
<select
value={sourceFilter}
onChange={e => setSourceFilter(e.target.value as any)}
style={{ padding: '6px 10px', borderRadius: '8px', border: '1px solid #D1D5DB', fontSize: '0.78rem', backgroundColor: '#FFFFFF', cursor: 'pointer', fontWeight: 600 }}
>
<option value="all">All Sources ({inventory.length})</option>
<option value="bulk">Bulk CSV Batch ({bulkCount})</option>
<option value="individual">Individual Form ({individualCount})</option>
</select>
</div>
{/* Category Filter */}
<div style={{ display: 'flex', alignItems: 'center', gap: '4px', fontSize: '0.78rem' }}>
<span style={{ color: '#6B7280', fontWeight: 600 }}>Category:</span>
<select
value={categoryFilter}
onChange={e => setCategoryFilter(e.target.value)}
style={{ padding: '6px 10px', borderRadius: '8px', border: '1px solid #D1D5DB', fontSize: '0.78rem', backgroundColor: '#FFFFFF', cursor: 'pointer', fontWeight: 600 }}
>
{categoriesList.map(cat => (
<option key={cat} value={cat}>{cat}</option>
))}
</select>
</div>
{/* Stock Filter */}
<div style={{ display: 'flex', alignItems: 'center', gap: '4px', fontSize: '0.78rem' }}>
<span style={{ color: '#6B7280', fontWeight: 600 }}>Stock:</span>
<select
value={stockFilter}
onChange={e => setStockFilter(e.target.value as any)}
style={{ padding: '6px 10px', borderRadius: '8px', border: '1px solid #D1D5DB', fontSize: '0.78rem', backgroundColor: '#FFFFFF', cursor: 'pointer', fontWeight: 600 }}
>
<option value="all">All Stock Levels</option>
<option value="in_stock">In Stock (&gt;0)</option>
<option value="low_stock">Out of Stock (=0)</option>
</select>
</div>
</div>
</div>
{/* Product Items Table */}
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', textAlign: 'left' }}>
<thead>
<tr style={{ backgroundColor: '#F9FAFB', borderBottom: '1px solid #E5E7EB', fontSize: '0.72rem', fontWeight: 700, color: '#6B7280', textTransform: 'uppercase', letterSpacing: '0.04em' }}>
<th style={{ padding: '12px 18px' }}>Craft Product &amp; SKU</th>
<th style={{ padding: '12px 18px' }}>Upload Source</th>
<th style={{ padding: '12px 18px' }}>Category &amp; GI Tag</th>
<th style={{ padding: '12px 18px' }}>Pricing (INR)</th>
<th style={{ padding: '12px 18px' }}>Stock &amp; Adjust</th>
<th style={{ padding: '12px 18px' }}>Status</th>
<th style={{ padding: '12px 18px', textAlign: 'right' }}>Actions</th>
</tr>
</thead>
<tbody>
{filteredInventory.length === 0 ? (
<tr>
<td colSpan={7} style={{ padding: '36px', textAlign: 'center', color: '#6B7280', fontSize: '0.84rem' }}>
No items found matching your filter criteria. Try uploading a batch file above or clearing filters.
</td>
</tr>
) : (
filteredInventory.map(item => {
const srcInfo = getItemSourceInfo(item);
const SourceIcon = srcInfo.icon;
const stockVal = stockValues[item.id] ?? item.stock;
const healthKey = healthFromStock(stockVal);
const healthCfg = HEALTH_CONFIG[healthKey] || HEALTH_CONFIG.Adequate;
const discountPct = item.mrp && item.mrp > item.price ? Math.round(((item.mrp - item.price) / item.mrp) * 100) : 0;
return (
<tr key={item.id} style={{ borderBottom: '1px solid #F3F4F6', fontSize: '0.78rem' }}>
{/* Item Details */}
<td style={{ padding: '14px 18px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<img
src={item.image || '/images/placeholder.jpg'}
alt={item.title}
style={{ width: '44px', height: '44px', borderRadius: '8px', objectFit: 'cover', border: '1px solid #E5E7EB' }}
onError={(e) => {
(e.target as HTMLElement).setAttribute('src', 'https://images.unsplash.com/photo-1544022613-e87ca75a784a?auto=format&fit=crop&q=80&w=200');
}}
/>
<div>
<div style={{ fontWeight: 700, color: '#111827', fontSize: '0.84rem', lineHeight: 1.3 }}>{item.title}</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', marginTop: '2px' }}>
<span style={{ fontSize: '0.7rem', fontWeight: 700, color: '#701A29', backgroundColor: '#FFF0F2', padding: '1px 6px', borderRadius: '4px' }}>
{item.sku}
</span>
{item.giTagged && (
<span style={{ fontSize: '0.66rem', fontWeight: 700, color: '#047857', backgroundColor: '#D1FAE5', padding: '1px 6px', borderRadius: '4px', display: 'inline-flex', alignItems: 'center', gap: '3px' }}>
<ShieldCheck size={10} />
GI Tagged
</span>
)}
</div>
</div>
</div>
</td>
{/* Source Badge */}
<td style={{ padding: '14px 18px' }}>
<span style={{
display: 'inline-flex',
alignItems: 'center',
gap: '5px',
fontSize: '0.7rem',
fontWeight: 700,
backgroundColor: srcInfo.bg,
color: srcInfo.color,
border: `1px solid ${srcInfo.border}`,
borderRadius: '9999px',
padding: '4px 10px'
}}>
<SourceIcon size={12} />
<span>{srcInfo.label}</span>
</span>
</td>
{/* Category */}
<td style={{ padding: '14px 18px' }}>
<div style={{ fontWeight: 600, color: '#374151' }}>{item.category}</div>
<div style={{ fontSize: '0.7rem', color: '#6B7280', marginTop: '2px' }}>{item.craftTechnique || 'Authentic Craft'}</div>
</td>
{/* Pricing */}
<td style={{ padding: '14px 18px' }}>
<div style={{ fontWeight: 700, color: '#111827', fontSize: '0.85rem' }}>₹{item.price.toLocaleString('en-IN')}</div>
{item.mrp && item.mrp > item.price && (
<div style={{ fontSize: '0.7rem', color: '#9CA3AF', textDecoration: 'line-through' }}>
MRP ₹{item.mrp.toLocaleString('en-IN')} {discountPct > 0 && <span style={{ color: '#16A34A', textDecoration: 'none', fontWeight: 700 }}>({discountPct}% off)</span>}
</div>
)}
</td>
{/* Stock Controls */}
<td style={{ padding: '14px 18px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<button
type="button"
onClick={() => handleStepStock(item.id, -1)}
style={{
width: '24px',
height: '24px',
borderRadius: '6px',
border: '1px solid #D1D5DB',
backgroundColor: '#FFFFFF',
display: 'flex',
alignItems: 'center',
gap: '6px',
justifyContent: 'center',
cursor: 'pointer'
}}
>
<RefreshCw size={12} className={isLoadingLogs ? 'is-loading' : ''} />
<span>Refresh Logs</span>
<Minus size={12} />
</button>
<span style={{ fontWeight: 700, minWidth: '24px', textAlign: 'center', fontSize: '0.85rem', color: stockVal === 0 ? '#DC2626' : '#111827' }}>
{stockVal}
</span>
<button
type="button"
onClick={() => handleStepStock(item.id, 1)}
style={{
width: '24px',
height: '24px',
borderRadius: '6px',
border: '1px solid #D1D5DB',
backgroundColor: '#FFFFFF',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer'
}}
>
<Plus size={12} />
</button>
</div>
<div style={{ marginTop: '4px' }}>
<span style={{
fontSize: '0.65rem',
fontWeight: 700,
backgroundColor: healthCfg.bg,
color: healthCfg.color,
padding: '2px 6px',
borderRadius: '4px',
display: 'inline-flex',
alignItems: 'center',
gap: '3px'
}}>
<span style={{ width: '5px', height: '5px', borderRadius: '50%', backgroundColor: healthCfg.dot }}></span>
{healthCfg.label}
</span>
</div>
</td>
{/* Approval Status */}
<td style={{ padding: '14px 18px' }}>
<span style={{
fontSize: '0.7rem',
fontWeight: 700,
padding: '4px 10px',
borderRadius: '9999px',
backgroundColor: item.approvalStatus === 'approved' ? '#DCFCE7' : item.approvalStatus === 'rejected' ? '#FEE2E2' : '#FEF3C7',
color: item.approvalStatus === 'approved' ? '#166534' : item.approvalStatus === 'rejected' ? '#991B1B' : '#92400E',
display: 'inline-flex',
alignItems: 'center',
gap: '4px'
}}>
{item.approvalStatus === 'approved' ? <CheckCircle2 size={12} /> : item.approvalStatus === 'rejected' ? <AlertTriangle size={12} /> : <Clock size={12} />}
<span>{item.approvalStatus === 'approved' ? 'Approved & Live' : item.approvalStatus === 'rejected' ? 'Needs Review' : 'In Verification'}</span>
</span>
</td>
{/* Actions */}
<td style={{ padding: '14px 18px', textAlign: 'right' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '8px' }}>
<button
type="button"
onClick={() => handleOpenEdit(item)}
title="Quick edit product"
style={{
backgroundColor: '#FFFFFF',
border: '1px solid #D1D5DB',
borderRadius: '6px',
padding: '5px 8px',
fontSize: '0.72rem',
color: '#374151',
cursor: 'pointer',
display: 'inline-flex',
alignItems: 'center',
gap: '4px'
}}
>
<Edit3 size={12} />
<span>Edit</span>
</button>
<button
type="button"
onClick={() => handleDeleteItem(item.id, item.title)}
title="Delete item"
style={{
backgroundColor: '#FFFFFF',
border: '1px solid #FCA5A5',
borderRadius: '6px',
padding: '5px 8px',
fontSize: '0.72rem',
color: '#DC2626',
cursor: 'pointer',
display: 'inline-flex',
alignItems: 'center'
}}
>
<Trash2 size={12} />
</button>
</div>
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
</div>
)}
{/* Tab 2: Bulk Batch History & Ingestion Logs */}
{activeTab === 'bulk_logs' && (
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', textAlign: 'left' }}>
<thead>
@ -431,7 +996,14 @@ export const BulkUploadPage: React.FC = () => {
</tr>
</thead>
<tbody>
{batches.map((b) => (
{batches.length === 0 ? (
<tr>
<td colSpan={7} style={{ padding: '36px', textAlign: 'center', color: '#6B7280', fontSize: '0.84rem' }}>
No bulk upload batches processed yet. Upload your CSV or ZIP batch file above to add products to your catalog.
</td>
</tr>
) : (
batches.map((b) => (
<tr key={b.id} style={{ borderBottom: '1px solid #F3F4F6', fontSize: '0.78rem' }}>
<td style={{ padding: '14px 18px', fontWeight: 700, color: '#701A29' }}>{b.id}</td>
<td style={{ padding: '14px 18px' }}>
@ -481,11 +1053,140 @@ export const BulkUploadPage: React.FC = () => {
)}
</td>
</tr>
))}
))
)}
</tbody>
</table>
</div>
)}
</div>
{/* Quick Edit Modal */}
{editingProduct && (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.4)',
zIndex: 1000,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '1rem'
}}>
<div style={{
backgroundColor: '#FFFFFF',
borderRadius: '16px',
maxWidth: '460px',
width: '100%',
padding: '1.5rem',
boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)'
}}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '1rem' }}>
<h3 style={{ fontSize: '1.1rem', fontWeight: 800, color: '#111827' }}>Quick Edit Product</h3>
<button
type="button"
onClick={() => setEditingProduct(null)}
style={{ border: 'none', background: 'none', cursor: 'pointer', color: '#6B7280' }}
>
<X size={18} />
</button>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '1.25rem', backgroundColor: '#FAF9F6', padding: '10px 12px', borderRadius: '10px' }}>
<img
src={editingProduct.image || '/images/placeholder.jpg'}
alt={editingProduct.title}
style={{ width: '40px', height: '40px', borderRadius: '6px', objectFit: 'cover' }}
/>
<div>
<div style={{ fontWeight: 700, fontSize: '0.82rem', color: '#111827' }}>{editingProduct.title}</div>
<div style={{ fontSize: '0.7rem', color: '#6B7280' }}>SKU: {editingProduct.sku}</div>
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', marginBottom: '1.5rem' }}>
<div>
<label style={{ display: 'block', fontSize: '0.75rem', fontWeight: 700, color: '#374151', marginBottom: '4px' }}>
Selling Price (₹)
</label>
<input
type="number"
value={editPrice}
onChange={e => setEditPrice(e.target.value === '' ? '' : Number(e.target.value))}
style={{
width: '100%',
padding: '8px 12px',
fontSize: '0.84rem',
borderRadius: '8px',
border: '1px solid #D1D5DB',
outline: 'none'
}}
/>
</div>
<div>
<label style={{ display: 'block', fontSize: '0.75rem', fontWeight: 700, color: '#374151', marginBottom: '4px' }}>
Available Stock Quantity
</label>
<input
type="number"
value={editStock}
onChange={e => setEditStock(e.target.value === '' ? '' : Number(e.target.value))}
style={{
width: '100%',
padding: '8px 12px',
fontSize: '0.84rem',
borderRadius: '8px',
border: '1px solid #D1D5DB',
outline: 'none'
}}
/>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '10px' }}>
<button
type="button"
onClick={() => setEditingProduct(null)}
style={{
padding: '8px 16px',
borderRadius: '8px',
border: '1px solid #D1D5DB',
backgroundColor: '#FFFFFF',
color: '#374151',
fontSize: '0.8rem',
fontWeight: 600,
cursor: 'pointer'
}}
>
Cancel
</button>
<button
type="button"
onClick={handleSaveEditModal}
disabled={isSavingEdit}
style={{
padding: '8px 18px',
borderRadius: '8px',
border: 'none',
backgroundColor: '#701A29',
color: '#FFFFFF',
fontSize: '0.8rem',
fontWeight: 700,
cursor: isSavingEdit ? 'not-allowed' : 'pointer'
}}
>
{isSavingEdit ? 'Saving...' : 'Save Changes'}
</button>
</div>
</div>
</div>
)}
</div>
);
};

View file

@ -1,16 +1,14 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { useSeller } from '../../context/SellerContext';
import { api } from '../../services/api';
import {
Search,
Filter,
Trash2,
UploadCloud,
Plus,
Minus,
Award,
AlertCircle,
AlertTriangle,
Package,
IndianRupee,
@ -19,8 +17,7 @@ import {
Edit3,
ShieldCheck,
CheckCircle2,
Clock,
XCircle
FileSpreadsheet
} from 'lucide-react';
const HEALTH_CONFIG: Record<string, { label: string; bg: string; color: string; dot: string }> = {
@ -31,8 +28,8 @@ const HEALTH_CONFIG: Record<string, { label: string; bg: string; color: string;
};
function healthFromStock(stock: number): string {
if (stock <= 4) return 'Critical';
if (stock <= 10) return 'Low';
if (stock === 0) return 'Critical';
if (stock <= 4) return 'Low';
if (stock <= 20) return 'Adequate';
return 'Healthy';
}
@ -42,14 +39,45 @@ const PAGE_SIZE = 10;
export const InventoryPage: React.FC = () => {
const { inventory, updateStock, deleteProduct, fetchData } = useSeller();
useEffect(() => {
fetchData();
}, []);
const getItemSourceInfo = (item: any) => {
const isBulk = item.uploadSource === 'bulk' ||
item.sku?.startsWith('TRDX-') ||
item.sku?.startsWith('BATCH-') ||
item.sku?.includes('CSV');
if (isBulk) {
return {
type: 'bulk',
label: 'Bulk CSV',
icon: FileSpreadsheet,
bg: '#EFF6FF',
color: '#1D4ED8',
border: '#BFDBFE'
};
}
return {
type: 'individual',
label: 'Single Craft',
icon: Plus,
bg: '#F0FDF4',
color: '#15803D',
border: '#BBF7D0'
};
};
const allSkus = inventory.map(inv => ({
...inv,
subtitle: inv.craftTechnique || inv.category,
health: healthFromStock(inv.stock)
health: healthFromStock(inv.stock),
sourceInfo: getItemSourceInfo(inv)
}));
const [searchTerm, setSearchTerm] = useState('');
const [stockFilter, setStockFilter] = useState<'all' | 'in_stock' | 'low_stock'>('all');
const [sourceFilter, setSourceFilter] = useState<'all' | 'bulk' | 'individual'>('all');
const [categoryFilter, setCategoryFilter] = useState('All');
const [stockValues, setStockValues] = useState<Record<string, number>>({});
const [currentPage, setCurrentPage] = useState(1);
@ -57,7 +85,7 @@ export const InventoryPage: React.FC = () => {
const [editPrice, setEditPrice] = useState<number | ''>('');
const [isSavingEdit, setIsSavingEdit] = useState(false);
React.useEffect(() => {
useEffect(() => {
setStockValues(prev => {
const next = { ...prev };
inventory.forEach(inv => {
@ -67,21 +95,24 @@ export const InventoryPage: React.FC = () => {
});
}, [inventory]);
const categories = ['All', 'Pottery & Ceramics', 'Brass & Metal Craft', 'Woodcraft & Carvings', 'Handloom & Textiles', 'Jewellery & Ornaments'];
const categories = ['All', ...Array.from(new Set(allSkus.map(s => s.category || 'General')))];
const filtered = allSkus.filter(item => {
const q = searchTerm.toLowerCase();
const matchSearch = !q || item.title.toLowerCase().includes(q) || item.sku.toLowerCase().includes(q) || item.category.toLowerCase().includes(q);
const stk = stockValues[item.id] ?? item.stock;
const matchStock = stockFilter === 'all' ? true : stockFilter === 'in_stock' ? stk > 4 : stk <= 4;
const matchStock = stockFilter === 'all' ? true : stockFilter === 'in_stock' ? stk > 0 : stk <= 4;
const matchSource = sourceFilter === 'all' ? true : sourceFilter === item.sourceInfo.type;
const matchCat = categoryFilter === 'All' || item.category === categoryFilter;
return matchSearch && matchStock && matchCat;
return matchSearch && matchStock && matchSource && matchCat;
});
const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
const paginated = filtered.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE);
const totalListed = allSkus.length;
const bulkCount = allSkus.filter(s => s.sourceInfo.type === 'bulk').length;
const singleCount = allSkus.filter(s => s.sourceInfo.type === 'individual').length;
const lowStockCount = allSkus.filter(s => (stockValues[s.id] ?? s.stock) <= 4).length;
const catalogValue = allSkus.reduce((sum, s) => sum + (stockValues[s.id] ?? s.stock) * s.price, 0);
const saleableValue = allSkus.filter(s => (stockValues[s.id] ?? s.stock) > 0)
@ -102,6 +133,17 @@ export const InventoryPage: React.FC = () => {
updateStock(id, n);
};
const handleDeleteProduct = async (id: string, title: string) => {
if (window.confirm(`Are you sure you want to delete "${title}" from your inventory catalog?`)) {
try {
await deleteProduct(id);
await fetchData();
} catch (err: any) {
alert(err.message || 'Could not delete product');
}
}
};
const handleSaveEdit = async () => {
if (!editingItem) return;
setIsSavingEdit(true);
@ -114,6 +156,7 @@ export const InventoryPage: React.FC = () => {
if (editingItem.id && !String(editingItem.id).startsWith('prod-')) {
await api.patchProduct(editingItem.id, patchPayload);
}
await updateStock(editingItem.id, currentStock);
await fetchData();
} catch (err) {
console.warn('Failed to update product via quick edit drawer:', err);
@ -133,7 +176,7 @@ export const InventoryPage: React.FC = () => {
Catalog &amp; Inventory
</h1>
<p style={{ fontSize: '0.82rem', color: '#6B7280', marginTop: '4px' }}>
Manage your artisan SKUs, stock levels, and pricing across all craft disciplines.
Manage your artisan SKUs (both single and bulk uploaded items), stock levels, and pricing.
</p>
</div>
<div className="is-flex" style={{ gap: '0.6rem', flexWrap: 'wrap' }}>
@ -158,10 +201,11 @@ export const InventoryPage: React.FC = () => {
<Package size={14} color="#BE123C" />
</div>
</div>
<div style={{ fontSize: '2rem', fontWeight: 800, color: '#111827', lineHeight: 1.1 }}>{totalListed} <span style={{ fontSize: '1rem', fontWeight: 600, color: '#6B7280' }}>SKUs</span></div>
<div style={{ fontSize: '1.8rem', fontWeight: 800, color: '#111827', lineHeight: 1.1 }}>{totalListed} <span style={{ fontSize: '0.9rem', fontWeight: 600, color: '#6B7280' }}>SKUs</span></div>
<div className="is-flex is-align-items-center mt-2" style={{ gap: '6px', fontSize: '0.74rem' }}>
<span style={{ color: '#16A34A', fontWeight: 700 }}>↑ +6 this week</span>
<span style={{ color: '#6B7280' }}>· {allSkus.filter(s => (stockValues[s.id] ?? s.stock) > 0).length} active</span>
<span style={{ color: '#2563EB', fontWeight: 700 }}>{bulkCount} Bulk</span>
<span style={{ color: '#6B7280' }}>·</span>
<span style={{ color: '#16A34A', fontWeight: 700 }}>{singleCount} Single</span>
</div>
<div style={{ width: '36px', height: '3px', backgroundColor: '#701A29', borderRadius: '2px', marginTop: '8px' }} />
</div>
@ -175,8 +219,8 @@ export const InventoryPage: React.FC = () => {
<AlertTriangle size={14} color="#DC2626" />
</div>
</div>
<div style={{ fontSize: '2rem', fontWeight: 800, color: lowStockCount > 0 ? '#DC2626' : '#16A34A', lineHeight: 1.1 }}>
{lowStockCount} <span style={{ fontSize: '1rem', fontWeight: 600, color: '#6B7280' }}>Crafts</span>
<div style={{ fontSize: '1.8rem', fontWeight: 800, color: lowStockCount > 0 ? '#DC2626' : '#16A34A', lineHeight: 1.1 }}>
{lowStockCount} <span style={{ fontSize: '0.9rem', fontWeight: 600, color: '#6B7280' }}>Crafts</span>
</div>
<div style={{ fontSize: '0.74rem', color: '#6B7280', marginTop: '6px' }}>
{lowStockCount > 0 ? 'Restock urgently to avoid cancellations' : 'All SKUs adequately stocked'}
@ -207,19 +251,43 @@ export const InventoryPage: React.FC = () => {
{/* Filter Controls */}
<div className="p-4" style={{ backgroundColor: '#FFFFFF', border: '1px solid #E5E7EB', borderRadius: '16px', boxShadow: '0 1px 3px rgba(0,0,0,0.02)' }}>
<div className="is-flex is-justify-content-space-between is-align-items-center is-flex-wrap-wrap" style={{ gap: '0.75rem' }}>
<div className="is-flex" style={{ gap: '6px', flexWrap: 'wrap' }}>
{([
{ key: 'all', label: `All (${allSkus.length})` },
{ key: 'in_stock', label: `In Stock (${allSkus.filter(s => (stockValues[s.id] ?? s.stock) > 4).length})` },
{ key: 'low_stock', label: `Low Stock (${lowStockCount})` },
] as const).map(f => (
<button key={f.key} type="button" onClick={() => { setStockFilter(f.key); setCurrentPage(1); }}
<button
type="button"
onClick={() => { setSourceFilter('all'); setStockFilter('all'); setCurrentPage(1); }}
className="button is-small"
style={{ borderRadius: '9999px', backgroundColor: stockFilter === f.key ? '#701A29' : '#F9FAFB', borderColor: stockFilter === f.key ? '#701A29' : '#E5E7EB', color: stockFilter === f.key ? '#FFFFFF' : '#374151', fontWeight: 600, fontSize: '0.78rem', height: '32px', padding: '0 14px', transition: 'all 0.15s ease' }}
style={{ borderRadius: '9999px', backgroundColor: (sourceFilter === 'all' && stockFilter === 'all') ? '#701A29' : '#F9FAFB', borderColor: (sourceFilter === 'all' && stockFilter === 'all') ? '#701A29' : '#E5E7EB', color: (sourceFilter === 'all' && stockFilter === 'all') ? '#FFFFFF' : '#374151', fontWeight: 600, fontSize: '0.78rem', height: '32px', padding: '0 14px' }}
>
{f.label}
All ({allSkus.length})
</button>
<button
type="button"
onClick={() => { setSourceFilter('bulk'); setCurrentPage(1); }}
className="button is-small"
style={{ borderRadius: '9999px', backgroundColor: sourceFilter === 'bulk' ? '#2563EB' : '#F9FAFB', borderColor: sourceFilter === 'bulk' ? '#2563EB' : '#E5E7EB', color: sourceFilter === 'bulk' ? '#FFFFFF' : '#374151', fontWeight: 600, fontSize: '0.78rem', height: '32px', padding: '0 14px' }}
>
Bulk Uploaded ({bulkCount})
</button>
<button
type="button"
onClick={() => { setSourceFilter('individual'); setCurrentPage(1); }}
className="button is-small"
style={{ borderRadius: '9999px', backgroundColor: sourceFilter === 'individual' ? '#16A34A' : '#F9FAFB', borderColor: sourceFilter === 'individual' ? '#16A34A' : '#E5E7EB', color: sourceFilter === 'individual' ? '#FFFFFF' : '#374151', fontWeight: 600, fontSize: '0.78rem', height: '32px', padding: '0 14px' }}
>
Single Uploaded ({singleCount})
</button>
<button
type="button"
onClick={() => { setStockFilter('low_stock'); setCurrentPage(1); }}
className="button is-small"
style={{ borderRadius: '9999px', backgroundColor: stockFilter === 'low_stock' ? '#DC2626' : '#F9FAFB', borderColor: stockFilter === 'low_stock' ? '#DC2626' : '#E5E7EB', color: stockFilter === 'low_stock' ? '#FFFFFF' : '#374151', fontWeight: 600, fontSize: '0.78rem', height: '32px', padding: '0 14px' }}
>
Low Stock ({lowStockCount})
</button>
))}
</div>
<div className="is-flex is-align-items-center" style={{ gap: '0.6rem', flexWrap: 'wrap' }}>
@ -268,6 +336,8 @@ export const InventoryPage: React.FC = () => {
const stk = stockValues[item.id] ?? item.stock;
const health = (item as { health?: string }).health || healthFromStock(stk);
const hConf = HEALTH_CONFIG[health] || HEALTH_CONFIG['Healthy'];
const SrcIcon = item.sourceInfo.icon;
return (
<tr key={item.id} style={{ borderBottom: idx < paginated.length - 1 ? '1px solid #F3F4F6' : 'none' }}
onMouseEnter={e => (e.currentTarget.style.backgroundColor = '#FAFAFA')}
@ -275,16 +345,33 @@ export const InventoryPage: React.FC = () => {
>
<td style={{ padding: '14px 16px', verticalAlign: 'middle', minWidth: '260px' }}>
<div className="is-flex is-align-items-center" style={{ gap: '12px' }}>
<img src={item.image} alt={item.title}
<img src={item.image || '/images/placeholder.jpg'} alt={item.title}
style={{ width: '48px', height: '48px', borderRadius: '10px', objectFit: 'cover', border: '1px solid #E5E7EB', flexShrink: 0 }}
onError={e => { (e.target as HTMLImageElement).src = '/terracotta_vases.jpg'; }}
onError={e => { (e.target as HTMLImageElement).src = 'https://images.unsplash.com/photo-1544022613-e87ca75a784a?auto=format&fit=crop&q=80&w=200'; }}
/>
<div>
<div style={{ fontSize: '0.86rem', fontWeight: 700, color: '#111827', lineHeight: 1.3 }}>{item.title}</div>
<div style={{ fontSize: '0.72rem', color: '#6B7280', marginTop: '2px' }}>
{(item as { subtitle?: string }).subtitle || item.craftTechnique}
</div>
<div style={{ fontSize: '0.68rem', fontWeight: 700, color: '#701A29', marginTop: '3px' }}>{item.sku}</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', marginTop: '3px' }}>
<span style={{ fontSize: '0.68rem', fontWeight: 700, color: '#701A29' }}>{item.sku}</span>
<span style={{
fontSize: '0.62rem',
fontWeight: 700,
backgroundColor: item.sourceInfo.bg,
color: item.sourceInfo.color,
border: `1px solid ${item.sourceInfo.border}`,
borderRadius: '4px',
padding: '1px 5px',
display: 'inline-flex',
alignItems: 'center',
gap: '3px'
}}>
<SrcIcon size={9} />
{item.sourceInfo.label}
</span>
</div>
</div>
</div>
</td>
@ -301,8 +388,12 @@ export const InventoryPage: React.FC = () => {
<td style={{ padding: '14px 16px', verticalAlign: 'middle', minWidth: '120px' }}>
<div style={{ fontSize: '1rem', fontWeight: 800, color: '#111827' }}>₹{item.price.toLocaleString('en-IN')}</div>
{item.mrp && item.mrp > item.price && (
<div style={{ fontSize: '0.72rem', color: '#9CA3AF', textDecoration: 'line-through', marginTop: '2px' }}>MRP ₹{item.mrp.toLocaleString('en-IN')}</div>
)}
{item.mrp && item.mrp > item.price && (
<div style={{ fontSize: '0.68rem', color: '#16A34A', fontWeight: 700, marginTop: '2px' }}>{Math.round(((item.mrp - item.price) / item.mrp) * 100)}% off</div>
)}
</td>
<td style={{ padding: '14px 16px', verticalAlign: 'middle', minWidth: '140px' }}>
@ -318,60 +409,61 @@ export const InventoryPage: React.FC = () => {
<Plus size={12} color="#374151" />
</button>
</div>
<div style={{ fontSize: '0.68rem', color: '#9CA3AF', marginTop: '4px' }}>{item.salesCount} sold all-time</div>
</td>
<td style={{ padding: '14px 16px', verticalAlign: 'middle', minWidth: '140px' }}>
{((item as any).approvalStatus === 'pending' || (item as any).status === 'pending') ? (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '5px', backgroundColor: '#FEF3C7', color: '#92400E', fontSize: '0.72rem', fontWeight: 700, borderRadius: '9999px', padding: '4px 10px' }}>
<Clock size={12} color="#D97706" />
Pending Approval
<td style={{ padding: '14px 16px', verticalAlign: 'middle', minWidth: '130px' }}>
<span className="is-inline-flex is-align-items-center" style={{
fontSize: '0.7rem',
fontWeight: 700,
padding: '4px 10px',
borderRadius: '9999px',
backgroundColor: item.approvalStatus === 'approved' ? '#DCFCE7' : item.approvalStatus === 'rejected' ? '#FEE2E2' : '#FEF3C7',
color: item.approvalStatus === 'approved' ? '#166534' : item.approvalStatus === 'rejected' ? '#991B1B' : '#92400E',
gap: '4px'
}}>
<CheckCircle2 size={12} />
<span>{item.approvalStatus === 'approved' ? 'Approved' : item.approvalStatus === 'rejected' ? 'Rejected' : 'Pending'}</span>
</span>
) : (item as any).approvalStatus === 'rejected' ? (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '5px', backgroundColor: '#FEE2E2', color: '#991B1B', fontSize: '0.72rem', fontWeight: 700, borderRadius: '9999px', padding: '4px 10px' }}>
<XCircle size={12} color="#DC2626" />
Rejected
</span>
) : (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '5px', backgroundColor: '#E6F4EA', color: '#166534', fontSize: '0.72rem', fontWeight: 700, borderRadius: '9999px', padding: '4px 10px' }}>
<CheckCircle2 size={12} color="#16A34A" />
Approved
</span>
)}
</td>
<td style={{ padding: '14px 16px', verticalAlign: 'middle', minWidth: '120px' }}>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '5px', backgroundColor: hConf.bg, color: hConf.color, fontSize: '0.72rem', fontWeight: 700, borderRadius: '9999px', padding: '4px 10px' }}>
<span style={{ width: '6px', height: '6px', borderRadius: '50%', backgroundColor: hConf.dot, flexShrink: 0 }} />
{hConf.label}
<span className="is-inline-flex is-align-items-center" style={{
fontSize: '0.7rem',
fontWeight: 700,
backgroundColor: hConf.bg,
color: hConf.color,
padding: '4px 10px',
borderRadius: '9999px',
gap: '5px'
}}>
<span style={{ width: '6px', height: '6px', borderRadius: '50%', backgroundColor: hConf.dot }}></span>
<span>{hConf.label}</span>
</span>
</td>
<td style={{ padding: '14px 16px', verticalAlign: 'middle', minWidth: '80px' }}>
<div className="is-flex" style={{ gap: '4px' }}>
<button type="button" title="Quick Edit SKU" onClick={() => { setEditingItem(item); setEditPrice(item.price); }}
style={{ width: '30px', height: '30px', borderRadius: '8px', border: '1px solid #E5E7EB', backgroundColor: '#FFFFFF', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', transition: 'all 0.2s' }}
onMouseOver={e => e.currentTarget.style.backgroundColor = '#F3F4F6'}
onMouseOut={e => e.currentTarget.style.backgroundColor = '#FFFFFF'}
<td style={{ padding: '14px 16px', verticalAlign: 'middle', textAlign: 'right' }}>
<div className="is-flex is-align-items-center is-justify-content-end" style={{ gap: '6px' }}>
<button type="button" onClick={() => { setEditingItem(item); setEditPrice(item.price); }}
className="button is-small" style={{ borderRadius: '6px', height: '28px', padding: '0 8px', border: '1px solid #D1D5DB', backgroundColor: '#FFFFFF', color: '#374151' }}
>
<Edit3 size={13} color="#6B7280" />
<Edit3 size={12} />
</button>
<button type="button" onClick={() => deleteProduct(item.id)} title="Delete SKU"
style={{ width: '30px', height: '30px', borderRadius: '8px', border: '1px solid #FFE4E6', backgroundColor: '#FFF5F5', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', transition: 'all 0.2s' }}
onMouseOver={e => e.currentTarget.style.backgroundColor = '#FEE2E2'}
onMouseOut={e => e.currentTarget.style.backgroundColor = '#FFF5F5'}
<button type="button" onClick={() => handleDeleteProduct(item.id, item.title)}
className="button is-small" style={{ borderRadius: '6px', height: '28px', padding: '0 8px', border: '1px solid #FCA5A5', backgroundColor: '#FFFFFF', color: '#DC2626' }}
>
<Trash2 size={13} color="#DC2626" />
<Trash2 size={12} />
</button>
</div>
</td>
</tr>
);
}) : (
<tr>
<td colSpan={7} style={{ padding: '3rem', textAlign: 'center' }}>
<AlertCircle size={32} color="#D1D5DB" style={{ marginBottom: '8px' }} />
<p style={{ fontSize: '0.88rem', color: '#6B7280' }}>No matching craft SKUs found.</p>
<td colSpan={7} style={{ padding: '48px 24px', textAlign: 'center', color: '#6B7280' }}>
<AlertTriangle size={24} color="#9CA3AF" style={{ margin: '0 auto 8px', display: 'block' }} />
<p style={{ fontSize: '0.86rem', fontWeight: 600, color: '#374151' }}>No matching craft SKUs found.</p>
<p style={{ fontSize: '0.78rem', color: '#9CA3AF', marginTop: '4px' }}>Try resetting your search query or filter settings above.</p>
</td>
</tr>
)}
@ -379,127 +471,32 @@ export const InventoryPage: React.FC = () => {
</table>
</div>
{/* Pagination Strip */}
<div className="px-5 py-3 is-flex is-align-items-center is-justify-content-space-between" style={{ borderTop: '1px solid #F3F4F6', backgroundColor: '#FAFAFA', flexWrap: 'wrap', gap: '0.5rem' }}>
<span style={{ fontSize: '0.76rem', color: '#6B7280' }}>
Showing <strong style={{ color: '#111827' }}>{Math.min(filtered.length, currentPage * PAGE_SIZE)}</strong> of{' '}
<strong style={{ color: '#111827' }}>{filtered.length > 0 ? filtered.length : allSkus.length} Handcrafted SKUs</strong> · All GI Tags Verified
</span>
<div className="is-flex is-align-items-center" style={{ gap: '4px' }}>
{/* Pagination Footer */}
{filtered.length > 0 && (
<div className="px-5 py-3 is-flex is-align-items-center is-justify-content-space-between" style={{ borderTop: '1px solid #F3F4F6', fontSize: '0.78rem', color: '#6B7280' }}>
<div>
Showing <strong style={{ color: '#111827' }}>{paginated.length}</strong> of <strong style={{ color: '#111827' }}>{filtered.length}</strong> Handcrafted SKUs
</div>
<div className="is-flex is-align-items-center" style={{ gap: '6px' }}>
<button type="button" onClick={() => setCurrentPage(p => Math.max(1, p - 1))} disabled={currentPage === 1}
style={{ width: '28px', height: '28px', borderRadius: '6px', border: '1px solid #E5E7EB', backgroundColor: '#FFFFFF', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: currentPage === 1 ? 'not-allowed' : 'pointer', opacity: currentPage === 1 ? 0.4 : 1 }}>
<ChevronLeft size={14} color="#374151" />
</button>
{Array.from({ length: totalPages }, (_, i) => i + 1).map(pg => (
<button key={pg} type="button" onClick={() => setCurrentPage(pg)}
style={{ minWidth: '28px', height: '28px', borderRadius: '6px', border: `1px solid ${pg === currentPage ? '#701A29' : '#E5E7EB'}`, backgroundColor: pg === currentPage ? '#701A29' : '#FFFFFF', color: pg === currentPage ? '#FFFFFF' : '#374151', fontSize: '0.78rem', fontWeight: 600, cursor: 'pointer', padding: '0 6px' }}>
{pg}
</button>
))}
<button type="button" onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))} disabled={currentPage === totalPages}
style={{ width: '28px', height: '28px', borderRadius: '6px', border: '1px solid #E5E7EB', backgroundColor: '#FFFFFF', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: currentPage === totalPages ? 'not-allowed' : 'pointer', opacity: currentPage === totalPages ? 0.4 : 1 }}>
<ChevronRight size={14} color="#374151" />
</button>
</div>
</div>
</div>
{/* Quick Edit Drawer */}
{editingItem && (
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, zIndex: 100, display: 'flex', justifyContent: 'flex-end' }}>
{/* Backdrop */}
<div
style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(255, 255, 255, 0.7)', backdropFilter: 'blur(3px)' }}
onClick={() => setEditingItem(null)}
/>
{/* Drawer Panel */}
<div style={{ position: 'relative', width: '440px', backgroundColor: '#FFFFFF', height: '100%', display: 'flex', flexDirection: 'column', boxShadow: '-4px 0 24px rgba(0,0,0,0.06)' }}>
{/* Drawer Header */}
<div style={{ padding: '24px 32px', display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between' }}>
<div>
<h2 style={{ fontSize: '1.25rem', fontWeight: 800, color: '#111827' }}>Quick Edit SKU</h2>
<div style={{ fontSize: '0.8rem', color: '#6B7280', marginTop: '2px' }}>SKU: {editingItem.sku}</div>
</div>
<button type="button" onClick={() => setEditingItem(null)} style={{ width: '32px', height: '32px', borderRadius: '50%', backgroundColor: '#F3F4F6', border: 'none', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#374151" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</button>
</div>
{/* Drawer Content Scrollable */}
<div style={{ flex: 1, overflowY: 'auto', padding: '0 32px 32px 32px', display: 'flex', flexDirection: 'column', gap: '20px' }}>
{/* Product Info Card */}
<div style={{ backgroundColor: '#FAF7F7', borderRadius: '12px', padding: '16px', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<img src={editingItem.image} alt="Product" style={{ width: '48px', height: '48px', borderRadius: '8px', objectFit: 'cover' }} onError={e => { (e.target as HTMLImageElement).src = '/terracotta_vases.jpg'; }} />
<div>
<div style={{ fontSize: '0.9rem', fontWeight: 700, color: '#111827', lineHeight: 1.2 }}>{editingItem.title}</div>
<div style={{ fontSize: '0.75rem', color: '#6B7280', marginTop: '2px' }}>{editingItem.category} · Handcrafted</div>
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '4px' }}>
{/* Toggle Switch */}
<div style={{ width: '40px', height: '22px', backgroundColor: '#701A29', borderRadius: '11px', display: 'flex', alignItems: 'center', justifyContent: 'flex-end', padding: '2px', cursor: 'pointer' }}>
<div style={{ width: '18px', height: '18px', backgroundColor: '#FFFFFF', borderRadius: '50%' }} />
</div>
<div style={{ fontSize: '0.65rem', fontWeight: 700, color: '#166534' }}>Live on Storefront</div>
</div>
</div>
{/* Selling Price Card */}
<div style={{ backgroundColor: '#FAF7F7', borderRadius: '12px', padding: '16px' }}>
<label style={{ display: 'block', fontSize: '0.85rem', fontWeight: 600, color: '#374151', marginBottom: '8px' }}>Selling Price</label>
<div style={{ position: 'relative' }}>
<span style={{ position: 'absolute', left: '16px', top: '50%', transform: 'translateY(-50%)', fontWeight: 600, color: '#374151' }}>₹</span>
<input
type="number"
value={editPrice}
onChange={e => setEditPrice(e.target.value === '' ? '' : parseFloat(e.target.value))}
style={{ width: '100%', border: 'none', borderRadius: '4px', padding: '12px 16px 12px 32px', fontSize: '1rem', fontWeight: 700, color: '#111827', outline: 'none' }}
/>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: '12px', fontSize: '0.75rem' }}>
<span style={{ color: '#6B7280' }}>Artisan Payout (100% retained)</span>
<span style={{ fontWeight: 700, color: '#701A29', fontSize: '0.9rem' }}>₹{editPrice || editingItem.price}</span>
</div>
</div>
{/* Stock on Hand Card */}
<div style={{ backgroundColor: '#FAF7F7', borderRadius: '12px', padding: '16px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<div style={{ fontSize: '0.85rem', fontWeight: 600, color: '#374151' }}>Stock on Hand</div>
<div style={{ fontSize: '0.75rem', color: '#6B7280', marginTop: '2px' }}>Finished sets ready for dispatch</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', backgroundColor: '#FFFFFF', borderRadius: '4px', border: 'none', overflow: 'hidden' }}>
<button type="button" onClick={() => handleStepStock(editingItem.id, -1)} style={{ width: '32px', height: '36px', display: 'flex', alignItems: 'center', justifyContent: 'center', border: 'none', backgroundColor: '#FFFFFF', cursor: 'pointer', fontSize: '1.2rem', color: '#374151' }}>−</button>
<div style={{ width: '36px', height: '36px', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '0.9rem', fontWeight: 700, color: '#111827' }}>{(stockValues[editingItem.id] ?? editingItem.stock)}</div>
<button type="button" onClick={() => handleStepStock(editingItem.id, 1)} style={{ width: '32px', height: '36px', display: 'flex', alignItems: 'center', justifyContent: 'center', border: 'none', backgroundColor: '#FFFFFF', cursor: 'pointer', fontSize: '1.2rem', color: '#374151' }}>+</button>
</div>
</div>
<div style={{ fontSize: '0.85rem', fontWeight: 600, color: '#374151', paddingLeft: '4px' }}>Lead Time / Dispatch Readiness</div>
</div>
{/* Drawer Footer */}
<div style={{ padding: '24px 32px', display: 'flex', justifyContent: 'flex-end', gap: '12px' }}>
<button type="button" onClick={() => setEditingItem(null)} style={{ padding: '10px 24px', borderRadius: '9999px', border: 'none', backgroundColor: '#F3F4F6', color: '#374151', fontSize: '0.85rem', fontWeight: 600, cursor: 'pointer' }}>Cancel</button>
<button
type="button"
onClick={handleSaveEdit}
disabled={isSavingEdit}
style={{ padding: '10px 24px', borderRadius: '9999px', border: 'none', backgroundColor: '#701A29', color: '#FFFFFF', fontSize: '0.85rem', fontWeight: 700, cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '6px', opacity: isSavingEdit ? 0.7 : 1 }}
className="button is-small" style={{ borderRadius: '6px', height: '28px', width: '28px', padding: 0, opacity: currentPage === 1 ? 0.4 : 1 }}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>
{isSavingEdit ? 'Saving...' : 'Save Changes'}
<ChevronLeft size={14} />
</button>
<span style={{ fontWeight: 700, color: '#701A29', backgroundColor: '#FFF0F2', width: '28px', height: '28px', display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: '6px' }}>
{currentPage}
</span>
<button type="button" onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))} disabled={currentPage === totalPages}
className="button is-small" style={{ borderRadius: '6px', height: '28px', width: '28px', padding: 0, opacity: currentPage === totalPages ? 0.4 : 1 }}
>
<ChevronRight size={14} />
</button>
</div>
</div>
</div>
)}
</div>
</div>
);
};