import Api from './api.js';
import Storage from './storage.js';
import { getEl, addSafeEventListener, setElementDisplay } from './util/dom-utils.js';
import { applyTheme, fadeOutLoader } from './util/theme-utils.js';
import { customConfirm } from './util/confirm-dialog.js';
/**
* Users authentication, profile management, and administrative control.
*/
class Users {
static instance = null;
static isAdmin = false;
static currentUser = null;
#api;
#storage;
// Auth elements
loginForm;
usernameInput;
passcodeInput;
loginButton;
errorMessageDiv;
logoutButton;
// Profile Form Elements
profileForm;
profileNameInput;
profileEmailInput;
profileStatusDiv;
saveProfileButton;
// Account Elements
accountBalanceSpan;
accountCostMonthSpan;
accountCostAllSpan;
// Admin Elements - Main
adminForm;
btnAdminShowCreateUser;
btnAdminShowAddFunds;
adminUsersTbody;
adminStatusDiv;
// Admin Elements - Create User
adminCreateUserPanel;
newUsernameInput;
newPasswordInput;
newNameInput;
newEmailInput;
newRoleSelect;
newBalanceInput;
btnAdminCancelCreate;
btnAdminSubmitCreate;
// Admin Elements - Edit User
adminEditUserPanel;
editUserIdInput;
editUsernameDisplay;
editUsernameInput;
editPasswordInput;
editNameInput;
editEmailInput;
editRoleSelect;
btnAdminCancelEdit;
btnAdminSubmitEdit;
// Admin Elements - Add Funds
adminAddFundsPanel;
adminFundsUserSelect;
adminFundsAmountInput;
btnAdminCancelAddFunds;
btnAdminSubmitAddFunds;
/**
* Initializes the Users class, UI element bindings, and session check.
*/
constructor() {
Users.instance = this;
this.#api = new Api();
this.#storage = new Storage();
// Apply theme on initialization
this.applySavedTheme();
// Auth elements
this.loginForm = getEl('login-form') || getEl('form-login');
this.usernameInput = getEl('username') || getEl('login-username');
this.passcodeInput = getEl('passcode') || getEl('login-passcode');
this.loginButton = getEl('login-button') || getEl('btn-login');
this.errorMessageDiv = getEl('login-error-message');
this.logoutButton = getEl('id-btn-logout') || getEl('btn-logout');
// Profile elements
this.profileForm = getEl('id-form-profile-options');
this.profileNameInput = getEl('profile-name');
this.profileEmailInput = getEl('profile-email');
this.profileStatusDiv = getEl('profile-status-message');
this.saveProfileButton = getEl('btn-save-profile');
// Account elements
this.accountBalanceSpan = getEl('account-balance-amount');
this.accountCostMonthSpan = getEl('account-cost-month');
this.accountCostAllSpan = getEl('account-cost-all');
// Admin elements - Main
this.adminForm = getEl('id-form-admin-options');
this.btnAdminShowCreateUser = getEl('btn-admin-show-create-user');
this.btnAdminShowAddFunds = getEl('btn-admin-show-add-funds');
this.adminUsersTbody = getEl('admin-users-tbody');
this.adminStatusDiv = getEl('admin-management-status');
// Admin elements - Create User
this.adminCreateUserPanel = getEl('admin-create-user-panel');
this.newUsernameInput = getEl('new-user-username');
this.newPasswordInput = getEl('new-user-password');
this.newNameInput = getEl('new-user-name');
this.newEmailInput = getEl('new-user-email');
this.newRoleSelect = getEl('new-user-role');
this.newBalanceInput = getEl('new-user-balance');
this.btnAdminCancelCreate = getEl('btn-admin-cancel-create');
this.btnAdminSubmitCreate = getEl('btn-admin-submit-create');
// Admin elements - Edit User
this.adminEditUserPanel = getEl('admin-edit-user-panel');
this.editUserIdInput = getEl('admin-edit-user-id');
this.editUsernameDisplay = getEl('admin-edit-username-display');
this.editUsernameInput = getEl('admin-edit-username');
this.editPasswordInput = getEl('admin-edit-password');
this.editNameInput = getEl('admin-edit-name');
this.editEmailInput = getEl('admin-edit-email');
this.editRoleSelect = getEl('admin-edit-role');
this.btnAdminCancelEdit = getEl('btn-admin-cancel-edit');
this.btnAdminSubmitEdit = getEl('btn-admin-submit-edit');
// Admin elements - Add Funds
this.adminAddFundsPanel = getEl('admin-add-funds-panel');
this.adminFundsUserSelect = getEl('admin-funds-user-select');
this.adminFundsAmountInput = getEl('admin-funds-amount');
this.btnAdminCancelAddFunds = getEl('btn-admin-cancel-add-funds');
this.btnAdminSubmitAddFunds = getEl('btn-admin-submit-add-funds');
this.initEventListeners();
this.checkLoginStatus();
}
/**
* Applies the saved theme from configuration.
*/
applySavedTheme() {
const config = this.#storage.get_app_config();
const theme = (config && config[this.#storage.KEY_CONFIG_THEME]) ? config[this.#storage.KEY_CONFIG_THEME] : 'theme_dark';
applyTheme(theme, true);
}
/**
* Binds event listeners for user login, logout, profile, and admin forms.
*/
initEventListeners() {
if (this.loginForm) {
addSafeEventListener(this.loginForm, 'submit', (e) => {
e.preventDefault();
this.handleLogin();
});
}
addSafeEventListener(this.loginButton, 'click', (e) => {
if (e) e.preventDefault();
this.handleLogin();
});
addSafeEventListener(this.logoutButton, 'click', (e) => {
if (e) e.preventDefault();
this.handleLogout();
});
// Profile form
if (this.profileForm) {
addSafeEventListener(this.profileForm, 'submit', (e) => {
e.preventDefault();
this.handleSaveProfile();
});
}
addSafeEventListener(this.saveProfileButton, 'click', (e) => {
if (e) e.preventDefault();
this.handleSaveProfile();
});
// Admin management - Create user
addSafeEventListener(this.btnAdminShowCreateUser, 'click', (e) => {
if (e) e.preventDefault();
if (this.adminAddFundsPanel) setElementDisplay(this.adminAddFundsPanel, 'none');
if (this.adminEditUserPanel) setElementDisplay(this.adminEditUserPanel, 'none');
if (this.adminCreateUserPanel) {
const isHidden = this.adminCreateUserPanel.style.display === 'none' || !this.adminCreateUserPanel.style.display;
setElementDisplay(this.adminCreateUserPanel, isHidden ? 'block' : 'none');
}
});
addSafeEventListener(this.btnAdminCancelCreate, 'click', (e) => {
if (e) e.preventDefault();
this.resetCreateUserPanel();
});
addSafeEventListener(this.btnAdminSubmitCreate, 'click', (e) => {
if (e) e.preventDefault();
this.handleCreateUser();
});
// Admin management - Edit user
addSafeEventListener(this.btnAdminCancelEdit, 'click', (e) => {
if (e) e.preventDefault();
this.resetEditUserPanel();
});
addSafeEventListener(this.btnAdminSubmitEdit, 'click', (e) => {
if (e) e.preventDefault();
this.handleSaveEditUser();
});
// Admin management - Add funds
addSafeEventListener(this.btnAdminShowAddFunds, 'click', (e) => {
if (e) e.preventDefault();
if (this.adminCreateUserPanel) setElementDisplay(this.adminCreateUserPanel, 'none');
if (this.adminEditUserPanel) setElementDisplay(this.adminEditUserPanel, 'none');
if (this.adminAddFundsPanel) {
const isHidden = this.adminAddFundsPanel.style.display === 'none' || !this.adminAddFundsPanel.style.display;
setElementDisplay(this.adminAddFundsPanel, isHidden ? 'block' : 'none');
}
});
addSafeEventListener(this.btnAdminCancelAddFunds, 'click', (e) => {
if (e) e.preventDefault();
this.resetAddFundsPanel();
});
addSafeEventListener(this.btnAdminSubmitAddFunds, 'click', (e) => {
if (e) e.preventDefault();
this.handleAddFundsFromAdmin();
});
}
/**
* Handles authentication request on login form submission.
*/
handleLogin() {
if (!this.usernameInput || !this.passcodeInput) return;
const username = this.usernameInput.value.trim();
const passcode = this.passcodeInput.value.trim();
if (!username || !passcode) {
this.displayErrorMessage('Please enter both username and passcode.');
return;
}
this.displayErrorMessage('');
this.#api.post(
{ action: 'login', username, passcode },
(response) => {
if (response.success) {
window.location.reload();
} else {
this.displayErrorMessage(response.error || response.message || 'Login failed.');
}
},
(error) => {
this.displayErrorMessage('An error occurred during login. Please try again.');
console.error('Login error:', error);
},
'users'
);
}
/**
* Handles user logout and reloads the application.
*/
handleLogout() {
this.#api.post(
{ action: 'logout' },
(response) => {
if (response.success) {
window.location.reload();
} else {
alert(response.message || response.error || 'Logout failed.');
}
},
(error) => {
alert('An error occurred during logout. Please try again.');
console.error('Logout error:', error);
},
'users'
);
}
/**
* Displays an authentication error message.
* @param {string} message - Error message text.
*/
displayErrorMessage(message) {
if (this.errorMessageDiv) {
this.errorMessageDiv.textContent = message;
setElementDisplay(this.errorMessageDiv, message ? 'block' : 'none');
}
}
/**
* Checks current session authentication status with the server.
*/
checkLoginStatus() {
this.#api.post(
{ action: 'session' },
(response) => {
if (response['isLoggedIn']) {
Users.isAdmin = !!response['isAdmin'];
setElementDisplay(this.loginForm, 'none');
setElementDisplay(this.logoutButton, 'block');
this.loadProfile();
if (Users.isAdmin) {
this.loadAdminUsers();
}
} else {
Users.isAdmin = false;
setElementDisplay(this.loginForm, 'block');
setElementDisplay(this.logoutButton, 'none');
fadeOutLoader();
}
},
(error) => {
console.error('Check login status error:', error);
fadeOutLoader();
},
'users'
);
}
/**
* Fetches user profile and balance data from the server.
*/
loadProfile() {
this.#api.post(
{ action: 'get_profile' },
(response) => {
if (response.success && response['profile']) {
Users.currentUser = response['profile'];
this.displayProfile(response['profile']);
}
fadeOutLoader();
},
(error) => {
console.error('Load profile error:', error);
fadeOutLoader();
},
'users'
);
}
/**
* Populates profile and account balance information into the UI.
* @param {object} profile - User profile data object.
*/
displayProfile(profile) {
if (this.profileNameInput) {
this.profileNameInput.value = profile.name || profile.username || '';
}
if (this.profileEmailInput) {
this.profileEmailInput.value = profile.email || '';
}
const balance = parseFloat(profile.balance || 0);
const lifetimeSpend = parseFloat(profile.lifetime_spend || 0);
if (this.accountBalanceSpan) {
this.accountBalanceSpan.textContent = `$${balance.toFixed(6)}`;
}
if (this.accountCostMonthSpan) {
this.accountCostMonthSpan.textContent = `$${lifetimeSpend.toFixed(6)}`;
}
if (this.accountCostAllSpan) {
this.accountCostAllSpan.textContent = `$${lifetimeSpend.toFixed(6)}`;
}
}
/**
* Submits updated user profile information (name, email) to the server.
*/
handleSaveProfile() {
const name = this.profileNameInput ? this.profileNameInput.value.trim() : '';
const email = this.profileEmailInput ? this.profileEmailInput.value.trim() : '';
this.#api.post(
{ action: 'update_profile', name, email },
(response) => {
if (response.success) {
this.displayProfileStatus('Profile saved successfully!', false);
this.loadProfile();
} else {
this.displayProfileStatus(response.error || response.message || 'Failed to save profile.', true);
}
},
(error) => {
this.displayProfileStatus('An error occurred while saving profile.', true);
console.error('Save profile error:', error);
},
'users'
);
}
/**
* Displays a temporary status message in the profile form.
* @param {string} message - Status message text.
* @param {boolean} isError - Whether the status represents an error.
*/
displayProfileStatus(message, isError) {
if (this.profileStatusDiv) {
this.profileStatusDiv.textContent = message;
this.profileStatusDiv.className = isError ? 'status-msg status-error' : 'status-msg status-success';
setElementDisplay(this.profileStatusDiv, 'block');
setTimeout(() => {
setElementDisplay(this.profileStatusDiv, 'none');
}, 4000);
}
}
/**
* Loads all users for the admin management table and user dropdown.
*/
loadAdminUsers() {
if (!Users.isAdmin) return;
this.#api.post(
{ action: 'get_all_users' },
(response) => {
if (response.success && Array.isArray(response.users)) {
this.renderAdminUsersTable(response.users);
this.populateAdminFundsUserSelect(response.users);
} else {
this.displayAdminStatus(response.error || response.message || 'Failed to load users.', true);
}
},
(error) => {
this.displayAdminStatus('An error occurred while loading users.', true);
console.error('Load admin users error:', error);
},
'users'
);
}
/**
* Populates user select dropdown in the admin add funds panel.
* @param {Array<object>} users
*/
populateAdminFundsUserSelect(users) {
if (!this.adminFundsUserSelect) return;
const currentSelected = this.adminFundsUserSelect.value;
this.adminFundsUserSelect.innerHTML = users.map(user => {
const label = user.name ? `${user.username} (${user.name})` : user.username;
const balance = parseFloat(user.balance || 0).toFixed(2);
return `<option value="${user.id}">${this.escapeHtml(label)} — Balance: $${balance}</option>`;
}).join('');
if (currentSelected && users.some(u => String(u.id) === String(currentSelected))) {
this.adminFundsUserSelect.value = currentSelected;
}
}
/**
* Renders the users into the admin table.
* @param {Array<object>} users - List of user objects.
*/
renderAdminUsersTable(users) {
if (!this.adminUsersTbody) return;
this.adminUsersTbody.innerHTML = '';
users.forEach(user => {
const tr = document.createElement('tr');
const balance = parseFloat(user.balance || 0).toFixed(2);
const spend = parseFloat(user.lifetime_spend || 0).toFixed(2);
const isUserAdmin = Number(user.admin) === 1;
const isCurrent = Users.currentUser && String(Users.currentUser.id) === String(user.id);
tr.innerHTML = `
<td style="padding: 6px 4px;">
<strong>${this.escapeHtml(user.username)}</strong>
${user.name ? `<br/><small style="opacity: 0.7;">${this.escapeHtml(user.name)}</small>` : ''}
${user.email ? `<br/><small style="opacity: 0.5;">${this.escapeHtml(user.email)}</small>` : ''}
</td>
<td style="padding: 6px 4px;">${isUserAdmin ? '<span class="badge-admin">Admin</span>' : 'User'}</td>
<td style="padding: 6px 4px; font-family: monospace;">$${balance}</td>
<td style="padding: 6px 4px; font-family: monospace;">$${spend}</td>
<td style="padding: 6px 4px; text-align: right; white-space: nowrap;">
<button type="button" class="btn-table-edit-user as-icon" data-user-id="${user.id}" title="Edit User">Edit</button>
<button type="button" class="btn-table-add-funds as-icon" data-user-id="${user.id}" title="Add Funds">+ Funds</button>
${!isCurrent ? `<button type="button" class="btn-delete-user as-icon" data-user-id="${user.id}" title="Delete User">Delete</button>` : ''}
</td>
`;
const editBtn = tr.querySelector('.btn-table-edit-user');
if (editBtn) {
addSafeEventListener(editBtn, 'click', () => {
this.openEditUserPanel(user);
});
}
const addFundsBtn = tr.querySelector('.btn-table-add-funds');
if (addFundsBtn) {
addSafeEventListener(addFundsBtn, 'click', () => {
this.openAddFundsPanelForUser(user.id);
});
}
const deleteBtn = tr.querySelector('.btn-delete-user');
if (deleteBtn) {
addSafeEventListener(deleteBtn, 'click', async () => {
if (await customConfirm('Delete User', `Are you sure you want to delete user "${user.username}"?`)) {
this.handleDeleteUser(user.id);
}
});
}
this.adminUsersTbody.appendChild(tr);
});
}
/**
* Opens Edit User panel with the selected user's details.
* @param {object} user
*/
openEditUserPanel(user) {
if (this.adminCreateUserPanel) setElementDisplay(this.adminCreateUserPanel, 'none');
if (this.adminAddFundsPanel) setElementDisplay(this.adminAddFundsPanel, 'none');
if (this.adminEditUserPanel) setElementDisplay(this.adminEditUserPanel, 'block');
if (this.editUserIdInput) this.editUserIdInput.value = user.id;
if (this.editUsernameDisplay) this.editUsernameDisplay.textContent = user.username;
if (this.editUsernameInput) this.editUsernameInput.value = user.username;
if (this.editPasswordInput) this.editPasswordInput.value = '';
if (this.editNameInput) this.editNameInput.value = user.name || '';
if (this.editEmailInput) this.editEmailInput.value = user.email || '';
if (this.editRoleSelect) this.editRoleSelect.value = Number(user.admin) === 1 ? 'admin' : 'user';
}
/**
* Handles saving edited user changes (including password update).
*/
handleSaveEditUser() {
if (!this.editUserIdInput || !this.editUsernameInput) return;
const id = parseInt(this.editUserIdInput.value, 10);
const username = this.editUsernameInput.value.trim();
const password = this.editPasswordInput ? this.editPasswordInput.value.trim() : '';
const name = this.editNameInput ? this.editNameInput.value.trim() : '';
const email = this.editEmailInput ? this.editEmailInput.value.trim() : '';
const role = this.editRoleSelect ? this.editRoleSelect.value : 'user';
if (!id || !username) {
this.displayAdminStatus('User ID and username are required.', true);
return;
}
const payload = {
action: 'add_or_update_user',
id,
username,
name,
email,
admin: role === 'admin'
};
if (password) {
payload.passcode = password;
}
this.#api.post(
payload,
(response) => {
if (response.success) {
this.displayAdminStatus(`User "${username}" updated successfully!`, false);
this.resetEditUserPanel();
this.loadProfile();
this.loadAdminUsers();
} else {
this.displayAdminStatus(response.error || response.message || 'Failed to update user.', true);
}
},
(error) => {
this.displayAdminStatus('An error occurred while updating user.', true);
console.error('Update user error:', error);
},
'users'
);
}
/**
* Resets and closes the admin edit user panel.
*/
resetEditUserPanel() {
if (this.adminEditUserPanel) {
setElementDisplay(this.adminEditUserPanel, 'none');
}
if (this.editUserIdInput) this.editUserIdInput.value = '';
if (this.editUsernameDisplay) this.editUsernameDisplay.textContent = '';
if (this.editUsernameInput) this.editUsernameInput.value = '';
if (this.editPasswordInput) this.editPasswordInput.value = '';
if (this.editNameInput) this.editNameInput.value = '';
if (this.editEmailInput) this.editEmailInput.value = '';
if (this.editRoleSelect) this.editRoleSelect.value = 'user';
}
/**
* Opens Add Funds panel pre-selecting a specific user.
* @param {string|number} userId
*/
openAddFundsPanelForUser(userId) {
if (this.adminCreateUserPanel) setElementDisplay(this.adminCreateUserPanel, 'none');
if (this.adminEditUserPanel) setElementDisplay(this.adminEditUserPanel, 'none');
if (this.adminAddFundsPanel) setElementDisplay(this.adminAddFundsPanel, 'block');
if (this.adminFundsUserSelect) this.adminFundsUserSelect.value = String(userId);
if (this.adminFundsAmountInput) this.adminFundsAmountInput.focus();
}
/**
* Handles adding funds to a user from the admin Add Funds form.
*/
handleAddFundsFromAdmin() {
if (!this.adminFundsUserSelect || !this.adminFundsAmountInput) return;
const userId = this.adminFundsUserSelect.value;
const amount = parseFloat(this.adminFundsAmountInput.value);
if (!userId) {
this.displayAdminStatus('Please select a user.', true);
return;
}
if (isNaN(amount) || amount <= 0) {
this.displayAdminStatus('Please enter a valid amount greater than 0.', true);
return;
}
this.#api.post(
{ action: 'add_funds', id: userId, amount },
(response) => {
if (response.success) {
this.displayAdminStatus(`Successfully added $${amount.toFixed(2)} in funds!`, false);
this.resetAddFundsPanel();
this.loadProfile();
this.loadAdminUsers();
} else {
this.displayAdminStatus(response.error || response.message || 'Failed to add funds.', true);
}
},
(error) => {
this.displayAdminStatus('An error occurred while adding funds.', true);
console.error('Add funds error:', error);
},
'users'
);
}
/**
* Resets and closes the admin add funds panel.
*/
resetAddFundsPanel() {
if (this.adminAddFundsPanel) {
setElementDisplay(this.adminAddFundsPanel, 'none');
}
if (this.adminFundsAmountInput) this.adminFundsAmountInput.value = '10.00';
}
/**
* Handles creating a new user from the admin create panel.
*/
handleCreateUser() {
const username = this.newUsernameInput ? this.newUsernameInput.value.trim() : '';
const password = this.newPasswordInput ? this.newPasswordInput.value.trim() : '';
const name = this.newNameInput ? this.newNameInput.value.trim() : '';
const email = this.newEmailInput ? this.newEmailInput.value.trim() : '';
const role = this.newRoleSelect ? this.newRoleSelect.value : 'user';
const balance = this.newBalanceInput ? parseFloat(this.newBalanceInput.value) || 0 : 0;
if (!username || !password) {
this.displayAdminStatus('Username and password are required.', true);
return;
}
this.#api.post(
{
action: 'add_or_update_user',
username,
passcode: password,
name,
email,
admin: role === 'admin',
initial_balance: balance
},
(response) => {
if (response.success) {
this.displayAdminStatus('User created successfully!', false);
this.resetCreateUserPanel();
this.loadAdminUsers();
} else {
this.displayAdminStatus(response.error || response.message || 'Failed to create user.', true);
}
},
(error) => {
this.displayAdminStatus('An error occurred while creating user.', true);
console.error('Create user error:', error);
},
'users'
);
}
/**
* Resets and closes the admin create user panel.
*/
resetCreateUserPanel() {
if (this.adminCreateUserPanel) {
setElementDisplay(this.adminCreateUserPanel, 'none');
}
if (this.newUsernameInput) this.newUsernameInput.value = '';
if (this.newPasswordInput) this.newPasswordInput.value = '';
if (this.newNameInput) this.newNameInput.value = '';
if (this.newEmailInput) this.newEmailInput.value = '';
if (this.newRoleSelect) this.newRoleSelect.value = 'user';
if (this.newBalanceInput) this.newBalanceInput.value = '0.00';
}
/**
* Handles deleting a user by ID.
* @param {number|string} userId
*/
handleDeleteUser(userId) {
this.#api.post(
{ action: 'delete_user', id: userId },
(response) => {
if (response.success) {
this.displayAdminStatus('User deleted successfully!', false);
this.loadAdminUsers();
} else {
this.displayAdminStatus(response.error || response.message || 'Failed to delete user.', true);
}
},
(error) => {
this.displayAdminStatus('An error occurred while deleting user.', true);
console.error('Delete user error:', error);
},
'users'
);
}
/**
* Displays a temporary status message in the admin panel.
* @param {string} message - Status message text.
* @param {boolean} isError - Whether the status represents an error.
*/
displayAdminStatus(message, isError) {
if (this.adminStatusDiv) {
this.adminStatusDiv.textContent = message;
this.adminStatusDiv.className = isError ? 'status-msg status-error' : 'status-msg status-success';
setElementDisplay(this.adminStatusDiv, 'block');
setTimeout(() => {
setElementDisplay(this.adminStatusDiv, 'none');
}, 4000);
}
}
/**
* Utility method to escape HTML text.
* @param {string} str
* @returns {string}
*/
escapeHtml(str) {
if (!str) return '';
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
}
export default Users;