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>
316 lines
9.6 KiB
JavaScript
316 lines
9.6 KiB
JavaScript
// static/vendor/js/customers.js
|
|
/**
|
|
* Vendor customers management page logic
|
|
* View and manage customer relationships
|
|
*/
|
|
|
|
const vendorCustomersLog = window.LogConfig.loggers.vendorCustomers ||
|
|
window.LogConfig.createLogger('vendorCustomers', false);
|
|
|
|
vendorCustomersLog.info('Loading...');
|
|
|
|
function vendorCustomers() {
|
|
vendorCustomersLog.info('vendorCustomers() called');
|
|
|
|
return {
|
|
// Inherit base layout state
|
|
...data(),
|
|
|
|
// Set page identifier
|
|
currentPage: 'customers',
|
|
|
|
// Loading states
|
|
loading: true,
|
|
error: '',
|
|
saving: false,
|
|
|
|
// Customers data
|
|
customers: [],
|
|
stats: {
|
|
total: 0,
|
|
active: 0,
|
|
new_this_month: 0
|
|
},
|
|
|
|
// Filters
|
|
filters: {
|
|
search: '',
|
|
status: ''
|
|
},
|
|
|
|
// Pagination
|
|
pagination: {
|
|
page: 1,
|
|
per_page: 20,
|
|
total: 0,
|
|
pages: 0
|
|
},
|
|
|
|
// Modal states
|
|
showDetailModal: false,
|
|
showOrdersModal: false,
|
|
selectedCustomer: null,
|
|
customerOrders: [],
|
|
|
|
// 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() {
|
|
vendorCustomersLog.info('Customers init() called');
|
|
|
|
// Guard against multiple initialization
|
|
if (window._vendorCustomersInitialized) {
|
|
vendorCustomersLog.warn('Already initialized, skipping');
|
|
return;
|
|
}
|
|
window._vendorCustomersInitialized = 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.loadCustomers();
|
|
} catch (error) {
|
|
vendorCustomersLog.error('Init failed:', error);
|
|
this.error = 'Failed to initialize customers page';
|
|
}
|
|
|
|
vendorCustomersLog.info('Customers initialization complete');
|
|
},
|
|
|
|
/**
|
|
* Load customers with filtering and pagination
|
|
*/
|
|
async loadCustomers() {
|
|
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.status) {
|
|
params.append('status', this.filters.status);
|
|
}
|
|
|
|
const response = await apiClient.get(`/vendor/customers?${params.toString()}`);
|
|
|
|
this.customers = response.customers || [];
|
|
this.pagination.total = response.total || 0;
|
|
this.pagination.pages = Math.ceil(this.pagination.total / this.pagination.per_page);
|
|
|
|
// Calculate stats
|
|
this.stats = {
|
|
total: this.pagination.total,
|
|
active: this.customers.filter(c => c.is_active !== false).length,
|
|
new_this_month: this.customers.filter(c => {
|
|
if (!c.created_at) return false;
|
|
const created = new Date(c.created_at);
|
|
const now = new Date();
|
|
return created.getMonth() === now.getMonth() && created.getFullYear() === now.getFullYear();
|
|
}).length
|
|
};
|
|
|
|
vendorCustomersLog.info('Loaded customers:', this.customers.length, 'of', this.pagination.total);
|
|
} catch (error) {
|
|
vendorCustomersLog.error('Failed to load customers:', error);
|
|
this.error = error.message || 'Failed to load customers';
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Debounced search handler
|
|
*/
|
|
debouncedSearch() {
|
|
clearTimeout(this.searchTimeout);
|
|
this.searchTimeout = setTimeout(() => {
|
|
this.pagination.page = 1;
|
|
this.loadCustomers();
|
|
}, 300);
|
|
},
|
|
|
|
/**
|
|
* Apply filter and reload
|
|
*/
|
|
applyFilter() {
|
|
this.pagination.page = 1;
|
|
this.loadCustomers();
|
|
},
|
|
|
|
/**
|
|
* Clear all filters
|
|
*/
|
|
clearFilters() {
|
|
this.filters = {
|
|
search: '',
|
|
status: ''
|
|
};
|
|
this.pagination.page = 1;
|
|
this.loadCustomers();
|
|
},
|
|
|
|
/**
|
|
* View customer details
|
|
*/
|
|
async viewCustomer(customer) {
|
|
this.loading = true;
|
|
try {
|
|
const response = await apiClient.get(`/vendor/customers/${customer.id}`);
|
|
this.selectedCustomer = response;
|
|
this.showDetailModal = true;
|
|
vendorCustomersLog.info('Loaded customer details:', customer.id);
|
|
} catch (error) {
|
|
vendorCustomersLog.error('Failed to load customer details:', error);
|
|
Utils.showToast(error.message || 'Failed to load customer details', 'error');
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* View customer orders
|
|
*/
|
|
async viewCustomerOrders(customer) {
|
|
this.loading = true;
|
|
try {
|
|
const response = await apiClient.get(`/vendor/customers/${customer.id}/orders`);
|
|
this.selectedCustomer = customer;
|
|
this.customerOrders = response.orders || [];
|
|
this.showOrdersModal = true;
|
|
vendorCustomersLog.info('Loaded customer orders:', customer.id, this.customerOrders.length);
|
|
} catch (error) {
|
|
vendorCustomersLog.error('Failed to load customer orders:', error);
|
|
Utils.showToast(error.message || 'Failed to load customer orders', 'error');
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Send message to customer
|
|
*/
|
|
messageCustomer(customer) {
|
|
window.location.href = `/vendor/${this.vendorCode}/messages?customer=${customer.id}`;
|
|
},
|
|
|
|
/**
|
|
* Get customer initials for avatar
|
|
*/
|
|
getInitials(customer) {
|
|
const first = customer.first_name || '';
|
|
const last = customer.last_name || '';
|
|
return (first.charAt(0) + last.charAt(0)).toUpperCase() || '?';
|
|
},
|
|
|
|
/**
|
|
* Format date for display
|
|
*/
|
|
formatDate(dateStr) {
|
|
if (!dateStr) return '-';
|
|
return new Date(dateStr).toLocaleDateString('de-DE', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric'
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Format price for display
|
|
*/
|
|
formatPrice(cents) {
|
|
if (!cents && cents !== 0) return '-';
|
|
return new Intl.NumberFormat('de-DE', {
|
|
style: 'currency',
|
|
currency: 'EUR'
|
|
}).format(cents / 100);
|
|
},
|
|
|
|
/**
|
|
* Pagination: Previous page
|
|
*/
|
|
previousPage() {
|
|
if (this.pagination.page > 1) {
|
|
this.pagination.page--;
|
|
this.loadCustomers();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Pagination: Next page
|
|
*/
|
|
nextPage() {
|
|
if (this.pagination.page < this.totalPages) {
|
|
this.pagination.page++;
|
|
this.loadCustomers();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Pagination: Go to specific page
|
|
*/
|
|
goToPage(pageNum) {
|
|
if (pageNum !== '...' && pageNum >= 1 && pageNum <= this.totalPages) {
|
|
this.pagination.page = pageNum;
|
|
this.loadCustomers();
|
|
}
|
|
}
|
|
};
|
|
}
|