From 61870417898fa3faa9a0d06eb35441a652ec75fa Mon Sep 17 00:00:00 2001 From: vickytechkey Date: Sun, 8 Nov 2026 15:48:39 +0530 Subject: [PATCH] feat: add seller context, product management pages, and initiative validation tests --- package.json | 2 +- scripts/test_initiatives_validation.js | 98 ++++++ src/context/SellerContext.tsx | 38 ++- src/pages/dashboard/AddProductPage.tsx | 402 ++++++++++++++++++++++++- src/pages/dashboard/BulkUploadPage.tsx | 118 +++++++- src/types/seller.ts | 13 + 6 files changed, 654 insertions(+), 17 deletions(-) create mode 100644 scripts/test_initiatives_validation.js diff --git a/package.json b/package.json index 40ab3a5..e471af8 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/scripts/test_initiatives_validation.js b/scripts/test_initiatives_validation.js new file mode 100644 index 0000000..73dc911 --- /dev/null +++ b/scripts/test_initiatives_validation.js @@ -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 ---'); diff --git a/src/context/SellerContext.tsx b/src/context/SellerContext.tsx index 7729dfe..7ff7582 100644 --- a/src/context/SellerContext.tsx +++ b/src/context/SellerContext.tsx @@ -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) => { + 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, diff --git a/src/pages/dashboard/AddProductPage.tsx b/src/pages/dashboard/AddProductPage.tsx index 30e62d4..83b90ce 100644 --- a/src/pages/dashboard/AddProductPage.tsx +++ b/src/pages/dashboard/AddProductPage.tsx @@ -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(editProduct?.is_odop || editProduct?.isOdop)); + const [odopDistrict, setOdopDistrict] = useState(editProduct?.odop_district || editProduct?.odopDistrict || ''); + const [giTagged, setGiTagged] = useState(Boolean(editProduct?.is_gi_tagged || editProduct?.giTagged)); + const [giTagName, setGiTagName] = useState(editProduct?.gi_tag_name || editProduct?.giTagName || editProduct?.giCertRef || ''); + const [isSustainable, setIsSustainable] = useState(Boolean(editProduct?.is_sustainable || editProduct?.isSustainable)); + const [sustainableType, setSustainableType] = useState(editProduct?.sustainable_type || editProduct?.sustainableType || ''); const handleSupportingPhotoUpload = async (index: number, e: React.ChangeEvent) => { 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 = () => { )} + + {/* Heritage, GI & Sustainability Slabs */} +
+
+

+ Craft Heritage & Sustainability Slabs + Optional +

+

+ Highlight recognized regional origin, GI registry status, and sustainability credentials to conscious buyers. +

+
+ +
+ + {/* 1. ODOP Product Slab */} +
+
+
+
+ +
+
+
+ + ODOP Product (One District One Product) + + + Govt. Initiative + +
+

+ Recognized under the Government of India's ODOP initiative fostering regional craftsmanship & indigenous clusters. +

+
+
+ + {/* Toggle Switch */} + +
+ + {isOdop && ( +
+ + { 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' + }} + /> +
+ This district badge will be shown to buyers and automatically connects to ODOP regional collection filters. +
+
+ )} +
+ + {/* 2. GI Tag Product Slab */} +
+
+
+
+ +
+
+
+ + GI Tag Product (Geographical Indication) + + + Certified Heritage + +
+

+ Holds a registered Geographical Indication certifying authentic craft origins rooted in a protected geographical territory. +

+
+
+ + {/* Toggle Switch */} + +
+ + {giTagged && ( +
+ + { 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' + }} + /> +
+ Accredited GI certificate credentials are surfaced on the product showcase page with authentic verification badge. +
+
+ )} +
+ + {/* 3. Sustainable Product Slab */} +
+
+
+
+ +
+
+
+ + Sustainable Product Slab + + + Eco-Conscious + +
+

+ Handmade using non-toxic, zero-plastic, natural, or circular upcycled materials with low environmental footprint. +

+
+
+ + {/* Toggle Switch */} + +
+ + {isSustainable && ( +
+ + { 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' + }} + /> + + +
+ Enables the Sustainable Craft badge on your product card and makes it discoverable in eco-friendly storefront collections. +
+
+ )} +
+ +
+
)} diff --git a/src/pages/dashboard/BulkUploadPage.tsx b/src/pages/dashboard/BulkUploadPage.tsx index 8bb261a..f5f283a 100644 --- a/src/pages/dashboard/BulkUploadPage.tsx +++ b/src/pages/dashboard/BulkUploadPage.tsx @@ -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(null); const [editPrice, setEditPrice] = useState(''); const [editStock, setEditStock] = useState(''); + const [editIsOdop, setEditIsOdop] = useState(false); + const [editOdopDistrict, setEditOdopDistrict] = useState(''); + const [editIsGi, setEditIsGi] = useState(false); + const [editGiTagName, setEditGiTagName] = useState(''); + const [editIsSustainable, setEditIsSustainable] = useState(false); + const [editSustainableType, setEditSustainableType] = useState(''); const [isSavingEdit, setIsSavingEdit] = useState(false); const [stockValues, setStockValues] = useState>({}); @@ -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 = () => {
Craft_Standard_Catalog.csv
-
All 11 heritage & pricing columns
+
All ODOP, GI Tag, Sustainable, heritage & pricing columns