feat: multi-module improvements across merchant, store, i18n, and customer systems
All checks were successful
CI / ruff (push) Successful in 12s
CI / pytest (push) Successful in 50m57s
CI / validate (push) Successful in 24s
CI / dependency-scanning (push) Successful in 29s
CI / docs (push) Successful in 40s
CI / deploy (push) Successful in 51s

- 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:
2026-03-08 23:48:25 +01:00
parent f141cc4e6a
commit a77a8a3a98
113 changed files with 3741 additions and 2923 deletions

View File

@@ -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
*/

View 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);
}
};
}

View File

@@ -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
*/