// static/vendor/js/onboarding.js // noqa: js-003 - Standalone page without vendor layout (no base.html extends) // noqa: js-004 - Standalone page has no currentPage sidebar highlight /** * Vendor Onboarding Wizard * * Handles the 4-step mandatory onboarding flow: * 1. Company Profile Setup * 2. Letzshop API Configuration * 3. Product & Order Import Configuration * 4. Order Sync (historical import) */ const onboardingLog = window.LogConfig?.createLogger('ONBOARDING') || console; // 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: 'Find your CSV URL in Letzshop Admin Panel > API > Export Products', 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: 'Trouvez votre URL CSV dans Letzshop Admin > API > Exporter Produits', 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: 'Finden Sie Ihre CSV-URL im Letzshop Admin-Panel > API > Produkte exportieren', 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 (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', completedSteps: 0, status: null, // Form data formData: { // Step 1: Company Profile company_name: '', brand_name: '', description: '', contact_email: '', contact_phone: '', website: '', business_address: '', tax_number: '', default_language: 'fr', dashboard_language: 'fr', // Step 2: Letzshop API api_key: '', shop_slug: '', // Step 3: Product Import csv_url_fr: '', csv_url_en: '', csv_url_de: '', default_tax_rate: 17, delivery_method: 'package_delivery', preorder_days: 1, // Step 4: Order Sync days_back: 90, }, // Letzshop connection test state connectionStatus: null, // null, 'success', 'failed' connectionError: null, // Order sync state syncJobId: null, syncProgress: 0, syncPhase: '', ordersImported: 0, syncComplete: false, syncPollInterval: null, // Computed get currentStepIndex() { return this.steps.findIndex(s => s.id === this.currentStep); }, // Initialize async init() { // Guard against multiple initialization if (window._vendorOnboardingInitialized) return; window._vendorOnboardingInitialized = true; try { await this.loadStatus(); } catch (error) { onboardingLog.error('Failed to initialize onboarding:', error); } }, // Load current onboarding status async loadStatus() { this.loading = true; this.error = null; try { const response = await apiClient.get('/vendor/onboarding/status'); this.status = response; this.currentStep = response.current_step; this.completedSteps = response.completed_steps_count; // Pre-populate form data from status if available if (response.company_profile?.data) { Object.assign(this.formData, response.company_profile.data); } // Check if we were in the middle of an order sync if (response.order_sync?.job_id && this.currentStep === 'order_sync') { this.syncJobId = response.order_sync.job_id; this.startSyncPolling(); } // Load step-specific data await this.loadStepData(); } catch (err) { onboardingLog.error('Failed to load onboarding status:', err); this.error = err.message || 'Failed to load onboarding status'; } finally { this.loading = false; } }, // Load data for current step async loadStepData() { try { if (this.currentStep === 'company_profile') { const data = await apiClient.get('/vendor/onboarding/step/company-profile'); if (data) { Object.assign(this.formData, data); } } else if (this.currentStep === 'product_import') { const data = await apiClient.get('/vendor/onboarding/step/product-import'); if (data) { Object.assign(this.formData, { csv_url_fr: data.csv_url_fr || '', csv_url_en: data.csv_url_en || '', csv_url_de: data.csv_url_de || '', default_tax_rate: data.default_tax_rate || 17, delivery_method: data.delivery_method || 'package_delivery', preorder_days: data.preorder_days || 1, }); } } } catch (err) { onboardingLog.warn('Failed to load step data:', err); } }, // Check if a step is completed isStepCompleted(stepId) { if (!this.status) return false; const stepData = this.status[stepId]; return stepData?.completed === true; }, // Go to previous step goToPreviousStep() { const prevIndex = this.currentStepIndex - 1; if (prevIndex >= 0) { this.currentStep = this.steps[prevIndex].id; this.loadStepData(); } }, // Test Letzshop API connection async testLetzshopApi() { this.testing = true; this.connectionStatus = null; this.connectionError = null; try { const response = await apiClient.post('/vendor/onboarding/step/letzshop-api/test', { api_key: this.formData.api_key, shop_slug: this.formData.shop_slug, }); if (response.success) { this.connectionStatus = 'success'; } else { this.connectionStatus = 'failed'; this.connectionError = response.message; } } catch (err) { this.connectionStatus = 'failed'; this.connectionError = err.message || 'Connection test failed'; } finally { this.testing = false; } }, // Start order sync async startOrderSync() { this.saving = true; try { const response = await apiClient.post('/vendor/onboarding/step/order-sync/trigger', { days_back: parseInt(this.formData.days_back), include_products: true, }); if (response.success && response.job_id) { this.syncJobId = response.job_id; this.startSyncPolling(); } else { throw new Error(response.message || 'Failed to start import'); } } catch (err) { onboardingLog.error('Failed to start order sync:', err); this.error = err.message || 'Failed to start import'; } finally { this.saving = false; } }, // Start polling for sync progress startSyncPolling() { this.syncPollInterval = setInterval(async () => { await this.pollSyncProgress(); }, 2000); }, // Poll sync progress async pollSyncProgress() { try { const response = await apiClient.get( `/vendor/onboarding/step/order-sync/progress/${this.syncJobId}` ); this.syncProgress = response.progress_percentage || 0; this.syncPhase = this.formatPhase(response.current_phase); this.ordersImported = response.orders_imported || 0; if (response.status === 'completed' || response.status === 'failed') { this.stopSyncPolling(); this.syncComplete = true; this.syncProgress = response.status === 'completed' ? 100 : this.syncProgress; } } catch (err) { onboardingLog.error('Failed to poll sync progress:', err); } }, // Stop sync polling stopSyncPolling() { if (this.syncPollInterval) { clearInterval(this.syncPollInterval); this.syncPollInterval = null; } }, // Format phase for display formatPhase(phase) { const phases = { fetching: 'Fetching orders from Letzshop...', orders: 'Processing orders...', products: 'Importing products...', finalizing: 'Finalizing import...', complete: 'Import complete!', }; return phases[phase] || 'Processing...'; }, // Save current step and continue async saveAndContinue() { this.saving = true; this.error = null; try { let endpoint = ''; let payload = {}; switch (this.currentStep) { case 'company_profile': endpoint = '/vendor/onboarding/step/company-profile'; payload = { company_name: this.formData.company_name, brand_name: this.formData.brand_name, description: this.formData.description, contact_email: this.formData.contact_email, contact_phone: this.formData.contact_phone, website: this.formData.website, business_address: this.formData.business_address, tax_number: this.formData.tax_number, default_language: this.formData.default_language, dashboard_language: this.formData.dashboard_language, }; break; case 'letzshop_api': endpoint = '/vendor/onboarding/step/letzshop-api'; payload = { api_key: this.formData.api_key, shop_slug: this.formData.shop_slug, }; break; case 'product_import': endpoint = '/vendor/onboarding/step/product-import'; payload = { csv_url_fr: this.formData.csv_url_fr || null, csv_url_en: this.formData.csv_url_en || null, csv_url_de: this.formData.csv_url_de || null, default_tax_rate: parseInt(this.formData.default_tax_rate), delivery_method: this.formData.delivery_method, preorder_days: parseInt(this.formData.preorder_days), }; break; case 'order_sync': // Complete onboarding endpoint = '/vendor/onboarding/step/order-sync/complete'; payload = { job_id: this.syncJobId, }; break; } const response = await apiClient.post(endpoint, payload); if (!response.success) { throw new Error(response.message || 'Save failed'); } // Handle completion if (response.onboarding_completed || response.redirect_url) { // Redirect to dashboard window.location.href = response.redirect_url || window.location.pathname.replace('/onboarding', '/dashboard'); return; } // Move to next step if (response.next_step) { this.currentStep = response.next_step; this.completedSteps++; await this.loadStepData(); } } catch (err) { onboardingLog.error('Failed to save step:', err); this.error = err.message || 'Failed to save. Please try again.'; } finally { this.saving = false; } }, // Logout handler async handleLogout() { onboardingLog.info('Logging out from onboarding...'); // Get vendor code from URL const path = window.location.pathname; const segments = path.split('/').filter(Boolean); const vendorCode = segments[0] === 'vendor' && segments[1] ? segments[1] : ''; try { // Call logout API await apiClient.post('/vendor/auth/logout'); onboardingLog.info('Logout API called successfully'); } catch (error) { onboardingLog.warn('Logout API error (continuing anyway):', error); } finally { // Clear vendor tokens only (not admin or customer tokens) onboardingLog.info('Clearing vendor tokens...'); localStorage.removeItem('vendor_token'); localStorage.removeItem('vendor_user'); localStorage.removeItem('currentUser'); localStorage.removeItem('vendorCode'); // Note: Do NOT use localStorage.clear() - it would clear admin/customer tokens too onboardingLog.info('Redirecting to login...'); window.location.href = `/vendor/${vendorCode}/login`; } }, // Dark mode get dark() { return localStorage.getItem('dark') === 'true' || (!localStorage.getItem('dark') && window.matchMedia('(prefers-color-scheme: dark)').matches); }, }; }