botui/ui/suite/settings/settings.js

766 lines
25 KiB
JavaScript
Raw Normal View History

/* 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 =
'<div class="loading-state"><div class="spinner"></div><p>Loading...</p></div>';
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 =
'<div class="error-state"><p>Failed to load users.</p></div>';
});
}
function renderUsers(container) {
if (usersData.length === 0) {
container.innerHTML =
'<div class="empty-state"><p>No users found</p></div>';
return;
}
var html =
'<table class="data-table users-table"><thead><tr><th>User</th><th>Email</th><th>Organization</th><th>Roles</th><th>Status</th><th>Actions</th></tr></thead><tbody>';
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 += "<tr>";
html +=
'<td class="user-cell"><div class="user-avatar">' +
initials +
'</div><div class="user-info"><span class="user-name">' +
escapeHtml(displayName) +
'</span><span class="user-username">@' +
escapeHtml(user.username || "") +
"</span></div></td>";
html += "<td>" + escapeHtml(user.email || "") + "</td>";
html += "<td>" + escapeHtml(orgId) + "</td>";
html += "<td>" + escapeHtml(roles) + "</td>";
html +=
'<td><span class="status-badge ' +
(isActive ? "status-active" : "status-inactive") +
'">' +
(isActive ? "Active" : "Inactive") +
"</span></td>";
html += '<td class="actions-cell">';
html +=
'<button class="btn-icon" onclick="SettingsModule.editUser(\'' +
user.id +
'\')" title="Edit"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg></button>';
html +=
'<button class="btn-icon btn-danger" onclick="SettingsModule.deleteUser(\'' +
user.id +
'\')" title="Delete"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg></button>';
html += "</td></tr>";
});
html += "</tbody></table>";
container.innerHTML = html;
}
function loadGroups(search) {
var container = document.getElementById("groups-list");
if (!container) return;
container.innerHTML =
'<div class="loading-state"><div class="spinner"></div><p>Loading...</p></div>';
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 =
'<div class="error-state"><p>Failed to load groups.</p></div>';
});
}
function renderGroups(container) {
if (groupsData.length === 0) {
container.innerHTML =
'<div class="empty-state"><p>No groups found</p></div>';
return;
}
var html = '<div class="groups-grid">';
groupsData.forEach(function (group) {
html += '<div class="group-card" data-group-id="' + group.id + '">';
html +=
'<div class="group-header"><h3 class="group-name">' +
escapeHtml(group.name) +
'</h3><span class="member-count">' +
(group.member_count || 0) +
" members</span></div>";
if (group.description)
html +=
'<p class="group-description">' +
escapeHtml(group.description) +
"</p>";
html += '<div class="group-actions">';
html +=
'<button class="btn-secondary btn-sm" onclick="SettingsModule.viewGroupMembers(\'' +
group.id +
"')\">View Members</button>";
html +=
'<button class="btn-icon" onclick="SettingsModule.editGroup(\'' +
group.id +
'\')" title="Edit"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg></button>';
html +=
'<button class="btn-icon btn-danger" onclick="SettingsModule.deleteGroup(\'' +
group.id +
'\')" title="Delete"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg></button>';
html += "</div></div>";
});
html += "</div>";
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 =
'<span class="toast-message">' +
message +
'</span><button class="toast-close" onclick="this.parentElement.remove()"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button>';
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 = '<img src="' + e.target.result + '" alt="Avatar">';
};
reader.readAsDataURL(input.files[0]);
}
};
window.removeAvatar = function () {
var avatar = document.getElementById("current-avatar");
if (avatar) avatar.innerHTML = "<span>JD</span>";
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();
}
})();