feat: add sellers app, database router, serializers, views, and frontend pages
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 2m8s

This commit is contained in:
vickytechkey 2026-11-08 17:09:07 +05:30
parent 593c108c12
commit a4e46dc22c
7 changed files with 422 additions and 34 deletions

View file

@ -10,9 +10,9 @@ class CRMDatabaseRouter:
app = model._meta.app_label
name = model._meta.model_name
if app in ["sellers", "seller_models"]:
if name == "product":
if name in ["product", "bulkuploadlog"]:
return "product_db"
if name in ["wallet", "wallettransaction", "order", "returnrequest"]:
if name in ["wallet", "wallettransaction", "order", "sellerorder", "returnrequest"]:
return "customer_db"
return "seller_db"
if app in ["customers", "customer_models"]:
@ -23,9 +23,9 @@ class CRMDatabaseRouter:
app = model._meta.app_label
name = model._meta.model_name
if app in ["sellers", "seller_models"]:
if name == "product":
if name in ["product", "bulkuploadlog"]:
return "product_db"
if name in ["wallet", "wallettransaction", "order", "returnrequest"]:
if name in ["wallet", "wallettransaction", "order", "sellerorder", "returnrequest"]:
return "customer_db"
return "seller_db"
if app in ["customers", "customer_models"]:

View file

@ -106,7 +106,18 @@ class Product(models.Model):
compare_at_price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
description = models.TextField(blank=True, null=True)
is_gi_tagged = models.BooleanField(default=False)
gi_tag_name = models.CharField(max_length=200, blank=True, null=True)
is_odop = models.BooleanField(default=False)
odop_district = models.CharField(max_length=200, blank=True, null=True)
is_sustainable = models.BooleanField(default=False)
sustainable_type = models.CharField(max_length=200, blank=True, null=True)
geographical_origin = models.CharField(max_length=255, blank=True, null=True)
statutory_certifications = models.TextField(blank=True, null=True)
provenance_story = models.TextField(blank=True, null=True)
coordinates = models.CharField(max_length=100, blank=True, null=True)
track_inventory = models.BooleanField(default=True)
shipping_profile = models.CharField(max_length=100, default='Standard Fragile')
processing_days = models.IntegerField(default=3)
package_weight = models.FloatField(default=1.0)
upload_source = models.CharField(max_length=50, default='individual')
rejection_reason = models.TextField(blank=True, null=True)

View file

@ -53,7 +53,7 @@ class SupplierProfileListSerializer(serializers.ModelSerializer):
def get_products_count(self, obj):
try:
return Product.objects.filter(supplier_id=obj.id).count()
return Product.objects.filter(Q(supplier_id=obj.id) | Q(supplier_id=obj.user_id)).count()
except Exception:
return 0
@ -165,13 +165,15 @@ class SupplierProfileDetailSerializer(serializers.ModelSerializer):
def get_metrics(self, obj):
try:
supplier_q = Q(supplier_id=obj.id) | Q(supplier_id=obj.user_id)
# 1. Total Sales from delivered or all accepted orders
orders_qs = SellerOrder.objects.filter(supplier_id=obj.id)
orders_qs = SellerOrder.objects.filter(supplier_q)
sales_agg = orders_qs.exclude(status__in=['Cancelled', 'Rejected']).aggregate(total=Sum('total'))
total_sales = float(sales_agg['total'] or 0.0)
# 2. Total items in stock
products_qs = Product.objects.filter(supplier_id=obj.id)
products_qs = Product.objects.filter(supplier_q)
stock_agg = products_qs.aggregate(total_stock=Sum('stock'))
total_stock = int(stock_agg['total_stock'] or 0)
@ -183,7 +185,7 @@ class SupplierProfileDetailSerializer(serializers.ModelSerializer):
).count()
# 4. Total amount we need to pay to seller (outstanding wallet balance)
wallet = Wallet.objects.filter(supplier_id=obj.id).first()
wallet = Wallet.objects.filter(supplier_q).first()
payable_amt = float(wallet.outstanding) if wallet else 0.0
return {
@ -214,22 +216,21 @@ class SupplierProfileDetailSerializer(serializers.ModelSerializer):
def get_products(self, obj):
try:
items = Product.objects.filter(supplier_id=obj.id).order_by("-id")[:100]
items = Product.objects.filter(Q(supplier_id=obj.id) | Q(supplier_id=obj.user_id)).order_by("-id")[:100]
return ProductSerializer(items, many=True).data
except Exception:
return []
def get_bulk_upload_logs(self, obj):
try:
# Query by user_id or supplier_id
logs = BulkUploadLog.objects.filter(supplier_id=obj.user_id).order_by("-id")[:20]
logs = BulkUploadLog.objects.filter(Q(supplier_id=obj.id) | Q(supplier_id=obj.user_id)).order_by("-id")[:20]
return BulkUploadLogSerializer(logs, many=True).data
except Exception:
return []
def get_wallet(self, obj):
try:
wallet = Wallet.objects.filter(supplier_id=obj.id).first()
wallet = Wallet.objects.filter(Q(supplier_id=obj.id) | Q(supplier_id=obj.user_id)).first()
if wallet:
return WalletSerializer(wallet).data
except Exception:
@ -238,7 +239,7 @@ class SupplierProfileDetailSerializer(serializers.ModelSerializer):
def get_orders(self, obj):
try:
orders = SellerOrder.objects.filter(supplier_id=obj.id).order_by("-id")[:50]
orders = SellerOrder.objects.filter(Q(supplier_id=obj.id) | Q(supplier_id=obj.user_id)).order_by("-id")[:50]
return SellerOrderSerializer(orders, many=True).data
except Exception:
return []

View file

@ -295,9 +295,15 @@ class SellerProductApproveView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request, pk, product_id):
try:
product = Product.objects.get(pk=product_id, supplier_id=pk)
except Product.DoesNotExist:
seller = SupplierProfile.objects.filter(pk=pk).first()
if not seller:
return Response({"detail": "Seller not found."}, status=status.HTTP_404_NOT_FOUND)
product = Product.objects.filter(
models.Q(supplier_id=seller.id) | models.Q(supplier_id=seller.user_id),
pk=product_id
).first()
if not product:
return Response({"detail": "Product not found."}, status=status.HTTP_404_NOT_FOUND)
product.status = "approved"
@ -316,9 +322,15 @@ class SellerProductRejectView(APIView):
def post(self, request, pk, product_id):
reason = request.data.get("reason", "Product does not comply with catalogue quality or listing policies.")
try:
product = Product.objects.get(pk=product_id, supplier_id=pk)
except Product.DoesNotExist:
seller = SupplierProfile.objects.filter(pk=pk).first()
if not seller:
return Response({"detail": "Seller not found."}, status=status.HTTP_404_NOT_FOUND)
product = Product.objects.filter(
models.Q(supplier_id=seller.id) | models.Q(supplier_id=seller.user_id),
pk=product_id
).first()
if not product:
return Response({"detail": "Product not found."}, status=status.HTTP_404_NOT_FOUND)
product.status = "rejected"
@ -337,16 +349,28 @@ class SellerProductDetailView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request, pk, product_id):
try:
product = Product.objects.get(pk=product_id, supplier_id=pk)
except Product.DoesNotExist:
seller = SupplierProfile.objects.filter(pk=pk).first()
if not seller:
return Response({"detail": "Seller not found."}, status=status.HTTP_404_NOT_FOUND)
product = Product.objects.filter(
models.Q(supplier_id=seller.id) | models.Q(supplier_id=seller.user_id),
pk=product_id
).first()
if not product:
return Response({"detail": "Product not found."}, status=status.HTTP_404_NOT_FOUND)
return Response(ProductSerializer(product).data)
def patch(self, request, pk, product_id):
try:
product = Product.objects.get(pk=product_id, supplier_id=pk)
except Product.DoesNotExist:
seller = SupplierProfile.objects.filter(pk=pk).first()
if not seller:
return Response({"detail": "Seller not found."}, status=status.HTTP_404_NOT_FOUND)
product = Product.objects.filter(
models.Q(supplier_id=seller.id) | models.Q(supplier_id=seller.user_id),
pk=product_id
).first()
if not product:
return Response({"detail": "Product not found."}, status=status.HTTP_404_NOT_FOUND)
updatable_fields = [
@ -358,6 +382,15 @@ class SellerProductDetailView(APIView):
"sku",
"description",
"is_gi_tagged",
"gi_tag_name",
"is_odop",
"odop_district",
"is_sustainable",
"sustainable_type",
"geographical_origin",
"statutory_certifications",
"provenance_story",
"coordinates",
"is_active",
"status",
"rejection_reason",
@ -381,9 +414,15 @@ class SellerProductToggleView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request, pk, product_id):
try:
product = Product.objects.get(pk=product_id, supplier_id=pk)
except Product.DoesNotExist:
seller = SupplierProfile.objects.filter(pk=pk).first()
if not seller:
return Response({"detail": "Seller not found."}, status=status.HTTP_404_NOT_FOUND)
product = Product.objects.filter(
models.Q(supplier_id=seller.id) | models.Q(supplier_id=seller.user_id),
pk=product_id
).first()
if not product:
return Response({"detail": "Product not found."}, status=status.HTTP_404_NOT_FOUND)
if "is_active" in request.data:
@ -403,8 +442,13 @@ class SellerOrderTrackView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request, pk, order_id):
seller = SupplierProfile.objects.filter(pk=pk).first()
supplier_id_q = models.Q(supplier_id=pk)
if seller:
supplier_id_q = models.Q(supplier_id=seller.id) | models.Q(supplier_id=seller.user_id)
try:
order = SellerOrder.objects.filter(models.Q(order_id=order_id) | models.Q(id=order_id), supplier_id=pk).first()
order = SellerOrder.objects.filter(models.Q(order_id=order_id) | models.Q(id=order_id)).filter(supplier_id_q).first()
if not order:
# Also fallback to general order search
order = SellerOrder.objects.filter(models.Q(order_id=order_id) | models.Q(id=order_id)).first()

View file

@ -0,0 +1,307 @@
import os
import sys
from unittest.mock import MagicMock
# Mock celery before importing settings
sys.modules['celery'] = MagicMock()
sys.modules['celery.schedules'] = MagicMock()
sys.modules['django_celery_beat'] = MagicMock()
sys.modules['drf_spectacular'] = MagicMock()
sys.modules['drf_spectacular.extensions'] = MagicMock()
sys.path.insert(0, 'D:/Tradhox_workspace/Django-CRM/backend')
os.environ['DJANGO_SETTINGS_MODULE'] = 'crm.settings'
import django
from django.conf import settings
# Override settings for standalone unit test
settings.DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
},
'seller_db': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
},
'product_db': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
},
'customer_db': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
},
}
settings.REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [],
'DEFAULT_PERMISSION_CLASSES': [],
}
settings.DATABASE_ROUTERS = ['crm.db_router.CRMDatabaseRouter']
settings.AUTH_USER_MODEL = 'auth.User'
settings.INSTALLED_APPS = [
'django.contrib.contenttypes',
'django.contrib.auth',
'rest_framework',
'sellers',
]
django.setup()
from sellers.models import SupplierProfile, Product, BulkUploadLog, SellerOrder, Wallet
from sellers.serializers import (
SupplierProfileListSerializer,
SupplierProfileDetailSerializer,
ProductSerializer
)
from crm.db_router import CRMDatabaseRouter
# 1. Test Router mappings
router = CRMDatabaseRouter()
assert router.db_for_read(Product) == 'product_db', f"Product should route to product_db, got {router.db_for_read(Product)}"
assert router.db_for_read(BulkUploadLog) == 'product_db', f"BulkUploadLog should route to product_db, got {router.db_for_read(BulkUploadLog)}"
assert router.db_for_read(SellerOrder) == 'customer_db', f"SellerOrder should route to customer_db, got {router.db_for_read(SellerOrder)}"
assert router.db_for_read(Wallet) == 'customer_db', f"Wallet should route to customer_db, got {router.db_for_read(Wallet)}"
assert router.db_for_read(SupplierProfile) == 'seller_db', f"SupplierProfile should route to seller_db, got {router.db_for_read(SupplierProfile)}"
print("CRMDatabaseRouter checks passed!")
# 2. Create tables in test DBs
from django.db import connections
def create_table_product():
with connections['product_db'].cursor() as c:
c.execute("""
CREATE TABLE api_product (
id INTEGER PRIMARY KEY AUTOINCREMENT,
supplier_id INTEGER,
title VARCHAR(255),
category VARCHAR(100),
price DECIMAL(10, 2),
stock INTEGER,
sku VARCHAR(100),
image TEXT,
is_active BOOLEAN,
status VARCHAR(50),
compare_at_price DECIMAL(10, 2),
description TEXT,
is_gi_tagged BOOLEAN,
gi_tag_name VARCHAR(200),
is_odop BOOLEAN,
odop_district VARCHAR(200),
is_sustainable BOOLEAN,
sustainable_type VARCHAR(200),
geographical_origin VARCHAR(255),
statutory_certifications TEXT,
provenance_story TEXT,
coordinates VARCHAR(100),
track_inventory BOOLEAN,
shipping_profile VARCHAR(100),
processing_days INTEGER,
package_weight REAL,
upload_source VARCHAR(50),
rejection_reason TEXT,
additional_images TEXT,
video_url VARCHAR(200),
view_360_url VARCHAR(200)
)
""")
def create_table_seller():
with connections['seller_db'].cursor() as c:
c.execute("""
CREATE TABLE api_supplierprofile (
id INTEGER PRIMARY KEY,
user_id INTEGER UNIQUE,
phone VARCHAR(20),
phone_verified BOOLEAN,
email_verified BOOLEAN,
gstin VARCHAR(15),
is_gstin_verified BOOLEAN,
pan_number VARCHAR(20),
is_pan_verified BOOLEAN,
verified_pan_name VARCHAR(255),
verified_gst_name VARCHAR(255),
is_verified BOOLEAN,
business_type VARCHAR(50),
store_slug VARCHAR(255),
aadhar_file VARCHAR(255),
pan_file VARCHAR(255),
aadhar_s3_key VARCHAR(255),
pan_s3_key VARCHAR(255),
store_name VARCHAR(255),
store_logo TEXT,
logo_s3_key VARCHAR(255),
business_bio TEXT,
street VARCHAR(255),
city VARCHAR(100),
state VARCHAR(100),
pincode VARCHAR(20),
latitude REAL,
longitude REAL,
status VARCHAR(50),
submitted_at DATETIME,
approved_at DATETIME,
approved_by VARCHAR(150),
rejected_reason TEXT,
is_profile_complete BOOLEAN,
pickup_code VARCHAR(100),
account_holder_name VARCHAR(255),
account_number VARCHAR(50),
ifsc_code VARCHAR(20),
bank_name VARCHAR(255),
is_bank_verified BOOLEAN,
tax_mode VARCHAR(20)
)
""")
def create_table_customer():
with connections['customer_db'].cursor() as c:
c.execute("""
CREATE TABLE api_order (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id VARCHAR(100),
customer VARCHAR(255),
item VARCHAR(255),
quantity INTEGER,
total DECIMAL(12, 2),
status VARCHAR(50),
carrier VARCHAR(100),
tracking VARCHAR(100),
date DATE,
supplier_id INTEGER,
payment_mode VARCHAR(20),
eta VARCHAR(50),
expected_delivery_date VARCHAR(100),
pickup_id VARCHAR(100),
pickup_payload TEXT
)
""")
c.execute("""
CREATE TABLE api_wallet (
id INTEGER PRIMARY KEY AUTOINCREMENT,
supplier_id INTEGER UNIQUE,
outstanding DECIMAL(12, 2),
withdrawn DECIMAL(12, 2)
)
""")
create_table_product()
create_table_seller()
create_table_customer()
# Create SupplierProfile with id=46, user_id=28
seller = SupplierProfile.objects.create(
id=46,
user_id=28,
store_name="vignesh store",
phone="9600494039",
status="approved"
)
# Create Product with supplier_id=28 (which is seller.user_id)
product = Product.objects.create(
id=20,
supplier_id=28,
title="Pure Kanchipuram Mulberry Silk Saree - Crimson Maroon & Gold Zari",
category="Clothing & Textiles",
price=14500.00,
stock=8,
sku="TRDX-KSS-960049",
is_gi_tagged=True,
gi_tag_name="Kancheepuram Silk (GI Application #26)",
is_odop=True,
odop_district="Kanchipuram, Tamil Nadu",
is_sustainable=True,
sustainable_type="Handloom Organic Mulberry Silk & Eco-dyes",
geographical_origin="Kanchipuram, Tamil Nadu, India",
statutory_certifications="GI Tag #26 Registered, Silk Mark Certified, Handloom Mark India",
status="pending",
is_active=True
)
# Test SupplierProfileListSerializer
list_data = SupplierProfileListSerializer(seller).data
print(f"SupplierProfileListSerializer products_count: {list_data['products_count']}")
assert list_data['products_count'] == 1, f"Expected products_count 1, got {list_data['products_count']}"
# Test SupplierProfileDetailSerializer
detail_data = SupplierProfileDetailSerializer(seller).data
print(f"Detail metrics: {detail_data['metrics']}")
assert detail_data['metrics']['total_products'] == 1, f"Expected 1 total_products, got {detail_data['metrics']['total_products']}"
assert detail_data['metrics']['total_items_in_stock'] == 8, f"Expected 8 items in stock, got {detail_data['metrics']['total_items_in_stock']}"
assert len(detail_data['products']) == 1, f"Expected 1 product in products list, got {len(detail_data['products'])}"
prod_data = detail_data['products'][0]
print(f"Product in detail: ID={prod_data['id']}, title='{prod_data['title']}', is_gi_tagged={prod_data['is_gi_tagged']}, is_odop={prod_data['is_odop']}, is_sustainable={prod_data['is_sustainable']}")
assert prod_data['id'] == 20
assert prod_data['is_gi_tagged'] is True
assert prod_data['is_odop'] is True
assert prod_data['is_sustainable'] is True
print("ALL SERIALIZER AND MODEL TESTS PASSED SUCCESSFULLY!")
# 3. Test Views logic (Approve, Reject, Toggle, Edit Content)
from sellers.views import (
SellerProductApproveView,
SellerProductRejectView,
SellerProductToggleView,
SellerProductDetailView
)
from django.core.management import call_command
call_command('migrate', database='default', run_syncdb=True, verbosity=0)
from rest_framework.test import APIRequestFactory, force_authenticate
from django.contrib.auth.models import User
factory = APIRequestFactory()
admin_user = User.objects.create_user(username='admin', email='admin@tradhox.com')
# Test Approve
req = factory.post(f"/api/sellers/46/products/20/approve/")
force_authenticate(req, user=admin_user)
resp = SellerProductApproveView.as_view()(req, pk=46, product_id=20)
assert resp.status_code == 200, f"Expected 200, got {resp.status_code}: {resp.data}"
p = Product.objects.using('product_db').get(pk=20)
assert p.status == 'approved'
assert p.is_active is True
print("SellerProductApproveView passed!")
# Test Reject
req = factory.post(f"/api/sellers/46/products/20/reject/", {"reason": "Quality check required"}, format="json")
force_authenticate(req, user=admin_user)
resp = SellerProductRejectView.as_view()(req, pk=46, product_id=20)
assert resp.status_code == 200
p = Product.objects.using('product_db').get(pk=20)
assert p.status == 'rejected'
assert p.is_active is False
assert p.rejection_reason == "Quality check required"
print("SellerProductRejectView passed!")
# Test Toggle
req = factory.post(f"/api/sellers/46/products/20/toggle/", {"is_active": True, "status": "approved"}, format="json")
force_authenticate(req, user=admin_user)
resp = SellerProductToggleView.as_view()(req, pk=46, product_id=20)
assert resp.status_code == 200
p = Product.objects.using('product_db').get(pk=20)
assert p.is_active is True
assert p.status == 'approved'
print("SellerProductToggleView passed!")
# Test Detail PATCH
req = factory.patch(f"/api/sellers/46/products/20/", {
"price": 15200.00,
"stock": 12,
"is_odop": True,
"odop_district": "Kanchipuram District",
"geographical_origin": "Kanchipuram, Tamil Nadu"
}, format="json")
force_authenticate(req, user=admin_user)
resp = SellerProductDetailView.as_view()(req, pk=46, product_id=20)
assert resp.status_code == 200
p = Product.objects.using('product_db').get(pk=20)
assert float(p.price) == 15200.00
assert p.stock == 12
assert p.odop_district == "Kanchipuram District"
print("SellerProductDetailView PATCH passed!")
print("ALL VIEWS AND API ENDPOINT TESTS PASSED SUCCESSFULLY!")

View file

@ -165,7 +165,7 @@ export const actions = {
const payload = {};
for (const [key, value] of data.entries()) {
if (key !== 'product_id' && key !== 'action') {
if (key === 'is_gi_tagged' || key === 'is_active') {
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') {
if (value !== '') payload[key] = Number(value);

View file

@ -624,11 +624,17 @@
</div>
{/if}
<div>
<div style="font-weight:600;font-size:13px;display:flex;align-items:center;gap:6px;">
<div style="font-weight:600;font-size:13px;display:flex;align-items:center;gap:6px;flex-wrap:wrap;">
{p.title}
{#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}
{#if p.is_odop}
<span style="font-size:10px;background:#F0FDF4;color:#166534;padding:1px 5px;border-radius:4px;font-weight:700;">ODOP</span>
{/if}
{#if p.is_sustainable}
<span style="font-size:10px;background:#EFF6FF;color:#1E40AF;padding:1px 5px;border-radius:4px;font-weight:700;">SUSTAINABLE</span>
{/if}
</div>
<div class="v2-muted" style="font-size:11px;">SKU: {p.sku || '—'} · ID: #{p.id}</div>
{#if p.rejection_reason}
@ -917,14 +923,33 @@
<textarea id="description" name="description" bind:value={editingProduct.description} rows="4" class="v2-input" style="width:100%;font-size:13px;padding:8px;"></textarea>
</div>
<div style="display:flex;align-items:center;gap:16px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
<div>
<label class="v2-sub" style="font-size:12px;display:block;margin-bottom:4px;" for="geographical_origin">Geographical Origin</label>
<input type="text" id="geographical_origin" name="geographical_origin" bind:value={editingProduct.geographical_origin} class="v2-input" style="width:100%;font-size:13px;padding:8px;" placeholder="e.g. Kanchipuram, Tamil Nadu" />
</div>
<div>
<label class="v2-sub" style="font-size:12px;display:block;margin-bottom:4px;" for="statutory_certifications">Statutory Certifications</label>
<input type="text" id="statutory_certifications" name="statutory_certifications" bind:value={editingProduct.statutory_certifications} class="v2-input" style="width:100%;font-size:13px;padding:8px;" placeholder="e.g. Silk Mark, Handloom Mark" />
</div>
</div>
<div style="display:flex;align-items:center;gap:16px;flex-wrap:wrap;">
<label style="display:flex;align-items:center;gap:6px;font-size:13px;cursor:pointer;">
<input type="checkbox" name="is_gi_tagged" value="true" checked={editingProduct.is_gi_tagged} />
<span>Certified Geographical Indication (GI Tag)</span>
<span>Certified GI Tag</span>
</label>
<label style="display:flex;align-items:center;gap:6px;font-size:13px;cursor:pointer;">
<input type="checkbox" name="is_odop" value="true" checked={editingProduct.is_odop} />
<span>ODOP Product</span>
</label>
<label style="display:flex;align-items:center;gap:6px;font-size:13px;cursor:pointer;">
<input type="checkbox" name="is_sustainable" value="true" checked={editingProduct.is_sustainable} />
<span>Sustainable Craft</span>
</label>
<label style="display:flex;align-items:center;gap:6px;font-size:13px;cursor:pointer;">
<input type="checkbox" name="is_active" value="true" checked={editingProduct.is_active} />
<span>Visible in Website Storefront</span>
<span>Visible in Storefront</span>
</label>
</div>
</div>