/* Settings Module JavaScript */ (function () { "use strict"; var STORAGE_KEYS = { LOCALE: "gb-locale", DATE_FORMAT: "gb-date-format", TIME_FORMAT: "gb-time-format", THEME: "gb-theme", COMPACT_MODE: "gb-compact-mode", ANIMATIONS: "gb-animations", }; var API_ENDPOINTS = { USERS_LIST: "/users/list", USERS_CREATE: "/users/create", USERS_UPDATE: "/users/:user_id/update", USERS_DELETE: "/users/:user_id/delete", USERS_PROFILE: "/users/:user_id/profile", USERS_ASSIGN_ORG: "/users/:user_id/organization", USERS_MEMBERSHIPS: "/users/:user_id/memberships", GROUPS_LIST: "/groups/list", GROUPS_CREATE: "/groups/create", GROUPS_UPDATE: "/groups/:group_id/update", GROUPS_DELETE: "/groups/:group_id/delete", GROUPS_MEMBERS: "/groups/:group_id/members", GROUPS_ADD_MEMBER: "/groups/:group_id/members/add", GROUPS_REMOVE_MEMBER: "/groups/:group_id/members/remove", ORGS_LIST: "/organizations/list", CURRENT_USER: "/api/auth/me", LOGOUT: "/api/auth/logout", }; var currentUser = null; var currentOrgId = null; var usersData = []; var groupsData = []; var organizationsData = []; function getAuthToken() { return localStorage.getItem("gb-access-token"); } function apiUrl(endpoint) { return "/api/directory" + endpoint; } function init() { bindNavigation(); bindToggles(); bindThemeSelector(); bindAvatarUpload(); bindFormValidation(); initLanguageSettings(); loadSavedSettings(); initUserManagement(); initGroupManagement(); loadCurrentUser(); } function loadCurrentUser() { var token = getAuthToken(); if (!token) return; fetch(API_ENDPOINTS.CURRENT_USER, { headers: { Authorization: "Bearer " + token }, }) .then(function (r) { return r.ok ? r.json() : null; }) .then(function (user) { if (user) { currentUser = user; currentOrgId = user.organization_id; updateUserDisplay(); checkAdminAccess(); } }) .catch(function (e) { console.warn("Failed to load user:", e); }); } function updateUserDisplay() { if (!currentUser) return; var displayNameEl = document.getElementById("user-display-name"); var emailEl = document.getElementById("user-email"); if (displayNameEl) { displayNameEl.textContent = currentUser.display_name || currentUser.username || ""; } if (emailEl) { emailEl.textContent = currentUser.email || ""; } } function getInitials(name) { if (!name) return "??"; var parts = name.split(" "); if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase(); return name.substring(0, 2).toUpperCase(); } function checkAdminAccess() { if (!currentUser || !currentUser.roles) return; var isAdmin = currentUser.roles.some(function (r) { var rl = r.toLowerCase(); return rl.indexOf("admin") !== -1 || rl.indexOf("super") !== -1; }); var adminSections = document.querySelectorAll( '[data-admin-only="true"], .admin-only', ); var adminNavItems = document.querySelectorAll( '.nav-item[href="#users"], .nav-item[href="#groups"]', ); adminSections.forEach(function (s) { s.style.display = isAdmin ? "" : "none"; }); adminNavItems.forEach(function (i) { i.style.display = isAdmin ? "" : "none"; }); if (isAdmin) { loadUsers(); loadGroups(); } } function bindNavigation() { document.querySelectorAll(".settings-nav-item").forEach(function (item) { item.addEventListener("click", function (e) { e.preventDefault(); document.querySelectorAll(".settings-nav-item").forEach(function (i) { i.classList.remove("active"); }); this.classList.add("active"); }); }); } function bindToggles() { document .querySelectorAll(".toggle-switch input") .forEach(function (toggle) { toggle.addEventListener("change", function () { saveSetting(this.dataset.setting, this.checked); }); }); } function bindThemeSelector() { document.querySelectorAll(".theme-option input").forEach(function (option) { option.addEventListener("change", function () { document.body.setAttribute("data-theme", this.value); saveSetting("theme", this.value); }); }); } function bindAvatarUpload() { var avatarInput = document.getElementById("avatar-input"); if (avatarInput) { avatarInput.addEventListener("change", function (e) { var file = e.target.files[0]; if (file) { var reader = new FileReader(); reader.onload = function (ev) { var preview = document.querySelector(".avatar-preview img"); if (preview) preview.src = ev.target.result; }; reader.readAsDataURL(file); } }); } } function bindFormValidation() { document.querySelectorAll(".settings-form").forEach(function (form) { form.addEventListener("submit", function (e) { var inputs = form.querySelectorAll("[required]"); var valid = true; inputs.forEach(function (input) { if (!input.value.trim()) { valid = false; input.classList.add("error"); } else { input.classList.remove("error"); } }); if (!valid) e.preventDefault(); }); }); } function initLanguageSettings() { var languageSelect = document.getElementById("language-select"); var savedLocale = localStorage.getItem(STORAGE_KEYS.LOCALE) || "en"; if (languageSelect) languageSelect.value = savedLocale; document.querySelectorAll(".language-option").forEach(function (opt) { opt.classList.toggle("active", opt.dataset.locale === savedLocale); }); } function loadSavedSettings() { var theme = localStorage.getItem(STORAGE_KEYS.THEME); if (theme) { document.body.setAttribute("data-theme", theme); var themeOption = document.querySelector( '.theme-option[data-theme="' + theme + '"]', ); if (themeOption) themeOption.classList.add("active"); } var compactMode = localStorage.getItem(STORAGE_KEYS.COMPACT_MODE) === "true"; var compactToggle = document.querySelector('[name="compact_mode"]'); if (compactToggle) { compactToggle.checked = compactMode; if (compactMode) document.body.classList.add("compact-mode"); } var animations = localStorage.getItem(STORAGE_KEYS.ANIMATIONS) !== "false"; var animationsToggle = document.querySelector('[name="animations"]'); if (animationsToggle) { animationsToggle.checked = animations; if (!animations) document.body.classList.add("no-animations"); } } function saveSetting(key, value) { try { localStorage.setItem(key, JSON.stringify(value)); } catch (e) { console.warn("Failed to save setting:", key, e); } } function initUserManagement() { var addUserBtn = document.getElementById("add-user-btn"); if (addUserBtn) addUserBtn.addEventListener("click", openAddUserDialog); var userSearchInput = document.getElementById("user-search"); if (userSearchInput) { userSearchInput.addEventListener( "input", debounce(function () { loadUsers(this.value); }, 300), ); } var addUserForm = document.getElementById("add-user-form"); if (addUserForm) addUserForm.addEventListener("submit", handleAddUser); } function initGroupManagement() { var addGroupBtn = document.getElementById("add-group-btn"); if (addGroupBtn) addGroupBtn.addEventListener("click", openAddGroupDialog); var groupSearchInput = document.getElementById("group-search"); if (groupSearchInput) { groupSearchInput.addEventListener( "input", debounce(function () { loadGroups(this.value); }, 300), ); } var addGroupForm = document.getElementById("add-group-form"); if (addGroupForm) addGroupForm.addEventListener("submit", handleAddGroup); } function loadUsers(search) { var container = document.getElementById("users-list"); if (!container) return; container.innerHTML = '

Loading...

'; var params = new URLSearchParams(); params.append("per_page", "50"); if (search) params.append("search", search); if (currentOrgId) params.append("organization_id", currentOrgId); var token = getAuthToken(); fetch(apiUrl(API_ENDPOINTS.USERS_LIST) + "?" + params.toString(), { headers: { Authorization: "Bearer " + token }, }) .then(function (r) { if (!r.ok) throw new Error("Failed"); return r.json(); }) .then(function (data) { usersData = data.users || []; renderUsers(container); }) .catch(function () { container.innerHTML = '

Failed to load users.

'; }); } function renderUsers(container) { if (usersData.length === 0) { container.innerHTML = '

No users found

'; return; } var html = ''; usersData.forEach(function (user) { var initials = getInitials(user.username || user.email); var displayName = user.display_name || user.username || user.email; var isActive = user.state === "active" || user.state === "USER_STATE_ACTIVE"; var roles = (user.roles || []).join(", ") || "user"; var orgId = user.organization_id || "-"; html += ""; html += '"; html += ""; html += ""; html += ""; html += '"; html += '"; }); html += "
UserEmailOrganizationRolesStatusActions
' + initials + '
" + escapeHtml(user.email || "") + "" + escapeHtml(orgId) + "" + escapeHtml(roles) + "' + (isActive ? "Active" : "Inactive") + "'; html += ''; html += ''; html += "
"; container.innerHTML = html; } function loadGroups(search) { var container = document.getElementById("groups-list"); if (!container) return; container.innerHTML = '

Loading...

'; var params = new URLSearchParams(); params.append("per_page", "50"); if (search) params.append("search", search); var token = getAuthToken(); fetch(apiUrl(API_ENDPOINTS.GROUPS_LIST) + "?" + params.toString(), { headers: { Authorization: "Bearer " + token }, }) .then(function (r) { if (!r.ok) throw new Error("Failed"); return r.json(); }) .then(function (data) { groupsData = data.groups || []; renderGroups(container); }) .catch(function () { container.innerHTML = '

Failed to load groups.

'; }); } function renderGroups(container) { if (groupsData.length === 0) { container.innerHTML = '

No groups found

'; return; } var html = '
'; groupsData.forEach(function (group) { html += '
'; html += '

' + escapeHtml(group.name) + '

' + (group.member_count || 0) + " members
"; if (group.description) html += '

' + escapeHtml(group.description) + "

"; html += '
'; html += '"; html += ''; html += ''; html += "
"; }); html += "
"; container.innerHTML = html; } function openAddUserDialog() { var dialog = document.getElementById("add-user-dialog"); if (dialog) dialog.showModal(); } function closeAddUserDialog() { var dialog = document.getElementById("add-user-dialog"); if (dialog) { dialog.close(); var f = document.getElementById("add-user-form"); if (f) f.reset(); } } function openAddGroupDialog() { var dialog = document.getElementById("add-group-dialog"); if (dialog) dialog.showModal(); } function closeAddGroupDialog() { var dialog = document.getElementById("add-group-dialog"); if (dialog) { dialog.close(); var f = document.getElementById("add-group-form"); if (f) f.reset(); } } function handleAddUser(e) { e.preventDefault(); var form = e.target, btn = form.querySelector('button[type="submit"]'), orig = btn.textContent; btn.disabled = true; btn.textContent = "Creating..."; var userData = { username: form.username.value, email: form.email.value, password: form.password ? form.password.value : null, first_name: form.first_name.value, last_name: form.last_name.value, role: form.role ? form.role.value : "user", organization_id: currentOrgId || (form.organization_id ? form.organization_id.value : null), roles: form.roles ? [form.roles.value] : ["user"], }; fetch(apiUrl(API_ENDPOINTS.USERS_CREATE), { method: "POST", headers: { "Content-Type": "application/json", Authorization: "Bearer " + getAuthToken(), }, body: JSON.stringify(userData), }) .then(function (r) { if (!r.ok) return r.json().then(function (err) { throw new Error(err.error || "Failed"); }); return r.json(); }) .then(function () { showToast("User created and added to organization", "success"); closeAddUserDialog(); loadUsers(); }) .catch(function (e) { showToast(e.message, "error"); }) .finally(function () { btn.disabled = false; btn.textContent = orig; }); } function handleAddGroup(e) { e.preventDefault(); var form = e.target, btn = form.querySelector('button[type="submit"]'), orig = btn.textContent; btn.disabled = true; btn.textContent = "Creating..."; var groupData = { name: form.name.value, description: form.description ? form.description.value : "", }; fetch(apiUrl(API_ENDPOINTS.GROUPS_CREATE), { method: "POST", headers: { "Content-Type": "application/json", Authorization: "Bearer " + getAuthToken(), }, body: JSON.stringify(groupData), }) .then(function (r) { if (!r.ok) return r.json().then(function (err) { throw new Error(err.error || "Failed"); }); return r.json(); }) .then(function () { showToast("Group created successfully", "success"); closeAddGroupDialog(); loadGroups(); }) .catch(function (e) { showToast(e.message, "error"); }) .finally(function () { btn.disabled = false; btn.textContent = orig; }); } function editUser(userId) { var user = usersData.find(function (u) { return u.id === userId; }); if (!user) return; var d = document.getElementById("edit-user-dialog"); if (d) d.showModal(); } function deleteUser(userId) { if (!confirm("Delete this user?")) return; fetch(apiUrl(API_ENDPOINTS.USERS_DELETE.replace(":user_id", userId)), { method: "DELETE", headers: { Authorization: "Bearer " + getAuthToken() }, }) .then(function (r) { if (!r.ok) throw new Error("Failed"); showToast("User deleted", "success"); loadUsers(); }) .catch(function (e) { showToast(e.message, "error"); }); } function editGroup(groupId) { var group = groupsData.find(function (g) { return g.id === groupId; }); if (!group) return; var d = document.getElementById("edit-group-dialog"); if (d) d.showModal(); } function deleteGroup(groupId) { if (!confirm("Delete this group?")) return; fetch(apiUrl(API_ENDPOINTS.GROUPS_DELETE.replace(":group_id", groupId)), { method: "DELETE", headers: { Authorization: "Bearer " + getAuthToken() }, }) .then(function (r) { if (!r.ok) throw new Error("Failed"); showToast("Group deleted", "success"); loadGroups(); }) .catch(function (e) { showToast(e.message, "error"); }); } function viewGroupMembers(groupId) { var d = document.getElementById("group-members-dialog"); if (d) d.showModal(); } function removeGroupMember(groupId, userId) { if (!confirm("Remove member?")) return; fetch( apiUrl(API_ENDPOINTS.GROUPS_REMOVE_MEMBER.replace(":group_id", groupId)), { method: "POST", headers: { "Content-Type": "application/json", Authorization: "Bearer " + getAuthToken(), }, body: JSON.stringify({ user_id: userId }), }, ) .then(function (r) { if (!r.ok) throw new Error("Failed"); showToast("Member removed", "success"); loadGroups(); }) .catch(function (e) { showToast(e.message, "error"); }); } function handleLogout() { var token = getAuthToken(); if (token) fetch(API_ENDPOINTS.LOGOUT, { method: "POST", headers: { Authorization: "Bearer " + token }, }).catch(function () {}); localStorage.removeItem("gb-access-token"); localStorage.removeItem("gb-refresh-token"); localStorage.removeItem("gb-token-expires"); localStorage.removeItem("gb-user-data"); window.location.href = "/auth/login.html"; } function escapeHtml(text) { if (!text) return ""; var d = document.createElement("div"); d.textContent = text; return d.innerHTML; } function debounce(func, wait) { var timeout; return function () { var ctx = this, args = arguments; clearTimeout(timeout); timeout = setTimeout(function () { func.apply(ctx, args); }, wait); }; } function showToast(message, type) { type = type || "success"; var existing = document.querySelector(".toast"); if (existing) existing.remove(); var toast = document.createElement("div"); toast.className = "toast toast-" + type; toast.innerHTML = '' + message + ''; document.body.appendChild(toast); requestAnimationFrame(function () { toast.classList.add("show"); }); setTimeout(function () { toast.classList.remove("show"); setTimeout(function () { toast.remove(); }, 300); }, 3000); } window.changeLanguage = function (locale) { localStorage.setItem(STORAGE_KEYS.LOCALE, locale); document.documentElement.lang = locale; showToast("Language changed"); setTimeout(function () { location.reload(); }, 500); }; window.selectLanguage = function (locale, element) { document.querySelectorAll(".language-option").forEach(function (o) { o.classList.remove("active"); }); if (element) element.classList.add("active"); changeLanguage(locale); }; window.changeDateFormat = function (format) { localStorage.setItem(STORAGE_KEYS.DATE_FORMAT, format); showToast("Date format updated"); }; window.changeTimeFormat = function (format) { localStorage.setItem(STORAGE_KEYS.TIME_FORMAT, format); showToast("Time format updated"); }; window.setTheme = function (theme, element) { document.body.setAttribute("data-theme", theme); localStorage.setItem(STORAGE_KEYS.THEME, theme); document.querySelectorAll(".theme-option").forEach(function (o) { o.classList.remove("active"); }); if (element) element.classList.add("active"); showToast("Theme updated"); }; window.toggleCompactMode = function (checkbox) { var enabled = checkbox.checked; localStorage.setItem(STORAGE_KEYS.COMPACT_MODE, enabled); document.body.classList.toggle("compact-mode", enabled); showToast(enabled ? "Compact mode enabled" : "Compact mode disabled"); }; window.toggleAnimations = function (checkbox) { var enabled = checkbox.checked; localStorage.setItem(STORAGE_KEYS.ANIMATIONS, enabled); document.body.classList.toggle("no-animations", !enabled); showToast(enabled ? "Animations enabled" : "Animations disabled"); }; window.showSection = function (sectionId, navElement) { document.querySelectorAll(".settings-section").forEach(function (s) { s.classList.remove("active"); }); document.querySelectorAll(".nav-item").forEach(function (i) { i.classList.remove("active"); }); var section = document.getElementById(sectionId + "-section"); if (section) section.classList.add("active"); if (navElement) navElement.classList.add("active"); history.replaceState(null, "", "#" + sectionId); }; window.showToast = showToast; window.previewAvatar = function (input) { if (input.files && input.files[0]) { var reader = new FileReader(); reader.onload = function (e) { var avatar = document.getElementById("current-avatar"); if (avatar) avatar.innerHTML = 'Avatar'; }; reader.readAsDataURL(input.files[0]); } }; window.removeAvatar = function () { var avatar = document.getElementById("current-avatar"); if (avatar) avatar.innerHTML = "JD"; showToast("Avatar removed"); }; function initFromHash() { var hash = window.location.hash.slice(1); if (hash) { var navItem = document.querySelector('.nav-item[href="#' + hash + '"]'); if (navItem) showSection(hash, navItem); } } window.SettingsModule = { init: init, changeLanguage: window.changeLanguage, changeDateFormat: window.changeDateFormat, changeTimeFormat: window.changeTimeFormat, setTheme: window.setTheme, showToast: showToast, editUser: editUser, deleteUser: deleteUser, editGroup: editGroup, deleteGroup: deleteGroup, viewGroupMembers: viewGroupMembers, removeGroupMember: removeGroupMember, logout: handleLogout, }; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", function () { init(); initFromHash(); }); } else { init(); initFromHash(); } })();