feat: add multilingual support to vendor onboarding workflow
- Add language selector with English, French, and German translations - Fix API key help text to reference Letzshop Support team - Update shop slug input with URL prefix and clearer example - Fix step validation bug by adding db.commit() to all POST endpoints - Translate all form labels, buttons, and messages 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
20
app/api/v1/vendor/onboarding.py
vendored
20
app/api/v1/vendor/onboarding.py
vendored
@@ -92,7 +92,7 @@ def save_company_profile(
|
||||
Updates vendor and company records with provided data.
|
||||
"""
|
||||
service = OnboardingService(db)
|
||||
return service.complete_company_profile(
|
||||
result = service.complete_company_profile(
|
||||
vendor_id=current_user.token_vendor_id,
|
||||
company_name=request.company_name,
|
||||
brand_name=request.brand_name,
|
||||
@@ -105,6 +105,8 @@ def save_company_profile(
|
||||
default_language=request.default_language,
|
||||
dashboard_language=request.dashboard_language,
|
||||
)
|
||||
db.commit() # Commit at API level for transaction control
|
||||
return result
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -142,12 +144,14 @@ def save_letzshop_api(
|
||||
Tests connection first, only saves if successful.
|
||||
"""
|
||||
service = OnboardingService(db)
|
||||
return service.complete_letzshop_api(
|
||||
result = service.complete_letzshop_api(
|
||||
vendor_id=current_user.token_vendor_id,
|
||||
api_key=request.api_key,
|
||||
shop_slug=request.shop_slug,
|
||||
letzshop_vendor_id=request.vendor_id,
|
||||
)
|
||||
db.commit() # Commit at API level for transaction control
|
||||
return result
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -181,7 +185,7 @@ def save_product_import_config(
|
||||
At least one CSV URL must be provided.
|
||||
"""
|
||||
service = OnboardingService(db)
|
||||
return service.complete_product_import(
|
||||
result = service.complete_product_import(
|
||||
vendor_id=current_user.token_vendor_id,
|
||||
csv_url_fr=request.csv_url_fr,
|
||||
csv_url_en=request.csv_url_en,
|
||||
@@ -190,6 +194,8 @@ def save_product_import_config(
|
||||
delivery_method=request.delivery_method,
|
||||
preorder_days=request.preorder_days,
|
||||
)
|
||||
db.commit() # Commit at API level for transaction control
|
||||
return result
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -209,12 +215,14 @@ def trigger_order_sync(
|
||||
Creates a background job that imports orders from Letzshop.
|
||||
"""
|
||||
service = OnboardingService(db)
|
||||
return service.trigger_order_sync(
|
||||
result = service.trigger_order_sync(
|
||||
vendor_id=current_user.token_vendor_id,
|
||||
user_id=current_user.id,
|
||||
days_back=request.days_back,
|
||||
include_products=request.include_products,
|
||||
)
|
||||
db.commit() # Commit at API level for transaction control
|
||||
return result
|
||||
|
||||
|
||||
@router.get(
|
||||
@@ -251,7 +259,9 @@ def complete_order_sync(
|
||||
This also marks the entire onboarding as complete.
|
||||
"""
|
||||
service = OnboardingService(db)
|
||||
return service.complete_order_sync(
|
||||
result = service.complete_order_sync(
|
||||
vendor_id=current_user.token_vendor_id,
|
||||
job_id=request.job_id,
|
||||
)
|
||||
db.commit() # Commit at API level for transaction control
|
||||
return result
|
||||
|
||||
163
app/templates/vendor/onboarding.html
vendored
163
app/templates/vendor/onboarding.html
vendored
@@ -23,8 +23,33 @@
|
||||
</div>
|
||||
<span class="text-xl font-semibold text-gray-800 dark:text-white">Wizamart</span>
|
||||
</div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||
<span x-text="completedSteps"></span> of 4 steps completed
|
||||
<div class="flex items-center space-x-4">
|
||||
<!-- Language Selector -->
|
||||
<div class="relative" x-data="{ open: false }">
|
||||
<button @click="open = !open"
|
||||
class="flex items-center space-x-2 px-3 py-2 rounded-lg bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors">
|
||||
<span x-text="languageFlags[lang]"></span>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300" x-text="languageNames[lang]"></span>
|
||||
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div x-show="open" @click.away="open = false" x-cloak
|
||||
class="absolute right-0 mt-2 w-40 bg-white dark:bg-gray-700 rounded-lg shadow-lg border border-gray-200 dark:border-gray-600 z-50">
|
||||
<template x-for="langCode in availableLanguages" :key="langCode">
|
||||
<button @click="setLang(langCode); open = false"
|
||||
class="w-full flex items-center space-x-2 px-4 py-2 text-left hover:bg-gray-50 dark:hover:bg-gray-600 first:rounded-t-lg last:rounded-b-lg"
|
||||
:class="{ 'bg-purple-50 dark:bg-purple-900/20': lang === langCode }">
|
||||
<span x-text="languageFlags[langCode]"></span>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300" x-text="languageNames[langCode]"></span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Progress -->
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||
<span x-text="completedSteps"></span> / 4
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -74,15 +99,14 @@
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<p class="mt-4 text-gray-600 dark:text-gray-400">Loading your setup...</p>
|
||||
<p class="mt-4 text-gray-600 dark:text-gray-400" x-text="t('loading')"></p>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div x-show="error && !loading" class="p-6">
|
||||
<div class="bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-lg p-4 text-center">
|
||||
<p class="text-red-600 dark:text-red-400" x-text="error"></p>
|
||||
<button @click="loadStatus()" class="mt-4 px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-lg hover:bg-red-700">
|
||||
Retry
|
||||
<button @click="loadStatus()" class="mt-4 px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-lg hover:bg-red-700" x-text="t('buttons.retry')">
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -92,77 +116,75 @@
|
||||
<!-- Step 1: Company Profile -->
|
||||
<div x-show="currentStep === 'company_profile'" x-transition>
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<h2 class="text-xl font-semibold text-gray-800 dark:text-white">Company Profile</h2>
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
Let's set up your company information. This will be used for invoices and customer communication.
|
||||
</p>
|
||||
<h2 class="text-xl font-semibold text-gray-800 dark:text-white" x-text="t('step1.title')"></h2>
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400" x-text="t('step1.description')"></p>
|
||||
</div>
|
||||
<div class="p-6 space-y-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Company Name</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300" x-text="t('step1.company_name')"></label>
|
||||
<input type="text" x-model="formData.company_name"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-purple-500 focus:ring-purple-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Brand Name</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300" x-text="t('step1.brand_name')"></label>
|
||||
<input type="text" x-model="formData.brand_name"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-purple-500 focus:ring-purple-500" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Description</label>
|
||||
<textarea x-model="formData.description" rows="3"
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300" x-text="t('step1.description_label')"></label>
|
||||
<textarea x-model="formData.description" rows="3" :placeholder="t('step1.description_placeholder')"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-purple-500 focus:ring-purple-500"></textarea>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Contact Email</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300" x-text="t('step1.contact_email')"></label>
|
||||
<input type="email" x-model="formData.contact_email"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-purple-500 focus:ring-purple-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Contact Phone</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300" x-text="t('step1.contact_phone')"></label>
|
||||
<input type="tel" x-model="formData.contact_phone"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-purple-500 focus:ring-purple-500" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Website</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300" x-text="t('step1.website')"></label>
|
||||
<input type="url" x-model="formData.website"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-purple-500 focus:ring-purple-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Tax Number (VAT)</label>
|
||||
<input type="text" x-model="formData.tax_number"
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300" x-text="t('step1.tax_number')"></label>
|
||||
<input type="text" x-model="formData.tax_number" :placeholder="t('step1.tax_number_placeholder')"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-purple-500 focus:ring-purple-500" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Business Address</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300" x-text="t('step1.business_address')"></label>
|
||||
<textarea x-model="formData.business_address" rows="2"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-purple-500 focus:ring-purple-500"></textarea>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Default Language</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300" x-text="t('step1.default_language')"></label>
|
||||
<select x-model="formData.default_language"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-purple-500 focus:ring-purple-500">
|
||||
<option value="fr">French</option>
|
||||
<option value="fr">Français</option>
|
||||
<option value="en">English</option>
|
||||
<option value="de">German</option>
|
||||
<option value="lb">Luxembourgish</option>
|
||||
<option value="de">Deutsch</option>
|
||||
<option value="lb">Lëtzebuergesch</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Dashboard Language</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300" x-text="t('step1.dashboard_language')"></label>
|
||||
<select x-model="formData.dashboard_language"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-purple-500 focus:ring-purple-500">
|
||||
<option value="fr">French</option>
|
||||
<option value="fr">Français</option>
|
||||
<option value="en">English</option>
|
||||
<option value="de">German</option>
|
||||
<option value="lb">Luxembourgish</option>
|
||||
<option value="de">Deutsch</option>
|
||||
<option value="lb">Lëtzebuergesch</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -172,53 +194,52 @@
|
||||
<!-- Step 2: Letzshop API -->
|
||||
<div x-show="currentStep === 'letzshop_api'" x-transition>
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<h2 class="text-xl font-semibold text-gray-800 dark:text-white">Letzshop API Configuration</h2>
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
Connect your Letzshop marketplace account to sync orders automatically.
|
||||
</p>
|
||||
<h2 class="text-xl font-semibold text-gray-800 dark:text-white" x-text="t('step2.title')"></h2>
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400" x-text="t('step2.description')"></p>
|
||||
</div>
|
||||
<div class="p-6 space-y-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Letzshop API Key</label>
|
||||
<input type="password" x-model="formData.api_key" placeholder="Enter your API key"
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300" x-text="t('step2.api_key')"></label>
|
||||
<input type="password" x-model="formData.api_key" :placeholder="t('step2.api_key_placeholder')"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-purple-500 focus:ring-purple-500" />
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Get your API key from Letzshop Vendor Portal > Settings > API Access
|
||||
<span x-text="t('step2.api_key_help')"></span> (<a href="mailto:support@letzshop.lu" class="text-purple-600 hover:underline">support@letzshop.lu</a>)
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Shop Slug</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300" x-text="t('step2.shop_slug')"></label>
|
||||
<div class="mt-1 flex rounded-md shadow-sm">
|
||||
<span class="inline-flex items-center px-3 rounded-l-md border border-r-0 border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-500 dark:text-gray-400 text-sm">
|
||||
letzshop.lu/vendors/
|
||||
letzshop.lu/.../vendors/
|
||||
</span>
|
||||
<input type="text" x-model="formData.shop_slug" placeholder="your-shop"
|
||||
<input type="text" x-model="formData.shop_slug" placeholder="your-shop-name"
|
||||
class="flex-1 block w-full rounded-none rounded-r-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-purple-500 focus:ring-purple-500" />
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400" x-text="t('step2.shop_slug_help')"></p>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<button @click="testLetzshopApi()" :disabled="saving || !formData.api_key || !formData.shop_slug"
|
||||
class="px-4 py-2 text-sm font-medium text-purple-600 bg-purple-100 rounded-lg hover:bg-purple-200 focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
<span x-show="!testing">Test Connection</span>
|
||||
<span x-show="!testing" x-text="t('step2.test_connection')"></span>
|
||||
<span x-show="testing" class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-2 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Testing...
|
||||
<span x-text="t('step2.testing')"></span>
|
||||
</span>
|
||||
</button>
|
||||
<span x-show="connectionStatus === 'success'" class="text-green-600 dark:text-green-400 text-sm flex items-center">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
Connection successful
|
||||
<span x-text="t('step2.connection_success')"></span>
|
||||
</span>
|
||||
<span x-show="connectionStatus === 'failed'" class="text-red-600 dark:text-red-400 text-sm flex items-center">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
<span x-text="connectionError || 'Connection failed'"></span>
|
||||
<span x-text="connectionError || t('step2.connection_failed')"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -227,24 +248,23 @@
|
||||
<!-- Step 3: Product Import -->
|
||||
<div x-show="currentStep === 'product_import'" x-transition>
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<h2 class="text-xl font-semibold text-gray-800 dark:text-white">Product Import Configuration</h2>
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
Set up your product CSV feed URLs for each language. At least one URL is required.
|
||||
</p>
|
||||
<h2 class="text-xl font-semibold text-gray-800 dark:text-white" x-text="t('step3.title')"></h2>
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400" x-text="t('step3.description')"></p>
|
||||
</div>
|
||||
<div class="p-6 space-y-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">French CSV URL</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300" x-text="t('step3.csv_url_fr')"></label>
|
||||
<input type="url" x-model="formData.csv_url_fr" placeholder="https://..."
|
||||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-purple-500 focus:ring-purple-500" />
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400" x-text="t('step3.csv_url_help')"></p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">English CSV URL (optional)</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300" x-text="t('step3.csv_url_en') + ' (optional)'"></label>
|
||||
<input type="url" x-model="formData.csv_url_en" placeholder="https://..."
|
||||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-purple-500 focus:ring-purple-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">German CSV URL (optional)</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300" x-text="t('step3.csv_url_de') + ' (optional)'"></label>
|
||||
<input type="url" x-model="formData.csv_url_de" placeholder="https://..."
|
||||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-purple-500 focus:ring-purple-500" />
|
||||
</div>
|
||||
@@ -252,7 +272,7 @@
|
||||
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-4">Letzshop Feed Settings</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div>
|
||||
<label class="block text-sm text-gray-600 dark:text-gray-400">Default Tax Rate</label>
|
||||
<label class="block text-sm text-gray-600 dark:text-gray-400" x-text="t('step3.default_tax_rate')"></label>
|
||||
<select x-model="formData.default_tax_rate"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-purple-500 focus:ring-purple-500">
|
||||
<option value="17">17% (Standard)</option>
|
||||
@@ -263,18 +283,19 @@
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-600 dark:text-gray-400">Delivery Method</label>
|
||||
<label class="block text-sm text-gray-600 dark:text-gray-400" x-text="t('step3.delivery_method')"></label>
|
||||
<select x-model="formData.delivery_method"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-purple-500 focus:ring-purple-500">
|
||||
<option value="package_delivery">Package Delivery</option>
|
||||
<option value="self_collect">Self Collect</option>
|
||||
<option value="package_delivery" x-text="t('step3.delivery_package')"></option>
|
||||
<option value="self_collect" x-text="t('step3.delivery_pickup')"></option>
|
||||
<option value="nationwide">Nationwide</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-600 dark:text-gray-400">Pre-order Days</label>
|
||||
<label class="block text-sm text-gray-600 dark:text-gray-400" x-text="t('step3.preorder_days')"></label>
|
||||
<input type="number" x-model="formData.preorder_days" min="0" max="30"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-purple-500 focus:ring-purple-500" />
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400" x-text="t('step3.preorder_days_help')"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -284,28 +305,26 @@
|
||||
<!-- Step 4: Order Sync -->
|
||||
<div x-show="currentStep === 'order_sync'" x-transition>
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<h2 class="text-xl font-semibold text-gray-800 dark:text-white">Historical Order Import</h2>
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
Import your existing orders from Letzshop to get started with a complete order history.
|
||||
</p>
|
||||
<h2 class="text-xl font-semibold text-gray-800 dark:text-white" x-text="t('step4.title')"></h2>
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400" x-text="t('step4.description')"></p>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<!-- Before Sync -->
|
||||
<div x-show="!syncJobId">
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Import orders from the last</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2" x-text="t('step4.days_back')"></label>
|
||||
<select x-model="formData.days_back"
|
||||
class="block w-48 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-purple-500 focus:ring-purple-500">
|
||||
<option value="30">30 days</option>
|
||||
<option value="60">60 days</option>
|
||||
<option value="90">90 days</option>
|
||||
<option value="180">6 months</option>
|
||||
<option value="365">1 year</option>
|
||||
<option value="30">30 <span x-text="t('step4.days')"></span></option>
|
||||
<option value="60">60 <span x-text="t('step4.days')"></span></option>
|
||||
<option value="90">90 <span x-text="t('step4.days')"></span></option>
|
||||
<option value="180">180 <span x-text="t('step4.days')"></span></option>
|
||||
<option value="365">365 <span x-text="t('step4.days')"></span></option>
|
||||
</select>
|
||||
</div>
|
||||
<button @click="startOrderSync()" :disabled="saving"
|
||||
class="px-6 py-3 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700 focus:outline-none disabled:opacity-50">
|
||||
Start Import
|
||||
class="px-6 py-3 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700 focus:outline-none disabled:opacity-50"
|
||||
x-text="saving ? t('step4.importing') : t('step4.start_import')">
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -322,7 +341,7 @@
|
||||
</p>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1" x-text="syncPhase"></p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-500 mt-2">
|
||||
<span x-text="ordersImported"></span> orders imported
|
||||
<span x-text="ordersImported"></span> <span x-text="t('step4.orders_imported')"></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -333,9 +352,9 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="text-lg font-medium text-gray-800 dark:text-white">Import Complete!</p>
|
||||
<p class="text-lg font-medium text-gray-800 dark:text-white" x-text="t('step4.import_complete')"></p>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
<span x-text="ordersImported"></span> orders have been imported.
|
||||
<span x-text="ordersImported"></span> <span x-text="t('step4.orders_imported')"></span>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -345,8 +364,8 @@
|
||||
<div class="p-6 bg-gray-50 dark:bg-gray-700/50 border-t border-gray-200 dark:border-gray-700 flex justify-between">
|
||||
<button x-show="currentStepIndex > 0 && !syncJobId"
|
||||
@click="goToPreviousStep()"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
Previous
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
x-text="t('buttons.back')">
|
||||
</button>
|
||||
<div x-show="currentStepIndex === 0"></div>
|
||||
|
||||
@@ -354,14 +373,14 @@
|
||||
@click="saveAndContinue()" :disabled="saving"
|
||||
class="px-6 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700 focus:outline-none disabled:opacity-50">
|
||||
<span x-show="!saving">
|
||||
<span x-text="currentStep === 'order_sync' && syncComplete ? 'Finish Setup' : 'Save & Continue'"></span>
|
||||
<span x-text="currentStep === 'order_sync' && syncComplete ? t('buttons.complete') : t('buttons.save_continue')"></span>
|
||||
</span>
|
||||
<span x-show="saving" class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-2 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Saving...
|
||||
<span x-text="t('buttons.saving')"></span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
278
static/vendor/js/onboarding.js
vendored
278
static/vendor/js/onboarding.js
vendored
@@ -13,21 +13,283 @@
|
||||
|
||||
const onboardingLog = window.LogConfig?.createLogger('ONBOARDING') || console;
|
||||
|
||||
function vendorOnboarding() {
|
||||
// Onboarding translations
|
||||
const onboardingTranslations = {
|
||||
en: {
|
||||
title: 'Welcome to Wizamart',
|
||||
subtitle: 'Complete these steps to set up your store',
|
||||
steps: {
|
||||
company_profile: 'Company Profile',
|
||||
letzshop_api: 'Letzshop API',
|
||||
product_import: 'Product Import',
|
||||
order_sync: 'Order Sync',
|
||||
},
|
||||
step1: {
|
||||
title: 'Company Profile Setup',
|
||||
description: 'Tell us about your business. This information will be used for invoices and your store profile.',
|
||||
company_name: 'Company Name',
|
||||
brand_name: 'Brand Name',
|
||||
brand_name_help: 'The name customers will see',
|
||||
description_label: 'Description',
|
||||
description_placeholder: 'Brief description of your business',
|
||||
contact_email: 'Contact Email',
|
||||
contact_phone: 'Contact Phone',
|
||||
website: 'Website',
|
||||
business_address: 'Business Address',
|
||||
tax_number: 'Tax Number (VAT)',
|
||||
tax_number_placeholder: 'e.g., LU12345678',
|
||||
default_language: 'Default Shop Language',
|
||||
dashboard_language: 'Dashboard Language',
|
||||
},
|
||||
step2: {
|
||||
title: 'Letzshop API Configuration',
|
||||
description: 'Connect your Letzshop marketplace account to sync orders automatically.',
|
||||
api_key: 'Letzshop API Key',
|
||||
api_key_placeholder: 'Enter your API key',
|
||||
api_key_help: 'Get your API key from Letzshop Support team',
|
||||
shop_slug: 'Shop Slug',
|
||||
shop_slug_help: 'Enter the last part of your Letzshop vendor URL',
|
||||
test_connection: 'Test Connection',
|
||||
testing: 'Testing...',
|
||||
connection_success: 'Connection successful',
|
||||
connection_failed: 'Connection failed',
|
||||
},
|
||||
step3: {
|
||||
title: 'Product Import Configuration',
|
||||
description: 'Configure how products are imported from your CSV feeds.',
|
||||
csv_urls: 'CSV Feed URLs',
|
||||
csv_url_fr: 'French CSV URL',
|
||||
csv_url_en: 'English CSV URL',
|
||||
csv_url_de: 'German CSV URL',
|
||||
csv_url_help: 'At least one CSV URL is required',
|
||||
default_tax_rate: 'Default Tax Rate (%)',
|
||||
delivery_method: 'Delivery Method',
|
||||
delivery_package: 'Package Delivery',
|
||||
delivery_pickup: 'Store Pickup',
|
||||
preorder_days: 'Preorder Days',
|
||||
preorder_days_help: 'Days before product is available after order',
|
||||
},
|
||||
step4: {
|
||||
title: 'Historical Order Import',
|
||||
description: 'Import your existing orders from Letzshop to start managing them in Wizamart.',
|
||||
days_back: 'Import orders from last',
|
||||
days: 'days',
|
||||
start_import: 'Start Import',
|
||||
importing: 'Importing...',
|
||||
import_complete: 'Import Complete!',
|
||||
orders_imported: 'orders imported',
|
||||
skip_step: 'Skip this step',
|
||||
},
|
||||
buttons: {
|
||||
save_continue: 'Save & Continue',
|
||||
saving: 'Saving...',
|
||||
back: 'Back',
|
||||
complete: 'Complete Setup',
|
||||
retry: 'Retry',
|
||||
},
|
||||
loading: 'Loading your setup...',
|
||||
errors: {
|
||||
load_failed: 'Failed to load onboarding status',
|
||||
save_failed: 'Failed to save. Please try again.',
|
||||
},
|
||||
},
|
||||
fr: {
|
||||
title: 'Bienvenue sur Wizamart',
|
||||
subtitle: 'Complétez ces étapes pour configurer votre boutique',
|
||||
steps: {
|
||||
company_profile: 'Profil Entreprise',
|
||||
letzshop_api: 'API Letzshop',
|
||||
product_import: 'Import Produits',
|
||||
order_sync: 'Sync Commandes',
|
||||
},
|
||||
step1: {
|
||||
title: 'Configuration du Profil Entreprise',
|
||||
description: 'Parlez-nous de votre entreprise. Ces informations seront utilisées pour les factures et le profil de votre boutique.',
|
||||
company_name: 'Nom de l\'Entreprise',
|
||||
brand_name: 'Nom de la Marque',
|
||||
brand_name_help: 'Le nom que les clients verront',
|
||||
description_label: 'Description',
|
||||
description_placeholder: 'Brève description de votre activité',
|
||||
contact_email: 'Email de Contact',
|
||||
contact_phone: 'Téléphone de Contact',
|
||||
website: 'Site Web',
|
||||
business_address: 'Adresse Professionnelle',
|
||||
tax_number: 'Numéro de TVA',
|
||||
tax_number_placeholder: 'ex: LU12345678',
|
||||
default_language: 'Langue par Défaut de la Boutique',
|
||||
dashboard_language: 'Langue du Tableau de Bord',
|
||||
},
|
||||
step2: {
|
||||
title: 'Configuration de l\'API Letzshop',
|
||||
description: 'Connectez votre compte Letzshop pour synchroniser automatiquement les commandes.',
|
||||
api_key: 'Clé API Letzshop',
|
||||
api_key_placeholder: 'Entrez votre clé API',
|
||||
api_key_help: 'Obtenez votre clé API auprès de l\'équipe Support Letzshop',
|
||||
shop_slug: 'Identifiant Boutique',
|
||||
shop_slug_help: 'Entrez la dernière partie de votre URL vendeur Letzshop',
|
||||
test_connection: 'Tester la Connexion',
|
||||
testing: 'Test en cours...',
|
||||
connection_success: 'Connexion réussie',
|
||||
connection_failed: 'Échec de la connexion',
|
||||
},
|
||||
step3: {
|
||||
title: 'Configuration Import Produits',
|
||||
description: 'Configurez comment les produits sont importés depuis vos flux CSV.',
|
||||
csv_urls: 'URLs des Flux CSV',
|
||||
csv_url_fr: 'URL CSV Français',
|
||||
csv_url_en: 'URL CSV Anglais',
|
||||
csv_url_de: 'URL CSV Allemand',
|
||||
csv_url_help: 'Au moins une URL CSV est requise',
|
||||
default_tax_rate: 'Taux de TVA par Défaut (%)',
|
||||
delivery_method: 'Méthode de Livraison',
|
||||
delivery_package: 'Livraison Colis',
|
||||
delivery_pickup: 'Retrait en Magasin',
|
||||
preorder_days: 'Jours de Précommande',
|
||||
preorder_days_help: 'Jours avant disponibilité du produit après commande',
|
||||
},
|
||||
step4: {
|
||||
title: 'Import Historique des Commandes',
|
||||
description: 'Importez vos commandes existantes de Letzshop pour commencer à les gérer dans Wizamart.',
|
||||
days_back: 'Importer les commandes des derniers',
|
||||
days: 'jours',
|
||||
start_import: 'Démarrer l\'Import',
|
||||
importing: 'Import en cours...',
|
||||
import_complete: 'Import Terminé !',
|
||||
orders_imported: 'commandes importées',
|
||||
skip_step: 'Passer cette étape',
|
||||
},
|
||||
buttons: {
|
||||
save_continue: 'Enregistrer & Continuer',
|
||||
saving: 'Enregistrement...',
|
||||
back: 'Retour',
|
||||
complete: 'Terminer la Configuration',
|
||||
retry: 'Réessayer',
|
||||
},
|
||||
loading: 'Chargement de votre configuration...',
|
||||
errors: {
|
||||
load_failed: 'Échec du chargement du statut d\'onboarding',
|
||||
save_failed: 'Échec de l\'enregistrement. Veuillez réessayer.',
|
||||
},
|
||||
},
|
||||
de: {
|
||||
title: 'Willkommen bei Wizamart',
|
||||
subtitle: 'Führen Sie diese Schritte aus, um Ihren Shop einzurichten',
|
||||
steps: {
|
||||
company_profile: 'Firmenprofil',
|
||||
letzshop_api: 'Letzshop API',
|
||||
product_import: 'Produktimport',
|
||||
order_sync: 'Bestellsync',
|
||||
},
|
||||
step1: {
|
||||
title: 'Firmenprofil Einrichten',
|
||||
description: 'Erzählen Sie uns von Ihrem Unternehmen. Diese Informationen werden für Rechnungen und Ihr Shop-Profil verwendet.',
|
||||
company_name: 'Firmenname',
|
||||
brand_name: 'Markenname',
|
||||
brand_name_help: 'Der Name, den Kunden sehen werden',
|
||||
description_label: 'Beschreibung',
|
||||
description_placeholder: 'Kurze Beschreibung Ihres Unternehmens',
|
||||
contact_email: 'Kontakt-E-Mail',
|
||||
contact_phone: 'Kontakttelefon',
|
||||
website: 'Website',
|
||||
business_address: 'Geschäftsadresse',
|
||||
tax_number: 'Steuernummer (USt-IdNr.)',
|
||||
tax_number_placeholder: 'z.B. LU12345678',
|
||||
default_language: 'Standard-Shop-Sprache',
|
||||
dashboard_language: 'Dashboard-Sprache',
|
||||
},
|
||||
step2: {
|
||||
title: 'Letzshop API Konfiguration',
|
||||
description: 'Verbinden Sie Ihr Letzshop-Konto, um Bestellungen automatisch zu synchronisieren.',
|
||||
api_key: 'Letzshop API-Schlüssel',
|
||||
api_key_placeholder: 'Geben Sie Ihren API-Schlüssel ein',
|
||||
api_key_help: 'Erhalten Sie Ihren API-Schlüssel vom Letzshop Support-Team',
|
||||
shop_slug: 'Shop-Slug',
|
||||
shop_slug_help: 'Geben Sie den letzten Teil Ihrer Letzshop-Verkäufer-URL ein',
|
||||
test_connection: 'Verbindung Testen',
|
||||
testing: 'Teste...',
|
||||
connection_success: 'Verbindung erfolgreich',
|
||||
connection_failed: 'Verbindung fehlgeschlagen',
|
||||
},
|
||||
step3: {
|
||||
title: 'Produktimport Konfiguration',
|
||||
description: 'Konfigurieren Sie, wie Produkte aus Ihren CSV-Feeds importiert werden.',
|
||||
csv_urls: 'CSV-Feed-URLs',
|
||||
csv_url_fr: 'Französische CSV-URL',
|
||||
csv_url_en: 'Englische CSV-URL',
|
||||
csv_url_de: 'Deutsche CSV-URL',
|
||||
csv_url_help: 'Mindestens eine CSV-URL ist erforderlich',
|
||||
default_tax_rate: 'Standard-Steuersatz (%)',
|
||||
delivery_method: 'Liefermethode',
|
||||
delivery_package: 'Paketlieferung',
|
||||
delivery_pickup: 'Abholung im Geschäft',
|
||||
preorder_days: 'Vorbestelltage',
|
||||
preorder_days_help: 'Tage bis zur Verfügbarkeit nach Bestellung',
|
||||
},
|
||||
step4: {
|
||||
title: 'Historischer Bestellimport',
|
||||
description: 'Importieren Sie Ihre bestehenden Bestellungen von Letzshop, um sie in Wizamart zu verwalten.',
|
||||
days_back: 'Bestellungen der letzten importieren',
|
||||
days: 'Tage',
|
||||
start_import: 'Import Starten',
|
||||
importing: 'Importiere...',
|
||||
import_complete: 'Import Abgeschlossen!',
|
||||
orders_imported: 'Bestellungen importiert',
|
||||
skip_step: 'Diesen Schritt überspringen',
|
||||
},
|
||||
buttons: {
|
||||
save_continue: 'Speichern & Fortfahren',
|
||||
saving: 'Speichern...',
|
||||
back: 'Zurück',
|
||||
complete: 'Einrichtung Abschließen',
|
||||
retry: 'Erneut versuchen',
|
||||
},
|
||||
loading: 'Ihre Einrichtung wird geladen...',
|
||||
errors: {
|
||||
load_failed: 'Onboarding-Status konnte nicht geladen werden',
|
||||
save_failed: 'Speichern fehlgeschlagen. Bitte versuchen Sie es erneut.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function vendorOnboarding(initialLang = 'en') {
|
||||
return {
|
||||
// Language
|
||||
lang: initialLang || localStorage.getItem('onboarding_lang') || 'en',
|
||||
availableLanguages: ['en', 'fr', 'de'],
|
||||
languageNames: { en: 'English', fr: 'Français', de: 'Deutsch' },
|
||||
languageFlags: { en: '🇬🇧', fr: '🇫🇷', de: '🇩🇪' },
|
||||
|
||||
// Translation helper
|
||||
t(key) {
|
||||
const keys = key.split('.');
|
||||
let value = onboardingTranslations[this.lang];
|
||||
for (const k of keys) {
|
||||
value = value?.[k];
|
||||
}
|
||||
return value || key;
|
||||
},
|
||||
|
||||
// Change language
|
||||
setLang(newLang) {
|
||||
this.lang = newLang;
|
||||
localStorage.setItem('onboarding_lang', newLang);
|
||||
},
|
||||
|
||||
// State
|
||||
loading: true,
|
||||
saving: false,
|
||||
testing: false,
|
||||
error: null,
|
||||
|
||||
// Steps configuration
|
||||
steps: [
|
||||
{ id: 'company_profile', title: 'Company Profile' },
|
||||
{ id: 'letzshop_api', title: 'Letzshop API' },
|
||||
{ id: 'product_import', title: 'Product Import' },
|
||||
{ id: 'order_sync', title: 'Order Sync' },
|
||||
],
|
||||
// Steps configuration (will be populated with translated titles)
|
||||
get steps() {
|
||||
return [
|
||||
{ id: 'company_profile', title: this.t('steps.company_profile') },
|
||||
{ id: 'letzshop_api', title: this.t('steps.letzshop_api') },
|
||||
{ id: 'product_import', title: this.t('steps.product_import') },
|
||||
{ id: 'order_sync', title: this.t('steps.order_sync') },
|
||||
];
|
||||
},
|
||||
|
||||
// Current state
|
||||
currentStep: 'company_profile',
|
||||
|
||||
Reference in New Issue
Block a user