diff --git a/app/modules/tenancy/routes/pages/admin.py b/app/modules/tenancy/routes/pages/admin.py index 52a91eef..c804db86 100644 --- a/app/modules/tenancy/routes/pages/admin.py +++ b/app/modules/tenancy/routes/pages/admin.py @@ -267,6 +267,27 @@ async def admin_merchant_users_list_page( ) +@router.get( + "/merchant-users/{user_id}", response_class=HTMLResponse, include_in_schema=False +) +async def admin_merchant_user_detail_page( + request: Request, + user_id: int = Path(..., description="User ID"), + current_user: User = Depends( + require_menu_access("merchant-users", FrontendType.ADMIN) + ), + db: Session = Depends(get_db), +): + """ + Render merchant user detail view. + Shows details for a merchant owner or store team member. + """ + return templates.TemplateResponse( + "tenancy/admin/merchant-user-detail.html", + get_admin_context(request, db, current_user, user_id=user_id), + ) + + # ============================================================================ # ADMIN USER MANAGEMENT ROUTES (Super Admin Only) # ============================================================================ diff --git a/app/modules/tenancy/services/tenancy_metrics.py b/app/modules/tenancy/services/tenancy_metrics.py index 0170b4a4..6766103e 100644 --- a/app/modules/tenancy/services/tenancy_metrics.py +++ b/app/modules/tenancy/services/tenancy_metrics.py @@ -374,7 +374,7 @@ class TenancyMetricsProvider: value=merchant_owners, label="Merchant Owners", category="tenancy", - icon="briefcase", + icon="office-building", description="Distinct merchant owners", ), MetricValue( diff --git a/app/modules/tenancy/static/admin/js/merchant-user-detail.js b/app/modules/tenancy/static/admin/js/merchant-user-detail.js new file mode 100644 index 00000000..a44d727d --- /dev/null +++ b/app/modules/tenancy/static/admin/js/merchant-user-detail.js @@ -0,0 +1,177 @@ +// noqa: js-006 - async init pattern is safe, loadData has try/catch +// static/admin/js/merchant-user-detail.js + +// Create custom logger for merchant user detail +const merchantUserDetailLog = window.LogConfig.createLogger('MERCHANT-USER-DETAIL'); + +function merchantUserDetailPage() { + return { + // Inherit base layout functionality from init-alpine.js + ...data(), + + // Merchant user detail page specific state + currentPage: 'merchant-users', + merchantUser: null, + loading: false, + saving: false, + error: null, + userId: null, + + // Initialize + async init() { + // Load i18n translations + await I18n.loadModule('tenancy'); + + merchantUserDetailLog.info('=== MERCHANT USER DETAIL PAGE INITIALIZING ==='); + + // Prevent multiple initializations + if (window._merchantUserDetailInitialized) { + merchantUserDetailLog.warn('Merchant user detail page already initialized, skipping...'); + return; + } + window._merchantUserDetailInitialized = true; + + // Get user ID from URL + const path = window.location.pathname; + const match = path.match(/\/admin\/merchant-users\/(\d+)$/); + + if (match) { + this.userId = match[1]; + merchantUserDetailLog.info('Viewing merchant user:', this.userId); + await this.loadMerchantUser(); + } else { + merchantUserDetailLog.error('No user ID in URL'); + this.error = 'Invalid merchant user URL'; + Utils.showToast('Invalid merchant user URL', 'error'); + } + + merchantUserDetailLog.info('=== MERCHANT USER DETAIL PAGE INITIALIZATION COMPLETE ==='); + }, + + // Load merchant user data + async loadMerchantUser() { + merchantUserDetailLog.info('Loading merchant user details...'); + this.loading = true; + this.error = null; + + try { + const url = `/admin/users/${this.userId}`; + window.LogConfig.logApiCall('GET', url, null, 'request'); + + const startTime = performance.now(); + const response = await apiClient.get(url); + const duration = performance.now() - startTime; + + window.LogConfig.logApiCall('GET', url, response, 'response'); + window.LogConfig.logPerformance('Load Merchant User Details', duration); + + // Transform API response + this.merchantUser = { + ...response, + full_name: [response.first_name, response.last_name].filter(Boolean).join(' ') || null + }; + + merchantUserDetailLog.info(`Merchant user loaded in ${duration}ms`, { + id: this.merchantUser.id, + username: this.merchantUser.username, + owned_merchants_count: this.merchantUser.owned_merchants_count, + store_memberships_count: this.merchantUser.store_memberships_count + }); + + } catch (error) { + window.LogConfig.logError(error, 'Load Merchant User Details'); + this.error = error.message || 'Failed to load merchant user details'; + Utils.showToast('Failed to load merchant user details', 'error'); + } finally { + this.loading = false; + } + }, + + // Format date + formatDate(dateString) { + if (!dateString) { + return '-'; + } + return Utils.formatDate(dateString); + }, + + // Toggle user status + async toggleStatus() { + const action = this.merchantUser.is_active ? 'deactivate' : 'activate'; + merchantUserDetailLog.info(`Toggle status: ${action}`); + + if (!confirm(`Are you sure you want to ${action} "${this.merchantUser.full_name || this.merchantUser.username}"?`)) { + merchantUserDetailLog.info('Status toggle cancelled by user'); + return; + } + + this.saving = true; + try { + const url = `/admin/users/${this.userId}/status`; + window.LogConfig.logApiCall('PUT', url, null, 'request'); + + const response = await apiClient.put(url); + + window.LogConfig.logApiCall('PUT', url, response, 'response'); + + this.merchantUser.is_active = response.is_active; + Utils.showToast(`User ${action}d successfully`, 'success'); + merchantUserDetailLog.info(`User ${action}d successfully`); + + } catch (error) { + window.LogConfig.logError(error, `Toggle Status (${action})`); + Utils.showToast(error.message || `Failed to ${action} user`, 'error'); + } finally { + this.saving = false; + } + }, + + // Delete user + async deleteUser() { + merchantUserDetailLog.info('Delete user requested:', this.userId); + + if (!confirm(`Are you sure you want to delete "${this.merchantUser.full_name || this.merchantUser.username}"?\n\nThis action cannot be undone.`)) { + merchantUserDetailLog.info('Delete cancelled by user'); + return; + } + + // Second confirmation for safety + if (!confirm(`FINAL CONFIRMATION\n\nAre you absolutely sure you want to delete "${this.merchantUser.full_name || this.merchantUser.username}"?`)) { + merchantUserDetailLog.info('Delete cancelled by user (second confirmation)'); + return; + } + + this.saving = true; + try { + const url = `/admin/users/${this.userId}`; + window.LogConfig.logApiCall('DELETE', url, null, 'request'); + + await apiClient.delete(url); + + window.LogConfig.logApiCall('DELETE', url, null, 'response'); + + Utils.showToast('User deleted successfully', 'success'); + merchantUserDetailLog.info('User deleted successfully'); + + // Redirect to merchant users list + setTimeout(() => window.location.href = '/admin/merchant-users', 1500); + + } catch (error) { + window.LogConfig.logError(error, 'Delete User'); + Utils.showToast(error.message || 'Failed to delete user', 'error'); + } finally { + this.saving = false; + } + }, + + // Refresh user data + async refresh() { + merchantUserDetailLog.info('=== MERCHANT USER REFRESH TRIGGERED ==='); + await this.loadMerchantUser(); + Utils.showToast('Merchant user details refreshed', 'success'); + merchantUserDetailLog.info('=== MERCHANT USER REFRESH COMPLETE ==='); + } + }; +} + +merchantUserDetailLog.info('Merchant user detail module loaded'); diff --git a/app/modules/tenancy/static/admin/js/merchant-users.js b/app/modules/tenancy/static/admin/js/merchant-users.js index 4a6c2dc9..58618b74 100644 --- a/app/modules/tenancy/static/admin/js/merchant-users.js +++ b/app/modules/tenancy/static/admin/js/merchant-users.js @@ -223,6 +223,69 @@ function merchantUsersPage() { merchantUsersLog.info('Go to page:', this.pagination.page); this.loadMerchantUsers(); } + }, + + // Toggle user active status + async toggleUserStatus(user) { + const action = user.is_active ? 'deactivate' : 'activate'; + merchantUsersLog.info(`Toggle status: ${action} for user`, user.username); + + if (!confirm(`Are you sure you want to ${action} "${user.full_name || user.username || user.email}"?`)) { + merchantUsersLog.info('Status toggle cancelled by user'); + return; + } + + try { + const url = `/admin/users/${user.id}/status`; + window.LogConfig.logApiCall('PUT', url, null, 'request'); + + const response = await apiClient.put(url); + + window.LogConfig.logApiCall('PUT', url, response, 'response'); + + user.is_active = response.is_active; + Utils.showToast(`User ${action}d successfully`, 'success'); + merchantUsersLog.info(`User ${action}d successfully`); + + await this.loadStats(); + } catch (error) { + window.LogConfig.logError(error, `Toggle Status (${action})`); + Utils.showToast(error.message || `Failed to ${action} user`, 'error'); + } + }, + + // Delete user + async deleteUser(user) { + merchantUsersLog.warn('Delete user requested:', user.username); + + if (!confirm(`Are you sure you want to delete "${user.full_name || user.username || user.email}"?\n\nThis action cannot be undone.`)) { + merchantUsersLog.info('Delete cancelled by user'); + return; + } + + // Second confirmation for safety + if (!confirm(`FINAL CONFIRMATION\n\nAre you absolutely sure you want to delete "${user.full_name || user.username || user.email}"?`)) { + merchantUsersLog.info('Delete cancelled by user (second confirmation)'); + return; + } + + try { + const url = `/admin/users/${user.id}`; + window.LogConfig.logApiCall('DELETE', url, null, 'request'); + + await apiClient.delete(url); + + window.LogConfig.logApiCall('DELETE', url, null, 'response'); + + Utils.showToast('User deleted successfully', 'success'); + merchantUsersLog.info('User deleted successfully'); + + await this.loadMerchantUsers(); + await this.loadStats(); + } catch (error) { + window.LogConfig.logError(error, 'Delete User'); + Utils.showToast(error.message || 'Failed to delete user', 'error'); + } } }; } diff --git a/app/modules/tenancy/templates/tenancy/admin/merchant-user-detail.html b/app/modules/tenancy/templates/tenancy/admin/merchant-user-detail.html new file mode 100644 index 00000000..9d663ac6 --- /dev/null +++ b/app/modules/tenancy/templates/tenancy/admin/merchant-user-detail.html @@ -0,0 +1,224 @@ +{# app/templates/admin/merchant-user-detail.html #} +{% extends "admin/base.html" %} +{% from 'shared/macros/alerts.html' import loading_state, error_state %} +{% from 'shared/macros/headers.html' import detail_page_header %} + +{% block title %}Merchant User Details{% endblock %} + +{% block alpine_data %}merchantUserDetailPage(){% endblock %} + +{% block content %} +{% call detail_page_header("merchantUser?.full_name || merchantUser?.username || 'Merchant User Details'", '/admin/merchant-users', subtitle_show='merchantUser') %} + @ + | + +{% endcall %} + +{{ loading_state('Loading merchant user details...') }} + +{{ error_state('Error loading merchant user') }} + + +
+ +
+

+ Quick Actions +

+
+ + +
+
+ + +
+ +
+
+ +
+
+

+ User Type +

+

+ - +

+
+
+ + +
+
+ +
+
+

+ Status +

+

+ - +

+
+
+ + +
+
+ +
+
+

+ Merchants Owned +

+

+ 0 +

+
+
+ + +
+
+ +
+
+

+ Store Memberships +

+

+ 0 +

+
+
+
+ + +
+ +
+

+ Account Information +

+
+
+

Username

+

@

+
+
+

Email

+

-

+
+
+

Role

+

-

+
+
+

Email Verified

+ + +
+
+
+ + +
+

+ Personal Information +

+
+
+

Full Name

+

-

+
+
+

First Name

+

-

+
+
+

Last Name

+

-

+
+
+
+
+ + + + + + + + +
+

+ Activity Information +

+
+
+

Last Login

+

-

+
+
+

Created At

+

-

+
+
+

Last Updated

+

-

+
+
+
+
+{% endblock %} + +{% block extra_scripts %} + +{% endblock %} diff --git a/app/modules/tenancy/templates/tenancy/admin/merchant-users.html b/app/modules/tenancy/templates/tenancy/admin/merchant-users.html index d994c0b5..a185e391 100644 --- a/app/modules/tenancy/templates/tenancy/admin/merchant-users.html +++ b/app/modules/tenancy/templates/tenancy/admin/merchant-users.html @@ -36,7 +36,7 @@
- +

@@ -189,12 +189,31 @@

+ + + + + +