// static/vendor/js/inventory.js /** * Vendor inventory management page logic * View and manage stock levels */ const vendorInventoryLog = window.LogConfig.loggers.vendorInventory || window.LogConfig.createLogger('vendorInventory', false); vendorInventoryLog.info('Loading...'); function vendorInventory() { vendorInventoryLog.info('vendorInventory() called'); return { // Inherit base layout state ...data(), // Set page identifier currentPage: 'inventory', // Loading states loading: true, error: '', saving: false, // Inventory data inventory: [], stats: { total_entries: 0, total_quantity: 0, low_stock_count: 0, out_of_stock_count: 0 }, // Filters filters: { search: '', location: '', low_stock: '' }, // Available locations for filter dropdown locations: [], // Pagination pagination: { page: 1, per_page: 20, total: 0, pages: 0 }, // Modal states showAdjustModal: false, showSetModal: false, selectedItem: null, // Form data adjustForm: { quantity: 0, reason: '' }, setForm: { quantity: 0 }, // Bulk operations selectedItems: [], showBulkAdjustModal: false, bulkAdjustForm: { quantity: 0, reason: '' }, // Debounce timer searchTimeout: null, // Computed: Total pages get totalPages() { return this.pagination.pages; }, // Computed: Start index for pagination display get startIndex() { if (this.pagination.total === 0) return 0; return (this.pagination.page - 1) * this.pagination.per_page + 1; }, // Computed: End index for pagination display get endIndex() { const end = this.pagination.page * this.pagination.per_page; return end > this.pagination.total ? this.pagination.total : end; }, // Computed: Page numbers for pagination get pageNumbers() { const pages = []; const totalPages = this.totalPages; const current = this.pagination.page; if (totalPages <= 7) { for (let i = 1; i <= totalPages; i++) { pages.push(i); } } else { pages.push(1); if (current > 3) pages.push('...'); const start = Math.max(2, current - 1); const end = Math.min(totalPages - 1, current + 1); for (let i = start; i <= end; i++) { pages.push(i); } if (current < totalPages - 2) pages.push('...'); pages.push(totalPages); } return pages; }, // Computed: Check if all visible items are selected get allSelected() { return this.inventory.length > 0 && this.selectedItems.length === this.inventory.length; }, // Computed: Check if some but not all items are selected get someSelected() { return this.selectedItems.length > 0 && this.selectedItems.length < this.inventory.length; }, async init() { vendorInventoryLog.info('Inventory init() called'); // Guard against multiple initialization if (window._vendorInventoryInitialized) { vendorInventoryLog.warn('Already initialized, skipping'); return; } window._vendorInventoryInitialized = true; // IMPORTANT: Call parent init first to set vendorCode from URL const parentInit = data().init; if (parentInit) { await parentInit.call(this); } // Load platform settings for rows per page if (window.PlatformSettings) { this.pagination.per_page = await window.PlatformSettings.getRowsPerPage(); } try { await this.loadInventory(); } catch (error) { vendorInventoryLog.error('Init failed:', error); this.error = 'Failed to initialize inventory page'; } vendorInventoryLog.info('Inventory initialization complete'); }, /** * Load inventory with filtering and pagination */ async loadInventory() { this.loading = true; this.error = ''; try { const params = new URLSearchParams({ skip: (this.pagination.page - 1) * this.pagination.per_page, limit: this.pagination.per_page }); // Add filters if (this.filters.search) { params.append('search', this.filters.search); } if (this.filters.location) { params.append('location', this.filters.location); } if (this.filters.low_stock) { params.append('low_stock', this.filters.low_stock); } const response = await apiClient.get(`/vendor/inventory?${params.toString()}`); this.inventory = response.items || []; this.pagination.total = response.total || 0; this.pagination.pages = Math.ceil(this.pagination.total / this.pagination.per_page); // Extract unique locations this.extractLocations(); // Calculate stats this.calculateStats(); vendorInventoryLog.info('Loaded inventory:', this.inventory.length, 'of', this.pagination.total); } catch (error) { vendorInventoryLog.error('Failed to load inventory:', error); this.error = error.message || 'Failed to load inventory'; } finally { this.loading = false; } }, /** * Extract unique locations from inventory */ extractLocations() { const locationSet = new Set(this.inventory.map(i => i.location).filter(Boolean)); this.locations = Array.from(locationSet).sort(); }, /** * Calculate inventory statistics */ calculateStats() { this.stats = { total_entries: this.pagination.total, total_quantity: this.inventory.reduce((sum, i) => sum + (i.quantity || 0), 0), low_stock_count: this.inventory.filter(i => i.quantity > 0 && i.quantity <= (i.low_stock_threshold || 5)).length, out_of_stock_count: this.inventory.filter(i => i.quantity <= 0).length }; }, /** * Debounced search handler */ debouncedSearch() { clearTimeout(this.searchTimeout); this.searchTimeout = setTimeout(() => { this.pagination.page = 1; this.loadInventory(); }, 300); }, /** * Apply filter and reload */ applyFilter() { this.pagination.page = 1; this.loadInventory(); }, /** * Clear all filters */ clearFilters() { this.filters = { search: '', location: '', low_stock: '' }; this.pagination.page = 1; this.loadInventory(); }, /** * Open adjust stock modal */ openAdjustModal(item) { this.selectedItem = item; this.adjustForm = { quantity: 0, reason: '' }; this.showAdjustModal = true; }, /** * Open set quantity modal */ openSetModal(item) { this.selectedItem = item; this.setForm = { quantity: item.quantity || 0 }; this.showSetModal = true; }, /** * Execute stock adjustment */ async executeAdjust() { if (!this.selectedItem || this.adjustForm.quantity === 0) return; this.saving = true; try { await apiClient.post(`/vendor/inventory/adjust`, { product_id: this.selectedItem.product_id, location: this.selectedItem.location, quantity: this.adjustForm.quantity, reason: this.adjustForm.reason || null }); vendorInventoryLog.info('Adjusted inventory:', this.selectedItem.id); this.showAdjustModal = false; this.selectedItem = null; Utils.showToast('Stock adjusted successfully', 'success'); await this.loadInventory(); } catch (error) { vendorInventoryLog.error('Failed to adjust inventory:', error); Utils.showToast(error.message || 'Failed to adjust stock', 'error'); } finally { this.saving = false; } }, /** * Execute set quantity */ async executeSet() { if (!this.selectedItem || this.setForm.quantity < 0) return; this.saving = true; try { await apiClient.post(`/vendor/inventory/set`, { product_id: this.selectedItem.product_id, location: this.selectedItem.location, quantity: this.setForm.quantity }); vendorInventoryLog.info('Set inventory quantity:', this.selectedItem.id); this.showSetModal = false; this.selectedItem = null; Utils.showToast('Quantity set successfully', 'success'); await this.loadInventory(); } catch (error) { vendorInventoryLog.error('Failed to set inventory:', error); Utils.showToast(error.message || 'Failed to set quantity', 'error'); } finally { this.saving = false; } }, /** * Get stock status class */ getStockStatus(item) { if (item.quantity <= 0) return 'out'; if (item.quantity <= (item.low_stock_threshold || 5)) return 'low'; return 'ok'; }, /** * Format number with locale */ formatNumber(num) { if (num === null || num === undefined) return '0'; const locale = window.VENDOR_CONFIG?.locale || 'en-GB'; return new Intl.NumberFormat(locale).format(num); }, /** * Pagination: Previous page */ previousPage() { if (this.pagination.page > 1) { this.pagination.page--; this.loadInventory(); } }, /** * Pagination: Next page */ nextPage() { if (this.pagination.page < this.totalPages) { this.pagination.page++; this.loadInventory(); } }, /** * Pagination: Go to specific page */ goToPage(pageNum) { if (pageNum !== '...' && pageNum >= 1 && pageNum <= this.totalPages) { this.pagination.page = pageNum; this.loadInventory(); } }, // ============================================================================ // BULK OPERATIONS // ============================================================================ /** * Toggle select all items on current page */ toggleSelectAll() { if (this.allSelected) { this.selectedItems = []; } else { this.selectedItems = this.inventory.map(i => i.id); } }, /** * Toggle selection of a single item */ toggleSelect(itemId) { const index = this.selectedItems.indexOf(itemId); if (index === -1) { this.selectedItems.push(itemId); } else { this.selectedItems.splice(index, 1); } }, /** * Check if item is selected */ isSelected(itemId) { return this.selectedItems.includes(itemId); }, /** * Clear all selections */ clearSelection() { this.selectedItems = []; }, /** * Open bulk adjust modal */ openBulkAdjustModal() { if (this.selectedItems.length === 0) return; this.bulkAdjustForm = { quantity: 0, reason: '' }; this.showBulkAdjustModal = true; }, /** * Execute bulk stock adjustment */ async bulkAdjust() { if (this.selectedItems.length === 0 || this.bulkAdjustForm.quantity === 0) return; this.saving = true; try { let successCount = 0; for (const itemId of this.selectedItems) { const item = this.inventory.find(i => i.id === itemId); if (item) { try { await apiClient.post(`/vendor/inventory/adjust`, { product_id: item.product_id, location: item.location, quantity: this.bulkAdjustForm.quantity, reason: this.bulkAdjustForm.reason || 'Bulk adjustment' }); successCount++; } catch (error) { vendorInventoryLog.warn(`Failed to adjust item ${itemId}:`, error); } } } Utils.showToast(`${successCount} item(s) adjusted by ${this.bulkAdjustForm.quantity > 0 ? '+' : ''}${this.bulkAdjustForm.quantity}`, 'success'); this.showBulkAdjustModal = false; this.clearSelection(); await this.loadInventory(); } catch (error) { vendorInventoryLog.error('Bulk adjust failed:', error); Utils.showToast(error.message || 'Failed to adjust inventory', 'error'); } finally { this.saving = false; } }, /** * Export selected items as CSV */ exportSelectedItems() { if (this.selectedItems.length === 0) return; const selectedData = this.inventory.filter(i => this.selectedItems.includes(i.id)); // Build CSV content const headers = ['Product', 'SKU', 'Location', 'Quantity', 'Low Stock Threshold', 'Status']; const rows = selectedData.map(i => [ i.product_name || '-', i.sku || '-', i.location || 'Default', i.quantity || 0, i.low_stock_threshold || 5, this.getStockStatus(i) ]); const csvContent = [ headers.join(','), ...rows.map(row => row.map(cell => `"${cell}"`).join(',')) ].join('\n'); // Download const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = `inventory_export_${new Date().toISOString().split('T')[0]}.csv`; link.click(); Utils.showToast(`Exported ${selectedData.length} item(s)`, 'success'); } }; }