diff --git a/app/modules/tenancy/routes/pages/admin.py b/app/modules/tenancy/routes/pages/admin.py index bac5c4b1..823f64d5 100644 --- a/app/modules/tenancy/routes/pages/admin.py +++ b/app/modules/tenancy/routes/pages/admin.py @@ -288,6 +288,29 @@ async def admin_merchant_user_detail_page( ) +@router.get( + "/merchant-users/{user_id}/edit", + response_class=HTMLResponse, + include_in_schema=False, +) +async def admin_merchant_user_edit_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 edit form. + Allows editing merchant owner or store team member details. + """ + return templates.TemplateResponse( + "tenancy/admin/merchant-user-edit.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/static/admin/js/merchant-user-edit.js b/app/modules/tenancy/static/admin/js/merchant-user-edit.js new file mode 100644 index 00000000..1c928bcc --- /dev/null +++ b/app/modules/tenancy/static/admin/js/merchant-user-edit.js @@ -0,0 +1,254 @@ +// static/admin/js/merchant-user-edit.js + +// Create custom logger for merchant user edit +const merchantUserEditLog = window.LogConfig.createLogger('MERCHANT-USER-EDIT'); + +function merchantUserEditPage() { + return { + // Inherit base layout functionality from init-alpine.js + ...data(), + + // Merchant user edit page specific state + currentPage: 'merchant-users', + loading: false, + merchantUser: null, + errors: {}, + saving: false, + userId: null, + + // Editable profile form + editForm: { + username: '', + email: '', + first_name: '', + last_name: '' + }, + + // Confirmation modal state + showToggleStatusModal: false, + showDeleteModal: false, + showDeleteFinalModal: false, + + // Initialize + async init() { + try { + // Load i18n translations + await I18n.loadModule('tenancy'); + + merchantUserEditLog.info('=== MERCHANT USER EDIT PAGE INITIALIZING ==='); + + // Prevent multiple initializations + if (window._merchantUserEditInitialized) { + merchantUserEditLog.warn('Merchant user edit page already initialized, skipping...'); + return; + } + window._merchantUserEditInitialized = true; + + // Get user ID from URL + const path = window.location.pathname; + const match = path.match(/\/admin\/merchant-users\/(\d+)\/edit/); + + if (match) { + this.userId = parseInt(match[1], 10); + merchantUserEditLog.info('Editing merchant user:', this.userId); + await this.loadMerchantUser(); + } else { + merchantUserEditLog.error('No user ID in URL'); + Utils.showToast('Invalid merchant user URL', 'error'); + setTimeout(() => window.location.href = '/admin/merchant-users', 2000); + } + + merchantUserEditLog.info('=== MERCHANT USER EDIT PAGE INITIALIZATION COMPLETE ==='); + } catch (error) { + merchantUserEditLog.error('Init failed:', error); + this.error = 'Failed to initialize merchant user edit page'; + } + }, + + // Load merchant user data + async loadMerchantUser() { + merchantUserEditLog.info('Loading merchant user data...'); + this.loading = true; + + 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', duration); + + // Transform API response + this.merchantUser = { + ...response, + full_name: [response.first_name, response.last_name].filter(Boolean).join(' ') || null + }; + + // Populate editable form + this.editForm = { + username: this.merchantUser.username || '', + email: this.merchantUser.email || '', + first_name: this.merchantUser.first_name || '', + last_name: this.merchantUser.last_name || '' + }; + + merchantUserEditLog.info(`Merchant user loaded in ${duration}ms`, { + id: this.merchantUser.id, + username: this.merchantUser.username, + role: this.merchantUser.role + }); + + } catch (error) { + window.LogConfig.logError(error, 'Load Merchant User'); + Utils.showToast('Failed to load merchant user', 'error'); + setTimeout(() => window.location.href = '/admin/merchant-users', 2000); + } finally { + this.loading = false; + } + }, + + // Format date + formatDate(dateString) { + if (!dateString) { + return '-'; + } + return Utils.formatDate(dateString); + }, + + // Check if profile form has unsaved changes + get profileDirty() { + if (!this.merchantUser) return false; + return this.editForm.username !== (this.merchantUser.username || '') || + this.editForm.email !== (this.merchantUser.email || '') || + this.editForm.first_name !== (this.merchantUser.first_name || '') || + this.editForm.last_name !== (this.merchantUser.last_name || ''); + }, + + // Save profile changes + async saveProfile() { + merchantUserEditLog.info('Saving profile changes...'); + this.errors = {}; + + // Build update payload with only changed fields + const payload = {}; + if (this.editForm.username !== (this.merchantUser.username || '')) { + payload.username = this.editForm.username; + } + if (this.editForm.email !== (this.merchantUser.email || '')) { + payload.email = this.editForm.email; + } + if (this.editForm.first_name !== (this.merchantUser.first_name || '')) { + payload.first_name = this.editForm.first_name; + } + if (this.editForm.last_name !== (this.merchantUser.last_name || '')) { + payload.last_name = this.editForm.last_name; + } + + if (Object.keys(payload).length === 0) return; + + this.saving = true; + try { + const url = `/admin/users/${this.userId}`; + window.LogConfig.logApiCall('PUT', url, payload, 'request'); + + const response = await apiClient.put(url, payload); + + window.LogConfig.logApiCall('PUT', url, response, 'response'); + + // Update local state + this.merchantUser.username = response.username; + this.merchantUser.email = response.email; + this.merchantUser.first_name = response.first_name; + this.merchantUser.last_name = response.last_name; + this.merchantUser.full_name = [response.first_name, response.last_name].filter(Boolean).join(' ') || null; + + // Re-sync form + this.editForm = { + username: response.username || '', + email: response.email || '', + first_name: response.first_name || '', + last_name: response.last_name || '' + }; + + Utils.showToast('Profile updated successfully', 'success'); + merchantUserEditLog.info('Profile updated successfully'); + + } catch (error) { + window.LogConfig.logError(error, 'Save Profile'); + if (error.details) { + for (const detail of error.details) { + const field = detail.loc?.[detail.loc.length - 1]; + if (field) this.errors[field] = detail.msg; + } + } + Utils.showToast(error.message || 'Failed to save profile', 'error'); + } finally { + this.saving = false; + } + }, + + // Toggle user status + async toggleStatus() { + const action = this.merchantUser.is_active ? 'deactivate' : 'activate'; + merchantUserEditLog.info(`Toggle status: ${action}`); + + 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'); + merchantUserEditLog.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; + } + }, + + // Intermediate step for double-confirm delete + confirmDeleteStep() { + merchantUserEditLog.info('First delete confirmation accepted, showing final confirmation'); + this.showDeleteFinalModal = true; + }, + + // Delete user + async deleteUser() { + merchantUserEditLog.info('Delete user requested:', this.userId); + + 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'); + merchantUserEditLog.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; + } + } + }; +} + +merchantUserEditLog.info('Merchant user edit 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 43c53058..365a3bcb 100644 --- a/app/modules/tenancy/static/admin/js/merchant-users.js +++ b/app/modules/tenancy/static/admin/js/merchant-users.js @@ -16,10 +16,8 @@ function merchantUsersPage() { merchantUsers: [], loading: false, error: null, - showToggleStatusModal: false, showDeleteModal: false, showDeleteFinalModal: false, - userToToggle: null, userToDelete: null, filters: { search: '', @@ -230,30 +228,6 @@ function merchantUsersPage() { } }, - // Toggle user active status - async toggleUserStatus(user) { - const action = user.is_active ? 'deactivate' : 'activate'; - merchantUsersLog.info(`Toggle status: ${action} for user`, user.username); - - 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'); - } - }, - // Intermediate step for double-confirm delete confirmDeleteStep() { merchantUsersLog.info('First delete confirmation accepted, showing final confirmation'); diff --git a/app/modules/tenancy/templates/tenancy/admin/merchant-user-edit.html b/app/modules/tenancy/templates/tenancy/admin/merchant-user-edit.html new file mode 100644 index 00000000..ca635ac5 --- /dev/null +++ b/app/modules/tenancy/templates/tenancy/admin/merchant-user-edit.html @@ -0,0 +1,251 @@ +{# app/templates/admin/merchant-user-edit.html #} +{% extends "admin/base.html" %} +{% from 'shared/macros/alerts.html' import loading_state %} +{% from 'shared/macros/headers.html' import edit_page_header %} +{% from 'shared/macros/modals.html' import confirm_modal_dynamic %} + +{% block title %}Edit Merchant User{% endblock %} + +{% block alpine_data %}merchantUserEditPage(){% endblock %} + +{% block content %} +{% call edit_page_header('Edit Merchant User', '/admin/merchant-users', subtitle_show='merchantUser', back_label='Back to Merchant Users') %} + @ +{% endcall %} + +{{ loading_state('Loading merchant user...', show_condition='loading') }} + + +
+ +
+

+ Quick Actions +

+
+ + + + +
+ + Merchant Owner + + + Store Member + + + Active + + + Inactive + +
+
+
+ + +
+
+

+ User Information +

+ +
+
+
+ +
+
+ + +

+
+
+ + +

+
+
+ +
+
+ + +
+
+ + +
+
+
+
+ Unsaved changes + +
+
+
+ + + + + + + + +
+

+ Danger Zone +

+
+ + +
+

+ + Deleting a user is permanent and cannot be undone. +

+
+
+ + +{{ confirm_modal_dynamic( + 'toggleStatusModal', + 'Toggle User Status', + "'Are you sure you want to ' + (merchantUser?.is_active ? 'deactivate' : 'activate') + ' \"' + (merchantUser?.full_name || merchantUser?.username || '') + '\"?'", + 'toggleStatus()', + 'showToggleStatusModal', + 'Confirm', + 'Cancel', + 'warning' +) }} + + +{{ confirm_modal_dynamic( + 'deleteUserModal', + 'Delete User', + "'Are you sure you want to delete \"' + (merchantUser?.full_name || merchantUser?.username || '') + '\"? This action cannot be undone.'", + 'confirmDeleteStep()', + 'showDeleteModal', + 'Delete', + 'Cancel', + 'danger' +) }} + + +{{ confirm_modal_dynamic( + 'deleteUserFinalModal', + 'Final Confirmation', + "'FINAL CONFIRMATION: Are you absolutely sure you want to permanently delete \"' + (merchantUser?.full_name || merchantUser?.username || '') + '\"?'", + 'deleteUser()', + 'showDeleteFinalModal', + 'Permanently Delete', + 'Cancel', + 'danger' +) }} +{% 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 099a98a2..ce1c0ac7 100644 --- a/app/modules/tenancy/templates/tenancy/admin/merchant-users.html +++ b/app/modules/tenancy/templates/tenancy/admin/merchant-users.html @@ -4,7 +4,7 @@ {% from 'shared/macros/headers.html' import page_header %} {% from 'shared/macros/alerts.html' import loading_state, error_state %} {% from 'shared/macros/tables.html' import table_wrapper, table_header %} -{% from 'shared/macros/modals.html' import confirm_modal, confirm_modal_dynamic %} +{% from 'shared/macros/modals.html' import confirm_modal_dynamic %} {% block title %}Merchant Users{% endblock %} @@ -197,15 +197,14 @@ - - + +