- API-004: Add noqa for factory-pattern auth in user_account routes and payments admin - MDL-003: Add from_attributes to MerchantStoreDetailResponse schema - EXC-003: Suppress broad except in merchant_store_service and admin_subscription_service (intentional fallbacks for optional billing module) - NAM-002: Rename onboarding files to *_service.py suffix and update all imports - JS-001: Add file-level noqa for dev-toolbar.js (console interceptor by design) - JS-005: Add init guards to dashboard.js and customer-detail.js - IMPORT-004: Break circular deps by removing orders from inventory requires and marketplace from orders requires; add IMPORT-002 suppression for lazy cross-imports - MOD-025: Remove unused OnboardingAlreadyCompletedException Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
154 lines
4.8 KiB
JavaScript
154 lines
4.8 KiB
JavaScript
// app/modules/customers/static/store/js/customer-detail.js
|
|
/**
|
|
* Store customer detail page logic.
|
|
* Loads customer profile, order stats, and recent orders from existing APIs.
|
|
*/
|
|
|
|
const customerDetailLog = window.LogConfig?.createLogger('customerDetail') || console;
|
|
|
|
function storeCustomerDetail() {
|
|
return {
|
|
// Inherit base layout state
|
|
...data(),
|
|
|
|
// Page identifier
|
|
currentPage: 'customers',
|
|
|
|
// Loading states
|
|
loading: true,
|
|
error: '',
|
|
|
|
// Data
|
|
customerId: window.customerDetailData?.customerId,
|
|
customer: null,
|
|
orderStats: {
|
|
total_orders: 0,
|
|
total_spent_cents: 0,
|
|
last_order_date: null,
|
|
first_order_date: null
|
|
},
|
|
recentOrders: [],
|
|
|
|
// Computed
|
|
get customerName() {
|
|
if (this.customer?.first_name && this.customer?.last_name) {
|
|
return `${this.customer.first_name} ${this.customer.last_name}`;
|
|
}
|
|
return this.customer?.email || 'Unknown';
|
|
},
|
|
|
|
async init() {
|
|
if (window._customerDetailInitialized) return;
|
|
window._customerDetailInitialized = true;
|
|
|
|
try {
|
|
// Load i18n translations
|
|
await I18n.loadModule('customers');
|
|
|
|
customerDetailLog.info('Customer detail init, id:', this.customerId);
|
|
|
|
// Call parent init to set storeCode from URL
|
|
const parentInit = data().init;
|
|
if (parentInit) {
|
|
await parentInit.call(this);
|
|
}
|
|
|
|
// Load all data in parallel
|
|
await Promise.all([
|
|
this.loadCustomer(),
|
|
this.loadOrderStats(),
|
|
this.loadRecentOrders()
|
|
]);
|
|
|
|
customerDetailLog.info('Customer detail loaded');
|
|
} catch (error) {
|
|
customerDetailLog.error('Init failed:', error);
|
|
this.error = 'Failed to load customer details';
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Load customer profile
|
|
*/
|
|
async loadCustomer() {
|
|
try {
|
|
const response = await apiClient.get(`/store/customers/${this.customerId}`);
|
|
this.customer = response;
|
|
} catch (error) {
|
|
customerDetailLog.error('Failed to load customer:', error);
|
|
this.error = error.message || 'Customer not found';
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Load order statistics from orders module
|
|
*/
|
|
async loadOrderStats() {
|
|
try {
|
|
const response = await apiClient.get(`/store/customers/${this.customerId}/order-stats`);
|
|
this.orderStats = response;
|
|
} catch (error) {
|
|
customerDetailLog.warn('Failed to load order stats:', error);
|
|
// Non-fatal — page still works without stats
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Load recent orders from orders module
|
|
*/
|
|
async loadRecentOrders() {
|
|
try {
|
|
const response = await apiClient.get(`/store/customers/${this.customerId}/orders?limit=5`);
|
|
this.recentOrders = response.orders || [];
|
|
} catch (error) {
|
|
customerDetailLog.warn('Failed to load recent orders:', error);
|
|
// Non-fatal
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Get customer initials for avatar
|
|
*/
|
|
getInitials() {
|
|
const first = this.customer?.first_name || '';
|
|
const last = this.customer?.last_name || '';
|
|
return (first.charAt(0) + last.charAt(0)).toUpperCase() || '?';
|
|
},
|
|
|
|
/**
|
|
* Navigate to send message
|
|
*/
|
|
messageCustomer() {
|
|
window.location.href = `/store/${this.storeCode}/messages?customer=${this.customerId}`;
|
|
},
|
|
|
|
/**
|
|
* Format date for display
|
|
*/
|
|
formatDate(dateStr) {
|
|
if (!dateStr) return '-';
|
|
const locale = window.STORE_CONFIG?.locale || 'en-GB';
|
|
return new Date(dateStr).toLocaleDateString(locale, {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric'
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Format price (cents to currency)
|
|
*/
|
|
formatPrice(cents) {
|
|
if (!cents && cents !== 0) return '-';
|
|
const locale = window.STORE_CONFIG?.locale || 'en-GB';
|
|
const currency = window.STORE_CONFIG?.currency || 'EUR';
|
|
return new Intl.NumberFormat(locale, {
|
|
style: 'currency',
|
|
currency: currency
|
|
}).format(cents / 100);
|
|
}
|
|
};
|
|
}
|