refactor(js): migrate JavaScript files to module directories

Move 47 JS files from static/{admin,vendor,shared}/js/ to their
respective module directories app/modules/*/static/*/js/:

- Orders: orders.js, order-detail.js
- Catalog: products.js (renamed from vendor-products.js), product-*.js
- Inventory: inventory.js (admin & vendor)
- Customers: customers.js, users.js, user-*.js
- Billing: billing-history.js, subscriptions.js, subscription-tiers.js,
  billing.js, invoices.js, feature-store.js, upgrade-prompts.js
- Messaging: messages.js, notifications.js, email-templates.js
- Marketplace: marketplace*.js, letzshop*.js, onboarding.js
- Monitoring: monitoring.js, background-tasks.js, imports.js, logs.js
- Dev Tools: testing-*.js, code-quality-*.js

Update 39 templates to reference new module static paths using
url_for('{module}_static', path='...') pattern.

Files staying in static/ (platform core):
- admin: dashboard, login, platforms, vendors, companies, admin-users,
  settings, components, init-alpine, module-config
- vendor: dashboard, login, profile, settings, team, media, init-alpine
- shared: api-client, utils, money, icons, log-config, vendor-selector,
  media-picker

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-30 22:08:20 +01:00
parent 434db1560a
commit 0b4291d893
86 changed files with 63 additions and 63 deletions

View File

@@ -0,0 +1,596 @@
// 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
};
}
};
}

View File

@@ -0,0 +1,514 @@
// app/modules/inventory/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');
}
};
}