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
})));

File diff suppressed because it is too large Load diff

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); }}
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' }}
>
{f.label}
</button>
))}
<button
type="button"
onClick={() => { setSourceFilter('all'); setStockFilter('all'); setCurrentPage(1); }}
className="button is-small"
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' }}
>
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>
<div style={{ fontSize: '0.72rem', color: '#9CA3AF', textDecoration: 'line-through', marginTop: '2px' }}>MRP ₹{item.mrp.toLocaleString('en-IN')}</div>
<div style={{ fontSize: '0.68rem', color: '#16A34A', fontWeight: 700, marginTop: '2px' }}>{Math.round(((item.mrp - item.price) / item.mrp) * 100)}% off</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
</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}
<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>
</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', minWidth: '120px' }}>
<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', 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' }}>
<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}
{/* 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}
className="button is-small" style={{ borderRadius: '6px', height: '28px', width: '28px', padding: 0, opacity: currentPage === 1 ? 0.4 : 1 }}
>
<ChevronLeft size={14} />
</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>
<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>
{/* 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 }}
>
<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'}
</button>
</div>
</div>
</div>
)}
</div>
);
};