diff --git a/frontend/src/routes/(app)/sellers/[id]/+page.svelte b/frontend/src/routes/(app)/sellers/[id]/+page.svelte index 434a1cd7..09253b74 100644 --- a/frontend/src/routes/(app)/sellers/[id]/+page.svelte +++ b/frontend/src/routes/(app)/sellers/[id]/+page.svelte @@ -617,15 +617,28 @@
{#if p.image} - + + + {:else} -
- -
+ +
+ +
+
{/if}
- {p.title} + + {p.title} + + {#if p.is_gi_tagged} GI TAG {/if} @@ -690,15 +703,17 @@ {/if} - - + Review & Edit +
diff --git a/frontend/src/routes/(app)/sellers/[id]/products/[productId]/+page.server.js b/frontend/src/routes/(app)/sellers/[id]/products/[productId]/+page.server.js new file mode 100644 index 00000000..b4d732a3 --- /dev/null +++ b/frontend/src/routes/(app)/sellers/[id]/products/[productId]/+page.server.js @@ -0,0 +1,103 @@ +import { apiRequest } from '#lib/api-helpers.js'; +import { error, fail } from '@sveltejs/kit'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ cookies, params }) { + const sellerId = params.id; + const productId = params.productId; + + try { + const [seller, product] = await Promise.all([ + apiRequest(`/sellers/${sellerId}/`, {}, { cookies }), + apiRequest(`/sellers/${sellerId}/products/${productId}/`, {}, { cookies }) + ]); + + return { + seller, + product + }; + } catch (err) { + console.error('Failed to load product review detail:', err); + error(404, 'Product not found or inaccessible'); + } +} + +/** @type {import('./$types').Actions} */ +export const actions = { + approve: async ({ cookies, params }) => { + try { + await apiRequest( + `/sellers/${params.id}/products/${params.productId}/approve/`, + { method: 'POST' }, + { cookies } + ); + return { success: true, message: 'Product approved and published to customer storefront' }; + } catch (err) { + return fail(400, { error: err.message || 'Failed to approve product' }); + } + }, + + reject: async ({ request, cookies, params }) => { + const data = await request.formData(); + const reason = data.get('reason') || 'Product does not comply with catalogue quality standards.'; + try { + await apiRequest( + `/sellers/${params.id}/products/${params.productId}/reject/`, + { method: 'POST', body: { reason: String(reason) } }, + { cookies } + ); + return { success: true, message: 'Product rejected' }; + } catch (err) { + return fail(400, { error: err.message || 'Failed to reject product' }); + } + }, + + toggle: async ({ request, cookies, params }) => { + const data = await request.formData(); + const is_active = data.get('is_active') === 'true'; + try { + await apiRequest( + `/sellers/${params.id}/products/${params.productId}/toggle/`, + { method: 'POST', body: { is_active } }, + { cookies } + ); + return { success: true, message: 'Product visibility updated' }; + } catch (err) { + return fail(400, { error: err.message || 'Failed to toggle product status' }); + } + }, + + save: async ({ request, cookies, params }) => { + const data = await request.formData(); + /** @type {Record} */ + const payload = {}; + for (const [key, value] of data.entries()) { + if (key !== 'action') { + if (key === 'is_gi_tagged' || key === 'is_active' || key === 'is_odop' || key === 'is_sustainable') { + payload[key] = value === 'true'; + } else if ( + key === 'price' || + key === 'compare_at_price' || + key === 'stock' || + key === 'package_weight' || + key === 'processing_days' + ) { + if (value !== '') payload[key] = Number(value); + } else { + payload[key] = value; + } + } + } + try { + await apiRequest( + `/sellers/${params.id}/products/${params.productId}/`, + { method: 'PATCH', body: payload }, + { cookies } + ); + return { success: true, message: 'Product content updated successfully in website catalogue' }; + } catch (err) { + return fail(400, { error: err.message || 'Failed to update product content' }); + } + } +}; + diff --git a/frontend/src/routes/(app)/sellers/[id]/products/[productId]/+page.svelte b/frontend/src/routes/(app)/sellers/[id]/products/[productId]/+page.svelte new file mode 100644 index 00000000..b8f5b71e --- /dev/null +++ b/frontend/src/routes/(app)/sellers/[id]/products/[productId]/+page.svelte @@ -0,0 +1,768 @@ + + + + {#snippet crumb()} + Sellers + + {seller.store_name || seller.name || `Seller #${seller.id}`} + + Product #{product.id} + {/snippet} + + {#snippet sub()} + {[ + seller.store_name ? `Store: ${seller.store_name}` : null, + product.category ? `Category: ${product.category}` : null, + product.sku ? `SKU: ${product.sku}` : null, + product.upload_source === 'bulk' ? 'Bulk CSV Import' : 'Individual Upload' + ] + .filter(Boolean) + .join(' · ')} + {/snippet} + + {#snippet actions()} +
+ + {product.status} + + + + {product.is_active ? 'Storefront Published' : 'Hidden from Store'} + + + {#if product.status !== 'approved'} + + + + {/if} + + {#if product.status !== 'rejected'} + + {/if} + +
+ + +
+ + + Back to Seller + +
+ {/snippet} +
+ +{#if form?.success} +
+ + {form.message || 'Action executed successfully.'} +
+{:else if form?.error} +
+ + {form.error} +
+{/if} + +{#if product.rejection_reason} +
+ +
+
Product Rejection Notice
+
{product.rejection_reason}
+
+
+{/if} + +
+ +
+ +
+
+

+ Product Media & Visuals +

+ {allImages().length} Photos +
+ + +
+ {#if selectedImage || product.image} + {product.title} + {:else} +
+ + No image uploaded +
+ {/if} +
+ + + {#if allImages().length > 1} +
+ {#each allImages() as img, idx} + + {/each} +
+ {/if} + + + {#if product.video_url || product.view_360_url} +
+ {#if product.video_url} + + + {/if} + {#if product.view_360_url} + + View 360° Interactive + + + {/if} +
+ {/if} +
+ + +
+

+ Heritage & Statutory Badges +

+ +
+ +
+
+ +
+
Geographical Indication (GI Tag)
+
+ {product.is_gi_tagged + ? product.gi_tag_name || 'Registered Geographical Indication Certified' + : 'Not marked as GI Tag'} +
+
+
+ + {product.is_gi_tagged ? 'Certified GI' : 'Standard'} + +
+ + +
+
+ +
+
One District One Product (ODOP)
+
+ {product.is_odop + ? product.odop_district || 'ODOP Identified Heritage Craft' + : 'Not marked as ODOP'} +
+
+
+ + {product.is_odop ? 'ODOP Craft' : 'Standard'} + +
+ + +
+
+ +
+
Sustainable & Eco-Craft
+
+ {product.is_sustainable + ? product.sustainable_type || 'Eco-compliant materials & traditional dyes' + : 'Not marked as Sustainable'} +
+
+
+ + {product.is_sustainable ? 'Eco-Sustainable' : 'Standard'} + +
+ + + {#if product.geographical_origin || product.coordinates} +
+
+ Geographical Origin +
+
{product.geographical_origin || 'Origin not specified'}
+ {#if product.coordinates} +
GPS: {product.coordinates}
+ {/if} +
+ {/if} + + + {#if product.statutory_certifications} +
+
+ Statutory Certifications +
+
{product.statutory_certifications}
+
+ {/if} +
+
+
+ + +
+
+
+

+ Product Specifications & Storefront Content +

+ Edit details and click Save +
+ +
+
+ +
+ + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ + +
+ + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ + +
+
Heritage Attributes & Moderation Toggles
+ +
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ +
+
+
+ + +
+ +
+
+
+
+
+
+ + +{#if showRejectModal} +
+
+

Reject Product Listing

+

+ State the reason why {product.title} is being rejected. This notification will be displayed to the seller. +

+
(showRejectModal = false)}> + +
+ + +
+
+
+
+{/if} + + +