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

@@ -264,7 +264,7 @@ function languageSelector(currentLang, enabledLanguages) {
return {
isLangOpen: false,
currentLang: currentLang || 'fr',
languages: enabledLanguages || ['fr', 'de', 'en'],
languages: enabledLanguages || ['en', 'fr', 'de', 'lb'],
languageNames: {
'en': 'English',
'fr': 'Français',

View File

@@ -6,6 +6,27 @@
// Create custom logger for login page
const loginLog = window.LogConfig.createLogger('LOGIN');
function languageSelector(currentLang, enabledLanguages) {
return {
currentLang: currentLang || 'en',
languages: enabledLanguages || ['en', 'fr', 'de', 'lb'],
async setLanguage(lang) {
if (lang === this.currentLang) return;
try {
// noqa: JS-008 - Login page has no apiClient; raw fetch is intentional
await fetch('/api/v1/platform/language/set', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ language: lang }),
});
window.location.reload();
} catch (error) {
loginLog.error('Failed to set language:', error);
}
},
};
}
function adminLogin() {
return {
dark: false,
@@ -17,6 +38,10 @@ function adminLogin() {
error: null,
success: null,
errors: {},
rememberMe: false,
showForgotPassword: false,
forgotPasswordEmail: '',
forgotPasswordLoading: false,
init() {
// Guard against multiple initialization
@@ -196,6 +221,14 @@ function adminLogin() {
window.location.href = '/admin/select-platform';
return;
}
if (platformsResponse.is_super_admin && !platformsResponse.current_platform_id) {
// Super admin with no platform selected - offer platform selection
loginLog.info('Super admin without platform, redirecting to platform selector...');
this.success = 'Login successful! Select a platform or stay in global mode...';
window.location.href = '/admin/select-platform';
return;
}
} catch (platformError) {
loginLog.warn('Could not check platforms, proceeding to dashboard:', platformError);
}
@@ -233,6 +266,28 @@ function adminLogin() {
}
},
async handleForgotPassword() {
if (!this.forgotPasswordEmail) {
this.error = 'Please enter your email address';
return;
}
this.forgotPasswordLoading = true;
this.clearErrors();
try {
const response = await apiClient.post('/admin/auth/forgot-password', {
email: this.forgotPasswordEmail
});
this.success = response.message || 'If an account exists with that email, a reset link has been sent.';
this.forgotPasswordEmail = '';
} catch (error) {
// Show generic message to prevent email enumeration
this.success = 'If an account exists with that email, a reset link has been sent.';
} finally {
this.forgotPasswordLoading = false;
}
},
toggleDarkMode() {
loginLog.debug('Toggling dark mode...');
this.dark = !this.dark;

View File

@@ -1,177 +0,0 @@
// static/admin/js/my-menu-config.js
// Personal menu configuration for super admins
//
// NOTE: The page method for loading user menu config is named loadUserMenuConfig()
// (not loadMenuConfig()) to avoid shadowing the sidebar's loadMenuConfig() inherited
// from data() via the spread operator. Shadowing caused the sidebar to never populate
// its menuData, resulting in a blank sidebar on this page.
const myMenuConfigLog = window.LogConfig?.loggers?.myMenuConfig || window.LogConfig?.createLogger?.('myMenuConfig') || console;
function adminMyMenuConfig() {
return {
// Inherit base layout functionality from init-alpine.js
...data(),
// Page-specific state
currentPage: 'my-menu',
loading: true,
error: null,
successMessage: null,
saving: false,
// Data
menuConfig: null,
showShowAllModal: false,
showHideAllModal: false,
// Computed grouped items
get groupedItems() {
if (!this.menuConfig?.items) return [];
// Group items by section
const sections = {};
for (const item of this.menuConfig.items) {
const sectionId = item.section_id;
if (!sections[sectionId]) {
sections[sectionId] = {
id: sectionId,
label: item.section_label,
isSuperAdminOnly: item.is_super_admin_only,
items: [],
visibleCount: 0
};
}
sections[sectionId].items.push(item);
if (item.is_visible) {
sections[sectionId].visibleCount++;
}
}
// Convert to array and maintain order
return Object.values(sections);
},
async init() {
// Guard against multiple initialization
if (window._adminMyMenuConfigInitialized) {
myMenuConfigLog.warn('Already initialized, skipping');
return;
}
window._adminMyMenuConfigInitialized = true;
myMenuConfigLog.info('=== MY MENU CONFIG PAGE INITIALIZING ===');
try {
// Load core translations for confirmations
await I18n.loadModule('core');
await this.loadUserMenuConfig();
myMenuConfigLog.info('=== MY MENU CONFIG PAGE INITIALIZED ===');
} catch (error) {
myMenuConfigLog.error('Failed to initialize my menu config page:', error);
this.error = 'Failed to load page data. Please refresh.';
}
},
async refresh() {
this.error = null;
this.successMessage = null;
await this.loadUserMenuConfig();
},
async loadUserMenuConfig() {
this.loading = true;
this.error = null;
try {
this.menuConfig = await apiClient.get('/admin/menu-config/user');
myMenuConfigLog.info('Loaded menu config:', {
totalItems: this.menuConfig?.total_items,
visibleItems: this.menuConfig?.visible_items
});
} catch (error) {
myMenuConfigLog.error('Failed to load menu config:', error);
this.error = error.message || 'Failed to load menu configuration';
} finally {
this.loading = false;
}
},
async toggleVisibility(item) {
if (item.is_mandatory) {
myMenuConfigLog.warn('Cannot toggle mandatory item:', item.id);
return;
}
this.saving = true;
this.error = null;
this.successMessage = null;
const newVisibility = !item.is_visible;
try {
await apiClient.put('/admin/menu-config/user', {
menu_item_id: item.id,
is_visible: newVisibility
});
// Update local state
item.is_visible = newVisibility;
// Update counts
if (newVisibility) {
this.menuConfig.visible_items++;
this.menuConfig.hidden_items--;
} else {
this.menuConfig.visible_items--;
this.menuConfig.hidden_items++;
}
myMenuConfigLog.info('Toggled visibility:', item.id, newVisibility);
// Reload the page to refresh sidebar
window.location.reload();
} catch (error) {
myMenuConfigLog.error('Failed to toggle visibility:', error);
this.error = error.message || 'Failed to update menu visibility';
this.saving = false;
}
},
async showAll() {
this.saving = true;
this.error = null;
this.successMessage = null;
try {
await apiClient.post('/admin/menu-config/user/show-all');
myMenuConfigLog.info('Showed all menu items');
// Reload the page to refresh sidebar
window.location.reload();
} catch (error) {
myMenuConfigLog.error('Failed to show all menu items:', error);
this.error = error.message || 'Failed to show all menu items';
this.saving = false;
}
},
async resetToDefaults() {
this.saving = true;
this.error = null;
this.successMessage = null;
try {
await apiClient.post('/admin/menu-config/user/reset');
myMenuConfigLog.info('Reset menu config to defaults');
// Reload the page to refresh sidebar
window.location.reload();
} catch (error) {
myMenuConfigLog.error('Failed to reset menu config:', error);
this.error = error.message || 'Failed to reset menu configuration';
this.saving = false;
}
}
};
}

View File

@@ -5,6 +5,27 @@
// Use centralized logger
const loginLog = window.LogConfig.createLogger('MERCHANT-LOGIN');
function languageSelector(currentLang, enabledLanguages) {
return {
currentLang: currentLang || 'fr',
languages: enabledLanguages || ['en', 'fr', 'de', 'lb'],
async setLanguage(lang) {
if (lang === this.currentLang) return;
try {
// noqa: JS-008 - Login page has no apiClient; raw fetch is intentional
await fetch('/api/v1/platform/language/set', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ language: lang }),
});
window.location.reload();
} catch (error) {
loginLog.error('Failed to set language:', error);
}
},
};
}
function merchantLogin() {
return {
dark: false,
@@ -134,6 +155,7 @@ function merchantLogin() {
},
// Forgot password state
rememberMe: false,
showForgotPassword: false,
forgotPasswordEmail: '',
forgotPasswordLoading: false,

View File

@@ -15,7 +15,9 @@ storeDashLog.info('[STORE DASHBOARD] data function exists?', typeof data);
* Fetches onboarding steps from API, supports session-scoped dismiss.
*/
function onboardingBanner() {
const t = (key, vars) => I18n.t(key, vars);
return {
t,
visible: false,
steps: [],
totalSteps: 0,
@@ -30,7 +32,19 @@ function onboardingBanner() {
try {
const response = await apiClient.get('/store/dashboard/onboarding');
this.steps = response.steps || [];
const steps = response.steps || [];
// Load module translations BEFORE setting reactive data
// Keys are like "tenancy.onboarding...." — first segment is the module
const modules = new Set();
for (const step of steps) {
const mod = step.title_key?.split('.')[0];
if (mod) modules.add(mod);
}
await Promise.all([...modules].map(m => I18n.loadModule(m)));
// Now set reactive data — Alpine re-renders with translations ready
this.steps = steps;
this.totalSteps = response.total_steps || 0;
this.completedSteps = response.completed_steps || 0;
this.progressPercentage = response.progress_percentage || 0;

View File

@@ -253,7 +253,7 @@ function languageSelector(currentLang, enabledLanguages) {
return {
isLangOpen: false,
currentLang: currentLang || 'fr',
languages: enabledLanguages || ['fr', 'de', 'en'],
languages: enabledLanguages || ['en', 'fr', 'de', 'lb'],
languageNames: {
'en': 'English',
'fr': 'Français',