feat: add mandatory vendor onboarding wizard
Implement 4-step onboarding flow for new vendors after signup: - Step 1: Company profile setup - Step 2: Letzshop API configuration with connection testing - Step 3: Product & order import CSV URL configuration - Step 4: Historical order sync with progress bar Key features: - Blocks dashboard access until completed - Step indicators with visual progress - Resume capability (progress persisted in DB) - Admin skip capability for support cases 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
346
static/vendor/js/onboarding.js
vendored
Normal file
346
static/vendor/js/onboarding.js
vendored
Normal file
@@ -0,0 +1,346 @@
|
||||
// static/vendor/js/onboarding.js
|
||||
/**
|
||||
* 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)
|
||||
*/
|
||||
|
||||
function vendorOnboarding() {
|
||||
return {
|
||||
// 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' },
|
||||
],
|
||||
|
||||
// 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() {
|
||||
await this.loadStatus();
|
||||
},
|
||||
|
||||
// Load current onboarding status
|
||||
async loadStatus() {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
const response = await window.apiClient.get('/api/v1/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) {
|
||||
console.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 window.apiClient.get('/api/v1/vendor/onboarding/step/company-profile');
|
||||
if (data) {
|
||||
Object.assign(this.formData, data);
|
||||
}
|
||||
} else if (this.currentStep === 'product_import') {
|
||||
const data = await window.apiClient.get('/api/v1/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) {
|
||||
console.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 window.apiClient.post('/api/v1/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 window.apiClient.post('/api/v1/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) {
|
||||
console.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 window.apiClient.get(
|
||||
`/api/v1/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) {
|
||||
console.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 = '/api/v1/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 = '/api/v1/vendor/onboarding/step/letzshop-api';
|
||||
payload = {
|
||||
api_key: this.formData.api_key,
|
||||
shop_slug: this.formData.shop_slug,
|
||||
};
|
||||
break;
|
||||
|
||||
case 'product_import':
|
||||
endpoint = '/api/v1/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 = '/api/v1/vendor/onboarding/step/order-sync/complete';
|
||||
payload = {
|
||||
job_id: this.syncJobId,
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
const response = await window.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) {
|
||||
console.error('Failed to save step:', err);
|
||||
this.error = err.message || 'Failed to save. Please try again.';
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Dark mode
|
||||
get dark() {
|
||||
return localStorage.getItem('dark') === 'true' ||
|
||||
(!localStorage.getItem('dark') && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user