Files
orion/static/vendor/js/inventory.js
Samir Boulahtit 34d115dc58 fix: remove vendorCode from vendor API paths
Vendor API endpoints use JWT authentication, not URL path parameters.
The vendorCode should only be used for page URLs (navigation), not API calls.

Fixed API paths in 10 vendor JS files:
- analytics.js, customers.js, inventory.js, notifications.js
- order-detail.js, orders.js, products.js, profile.js
- settings.js, team.js

Added architecture rule JS-014 to prevent this pattern from recurring.
Added validation check _check_vendor_api_paths to validate_architecture.py.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 21:49:24 +01:00

514 lines
16 KiB
JavaScript

// 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';
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();
}
},
// ============================================================================
// 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');
}
};
}