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
+
+
+
+
+
+
+
+
+
+
+
+ Owned Merchants
+
+
+
+
+
+
+
+
+
+
+ Store Memberships
+
+
+
+
+
+
+
+
+ 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 @@
-
-
+
+