-
This commit is contained in:
@@ -92,6 +92,10 @@ function renderAdminStats() {
|
||||
["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]) => `
|
||||
@@ -116,7 +120,7 @@ function renderAdminUsers() {
|
||||
<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="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">
|
||||
@@ -130,6 +134,10 @@ function renderAdminUsers() {
|
||||
<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>
|
||||
|
||||
+182
-13
@@ -114,6 +114,7 @@ function render() {
|
||||
applyTheme();
|
||||
renderControls();
|
||||
renderSummaries();
|
||||
renderMedical();
|
||||
renderTimeline();
|
||||
renderNutrition();
|
||||
renderRoutines();
|
||||
@@ -141,7 +142,7 @@ function applyTheme() {
|
||||
function renderControls() {
|
||||
const data = appState.data;
|
||||
const user = data.currentUser;
|
||||
$("#profileFocus").textContent = user.fitness_focus || "Multi-profile training dashboard";
|
||||
$("#profileFocus").textContent = user.fitness_focus || "Multi-profile medical dashboard";
|
||||
if (data.currentUserIsAdmin) {
|
||||
$("#profileFocus").textContent += " / admin";
|
||||
}
|
||||
@@ -158,18 +159,21 @@ function renderControls() {
|
||||
}
|
||||
|
||||
function renderSummaries() {
|
||||
const { workoutSummary, nutritionSummary, latestMetric, supplementLogs } = appState.data;
|
||||
const actual = nutritionSummary.actual;
|
||||
const target = nutritionSummary.target;
|
||||
const supplementTaken = supplementLogs.filter((item) => item.status === "taken").length;
|
||||
const { latestVitals, medicationLogs, labResults, symptomLogs, allergyRecords, appointments } = appState.data;
|
||||
const medicationTaken = medicationLogs.filter((item) => item.status === "taken").length;
|
||||
const bp = latestVitals && Number(latestVitals.systolic) > 0 ? `${latestVitals.systolic}/${latestVitals.diastolic}` : "No BP";
|
||||
const highestSeverity = symptomLogs.reduce((max, item) => Math.max(max, Number(item.severity || 0)), 0);
|
||||
const activeAllergies = allergyRecords.filter((item) => item.status === "active").length;
|
||||
const scheduledAppointments = appointments.filter((item) => item.status === "scheduled").length;
|
||||
|
||||
const cards = [
|
||||
["Completion", `${number(workoutSummary.completion_rate, 1)}%`, `${workoutSummary.completed}/${workoutSummary.planned} workouts`],
|
||||
["Volume", number(workoutSummary.weight_volume_lbs), "lb lifted"],
|
||||
["Cardio", `${number(workoutSummary.minutes, 0)} min`, `${number(workoutSummary.distance_mi, 2)} mi`],
|
||||
["Calories", number(actual.calories), `${number(target.calories)} target`],
|
||||
["Protein", `${number(actual.protein_g)} g`, `${number(target.protein_g)} g target`],
|
||||
["Weight", latestMetric ? `${number(latestMetric.weight_lbs, 1)} lb` : "No log", latestMetric ? `${number(latestMetric.sleep_hours, 1)} hr sleep` : `${supplementTaken} doses taken`],
|
||||
["Medications", `${number(medicationLogs.length)}`, `${number(medicationTaken)} taken in range`],
|
||||
["Blood Pressure", bp, latestVitals ? `${number(latestVitals.pulse)} pulse` : "No vitals log"],
|
||||
["Glucose", latestVitals && Number(latestVitals.glucose_mg_dl) > 0 ? `${number(latestVitals.glucose_mg_dl)} mg/dL` : "No glucose", latestVitals ? `${number(latestVitals.oxygen_pct, 1)}% oxygen` : "No oxygen log"],
|
||||
["Labs", `${number(labResults.length)}`, "results in range"],
|
||||
["Symptoms", `${number(symptomLogs.length)}`, `highest severity ${number(highestSeverity)}`],
|
||||
["Allergies", `${number(activeAllergies)}`, `${number(allergyRecords.length)} total records`],
|
||||
["Appointments", `${number(scheduledAppointments)}`, "scheduled in range"],
|
||||
];
|
||||
|
||||
$("#summaryGrid").innerHTML = cards
|
||||
@@ -244,6 +248,143 @@ function renderWorkoutItem(workout) {
|
||||
`;
|
||||
}
|
||||
|
||||
function renderMedical() {
|
||||
renderMedicationLogs();
|
||||
renderVitalLogs();
|
||||
renderLabResults();
|
||||
renderSymptomLogs();
|
||||
renderAllergyRecords();
|
||||
renderAppointments();
|
||||
renderImmunizationRecords();
|
||||
renderStudies();
|
||||
}
|
||||
|
||||
function renderMedicationLogs() {
|
||||
const logs = appState.data.medicationLogs || [];
|
||||
$("#medicationLogList").innerHTML = logs.length ? logs.map((log) => `
|
||||
<article class="workout-item">
|
||||
<div class="workout-top">
|
||||
<div>
|
||||
<h3>${escapeHtml(log.medication_name)}</h3>
|
||||
<p class="details-line">${shortDate(log.taken_on)} | ${escapeHtml(log.dose || "open dose")} | ${escapeHtml(log.route || "route not set")} | ${escapeHtml(log.frequency || "frequency not set")}</p>
|
||||
${log.class_name ? `<p class="timeline-meta">${escapeHtml(log.class_name)}</p>` : ""}
|
||||
</div>
|
||||
<span class="badge ${escapeHtml(log.status)}">${escapeHtml(log.status)}</span>
|
||||
</div>
|
||||
<p class="muted">${escapeHtml([log.prescriber, log.notes].filter(Boolean).join(" | ") || "No notes")}</p>
|
||||
${log.rxnorm_url ? `<a class="text-button source-button" href="${escapeHtml(log.rxnorm_url)}">RxNorm</a>` : ""}
|
||||
</article>
|
||||
`).join("") : `<div class="empty-state">No medication logs in this range</div>`;
|
||||
}
|
||||
|
||||
function renderVitalLogs() {
|
||||
const logs = appState.data.vitalLogs || [];
|
||||
$("#vitalLogList").innerHTML = logs.length ? logs.map((log) => {
|
||||
const bp = Number(log.systolic) > 0 ? `${log.systolic}/${log.diastolic}` : "No BP";
|
||||
return `
|
||||
<article class="log-item">
|
||||
<div class="log-top">
|
||||
<strong>${shortDate(log.measured_on)}</strong>
|
||||
<span class="badge">${bp}</span>
|
||||
</div>
|
||||
<p>${number(log.pulse)} pulse / ${number(log.temperature_f, 1)} F / ${number(log.oxygen_pct, 1)}% O2 / ${number(log.glucose_mg_dl)} glucose / pain ${number(log.pain_score)}</p>
|
||||
<p class="muted">${escapeHtml([log.symptoms, log.notes].filter(Boolean).join(" | ") || "No symptoms noted")}</p>
|
||||
</article>
|
||||
`;
|
||||
}).join("") : `<div class="empty-state">No vitals in this range</div>`;
|
||||
}
|
||||
|
||||
function renderLabResults() {
|
||||
const labs = appState.data.labResults || [];
|
||||
$("#labResultList").innerHTML = labs.length ? labs.map((lab) => `
|
||||
<article class="log-item">
|
||||
<div class="log-top">
|
||||
<strong>${escapeHtml(lab.test_name)}</strong>
|
||||
<span class="badge">${escapeHtml(lab.result_value)} ${escapeHtml(lab.unit || "")}</span>
|
||||
</div>
|
||||
<p>${shortDate(lab.collected_on)} / ${escapeHtml(lab.reference_range || "No reference range")}</p>
|
||||
<p class="muted">${escapeHtml([lab.ordering_clinician, lab.notes].filter(Boolean).join(" | ") || "No notes")}</p>
|
||||
</article>
|
||||
`).join("") : `<div class="empty-state">No lab results in this range</div>`;
|
||||
}
|
||||
|
||||
function renderSymptomLogs() {
|
||||
const logs = appState.data.symptomLogs || [];
|
||||
$("#symptomLogList").innerHTML = logs.length ? logs.map((log) => `
|
||||
<article class="log-item">
|
||||
<div class="log-top">
|
||||
<strong>${escapeHtml(log.symptom)}</strong>
|
||||
<span class="badge">severity ${number(log.severity)}</span>
|
||||
</div>
|
||||
<p>${shortDate(log.observed_on)} / ${escapeHtml(log.body_area || "body area not set")} / ${escapeHtml(log.duration || "duration not set")}</p>
|
||||
<p class="muted">${escapeHtml([log.triggers, log.action_taken, log.notes].filter(Boolean).join(" | ") || "No notes")}</p>
|
||||
</article>
|
||||
`).join("") : `<div class="empty-state">No symptom logs in this range</div>`;
|
||||
}
|
||||
|
||||
function renderAllergyRecords() {
|
||||
const records = appState.data.allergyRecords || [];
|
||||
$("#allergyRecordList").innerHTML = records.length ? records.map((record) => `
|
||||
<article class="log-item">
|
||||
<div class="log-top">
|
||||
<strong>${escapeHtml(record.allergen)}</strong>
|
||||
<span class="badge ${escapeHtml(record.status)}">${escapeHtml(record.severity)}</span>
|
||||
</div>
|
||||
<p>${escapeHtml(record.reaction || "Reaction not set")} / ${escapeHtml(record.status)}</p>
|
||||
<p class="muted">${escapeHtml([record.first_noted_on ? `first noted ${shortDate(record.first_noted_on)}` : "", record.notes].filter(Boolean).join(" | ") || "No notes")}</p>
|
||||
</article>
|
||||
`).join("") : `<div class="empty-state">No allergy records yet</div>`;
|
||||
}
|
||||
|
||||
function renderAppointments() {
|
||||
const appointments = appState.data.appointments || [];
|
||||
$("#appointmentList").innerHTML = appointments.length ? appointments.map((appointment) => `
|
||||
<article class="log-item">
|
||||
<div class="log-top">
|
||||
<strong>${escapeHtml(appointment.reason)}</strong>
|
||||
<span class="badge ${escapeHtml(appointment.status)}">${escapeHtml(appointment.status)}</span>
|
||||
</div>
|
||||
<p>${shortDate(appointment.appointment_on)} / ${escapeHtml(appointment.clinician || "clinician not set")} / ${escapeHtml(appointment.specialty || "specialty not set")}</p>
|
||||
<p class="muted">${escapeHtml([appointment.location, appointment.follow_up_notes].filter(Boolean).join(" | ") || "No follow-up notes")}</p>
|
||||
</article>
|
||||
`).join("") : `<div class="empty-state">No appointments in this range</div>`;
|
||||
}
|
||||
|
||||
function renderImmunizationRecords() {
|
||||
const records = appState.data.immunizationRecords || [];
|
||||
$("#immunizationRecordList").innerHTML = records.length ? records.slice(0, 8).map((record) => `
|
||||
<article class="log-item">
|
||||
<div class="log-top">
|
||||
<strong>${escapeHtml(record.vaccine_name)}</strong>
|
||||
<span class="badge">${shortDate(record.administered_on)}</span>
|
||||
</div>
|
||||
<p>${escapeHtml(record.dose || "dose not set")} / ${escapeHtml(record.clinician_or_site || "site not set")}</p>
|
||||
<p class="muted">${escapeHtml([record.next_due_on ? `next due ${shortDate(record.next_due_on)}` : "", record.lot_number ? `lot ${record.lot_number}` : "", record.notes].filter(Boolean).join(" | ") || "No notes")}</p>
|
||||
</article>
|
||||
`).join("") : `<div class="empty-state">No immunization records yet</div>`;
|
||||
}
|
||||
|
||||
function renderStudies() {
|
||||
const studies = (appState.data.researchStudies || []).slice(0, 6);
|
||||
$("#studyList").innerHTML = studies.length ? studies.map((study) => `
|
||||
<article class="supplement-item">
|
||||
<div class="supplement-top">
|
||||
<div>
|
||||
<h3>${escapeHtml(study.title)}</h3>
|
||||
<p class="details-line">${escapeHtml(study.category)} | ${escapeHtml(study.related_medicine || "general")}</p>
|
||||
</div>
|
||||
<span class="badge">${escapeHtml(study.study_type || "research")}</span>
|
||||
</div>
|
||||
<p>${escapeHtml(study.summary)}</p>
|
||||
<div class="status-row">
|
||||
<a class="status-button" href="${escapeHtml(study.source_url)}">NIH/NLM</a>
|
||||
<a class="status-button" href="${escapeHtml(study.pubmed_url)}">PubMed</a>
|
||||
<a class="status-button" href="${escapeHtml(study.clinicaltrials_url)}">ClinicalTrials.gov</a>
|
||||
</div>
|
||||
</article>
|
||||
`).join("") : `<div class="empty-state">No study links seeded</div>`;
|
||||
}
|
||||
|
||||
function renderNutrition() {
|
||||
const { nutritionSummary, nutritionLogs } = appState.data;
|
||||
const target = nutritionSummary.target;
|
||||
@@ -374,7 +515,7 @@ function renderAdmin() {
|
||||
<div>
|
||||
<h3>${escapeHtml(user.name)}${isCurrent ? " (current)" : ""}</h3>
|
||||
<p class="details-line">${escapeHtml(user.email || "No email")}</p>
|
||||
<p class="details-line">${escapeHtml(user.fitness_focus || "No focus set")}</p>
|
||||
<p class="details-line">${escapeHtml(user.fitness_focus || "No care focus set")}</p>
|
||||
<p class="timeline-meta">${escapeHtml(user.phone || "No phone")}${escapeHtml(training)}${escapeHtml(height)}</p>
|
||||
</div>
|
||||
<div class="admin-badges">
|
||||
@@ -388,6 +529,10 @@ function renderAdmin() {
|
||||
<span>${number(user.nutrition_log_count)} nutrition logs</span>
|
||||
<span>${number(user.supplement_log_count)} supplement logs</span>
|
||||
<span>${number(user.metric_count)} metrics</span>
|
||||
<span>${number(user.medication_log_count)} meds</span>
|
||||
<span>${number(user.vital_log_count)} vitals</span>
|
||||
<span>${number(user.lab_result_count)} labs</span>
|
||||
<span>${number(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>
|
||||
@@ -455,18 +600,26 @@ function option(value, label, selected = false) {
|
||||
}
|
||||
|
||||
function populateForms() {
|
||||
const { routines, exercises, supplements, nutritionGoals, range } = appState.data;
|
||||
const { routines, exercises, supplements, nutritionGoals, medicationCatalog, range } = appState.data;
|
||||
const routineOptions = option("", "No routine") + routines.map((routine) => option(routine.id, `${routine.name} (${routine.planner_scope})`)).join("");
|
||||
const goalOptions = option("", "Separate target") + nutritionGoals.map((goal) => option(goal.id, `${goal.label} (${goal.scope})`)).join("");
|
||||
const exerciseOptions = option("", "Choose exercise") + exercises.map((exercise) => option(exercise.id, `${exercise.name} (${exercise.category})`)).join("");
|
||||
const supplementOptions = option("", "Choose supplement") + supplements.map((supplement) => option(supplement.id, `${supplement.name} (${supplement.timing})`)).join("");
|
||||
const medicationOptions = option("", "Choose medication") + medicationCatalog.map((medicine) => option(medicine.id, `${medicine.generic_name} (${medicine.class_name})`)).join("");
|
||||
|
||||
$$("#workoutForm [name='routine_id']").forEach((select) => select.innerHTML = routineOptions);
|
||||
$$("#routineForm [name='nutrition_goal_id']").forEach((select) => select.innerHTML = goalOptions);
|
||||
$$("#workoutForm [name='exercise_id']").forEach((select) => select.innerHTML = exerciseOptions);
|
||||
$$("#supplementLogForm [name='supplement_id']").forEach((select) => select.innerHTML = supplementOptions);
|
||||
$$("#medicationLogForm [name='medication_id']").forEach((select) => select.innerHTML = medicationOptions);
|
||||
|
||||
const dateFields = [
|
||||
"#medicationLogForm [name='taken_on']",
|
||||
"#vitalLogForm [name='measured_on']",
|
||||
"#labResultForm [name='collected_on']",
|
||||
"#symptomLogForm [name='observed_on']",
|
||||
"#appointmentForm [name='appointment_on']",
|
||||
"#immunizationRecordForm [name='administered_on']",
|
||||
"#workoutForm [name='scheduled_on']",
|
||||
"#routineForm [name='start_date']",
|
||||
"#nutritionGoalForm [name='start_date']",
|
||||
@@ -575,6 +728,15 @@ function wireControls() {
|
||||
$("#supplementLogForm [name='dose']").value = supplement.default_dose;
|
||||
});
|
||||
|
||||
$("#medicationLogForm [name='medication_id']").addEventListener("change", (event) => {
|
||||
const medicine = appState.data.medicationCatalog.find((item) => String(item.id) === event.target.value);
|
||||
if (!medicine) return;
|
||||
$("#medicationLogForm [name='medication_name']").value = medicine.generic_name;
|
||||
if (!$("#medicationLogForm [name='notes']").value) {
|
||||
$("#medicationLogForm [name='notes']").value = medicine.tracking_note;
|
||||
}
|
||||
});
|
||||
|
||||
$("#newAdminUserButton").addEventListener("click", () => {
|
||||
resetAdminUserForm();
|
||||
$("#adminUserForm").scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
@@ -642,6 +804,13 @@ async function saveTheme(mode = null, accent = null) {
|
||||
}
|
||||
|
||||
function wireForms() {
|
||||
wireForm("#medicationLogForm", "create_medication_log", "Medication logged");
|
||||
wireForm("#vitalLogForm", "create_vital_log", "Vitals logged");
|
||||
wireForm("#labResultForm", "create_lab_result", "Lab result logged");
|
||||
wireForm("#symptomLogForm", "create_symptom_log", "Symptom logged");
|
||||
wireForm("#allergyRecordForm", "create_allergy_record", "Allergy saved");
|
||||
wireForm("#appointmentForm", "create_appointment", "Appointment saved");
|
||||
wireForm("#immunizationRecordForm", "create_immunization_record", "Immunization saved");
|
||||
wireForm("#workoutForm", "create_workout", "Workout saved");
|
||||
wireForm("#routineForm", "create_routine", "Routine saved");
|
||||
wireForm("#nutritionGoalForm", "create_nutrition_goal", "Nutrition target saved");
|
||||
|
||||
@@ -323,6 +323,7 @@ figcaption a {
|
||||
.detail-grid article,
|
||||
.site-metric-stack article,
|
||||
.signup-steps article,
|
||||
.term-table article,
|
||||
.admin-denied,
|
||||
.admin-stat,
|
||||
.admin-user-card,
|
||||
@@ -346,6 +347,7 @@ figcaption a {
|
||||
|
||||
.feature-grid p,
|
||||
.detail-grid p,
|
||||
.term-table p,
|
||||
.site-metric-stack span,
|
||||
.signup-steps span,
|
||||
.details-line,
|
||||
@@ -355,6 +357,43 @@ figcaption a {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.term-table {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.term-table article {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(180px, 0.34fr) minmax(0, 0.4fr) minmax(0, 0.4fr) auto;
|
||||
gap: 12px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.term-table span {
|
||||
color: var(--accent);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.term-table a,
|
||||
.source-links a {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.source-links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.source-links a {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
padding: 4px 8px;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.site-split,
|
||||
.detail-hero,
|
||||
.signup-layout {
|
||||
@@ -730,6 +769,10 @@ textarea:focus {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.term-table article {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.detail-hero img {
|
||||
height: 320px;
|
||||
}
|
||||
|
||||
@@ -630,6 +630,7 @@ textarea:focus {
|
||||
}
|
||||
|
||||
.badge.skipped,
|
||||
.badge.stopped,
|
||||
.badge.inactive {
|
||||
color: var(--danger);
|
||||
border-color: color-mix(in srgb, var(--danger), transparent 50%);
|
||||
|
||||
Reference in New Issue
Block a user