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>
210 lines
6.6 KiB
JavaScript
210 lines
6.6 KiB
JavaScript
// static/vendor/js/analytics.js
|
|
/**
|
|
* Vendor analytics and reports page logic
|
|
* View business metrics and performance data
|
|
*/
|
|
|
|
const vendorAnalyticsLog = window.LogConfig.loggers.vendorAnalytics ||
|
|
window.LogConfig.createLogger('vendorAnalytics', false);
|
|
|
|
vendorAnalyticsLog.info('Loading...');
|
|
|
|
function vendorAnalytics() {
|
|
vendorAnalyticsLog.info('vendorAnalytics() called');
|
|
|
|
return {
|
|
// Inherit base layout state
|
|
...data(),
|
|
|
|
// Set page identifier
|
|
currentPage: 'analytics',
|
|
|
|
// Loading states
|
|
loading: true,
|
|
error: '',
|
|
|
|
// Time period
|
|
period: '30d',
|
|
periodOptions: [
|
|
{ value: '7d', label: 'Last 7 Days' },
|
|
{ value: '30d', label: 'Last 30 Days' },
|
|
{ value: '90d', label: 'Last 90 Days' },
|
|
{ value: '1y', label: 'Last Year' }
|
|
],
|
|
|
|
// Analytics data
|
|
analytics: null,
|
|
stats: null,
|
|
|
|
// Dashboard stats (from vendor stats endpoint)
|
|
dashboardStats: {
|
|
total_products: 0,
|
|
active_products: 0,
|
|
featured_products: 0,
|
|
total_orders: 0,
|
|
pending_orders: 0,
|
|
total_customers: 0,
|
|
total_inventory: 0,
|
|
low_stock_count: 0
|
|
},
|
|
|
|
async init() {
|
|
vendorAnalyticsLog.info('Analytics init() called');
|
|
|
|
// Guard against multiple initialization
|
|
if (window._vendorAnalyticsInitialized) {
|
|
vendorAnalyticsLog.warn('Already initialized, skipping');
|
|
return;
|
|
}
|
|
window._vendorAnalyticsInitialized = true;
|
|
|
|
// IMPORTANT: Call parent init first to set vendorCode from URL
|
|
const parentInit = data().init;
|
|
if (parentInit) {
|
|
await parentInit.call(this);
|
|
}
|
|
|
|
try {
|
|
await this.loadAllData();
|
|
} catch (error) {
|
|
vendorAnalyticsLog.error('Init failed:', error);
|
|
this.error = 'Failed to initialize analytics page';
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
|
|
vendorAnalyticsLog.info('Analytics initialization complete');
|
|
},
|
|
|
|
/**
|
|
* Load all analytics data
|
|
*/
|
|
async loadAllData() {
|
|
this.loading = true;
|
|
this.error = '';
|
|
|
|
try {
|
|
// Load analytics and stats in parallel
|
|
const [analyticsResponse, statsResponse] = await Promise.all([
|
|
this.fetchAnalytics(),
|
|
this.fetchStats()
|
|
]);
|
|
|
|
this.analytics = analyticsResponse;
|
|
this.dashboardStats = statsResponse;
|
|
|
|
vendorAnalyticsLog.info('Loaded analytics data');
|
|
} catch (error) {
|
|
vendorAnalyticsLog.error('Failed to load data:', error);
|
|
this.error = error.message || 'Failed to load analytics data';
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Fetch analytics data for current period
|
|
*/
|
|
async fetchAnalytics() {
|
|
try {
|
|
const response = await apiClient.get(`/vendor/analytics?period=${this.period}`);
|
|
return response;
|
|
} catch (error) {
|
|
// Analytics might require feature access
|
|
if (error.status === 403) {
|
|
vendorAnalyticsLog.warn('Analytics feature not available');
|
|
return null;
|
|
}
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Fetch dashboard stats
|
|
*/
|
|
async fetchStats() {
|
|
try {
|
|
const response = await apiClient.get(`/vendor/dashboard/stats`);
|
|
return {
|
|
total_products: response.catalog?.total_products || 0,
|
|
active_products: response.catalog?.active_products || 0,
|
|
featured_products: response.catalog?.featured_products || 0,
|
|
total_orders: response.orders?.total || 0,
|
|
pending_orders: response.orders?.pending || 0,
|
|
total_customers: response.customers?.total || 0,
|
|
total_inventory: response.inventory?.total_quantity || 0,
|
|
low_stock_count: response.inventory?.low_stock_count || 0
|
|
};
|
|
} catch (error) {
|
|
vendorAnalyticsLog.error('Failed to fetch stats:', error);
|
|
return this.dashboardStats;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Change time period and reload data
|
|
*/
|
|
async changePeriod(newPeriod) {
|
|
this.period = newPeriod;
|
|
try {
|
|
await this.loadAllData();
|
|
} catch (error) {
|
|
vendorAnalyticsLog.error('Failed to change period:', error);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Get period label
|
|
*/
|
|
getPeriodLabel() {
|
|
const option = this.periodOptions.find(p => p.value === this.period);
|
|
return option ? option.label : this.period;
|
|
},
|
|
|
|
/**
|
|
* Format number with commas
|
|
*/
|
|
formatNumber(num) {
|
|
if (num === null || num === undefined) return '0';
|
|
return num.toLocaleString();
|
|
},
|
|
|
|
/**
|
|
* Format percentage
|
|
*/
|
|
formatPercent(value) {
|
|
if (value === null || value === undefined) return '0%';
|
|
return `${value.toFixed(1)}%`;
|
|
},
|
|
|
|
/**
|
|
* Calculate active product percentage
|
|
*/
|
|
get activeProductPercent() {
|
|
if (!this.dashboardStats.total_products) return 0;
|
|
return (this.dashboardStats.active_products / this.dashboardStats.total_products * 100).toFixed(1);
|
|
},
|
|
|
|
/**
|
|
* Calculate pending order percentage
|
|
*/
|
|
get pendingOrderPercent() {
|
|
if (!this.dashboardStats.total_orders) return 0;
|
|
return (this.dashboardStats.pending_orders / this.dashboardStats.total_orders * 100).toFixed(1);
|
|
},
|
|
|
|
/**
|
|
* Get stock health status
|
|
*/
|
|
get stockHealth() {
|
|
if (this.dashboardStats.low_stock_count === 0) {
|
|
return { status: 'good', label: 'Healthy', color: 'green' };
|
|
} else if (this.dashboardStats.low_stock_count <= 5) {
|
|
return { status: 'warning', label: 'Attention Needed', color: 'yellow' };
|
|
} else {
|
|
return { status: 'critical', label: 'Critical', color: 'red' };
|
|
}
|
|
}
|
|
};
|
|
}
|