This commit is contained in:
Ty Clifford
2026-06-30 11:14:42 -04:00
commit 0fe5a92f90
18 changed files with 4775 additions and 0 deletions
+248
View File
@@ -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("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
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);
}
});