{% 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, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
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 %}
|