feat: add seller context, product management pages, and initiative validation tests
All checks were successful
Beta CI/CD Pipeline / build-and-test (push) Successful in 2m5s
Beta CI/CD Pipeline / deploy-beta (push) Successful in 44s

This commit is contained in:
vickytechkey 2026-11-08 15:48:39 +05:30
parent 557871606c
commit 6187041789
6 changed files with 654 additions and 17 deletions

View file

@ -7,7 +7,7 @@
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "tsc --noEmit",
"test": "echo 'All tests passed'",
"test": "node scripts/test_initiatives_validation.js && tsc --noEmit",
"preview": "vite preview"
},
"dependencies": {

View file

@ -0,0 +1,98 @@
// Test suite for ODOP, GI Tag, and Sustainable Product slabs
import assert from 'assert';
console.log('--- Running Frontend Initiatives Test Suite ---');
// Test 1: Sample CSV Headers Validation
function testSampleCsvHeaders() {
const expectedHeaders = [
'Title', 'SKU', 'Category', 'Price', 'MRP', 'Stock', 'Description',
'ODOP_Product', 'ODOP_District', 'GI_Tagged', 'GI_Tag_Name',
'Sustainable_Product', 'Sustainable_Type', 'Package_Weight_Kg',
'Shipping_Profile', 'Image_URL', 'Additional_Images', 'Video_URL'
];
const headersStr = 'Title,SKU,Category,Price,MRP,Stock,Description,ODOP_Product,ODOP_District,GI_Tagged,GI_Tag_Name,Sustainable_Product,Sustainable_Type,Package_Weight_Kg,Shipping_Profile,Image_URL,Additional_Images,Video_URL';
const parsedHeaders = headersStr.split(',');
assert.strictEqual(parsedHeaders.length, 18, 'Template should contain all 18 headers');
expectedHeaders.forEach(h => {
assert.ok(parsedHeaders.includes(h), `Header ${h} must be present in sample CSV`);
});
console.log('✓ Test 1 Passed: Sample CSV template contains all 18 mandatory & initiative columns');
}
// Test 2: Payload Construction for Individual Product Upload
function testIndividualUploadPayload() {
const formState = {
title: 'Kanchipuram Silk Saree',
sku: 'TRDX-KSS-001',
category: 'Clothing & Textiles',
price: 6500,
mrp: 8500,
stock: 12,
isOdop: true,
odopDistrict: 'Kanchipuram, Tamil Nadu',
giTagged: true,
giTagName: 'Kancheepuram Silk (GI-12)',
isSustainable: true,
sustainableType: '100% Biodegradable & Natural Fibers'
};
const payload = {
title: formState.title,
sku: formState.sku,
category: formState.category,
price: formState.price,
mrp: formState.mrp,
stock: formState.stock,
is_odop: formState.isOdop,
odop_district: formState.isOdop ? formState.odopDistrict.trim() : '',
is_gi_tagged: formState.giTagged,
gi_tag_name: formState.giTagged ? formState.giTagName.trim() : '',
is_sustainable: formState.isSustainable,
sustainable_type: formState.isSustainable ? formState.sustainableType.trim() : ''
};
assert.strictEqual(payload.is_odop, true);
assert.strictEqual(payload.odop_district, 'Kanchipuram, Tamil Nadu');
assert.strictEqual(payload.is_gi_tagged, true);
assert.strictEqual(payload.gi_tag_name, 'Kancheepuram Silk (GI-12)');
assert.strictEqual(payload.is_sustainable, true);
assert.strictEqual(payload.sustainable_type, '100% Biodegradable & Natural Fibers');
console.log('✓ Test 2 Passed: Individual product payload correctly formats initiative flags and text fields');
}
// Test 3: Disabled Initiatives Clear Sub-Fields
function testDisabledInitiativesSanitization() {
const formState = {
isOdop: false,
odopDistrict: 'Stale District',
giTagged: false,
giTagName: 'Stale Tag',
isSustainable: false,
sustainableType: 'Stale Type'
};
const payload = {
is_odop: formState.isOdop,
odop_district: formState.isOdop ? formState.odopDistrict.trim() : '',
is_gi_tagged: formState.giTagged,
gi_tag_name: formState.giTagged ? formState.giTagName.trim() : '',
is_sustainable: formState.isSustainable,
sustainable_type: formState.isSustainable ? formState.sustainableType.trim() : ''
};
assert.strictEqual(payload.is_odop, false);
assert.strictEqual(payload.odop_district, '', 'District must be empty when ODOP is disabled');
assert.strictEqual(payload.is_gi_tagged, false);
assert.strictEqual(payload.gi_tag_name, '', 'GI Tag name must be empty when GI is disabled');
assert.strictEqual(payload.is_sustainable, false);
assert.strictEqual(payload.sustainable_type, '', 'Sustainability type must be empty when sustainable is disabled');
console.log('✓ Test 3 Passed: Toggling flags off cleanly resets associated sub-attributes');
}
testSampleCsvHeaders();
testIndividualUploadPayload();
testDisabledInitiativesSanitization();
console.log('--- All Frontend Initiatives Tests Passed Successfully ---');

View file

@ -257,6 +257,17 @@ export const SellerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
category: p.category || 'General',
craftTechnique: p.craftTechnique || 'Authentic Craft',
giTagged: p.is_gi_tagged || false,
is_gi_tagged: p.is_gi_tagged || false,
gi_tag_name: p.gi_tag_name || '',
giTagName: p.gi_tag_name || '',
is_odop: p.is_odop || false,
isOdop: p.is_odop || false,
odop_district: p.odop_district || '',
odopDistrict: p.odop_district || '',
is_sustainable: p.is_sustainable || false,
isSustainable: p.is_sustainable || false,
sustainable_type: p.sustainable_type || '',
sustainableType: p.sustainable_type || '',
price: parseFloat(p.price) || 0,
mrp: parseFloat(p.compare_at_price) || ((parseFloat(p.price) || 0) * 1.2),
stock: p.stock || 0,
@ -583,13 +594,31 @@ export const SellerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
};
const addProduct = async (product: Omit<InventoryItem, 'id' | 'salesCount'>) => {
const isGi = Boolean(product.is_gi_tagged || product.giTagged);
const giName = product.gi_tag_name || product.giTagName || '';
const isOdop = Boolean(product.is_odop || product.isOdop);
const odopDist = product.odop_district || product.odopDistrict || '';
const isSust = Boolean(product.is_sustainable || product.isSustainable);
const sustType = product.sustainable_type || product.sustainableType || '';
const newLocalItem: InventoryItem = {
id: `prod-${Date.now()}`,
sku: product.sku || `TRDX-${Date.now().toString().slice(-5)}`,
title: product.title,
category: product.category,
craftTechnique: product.craftTechnique || 'Authentic Craft',
giTagged: product.giTagged || false,
giTagged: isGi,
is_gi_tagged: isGi,
gi_tag_name: giName,
giTagName: giName,
is_odop: isOdop,
isOdop: isOdop,
odop_district: odopDist,
odopDistrict: odopDist,
is_sustainable: isSust,
isSustainable: isSust,
sustainable_type: sustType,
sustainableType: sustType,
price: product.price,
mrp: product.mrp,
stock: product.stock,
@ -613,7 +642,12 @@ export const SellerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
stock: product.stock,
image: product.image,
description: product.description || '',
is_gi_tagged: product.giTagged || false,
is_gi_tagged: isGi,
gi_tag_name: isGi ? giName : '',
is_odop: isOdop,
odop_district: isOdop ? odopDist : '',
is_sustainable: isSust,
sustainable_type: isSust ? sustType : '',
additional_images: product.additional_images || [],
video_url: product.video_url || null,
compare_at_price: product.compare_at_price || product.mrp || null,

View file

@ -125,8 +125,14 @@ export const AddProductPage: React.FC = () => {
const [category, setCategory] = useState(editProduct?.category || '');
const [craftType, setCraftType] = useState(editProduct?.craftTechnique || '');
const [cluster, setCluster] = useState('');
const [giTagged, setGiTagged] = useState(editProduct?.giTagged || false);
const [giCertRef, setGiCertRef] = useState('');
// Step 1: Heritage & Initiative slabs state
const [isOdop, setIsOdop] = useState<boolean>(Boolean(editProduct?.is_odop || editProduct?.isOdop));
const [odopDistrict, setOdopDistrict] = useState<string>(editProduct?.odop_district || editProduct?.odopDistrict || '');
const [giTagged, setGiTagged] = useState<boolean>(Boolean(editProduct?.is_gi_tagged || editProduct?.giTagged));
const [giTagName, setGiTagName] = useState<string>(editProduct?.gi_tag_name || editProduct?.giTagName || editProduct?.giCertRef || '');
const [isSustainable, setIsSustainable] = useState<boolean>(Boolean(editProduct?.is_sustainable || editProduct?.isSustainable));
const [sustainableType, setSustainableType] = useState<string>(editProduct?.sustainable_type || editProduct?.sustainableType || '');
const handleSupportingPhotoUpload = async (index: number, e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
@ -229,9 +235,29 @@ export const AddProductPage: React.FC = () => {
const [fragile, setFragile] = useState(Boolean(editProduct?.shipping_profile?.includes('Fragile')));
useEffect(() => {
if (editProduct?.image) {
setImage(editProduct.image);
setCoverPreview(editProduct.image);
if (editProduct) {
if (editProduct.image) {
setImage(editProduct.image);
setCoverPreview(editProduct.image);
}
if (editProduct.is_odop !== undefined || editProduct.isOdop !== undefined) {
setIsOdop(Boolean(editProduct.is_odop || editProduct.isOdop));
}
if (editProduct.odop_district || editProduct.odopDistrict) {
setOdopDistrict(editProduct.odop_district || editProduct.odopDistrict);
}
if (editProduct.is_gi_tagged !== undefined || editProduct.giTagged !== undefined) {
setGiTagged(Boolean(editProduct.is_gi_tagged || editProduct.giTagged));
}
if (editProduct.gi_tag_name || editProduct.giTagName) {
setGiTagName(editProduct.gi_tag_name || editProduct.giTagName);
}
if (editProduct.is_sustainable !== undefined || editProduct.isSustainable !== undefined) {
setIsSustainable(Boolean(editProduct.is_sustainable || editProduct.isSustainable));
}
if (editProduct.sustainable_type || editProduct.sustainableType) {
setSustainableType(editProduct.sustainable_type || editProduct.sustainableType);
}
}
}, [editProduct]);
@ -361,6 +387,17 @@ export const AddProductPage: React.FC = () => {
description: story || `${title} - Handcrafted ${category}`,
craftTechnique: craftType || craftTechnique || 'Traditional',
giTagged,
is_gi_tagged: giTagged,
gi_tag_name: giTagged ? giTagName.trim() : '',
giTagName: giTagged ? giTagName.trim() : '',
is_odop: isOdop,
isOdop,
odop_district: isOdop ? odopDistrict.trim() : '',
odopDistrict: isOdop ? odopDistrict.trim() : '',
is_sustainable: isSustainable,
isSustainable,
sustainable_type: isSustainable ? sustainableType.trim() : '',
sustainableType: isSustainable ? sustainableType.trim() : '',
price: Number(price),
mrp: Number(mrp) || Math.round(Number(price) * 1.2),
compare_at_price: Number(mrp) || Math.round(Number(price) * 1.2),
@ -584,6 +621,361 @@ export const AddProductPage: React.FC = () => {
</select>
</div>
)}
{/* Heritage, GI & Sustainability Slabs */}
<div style={{ marginTop: '2rem', paddingTop: '1.75rem', borderTop: '1px solid #F3F4F6' }}>
<div style={{ marginBottom: '1.25rem' }}>
<h3 style={{ fontSize: '1.05rem', fontWeight: 800, color: '#111827', display: 'flex', alignItems: 'center', gap: '8px' }}>
<span>Craft Heritage &amp; Sustainability Slabs</span>
<span style={{ fontSize: '0.7rem', fontWeight: 600, color: '#6B7280', backgroundColor: '#F3F4F6', padding: '2px 8px', borderRadius: '12px' }}>Optional</span>
</h3>
<p style={{ fontSize: '0.8rem', color: '#6B7280', marginTop: '4px' }}>
Highlight recognized regional origin, GI registry status, and sustainability credentials to conscious buyers.
</p>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '14px' }}>
{/* 1. ODOP Product Slab */}
<div style={{
border: isOdop ? '1.5px solid #F59E0B' : '1px solid #E5E7EB',
borderRadius: '16px',
backgroundColor: isOdop ? '#FFFDF5' : '#FAFAFA',
padding: '16px 20px',
transition: 'all 0.2s ease',
boxShadow: isOdop ? '0 4px 12px rgba(245, 158, 11, 0.08)' : 'none'
}}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '14px' }}>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '14px' }}>
<div style={{
width: '42px',
height: '42px',
borderRadius: '12px',
backgroundColor: isOdop ? '#FEF3C7' : '#F3F4F6',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
transition: 'background-color 0.2s ease'
}}>
<Award size={22} color={isOdop ? '#B45309' : '#6B7280'} />
</div>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' }}>
<span style={{ fontWeight: 800, fontSize: '0.92rem', color: isOdop ? '#92400E' : '#1F2937' }}>
ODOP Product (One District One Product)
</span>
<span style={{
fontSize: '0.64rem',
fontWeight: 800,
letterSpacing: '0.04em',
textTransform: 'uppercase',
padding: '2px 8px',
borderRadius: '9999px',
backgroundColor: isOdop ? '#FEF3C7' : '#E5E7EB',
color: isOdop ? '#92400E' : '#4B5563'
}}>
Govt. Initiative
</span>
</div>
<p style={{ fontSize: '0.78rem', color: '#6B7280', marginTop: '3px', lineHeight: 1.4 }}>
Recognized under the Government of India's ODOP initiative fostering regional craftsmanship &amp; indigenous clusters.
</p>
</div>
</div>
{/* Toggle Switch */}
<button
type="button"
onClick={() => { setIsOdop(!isOdop); simulateAutoSave(); }}
style={{
position: 'relative',
width: '48px',
height: '26px',
borderRadius: '9999px',
backgroundColor: isOdop ? '#B45309' : '#E5E7EB',
border: 'none',
cursor: 'pointer',
transition: 'background-color 0.2s ease',
flexShrink: 0,
padding: '2px',
display: 'flex',
alignItems: 'center'
}}
title={isOdop ? 'Disable ODOP designation' : 'Enable ODOP designation'}
>
<span style={{
width: '22px',
height: '22px',
borderRadius: '50%',
backgroundColor: '#FFFFFF',
transform: isOdop ? 'translateX(22px)' : 'translateX(0px)',
transition: 'transform 0.2s ease',
boxShadow: '0 1px 3px rgba(0,0,0,0.2)'
}} />
</button>
</div>
{isOdop && (
<div style={{ marginTop: '14px', paddingTop: '14px', borderTop: '1px solid #FEF3C7' }}>
<label style={{ display: 'block', fontSize: '0.78rem', fontWeight: 700, color: '#92400E', marginBottom: '6px' }}>
Associated ODOP District &amp; State <span style={{ color: '#DC2626' }}>*</span>
</label>
<input
type="text"
value={odopDistrict}
onChange={e => { setOdopDistrict(e.target.value); simulateAutoSave(); }}
placeholder="e.g. Kanchipuram (Tamil Nadu), Moradabad (Uttar Pradesh)"
style={{
width: '100%',
height: '40px',
border: '1px solid #FCD34D',
borderRadius: '10px',
padding: '0 14px',
fontSize: '0.86rem',
color: '#111827',
outline: 'none',
backgroundColor: '#FFFFFF'
}}
/>
<div style={{ fontSize: '0.72rem', color: '#B45309', marginTop: '5px' }}>
This district badge will be shown to buyers and automatically connects to ODOP regional collection filters.
</div>
</div>
)}
</div>
{/* 2. GI Tag Product Slab */}
<div style={{
border: giTagged ? '1.5px solid #10B981' : '1px solid #E5E7EB',
borderRadius: '16px',
backgroundColor: giTagged ? '#F0FDF4' : '#FAFAFA',
padding: '16px 20px',
transition: 'all 0.2s ease',
boxShadow: giTagged ? '0 4px 12px rgba(16, 185, 129, 0.08)' : 'none'
}}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '14px' }}>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '14px' }}>
<div style={{
width: '42px',
height: '42px',
borderRadius: '12px',
backgroundColor: giTagged ? '#DCFCE7' : '#F3F4F6',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
transition: 'background-color 0.2s ease'
}}>
<ShieldCheck size={22} color={giTagged ? '#059669' : '#6B7280'} />
</div>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' }}>
<span style={{ fontWeight: 800, fontSize: '0.92rem', color: giTagged ? '#065F46' : '#1F2937' }}>
GI Tag Product (Geographical Indication)
</span>
<span style={{
fontSize: '0.64rem',
fontWeight: 800,
letterSpacing: '0.04em',
textTransform: 'uppercase',
padding: '2px 8px',
borderRadius: '9999px',
backgroundColor: giTagged ? '#DCFCE7' : '#E5E7EB',
color: giTagged ? '#065F46' : '#4B5563'
}}>
Certified Heritage
</span>
</div>
<p style={{ fontSize: '0.78rem', color: '#6B7280', marginTop: '3px', lineHeight: 1.4 }}>
Holds a registered Geographical Indication certifying authentic craft origins rooted in a protected geographical territory.
</p>
</div>
</div>
{/* Toggle Switch */}
<button
type="button"
onClick={() => { setGiTagged(!giTagged); simulateAutoSave(); }}
style={{
position: 'relative',
width: '48px',
height: '26px',
borderRadius: '9999px',
backgroundColor: giTagged ? '#059669' : '#E5E7EB',
border: 'none',
cursor: 'pointer',
transition: 'background-color 0.2s ease',
flexShrink: 0,
padding: '2px',
display: 'flex',
alignItems: 'center'
}}
title={giTagged ? 'Disable GI Tag designation' : 'Enable GI Tag designation'}
>
<span style={{
width: '22px',
height: '22px',
borderRadius: '50%',
backgroundColor: '#FFFFFF',
transform: giTagged ? 'translateX(22px)' : 'translateX(0px)',
transition: 'transform 0.2s ease',
boxShadow: '0 1px 3px rgba(0,0,0,0.2)'
}} />
</button>
</div>
{giTagged && (
<div style={{ marginTop: '14px', paddingTop: '14px', borderTop: '1px solid #BBF7D0' }}>
<label style={{ display: 'block', fontSize: '0.78rem', fontWeight: 700, color: '#065F46', marginBottom: '6px' }}>
GI Tag Name or Certificate Reference <span style={{ color: '#DC2626' }}>*</span>
</label>
<input
type="text"
value={giTagName}
onChange={e => { setGiTagName(e.target.value); simulateAutoSave(); }}
placeholder="e.g. Kancheepuram Silk (GI-12), Tanjore Art Plate, Mysore Sandalwood Oil"
style={{
width: '100%',
height: '40px',
border: '1px solid #A7F3D0',
borderRadius: '10px',
padding: '0 14px',
fontSize: '0.86rem',
color: '#111827',
outline: 'none',
backgroundColor: '#FFFFFF'
}}
/>
<div style={{ fontSize: '0.72rem', color: '#047857', marginTop: '5px' }}>
Accredited GI certificate credentials are surfaced on the product showcase page with authentic verification badge.
</div>
</div>
)}
</div>
{/* 3. Sustainable Product Slab */}
<div style={{
border: isSustainable ? '1.5px solid #0284C7' : '1px solid #E5E7EB',
borderRadius: '16px',
backgroundColor: isSustainable ? '#F0F9FF' : '#FAFAFA',
padding: '16px 20px',
transition: 'all 0.2s ease',
boxShadow: isSustainable ? '0 4px 12px rgba(2, 132, 199, 0.08)' : 'none'
}}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '14px' }}>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '14px' }}>
<div style={{
width: '42px',
height: '42px',
borderRadius: '12px',
backgroundColor: isSustainable ? '#E0F2FE' : '#F3F4F6',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
transition: 'background-color 0.2s ease'
}}>
<Leaf size={22} color={isSustainable ? '#0284C7' : '#6B7280'} />
</div>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' }}>
<span style={{ fontWeight: 800, fontSize: '0.92rem', color: isSustainable ? '#0369A1' : '#1F2937' }}>
Sustainable Product Slab
</span>
<span style={{
fontSize: '0.64rem',
fontWeight: 800,
letterSpacing: '0.04em',
textTransform: 'uppercase',
padding: '2px 8px',
borderRadius: '9999px',
backgroundColor: isSustainable ? '#E0F2FE' : '#E5E7EB',
color: isSustainable ? '#0369A1' : '#4B5563'
}}>
Eco-Conscious
</span>
</div>
<p style={{ fontSize: '0.78rem', color: '#6B7280', marginTop: '3px', lineHeight: 1.4 }}>
Handmade using non-toxic, zero-plastic, natural, or circular upcycled materials with low environmental footprint.
</p>
</div>
</div>
{/* Toggle Switch */}
<button
type="button"
onClick={() => { setIsSustainable(!isSustainable); simulateAutoSave(); }}
style={{
position: 'relative',
width: '48px',
height: '26px',
borderRadius: '9999px',
backgroundColor: isSustainable ? '#0284C7' : '#E5E7EB',
border: 'none',
cursor: 'pointer',
transition: 'background-color 0.2s ease',
flexShrink: 0,
padding: '2px',
display: 'flex',
alignItems: 'center'
}}
title={isSustainable ? 'Disable Sustainable designation' : 'Enable Sustainable designation'}
>
<span style={{
width: '22px',
height: '22px',
borderRadius: '50%',
backgroundColor: '#FFFFFF',
transform: isSustainable ? 'translateX(22px)' : 'translateX(0px)',
transition: 'transform 0.2s ease',
boxShadow: '0 1px 3px rgba(0,0,0,0.2)'
}} />
</button>
</div>
{isSustainable && (
<div style={{ marginTop: '14px', paddingTop: '14px', borderTop: '1px solid #BAE6FD' }}>
<label style={{ display: 'block', fontSize: '0.78rem', fontWeight: 700, color: '#0369A1', marginBottom: '6px' }}>
Sustainability Classification / Slab Type
</label>
<input
type="text"
list="sustainable-options"
value={sustainableType}
onChange={e => { setSustainableType(e.target.value); simulateAutoSave(); }}
placeholder="Select or enter classification (e.g. 100% Biodegradable, Natural Dyes)"
style={{
width: '100%',
height: '40px',
border: '1px solid #7DD3FC',
borderRadius: '10px',
padding: '0 14px',
fontSize: '0.86rem',
color: '#111827',
outline: 'none',
backgroundColor: '#FFFFFF'
}}
/>
<datalist id="sustainable-options">
<option value="100% Biodegradable & Natural Fibers" />
<option value="Upcycled & Zero-Waste Handcraft" />
<option value="Chemical-Free / Natural Organic Dyes" />
<option value="Hand-spun & Hand-woven Carbon-Neutral" />
<option value="Sustainably Harvested Clay / Terracotta" />
<option value="Sustainably Sourced Wood & Bamboo" />
<option value="Recycled Brass / Metal Heritage" />
<option value="Zero-Plastic Eco Packaging" />
</datalist>
<div style={{ fontSize: '0.72rem', color: '#0284C7', marginTop: '5px' }}>
Enables the Sustainable Craft badge on your product card and makes it discoverable in eco-friendly storefront collections.
</div>
</div>
)}
</div>
</div>
</div>
</div>
)}

View file

@ -20,6 +20,7 @@ import {
Trash2,
Tag,
ShieldCheck,
Leaf,
Check,
X,
Sparkles,
@ -81,6 +82,12 @@ export const BulkUploadPage: React.FC = () => {
const [editingProduct, setEditingProduct] = useState<any | null>(null);
const [editPrice, setEditPrice] = useState<number | ''>('');
const [editStock, setEditStock] = useState<number | ''>('');
const [editIsOdop, setEditIsOdop] = useState<boolean>(false);
const [editOdopDistrict, setEditOdopDistrict] = useState<string>('');
const [editIsGi, setEditIsGi] = useState<boolean>(false);
const [editGiTagName, setEditGiTagName] = useState<string>('');
const [editIsSustainable, setEditIsSustainable] = useState<boolean>(false);
const [editSustainableType, setEditSustainableType] = useState<string>('');
const [isSavingEdit, setIsSavingEdit] = useState(false);
const [stockValues, setStockValues] = useState<Record<string, number>>({});
@ -206,11 +213,11 @@ export const BulkUploadPage: React.FC = () => {
};
const handleDownloadSampleCsv = () => {
const headers = 'Title,SKU,Category,Price,MRP,Stock,Description,GI_Tagged,Package_Weight_Kg,Shipping_Profile,Image_URL\n';
const headers = 'Title,SKU,Category,Price,MRP,Stock,Description,ODOP_Product,ODOP_District,GI_Tagged,GI_Tag_Name,Sustainable_Product,Sustainable_Type,Package_Weight_Kg,Shipping_Profile,Image_URL,Additional_Images,Video_URL\n';
const rows = [
'Jaipur Blue Pottery Floral Vase,TRDX-JBP-001,Handicrafts & Art,1499,1999,20,Handcrafted glazed ceramic blue pottery floral vase from Jaipur cluster.,Yes,1.2,Fragile Handcrafted,https://images.unsplash.com/photo-1544022613-e87ca75a784a?auto=format&fit=crop&q=80&w=600',
'Kanchipuram Pure Mulberry Silk Saree,TRDX-KSS-002,Handloom & Textiles,6500,8500,12,Generational handloom pure mulberry silk saree with gold zari border.,Yes,0.8,Standard Fragile,https://images.unsplash.com/photo-1610030469983-98e550d6193c?auto=format&fit=crop&q=80&w=600',
'Saharanpur Hand-Carved Sheesham Tray,TRDX-SHM-003,Home & Living,1850,2400,15,Solid sheesham wood serving tray with traditional floral brass inlay work.,No,1.5,Standard Fragile,https://images.unsplash.com/photo-1578749556568-bc2c40e68b61?auto=format&fit=crop&q=80&w=600'
'Jaipur Blue Pottery Floral Vase,TRDX-JBP-001,Handicrafts & Art,1499,1999,20,Handcrafted glazed ceramic blue pottery floral vase from Jaipur cluster.,Yes,Jaipur (Rajasthan),Yes,Jaipur Blue Pottery,Yes,Zero-Lead Eco Glaze,1.2,Fragile Handcrafted,https://images.unsplash.com/photo-1544022613-e87ca75a784a?auto=format&fit=crop&q=80&w=600,,',
'Kanchipuram Pure Mulberry Silk Saree,TRDX-KSS-002,Handloom & Textiles,6500,8500,12,Generational handloom pure mulberry silk saree with gold zari border.,Yes,Kanchipuram (Tamil Nadu),Yes,Kanchipuram Silk,Yes,Natural Mulberry Silk,0.8,Standard Fragile,https://images.unsplash.com/photo-1610030469983-98e550d6193c?auto=format&fit=crop&q=80&w=600,,',
'Saharanpur Hand-Carved Sheesham Tray,TRDX-SHM-003,Wooden Bamboo & Coir Products,1850,2400,15,Solid sheesham wood serving tray with traditional floral brass inlay work.,Yes,Saharanpur (Uttar Pradesh),No,,Yes,Sustainably Harvested Sheesham Wood,1.5,Standard Fragile,https://images.unsplash.com/photo-1578749556568-bc2c40e68b61?auto=format&fit=crop&q=80&w=600,,'
].join('\n');
const csvContent = headers + rows;
@ -274,6 +281,12 @@ export const BulkUploadPage: React.FC = () => {
setEditingProduct(item);
setEditPrice(item.price);
setEditStock(stockValues[item.id] ?? item.stock);
setEditIsOdop(Boolean(item.isOdop || item.is_odop));
setEditOdopDistrict(item.odopDistrict || item.odop_district || '');
setEditIsGi(Boolean(item.giTagged || item.is_gi_tagged));
setEditGiTagName(item.giTagName || item.gi_tag_name || '');
setEditIsSustainable(Boolean(item.isSustainable || item.is_sustainable));
setEditSustainableType(item.sustainableType || item.sustainable_type || '');
};
const handleSaveEditModal = async () => {
@ -286,7 +299,13 @@ export const BulkUploadPage: React.FC = () => {
if (editingProduct.id && !String(editingProduct.id).startsWith('prod-')) {
await api.patchProduct(editingProduct.id, {
price: updatedPrice,
stock: updatedStock
stock: updatedStock,
is_odop: editIsOdop,
odop_district: editIsOdop ? editOdopDistrict.trim() : '',
is_gi_tagged: editIsGi,
gi_tag_name: editIsGi ? editGiTagName.trim() : '',
is_sustainable: editIsSustainable,
sustainable_type: editIsSustainable ? editSustainableType.trim() : ''
});
}
await updateStock(editingProduct.id, updatedStock);
@ -580,7 +599,7 @@ export const BulkUploadPage: React.FC = () => {
</div>
<div>
<div style={{ fontSize: '0.82rem', fontWeight: 700, color: '#111827' }}>Craft_Standard_Catalog.csv</div>
<div style={{ fontSize: '0.72rem', color: '#6B7280' }}>All 11 heritage &amp; pricing columns</div>
<div style={{ fontSize: '0.72rem', color: '#6B7280' }}>All ODOP, GI Tag, Sustainable, heritage &amp; pricing columns</div>
</div>
</div>
<button
@ -610,7 +629,9 @@ export const BulkUploadPage: React.FC = () => {
<ul style={{ paddingLeft: '1.2rem', listStyleType: 'disc' }}>
<li><strong>Title &amp; SKU</strong>: Unique identifier per variant.</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>ODOP_Product &amp; District</strong>: <code>Yes/No</code> and origin district (e.g., <em>Jaipur</em>).</li>
<li><strong>GI_Tagged &amp; GI_Tag_Name</strong>: <code>Yes/No</code> and registered GI name.</li>
<li><strong>Sustainable_Product &amp; Type</strong>: <code>Yes/No</code> and eco-craft slab classification.</li>
<li><strong>ZIP Ingestion</strong>: Name image files matching SKU (e.g. <code>TRDX-001.jpg</code>).</li>
</ul>
</div>
@ -807,12 +828,24 @@ export const BulkUploadPage: React.FC = () => {
<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' }}>
{(item.isOdop || item.is_odop) && (
<span style={{ fontSize: '0.66rem', fontWeight: 700, color: '#B45309', backgroundColor: '#FEF3C7', padding: '1px 6px', borderRadius: '4px', display: 'inline-flex', alignItems: 'center', gap: '3px' }} title={item.odopDistrict || item.odop_district ? `ODOP: ${item.odopDistrict || item.odop_district}` : 'One District One Product'}>
<Award size={10} />
ODOP
</span>
)}
{(item.giTagged || item.is_gi_tagged) && (
<span style={{ fontSize: '0.66rem', fontWeight: 700, color: '#047857', backgroundColor: '#D1FAE5', padding: '1px 6px', borderRadius: '4px', display: 'inline-flex', alignItems: 'center', gap: '3px' }} title={item.giTagName || item.gi_tag_name ? `GI: ${item.giTagName || item.gi_tag_name}` : 'Geographical Indication'}>
<ShieldCheck size={10} />
GI Tagged
</span>
)}
{(item.isSustainable || item.is_sustainable) && (
<span style={{ fontSize: '0.66rem', fontWeight: 700, color: '#0369A1', backgroundColor: '#E0F2FE', padding: '1px 6px', borderRadius: '4px', display: 'inline-flex', alignItems: 'center', gap: '3px' }} title={item.sustainableType || item.sustainable_type ? `Sustainable: ${item.sustainableType || item.sustainable_type}` : 'Sustainable Craft'}>
<Leaf size={10} />
Sustainable
</span>
)}
</div>
</div>
</div>
@ -1150,6 +1183,73 @@ export const BulkUploadPage: React.FC = () => {
}}
/>
</div>
{/* Heritage & Sustainability Tags */}
<div style={{ borderTop: '1px solid #F3F4F6', paddingTop: '10px', display: 'flex', flexDirection: 'column', gap: '8px' }}>
<span style={{ fontSize: '0.72rem', fontWeight: 800, color: '#4B5563', textTransform: 'uppercase', letterSpacing: '0.04em' }}>
Heritage &amp; Sustainability Tags
</span>
{/* ODOP Toggle */}
<div style={{ backgroundColor: editIsOdop ? '#FFFDF5' : '#FAFAFA', border: editIsOdop ? '1px solid #FCD34D' : '1px solid #E5E7EB', borderRadius: '8px', padding: '8px 10px' }}>
<label style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', cursor: 'pointer', fontSize: '0.78rem', fontWeight: 700, color: '#111827' }}>
<span style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<Award size={14} color="#B45309" />
ODOP Product
</span>
<input type="checkbox" checked={editIsOdop} onChange={e => setEditIsOdop(e.target.checked)} />
</label>
{editIsOdop && (
<input
type="text"
placeholder="ODOP District (e.g. Jaipur, Rajasthan)"
value={editOdopDistrict}
onChange={e => setEditOdopDistrict(e.target.value)}
style={{ width: '100%', marginTop: '6px', padding: '6px 10px', fontSize: '0.76rem', borderRadius: '6px', border: '1px solid #D1D5DB', outline: 'none' }}
/>
)}
</div>
{/* GI Tag Toggle */}
<div style={{ backgroundColor: editIsGi ? '#F0FDF4' : '#FAFAFA', border: editIsGi ? '1px solid #A7F3D0' : '1px solid #E5E7EB', borderRadius: '8px', padding: '8px 10px' }}>
<label style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', cursor: 'pointer', fontSize: '0.78rem', fontWeight: 700, color: '#111827' }}>
<span style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<ShieldCheck size={14} color="#059669" />
GI Tagged Product
</span>
<input type="checkbox" checked={editIsGi} onChange={e => setEditIsGi(e.target.checked)} />
</label>
{editIsGi && (
<input
type="text"
placeholder="GI Tag Name (e.g. Kanchipuram Silk)"
value={editGiTagName}
onChange={e => setEditGiTagName(e.target.value)}
style={{ width: '100%', marginTop: '6px', padding: '6px 10px', fontSize: '0.76rem', borderRadius: '6px', border: '1px solid #D1D5DB', outline: 'none' }}
/>
)}
</div>
{/* Sustainable Toggle */}
<div style={{ backgroundColor: editIsSustainable ? '#F0F9FF' : '#FAFAFA', border: editIsSustainable ? '1px solid #7DD3FC' : '1px solid #E5E7EB', borderRadius: '8px', padding: '8px 10px' }}>
<label style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', cursor: 'pointer', fontSize: '0.78rem', fontWeight: 700, color: '#111827' }}>
<span style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<Leaf size={14} color="#0284C7" />
Sustainable Product
</span>
<input type="checkbox" checked={editIsSustainable} onChange={e => setEditIsSustainable(e.target.checked)} />
</label>
{editIsSustainable && (
<input
type="text"
placeholder="Sustainability Type (e.g. Biodegradable, Natural Dyes)"
value={editSustainableType}
onChange={e => setEditSustainableType(e.target.value)}
style={{ width: '100%', marginTop: '6px', padding: '6px 10px', fontSize: '0.76rem', borderRadius: '6px', border: '1px solid #D1D5DB', outline: 'none' }}
/>
)}
</div>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '10px' }}>

View file

@ -96,6 +96,17 @@ export interface InventoryItem {
category: string;
craftTechnique: string;
giTagged: boolean;
is_gi_tagged?: boolean;
gi_tag_name?: string;
giTagName?: string;
is_odop?: boolean;
isOdop?: boolean;
odop_district?: string;
odopDistrict?: string;
is_sustainable?: boolean;
isSustainable?: boolean;
sustainable_type?: string;
sustainableType?: string;
price: number;
mrp: number;
stock: number;
@ -110,6 +121,8 @@ export interface InventoryItem {
compare_at_price?: number;
package_weight?: number;
shipping_profile?: string;
uploadSource?: string;
upload_source?: string;
dimensions?: string;
materials?: string;
care_instructions?: string;