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:
423
app/api/v1/vendor/settings.py
vendored
423
app/api/v1/vendor/settings.py
vendored
@@ -8,51 +8,440 @@ The get_current_vendor_api dependency guarantees token_vendor_id is present.
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api
|
||||
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 models.database.user import User
|
||||
|
||||
router = APIRouter(prefix="/settings")
|
||||
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("")
|
||||
def get_vendor_settings(
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
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)
|
||||
|
||||
# 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 {
|
||||
# General info
|
||||
"vendor_code": vendor.vendor_code,
|
||||
"subdomain": vendor.subdomain,
|
||||
"name": vendor.name,
|
||||
"contact_email": vendor.contact_email,
|
||||
"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,
|
||||
"description": vendor.description,
|
||||
"is_active": vendor.is_active,
|
||||
"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")
|
||||
def update_marketplace_settings(
|
||||
marketplace_config: dict,
|
||||
@router.put("/business-info")
|
||||
def update_business_info(
|
||||
business_info: BusinessInfoUpdate,
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update marketplace integration settings."""
|
||||
# Service handles permission checking and raises InsufficientPermissionsException if needed
|
||||
return vendor_service.update_marketplace_settings(
|
||||
db, current_user.token_vendor_id, marketplace_config, current_user
|
||||
"""
|
||||
Update vendor business info.
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
803
app/templates/vendor/settings.html
vendored
803
app/templates/vendor/settings.html
vendored
@@ -18,15 +18,15 @@
|
||||
|
||||
<!-- Settings Content -->
|
||||
<div x-show="!loading && !error" class="w-full mb-8">
|
||||
<div class="flex flex-col md:flex-row gap-6">
|
||||
<!-- Settings Navigation -->
|
||||
<div class="w-full md:w-64 flex-shrink-0">
|
||||
<div class="bg-white rounded-lg shadow-xs dark:bg-gray-800 p-2">
|
||||
<div class="flex flex-col lg:flex-row gap-6">
|
||||
<!-- Settings Navigation (Sidebar) -->
|
||||
<div class="w-full lg:w-56 flex-shrink-0">
|
||||
<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">
|
||||
<button
|
||||
@click="setSection(section.id)"
|
||||
: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-gray-600 hover:bg-gray-50 dark:text-gray-400 dark:hover:bg-gray-700': activeSection !== section.id
|
||||
}"
|
||||
@@ -38,8 +38,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Panels -->
|
||||
<div class="flex-1">
|
||||
<!-- Settings Panels (Main Content) -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<!-- General Settings -->
|
||||
<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">
|
||||
@@ -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"
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<!-- 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 -->
|
||||
<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">
|
||||
<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 class="p-4">
|
||||
<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 -->
|
||||
<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>
|
||||
@@ -122,7 +494,7 @@
|
||||
<input
|
||||
type="url"
|
||||
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"
|
||||
placeholder="https://..."
|
||||
/>
|
||||
@@ -145,7 +517,7 @@
|
||||
<input
|
||||
type="url"
|
||||
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"
|
||||
placeholder="https://..."
|
||||
/>
|
||||
@@ -168,7 +540,7 @@
|
||||
<input
|
||||
type="url"
|
||||
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"
|
||||
placeholder="https://..."
|
||||
/>
|
||||
@@ -181,17 +553,406 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Save Button -->
|
||||
<div class="flex justify-end mt-4 pt-4 border-t dark:border-gray-600">
|
||||
<button
|
||||
@click="saveMarketplaceSettings()"
|
||||
:disabled="saving || !hasChanges"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700 disabled:opacity-50"
|
||||
<!-- 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"
|
||||
>
|
||||
<span x-show="!saving">Save Marketplace Settings</span>
|
||||
<span x-show="saving">Saving...</span>
|
||||
</button>
|
||||
<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 -->
|
||||
<div class="flex justify-end pt-4 border-t dark:border-gray-600">
|
||||
<button
|
||||
@click="saveMarketplaceSettings()"
|
||||
: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"
|
||||
>
|
||||
<span x-show="!saving">Save Marketplace Settings</span>
|
||||
<span x-show="saving">Saving...</span>
|
||||
</button>
|
||||
</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>
|
||||
|
||||
107
docs/implementation/email-templates-architecture.md
Normal file
107
docs/implementation/email-templates-architecture.md
Normal 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
|
||||
@@ -168,6 +168,7 @@ nav:
|
||||
- OMS Feature Plan: implementation/oms-feature-plan.md
|
||||
- Vendor Frontend Parity: implementation/vendor-frontend-parity-plan.md
|
||||
- Stock Management Integration: implementation/stock-management-integration.md
|
||||
- Email Templates Architecture: implementation/email-templates-architecture.md
|
||||
|
||||
# --- Testing ---
|
||||
- Testing:
|
||||
|
||||
@@ -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>`,
|
||||
'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>`,
|
||||
'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
|
||||
'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-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-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
|
||||
'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>`,
|
||||
|
||||
228
static/vendor/js/settings.js
vendored
228
static/vendor/js/settings.js
vendored
@@ -24,7 +24,7 @@ function vendorSettings() {
|
||||
error: '',
|
||||
saving: false,
|
||||
|
||||
// Settings data
|
||||
// Settings data from API
|
||||
settings: null,
|
||||
|
||||
// Active section
|
||||
@@ -33,7 +33,13 @@ function vendorSettings() {
|
||||
// Sections for navigation
|
||||
sections: [
|
||||
{ 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: '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' }
|
||||
],
|
||||
|
||||
@@ -43,10 +49,36 @@ function vendorSettings() {
|
||||
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: {
|
||||
letzshop_csv_url_fr: '',
|
||||
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: {
|
||||
@@ -55,8 +87,19 @@ function vendorSettings() {
|
||||
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,
|
||||
hasBusinessChanges: false,
|
||||
hasLocalizationChanges: false,
|
||||
hasMarketplaceChanges: false,
|
||||
|
||||
async init() {
|
||||
vendorSettingsLog.info('Settings init() called');
|
||||
@@ -96,19 +139,60 @@ function vendorSettings() {
|
||||
|
||||
this.settings = response;
|
||||
|
||||
// Populate forms
|
||||
// Populate general form
|
||||
this.generalForm = {
|
||||
subdomain: response.subdomain || '',
|
||||
is_active: response.is_active !== false
|
||||
};
|
||||
|
||||
this.marketplaceForm = {
|
||||
letzshop_csv_url_fr: response.letzshop_csv_url_fr || '',
|
||||
letzshop_csv_url_en: response.letzshop_csv_url_en || '',
|
||||
letzshop_csv_url_de: response.letzshop_csv_url_de || ''
|
||||
// Populate business info form with inheritance tracking
|
||||
const biz = response.business_info || {};
|
||||
this.businessForm = {
|
||||
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.hasBusinessChanges = false;
|
||||
this.hasLocalizationChanges = false;
|
||||
this.hasMarketplaceChanges = false;
|
||||
|
||||
vendorSettingsLog.info('Loaded settings');
|
||||
} catch (error) {
|
||||
vendorSettingsLog.error('Failed to load settings:', error);
|
||||
@@ -119,24 +203,111 @@ function vendorSettings() {
|
||||
},
|
||||
|
||||
/**
|
||||
* Mark form as changed
|
||||
* Mark general form as changed
|
||||
*/
|
||||
markChanged() {
|
||||
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() {
|
||||
this.saving = true;
|
||||
try {
|
||||
await apiClient.put(`/vendor/settings/marketplace`, this.marketplaceForm);
|
||||
await apiClient.put(`/vendor/settings/letzshop`, this.marketplaceForm);
|
||||
|
||||
Utils.showToast('Marketplace settings saved', 'success');
|
||||
vendorSettingsLog.info('Marketplace settings updated');
|
||||
|
||||
this.hasChanges = false;
|
||||
this.hasMarketplaceChanges = false;
|
||||
} catch (error) {
|
||||
vendorSettingsLog.error('Failed to save marketplace settings:', error);
|
||||
Utils.showToast(error.message || 'Failed to save settings', 'error');
|
||||
@@ -179,6 +350,39 @@ function vendorSettings() {
|
||||
*/
|
||||
setSection(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;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user