-
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
const adminState = {
|
||||
data: null,
|
||||
userId: localStorage.getItem("nht:adminUserId") || localStorage.getItem("nht:userId") || "",
|
||||
};
|
||||
|
||||
const admin$ = (selector, root = document) => root.querySelector(selector);
|
||||
|
||||
function adminEscape(value) {
|
||||
return String(value ?? "")
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
function adminNumber(value, digits = 0) {
|
||||
return Number(value || 0).toLocaleString(undefined, {
|
||||
maximumFractionDigits: digits,
|
||||
minimumFractionDigits: digits,
|
||||
});
|
||||
}
|
||||
|
||||
function adminToast(message) {
|
||||
const toast = admin$("#siteToast");
|
||||
if (!toast) return;
|
||||
toast.textContent = message;
|
||||
toast.classList.add("show");
|
||||
window.clearTimeout(adminToast.timer);
|
||||
adminToast.timer = window.setTimeout(() => toast.classList.remove("show"), 2400);
|
||||
}
|
||||
|
||||
async function adminApi(action, payload = null, method = "POST") {
|
||||
const options = { method };
|
||||
if (payload) {
|
||||
options.headers = { "Content-Type": "application/json" };
|
||||
options.body = JSON.stringify(payload);
|
||||
}
|
||||
const response = await fetch(`/api.php?action=${encodeURIComponent(action)}`, options);
|
||||
const json = await response.json();
|
||||
if (!response.ok || json.error) {
|
||||
throw new Error(json.error || "Request failed.");
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
async function loadAdminState() {
|
||||
const params = new URLSearchParams({
|
||||
action: "state",
|
||||
view: "weekly",
|
||||
});
|
||||
if (adminState.userId) {
|
||||
params.set("user_id", adminState.userId);
|
||||
}
|
||||
|
||||
const response = await fetch(`/api.php?${params.toString()}`);
|
||||
const json = await response.json();
|
||||
if (!response.ok || json.error) {
|
||||
throw new Error(json.error || "Unable to load admin dashboard.");
|
||||
}
|
||||
|
||||
adminState.data = json;
|
||||
adminState.userId = String(json.currentUser.id);
|
||||
localStorage.setItem("nht:adminUserId", adminState.userId);
|
||||
renderAdminPage();
|
||||
}
|
||||
|
||||
function renderAdminPage() {
|
||||
const data = adminState.data;
|
||||
const profileSelect = admin$("#adminProfileSelect");
|
||||
profileSelect.innerHTML = data.users.map((user) => `
|
||||
<option value="${user.id}" ${String(user.id) === String(data.currentUser.id) ? "selected" : ""}>
|
||||
${adminEscape(user.name)} (${adminEscape(user.role)})
|
||||
</option>
|
||||
`).join("");
|
||||
|
||||
admin$("#adminPanel").hidden = !data.currentUserIsAdmin;
|
||||
admin$("#adminDenied").hidden = data.currentUserIsAdmin;
|
||||
if (!data.currentUserIsAdmin) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderAdminStats();
|
||||
renderAdminUsers();
|
||||
}
|
||||
|
||||
function renderAdminStats() {
|
||||
const summary = adminState.data.adminSummary || {};
|
||||
const stats = [
|
||||
["Total", summary.total_users || 0],
|
||||
["Active", summary.active_users || 0],
|
||||
["Inactive", summary.inactive_users || 0],
|
||||
["Admins", summary.admins || 0],
|
||||
["Members", summary.members || 0],
|
||||
];
|
||||
|
||||
admin$("#adminStats").innerHTML = stats.map(([label, value]) => `
|
||||
<article class="admin-stat">
|
||||
<span>${adminEscape(label)}</span>
|
||||
<strong>${adminNumber(value)}</strong>
|
||||
</article>
|
||||
`).join("");
|
||||
}
|
||||
|
||||
function renderAdminUsers() {
|
||||
const currentId = Number(adminState.data.currentUser.id);
|
||||
const rows = adminState.data.adminUsers || [];
|
||||
admin$("#adminUserList").innerHTML = rows.length ? rows.map((user) => {
|
||||
const isCurrent = Number(user.id) === currentId;
|
||||
const nextStatus = user.status === "active" ? "inactive" : "active";
|
||||
const height = Number(user.height_in || 0) > 0 ? ` / ${adminNumber(user.height_in, 1)} in` : "";
|
||||
const training = user.training_level ? ` / ${user.training_level}` : "";
|
||||
return `
|
||||
<article class="admin-user-card">
|
||||
<div class="admin-user-main">
|
||||
<div>
|
||||
<h3>${adminEscape(user.name)}${isCurrent ? " (current)" : ""}</h3>
|
||||
<p class="details-line">${adminEscape(user.email || "No email")}</p>
|
||||
<p class="details-line">${adminEscape(user.fitness_focus || "No focus set")}</p>
|
||||
<p class="timeline-meta">${adminEscape(user.phone || "No phone")}${adminEscape(training)}${adminEscape(height)}</p>
|
||||
</div>
|
||||
<div class="admin-badges">
|
||||
<span class="badge ${adminEscape(user.role)}">${adminEscape(user.role)}</span>
|
||||
<span class="badge ${adminEscape(user.status)}">${adminEscape(user.status)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-user-metrics">
|
||||
<span>${adminNumber(user.routine_count)} routines</span>
|
||||
<span>${adminNumber(user.workout_count)} workouts</span>
|
||||
<span>${adminNumber(user.nutrition_log_count)} nutrition logs</span>
|
||||
<span>${adminNumber(user.supplement_log_count)} supplement logs</span>
|
||||
<span>${adminNumber(user.metric_count)} metrics</span>
|
||||
</div>
|
||||
<div class="status-row">
|
||||
<button class="status-button" type="button" data-admin-action="edit" data-admin-user-id="${user.id}">Edit</button>
|
||||
<button class="status-button" type="button" data-admin-action="status" data-admin-user-id="${user.id}" data-next-status="${nextStatus}">
|
||||
${nextStatus === "active" ? "Activate" : "Deactivate"}
|
||||
</button>
|
||||
<button class="status-button danger-action" type="button" data-admin-action="delete" data-admin-user-id="${user.id}" ${isCurrent ? "disabled" : ""}>Delete</button>
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
}).join("") : `<div class="admin-denied">No users found.</div>`;
|
||||
}
|
||||
|
||||
function resetAdminForm() {
|
||||
const form = admin$("#adminUserForm");
|
||||
form.reset();
|
||||
form.elements.target_user_id.value = "";
|
||||
form.elements.role.value = "member";
|
||||
form.elements.status.value = "active";
|
||||
form.elements.theme_mode.value = "dark";
|
||||
form.elements.theme_accent.value = "green";
|
||||
admin$("#adminUserFormTitle").textContent = "Create Profile";
|
||||
}
|
||||
|
||||
function fillAdminForm(user) {
|
||||
const form = admin$("#adminUserForm");
|
||||
form.elements.target_user_id.value = user.id || "";
|
||||
form.elements.name.value = user.name || "";
|
||||
form.elements.email.value = user.email || "";
|
||||
form.elements.role.value = user.role || "member";
|
||||
form.elements.status.value = user.status || "active";
|
||||
form.elements.phone.value = user.phone || "";
|
||||
form.elements.birthday.value = user.birthday || "";
|
||||
form.elements.height_in.value = Number(user.height_in || 0) > 0 ? user.height_in : "";
|
||||
form.elements.training_level.value = user.training_level || "";
|
||||
form.elements.theme_mode.value = user.theme_mode || "dark";
|
||||
form.elements.theme_accent.value = user.theme_accent || "green";
|
||||
form.elements.fitness_focus.value = user.fitness_focus || "";
|
||||
form.elements.emergency_contact.value = user.emergency_contact || "";
|
||||
admin$("#adminUserFormTitle").textContent = `Edit ${user.name}`;
|
||||
}
|
||||
|
||||
function adminFormPayload(form) {
|
||||
const payload = Object.fromEntries(new FormData(form).entries());
|
||||
payload.acting_user_id = adminState.data.currentUser.id;
|
||||
return payload;
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
admin$("#adminProfileSelect").addEventListener("change", async (event) => {
|
||||
adminState.userId = event.target.value;
|
||||
localStorage.setItem("nht:adminUserId", adminState.userId);
|
||||
await loadAdminState();
|
||||
});
|
||||
|
||||
admin$("#clearAdminUserFormButton").addEventListener("click", resetAdminForm);
|
||||
|
||||
admin$("#adminUserList").addEventListener("click", async (event) => {
|
||||
const button = event.target.closest("[data-admin-action]");
|
||||
if (!button) return;
|
||||
const user = (adminState.data.adminUsers || []).find((item) => String(item.id) === button.dataset.adminUserId);
|
||||
if (!user) return;
|
||||
|
||||
if (button.dataset.adminAction === "edit") {
|
||||
fillAdminForm(user);
|
||||
admin$("#adminUserForm").scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (button.dataset.adminAction === "status") {
|
||||
await adminApi("update_user", {
|
||||
acting_user_id: adminState.data.currentUser.id,
|
||||
target_user_id: user.id,
|
||||
status: button.dataset.nextStatus,
|
||||
});
|
||||
adminToast("Profile status updated");
|
||||
}
|
||||
|
||||
if (button.dataset.adminAction === "delete") {
|
||||
const ok = window.confirm(`Delete ${user.name} and all tracker data attached to this profile?`);
|
||||
if (!ok) return;
|
||||
await adminApi("delete_user", {
|
||||
acting_user_id: adminState.data.currentUser.id,
|
||||
target_user_id: user.id,
|
||||
});
|
||||
adminToast("Profile deleted");
|
||||
}
|
||||
|
||||
resetAdminForm();
|
||||
await loadAdminState();
|
||||
} catch (error) {
|
||||
adminToast(error.message);
|
||||
}
|
||||
});
|
||||
|
||||
admin$("#adminUserForm").addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
const payload = adminFormPayload(event.currentTarget);
|
||||
const action = payload.target_user_id ? "update_user" : "create_user";
|
||||
try {
|
||||
await adminApi(action, payload);
|
||||
resetAdminForm();
|
||||
await loadAdminState();
|
||||
adminToast(action === "create_user" ? "Profile created" : "Profile updated");
|
||||
} catch (error) {
|
||||
adminToast(error.message);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await loadAdminState();
|
||||
} catch (error) {
|
||||
adminToast(error.message);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user