feat: comprehensive vendor settings overhaul with Company inheritance

- Add structured API response with business_info, localization, letzshop,
  invoice_settings, theme_settings, domains, and stripe_info sections
- Add PUT /vendor/settings/business-info with reset_to_company capability
- Add PUT /vendor/settings/letzshop with validation for tax rates, delivery
- Add 9 settings sections: General, Business Info, Localization, Marketplace,
  Invoices, Branding, Domains, API & Payments, Notifications
- Business Info shows "Inherited" badges and Reset buttons for company fields
- Marketplace section includes Letzshop CSV URLs and feed options
- Read-only sections for Invoices, Branding, Domains, API with contact support
- Add globe-alt icon for Domains section
- Document email templates architecture for future implementation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-03 12:16:36 +01:00
parent 3715493e47
commit 7d1ec2bdc2
6 changed files with 1514 additions and 50 deletions

View File

@@ -8,51 +8,440 @@ The get_current_vendor_api dependency guarantees token_vendor_id is present.
import logging import logging
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.api.deps import get_current_vendor_api from app.api.deps import get_current_vendor_api
from app.core.database import get_db from app.core.database import get_db
from app.services.platform_settings_service import platform_settings_service
from app.services.vendor_service import vendor_service from app.services.vendor_service import vendor_service
from models.database.user import User from models.database.user import User
router = APIRouter(prefix="/settings") router = APIRouter(prefix="/settings")
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Supported languages for dropdown
SUPPORTED_LANGUAGES = [
{"code": "en", "name": "English"},
{"code": "fr", "name": "Français"},
{"code": "de", "name": "Deutsch"},
{"code": "lb", "name": "Lëtzebuergesch"},
]
# Supported locales for currency/number formatting
SUPPORTED_LOCALES = [
{"code": "fr-LU", "name": "Luxembourg (French)", "example": "29,99 €"},
{"code": "de-LU", "name": "Luxembourg (German)", "example": "29,99 €"},
{"code": "de-DE", "name": "Germany", "example": "29,99 €"},
{"code": "fr-FR", "name": "France", "example": "29,99 €"},
{"code": "en-GB", "name": "United Kingdom", "example": "€29.99"},
]
class LocalizationSettingsUpdate(BaseModel):
"""Schema for updating localization settings."""
default_language: str | None = Field(
None, description="Default language for vendor content"
)
dashboard_language: str | None = Field(
None, description="Language for vendor dashboard UI"
)
storefront_language: str | None = Field(
None, description="Default language for customer storefront"
)
storefront_languages: list[str] | None = Field(
None, description="Enabled languages for storefront selector"
)
storefront_locale: str | None = Field(
None, description="Locale for currency/number formatting"
)
class BusinessInfoUpdate(BaseModel):
"""Schema for updating business info (can override company values)."""
name: str | None = Field(None, description="Store/brand name")
description: str | None = Field(None, description="Store description")
contact_email: str | None = Field(None, description="Contact email (null = inherit from company)")
contact_phone: str | None = Field(None, description="Contact phone (null = inherit from company)")
website: str | None = Field(None, description="Website URL (null = inherit from company)")
business_address: str | None = Field(None, description="Business address (null = inherit from company)")
tax_number: str | None = Field(None, description="Tax/VAT number (null = inherit from company)")
reset_to_company: list[str] | None = Field(
None, description="List of fields to reset to company values (e.g., ['contact_email', 'website'])"
)
class LetzshopFeedSettingsUpdate(BaseModel):
"""Schema for updating Letzshop feed settings."""
letzshop_csv_url_fr: str | None = Field(None, description="French CSV feed URL")
letzshop_csv_url_en: str | None = Field(None, description="English CSV feed URL")
letzshop_csv_url_de: str | None = Field(None, description="German CSV feed URL")
letzshop_default_tax_rate: int | None = Field(None, description="Default VAT rate (0, 3, 8, 14, 17)")
letzshop_boost_sort: str | None = Field(None, description="Sort priority (0.0-10.0)")
letzshop_delivery_method: str | None = Field(None, description="Delivery method")
letzshop_preorder_days: int | None = Field(None, description="Pre-order lead time in days")
# Valid Letzshop tax rates
VALID_TAX_RATES = [0, 3, 8, 14, 17]
# Valid delivery methods
VALID_DELIVERY_METHODS = ["nationwide", "package_delivery", "self_collect"]
@router.get("") @router.get("")
def get_vendor_settings( def get_vendor_settings(
current_user: User = Depends(get_current_vendor_api), current_user: User = Depends(get_current_vendor_api),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
"""Get vendor settings and configuration.""" """Get comprehensive vendor settings and configuration."""
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id)
# Get platform defaults for display
platform_config = platform_settings_service.get_storefront_config(db)
# Get business info with inheritance flags
contact_info = vendor.get_contact_info_with_inheritance()
# Get invoice settings if exists
invoice_settings = None
if vendor.invoice_settings:
inv = vendor.invoice_settings
invoice_settings = {
"company_name": inv.company_name,
"company_address": inv.company_address,
"company_city": inv.company_city,
"company_postal_code": inv.company_postal_code,
"company_country": inv.company_country,
"vat_number": inv.vat_number,
"is_vat_registered": inv.is_vat_registered,
"invoice_prefix": inv.invoice_prefix,
"invoice_next_number": inv.invoice_next_number,
"payment_terms": inv.payment_terms,
"bank_name": inv.bank_name,
"bank_iban": inv.bank_iban,
"bank_bic": inv.bank_bic,
"footer_text": inv.footer_text,
"default_vat_rate": inv.default_vat_rate,
}
# Get theme settings if exists
theme_settings = None
if vendor.vendor_theme:
theme = vendor.vendor_theme
theme_settings = {
"theme_name": theme.theme_name,
"colors": theme.colors,
"font_family_heading": theme.font_family_heading,
"font_family_body": theme.font_family_body,
"logo_url": theme.logo_url,
"logo_dark_url": theme.logo_dark_url,
"favicon_url": theme.favicon_url,
"banner_url": theme.banner_url,
"layout_style": theme.layout_style,
"header_style": theme.header_style,
"product_card_style": theme.product_card_style,
"social_links": theme.social_links,
"custom_css": theme.custom_css,
}
# Get domains (read-only)
domains = []
for domain in vendor.domains:
domains.append({
"id": domain.id,
"domain": domain.domain,
"is_primary": domain.is_primary,
"is_active": domain.is_active,
"ssl_status": domain.ssl_status,
"is_verified": domain.is_verified,
})
# Get Stripe info from subscription (read-only, masked)
stripe_info = None
if vendor.subscription and vendor.subscription.stripe_customer_id:
stripe_info = {
"has_stripe_customer": True,
"customer_id_masked": f"cus_***{vendor.subscription.stripe_customer_id[-4:]}",
}
return { return {
# General info
"vendor_code": vendor.vendor_code, "vendor_code": vendor.vendor_code,
"subdomain": vendor.subdomain, "subdomain": vendor.subdomain,
"name": vendor.name, "name": vendor.name,
"contact_email": vendor.contact_email, "description": vendor.description,
"contact_phone": vendor.contact_phone,
"website": vendor.website,
"business_address": vendor.business_address,
"tax_number": vendor.tax_number,
"letzshop_csv_url_fr": vendor.letzshop_csv_url_fr,
"letzshop_csv_url_en": vendor.letzshop_csv_url_en,
"letzshop_csv_url_de": vendor.letzshop_csv_url_de,
"is_active": vendor.is_active, "is_active": vendor.is_active,
"is_verified": vendor.is_verified, "is_verified": vendor.is_verified,
# Business info with inheritance (values + flags)
"business_info": {
"contact_email": contact_info["contact_email"],
"contact_email_inherited": contact_info["contact_email_inherited"],
"contact_email_override": vendor.contact_email, # Raw override value
"contact_phone": contact_info["contact_phone"],
"contact_phone_inherited": contact_info["contact_phone_inherited"],
"contact_phone_override": vendor.contact_phone,
"website": contact_info["website"],
"website_inherited": contact_info["website_inherited"],
"website_override": vendor.website,
"business_address": contact_info["business_address"],
"business_address_inherited": contact_info["business_address_inherited"],
"business_address_override": vendor.business_address,
"tax_number": contact_info["tax_number"],
"tax_number_inherited": contact_info["tax_number_inherited"],
"tax_number_override": vendor.tax_number,
"company_name": vendor.company.name if vendor.company else None,
},
# Localization settings
"localization": {
"default_language": vendor.default_language,
"dashboard_language": vendor.dashboard_language,
"storefront_language": vendor.storefront_language,
"storefront_languages": vendor.storefront_languages or ["fr", "de", "en"],
"storefront_locale": vendor.storefront_locale,
"platform_default_locale": platform_config["locale"],
"platform_currency": platform_config["currency"],
},
# Letzshop marketplace settings
"letzshop": {
"csv_url_fr": vendor.letzshop_csv_url_fr,
"csv_url_en": vendor.letzshop_csv_url_en,
"csv_url_de": vendor.letzshop_csv_url_de,
"default_tax_rate": vendor.letzshop_default_tax_rate,
"boost_sort": vendor.letzshop_boost_sort,
"delivery_method": vendor.letzshop_delivery_method,
"preorder_days": vendor.letzshop_preorder_days,
"vendor_id": vendor.letzshop_vendor_id,
"vendor_slug": vendor.letzshop_vendor_slug,
"has_credentials": vendor.letzshop_credentials is not None,
"auto_sync_enabled": vendor.letzshop_credentials.auto_sync_enabled if vendor.letzshop_credentials else False,
},
# Invoice settings
"invoice_settings": invoice_settings,
# Theme/branding settings
"theme_settings": theme_settings,
# Domains (read-only)
"domains": domains,
"default_subdomain": f"{vendor.subdomain}.letzshop.lu",
# Stripe info (read-only)
"stripe_info": stripe_info,
# Options for dropdowns
"options": {
"supported_languages": SUPPORTED_LANGUAGES,
"supported_locales": SUPPORTED_LOCALES,
"tax_rates": [
{"value": 0, "label": "0% (Exempt)"},
{"value": 3, "label": "3% (Super-reduced)"},
{"value": 8, "label": "8% (Reduced)"},
{"value": 14, "label": "14% (Intermediate)"},
{"value": 17, "label": "17% (Standard)"},
],
"delivery_methods": [
{"value": "nationwide", "label": "Nationwide (all methods)"},
{"value": "package_delivery", "label": "Package Delivery"},
{"value": "self_collect", "label": "Self Collect"},
],
},
} }
@router.put("/marketplace") @router.put("/business-info")
def update_marketplace_settings( def update_business_info(
marketplace_config: dict, business_info: BusinessInfoUpdate,
current_user: User = Depends(get_current_vendor_api), current_user: User = Depends(get_current_vendor_api),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
"""Update marketplace integration settings.""" """
# Service handles permission checking and raises InsufficientPermissionsException if needed Update vendor business info.
return vendor_service.update_marketplace_settings(
db, current_user.token_vendor_id, marketplace_config, current_user Fields can be set to override company values, or reset to inherit from company.
Use reset_to_company list to reset specific fields to inherit from company.
"""
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id)
update_data = business_info.model_dump(exclude_unset=True)
# Handle reset_to_company - set those fields to None
reset_fields = update_data.pop("reset_to_company", None) or []
for field in reset_fields:
if field in ["contact_email", "contact_phone", "website", "business_address", "tax_number"]:
setattr(vendor, field, None)
logger.info(f"Reset {field} to inherit from company for vendor {vendor.id}")
# Update other fields
for key, value in update_data.items():
if key in ["name", "description", "contact_email", "contact_phone", "website", "business_address", "tax_number"]:
setattr(vendor, key, value)
db.commit()
db.refresh(vendor)
logger.info(f"Business info updated for vendor {vendor.id}")
# Return updated info with inheritance
contact_info = vendor.get_contact_info_with_inheritance()
return {
"message": "Business info updated",
"name": vendor.name,
"description": vendor.description,
"business_info": contact_info,
}
@router.put("/letzshop")
def update_letzshop_settings(
letzshop_config: LetzshopFeedSettingsUpdate,
current_user: User = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""Update Letzshop marketplace feed settings."""
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id)
update_data = letzshop_config.model_dump(exclude_unset=True)
# Validate tax rate
if "letzshop_default_tax_rate" in update_data:
if update_data["letzshop_default_tax_rate"] not in VALID_TAX_RATES:
raise HTTPException(
status_code=400,
detail=f"Invalid tax rate. Must be one of: {VALID_TAX_RATES}"
) )
# Validate delivery method
if "letzshop_delivery_method" in update_data:
methods = update_data["letzshop_delivery_method"].split(",")
for method in methods:
if method.strip() not in VALID_DELIVERY_METHODS:
raise HTTPException(
status_code=400,
detail=f"Invalid delivery method. Must be one of: {VALID_DELIVERY_METHODS}"
)
# Validate boost_sort (0.0 - 10.0)
if "letzshop_boost_sort" in update_data:
try:
boost = float(update_data["letzshop_boost_sort"])
if boost < 0.0 or boost > 10.0:
raise HTTPException(
status_code=400,
detail="Boost sort must be between 0.0 and 10.0"
)
except ValueError:
raise HTTPException(
status_code=400,
detail="Boost sort must be a valid number"
)
# Validate preorder_days
if "letzshop_preorder_days" in update_data:
if update_data["letzshop_preorder_days"] < 0:
raise HTTPException(
status_code=400,
detail="Preorder days must be non-negative"
)
# Apply updates
for key, value in update_data.items():
setattr(vendor, key, value)
db.commit()
db.refresh(vendor)
logger.info(f"Letzshop settings updated for vendor {vendor.id}")
return {
"message": "Letzshop settings updated",
"csv_url_fr": vendor.letzshop_csv_url_fr,
"csv_url_en": vendor.letzshop_csv_url_en,
"csv_url_de": vendor.letzshop_csv_url_de,
"default_tax_rate": vendor.letzshop_default_tax_rate,
"boost_sort": vendor.letzshop_boost_sort,
"delivery_method": vendor.letzshop_delivery_method,
"preorder_days": vendor.letzshop_preorder_days,
}
@router.put("/localization")
def update_localization_settings(
localization_config: LocalizationSettingsUpdate,
current_user: User = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Update vendor localization settings.
Allows vendors to configure:
- default_language: Default language for vendor content
- dashboard_language: UI language for vendor dashboard
- storefront_language: Default language for customer storefront
- storefront_languages: Enabled languages for storefront selector
- storefront_locale: Locale for currency/number formatting (or null for platform default)
"""
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id)
# Update only provided fields
update_data = localization_config.model_dump(exclude_unset=True)
# Validate language codes
valid_language_codes = {lang["code"] for lang in SUPPORTED_LANGUAGES}
valid_locale_codes = {loc["code"] for loc in SUPPORTED_LOCALES}
if "default_language" in update_data and update_data["default_language"]:
if update_data["default_language"] not in valid_language_codes:
raise HTTPException(
status_code=400, detail=f"Invalid language: {update_data['default_language']}"
)
if "dashboard_language" in update_data and update_data["dashboard_language"]:
if update_data["dashboard_language"] not in valid_language_codes:
raise HTTPException(
status_code=400, detail=f"Invalid language: {update_data['dashboard_language']}"
)
if "storefront_language" in update_data and update_data["storefront_language"]:
if update_data["storefront_language"] not in valid_language_codes:
raise HTTPException(
status_code=400, detail=f"Invalid language: {update_data['storefront_language']}"
)
if "storefront_languages" in update_data and update_data["storefront_languages"]:
for lang in update_data["storefront_languages"]:
if lang not in valid_language_codes:
raise HTTPException(status_code=400, detail=f"Invalid language: {lang}")
if "storefront_locale" in update_data and update_data["storefront_locale"]:
if update_data["storefront_locale"] not in valid_locale_codes:
raise HTTPException(
status_code=400, detail=f"Invalid locale: {update_data['storefront_locale']}"
)
# Apply updates
for key, value in update_data.items():
setattr(vendor, key, value)
db.commit()
db.refresh(vendor)
logger.info(
f"Localization settings updated for vendor {vendor.id}",
extra={"vendor_id": vendor.id, "updated_fields": list(update_data.keys())},
)
return {
"message": "Localization settings updated",
"default_language": vendor.default_language,
"dashboard_language": vendor.dashboard_language,
"storefront_language": vendor.storefront_language,
"storefront_languages": vendor.storefront_languages,
"storefront_locale": vendor.storefront_locale,
}

View File

@@ -18,15 +18,15 @@
<!-- Settings Content --> <!-- Settings Content -->
<div x-show="!loading && !error" class="w-full mb-8"> <div x-show="!loading && !error" class="w-full mb-8">
<div class="flex flex-col md:flex-row gap-6"> <div class="flex flex-col lg:flex-row gap-6">
<!-- Settings Navigation --> <!-- Settings Navigation (Sidebar) -->
<div class="w-full md:w-64 flex-shrink-0"> <div class="w-full lg:w-56 flex-shrink-0">
<div class="bg-white rounded-lg shadow-xs dark:bg-gray-800 p-2"> <div class="bg-white rounded-lg shadow-xs dark:bg-gray-800 p-2 lg:sticky lg:top-4">
<template x-for="section in sections" :key="section.id"> <template x-for="section in sections" :key="section.id">
<button <button
@click="setSection(section.id)" @click="setSection(section.id)"
:class="{ :class="{
'w-full flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-colors': true, 'w-full flex items-center px-3 py-2.5 text-sm font-medium rounded-lg transition-colors': true,
'text-purple-600 bg-purple-50 dark:bg-purple-900/20 dark:text-purple-400': activeSection === section.id, 'text-purple-600 bg-purple-50 dark:bg-purple-900/20 dark:text-purple-400': activeSection === section.id,
'text-gray-600 hover:bg-gray-50 dark:text-gray-400 dark:hover:bg-gray-700': activeSection !== section.id 'text-gray-600 hover:bg-gray-50 dark:text-gray-400 dark:hover:bg-gray-700': activeSection !== section.id
}" }"
@@ -38,8 +38,8 @@
</div> </div>
</div> </div>
<!-- Settings Panels --> <!-- Settings Panels (Main Content) -->
<div class="flex-1"> <div class="flex-1 min-w-0">
<!-- General Settings --> <!-- General Settings -->
<div x-show="activeSection === 'general'" class="bg-white rounded-lg shadow-xs dark:bg-gray-800"> <div x-show="activeSection === 'general'" class="bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-4 border-b dark:border-gray-700"> <div class="p-4 border-b dark:border-gray-700">
@@ -61,7 +61,7 @@
class="flex-1 px-4 py-2 text-sm text-gray-500 bg-gray-100 border border-gray-200 rounded-l-lg dark:text-gray-400 dark:bg-gray-700 dark:border-gray-600" class="flex-1 px-4 py-2 text-sm text-gray-500 bg-gray-100 border border-gray-200 rounded-l-lg dark:text-gray-400 dark:bg-gray-700 dark:border-gray-600"
/> />
<span class="px-4 py-2 text-sm text-gray-500 bg-gray-50 border border-l-0 border-gray-200 rounded-r-lg dark:text-gray-400 dark:bg-gray-600 dark:border-gray-600"> <span class="px-4 py-2 text-sm text-gray-500 bg-gray-50 border border-l-0 border-gray-200 rounded-r-lg dark:text-gray-400 dark:bg-gray-600 dark:border-gray-600">
.yourplatform.com .letzshop.lu
</span> </span>
</div> </div>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Contact support to change your subdomain</p> <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Contact support to change your subdomain</p>
@@ -98,14 +98,386 @@
</div> </div>
</div> </div>
<!-- Business Info Settings -->
<div x-show="activeSection === 'business'" class="bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-4 border-b dark:border-gray-700">
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200">Business Information</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">
Store details and contact information
<template x-if="companyName">
<span class="text-purple-600 dark:text-purple-400"> (inheriting from <span x-text="companyName"></span>)</span>
</template>
</p>
</div>
<div class="p-4">
<div class="space-y-6">
<!-- Store Name -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Store Name
</label>
<input
type="text"
x-model="businessForm.name"
@input="markBusinessChanged()"
class="w-full px-4 py-2 text-sm text-gray-700 bg-white border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-800 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
placeholder="Your store name"
/>
</div>
<!-- Description -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Store Description
</label>
<textarea
x-model="businessForm.description"
@input="markBusinessChanged()"
rows="3"
class="w-full px-4 py-2 text-sm text-gray-700 bg-white border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-800 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
placeholder="Describe your store..."
></textarea>
</div>
<!-- Contact Email (inheritable) -->
<div>
<label class="flex items-center text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Contact Email
<template x-if="isFieldInherited('contact_email')">
<span class="ml-2 px-2 py-0.5 text-xs font-medium text-purple-600 bg-purple-100 rounded dark:bg-purple-900/30 dark:text-purple-400">
Inherited
</span>
</template>
</label>
<div class="flex gap-2">
<input
type="email"
x-model="businessForm.contact_email"
@input="markBusinessChanged()"
:placeholder="getEffectiveBusinessValue('contact_email') || 'contact@example.com'"
class="flex-1 px-4 py-2 text-sm text-gray-700 bg-white border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-800 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
/>
<template x-if="businessForm.contact_email">
<button
@click="resetToCompany('contact_email')"
class="px-3 py-2 text-xs text-gray-600 bg-gray-100 rounded-lg hover:bg-gray-200 dark:text-gray-400 dark:bg-gray-700 dark:hover:bg-gray-600"
title="Reset to company value"
>
Reset
</button>
</template>
</div>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
Leave empty to use company default
</p>
</div>
<!-- Contact Phone (inheritable) -->
<div>
<label class="flex items-center text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Contact Phone
<template x-if="isFieldInherited('contact_phone')">
<span class="ml-2 px-2 py-0.5 text-xs font-medium text-purple-600 bg-purple-100 rounded dark:bg-purple-900/30 dark:text-purple-400">
Inherited
</span>
</template>
</label>
<div class="flex gap-2">
<input
type="tel"
x-model="businessForm.contact_phone"
@input="markBusinessChanged()"
:placeholder="getEffectiveBusinessValue('contact_phone') || '+352 123 456'"
class="flex-1 px-4 py-2 text-sm text-gray-700 bg-white border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-800 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
/>
<template x-if="businessForm.contact_phone">
<button
@click="resetToCompany('contact_phone')"
class="px-3 py-2 text-xs text-gray-600 bg-gray-100 rounded-lg hover:bg-gray-200 dark:text-gray-400 dark:bg-gray-700 dark:hover:bg-gray-600"
title="Reset to company value"
>
Reset
</button>
</template>
</div>
</div>
<!-- Website (inheritable) -->
<div>
<label class="flex items-center text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Website
<template x-if="isFieldInherited('website')">
<span class="ml-2 px-2 py-0.5 text-xs font-medium text-purple-600 bg-purple-100 rounded dark:bg-purple-900/30 dark:text-purple-400">
Inherited
</span>
</template>
</label>
<div class="flex gap-2">
<input
type="url"
x-model="businessForm.website"
@input="markBusinessChanged()"
:placeholder="getEffectiveBusinessValue('website') || 'https://'"
class="flex-1 px-4 py-2 text-sm text-gray-700 bg-white border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-800 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
/>
<template x-if="businessForm.website">
<button
@click="resetToCompany('website')"
class="px-3 py-2 text-xs text-gray-600 bg-gray-100 rounded-lg hover:bg-gray-200 dark:text-gray-400 dark:bg-gray-700 dark:hover:bg-gray-600"
title="Reset to company value"
>
Reset
</button>
</template>
</div>
</div>
<!-- Business Address (inheritable) -->
<div>
<label class="flex items-center text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Business Address
<template x-if="isFieldInherited('business_address')">
<span class="ml-2 px-2 py-0.5 text-xs font-medium text-purple-600 bg-purple-100 rounded dark:bg-purple-900/30 dark:text-purple-400">
Inherited
</span>
</template>
</label>
<div class="flex gap-2">
<textarea
x-model="businessForm.business_address"
@input="markBusinessChanged()"
rows="2"
:placeholder="getEffectiveBusinessValue('business_address') || 'Street, City, Country'"
class="flex-1 px-4 py-2 text-sm text-gray-700 bg-white border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-800 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
></textarea>
<template x-if="businessForm.business_address">
<button
@click="resetToCompany('business_address')"
class="px-3 py-2 text-xs text-gray-600 bg-gray-100 rounded-lg hover:bg-gray-200 dark:text-gray-400 dark:bg-gray-700 dark:hover:bg-gray-600"
title="Reset to company value"
>
Reset
</button>
</template>
</div>
</div>
<!-- Tax Number (inheritable) -->
<div>
<label class="flex items-center text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Tax / VAT Number
<template x-if="isFieldInherited('tax_number')">
<span class="ml-2 px-2 py-0.5 text-xs font-medium text-purple-600 bg-purple-100 rounded dark:bg-purple-900/30 dark:text-purple-400">
Inherited
</span>
</template>
</label>
<div class="flex gap-2">
<input
type="text"
x-model="businessForm.tax_number"
@input="markBusinessChanged()"
:placeholder="getEffectiveBusinessValue('tax_number') || 'LU12345678'"
class="flex-1 px-4 py-2 text-sm text-gray-700 bg-white border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-800 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
/>
<template x-if="businessForm.tax_number">
<button
@click="resetToCompany('tax_number')"
class="px-3 py-2 text-xs text-gray-600 bg-gray-100 rounded-lg hover:bg-gray-200 dark:text-gray-400 dark:bg-gray-700 dark:hover:bg-gray-600"
title="Reset to company value"
>
Reset
</button>
</template>
</div>
</div>
<!-- Save Button -->
<div class="flex justify-end pt-4 border-t dark:border-gray-600">
<button
@click="saveBusinessInfo()"
:disabled="saving || !hasBusinessChanges"
class="px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700 disabled:opacity-50"
>
<span x-show="!saving">Save Business Info</span>
<span x-show="saving">Saving...</span>
</button>
</div>
</div>
</div>
</div>
<!-- Localization Settings -->
<div x-show="activeSection === 'localization'" class="bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-4 border-b dark:border-gray-700">
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200">Localization</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Configure language and regional settings</p>
</div>
<div class="p-4">
<div class="space-y-6">
<!-- Currency (Read-only) -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Currency
</label>
<div class="flex items-center gap-3">
<input
type="text"
:value="settings?.localization?.platform_currency || 'EUR'"
disabled
class="w-32 px-4 py-2 text-sm text-gray-500 bg-gray-100 border border-gray-200 rounded-lg dark:text-gray-400 dark:bg-gray-700 dark:border-gray-600"
/>
<span class="text-sm text-gray-500 dark:text-gray-400">
Platform-wide currency (contact admin to change)
</span>
</div>
</div>
<!-- Storefront Locale -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Number & Currency Format
</label>
<select
x-model="localizationForm.storefront_locale"
@change="markLocalizationChanged()"
class="w-full px-4 py-2 text-sm text-gray-700 bg-white border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-800 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
>
<option value="">Use platform default (<span x-text="settings?.localization?.platform_default_locale"></span>)</option>
<template x-for="locale in settings?.options?.supported_locales || []" :key="locale.code">
<option :value="locale.code" x-text="`${locale.name} (${locale.example})`"></option>
</template>
</select>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
Controls how prices and numbers are displayed (e.g., "29,99 EUR" vs "EUR29.99")
</p>
</div>
<!-- Dashboard Language -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Dashboard Language
</label>
<select
x-model="localizationForm.dashboard_language"
@change="markLocalizationChanged()"
class="w-full px-4 py-2 text-sm text-gray-700 bg-white border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-800 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
>
<template x-for="lang in settings?.options?.supported_languages || []" :key="lang.code">
<option :value="lang.code" x-text="lang.name"></option>
</template>
</select>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
Language for the vendor dashboard interface
</p>
</div>
<!-- Default Content Language -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Default Content Language
</label>
<select
x-model="localizationForm.default_language"
@change="markLocalizationChanged()"
class="w-full px-4 py-2 text-sm text-gray-700 bg-white border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-800 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
>
<template x-for="lang in settings?.options?.supported_languages || []" :key="lang.code">
<option :value="lang.code" x-text="lang.name"></option>
</template>
</select>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
Primary language for products, emails, and other content
</p>
</div>
<!-- Storefront Default Language -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Storefront Default Language
</label>
<select
x-model="localizationForm.storefront_language"
@change="markLocalizationChanged()"
class="w-full px-4 py-2 text-sm text-gray-700 bg-white border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-800 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
>
<template x-for="lang in settings?.options?.supported_languages || []" :key="lang.code">
<option :value="lang.code" x-text="lang.name"></option>
</template>
</select>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
Default language shown to customers visiting your shop
</p>
</div>
<!-- Enabled Storefront Languages -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Enabled Storefront Languages
</label>
<div class="flex flex-wrap gap-3">
<template x-for="lang in settings?.options?.supported_languages || []" :key="lang.code">
<label class="inline-flex items-center cursor-pointer">
<input
type="checkbox"
:value="lang.code"
:checked="localizationForm.storefront_languages?.includes(lang.code)"
@change="toggleStorefrontLanguage(lang.code)"
class="w-4 h-4 text-purple-600 bg-gray-100 border-gray-300 rounded focus:ring-purple-500 dark:focus:ring-purple-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
/>
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300" x-text="lang.name"></span>
</label>
</template>
</div>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
Languages available in the storefront language selector
</p>
</div>
<!-- Save Button -->
<div class="flex justify-end pt-4 border-t dark:border-gray-600">
<button
@click="saveLocalizationSettings()"
:disabled="saving || !hasLocalizationChanges"
class="px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700 disabled:opacity-50"
>
<span x-show="!saving">Save Localization Settings</span>
<span x-show="saving">Saving...</span>
</button>
</div>
</div>
</div>
</div>
<!-- Marketplace Settings --> <!-- Marketplace Settings -->
<div x-show="activeSection === 'marketplace'" class="bg-white rounded-lg shadow-xs dark:bg-gray-800"> <div x-show="activeSection === 'marketplace'" class="bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-4 border-b dark:border-gray-700"> <div class="p-4 border-b dark:border-gray-700">
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200">Marketplace Integration</h3> <h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200">Marketplace Integration</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Configure external marketplace feeds</p> <p class="text-sm text-gray-500 dark:text-gray-400">Configure Letzshop marketplace feed settings</p>
</div> </div>
<div class="p-4"> <div class="p-4">
<div class="space-y-6"> <div class="space-y-6">
<!-- Letzshop Vendor Info (read-only) -->
<template x-if="settings?.letzshop?.vendor_id">
<div class="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg border border-green-200 dark:border-green-800">
<div class="flex items-center gap-2 mb-2">
<span x-html="$icon('check-circle', 'w-5 h-5 text-green-600 dark:text-green-400')"></span>
<span class="font-medium text-green-800 dark:text-green-300">Connected to Letzshop</span>
</div>
<p class="text-sm text-green-700 dark:text-green-400">
Vendor ID: <span x-text="settings?.letzshop?.vendor_id"></span>
<template x-if="settings?.letzshop?.vendor_slug">
<span> (<span x-text="settings?.letzshop?.vendor_slug"></span>)</span>
</template>
</p>
<template x-if="settings?.letzshop?.auto_sync_enabled">
<p class="text-sm text-green-700 dark:text-green-400 mt-1">
Auto-sync is enabled
</p>
</template>
</div>
</template>
<!-- Letzshop CSV URLs --> <!-- Letzshop CSV URLs -->
<div class="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg"> <div class="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
<h4 class="font-medium text-gray-700 dark:text-gray-300 mb-4">Letzshop CSV Feed URLs</h4> <h4 class="font-medium text-gray-700 dark:text-gray-300 mb-4">Letzshop CSV Feed URLs</h4>
@@ -122,7 +494,7 @@
<input <input
type="url" type="url"
x-model="marketplaceForm.letzshop_csv_url_fr" x-model="marketplaceForm.letzshop_csv_url_fr"
@input="markChanged()" @input="markMarketplaceChanged()"
class="flex-1 px-4 py-2 text-sm text-gray-700 bg-white border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-800 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400" class="flex-1 px-4 py-2 text-sm text-gray-700 bg-white border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-800 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
placeholder="https://..." placeholder="https://..."
/> />
@@ -145,7 +517,7 @@
<input <input
type="url" type="url"
x-model="marketplaceForm.letzshop_csv_url_en" x-model="marketplaceForm.letzshop_csv_url_en"
@input="markChanged()" @input="markMarketplaceChanged()"
class="flex-1 px-4 py-2 text-sm text-gray-700 bg-white border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-800 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400" class="flex-1 px-4 py-2 text-sm text-gray-700 bg-white border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-800 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
placeholder="https://..." placeholder="https://..."
/> />
@@ -168,7 +540,7 @@
<input <input
type="url" type="url"
x-model="marketplaceForm.letzshop_csv_url_de" x-model="marketplaceForm.letzshop_csv_url_de"
@input="markChanged()" @input="markMarketplaceChanged()"
class="flex-1 px-4 py-2 text-sm text-gray-700 bg-white border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-800 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400" class="flex-1 px-4 py-2 text-sm text-gray-700 bg-white border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-800 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
placeholder="https://..." placeholder="https://..."
/> />
@@ -181,12 +553,90 @@
</button> </button>
</div> </div>
</div> </div>
</div>
<!-- Letzshop Feed Options -->
<div class="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
<h4 class="font-medium text-gray-700 dark:text-gray-300 mb-4">Feed Options</h4>
<!-- Default Tax Rate -->
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Default VAT Rate
</label>
<select
x-model="marketplaceForm.letzshop_default_tax_rate"
@change="markMarketplaceChanged()"
class="w-full px-4 py-2 text-sm text-gray-700 bg-white border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-800 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
>
<option :value="null">Not set</option>
<template x-for="rate in settings?.options?.tax_rates || []" :key="rate.value">
<option :value="rate.value" x-text="rate.label"></option>
</template>
</select>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
Default VAT rate for products without explicit rate
</p>
</div>
<!-- Delivery Method -->
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Delivery Method
</label>
<select
x-model="marketplaceForm.letzshop_delivery_method"
@change="markMarketplaceChanged()"
class="w-full px-4 py-2 text-sm text-gray-700 bg-white border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-800 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
>
<option value="">Not set</option>
<template x-for="method in settings?.options?.delivery_methods || []" :key="method.value">
<option :value="method.value" x-text="method.label"></option>
</template>
</select>
</div>
<!-- Boost Sort -->
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Boost Sort Priority
</label>
<input
type="number"
step="0.1"
min="0"
max="10"
x-model="marketplaceForm.letzshop_boost_sort"
@input="markMarketplaceChanged()"
class="w-full px-4 py-2 text-sm text-gray-700 bg-white border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-800 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
placeholder="0.0 - 10.0"
/>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
Higher values boost product visibility (0.0 - 10.0)
</p>
</div>
<!-- Preorder Days -->
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Pre-order Lead Time (days)
</label>
<input
type="number"
min="0"
x-model="marketplaceForm.letzshop_preorder_days"
@input="markMarketplaceChanged()"
class="w-full px-4 py-2 text-sm text-gray-700 bg-white border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-800 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
placeholder="0"
/>
</div>
</div>
<!-- Save Button --> <!-- Save Button -->
<div class="flex justify-end mt-4 pt-4 border-t dark:border-gray-600"> <div class="flex justify-end pt-4 border-t dark:border-gray-600">
<button <button
@click="saveMarketplaceSettings()" @click="saveMarketplaceSettings()"
:disabled="saving || !hasChanges" :disabled="saving || !hasMarketplaceChanges"
class="px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700 disabled:opacity-50" class="px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700 disabled:opacity-50"
> >
<span x-show="!saving">Save Marketplace Settings</span> <span x-show="!saving">Save Marketplace Settings</span>
@@ -196,6 +646,317 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Invoice Settings -->
<div x-show="activeSection === 'invoices'" class="bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-4 border-b dark:border-gray-700">
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200">Invoice Settings</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Configure invoice generation and billing details</p>
</div>
<div class="p-4">
<template x-if="settings?.invoice_settings">
<div class="space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Company Name -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Company Name</label>
<p class="text-sm text-gray-600 dark:text-gray-400" x-text="settings?.invoice_settings?.company_name || '-'"></p>
</div>
<!-- VAT Number -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">VAT Number</label>
<p class="text-sm text-gray-600 dark:text-gray-400" x-text="settings?.invoice_settings?.vat_number || '-'"></p>
</div>
<!-- Address -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Address</label>
<p class="text-sm text-gray-600 dark:text-gray-400">
<span x-text="settings?.invoice_settings?.company_address || '-'"></span>
<template x-if="settings?.invoice_settings?.company_postal_code || settings?.invoice_settings?.company_city">
<br/><span x-text="`${settings?.invoice_settings?.company_postal_code || ''} ${settings?.invoice_settings?.company_city || ''}`"></span>
</template>
</p>
</div>
<!-- Invoice Prefix -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Invoice Prefix</label>
<p class="text-sm text-gray-600 dark:text-gray-400" x-text="settings?.invoice_settings?.invoice_prefix || '-'"></p>
</div>
<!-- Default VAT Rate -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Default VAT Rate</label>
<p class="text-sm text-gray-600 dark:text-gray-400" x-text="settings?.invoice_settings?.default_vat_rate ? settings.invoice_settings.default_vat_rate + '%' : '-'"></p>
</div>
<!-- Payment Terms -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Payment Terms</label>
<p class="text-sm text-gray-600 dark:text-gray-400" x-text="settings?.invoice_settings?.payment_terms || '-'"></p>
</div>
</div>
<!-- Bank Details -->
<div class="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
<h4 class="font-medium text-gray-700 dark:text-gray-300 mb-3">Bank Details</h4>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label class="block text-sm font-medium text-gray-600 dark:text-gray-400 mb-1">Bank</label>
<p class="text-sm text-gray-800 dark:text-gray-200" x-text="settings?.invoice_settings?.bank_name || '-'"></p>
</div>
<div>
<label class="block text-sm font-medium text-gray-600 dark:text-gray-400 mb-1">IBAN</label>
<p class="text-sm text-gray-800 dark:text-gray-200" x-text="settings?.invoice_settings?.bank_iban || '-'"></p>
</div>
<div>
<label class="block text-sm font-medium text-gray-600 dark:text-gray-400 mb-1">BIC</label>
<p class="text-sm text-gray-800 dark:text-gray-200" x-text="settings?.invoice_settings?.bank_bic || '-'"></p>
</div>
</div>
</div>
<p class="text-sm text-gray-500 dark:text-gray-400 italic">
Contact support to update invoice settings.
</p>
</div>
</template>
<template x-if="!settings?.invoice_settings">
<div class="text-center py-8">
<span x-html="$icon('document-text', 'w-12 h-12 mx-auto text-gray-400 dark:text-gray-500')"></span>
<p class="mt-2 text-gray-500 dark:text-gray-400">No invoice settings configured</p>
<p class="text-sm text-gray-400 dark:text-gray-500">Contact support to set up invoicing.</p>
</div>
</template>
</div>
</div>
<!-- Branding Settings -->
<div x-show="activeSection === 'branding'" class="bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-4 border-b dark:border-gray-700">
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200">Branding & Theme</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Customize your storefront appearance</p>
</div>
<div class="p-4">
<template x-if="settings?.theme_settings">
<div class="space-y-6">
<!-- Theme Name -->
<div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
<div>
<p class="font-medium text-gray-700 dark:text-gray-300">Active Theme</p>
<p class="text-sm text-gray-500 dark:text-gray-400" x-text="settings?.theme_settings?.theme_name || 'Default'"></p>
</div>
<span class="px-3 py-1 text-sm font-semibold text-green-700 bg-green-100 rounded-full dark:bg-green-700 dark:text-green-100">
Active
</span>
</div>
<!-- Logos -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<template x-if="settings?.theme_settings?.logo_url">
<div class="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
<p class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Logo</p>
<img :src="settings?.theme_settings?.logo_url" alt="Logo" class="max-h-16 object-contain" />
</div>
</template>
<template x-if="settings?.theme_settings?.favicon_url">
<div class="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
<p class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Favicon</p>
<img :src="settings?.theme_settings?.favicon_url" alt="Favicon" class="max-h-8 object-contain" />
</div>
</template>
</div>
<!-- Layout Style -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Layout Style</label>
<p class="text-sm text-gray-600 dark:text-gray-400" x-text="settings?.theme_settings?.layout_style || 'Default'"></p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Header Style</label>
<p class="text-sm text-gray-600 dark:text-gray-400" x-text="settings?.theme_settings?.header_style || 'Default'"></p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Product Card Style</label>
<p class="text-sm text-gray-600 dark:text-gray-400" x-text="settings?.theme_settings?.product_card_style || 'Default'"></p>
</div>
</div>
<!-- Social Links -->
<template x-if="settings?.theme_settings?.social_links && Object.keys(settings.theme_settings.social_links).length > 0">
<div class="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
<p class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Social Links</p>
<div class="flex flex-wrap gap-3">
<template x-for="(url, platform) in settings?.theme_settings?.social_links || {}" :key="platform">
<a :href="url" target="_blank" class="px-3 py-1 text-sm bg-white dark:bg-gray-600 rounded border dark:border-gray-500 hover:bg-gray-100 dark:hover:bg-gray-500">
<span x-text="platform"></span>
</a>
</template>
</div>
</div>
</template>
<p class="text-sm text-gray-500 dark:text-gray-400 italic">
Theme customization coming soon. Contact support for custom branding requests.
</p>
</div>
</template>
<template x-if="!settings?.theme_settings">
<div class="text-center py-8">
<span x-html="$icon('color-swatch', 'w-12 h-12 mx-auto text-gray-400 dark:text-gray-500')"></span>
<p class="mt-2 text-gray-500 dark:text-gray-400">Using default theme</p>
<p class="text-sm text-gray-400 dark:text-gray-500">Contact support for custom branding.</p>
</div>
</template>
</div>
</div>
<!-- Domains Settings -->
<div x-show="activeSection === 'domains'" class="bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-4 border-b dark:border-gray-700">
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200">Domains</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Manage your storefront domains</p>
</div>
<div class="p-4">
<div class="space-y-4">
<!-- Default Subdomain -->
<div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
<div>
<p class="font-medium text-gray-700 dark:text-gray-300">Default Subdomain</p>
<p class="text-sm text-purple-600 dark:text-purple-400" x-text="settings?.default_subdomain"></p>
</div>
<span class="px-3 py-1 text-sm font-semibold text-green-700 bg-green-100 rounded-full dark:bg-green-700 dark:text-green-100">
Active
</span>
</div>
<!-- Custom Domains -->
<template x-if="settings?.domains?.length > 0">
<div class="space-y-3">
<h4 class="font-medium text-gray-700 dark:text-gray-300">Custom Domains</h4>
<template x-for="domain in settings?.domains" :key="domain.id">
<div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
<div>
<p class="font-medium text-gray-700 dark:text-gray-300" x-text="domain.domain"></p>
<div class="flex items-center gap-2 mt-1">
<span
:class="domain.is_verified
? 'text-green-600 dark:text-green-400'
: 'text-yellow-600 dark:text-yellow-400'"
class="text-xs"
x-text="domain.is_verified ? 'Verified' : 'Pending verification'"
></span>
<span class="text-gray-300 dark:text-gray-600">|</span>
<span
:class="domain.ssl_status === 'active'
? 'text-green-600 dark:text-green-400'
: 'text-yellow-600 dark:text-yellow-400'"
class="text-xs"
x-text="domain.ssl_status === 'active' ? 'SSL Active' : 'SSL ' + (domain.ssl_status || 'pending')"
></span>
</div>
</div>
<div class="flex items-center gap-2">
<template x-if="domain.is_primary">
<span class="px-2 py-1 text-xs font-medium text-purple-600 bg-purple-100 rounded dark:bg-purple-900/30 dark:text-purple-400">
Primary
</span>
</template>
<span
:class="domain.is_active
? 'px-3 py-1 text-sm font-semibold text-green-700 bg-green-100 rounded-full dark:bg-green-700 dark:text-green-100'
: 'px-3 py-1 text-sm font-semibold text-gray-700 bg-gray-200 rounded-full dark:bg-gray-600 dark:text-gray-100'"
x-text="domain.is_active ? 'Active' : 'Inactive'"
></span>
</div>
</div>
</template>
</div>
</template>
<div class="mt-4 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
<div class="flex items-start gap-2">
<span x-html="$icon('information-circle', 'w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5')"></span>
<div>
<p class="text-sm text-blue-800 dark:text-blue-300">
Need a custom domain? Contact support to set up your own domain with SSL.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- API & Payments Settings -->
<div x-show="activeSection === 'api'" class="bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-4 border-b dark:border-gray-700">
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200">API & Payments</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Payment integrations and API access</p>
</div>
<div class="p-4">
<div class="space-y-6">
<!-- Stripe Integration -->
<div class="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
<div class="flex items-center gap-3 mb-4">
<div class="w-10 h-10 bg-purple-100 dark:bg-purple-900/30 rounded-lg flex items-center justify-center">
<span x-html="$icon('credit-card', 'w-6 h-6 text-purple-600 dark:text-purple-400')"></span>
</div>
<div>
<h4 class="font-medium text-gray-700 dark:text-gray-300">Stripe</h4>
<p class="text-sm text-gray-500 dark:text-gray-400">Payment processing</p>
</div>
</div>
<template x-if="settings?.stripe_info?.has_stripe_customer">
<div class="space-y-3">
<div class="flex items-center gap-2">
<span x-html="$icon('check-circle', 'w-5 h-5 text-green-600 dark:text-green-400')"></span>
<span class="text-sm text-green-700 dark:text-green-300">Connected</span>
</div>
<div>
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">Customer ID</label>
<p class="text-sm font-mono text-gray-700 dark:text-gray-300" x-text="settings?.stripe_info?.customer_id_masked"></p>
</div>
</div>
</template>
<template x-if="!settings?.stripe_info?.has_stripe_customer">
<div class="flex items-center gap-2">
<span x-html="$icon('x-circle', 'w-5 h-5 text-gray-400 dark:text-gray-500')"></span>
<span class="text-sm text-gray-500 dark:text-gray-400">Not connected</span>
</div>
</template>
</div>
<!-- Letzshop API (if credentials exist) -->
<template x-if="settings?.letzshop?.has_credentials">
<div class="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
<div class="flex items-center gap-3 mb-4">
<div class="w-10 h-10 bg-orange-100 dark:bg-orange-900/30 rounded-lg flex items-center justify-center">
<span x-html="$icon('shopping-cart', 'w-6 h-6 text-orange-600 dark:text-orange-400')"></span>
</div>
<div>
<h4 class="font-medium text-gray-700 dark:text-gray-300">Letzshop API</h4>
<p class="text-sm text-gray-500 dark:text-gray-400">Marketplace integration</p>
</div>
</div>
<div class="flex items-center gap-2">
<span x-html="$icon('check-circle', 'w-5 h-5 text-green-600 dark:text-green-400')"></span>
<span class="text-sm text-green-700 dark:text-green-300">Credentials configured</span>
</div>
</div>
</template>
<div class="mt-4 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
<div class="flex items-start gap-2">
<span x-html="$icon('information-circle', 'w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5')"></span>
<div>
<p class="text-sm text-blue-800 dark:text-blue-300">
API keys and payment credentials are managed securely. Contact support for changes.
</p>
</div>
</div>
</div>
</div>
</div>
</div> </div>
<!-- Notification Settings --> <!-- Notification Settings -->

View File

@@ -0,0 +1,107 @@
# Email Templates Architecture (Future)
## Overview
Email templates follow a similar pattern to the CMS content pages system, with platform defaults that vendors can override.
## Architecture
### Template Hierarchy
1. **Platform Default Templates** - Admin-managed base templates
- Order confirmation
- Shipping notification
- Password reset
- Welcome email
- Invoice email
- etc.
2. **Vendor Overrides** - Optional customization
- Vendors can override platform templates
- Cannot create new template types (unlike CMS pages)
- Must maintain required placeholders
### Multi-language Support
- Each template exists in all supported languages (FR, EN, DE, LB)
- Vendor overrides are per-language
- Falls back to platform default if no vendor override
### Key Differences from CMS Pages
| Feature | CMS Pages | Email Templates |
|---------|-----------|-----------------|
| Create new | Vendors can create | Vendors cannot create |
| Template types | Unlimited | Fixed set by platform |
| Tier limits | Number of pages per tier | No limits (all templates available) |
| Override | Full content | Full content |
## Database Design (Proposed)
```sql
-- Platform templates (admin-managed)
CREATE TABLE email_templates (
id SERIAL PRIMARY KEY,
template_key VARCHAR(100) NOT NULL, -- e.g., 'order_confirmation'
language VARCHAR(5) NOT NULL, -- e.g., 'fr', 'en', 'de'
subject TEXT NOT NULL,
html_body TEXT NOT NULL,
text_body TEXT,
placeholders JSONB, -- Required placeholders
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP,
updated_at TIMESTAMP,
UNIQUE(template_key, language)
);
-- Vendor overrides
CREATE TABLE vendor_email_templates (
id SERIAL PRIMARY KEY,
vendor_id INTEGER REFERENCES vendors(id),
template_key VARCHAR(100) NOT NULL,
language VARCHAR(5) NOT NULL,
subject TEXT NOT NULL,
html_body TEXT NOT NULL,
text_body TEXT,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP,
updated_at TIMESTAMP,
UNIQUE(vendor_id, template_key, language)
);
```
## Rendering Flow
1. Email service receives request (e.g., send order confirmation)
2. Check for vendor override by `(vendor_id, template_key, language)`
3. If no override, use platform default by `(template_key, language)`
4. Render template with context variables
5. Send via email service
## API Endpoints (Proposed)
### Admin
- `GET /admin/email-templates` - List all platform templates
- `GET /admin/email-templates/{key}` - Get template by key
- `PUT /admin/email-templates/{key}` - Update platform template
- `POST /admin/email-templates/{key}/preview` - Preview template
### Vendor
- `GET /vendor/email-templates` - List available templates with override status
- `GET /vendor/email-templates/{key}` - Get template (override or platform)
- `PUT /vendor/email-templates/{key}` - Create/update override
- `DELETE /vendor/email-templates/{key}` - Remove override (revert to platform)
- `POST /vendor/email-templates/{key}/preview` - Preview with vendor context
## UI Considerations
- Admin: Template editor with WYSIWYG or code view
- Vendor: Simple override interface showing platform default as reference
- Placeholder validation on save
- Preview with sample data
## Notes
- Email templates feature is NOT part of the current settings page
- Contact support messaging in settings directs vendors to admin
- Full implementation to follow CMS pages pattern

View File

@@ -168,6 +168,7 @@ nav:
- OMS Feature Plan: implementation/oms-feature-plan.md - OMS Feature Plan: implementation/oms-feature-plan.md
- Vendor Frontend Parity: implementation/vendor-frontend-parity-plan.md - Vendor Frontend Parity: implementation/vendor-frontend-parity-plan.md
- Stock Management Integration: implementation/stock-management-integration.md - Stock Management Integration: implementation/stock-management-integration.md
- Email Templates Architecture: implementation/email-templates-architecture.md
# --- Testing --- # --- Testing ---
- Testing: - Testing:

View File

@@ -93,6 +93,7 @@ const Icons = {
'chat': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/></svg>`, 'chat': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/></svg>`,
'bell': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/></svg>`, 'bell': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/></svg>`,
'inbox': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"/></svg>`, 'inbox': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"/></svg>`,
'paper-airplane': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"/></svg>`,
// Files & Documents // Files & Documents
'document': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>`, 'document': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>`,
@@ -119,6 +120,7 @@ const Icons = {
// Location // Location
'location-marker': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/></svg>`, 'location-marker': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/></svg>`,
'globe': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>`, 'globe': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>`,
'globe-alt': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"/></svg>`,
// Status & Indicators // Status & Indicators
'exclamation': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>`, 'exclamation': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>`,

View File

@@ -24,7 +24,7 @@ function vendorSettings() {
error: '', error: '',
saving: false, saving: false,
// Settings data // Settings data from API
settings: null, settings: null,
// Active section // Active section
@@ -33,7 +33,13 @@ function vendorSettings() {
// Sections for navigation // Sections for navigation
sections: [ sections: [
{ id: 'general', label: 'General', icon: 'cog' }, { id: 'general', label: 'General', icon: 'cog' },
{ id: 'business', label: 'Business Info', icon: 'office-building' },
{ id: 'localization', label: 'Localization', icon: 'globe' },
{ id: 'marketplace', label: 'Marketplace', icon: 'shopping-cart' }, { id: 'marketplace', label: 'Marketplace', icon: 'shopping-cart' },
{ id: 'invoices', label: 'Invoices', icon: 'document-text' },
{ id: 'branding', label: 'Branding', icon: 'color-swatch' },
{ id: 'domains', label: 'Domains', icon: 'globe-alt' },
{ id: 'api', label: 'API & Payments', icon: 'key' },
{ id: 'notifications', label: 'Notifications', icon: 'bell' } { id: 'notifications', label: 'Notifications', icon: 'bell' }
], ],
@@ -43,10 +49,36 @@ function vendorSettings() {
is_active: true is_active: true
}, },
businessForm: {
name: '',
description: '',
contact_email: '',
contact_phone: '',
website: '',
business_address: '',
tax_number: ''
},
// Track which fields are inherited from company
businessInherited: {
contact_email: false,
contact_phone: false,
website: false,
business_address: false,
tax_number: false
},
// Company name for display
companyName: '',
marketplaceForm: { marketplaceForm: {
letzshop_csv_url_fr: '', letzshop_csv_url_fr: '',
letzshop_csv_url_en: '', letzshop_csv_url_en: '',
letzshop_csv_url_de: '' letzshop_csv_url_de: '',
letzshop_default_tax_rate: null,
letzshop_boost_sort: '',
letzshop_delivery_method: '',
letzshop_preorder_days: null
}, },
notificationForm: { notificationForm: {
@@ -55,8 +87,19 @@ function vendorSettings() {
marketing_emails: false marketing_emails: false
}, },
// Track changes localizationForm: {
default_language: 'fr',
dashboard_language: 'fr',
storefront_language: 'fr',
storefront_languages: ['fr', 'de', 'en'],
storefront_locale: ''
},
// Track changes per section
hasChanges: false, hasChanges: false,
hasBusinessChanges: false,
hasLocalizationChanges: false,
hasMarketplaceChanges: false,
async init() { async init() {
vendorSettingsLog.info('Settings init() called'); vendorSettingsLog.info('Settings init() called');
@@ -96,19 +139,60 @@ function vendorSettings() {
this.settings = response; this.settings = response;
// Populate forms // Populate general form
this.generalForm = { this.generalForm = {
subdomain: response.subdomain || '', subdomain: response.subdomain || '',
is_active: response.is_active !== false is_active: response.is_active !== false
}; };
this.marketplaceForm = { // Populate business info form with inheritance tracking
letzshop_csv_url_fr: response.letzshop_csv_url_fr || '', const biz = response.business_info || {};
letzshop_csv_url_en: response.letzshop_csv_url_en || '', this.businessForm = {
letzshop_csv_url_de: response.letzshop_csv_url_de || '' name: response.name || '',
description: response.description || '',
contact_email: biz.contact_email_override || '',
contact_phone: biz.contact_phone_override || '',
website: biz.website_override || '',
business_address: biz.business_address_override || '',
tax_number: biz.tax_number_override || ''
};
this.businessInherited = {
contact_email: biz.contact_email_inherited || false,
contact_phone: biz.contact_phone_inherited || false,
website: biz.website_inherited || false,
business_address: biz.business_address_inherited || false,
tax_number: biz.tax_number_inherited || false
};
this.companyName = biz.company_name || '';
// Populate localization form from nested structure
const loc = response.localization || {};
this.localizationForm = {
default_language: loc.default_language || 'fr',
dashboard_language: loc.dashboard_language || 'fr',
storefront_language: loc.storefront_language || 'fr',
storefront_languages: loc.storefront_languages || ['fr', 'de', 'en'],
storefront_locale: loc.storefront_locale || ''
}; };
// Populate marketplace form from nested structure
const lz = response.letzshop || {};
this.marketplaceForm = {
letzshop_csv_url_fr: lz.csv_url_fr || '',
letzshop_csv_url_en: lz.csv_url_en || '',
letzshop_csv_url_de: lz.csv_url_de || '',
letzshop_default_tax_rate: lz.default_tax_rate,
letzshop_boost_sort: lz.boost_sort || '',
letzshop_delivery_method: lz.delivery_method || '',
letzshop_preorder_days: lz.preorder_days
};
// Reset all change flags
this.hasChanges = false; this.hasChanges = false;
this.hasBusinessChanges = false;
this.hasLocalizationChanges = false;
this.hasMarketplaceChanges = false;
vendorSettingsLog.info('Loaded settings'); vendorSettingsLog.info('Loaded settings');
} catch (error) { } catch (error) {
vendorSettingsLog.error('Failed to load settings:', error); vendorSettingsLog.error('Failed to load settings:', error);
@@ -119,24 +203,111 @@ function vendorSettings() {
}, },
/** /**
* Mark form as changed * Mark general form as changed
*/ */
markChanged() { markChanged() {
this.hasChanges = true; this.hasChanges = true;
}, },
/** /**
* Save marketplace settings * Mark business form as changed
*/
markBusinessChanged() {
this.hasBusinessChanges = true;
},
/**
* Mark localization form as changed
*/
markLocalizationChanged() {
this.hasLocalizationChanges = true;
},
/**
* Mark marketplace form as changed
*/
markMarketplaceChanged() {
this.hasMarketplaceChanges = true;
},
/**
* Get effective value for a business field (override or inherited)
*/
getEffectiveBusinessValue(field) {
const override = this.businessForm[field];
if (override) return override;
// Return the effective value from settings (includes company inheritance)
return this.settings?.business_info?.[field] || '';
},
/**
* Check if field is using inherited value
*/
isFieldInherited(field) {
return this.businessInherited[field] && !this.businessForm[field];
},
/**
* Reset a business field to inherit from company
*/
resetToCompany(field) {
this.businessForm[field] = '';
this.hasBusinessChanges = true;
},
/**
* Save business info
*/
async saveBusinessInfo() {
this.saving = true;
try {
// Determine which fields should be reset to company values
const resetFields = [];
for (const field of ['contact_email', 'contact_phone', 'website', 'business_address', 'tax_number']) {
if (!this.businessForm[field] && this.settings?.business_info?.[field]) {
resetFields.push(field);
}
}
const payload = {
name: this.businessForm.name,
description: this.businessForm.description,
contact_email: this.businessForm.contact_email || null,
contact_phone: this.businessForm.contact_phone || null,
website: this.businessForm.website || null,
business_address: this.businessForm.business_address || null,
tax_number: this.businessForm.tax_number || null,
reset_to_company: resetFields
};
await apiClient.put(`/vendor/settings/business-info`, payload);
Utils.showToast('Business info saved', 'success');
vendorSettingsLog.info('Business info updated');
// Reload to get updated inheritance flags
await this.loadSettings();
this.hasBusinessChanges = false;
} catch (error) {
vendorSettingsLog.error('Failed to save business info:', error);
Utils.showToast(error.message || 'Failed to save business info', 'error');
} finally {
this.saving = false;
}
},
/**
* Save marketplace settings (Letzshop)
*/ */
async saveMarketplaceSettings() { async saveMarketplaceSettings() {
this.saving = true; this.saving = true;
try { try {
await apiClient.put(`/vendor/settings/marketplace`, this.marketplaceForm); await apiClient.put(`/vendor/settings/letzshop`, this.marketplaceForm);
Utils.showToast('Marketplace settings saved', 'success'); Utils.showToast('Marketplace settings saved', 'success');
vendorSettingsLog.info('Marketplace settings updated'); vendorSettingsLog.info('Marketplace settings updated');
this.hasChanges = false; this.hasMarketplaceChanges = false;
} catch (error) { } catch (error) {
vendorSettingsLog.error('Failed to save marketplace settings:', error); vendorSettingsLog.error('Failed to save marketplace settings:', error);
Utils.showToast(error.message || 'Failed to save settings', 'error'); Utils.showToast(error.message || 'Failed to save settings', 'error');
@@ -179,6 +350,39 @@ function vendorSettings() {
*/ */
setSection(sectionId) { setSection(sectionId) {
this.activeSection = sectionId; this.activeSection = sectionId;
},
/**
* Toggle a storefront language
*/
toggleStorefrontLanguage(langCode) {
const index = this.localizationForm.storefront_languages.indexOf(langCode);
if (index === -1) {
this.localizationForm.storefront_languages.push(langCode);
} else {
this.localizationForm.storefront_languages.splice(index, 1);
}
this.hasLocalizationChanges = true;
},
/**
* Save localization settings
*/
async saveLocalizationSettings() {
this.saving = true;
try {
await apiClient.put(`/vendor/settings/localization`, this.localizationForm);
Utils.showToast('Localization settings saved', 'success');
vendorSettingsLog.info('Localization settings updated');
this.hasLocalizationChanges = false;
} catch (error) {
vendorSettingsLog.error('Failed to save localization settings:', error);
Utils.showToast(error.message || 'Failed to save settings', 'error');
} finally {
this.saving = false;
}
} }
}; };
} }