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:
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user