feat: add seller detail page and product management view
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 1m58s
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 1m58s
This commit is contained in:
parent
a4e46dc22c
commit
7b4f51e23d
3 changed files with 898 additions and 12 deletions
|
|
@ -617,15 +617,28 @@
|
|||
<td>
|
||||
<div style="display:flex;align-items:center;gap:12px;">
|
||||
{#if p.image}
|
||||
<img src={p.image} alt="" style="width:44px;height:44px;border-radius:4px;object-fit:cover;border:1px solid var(--v2-line);" />
|
||||
<a href={`/sellers/${seller.id}/products/${p.id}`} target="_blank" rel="noopener noreferrer" title="Open product in new tab">
|
||||
<img src={p.image} alt="" style="width:44px;height:44px;border-radius:4px;object-fit:cover;border:1px solid var(--v2-line);cursor:pointer;" />
|
||||
</a>
|
||||
{:else}
|
||||
<div style="width:44px;height:44px;border-radius:4px;background:var(--v2-line);display:flex;align-items:center;justify-content:center;color:var(--v2-muted);">
|
||||
<Package size={20} />
|
||||
</div>
|
||||
<a href={`/sellers/${seller.id}/products/${p.id}`} target="_blank" rel="noopener noreferrer" title="Open product in new tab" style="text-decoration:none;">
|
||||
<div style="width:44px;height:44px;border-radius:4px;background:var(--v2-line);display:flex;align-items:center;justify-content:center;color:var(--v2-muted);cursor:pointer;">
|
||||
<Package size={20} />
|
||||
</div>
|
||||
</a>
|
||||
{/if}
|
||||
<div>
|
||||
<div style="font-weight:600;font-size:13px;display:flex;align-items:center;gap:6px;flex-wrap:wrap;">
|
||||
{p.title}
|
||||
<a
|
||||
href={`/sellers/${seller.id}/products/${p.id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style="color:inherit;text-decoration:none;display:inline-flex;align-items:center;gap:4px;"
|
||||
title="Open product review in new tab"
|
||||
>
|
||||
<span>{p.title}</span>
|
||||
<ExternalLink size={12} style="opacity:0.6;flex-shrink:0;" />
|
||||
</a>
|
||||
{#if p.is_gi_tagged}
|
||||
<span style="font-size:10px;background:#FDF2F4;color:#671B26;padding:1px 5px;border-radius:4px;font-weight:700;">GI TAG</span>
|
||||
{/if}
|
||||
|
|
@ -690,15 +703,17 @@
|
|||
</button>
|
||||
{/if}
|
||||
|
||||
<!-- Edit Product Content -->
|
||||
<button
|
||||
type="button"
|
||||
<!-- Review & Edit in New Tab -->
|
||||
<a
|
||||
href={`/sellers/${seller.id}/products/${p.id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="v2-btn"
|
||||
onclick={() => openEditProduct(p)}
|
||||
style="font-size:11px;padding:3px 8px;"
|
||||
style="font-size:11px;padding:3px 8px;display:inline-flex;align-items:center;gap:4px;text-decoration:none;color:inherit;"
|
||||
title="Open all product details in new tab to review, edit, approve, or reject"
|
||||
>
|
||||
Edit Content
|
||||
</button>
|
||||
<ExternalLink size={11} /> Review & Edit
|
||||
</a>
|
||||
|
||||
<!-- Visibility Toggle -->
|
||||
<form method="POST" action="?/toggleProduct" use:enhance style="display:inline-block;">
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>} */
|
||||
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' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -0,0 +1,768 @@
|
|||
<script>
|
||||
import { resolve } from '$app/paths';
|
||||
import { enhance } from '$app/forms';
|
||||
import PageHeader from '#lib/v2/components/PageHeader.svelte';
|
||||
import Pill from '#lib/v2/components/Pill.svelte';
|
||||
import { money, longDate } from '#lib/v2/format.js';
|
||||
import {
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
Package,
|
||||
ExternalLink,
|
||||
ChevronRight,
|
||||
Eye,
|
||||
ShieldCheck,
|
||||
Award,
|
||||
Leaf,
|
||||
MapPin,
|
||||
Truck,
|
||||
ArrowLeft,
|
||||
Clock,
|
||||
Sparkles,
|
||||
Layers,
|
||||
Video,
|
||||
Tag
|
||||
} from '@lucide/svelte';
|
||||
|
||||
/** @type {{ data: any, form: any }} */
|
||||
let { data, form } = $props();
|
||||
|
||||
let seller = $derived(data.seller || {});
|
||||
let product = $derived(data.product || {});
|
||||
|
||||
// Selected active image preview (defaults to product.image)
|
||||
let selectedImage = $state('');
|
||||
$effect(() => {
|
||||
if (product.image && !selectedImage) {
|
||||
selectedImage = product.image;
|
||||
}
|
||||
});
|
||||
|
||||
// Rejection modal state
|
||||
let showRejectModal = $state(false);
|
||||
let rejectReason = $state('');
|
||||
|
||||
/** @param {string} status */
|
||||
function statusTone(status) {
|
||||
switch (status) {
|
||||
case 'approved':
|
||||
return 'moss';
|
||||
case 'pending':
|
||||
case 'pending_approval':
|
||||
return 'clay';
|
||||
case 'rejected':
|
||||
case 'suspended':
|
||||
case 'locked':
|
||||
return 'rust';
|
||||
default:
|
||||
return 'slate';
|
||||
}
|
||||
}
|
||||
|
||||
// Parse additional images array safely
|
||||
let allImages = $derived(() => {
|
||||
const list = [];
|
||||
if (product.image) list.push(product.image);
|
||||
if (Array.isArray(product.additional_images)) {
|
||||
for (const img of product.additional_images) {
|
||||
if (img && !list.includes(img)) list.push(img);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
});
|
||||
</script>
|
||||
|
||||
<PageHeader title={product.title || 'Product Review'} record>
|
||||
{#snippet crumb()}
|
||||
<a href="/sellers">Sellers</a>
|
||||
<ChevronRight size={12} />
|
||||
<a href={`/sellers/${seller.id}`}>{seller.store_name || seller.name || `Seller #${seller.id}`}</a>
|
||||
<ChevronRight size={12} />
|
||||
<span>Product #{product.id}</span>
|
||||
{/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()}
|
||||
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
|
||||
<Pill tone={statusTone(product.status)} dot>
|
||||
{product.status}
|
||||
</Pill>
|
||||
|
||||
<Pill tone={product.is_active ? 'moss' : 'rust'}>
|
||||
{product.is_active ? 'Storefront Published' : 'Hidden from Store'}
|
||||
</Pill>
|
||||
|
||||
{#if product.status !== 'approved'}
|
||||
<form method="POST" action="?/approve" use:enhance style="display:inline-block;">
|
||||
<button
|
||||
type="submit"
|
||||
class="v2-btn v2-btn-primary"
|
||||
style="background:var(--v2-moss);border-color:var(--v2-moss);color:white;display:inline-flex;align-items:center;gap:6px;"
|
||||
>
|
||||
<CheckCircle2 size={14} /> Approve Product
|
||||
</button>
|
||||
</form>
|
||||
{/if}
|
||||
|
||||
{#if product.status !== 'rejected'}
|
||||
<button
|
||||
type="button"
|
||||
class="v2-btn"
|
||||
onclick={() => {
|
||||
rejectReason = product.rejection_reason || '';
|
||||
showRejectModal = true;
|
||||
}}
|
||||
style="color:var(--v2-rust);display:inline-flex;align-items:center;gap:6px;"
|
||||
>
|
||||
<XCircle size={14} /> Reject
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<form method="POST" action="?/toggle" use:enhance style="display:inline-block;">
|
||||
<input type="hidden" name="is_active" value={product.is_active ? 'false' : 'true'} />
|
||||
<button type="submit" class="v2-btn" style="display:inline-flex;align-items:center;gap:6px;">
|
||||
<Eye size={14} /> {product.is_active ? 'Hide from Storefront' : 'Publish to Storefront'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<a
|
||||
href={`/sellers/${seller.id}`}
|
||||
class="v2-btn"
|
||||
style="display:inline-flex;align-items:center;gap:6px;text-decoration:none;"
|
||||
>
|
||||
<ArrowLeft size={14} /> Back to Seller
|
||||
</a>
|
||||
</div>
|
||||
{/snippet}
|
||||
</PageHeader>
|
||||
|
||||
{#if form?.success}
|
||||
<div style="padding:12px 16px;background:rgba(46,125,50,0.1);border:1px solid var(--v2-moss);color:var(--v2-moss);border-radius:6px;margin-bottom:16px;font-size:13px;display:flex;align-items:center;gap:8px;">
|
||||
<CheckCircle2 size={16} />
|
||||
<span>{form.message || 'Action executed successfully.'}</span>
|
||||
</div>
|
||||
{:else if form?.error}
|
||||
<div style="padding:12px 16px;background:rgba(211,47,47,0.1);border:1px solid var(--v2-rust);color:var(--v2-rust);border-radius:6px;margin-bottom:16px;font-size:13px;display:flex;align-items:center;gap:8px;">
|
||||
<XCircle size={16} />
|
||||
<span>{form.error}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if product.rejection_reason}
|
||||
<div style="padding:14px 18px;background:#FFF4F2;border:1px solid #FBD0C8;border-radius:8px;margin-bottom:20px;display:flex;align-items:flex-start;gap:12px;">
|
||||
<XCircle size={18} style="color:var(--v2-rust);margin-top:2px;flex-shrink:0;" />
|
||||
<div>
|
||||
<div style="font-weight:700;color:var(--v2-rust);font-size:13px;margin-bottom:2px;">Product Rejection Notice</div>
|
||||
<div style="font-size:13px;color:#4A1515;">{product.rejection_reason}</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="product-review-grid">
|
||||
<!-- LEFT COLUMN: Visual Media & Verification Badges -->
|
||||
<div class="review-media-col">
|
||||
<!-- Visual Showcase Card -->
|
||||
<div class="v2-panel-card">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;">
|
||||
<h3 style="font-size:14px;font-weight:700;margin:0;display:flex;align-items:center;gap:6px;">
|
||||
<Package size={16} /> Product Media & Visuals
|
||||
</h3>
|
||||
<span class="v2-sub" style="font-size:11px;">{allImages().length} Photos</span>
|
||||
</div>
|
||||
|
||||
<!-- Main High-Res Preview -->
|
||||
<div class="main-image-wrap">
|
||||
{#if selectedImage || product.image}
|
||||
<img src={selectedImage || product.image} alt={product.title} class="main-preview-img" />
|
||||
{:else}
|
||||
<div class="main-preview-placeholder">
|
||||
<Package size={48} style="opacity:0.4;" />
|
||||
<span style="font-size:12px;margin-top:8px;">No image uploaded</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Thumbnails Carousel/Row -->
|
||||
{#if allImages().length > 1}
|
||||
<div class="thumbnails-row">
|
||||
{#each allImages() as img, idx}
|
||||
<button
|
||||
type="button"
|
||||
class="thumb-btn"
|
||||
class:active={selectedImage === img || (!selectedImage && idx === 0)}
|
||||
onclick={() => (selectedImage = img)}
|
||||
>
|
||||
<img src={img} alt="" />
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Video / 360 View links if present -->
|
||||
{#if product.video_url || product.view_360_url}
|
||||
<div style="margin-top:14px;padding-top:12px;border-top:1px solid var(--v2-line);display:flex;gap:8px;flex-wrap:wrap;">
|
||||
{#if product.video_url}
|
||||
<a
|
||||
href={product.video_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="v2-btn"
|
||||
style="font-size:11px;display:inline-flex;align-items:center;gap:6px;"
|
||||
>
|
||||
<Video size={13} style="color:var(--v2-moss);" /> Watch 10s Product Video
|
||||
<ExternalLink size={10} />
|
||||
</a>
|
||||
{/if}
|
||||
{#if product.view_360_url}
|
||||
<a
|
||||
href={product.view_360_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="v2-btn"
|
||||
style="font-size:11px;display:inline-flex;align-items:center;gap:6px;"
|
||||
>
|
||||
<Sparkles size={13} style="color:#B8860B;" /> View 360° Interactive
|
||||
<ExternalLink size={10} />
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Heritage, ODOP, GI Tag & Provenance Badges -->
|
||||
<div class="v2-panel-card" style="margin-top:16px;">
|
||||
<h3 style="font-size:14px;font-weight:700;margin:0 0 14px 0;display:flex;align-items:center;gap:6px;">
|
||||
<Award size={16} style="color:#671B26;" /> Heritage & Statutory Badges
|
||||
</h3>
|
||||
|
||||
<div style="display:flex;flex-direction:column;gap:12px;">
|
||||
<!-- GI Tag -->
|
||||
<div class="badge-item" class:badge-active={product.is_gi_tagged}>
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<ShieldCheck size={18} style="color:#671B26;" />
|
||||
<div>
|
||||
<div style="font-weight:600;font-size:12px;">Geographical Indication (GI Tag)</div>
|
||||
<div class="v2-sub" style="font-size:11px;">
|
||||
{product.is_gi_tagged
|
||||
? product.gi_tag_name || 'Registered Geographical Indication Certified'
|
||||
: 'Not marked as GI Tag'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Pill tone={product.is_gi_tagged ? 'rust' : 'slate'}>
|
||||
{product.is_gi_tagged ? 'Certified GI' : 'Standard'}
|
||||
</Pill>
|
||||
</div>
|
||||
|
||||
<!-- ODOP -->
|
||||
<div class="badge-item" class:badge-active={product.is_odop}>
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<Tag size={18} style="color:#166534;" />
|
||||
<div>
|
||||
<div style="font-weight:600;font-size:12px;">One District One Product (ODOP)</div>
|
||||
<div class="v2-sub" style="font-size:11px;">
|
||||
{product.is_odop
|
||||
? product.odop_district || 'ODOP Identified Heritage Craft'
|
||||
: 'Not marked as ODOP'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Pill tone={product.is_odop ? 'moss' : 'slate'}>
|
||||
{product.is_odop ? 'ODOP Craft' : 'Standard'}
|
||||
</Pill>
|
||||
</div>
|
||||
|
||||
<!-- Sustainability -->
|
||||
<div class="badge-item" class:badge-active={product.is_sustainable}>
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<Leaf size={18} style="color:#1E40AF;" />
|
||||
<div>
|
||||
<div style="font-weight:600;font-size:12px;">Sustainable & Eco-Craft</div>
|
||||
<div class="v2-sub" style="font-size:11px;">
|
||||
{product.is_sustainable
|
||||
? product.sustainable_type || 'Eco-compliant materials & traditional dyes'
|
||||
: 'Not marked as Sustainable'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Pill tone={product.is_sustainable ? 'clay' : 'slate'}>
|
||||
{product.is_sustainable ? 'Eco-Sustainable' : 'Standard'}
|
||||
</Pill>
|
||||
</div>
|
||||
|
||||
<!-- Coordinates & Provenance -->
|
||||
{#if product.geographical_origin || product.coordinates}
|
||||
<div style="padding:10px;background:var(--v2-panel);border:1px solid var(--v2-line);border-radius:6px;font-size:12px;">
|
||||
<div style="display:flex;align-items:center;gap:6px;font-weight:600;margin-bottom:4px;">
|
||||
<MapPin size={14} style="color:var(--v2-rust);" /> Geographical Origin
|
||||
</div>
|
||||
<div style="color:var(--v2-text);">{product.geographical_origin || 'Origin not specified'}</div>
|
||||
{#if product.coordinates}
|
||||
<div class="v2-sub" style="font-size:11px;margin-top:2px;">GPS: {product.coordinates}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Statutory Certifications -->
|
||||
{#if product.statutory_certifications}
|
||||
<div style="padding:10px;background:var(--v2-panel);border:1px solid var(--v2-line);border-radius:6px;font-size:12px;">
|
||||
<div style="display:flex;align-items:center;gap:6px;font-weight:600;margin-bottom:4px;">
|
||||
<ShieldCheck size={14} style="color:var(--v2-moss);" /> Statutory Certifications
|
||||
</div>
|
||||
<div style="color:var(--v2-text);">{product.statutory_certifications}</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT COLUMN: Detailed Specifications & Content Edit Form -->
|
||||
<div class="review-content-col">
|
||||
<div class="v2-panel-card">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;border-bottom:1px solid var(--v2-line);padding-bottom:12px;">
|
||||
<h3 style="font-size:15px;font-weight:700;margin:0;">
|
||||
Product Specifications & Storefront Content
|
||||
</h3>
|
||||
<span class="v2-sub" style="font-size:11px;">Edit details and click Save</span>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="?/save" use:enhance>
|
||||
<div style="display:flex;flex-direction:column;gap:16px;">
|
||||
<!-- Product Title -->
|
||||
<div>
|
||||
<label class="v2-sub" style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;" for="title">
|
||||
Product Title
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="title"
|
||||
name="title"
|
||||
value={product.title || ''}
|
||||
class="v2-input"
|
||||
style="width:100%;font-size:13px;padding:8px;"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Category, SKU & Upload Source -->
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;">
|
||||
<div>
|
||||
<label class="v2-sub" style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;" for="category">
|
||||
Category
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="category"
|
||||
name="category"
|
||||
value={product.category || ''}
|
||||
class="v2-input"
|
||||
style="width:100%;font-size:13px;padding:8px;"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="v2-sub" style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;" for="sku">
|
||||
SKU Identifier
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="sku"
|
||||
name="sku"
|
||||
value={product.sku || ''}
|
||||
class="v2-input"
|
||||
style="width:100%;font-size:13px;padding:8px;"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="v2-sub" style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;" for="upload_source">
|
||||
Upload Source
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="upload_source"
|
||||
name="upload_source"
|
||||
value={product.upload_source || 'individual'}
|
||||
class="v2-input"
|
||||
style="width:100%;font-size:13px;padding:8px;background:var(--v2-line);color:var(--v2-muted);"
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pricing & Inventory -->
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;">
|
||||
<div>
|
||||
<label class="v2-sub" style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;" for="price">
|
||||
Selling Price (₹)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="price"
|
||||
name="price"
|
||||
step="0.01"
|
||||
value={product.price || 0}
|
||||
class="v2-input"
|
||||
style="width:100%;font-size:13px;padding:8px;font-weight:600;"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="v2-sub" style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;" for="compare_at_price">
|
||||
MRP / Compare Price (₹)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="compare_at_price"
|
||||
name="compare_at_price"
|
||||
step="0.01"
|
||||
value={product.compare_at_price || ''}
|
||||
class="v2-input"
|
||||
style="width:100%;font-size:13px;padding:8px;"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="v2-sub" style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;" for="stock">
|
||||
Available Stock (Units)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="stock"
|
||||
name="stock"
|
||||
value={product.stock || 0}
|
||||
class="v2-input"
|
||||
style="width:100%;font-size:13px;padding:8px;font-weight:600;"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shipping & Logistics -->
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;">
|
||||
<div>
|
||||
<label class="v2-sub" style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;" for="shipping_profile">
|
||||
Shipping Profile
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="shipping_profile"
|
||||
name="shipping_profile"
|
||||
value={product.shipping_profile || 'Standard Fragile'}
|
||||
class="v2-input"
|
||||
style="width:100%;font-size:13px;padding:8px;"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="v2-sub" style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;" for="package_weight">
|
||||
Package Weight (kg)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="package_weight"
|
||||
name="package_weight"
|
||||
step="0.01"
|
||||
value={product.package_weight || 1.0}
|
||||
class="v2-input"
|
||||
style="width:100%;font-size:13px;padding:8px;"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="v2-sub" style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;" for="processing_days">
|
||||
Dispatch Time (Days)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="processing_days"
|
||||
name="processing_days"
|
||||
value={product.processing_days || 3}
|
||||
class="v2-input"
|
||||
style="width:100%;font-size:13px;padding:8px;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Craft Story & Full Description -->
|
||||
<div>
|
||||
<label class="v2-sub" style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;" for="description">
|
||||
Craft Story & Detailed Description
|
||||
</label>
|
||||
<textarea
|
||||
id="description"
|
||||
name="description"
|
||||
rows="6"
|
||||
class="v2-input"
|
||||
style="width:100%;font-size:13px;padding:10px;line-height:1.5;"
|
||||
>{product.description || ''}</textarea>
|
||||
</div>
|
||||
|
||||
<!-- Geographical Provenance Narrative -->
|
||||
<div>
|
||||
<label class="v2-sub" style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;" for="provenance_story">
|
||||
Geographical Provenance Narrative
|
||||
</label>
|
||||
<textarea
|
||||
id="provenance_story"
|
||||
name="provenance_story"
|
||||
rows="3"
|
||||
class="v2-input"
|
||||
style="width:100%;font-size:13px;padding:10px;line-height:1.5;"
|
||||
placeholder="e.g. Origin story of the weaving cluster, indigenous materials and cultural significance..."
|
||||
>{product.provenance_story || ''}</textarea>
|
||||
</div>
|
||||
|
||||
<!-- Origin & Coordinates -->
|
||||
<div style="display:grid;grid-template-columns:2fr 1fr;gap:12px;">
|
||||
<div>
|
||||
<label class="v2-sub" style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;" for="geographical_origin">
|
||||
Geographical Origin
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="geographical_origin"
|
||||
name="geographical_origin"
|
||||
value={product.geographical_origin || ''}
|
||||
class="v2-input"
|
||||
style="width:100%;font-size:13px;padding:8px;"
|
||||
placeholder="e.g. Kanchipuram, Tamil Nadu, India"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="v2-sub" style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;" for="coordinates">
|
||||
Cluster GPS Coordinates
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="coordinates"
|
||||
name="coordinates"
|
||||
value={product.coordinates || ''}
|
||||
class="v2-input"
|
||||
style="width:100%;font-size:13px;padding:8px;"
|
||||
placeholder="e.g. 12.8342° N, 79.7036° E"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statutory Certifications -->
|
||||
<div>
|
||||
<label class="v2-sub" style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;" for="statutory_certifications">
|
||||
Statutory Certifications & Marks
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="statutory_certifications"
|
||||
name="statutory_certifications"
|
||||
value={product.statutory_certifications || ''}
|
||||
class="v2-input"
|
||||
style="width:100%;font-size:13px;padding:8px;"
|
||||
placeholder="e.g. GI Tag #26 Act (1999) Registered, Silk Mark Certified, Handloom Mark"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Heritage Checkboxes & Detail Fields -->
|
||||
<div style="padding:14px;background:var(--v2-panel);border:1px solid var(--v2-line);border-radius:8px;">
|
||||
<div style="font-weight:600;font-size:13px;margin-bottom:10px;">Heritage Attributes & Moderation Toggles</div>
|
||||
|
||||
<div style="display:flex;flex-direction:column;gap:10px;">
|
||||
<!-- GI Tag -->
|
||||
<div style="display:grid;grid-template-columns:auto 1fr;gap:10px;align-items:center;">
|
||||
<label style="display:flex;align-items:center;gap:6px;font-size:13px;cursor:pointer;">
|
||||
<input type="checkbox" name="is_gi_tagged" value="true" checked={product.is_gi_tagged} />
|
||||
<span style="font-weight:600;">Certified GI Tag</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="gi_tag_name"
|
||||
value={product.gi_tag_name || ''}
|
||||
class="v2-input"
|
||||
style="font-size:12px;padding:6px;"
|
||||
placeholder="GI Tag Name / Register No. (e.g. Kancheepuram Silk Application #26)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- ODOP -->
|
||||
<div style="display:grid;grid-template-columns:auto 1fr;gap:10px;align-items:center;">
|
||||
<label style="display:flex;align-items:center;gap:6px;font-size:13px;cursor:pointer;">
|
||||
<input type="checkbox" name="is_odop" value="true" checked={product.is_odop} />
|
||||
<span style="font-weight:600;">ODOP Product</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="odop_district"
|
||||
value={product.odop_district || ''}
|
||||
class="v2-input"
|
||||
style="font-size:12px;padding:6px;"
|
||||
placeholder="ODOP District (e.g. Kanchipuram District, Tamil Nadu)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Sustainable -->
|
||||
<div style="display:grid;grid-template-columns:auto 1fr;gap:10px;align-items:center;">
|
||||
<label style="display:flex;align-items:center;gap:6px;font-size:13px;cursor:pointer;">
|
||||
<input type="checkbox" name="is_sustainable" value="true" checked={product.is_sustainable} />
|
||||
<span style="font-weight:600;">Sustainable Craft</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="sustainable_type"
|
||||
value={product.sustainable_type || ''}
|
||||
class="v2-input"
|
||||
style="font-size:12px;padding:6px;"
|
||||
placeholder="Sustainable Material Type (e.g. Organic Mulberry Silk, Natural Plant Dyes)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Storefront visibility -->
|
||||
<div style="margin-top:6px;padding-top:8px;border-top:1px solid var(--v2-line);">
|
||||
<label style="display:flex;align-items:center;gap:6px;font-size:13px;cursor:pointer;">
|
||||
<input type="checkbox" name="is_active" value="true" checked={product.is_active} />
|
||||
<span style="font-weight:600;">Visible and purchasable in Website Storefront</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div style="display:flex;justify-content:flex-end;gap:10px;margin-top:8px;">
|
||||
<button type="submit" class="v2-btn v2-btn-primary" style="font-size:13px;padding:8px 16px;">
|
||||
Save Content Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reject Reason Modal -->
|
||||
{#if showRejectModal}
|
||||
<div style="position:fixed;inset:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:9999;">
|
||||
<div style="background:var(--v2-panel);border:1px solid var(--v2-line);border-radius:10px;padding:24px;width:100%;max-width:440px;">
|
||||
<h3 style="font-size:16px;font-weight:700;margin-bottom:8px;">Reject Product Listing</h3>
|
||||
<p class="v2-sub" style="font-size:13px;margin-bottom:16px;">
|
||||
State the reason why <strong>{product.title}</strong> is being rejected. This notification will be displayed to the seller.
|
||||
</p>
|
||||
<form method="POST" action="?/reject" use:enhance onsubmit={() => (showRejectModal = false)}>
|
||||
<textarea
|
||||
name="reason"
|
||||
bind:value={rejectReason}
|
||||
placeholder="e.g. Inaccurate GI tag claim, unclear product imagery, or non-compliant packaging weight..."
|
||||
required
|
||||
rows="4"
|
||||
class="v2-input"
|
||||
style="width:100%;font-size:13px;margin-bottom:16px;padding:8px;"
|
||||
></textarea>
|
||||
<div style="display:flex;justify-content:flex-end;gap:8px;">
|
||||
<button type="button" class="v2-btn" onclick={() => (showRejectModal = false)}>Cancel</button>
|
||||
<button type="submit" class="v2-btn" style="background:var(--v2-rust);color:white;">Confirm Product Rejection</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.product-review-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 380px 1fr;
|
||||
gap: 20px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.product-review-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.v2-panel-card {
|
||||
background: var(--v2-panel);
|
||||
border: 1px solid var(--v2-line);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.main-image-wrap {
|
||||
width: 100%;
|
||||
height: 340px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: var(--v2-bg);
|
||||
border: 1px solid var(--v2-line);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.main-preview-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.main-preview-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--v2-muted);
|
||||
}
|
||||
|
||||
.thumbnails-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.thumb-btn {
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
border-radius: 6px;
|
||||
border: 2px solid var(--v2-line);
|
||||
background: var(--v2-bg);
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
transition: border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.thumb-btn.active {
|
||||
border-color: var(--v2-moss, #2e7d32);
|
||||
box-shadow: 0 0 0 2px rgba(46, 125, 50, 0.2);
|
||||
}
|
||||
|
||||
.thumb-btn img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.badge-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--v2-line);
|
||||
border-radius: 6px;
|
||||
background: var(--v2-panel);
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.badge-item.badge-active {
|
||||
border-color: rgba(46, 125, 50, 0.4);
|
||||
background: rgba(46, 125, 50, 0.03);
|
||||
}
|
||||
</style>
|
||||
|
||||
Loading…
Reference in a new issue