Files
orion/static/vendor/js/inventory.js
Samir Boulahtit 463e1b67d5 feat: complete vendor frontend parity with admin
Phase 1 - Sidebar Refactor:
- Refactor sidebar to use collapsible sections with Alpine.js
- Add localStorage persistence for section states
- Reorganize navigation into logical groups

Phase 2 - Core JS Files:
- Add products.js: product CRUD, search, filtering, toggle active/featured
- Add orders.js: order list, status management, filtering
- Add inventory.js: stock tracking, adjust/set quantity modals
- Add customers.js: customer list, order history, messaging
- Add team.js: member invite, role management, remove members
- Add profile.js: profile editing with form validation
- Add settings.js: tabbed settings (general, marketplace, notifications)

Templates updated from placeholders to full functional UIs.
Vendor frontend now at ~90% parity with admin.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 15:34:38 +01:00

366 lines
11 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
},
// 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() {
vendorInventoryLog.info('Inventory init() called');
// Guard against multiple initialization
if (window._vendorInventoryInitialized) {
vendorInventoryLog.warn('Already initialized, skipping');
return;
}
window._vendorInventoryInitialized = true;
// 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/${this.vendorCode}/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/${this.vendorCode}/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/${this.vendorCode}/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();
}
}
};
}