Files
medicaltracker/public/assets/admin.js
T
Ty Clifford 96de47be43 - The medicine database is now much bigger and has a real update path.
Added:
Live source updater for RxNorm, DailyMed, and openFDA.
Admin dashboard panel to refresh medicine sources.
CLI updater for periodic jobs: 
[scripts/update_medicine_catalog.php](/Users/tyemeclifford/Documents/GH/workout/scripts/update_medicine_catalog.php)
Update history table: medication_source_updates.
Curl fallback for external fetches when PHP stream DNS fails.
README instructions for periodic updates.
I also ran the imports:
Current medication catalog count: 6,002.
Latest source imports:RxNorm: 3,875 inserted, 1,125 updated.
openFDA: 912 inserted, 88 updated.
DailyMed: 95 inserted.

Periodic update command:
/Users/tyemeclifford/frankenphp php-cli 
scripts/update_medicine_catalog.php 5000 all
2026-06-30 15:29:57 -04:00

294 lines
12 KiB
JavaScript

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],
["Med logs", summary.medication_logs || 0],
["Vitals", summary.vital_logs || 0],
["Labs", summary.lab_results || 0],
["Visits", summary.appointments || 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 care 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>
<span>${adminNumber(user.medication_log_count)} meds</span>
<span>${adminNumber(user.vital_log_count)} vitals</span>
<span>${adminNumber(user.lab_result_count)} labs</span>
<span>${adminNumber(user.appointment_count)} appointments</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;
}
function renderRefreshResult(result) {
const target = admin$("#medicineSourceRefreshResult");
if (!target) return;
const rows = (result.sources || []).map((source) => `
<article>
<strong>${adminEscape(source.source)}: ${adminEscape(source.status)}</strong>
<span>${adminNumber(source.inserted)} inserted / ${adminNumber(source.updated)} updated</span>
<small>${adminEscape(source.message || "")}</small>
</article>
`).join("");
target.innerHTML = `
<p>${adminNumber(result.catalog_count)} medicines in catalog after refresh.</p>
<div class="source-refresh-list">${rows}</div>
`;
}
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);
}
});
const refreshForm = admin$("#medicineSourceRefreshForm");
if (refreshForm) {
refreshForm.addEventListener("submit", async (event) => {
event.preventDefault();
const payload = adminFormPayload(refreshForm);
const button = refreshForm.querySelector("button[type='submit']");
if (button) button.disabled = true;
admin$("#medicineSourceRefreshResult").textContent = "Refreshing medicine sources...";
try {
const result = await adminApi("refresh_medication_sources", payload);
renderRefreshResult(result);
adminToast("Medicine sources refreshed");
} catch (error) {
admin$("#medicineSourceRefreshResult").textContent = error.message;
adminToast(error.message);
} finally {
if (button) button.disabled = false;
}
});
}
try {
await loadAdminState();
} catch (error) {
adminToast(error.message);
}
});