feat: add seller context, product management pages, and initiative validation tests
This commit is contained in:
parent
557871606c
commit
6187041789
6 changed files with 654 additions and 17 deletions
|
|
@ -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": {
|
||||
|
|
|
|||
98
scripts/test_initiatives_validation.js
Normal file
98
scripts/test_initiatives_validation.js
Normal 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 ---');
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 & 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 & 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 & 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>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 & pricing columns</div>
|
||||
<div style={{ fontSize: '0.72rem', color: '#6B7280' }}>All ODOP, GI Tag, Sustainable, heritage & 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 & SKU</strong>: Unique identifier per variant.</li>
|
||||
<li><strong>Price & 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 & District</strong>: <code>Yes/No</code> and origin district (e.g., <em>Jaipur</em>).</li>
|
||||
<li><strong>GI_Tagged & GI_Tag_Name</strong>: <code>Yes/No</code> and registered GI name.</li>
|
||||
<li><strong>Sustainable_Product & 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 & 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' }}>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue