// static/admin/js/vendor-product-create.js /** * Admin vendor product create page logic * Create new vendor product entries with translations */ const adminVendorProductCreateLog = window.LogConfig.loggers.adminVendorProductCreate || window.LogConfig.createLogger('adminVendorProductCreate', false); adminVendorProductCreateLog.info('Loading...'); function adminVendorProductCreate() { adminVendorProductCreateLog.info('adminVendorProductCreate() called'); // Default translations structure const defaultTranslations = () => ({ en: { title: '', description: '' }, fr: { title: '', description: '' }, de: { title: '', description: '' }, lu: { title: '', description: '' } }); return { // Inherit base layout state ...data(), // Include media picker functionality (vendor ID getter will be bound via loadMediaLibrary override) ...mediaPickerMixin(() => null, false), // Set page identifier currentPage: 'vendor-products', // Loading states loading: false, saving: false, // Tom Select instance vendorSelectInstance: null, // Active language tab activeLanguage: 'en', // Form data form: { vendor_id: null, // Translations by language translations: defaultTranslations(), // Product identifiers vendor_sku: '', brand: '', gtin: '', gtin_type: '', // Pricing price: null, sale_price: null, currency: 'EUR', tax_rate_percent: 17, availability: '', // Images primary_image_url: '', additional_images: [], // Status is_active: true, is_featured: false, is_digital: false }, async init() { adminVendorProductCreateLog.info('Vendor Product Create init() called'); // Guard against multiple initialization if (window._adminVendorProductCreateInitialized) { adminVendorProductCreateLog.warn('Already initialized, skipping'); return; } window._adminVendorProductCreateInitialized = true; // Initialize Tom Select this.initVendorSelect(); adminVendorProductCreateLog.info('Vendor Product Create initialization complete'); }, /** * Initialize Tom Select for vendor autocomplete */ initVendorSelect() { const selectEl = this.$refs.vendorSelect; if (!selectEl) { adminVendorProductCreateLog.warn('Vendor select element not found'); return; } // Wait for Tom Select to be available if (typeof TomSelect === 'undefined') { adminVendorProductCreateLog.warn('TomSelect not loaded, retrying in 100ms'); setTimeout(() => this.initVendorSelect(), 100); return; } this.vendorSelectInstance = new TomSelect(selectEl, { valueField: 'id', labelField: 'name', searchField: ['name', 'vendor_code'], placeholder: 'Search vendor...', load: async (query, callback) => { try { const response = await apiClient.get('/admin/vendors', { search: query, limit: 50 }); callback(response.vendors || []); } catch (error) { adminVendorProductCreateLog.error('Failed to search vendors:', error); callback([]); } }, render: { option: (data, escape) => { return `
${escape(data.name)} ${escape(data.vendor_code || '')}
`; }, item: (data, escape) => { return `
${escape(data.name)}
`; } }, onChange: (value) => { this.form.vendor_id = value ? parseInt(value) : null; } }); adminVendorProductCreateLog.info('Vendor select initialized'); }, /** * Generate a unique vendor SKU * Format: XXXX_XXXX_XXXX (includes vendor_id for uniqueness) */ generateSku() { const vendorId = this.form.vendor_id || 0; // Generate random alphanumeric segments const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; const generateSegment = (length) => { let result = ''; for (let i = 0; i < length; i++) { result += chars.charAt(Math.floor(Math.random() * chars.length)); } return result; }; // First segment includes vendor ID (padded) const vendorSegment = vendorId.toString().padStart(4, '0').slice(-4); // Generate SKU: VID + random + random const sku = `${vendorSegment}_${generateSegment(4)}_${generateSegment(4)}`; this.form.vendor_sku = sku; adminVendorProductCreateLog.info('Generated SKU:', sku); }, /** * Create the product */ async createProduct() { if (!this.form.vendor_id) { Utils.showToast('Please select a vendor', 'error'); return; } if (!this.form.translations.en.title?.trim()) { Utils.showToast('Please enter a product title (English)', 'error'); return; } this.saving = true; try { // Build translations object for API (only include non-empty) const translations = {}; for (const lang of ['en', 'fr', 'de', 'lu']) { const t = this.form.translations[lang]; if (t.title?.trim() || t.description?.trim()) { translations[lang] = { title: t.title?.trim() || null, description: t.description?.trim() || null }; } } // Build create payload const payload = { vendor_id: this.form.vendor_id, translations: Object.keys(translations).length > 0 ? translations : null, // Product identifiers brand: this.form.brand?.trim() || null, vendor_sku: this.form.vendor_sku?.trim() || null, gtin: this.form.gtin?.trim() || null, gtin_type: this.form.gtin_type || null, // Pricing price: this.form.price !== null && this.form.price !== '' ? parseFloat(this.form.price) : null, sale_price: this.form.sale_price !== null && this.form.sale_price !== '' ? parseFloat(this.form.sale_price) : null, currency: this.form.currency || 'EUR', tax_rate_percent: this.form.tax_rate_percent !== null ? parseInt(this.form.tax_rate_percent) : 17, availability: this.form.availability || null, // Images primary_image_url: this.form.primary_image_url?.trim() || null, additional_images: this.form.additional_images?.length > 0 ? this.form.additional_images : null, // Status is_active: this.form.is_active, is_featured: this.form.is_featured, is_digital: this.form.is_digital }; adminVendorProductCreateLog.info('Creating product with payload:', payload); const response = await apiClient.post('/admin/vendor-products', payload); adminVendorProductCreateLog.info('Product created:', response.id); Utils.showToast('Product created successfully', 'success'); // Redirect to the new product's detail page setTimeout(() => { window.location.href = `/admin/vendor-products/${response.id}`; }, 1000); } catch (error) { adminVendorProductCreateLog.error('Failed to create product:', error); Utils.showToast(error.message || 'Failed to create product', 'error'); } finally { this.saving = false; } }, // === Media Picker Overrides === // These override the mixin methods to use proper form context /** * Load media library for the selected vendor */ async loadMediaLibrary() { const vendorId = this.form?.vendor_id; if (!vendorId) { adminVendorProductCreateLog.warn('Media picker: No vendor ID selected'); return; } this.mediaPickerState.loading = true; this.mediaPickerState.skip = 0; try { const params = new URLSearchParams({ skip: '0', limit: this.mediaPickerState.limit.toString(), media_type: 'image', }); if (this.mediaPickerState.search) { params.append('search', this.mediaPickerState.search); } const response = await apiClient.get( `/admin/media/vendors/${vendorId}?${params.toString()}` ); this.mediaPickerState.media = response.media || []; this.mediaPickerState.total = response.total || 0; } catch (error) { adminVendorProductCreateLog.error('Failed to load media library:', error); Utils.showToast('Failed to load media library', 'error'); } finally { this.mediaPickerState.loading = false; } }, /** * Load more media (pagination) */ async loadMoreMedia() { const vendorId = this.form?.vendor_id; if (!vendorId) return; this.mediaPickerState.loading = true; this.mediaPickerState.skip += this.mediaPickerState.limit; try { const params = new URLSearchParams({ skip: this.mediaPickerState.skip.toString(), limit: this.mediaPickerState.limit.toString(), media_type: 'image', }); if (this.mediaPickerState.search) { params.append('search', this.mediaPickerState.search); } const response = await apiClient.get( `/admin/media/vendors/${vendorId}?${params.toString()}` ); this.mediaPickerState.media = [ ...this.mediaPickerState.media, ...(response.media || []) ]; } catch (error) { adminVendorProductCreateLog.error('Failed to load more media:', error); } finally { this.mediaPickerState.loading = false; } }, /** * Upload a new media file */ async uploadMediaFile(event) { const file = event.target.files?.[0]; if (!file) return; const vendorId = this.form?.vendor_id; if (!vendorId) { Utils.showToast('Please select a vendor first', 'error'); return; } if (!file.type.startsWith('image/')) { Utils.showToast('Please select an image file', 'error'); return; } if (file.size > 10 * 1024 * 1024) { Utils.showToast('Image must be less than 10MB', 'error'); return; } this.mediaPickerState.uploading = true; try { const formData = new FormData(); formData.append('file', file); const response = await apiClient.postFormData( `/admin/media/vendors/${vendorId}/upload?folder=products`, formData ); if (response.success && response.media) { this.mediaPickerState.media.unshift(response.media); this.mediaPickerState.total++; this.toggleMediaSelection(response.media); Utils.showToast('Image uploaded successfully', 'success'); } } catch (error) { adminVendorProductCreateLog.error('Failed to upload image:', error); Utils.showToast(error.message || 'Failed to upload image', 'error'); } finally { this.mediaPickerState.uploading = false; event.target.value = ''; } }, /** * Set main image from media picker */ setMainImage(media) { this.form.primary_image_url = media.url; adminVendorProductCreateLog.info('Main image set:', media.url); }, /** * Add additional images from media picker */ addAdditionalImages(mediaList) { const newUrls = mediaList.map(m => m.url); this.form.additional_images = [ ...this.form.additional_images, ...newUrls ]; adminVendorProductCreateLog.info('Additional images added:', newUrls); }, /** * Remove an additional image by index */ removeAdditionalImage(index) { this.form.additional_images.splice(index, 1); }, /** * Clear the main image */ clearMainImage() { this.form.primary_image_url = ''; } }; }