Files
orion/static/admin/js/inventory.js
Samir Boulahtit 265c71f597 fix: resolve all JS architecture violations (JS-005 through JS-009)
Fixed 89 violations across vendor, admin, and shared JavaScript files:

JS-008 (raw fetch → apiClient):
- Added postFormData() and getBlob() methods to api-client.js
- Updated inventory.js, messages.js to use apiClient.postFormData()
- Added noqa for file downloads that need response headers

JS-009 (window.showToast → Utils.showToast):
- Updated admin/messages.js, notifications.js, vendor/messages.js
- Replaced alert() in customers.js

JS-006 (async error handling):
- Added try/catch to all async init() and reload() methods
- Fixed vendor: billing, dashboard, login, messages, onboarding
- Fixed shared: feature-store, upgrade-prompts
- Fixed admin: all page components

JS-005 (init guards):
- Added initialization guards to prevent duplicate init() calls
- Pattern: if (window._componentInitialized) return;

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 21:32:19 +01:00

597 lines
19 KiB
JavaScript

// 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
};
}
};
}