feat: multi-module improvements across merchant, store, i18n, and customer systems
All checks were successful
All checks were successful
- Fix platform-grouped merchant sidebar menu with core items at root level - Add merchant store management (detail page, create store, team page) - Fix store settings 500 error by removing dead stripe/API tab - Move onboarding translations to module-owned locale files - Fix onboarding banner i18n with server-side rendering + context inheritance - Refactor login language selectors to use languageSelector() function (LANG-002) - Move HTTPException handling to global exception handler in merchant routes (API-003) - Add language selector to all login pages and portal headers - Fix customer module: drop order stats from customer model, add to orders module - Fix admin menu config visibility for super admin platform context - Fix storefront auth and layout issues - Add missing i18n translations for onboarding steps (en/fr/de/lb) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -27,11 +27,7 @@ function adminCustomers() {
|
||||
stats: {
|
||||
total: 0,
|
||||
active: 0,
|
||||
inactive: 0,
|
||||
with_orders: 0,
|
||||
total_spent: 0,
|
||||
total_orders: 0,
|
||||
avg_order_value: 0
|
||||
inactive: 0
|
||||
},
|
||||
|
||||
// Pagination (standard structure matching pagination macro)
|
||||
@@ -375,17 +371,6 @@ function adminCustomers() {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Format currency for display
|
||||
*/
|
||||
formatCurrency(amount) {
|
||||
if (amount == null) return '-';
|
||||
return new Intl.NumberFormat('de-DE', {
|
||||
style: 'currency',
|
||||
currency: 'EUR'
|
||||
}).format(amount);
|
||||
},
|
||||
|
||||
/**
|
||||
* Format date for display
|
||||
*/
|
||||
|
||||
150
app/modules/customers/static/store/js/customer-detail.js
Normal file
150
app/modules/customers/static/store/js/customer-detail.js
Normal file
@@ -0,0 +1,150 @@
|
||||
// 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() {
|
||||
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);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -48,9 +48,7 @@ function storeCustomers() {
|
||||
|
||||
// Modal states
|
||||
showDetailModal: false,
|
||||
showOrdersModal: false,
|
||||
selectedCustomer: null,
|
||||
customerOrders: [],
|
||||
|
||||
// Debounce timer
|
||||
searchTimeout: null,
|
||||
@@ -227,25 +225,6 @@ function storeCustomers() {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* View customer orders
|
||||
*/
|
||||
async viewCustomerOrders(customer) {
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await apiClient.get(`/store/customers/${customer.id}/orders`);
|
||||
this.selectedCustomer = customer;
|
||||
this.customerOrders = response.orders || [];
|
||||
this.showOrdersModal = true;
|
||||
storeCustomersLog.info('Loaded customer orders:', customer.id, this.customerOrders.length);
|
||||
} catch (error) {
|
||||
storeCustomersLog.error('Failed to load customer orders:', error);
|
||||
Utils.showToast(error.message || I18n.t('customers.messages.failed_to_load_customer_orders'), 'error');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Send message to customer
|
||||
*/
|
||||
@@ -275,19 +254,6 @@ function storeCustomers() {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Format price for display
|
||||
*/
|
||||
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);
|
||||
},
|
||||
|
||||
/**
|
||||
* Pagination: Previous page
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user