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 {
#api;
#storage;
loginForm;
usernameInput;
passcodeInput;
loginButton;
errorMessageDiv;
logoutButton;
// Profile Form Elements
profileForm;
latestTimezoneInput;
updateTimezoneButton;
profileStatusMessageDiv;
// Registration Form Elements
registrationForm;
regUsernameInput;
regPasscodeInput;
regAdminCheckbox;
regStatusMessageDiv;
// User Management Elements
userListContainer;
loadUsersButton;
userManagementStatusDiv;
// Edit User Modal Elements
editUserModal;
editUsernameInput;
editPasscodeInput;
adminCheckbox;
saveUserButton;
cancelEditButton;
editUserStatusDiv;
editingUserId = null;
/**
* Initializes the Users class, UI element bindings, and session check.
*/
constructor() {
this.#api = new Api();
this.#storage = new Storage();
// Apply theme on initialization
this.applySavedTheme();
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');
this.profileForm = getEl('id-form-profile-options');
this.latestTimezoneInput = getEl('id-input-latest-timezone') || getEl('profile-latest-timezone');
this.updateTimezoneButton = getEl('id-btn-update-timezone') || getEl('btn-update-timezone');
this.profileStatusMessageDiv = getEl('profile-status-message');
this.registrationForm = getEl('registration-form');
this.regUsernameInput = getEl('reg-username');
this.regPasscodeInput = getEl('reg-passcode');
this.regAdminCheckbox = getEl('reg-admin');
this.regStatusMessageDiv = getEl('reg-status-message');
this.userListContainer = getEl('id-div-user-table-container') || getEl('user-list-container');
this.loadUsersButton = getEl('btn-load-users');
this.userManagementStatusDiv = getEl('user-management-status');
this.editUserModal = getEl('edit-user-modal');
this.editUsernameInput = getEl('id-username') || getEl('edit-username');
this.editPasscodeInput = getEl('id-passcode') || getEl('edit-passcode');
this.adminCheckbox = getEl('id-admin') || getEl('edit-admin');
this.saveUserButton = getEl('id-btn-save-user') || getEl('btn-save-user');
this.cancelEditButton = getEl('id-btn-clear-user-form') || getEl('btn-cancel-edit');
this.editUserStatusDiv = getEl('edit-user-status');
this.initEventListeners();
if (!this.loginForm) {
this.checkLoginStatus();
} else {
fadeOutLoader();
}
}
/**
* 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();
});
addSafeEventListener(this.updateTimezoneButton, 'click', (e) => {
if (e) e.preventDefault();
this.handleUpdateTimezone();
});
const registerButton = getEl('btn-register');
addSafeEventListener(registerButton, 'click', this.handleRegister.bind(this));
addSafeEventListener(this.loadUsersButton, 'click', this.handleLoadUsers.bind(this));
addSafeEventListener(this.saveUserButton, 'click', this.handleSaveUser.bind(this));
addSafeEventListener(this.cancelEditButton, 'click', this.hideEditUserModal.bind(this));
const showRegisterButton = getEl('btn-show-register');
if (showRegisterButton && this.registrationForm) {
addSafeEventListener(showRegisterButton, 'click', () => {
const isHidden = this.registrationForm.style.display === 'none' || this.registrationForm.style.display === '';
setElementDisplay(this.registrationForm, isHidden ? 'block' : 'none');
showRegisterButton.textContent = isHidden ? 'Cancel Registration' : 'Register New User';
});
}
const timezoneSelect = getEl('profile-timezone-select');
if (timezoneSelect && this.latestTimezoneInput) {
addSafeEventListener(timezoneSelect, 'change', (e) => {
this.latestTimezoneInput.value = e.target.value;
});
}
}
/**
* Handles authentication request on login form submission.
*/
handleLogin() {
if (!this.usernameInput || !this.passcodeInput) return;
const username = this.usernameInput.value;
const passcode = this.passcodeInput.value;
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']) {
setElementDisplay(this.loginForm, 'none');
setElementDisplay(this.logoutButton, 'block');
this.loadProfile();
if (response['isAdmin']) {
this.setupAdminInterface();
}
} else {
setElementDisplay(this.loginForm, 'block');
setElementDisplay(this.logoutButton, 'none');
setElementDisplay(this.profileForm, 'none');
fadeOutLoader();
}
},
(error) => {
console.error('Check login status error:', error);
setElementDisplay(this.loginForm, 'block');
setElementDisplay(this.logoutButton, 'none');
setElementDisplay(this.profileForm, 'none');
fadeOutLoader();
},
'users'
);
}
/**
* Fetches user profile data from the server.
*/
loadProfile() {
this.#api.post(
{ action: 'get_profile' },
(response) => {
if (response.success && response['profile']) {
this.displayProfile(response['profile']);
} else if (response.success && !response['profile']) {
this.displayProfileStatus('Profile not found, but you can create one.', false);
setElementDisplay(this.profileForm, 'block');
} else {
this.displayProfileStatus(response.message || response.error || 'Failed to load profile.', true);
}
fadeOutLoader();
},
(error) => {
this.displayProfileStatus('An error occurred while loading profile.', true);
console.error('Load profile error:', error);
fadeOutLoader();
},
'users'
);
}
/**
* Populates the profile form controls with loaded user data.
* @param {object} profile - User profile data object.
*/
displayProfile(profile) {
if (this.latestTimezoneInput && profile.latest_timezone) {
this.latestTimezoneInput.value = profile.latest_timezone;
}
const timezoneSelect = getEl('profile-timezone-select');
if (timezoneSelect && profile.latest_timezone) {
timezoneSelect.value = profile.latest_timezone;
}
setElementDisplay(this.profileForm, 'block');
}
/**
* Submits the updated timezone preference to the server.
*/
handleUpdateTimezone() {
if (!this.latestTimezoneInput) return;
const latest_timezone = this.latestTimezoneInput.value;
this.#api.post(
{ action: 'update_timezone', latest_timezone },
(response) => {
if (response.success) {
this.displayProfileStatus('Timezone updated successfully!', false);
} else {
this.displayProfileStatus(response.message || response.error || 'Failed to update timezone.', true);
}
},
(error) => {
this.displayProfileStatus('An error occurred while updating timezone.', true);
console.error('Update timezone 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.profileStatusMessageDiv) {
this.profileStatusMessageDiv.textContent = message;
this.profileStatusMessageDiv.className = isError ? 'status-error' : 'status-success';
setElementDisplay(this.profileStatusMessageDiv, 'block');
setTimeout(() => {
setElementDisplay(this.profileStatusMessageDiv, 'none');
}, 5000);
}
}
/**
* Displays admin UI elements and loads user list if authorized.
*/
setupAdminInterface() {
const adminNavButton = getEl('id-btn-admin');
if (adminNavButton) {
setElementDisplay(adminNavButton, 'block');
}
const adminForm = getEl('id-form-admin-options');
if (adminForm) {
setElementDisplay(adminForm, 'block');
}
this.handleLoadUsers();
}
/**
* Handles user registration form submission by an admin.
*/
handleRegister() {
if (!this.regUsernameInput || !this.regPasscodeInput) return;
const username = this.regUsernameInput.value;
const passcode = this.regPasscodeInput.value;
const admin = this.regAdminCheckbox ? this.regAdminCheckbox.checked : false;
if (!username || !passcode) {
this.displayRegStatus('Please enter both username and passcode.', true);
return;
}
this.#api.post(
{ action: 'add_or_update_user', username, passcode, admin },
(response) => {
if (response.success) {
this.displayRegStatus('User registered successfully!', false);
this.regUsernameInput.value = '';
this.regPasscodeInput.value = '';
if (this.regAdminCheckbox) this.regAdminCheckbox.checked = false;
this.handleLoadUsers();
} else {
this.displayRegStatus(response.message || response.error || 'Registration failed.', true);
}
},
(error) => {
this.displayRegStatus('An error occurred during registration.', true);
console.error('Registration error:', error);
},
'users'
);
}
/**
* Displays a temporary status message in the registration form.
* @param {string} message - Status message text.
* @param {boolean} isError - Whether the status represents an error.
*/
displayRegStatus(message, isError) {
if (this.regStatusMessageDiv) {
this.regStatusMessageDiv.textContent = message;
this.regStatusMessageDiv.className = isError ? 'status-error' : 'status-success';
setElementDisplay(this.regStatusMessageDiv, 'block');
setTimeout(() => {
setElementDisplay(this.regStatusMessageDiv, 'none');
}, 5000);
}
}
/**
* Requests the list of all registered users for admin management.
*/
handleLoadUsers() {
this.#api.post(
{ action: 'get_all_users' },
(response) => {
if (response.success && response['users']) {
this.renderUserTable(response['users']);
} else {
this.displayUserManagementStatus(response.message || response.error || 'Failed to load users.', true);
}
},
(error) => {
this.displayUserManagementStatus('An error occurred while loading users.', true);
console.error('Load users error:', error);
},
'users'
);
}
/**
* Renders the user list table and attaches row action listeners.
* @param {Array<object>} users - Array of user objects.
*/
renderUserTable(users) {
if (!this.userListContainer) return;
let html = `
<table class="user-management-table" style="width: 100%; border-collapse: collapse;">
<thead>
<tr>
<th>ID</th>
<th>Username</th>
<th>Admin</th>
<th>Created At</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
`;
users.forEach(user => {
html += `
<tr data-user-id="${user.id}">
<td>${user.id}</td>
<td>${user.username}</td>
<td>${Number(user.admin) === 1 ? 'Yes' : 'No'}</td>
<td>${user.created_at || 'N/A'}</td>
<td>
<button class="btn-edit-user" data-user-id="${user.id}">Edit</button>
<button class="btn-delete-user" data-user-id="${user.id}">Delete</button>
</td>
</tr>
`;
});
html += `
</tbody>
</table>
`;
this.userListContainer.innerHTML = html;
this.attachUserTableEventListeners();
}
/**
* Binds click events for edit and delete buttons in the user table.
*/
attachUserTableEventListeners() {
if (!this.userListContainer) return;
this.userListContainer.querySelectorAll('.btn-edit-user').forEach(button => {
addSafeEventListener(button, 'click', (e) => {
const userId = e.target.dataset.userId;
this.showEditUserModal(userId);
});
});
this.userListContainer.querySelectorAll('.btn-delete-user').forEach(button => {
addSafeEventListener(button, 'click', async (e) => {
const userId = e.target.dataset.userId;
if (await customConfirm('Delete User', `Are you sure you want to delete user ID ${userId}?`)) {
this.handleDeleteUser(userId);
}
});
});
}
/**
* Displays the edit user modal populated with the selected user data.
* @param {string|number} userId - The ID of the user to edit.
*/
showEditUserModal(userId) {
this.editingUserId = userId;
this.#api.post(
{ action: 'get_all_users' },
(response) => {
if (response.success && response['users']) {
const user = response['users'].find(u => String(u.id) === String(userId));
if (user) {
if (this.editUsernameInput) this.editUsernameInput.value = user.username;
if (this.editPasscodeInput) this.editPasscodeInput.value = '';
if (this.adminCheckbox) this.adminCheckbox.checked = Number(user.admin) === 1;
if (this.editUserModal) setElementDisplay(this.editUserModal, 'block');
} else {
this.displayUserManagementStatus('User not found.', true);
}
}
},
(error) => {
this.displayUserManagementStatus('Failed to retrieve user details for editing.', true);
console.error(error);
},
'users'
);
}
/**
* Closes and resets the edit user modal form.
*/
hideEditUserModal() {
this.editingUserId = null;
if (this.editUserModal) setElementDisplay(this.editUserModal, 'none');
if (this.editUsernameInput) this.editUsernameInput.value = '';
if (this.editPasscodeInput) this.editPasscodeInput.value = '';
if (this.adminCheckbox) this.adminCheckbox.checked = false;
}
/**
* Submits edited user changes to the server.
*/
handleSaveUser() {
if (!this.editingUserId) return;
const updatedData = {
action: 'add_or_update_user',
id: this.editingUserId
};
if (this.editUsernameInput && this.editUsernameInput.value) {
updatedData.username = this.editUsernameInput.value;
}
if (this.editPasscodeInput && this.editPasscodeInput.value) {
updatedData.passcode = this.editPasscodeInput.value;
}
if (this.adminCheckbox) {
updatedData.admin = this.adminCheckbox.checked;
}
this.#api.post(
updatedData,
(response) => {
if (response.success) {
this.displayUserManagementStatus('User updated successfully!', false);
this.hideEditUserModal();
this.handleLoadUsers();
} else {
this.displayEditUserStatus(response.message || response.error || 'Failed to update user.', true);
}
},
(error) => {
this.displayEditUserStatus('An error occurred while updating user.', true);
console.error('Save user error:', error);
},
'users'
);
}
/**
* Submits a request to delete a specified user.
* @param {string|number} userId - The ID of the user to delete.
*/
handleDeleteUser(userId) {
this.#api.post(
{ action: 'delete_user', id: userId },
(response) => {
if (response.success) {
this.displayUserManagementStatus('User deleted successfully!', false);
this.handleLoadUsers();
} else {
this.displayUserManagementStatus(response.message || response.error || 'Failed to delete user.', true);
}
},
(error) => {
this.displayUserManagementStatus('An error occurred while deleting user.', true);
console.error('Delete user error:', error);
},
'users'
);
}
/**
* Displays a temporary status message in the user management panel.
* @param {string} message - Status message text.
* @param {boolean} isError - Whether the status represents an error.
*/
displayUserManagementStatus(message, isError) {
if (this.userManagementStatusDiv) {
this.userManagementStatusDiv.textContent = message;
this.userManagementStatusDiv.className = isError ? 'status-error' : 'status-success';
setElementDisplay(this.userManagementStatusDiv, 'block');
setTimeout(() => {
setElementDisplay(this.userManagementStatusDiv, 'none');
}, 5000);
}
}
/**
* Displays a temporary status message in the edit user modal.
* @param {string} message - Status message text.
* @param {boolean} isError - Whether the status represents an error.
*/
displayEditUserStatus(message, isError) {
if (this.editUserStatusDiv) {
this.editUserStatusDiv.textContent = message;
this.editUserStatusDiv.className = isError ? 'status-error' : 'status-success';
setElementDisplay(this.editUserStatusDiv, 'block');
setTimeout(() => {
setElementDisplay(this.editUserStatusDiv, 'none');
}, 5000);
}
}
}
export default Users;