// noqa: js-006 - async init pattern is safe, loadData has try/catch // static/admin/js/inventory.js /** * Admin inventory management page logic * View and manage stock levels across all vendors */ const adminInventoryLog = window.LogConfig.loggers.adminInventory || window.LogConfig.createLogger('adminInventory', false); adminInventoryLog.info('Loading...'); function adminInventory() { adminInventoryLog.info('adminInventory() 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, total_reserved: 0, total_available: 0, low_stock_count: 0, vendors_with_inventory: 0, unique_locations: 0 }, // Filters filters: { search: '', vendor_id: '', location: '', low_stock: '' }, // Available locations for filter dropdown locations: [], // Selected vendor (for prominent display and filtering) selectedVendor: null, // Vendor selector controller (Tom Select) vendorSelector: null, // Pagination pagination: { page: 1, per_page: 20, total: 0, pages: 0 }, // Modal states showAdjustModal: false, showSetModal: false, showDeleteModal: false, showImportModal: false, selectedItem: null, // Form data adjustForm: { quantity: 0, reason: '' }, setForm: { quantity: 0 }, // Import form importForm: { vendor_id: '', warehouse: 'strassen', file: null, clear_existing: false }, importing: false, importResult: null, vendorsList: [], // 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; }, async init() { adminInventoryLog.info('Inventory init() called'); // Guard against multiple initialization if (window._adminInventoryInitialized) { adminInventoryLog.warn('Already initialized, skipping'); return; } window._adminInventoryInitialized = true; // Load platform settings for rows per page if (window.PlatformSettings) { this.pagination.per_page = await window.PlatformSettings.getRowsPerPage(); } // Initialize vendor selector (Tom Select) this.$nextTick(() => { this.initVendorSelector(); }); // Load vendors list for import modal await this.loadVendorsList(); // Check localStorage for saved vendor const savedVendorId = localStorage.getItem('inventory_selected_vendor_id'); if (savedVendorId) { adminInventoryLog.info('Restoring saved vendor:', savedVendorId); // Restore vendor after a short delay to ensure TomSelect is ready setTimeout(async () => { await this.restoreSavedVendor(parseInt(savedVendorId)); }, 200); // Load stats and locations but not inventory (restoreSavedVendor will do that) await Promise.all([ this.loadStats(), this.loadLocations() ]); } else { // No saved vendor - load all data await Promise.all([ this.loadStats(), this.loadLocations(), this.loadInventory() ]); } adminInventoryLog.info('Inventory initialization complete'); }, /** * Restore saved vendor from localStorage */ async restoreSavedVendor(vendorId) { try { const vendor = await apiClient.get(`/admin/vendors/${vendorId}`); if (this.vendorSelector && vendor) { // Use the vendor selector's setValue method this.vendorSelector.setValue(vendor.id, vendor); // Set the filter state this.selectedVendor = vendor; this.filters.vendor_id = vendor.id; adminInventoryLog.info('Restored vendor:', vendor.name); // Load inventory with the vendor filter applied await this.loadInventory(); } } catch (error) { adminInventoryLog.warn('Failed to restore saved vendor, clearing localStorage:', error); localStorage.removeItem('inventory_selected_vendor_id'); // Load unfiltered inventory as fallback await this.loadInventory(); } }, /** * Initialize vendor selector with Tom Select */ initVendorSelector() { if (!this.$refs.vendorSelect) { adminInventoryLog.warn('Vendor select element not found'); return; } this.vendorSelector = initVendorSelector(this.$refs.vendorSelect, { placeholder: 'Filter by vendor...', onSelect: (vendor) => { adminInventoryLog.info('Vendor selected:', vendor); this.selectedVendor = vendor; this.filters.vendor_id = vendor.id; // Save to localStorage localStorage.setItem('inventory_selected_vendor_id', vendor.id.toString()); this.pagination.page = 1; this.loadLocations(); this.loadInventory(); this.loadStats(); }, onClear: () => { adminInventoryLog.info('Vendor filter cleared'); this.selectedVendor = null; this.filters.vendor_id = ''; // Clear from localStorage localStorage.removeItem('inventory_selected_vendor_id'); this.pagination.page = 1; this.loadLocations(); this.loadInventory(); this.loadStats(); } }); }, /** * Clear vendor filter */ clearVendorFilter() { if (this.vendorSelector) { this.vendorSelector.clear(); } this.selectedVendor = null; this.filters.vendor_id = ''; // Clear from localStorage localStorage.removeItem('inventory_selected_vendor_id'); this.pagination.page = 1; this.loadLocations(); this.loadInventory(); this.loadStats(); }, /** * Load inventory statistics */ async loadStats() { try { const params = new URLSearchParams(); if (this.filters.vendor_id) { params.append('vendor_id', this.filters.vendor_id); } const url = params.toString() ? `/admin/inventory/stats?${params}` : '/admin/inventory/stats'; const response = await apiClient.get(url); this.stats = response; adminInventoryLog.info('Loaded stats:', this.stats); } catch (error) { adminInventoryLog.error('Failed to load stats:', error); } }, /** * Load available locations for filter */ async loadLocations() { try { const params = this.filters.vendor_id ? `?vendor_id=${this.filters.vendor_id}` : ''; const response = await apiClient.get(`/admin/inventory/locations${params}`); this.locations = response.locations || []; adminInventoryLog.info('Loaded locations:', this.locations.length); } catch (error) { adminInventoryLog.error('Failed to load locations:', error); } }, /** * 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.vendor_id) { params.append('vendor_id', this.filters.vendor_id); } 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(`/admin/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); adminInventoryLog.info('Loaded inventory:', this.inventory.length, 'of', this.pagination.total); } catch (error) { adminInventoryLog.error('Failed to load inventory:', error); this.error = error.message || 'Failed to load inventory'; } finally { this.loading = false; } }, /** * Debounced search handler */ debouncedSearch() { clearTimeout(this.searchTimeout); this.searchTimeout = setTimeout(() => { this.pagination.page = 1; this.loadInventory(); }, 300); }, /** * Refresh inventory list */ async refresh() { await Promise.all([ this.loadStats(), this.loadLocations(), 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 }; this.showSetModal = true; }, /** * Confirm delete */ confirmDelete(item) { this.selectedItem = item; this.showDeleteModal = true; }, /** * Execute stock adjustment */ async executeAdjust() { if (!this.selectedItem || this.adjustForm.quantity === 0) return; this.saving = true; try { await apiClient.post('/admin/inventory/adjust', { vendor_id: this.selectedItem.vendor_id, product_id: this.selectedItem.product_id, location: this.selectedItem.location, quantity: this.adjustForm.quantity, reason: this.adjustForm.reason || null }); adminInventoryLog.info('Adjusted inventory:', this.selectedItem.id); this.showAdjustModal = false; this.selectedItem = null; Utils.showToast('Stock adjusted successfully.', 'success'); await this.refresh(); } catch (error) { adminInventoryLog.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('/admin/inventory/set', { vendor_id: this.selectedItem.vendor_id, product_id: this.selectedItem.product_id, location: this.selectedItem.location, quantity: this.setForm.quantity }); adminInventoryLog.info('Set inventory quantity:', this.selectedItem.id); this.showSetModal = false; this.selectedItem = null; Utils.showToast('Quantity set successfully.', 'success'); await this.refresh(); } catch (error) { adminInventoryLog.error('Failed to set inventory:', error); Utils.showToast(error.message || 'Failed to set quantity.', 'error'); } finally { this.saving = false; } }, /** * Execute delete */ async executeDelete() { if (!this.selectedItem) return; this.saving = true; try { await apiClient.delete(`/admin/inventory/${this.selectedItem.id}`); adminInventoryLog.info('Deleted inventory:', this.selectedItem.id); this.showDeleteModal = false; this.selectedItem = null; Utils.showToast('Inventory entry deleted.', 'success'); await this.refresh(); } catch (error) { adminInventoryLog.error('Failed to delete inventory:', error); Utils.showToast(error.message || 'Failed to delete entry.', 'error'); } finally { this.saving = false; } }, /** * Format number with locale */ formatNumber(num) { if (num === null || num === undefined) return '0'; return new Intl.NumberFormat('en-US').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(); } }, // ============================================================ // Import Methods // ============================================================ /** * Load vendors list for import modal */ async loadVendorsList() { try { const response = await apiClient.get('/admin/vendors', { limit: 100 }); this.vendorsList = response.vendors || []; } catch (error) { adminInventoryLog.error('Failed to load vendors:', error); } }, /** * Execute inventory import */ async executeImport() { if (!this.importForm.vendor_id || !this.importForm.file) { Utils.showToast('Please select a vendor and file', 'error'); return; } this.importing = true; this.importResult = null; try { const formData = new FormData(); formData.append('file', this.importForm.file); formData.append('vendor_id', this.importForm.vendor_id); formData.append('warehouse', this.importForm.warehouse || 'strassen'); formData.append('clear_existing', this.importForm.clear_existing); this.importResult = await apiClient.postFormData('/admin/inventory/import', formData); if (this.importResult.success) { adminInventoryLog.info('Import successful:', this.importResult); Utils.showToast( `Imported ${this.importResult.quantity_imported} units (${this.importResult.entries_created} new, ${this.importResult.entries_updated} updated)`, 'success' ); // Refresh inventory list await this.refresh(); } else { Utils.showToast('Import completed with errors', 'warning'); } } catch (error) { adminInventoryLog.error('Import failed:', error); this.importResult = { success: false, errors: [error.message || 'Import failed'] }; Utils.showToast(error.message || 'Import failed', 'error'); } finally { this.importing = false; } }, /** * Close import modal and reset form */ closeImportModal() { this.showImportModal = false; this.importResult = null; this.importForm = { vendor_id: '', warehouse: 'strassen', file: null, clear_existing: false }; } }; }