PHP Classes

File: templates/admin_users.twig

Recommend this page to a friend!
  Packages of Matthew Asham   Binkterm PHP   templates/admin_users.twig   Download  
File: templates/admin_users.twig
Role: Auxiliary data
Content type: text/plain
Description: Auxiliary data
Class: Binkterm PHP
Bulletin board system based on the Web
Author: By
Last change:
Date: 5 days ago
Size: 58,197 bytes
 

Contents

Class file image Download
{% extends "base.twig" %} {% block title %}{{ t('ui.admin_users.page_title', {}, locale, ['common']) }}{% endblock %} {% block content %} <div class="container-fluid mt-4"> <div class="row"> <div class="col-12"> <div class="d-flex justify-content-between align-items-center mb-4"> <h2><i class="fas fa-users me-2"></i>{{ t('ui.admin_users.heading', {}, locale, ['common']) }}</h2> <div> <button class="btn btn-success me-2" onclick="createNewUser()"> <i class="fas fa-user-plus me-2"></i>{{ t('ui.admin_users.create_new_user', {}, locale, ['common']) }} </button> <button class="btn btn-info me-2" onclick="showUsersNeedingReminders()"> <i class="fas fa-bell me-2"></i>{{ t('ui.admin_users.need_reminders', {}, locale, ['common']) }} <span id="reminders-count" class="badge bg-light text-dark ms-1" style="display: none;">0</span> </button> <button class="btn btn-warning me-2" onclick="cleanupRegistrations()"> <i class="fas fa-broom me-2"></i>{{ t('ui.admin_users.cleanup', {}, locale, ['common']) }} </button> <button class="btn btn-primary" onclick="refreshData()"> <i class="fas fa-sync-alt me-2"></i>{{ t('ui.common.refresh', {}, locale, ['common']) }} </button> </div> </div> <!-- Pending Registrations --> <div class="card mb-4"> <div class="card-header"> <h5 class="mb-0"> <i class="fas fa-user-clock me-2"></i>{{ t('ui.admin_users.pending_registrations', {}, locale, ['common']) }} <span id="pending-count" class="badge bg-warning ms-2">0</span> </h5> </div> <div class="card-body"> <div id="pending-users-container"> <div class="text-center py-4"> <div class="spinner-border" role="status"> <span class="visually-hidden">{{ t('ui.common.loading', {}, locale, ['common']) }}</span> </div> <p class="mt-2">{{ t('ui.admin_users.loading_pending_registrations', {}, locale, ['common']) }}</p> </div> </div> </div> </div> <!-- All Users --> <div class="card"> <div class="card-header"> <h5 class="mb-0"> <i class="fas fa-users me-2"></i>{{ t('ui.admin_users.all_users', {}, locale, ['common']) }} <span id="users-count" class="badge bg-info ms-2">0</span> </h5> </div> <div class="card-body"> <!-- Search and pagination controls --> <div class="row mb-3"> <div class="col-md-6"> <div class="input-group"> <input type="text" class="form-control" id="user-search" placeholder="{{ t('ui.admin_users.search_users_placeholder', {}, locale, ['common']) }}"> <button class="btn btn-outline-secondary" type="button" onclick="searchUsers()"> <i class="fas fa-search"></i> </button> </div> </div> <div class="col-md-6"> <div class="d-flex justify-content-end"> <select class="form-select" id="users-per-page" onchange="changeUsersPerPage()" style="width: auto;"> <option value="25">{{ t('ui.admin_users.per_page_25', {}, locale, ['common']) }}</option> <option value="50">{{ t('ui.admin_users.per_page_50', {}, locale, ['common']) }}</option> <option value="100">{{ t('ui.admin_users.per_page_100', {}, locale, ['common']) }}</option> </select> </div> </div> </div> <div id="users-container"> <div class="text-center py-4"> <div class="spinner-border" role="status"> <span class="visually-hidden">{{ t('ui.common.loading', {}, locale, ['common']) }}</span> </div> <p class="mt-2">{{ t('ui.admin_users.loading_users', {}, locale, ['common']) }}</p> </div> </div> <!-- Pagination --> <div id="users-pagination" class="d-flex justify-content-center mt-4" style="display: none !important;"> <!-- Pagination will be inserted here --> </div> </div> </div> </div> </div> </div> <!-- Approval Modal --> <div class="modal fade" id="approvalModal" tabindex="-1"> <div class="modal-dialog modal-lg"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title"> <i class="fas fa-user-check me-2"></i> <span id="approval-action-title">{{ t('ui.admin_users.approve_user', {}, locale, ['common']) }}</span> </h5> <button type="button" class="btn-close" data-bs-dismiss="modal"></button> </div> <div class="modal-body"> <div id="user-details"></div> <div class="mt-4"> <label for="admin-notes" class="form-label">{{ t('ui.admin_users.admin_notes', {}, locale, ['common']) }}</label> <textarea class="form-control" id="admin-notes" rows="3" placeholder="{{ t('ui.admin_users.optional_notes_placeholder', {}, locale, ['common']) }}"></textarea> </div> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ t('ui.common.cancel', {}, locale, ['common']) }}</button> <button type="button" class="btn btn-success" id="approve-btn" onclick="processUser('approve')"> <i class="fas fa-check me-2"></i>{{ t('ui.admin_users.approve', {}, locale, ['common']) }} </button> <button type="button" class="btn btn-danger" id="reject-btn" onclick="processUser('reject')"> <i class="fas fa-times me-2"></i>{{ t('ui.admin_users.reject', {}, locale, ['common']) }} </button> </div> </div> </div> </div> <!-- Create New User Modal --> <div class="modal fade" id="createUserModal" tabindex="-1"> <div class="modal-dialog modal-lg"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title"> <i class="fas fa-user-plus me-2"></i>{{ t('ui.admin_users.create_new_user', {}, locale, ['common']) }} </h5> <button type="button" class="btn-close" data-bs-dismiss="modal"></button> </div> <div class="modal-body"> <form id="createUserForm"> <div class="row"> <div class="col-md-6"> <div class="mb-3"> <label for="create-username" class="form-label">{{ t('ui.common.username', {}, locale, ['common']) }}</label> <input type="text" class="form-control" id="create-username" required pattern="[a-zA-Z0-9_]{3,20}" title="{{ t('ui.admin_users.username_validation', {}, locale, ['common']) }}"> <div class="form-text">{{ t('ui.admin_users.username_validation', {}, locale, ['common']) }}</div> </div> </div> <div class="col-md-6"> <div class="mb-3"> <label for="create-real-name" class="form-label">{{ t('ui.common.real_name', {}, locale, ['common']) }}</label> <input type="text" class="form-control" id="create-real-name" required> </div> </div> </div> <div class="mb-3"> <label for="create-email" class="form-label">{{ t('ui.common.email_address', {}, locale, ['common']) }}</label> <input type="email" class="form-control" id="create-email"> <div class="form-text">{{ t('ui.admin_users.email_optional_help', {}, locale, ['common']) }}</div> </div> <div class="mb-3"> <label for="create-password" class="form-label">{{ t('ui.common.password', {}, locale, ['common']) }}</label> <input type="password" class="form-control" id="create-password" required minlength="8"> <div class="form-text">{{ t('ui.admin_users.min_8_characters', {}, locale, ['common']) }}</div> </div> <div class="mb-3"> <label for="create-confirm-password" class="form-label">{{ t('ui.register.confirm_password', {}, locale, ['common']) }}</label> <input type="password" class="form-control" id="create-confirm-password" required> </div> <div class="row"> <div class="col-md-6"> <div class="form-check mb-3"> <input class="form-check-input" type="checkbox" id="create-is-active" checked> <label class="form-check-label" for="create-is-active"> {{ t('ui.admin_users.account_is_active', {}, locale, ['common']) }} </label> <div class="form-text">{{ t('ui.admin_users.inactive_cannot_login', {}, locale, ['common']) }}</div> </div> </div> <div class="col-md-6"> <div class="form-check mb-3"> <input class="form-check-input" type="checkbox" id="create-is-admin"> <label class="form-check-label" for="create-is-admin"> {{ t('ui.admin_users.admin_privileges', {}, locale, ['common']) }} </label> <div class="form-text">{{ t('ui.admin_users.admin_privileges_help', {}, locale, ['common']) }}</div> </div> <div class="form-check mb-3"> <input class="form-check-input" type="checkbox" id="create-is-system"> <label class="form-check-label" for="create-is-system"> {{ t('ui.admin_users.is_system_account', {}, locale, ['common']) }} </label> <div class="form-text">{{ t('ui.admin_users.is_system_account_help', {}, locale, ['common']) }}</div> </div> </div> </div> </form> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ t('ui.common.cancel', {}, locale, ['common']) }}</button> <button type="button" class="btn btn-success" onclick="saveNewUser()"> <i class="fas fa-user-plus me-2"></i>{{ t('ui.admin_users.create_user', {}, locale, ['common']) }} </button> </div> </div> </div> </div> <!-- Edit User Modal --> <div class="modal fade" id="editUserModal" tabindex="-1"> <div class="modal-dialog modal-lg"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title"> <i class="fas fa-user-edit me-2"></i>{{ t('ui.admin_users.edit_user', {}, locale, ['common']) }} </h5> <button type="button" class="btn-close" data-bs-dismiss="modal"></button> </div> <div class="modal-body"> <form id="editUserForm"> <input type="hidden" id="edit-user-id"> <div class="row"> <div class="col-md-6"> <div class="mb-3"> <label for="edit-username" class="form-label">{{ t('ui.common.username', {}, locale, ['common']) }}</label> <input type="text" class="form-control" id="edit-username" readonly> <div class="form-text">{{ t('ui.admin_users.username_cannot_change', {}, locale, ['common']) }}</div> </div> </div> <div class="col-md-6"> <div class="mb-3"> <label for="edit-real-name" class="form-label">{{ t('ui.common.real_name', {}, locale, ['common']) }}</label> <input type="text" class="form-control" id="edit-real-name" readonly> <div class="form-text">{{ t('ui.admin_users.real_name_cannot_change', {}, locale, ['common']) }}</div> </div> </div> </div> <div class="mb-3"> <label for="edit-email" class="form-label">{{ t('ui.common.email_address', {}, locale, ['common']) }}</label> <input type="email" class="form-control" id="edit-email"> </div> <div class="row"> <div class="col-md-6"> <div class="form-check mb-3"> <input class="form-check-input" type="checkbox" id="edit-is-active"> <label class="form-check-label" for="edit-is-active"> {{ t('ui.admin_users.account_is_active', {}, locale, ['common']) }} </label> <div class="form-text">{{ t('ui.admin_users.inactive_cannot_login', {}, locale, ['common']) }}</div> </div> </div> <div class="col-md-6"> <div class="form-check mb-3"> <input class="form-check-input" type="checkbox" id="edit-is-admin"> <label class="form-check-label" for="edit-is-admin"> {{ t('ui.admin_users.admin_privileges', {}, locale, ['common']) }} </label> <div class="form-text">{{ t('ui.admin_users.admin_privileges_help', {}, locale, ['common']) }}</div> </div> <div class="form-check mb-3"> <input class="form-check-input" type="checkbox" id="edit-is-system"> <label class="form-check-label" for="edit-is-system"> {{ t('ui.admin_users.is_system_account', {}, locale, ['common']) }} </label> <div class="form-text">{{ t('ui.admin_users.is_system_account_help', {}, locale, ['common']) }}</div> </div> <div class="form-check mb-3"> <input class="form-check-input" type="checkbox" id="edit-echomail-moderation-forced"> <label class="form-check-label text-warning" for="edit-echomail-moderation-forced"> {{ t('ui.admin.users.echomail_moderation_forced', {}, locale, ['common']) }} </label> <div class="form-text">{{ t('ui.admin.users.echomail_moderation_forced_help', {}, locale, ['common']) }}</div> </div> </div> </div> <div class="mb-3"> <label for="edit-new-password" class="form-label">{{ t('ui.admin_users.new_password', {}, locale, ['common']) }}</label> <input type="password" class="form-control" id="edit-new-password" minlength="8"> <div class="form-text">{{ t('ui.admin_users.new_password_help', {}, locale, ['common']) }}</div> </div> <hr> <div class="row"> <div class="col-12"> <h6>{{ t('ui.admin.users.grant_credits_heading', {}, locale, ['common']) }}</h6> </div> <div class="col-md-3 mb-3"> <label for="edit-credit-balance" class="form-label">{{ t('ui.admin.users.credit_balance', {}, locale, ['common']) }}</label> <input type="text" class="form-control" id="edit-credit-balance" readonly> </div> <div class="col-md-3 mb-3"> <label for="edit-grant-credits-amount" class="form-label">{{ t('ui.admin.users.grant_credits_amount', {}, locale, ['common']) }}</label> <input type="number" class="form-control" id="edit-grant-credits-amount" min="1" step="1"> </div> <div class="col-md-6 mb-3"> <label for="edit-grant-credits-note" class="form-label">{{ t('ui.admin.users.grant_credits_note', {}, locale, ['common']) }}</label> <input type="text" class="form-control" id="edit-grant-credits-note" maxlength="255" placeholder="{{ t('ui.admin.users.grant_credits_note_placeholder', {}, locale, ['common']) }}"> </div> <div class="col-12 d-flex justify-content-between align-items-center"> <div class="form-text">{{ t('ui.admin.users.grant_credits_help', {}, locale, ['common']) }}</div> <button type="button" class="btn btn-success" onclick="grantCreditsFromEditModal()"> <i class="fas fa-coins me-2"></i>{{ t('ui.admin.users.grant_credits_action', {}, locale, ['common']) }} </button> </div> </div> </form> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ t('ui.common.cancel', {}, locale, ['common']) }}</button> <button type="button" class="btn btn-primary" onclick="saveUserChanges()"> <i class="fas fa-save me-2"></i>{{ t('ui.common.save_changes', {}, locale, ['common']) }} </button> </div> </div> </div> </div> {% endblock %} {% block scripts %} <script> let currentPendingUserId = null; let currentPage = 1; let currentLimit = 25; let currentSearch = ''; let totalUsers = 0; function apiError(payload, fallback) { if (window.getApiErrorMessage) { return window.getApiErrorMessage(payload, fallback); } return payload && payload.error ? payload.error : fallback; } function uiT(key, fallback, params = {}) { if (window.t) { return window.t(key, params, fallback); } return fallback; } $(document).ready(function() { loadI18nNamespaces(['common', 'errors']).then(function() { loadPendingUsers(); loadAllUsers(); loadUsersNeedingReminders(); }); // Setup search on enter key $('#user-search').on('keypress', function(e) { if (e.which === 13) { searchUsers(); } }); }); function refreshData() { loadPendingUsers(); loadAllUsers(); loadUsersNeedingReminders(); } function loadPendingUsers() { console.log('Loading pending users...'); $.ajax({ url: '/api/admin/pending-users', method: 'GET', dataType: 'json', success: function(response) { console.log('Pending users response:', response); if (response.success) { renderPendingUsers(response.users); } else { console.error(uiT('ui.admin_users.pending_users_error', 'Pending users error:'), apiError(response, uiT('ui.common.unknown_error', 'Unknown error'))); showError(uiT('ui.admin_users.pending_users_load_failed_prefix', 'Failed to load pending users: ') + apiError(response, uiT('ui.common.unknown_error', 'Unknown error'))); } }, error: function(xhr) { console.error('AJAX error for pending users:', xhr.status, xhr.statusText, xhr.responseText); showError(uiT('ui.admin_users.pending_users_load_failed_prefix', 'Failed to load pending users: ') + xhr.status + ' ' + xhr.statusText); } }); } function loadAllUsers(page = 1) { currentPage = page; console.log('Loading all users...', { page: currentPage, limit: currentLimit, search: currentSearch }); const params = new URLSearchParams({ page: currentPage, limit: currentLimit, search: currentSearch }); $.ajax({ url: '/api/admin/users?' + params.toString(), method: 'GET', dataType: 'json', success: function(response) { console.log('All users response:', response); if (response.success) { renderAllUsers(response.users); if (response.pagination) { renderUsersPagination(response.pagination); totalUsers = response.pagination.total; } } else { console.error(uiT('ui.admin_users.all_users_error', 'All users error:'), apiError(response, uiT('ui.common.unknown_error', 'Unknown error'))); showError(uiT('ui.admin_users.users_load_failed_prefix', 'Failed to load users: ') + apiError(response, uiT('ui.common.unknown_error', 'Unknown error'))); } }, error: function(xhr) { console.error('AJAX error for all users:', xhr.status, xhr.statusText, xhr.responseText); showError(uiT('ui.admin_users.users_load_failed_prefix', 'Failed to load users: ') + xhr.status + ' ' + xhr.statusText); } }); } function renderSourceBadge(source) { const s = (source || 'web').toLowerCase(); if (s === 'telnet') return '<span class="badge bg-info">telnet</span>'; if (s === 'ssh') return '<span class="badge bg-secondary">ssh</span>'; return '<span class="badge bg-light text-dark">web</span>'; } function renderPendingUsers(users) { const container = $('#pending-users-container'); $('#pending-count').text(users.length); if (users.length === 0) { container.html(` <div class="text-center py-4 text-muted"> <i class="fas fa-inbox fa-3x mb-3"></i> <p>${uiT('ui.admin_users.no_pending_registrations', 'No pending registrations')}</p> </div> `); return; } let html = '<div class="table-responsive"><table class="table table-hover">'; html += ` <thead> <tr> <th>${uiT('ui.common.username', 'Username')}</th> <th>${uiT('ui.common.real_name', 'Real Name')}</th> <th>${uiT('ui.common.email', 'Email')}</th> <th>${uiT('ui.admin_users.source', 'Source')}</th> <th>${uiT('ui.admin_users.requested', 'Requested')}</th> <th>${uiT('ui.common.ip_address', 'IP Address')}</th> <th>${uiT('ui.common.actions', 'Actions')}</th> </tr> </thead> <tbody> `; users.forEach(user => { const requestedDate = new Date(user.requested_at).toLocaleString(); html += ` <tr> <td><strong>${escapeHtml(user.username)}</strong></td> <td>${escapeHtml(user.real_name || '')}</td> <td>${escapeHtml(user.email || uiT('ui.admin_users.not_provided', 'Not provided'))}</td> <td>${renderSourceBadge(user.registration_source)}</td> <td>${requestedDate}</td> <td><code>${escapeHtml(user.ip_address || '')}</code></td> <td> <button class="btn btn-sm btn-info me-1" onclick="viewUser(${user.id})"> <i class="fas fa-eye"></i> ${uiT('ui.common.view', 'View')} </button> <button class="btn btn-sm btn-success me-1" onclick="approveUserModal(${user.id}, 'approve')"> <i class="fas fa-check"></i> ${uiT('ui.admin_users.approve', 'Approve')} </button> <button class="btn btn-sm btn-danger" onclick="approveUserModal(${user.id}, 'reject')"> <i class="fas fa-times"></i> ${uiT('ui.admin_users.reject', 'Reject')} </button> </td> </tr> `; }); html += '</tbody></table></div>'; container.html(html); } function renderAllUsers(users) { const container = $('#users-container'); $('#users-count').text(totalUsers || users.length); let html = '<div class="table-responsive"><table class="table table-hover">'; html += ` <thead> <tr> <th>ID</th> <th>${uiT('ui.common.username', 'Username')}</th> <th>${uiT('ui.common.real_name', 'Real Name')}</th> <th>${uiT('ui.common.status', 'Status')}</th> <th>${uiT('ui.common.admin', 'Admin')}</th> <th>${uiT('ui.common.created', 'Created')}</th> <th>${uiT('ui.common.last_login', 'Last Login')}</th> <th>${uiT('ui.admin_users.last_reminded', 'Last Reminded')}</th> <th>${uiT('ui.common.actions', 'Actions')}</th> </tr> </thead> <tbody> `; users.forEach(user => { const createdDate = user.created_at ? new Date(user.created_at).toLocaleDateString() : uiT('ui.common.unknown', 'Unknown'); const lastLogin = user.last_login ? new Date(user.last_login).toLocaleString() : uiT('ui.common.never', 'Never'); let lastReminded = uiT('ui.common.never', 'Never'); if (user.days_since_reminder !== null && user.days_since_reminder !== undefined) { if (user.days_since_reminder === 0) { lastReminded = uiT('ui.common.today', 'Today'); } else if (user.days_since_reminder === 1) { lastReminded = uiT('ui.common.one_day_ago', '1 day ago'); } else { lastReminded = uiT('ui.common.days_ago', '{count} days ago', { count: user.days_since_reminder }); } } // Debug logging console.log('User:', user.username, 'days_since_reminder:', user.days_since_reminder, 'display:', lastReminded); const statusBadge = user.is_active ? `<span class="badge bg-success">${uiT('ui.common.active', 'Active')}</span>` : `<span class="badge bg-danger">${uiT('ui.common.inactive', 'Inactive')}</span>`; const adminBadge = user.is_admin ? `<span class="badge bg-warning">${uiT('ui.common.admin', 'Admin')}</span>` : `<span class="badge bg-secondary">${uiT('ui.common.user', 'User')}</span>`; const systemBadge = user.is_system ? `<span class="badge bg-info ms-1">${uiT('ui.admin_users.is_system_account', 'System')}</span>` : ''; html += ` <tr> <td>${user.id}</td> <td><strong>${escapeHtml(user.username)}</strong></td> <td>${escapeHtml(user.real_name || '')}</td> <td>${statusBadge}</td> <td>${adminBadge}${systemBadge}</td> <td>${createdDate}</td> <td>${lastLogin}</td> <td>${lastReminded}</td> <td> <a href="/profile/${encodeURIComponent(user.username)}" class="btn btn-sm btn-secondary me-1" target="_blank" title="${uiT('ui.admin_users.view_profile', 'View Profile')}"> <i class="fas fa-user"></i> ${uiT('ui.admin_users.view_profile', 'View Profile')} </a> <button class="btn btn-sm btn-primary me-1" onclick="editUser(${user.id})"> <i class="fas fa-edit"></i> ${uiT('ui.common.edit', 'Edit')} </button> <button class="btn btn-sm btn-${user.is_active ? 'warning' : 'success'} me-1" onclick="toggleUserStatus(${user.id}, ${user.is_active ? 'false' : 'true'})"> <i class="fas fa-${user.is_active ? 'pause' : 'play'}"></i> ${user.is_active ? uiT('ui.common.disable', 'Disable') : uiT('ui.common.enable', 'Enable')} </button>`; // Add reminder button if user hasn't logged in yet if (!user.last_login && user.is_active) { html += ` <button class="btn btn-sm btn-info me-1" onclick="sendAccountReminder(${user.id}, '${escapeHtml(user.username)}')" title="${uiT('ui.admin_users.send_account_reminder', 'Send account reminder')}"> <i class="fas fa-bell"></i> ${uiT('ui.admin_users.remind', 'Remind')} </button>`; } html += ` </td> </tr> `; }); html += '</tbody></table></div>'; container.html(html); } function viewUser(userId) { // Load detailed user info for viewing $.ajax({ url: '/api/admin/pending-users/' + userId, method: 'GET', dataType: 'json', success: function(response) { if (response.success) { const user = response.user; const requestedDate = new Date(user.requested_at).toLocaleString(); let referrerInfo = ''; if (user.referrer_username) { referrerInfo = `<tr><th>${uiT('ui.admin_users.referred_by', 'Referred By:')}</th><td><strong>${escapeHtml(user.referrer_username)}</strong> (${escapeHtml(user.referrer_real_name || '')})</td></tr>`; } let details = ` <div class="row"> <div class="col-md-6"> <h6>${uiT('ui.admin_users.basic_information', 'Basic Information')}</h6> <table class="table table-sm"> <tr><th>${uiT('ui.common.username', 'Username')}:</th><td>${escapeHtml(user.username)}</td></tr> <tr><th>${uiT('ui.common.real_name', 'Real Name')}:</th><td>${escapeHtml(user.real_name || uiT('ui.admin_users.not_provided', 'Not provided'))}</td></tr> <tr><th>${uiT('ui.common.email', 'Email')}:</th><td>${escapeHtml(user.email || uiT('ui.admin_users.not_provided', 'Not provided'))}</td></tr> <tr><th>${uiT('ui.common.location', 'Location')}:</th><td>${escapeHtml(user.location || uiT('ui.admin_users.not_provided', 'Not provided'))}</td></tr> <tr><th>${uiT('ui.admin_users.requested', 'Requested')}:</th><td>${requestedDate}</td></tr> <tr><th>${uiT('ui.common.ip_address', 'IP Address')}:</th><td><code>${escapeHtml(user.ip_address || '')}</code></td></tr> <tr><th>${uiT('ui.admin_users.source', 'Source')}:</th><td>${renderSourceBadge(user.registration_source)}</td></tr> ${referrerInfo} </table> </div> <div class="col-md-6"> <h6>${uiT('ui.admin_users.request_details', 'Request Details')}</h6> <p><strong>${uiT('ui.admin_users.reason_for_joining', 'Reason for joining:')}</strong></p> <div class="border p-2 bg-light"> ${escapeHtml(user.reason || uiT('ui.admin_users.no_reason_provided', 'No reason provided')).replace(/\n/g, '<br>')} </div> <p class="mt-2"><strong>${uiT('ui.admin_users.user_agent', 'User Agent:')}</strong></p> <small class="text-muted">${escapeHtml(user.user_agent || uiT('ui.admin_users.not_available', 'Not available'))}</small> </div> </div> `; $('#user-details').html(details); currentPendingUserId = userId; $('#approval-action-title').text(uiT('ui.admin_users.user_details', 'User Details')); $('#approve-btn, #reject-btn').hide(); new bootstrap.Modal(document.getElementById('approvalModal')).show(); } } }); } function approveUserModal(userId, action) { viewUser(userId); // Show appropriate buttons after user details are loaded setTimeout(() => { if (action === 'approve') { $('#approval-action-title').text(uiT('ui.admin_users.approve_user_registration', 'Approve User Registration')); $('#approve-btn').show(); $('#reject-btn').hide(); } else { $('#approval-action-title').text(uiT('ui.admin_users.reject_user_registration', 'Reject User Registration')); $('#approve-btn').hide(); $('#reject-btn').show(); } }, 500); } function processUser(action) { if (!currentPendingUserId) return; const notes = $('#admin-notes').val(); const btn = action === 'approve' ? $('#approve-btn') : $('#reject-btn'); const originalText = btn.html(); btn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin me-2"></i>' + uiT('ui.admin_users.processing', 'Processing...')); $.ajax({ url: '/api/admin/pending-users/' + currentPendingUserId + '/' + action, method: 'POST', data: { notes: notes }, dataType: 'json', success: function(response) { if (response.success) { const fallbackMessage = action === 'approve' ? uiT('ui.admin_users.user_approved_success', 'User approved successfully!') : uiT('ui.admin_users.user_rejected_success', 'User rejected successfully!'); const successMessage = window.getApiMessage ? window.getApiMessage(response, fallbackMessage) : fallbackMessage; showSuccess(successMessage); $('#approvalModal').modal('hide'); refreshData(); $('#admin-notes').val(''); } else { showError(uiT('ui.admin_users.user_action_failed_prefix', 'Failed to {action} user: ', { action }) + apiError(response, uiT('ui.common.unknown_error', 'Unknown error'))); } }, error: function(xhr) { showError(uiT('ui.admin_users.user_action_failed_short', 'Failed to {action} user', { action })); }, complete: function() { btn.prop('disabled', false).html(originalText); } }); } function escapeHtml(text) { if (!text) return ''; return text .replace(/&/g, "&amp;") .replace(/</g, "&lt;") .replace(/>/g, "&gt;") .replace(/"/g, "&quot;") .replace(/'/g, "&#039;"); } function showSuccess(message) { // Simple success notification - could be enhanced with toast/alert system alert(uiT('ui.admin_users.success_prefix', 'Success: ') + message); } function showError(message) { // Simple error notification - could be enhanced with toast/alert system alert(uiT('ui.admin_users.error_prefix', 'Error: ') + message); } function editUser(userId) { console.log('Editing user:', userId); // Load user data $.ajax({ url: '/api/admin/users/' + userId, method: 'GET', dataType: 'json', success: function(response) { if (response.success) { const user = response.user; // Populate form fields $('#edit-user-id').val(user.id); $('#edit-username').val(user.username); $('#edit-real-name').val(user.real_name || ''); $('#edit-email').val(user.email || ''); $('#edit-is-active').prop('checked', !!user.is_active); $('#edit-is-admin').prop('checked', !!user.is_admin); $('#edit-is-system').prop('checked', !!user.is_system); $('#edit-echomail-moderation-forced').prop('checked', !!user.echomail_moderation_forced); $('#edit-credit-balance').val(user.credit_balance || 0); $('#edit-grant-credits-amount').val(''); $('#edit-grant-credits-note').val(''); $('#edit-new-password').val(''); // Show modal new bootstrap.Modal(document.getElementById('editUserModal')).show(); } else { showError(uiT('ui.admin_users.user_data_load_failed_prefix', 'Failed to load user data: ') + apiError(response, uiT('ui.common.unknown_error', 'Unknown error'))); } }, error: function(xhr) { console.error('AJAX error for edit user:', xhr.status, xhr.statusText, xhr.responseText); showError(uiT('ui.admin_users.user_data_load_failed_prefix', 'Failed to load user data: ') + xhr.status + ' ' + xhr.statusText); } }); } function saveUserChanges() { const userId = $('#edit-user-id').val(); const newPassword = $('#edit-new-password').val(); // Validate password if provided if (newPassword && newPassword.length < 8) { showError(uiT('ui.admin_users.password_min_length', 'Password must be at least 8 characters long')); return; } const data = { real_name: $('#edit-real-name').val(), email: $('#edit-email').val(), is_active: $('#edit-is-active').is(':checked') ? 1 : 0, is_admin: $('#edit-is-admin').is(':checked') ? 1 : 0, is_system: $('#edit-is-system').is(':checked') ? 1 : 0, echomail_moderation_forced: $('#edit-echomail-moderation-forced').is(':checked') ? 1 : 0 }; // Only include password if it's provided if (newPassword) { data.password = newPassword; } const btn = $('button[onclick="saveUserChanges()"]'); const originalText = btn.html(); btn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin me-2"></i>' + uiT('ui.common.saving', 'Saving...')); $.ajax({ url: '/api/admin/users/' + userId, method: 'POST', data: data, dataType: 'json', success: function(response) { if (response.success) { const successMessage = window.getApiMessage ? window.getApiMessage(response, uiT('ui.admin_users.user_updated_success', 'User updated successfully!')) : uiT('ui.admin_users.user_updated_success', 'User updated successfully!'); showSuccess(successMessage); $('#editUserModal').modal('hide'); refreshData(); } else { showError(uiT('ui.admin_users.user_update_failed_prefix', 'Failed to update user: ') + apiError(response, uiT('ui.common.unknown_error', 'Unknown error'))); } }, error: function(xhr) { console.error('AJAX error for save user:', xhr.status, xhr.statusText, xhr.responseText); showError(uiT('ui.admin_users.user_update_failed_prefix', 'Failed to update user: ') + xhr.status + ' ' + xhr.statusText); }, complete: function() { btn.prop('disabled', false).html(originalText); } }); } function grantCreditsFromEditModal() { const userId = $('#edit-user-id').val(); const amount = parseInt($('#edit-grant-credits-amount').val(), 10); const note = ($('#edit-grant-credits-note').val() || '').trim(); if (Number.isNaN(amount) || amount <= 0) { showError(uiT('ui.admin.users.grant_credits_amount_invalid', 'Enter a positive credit amount.')); return; } if (!note) { showError(uiT('ui.admin.users.grant_credits_note_missing', 'Enter a note for this credit grant.')); return; } const btn = $('button[onclick="grantCreditsFromEditModal()"]'); const originalText = btn.html(); btn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin me-2"></i>' + uiT('ui.common.processing', 'Processing...')); $.ajax({ url: '/api/admin/users/' + userId + '/credits', method: 'POST', data: { amount: amount, note: note }, dataType: 'json', success: function(response) { if (response.success) { $('#edit-credit-balance').val(response.balance || 0); $('#edit-grant-credits-amount').val(''); $('#edit-grant-credits-note').val(''); const successMessage = window.getApiMessage ? window.getApiMessage(response, uiT('ui.admin.users.credit_grant_success', 'Credits added successfully')) : uiT('ui.admin.users.credit_grant_success', 'Credits added successfully'); showSuccess(successMessage); loadAllUsers(); } else { showError(apiError(response, uiT('ui.admin.users.credit_grant_failed', 'Failed to add credits'))); } }, error: function(xhr) { let payload = {}; try { payload = JSON.parse(xhr.responseText); } catch (_) {} showError(apiError(payload, uiT('ui.admin.users.credit_grant_failed', 'Failed to add credits'))); }, complete: function() { btn.prop('disabled', false).html(originalText); } }); } function toggleUserStatus(userId, newStatus) { const action = newStatus === 'true' ? 'enable' : 'disable'; if (!confirm(uiT('ui.admin_users.user_toggle_confirm', 'Are you sure you want to {action} this user?', { action }))) { return; } $.ajax({ url: '/api/admin/users/' + userId + '/toggle-status', method: 'POST', data: { is_active: newStatus === 'true' ? 1 : 0 }, dataType: 'json', success: function(response) { if (response.success) { const successMessage = window.getApiMessage ? window.getApiMessage(response, uiT('ui.admin_users.user_toggled_success', 'User {action}d successfully!', { action })) : uiT('ui.admin_users.user_toggled_success', 'User {action}d successfully!', { action }); showSuccess(successMessage); refreshData(); } else { showError(uiT('ui.admin_users.user_action_failed_prefix', 'Failed to {action} user: ', { action }) + apiError(response, uiT('ui.common.unknown_error', 'Unknown error'))); } }, error: function(xhr) { console.error('AJAX error for toggle status:', xhr.status, xhr.statusText, xhr.responseText); showError(uiT('ui.admin_users.user_action_failed_prefix', 'Failed to {action} user: ', { action }) + xhr.status + ' ' + xhr.statusText); } }); } function createNewUser() { // Clear form fields $('#create-username').val(''); $('#create-real-name').val(''); $('#create-email').val(''); $('#create-password').val(''); $('#create-confirm-password').val(''); $('#create-is-active').prop('checked', true); $('#create-is-admin').prop('checked', false); $('#create-is-system').prop('checked', false); // Show modal new bootstrap.Modal(document.getElementById('createUserModal')).show(); } function saveNewUser() { const username = $('#create-username').val().trim(); const realName = $('#create-real-name').val().trim(); const email = $('#create-email').val().trim(); const password = $('#create-password').val(); const confirmPassword = $('#create-confirm-password').val(); const isActive = $('#create-is-active').is(':checked') ? 1 : 0; const isAdmin = $('#create-is-admin').is(':checked') ? 1 : 0; const isSystem = $('#create-is-system').is(':checked') ? 1 : 0; // Validate required fields if (!username || !realName || !password) { showError(uiT('ui.admin_users.required_fields', 'Username, real name, and password are required')); return; } // Validate username format if (!/^[a-zA-Z0-9_]{3,20}$/.test(username)) { showError(uiT('ui.admin_users.username_validation', 'Username must be 3-20 characters, letters, numbers, and underscores only')); return; } // Validate password if (password.length < 8) { showError(uiT('ui.admin_users.password_min_length', 'Password must be at least 8 characters long')); return; } // Validate passwords match if (password !== confirmPassword) { showError(uiT('ui.register.passwords_do_not_match', 'Passwords do not match')); return; } const data = { username: username, real_name: realName, email: email || null, password: password, is_active: isActive, is_admin: isAdmin, is_system: isSystem }; const btn = $('button[onclick="saveNewUser()"]'); const originalText = btn.html(); btn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin me-2"></i>' + uiT('ui.admin_users.creating', 'Creating...')); $.ajax({ url: '/api/admin/users/create', method: 'POST', data: data, dataType: 'json', success: function(response) { if (response.success) { const successMessage = window.getApiMessage ? window.getApiMessage(response, uiT('ui.admin_users.user_created_success', 'User created successfully!')) : uiT('ui.admin_users.user_created_success', 'User created successfully!'); showSuccess(successMessage); $('#createUserModal').modal('hide'); refreshData(); } else { showError(uiT('ui.admin_users.user_create_failed_prefix', 'Failed to create user: ') + apiError(response, uiT('ui.common.unknown_error', 'Unknown error'))); } }, error: function(xhr) { console.error('AJAX error for create user:', xhr.status, xhr.statusText, xhr.responseText); let errorMsg = uiT('ui.admin_users.user_create_failed', 'Failed to create user'); if (xhr && xhr.responseJSON) { errorMsg += ': ' + apiError(xhr.responseJSON, xhr.status + ' ' + xhr.statusText); } else { errorMsg += ': ' + xhr.status + ' ' + xhr.statusText; } showError(errorMsg); }, complete: function() { btn.prop('disabled', false).html(originalText); } }); } function cleanupRegistrations() { if (!confirm(uiT('ui.admin_users.cleanup_confirm', 'This will remove all approved registration records and old rejected registrations (30+ days). Are you sure?'))) { return; } const btn = $('button[onclick="cleanupRegistrations()"]'); const originalText = btn.html(); btn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin me-2"></i>' + uiT('ui.admin_users.cleaning', 'Cleaning...')); $.ajax({ url: '/api/admin/users/cleanup', method: 'POST', dataType: 'json', success: function(response) { if (response.success) { const result = response.result; const fallbackMessage = uiT('ui.admin_users.cleanup_success', 'Cleanup completed! Removed {approved} approved and {rejected} old rejected registrations ({total} total).', { approved: result.approved_removed, rejected: result.old_rejected_removed, total: result.total_cleaned }); const successMessage = window.getApiMessage ? window.getApiMessage(response, fallbackMessage) : fallbackMessage; showSuccess(successMessage); refreshData(); } else { showError(uiT('ui.admin_users.cleanup_failed_prefix', 'Failed to cleanup registrations: ') + apiError(response, uiT('ui.common.unknown_error', 'Unknown error'))); } }, error: function(xhr) { console.error('AJAX error for cleanup:', xhr.status, xhr.statusText, xhr.responseText); showError(uiT('ui.admin_users.cleanup_failed_prefix', 'Failed to cleanup registrations: ') + xhr.status + ' ' + xhr.statusText); }, complete: function() { btn.prop('disabled', false).html(originalText); } }); } function sendAccountReminder(userId, username) { if (!confirm(uiT('ui.admin_users.send_reminder_confirm', 'Send account reminder to {username}? This will send a reminder message via netmail and email (if available).', { username }))) { return; } $.ajax({ url: '/api/admin/users/' + userId + '/send-reminder', method: 'POST', dataType: 'json', success: function(response) { if (response.success) { let message = window.getApiMessage ? window.getApiMessage(response, uiT('ui.api.reminder.sent', 'Account reminder sent successfully')) : uiT('ui.api.reminder.sent', 'Account reminder sent successfully'); if (response.email_sent) { message += uiT('ui.admin_users.reminder_sent_netmail_email_suffix', ' (sent via netmail and email)'); } else { message += uiT('ui.admin_users.reminder_sent_netmail_only_suffix', ' (sent via netmail only)'); } showSuccess(message); refreshData(); // Refresh to update any status changes } else { showError(uiT('ui.admin_users.send_reminder_failed_prefix', 'Failed to send reminder: ') + apiError(response, uiT('ui.common.unknown_error', 'Unknown error'))); } }, error: function(xhr) { console.error('AJAX error for send reminder:', xhr.status, xhr.statusText, xhr.responseText); let errorMsg = uiT('ui.admin_users.send_reminder_failed', 'Failed to send reminder'); if (xhr && xhr.responseJSON) { errorMsg += ': ' + apiError(xhr.responseJSON, xhr.status + ' ' + xhr.statusText); } else { errorMsg += ': ' + xhr.status + ' ' + xhr.statusText; } showError(errorMsg); } }); } function loadUsersNeedingReminders() { $.ajax({ url: '/api/admin/users/need-reminders', method: 'GET', dataType: 'json', success: function(response) { if (response.success) { updateRemindersCount(response.users.length); } }, error: function(xhr) { console.error('AJAX error for users needing reminders:', xhr.status, xhr.statusText, xhr.responseText); // Hide count on error $('#reminders-count').hide(); } }); } function updateRemindersCount(count) { const badge = $('#reminders-count'); if (count > 0) { badge.text(count).show(); } else { badge.hide(); } } function showUsersNeedingReminders() { $.ajax({ url: '/api/admin/users/need-reminders', method: 'GET', dataType: 'json', success: function(response) { if (response.success) { if (response.users.length === 0) { showSuccess(uiT('ui.admin_users.no_reminders_needed', 'No users currently need account reminders. All users have logged in!')); return; } let message = uiT('ui.admin_users.reminders_found_header', `Found ${response.users.length} user(s) who haven't logged in yet:\n\n`, { count: response.users.length }); response.users.forEach(user => { const createdDate = new Date(user.created_at).toLocaleDateString(); message += uiT('ui.admin_users.reminder_user_line', '- {username} ({real_name}) - created {created_date}\n', { username: user.username, real_name: user.real_name || uiT('ui.admin_users.no_real_name', 'No real name'), created_date: createdDate }); }); message += uiT('ui.admin_users.reminders_found_footer', '\nYou can send individual reminders using the "Remind" buttons in the user list below.'); alert(message); } else { showError(uiT('ui.admin_users.reminders_load_failed_prefix', 'Failed to load users needing reminders: ') + apiError(response, uiT('ui.common.unknown_error', 'Unknown error'))); } }, error: function(xhr) { console.error('AJAX error for show users needing reminders:', xhr.status, xhr.statusText, xhr.responseText); showError(uiT('ui.admin_users.reminders_load_failed_prefix', 'Failed to load users needing reminders: ') + xhr.status + ' ' + xhr.statusText); } }); } function searchUsers() { currentSearch = $('#user-search').val(); currentPage = 1; // Reset to first page loadAllUsers(1); } function changeUsersPerPage() { currentLimit = parseInt($('#users-per-page').val()); currentPage = 1; // Reset to first page loadAllUsers(1); } function renderUsersPagination(pagination) { const container = $('#users-pagination'); if (pagination.pages <= 1) { container.hide(); return; } let html = `<nav aria-label="${uiT('ui.admin_users.users_pagination_aria', 'Users pagination')}"><ul class="pagination">`; // Previous button if (pagination.page > 1) { html += `<li class="page-item"> <a class="page-link" href="#" onclick="loadAllUsers(${pagination.page - 1}); return false;"> <i class="fas fa-chevron-left"></i> ${uiT('ui.common.previous', 'Previous')} </a> </li>`; } // Page numbers const startPage = Math.max(1, pagination.page - 2); const endPage = Math.min(pagination.pages, pagination.page + 2); if (startPage > 1) { html += `<li class="page-item"> <a class="page-link" href="#" onclick="loadAllUsers(1); return false;">1</a> </li>`; if (startPage > 2) { html += '<li class="page-item disabled"><span class="page-link">...</span></li>'; } } for (let i = startPage; i <= endPage; i++) { html += `<li class="page-item ${i === pagination.page ? 'active' : ''}"> <a class="page-link" href="#" onclick="loadAllUsers(${i}); return false;">${i}</a> </li>`; } if (endPage < pagination.pages) { if (endPage < pagination.pages - 1) { html += '<li class="page-item disabled"><span class="page-link">...</span></li>'; } html += `<li class="page-item"> <a class="page-link" href="#" onclick="loadAllUsers(${pagination.pages}); return false;">${pagination.pages}</a> </li>`; } // Next button if (pagination.page < pagination.pages) { html += `<li class="page-item"> <a class="page-link" href="#" onclick="loadAllUsers(${pagination.page + 1}); return false;"> ${uiT('ui.common.next', 'Next')} <i class="fas fa-chevron-right"></i> </a> </li>`; } html += '</ul></nav>'; // Add page info const start = (pagination.page - 1) * pagination.limit + 1; const end = Math.min(pagination.page * pagination.limit, pagination.total); html += `<small class="text-muted ms-3"> ${uiT('ui.admin_users.showing_users_range', 'Showing {start}-{end} of {total} users', { start, end, total: pagination.total })} </small>`; container.html(html).show(); } </script> {% endblock %}