/* 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 =
'
';
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 =
'';
});
}
function renderUsers(container) {
if (usersData.length === 0) {
container.innerHTML =
'';
return;
}
var html =
'| User | Email | Organization | Roles | Status | Actions |
';
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 +=
'' +
initials +
' ' +
escapeHtml(displayName) +
'@' +
escapeHtml(user.username || "") +
" | ";
html += "" + escapeHtml(user.email || "") + " | ";
html += "" + escapeHtml(orgId) + " | ";
html += "" + escapeHtml(roles) + " | ";
html +=
'' +
(isActive ? "Active" : "Inactive") +
" | ";
html += '';
html +=
'';
html +=
'';
html += " |
";
});
html += "
";
container.innerHTML = html;
}
function loadGroups(search) {
var container = document.getElementById("groups-list");
if (!container) return;
container.innerHTML =
'';
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 =
'';
});
}
function renderGroups(container) {
if (groupsData.length === 0) {
container.innerHTML =
'';
return;
}
var html = '';
groupsData.forEach(function (group) {
html += '
';
html +=
'";
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 = '
';
};
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();
}
})();