Files
medicaltracker/app/bootstrap.php
T
Ty Clifford 0d492451c7 - Implemented the symptom database as a full searchable reference area, parallel to terminology.
Added:
New medical_symptoms SQLite catalog with 242 seeded symptom records, 
categories, body systems, tracking notes, urgency-context notes, search 
terms, and MedlinePlus links: [app/bootstrap.php (line 
186)](/Users/tyemeclifford/Documents/GH/workout/app/bootstrap.php:186), 
[seed logic (line 
635)](/Users/tyemeclifford/Documents/GH/workout/app/bootstrap.php:635)
New searchable symptom directory: [public/symptoms.php (line 
1)](/Users/tyemeclifford/Documents/GH/workout/public/symptoms.php:1)
New dynamic per-symptom page, e.g. /symptom.php?id=24: 
[public/symptom.php (line 
1)](/Users/tyemeclifford/Documents/GH/workout/public/symptom.php:1)
Symptom helper/link lookup functions: [app/bootstrap.php (line 
1473)](/Users/tyemeclifford/Documents/GH/workout/app/bootstrap.php:1473)
medicalSymptoms added to the combined library search/API: 
[app/bootstrap.php (line 
2024)](/Users/tyemeclifford/Documents/GH/workout/app/bootstrap.php:2024)
Symptoms added to nav, homepage, and combined medical search: 
[site-header.php (line 
27)](/Users/tyemeclifford/Documents/GH/workout/public/partials/site-header.php:27), 
[index.php (line 
11)](/Users/tyemeclifford/Documents/GH/workout/public/index.php:11), 
[medical.php (line 
13)](/Users/tyemeclifford/Documents/GH/workout/public/medical.php:13)
2026-06-30 16:19:09 -04:00

3076 lines
175 KiB
PHP

<?php
declare(strict_types=1);
date_default_timezone_set('America/New_York');
const APP_ROOT = __DIR__ . '/..';
const DATA_DIR = APP_ROOT . '/data';
const DB_PATH = DATA_DIR . '/health_tracker.sqlite';
function app_db(): PDO
{
static $pdo = null;
if ($pdo instanceof PDO) {
return $pdo;
}
if (!is_dir(DATA_DIR)) {
mkdir(DATA_DIR, 0775, true);
}
$pdo = new PDO('sqlite:' . DB_PATH);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$pdo->exec('PRAGMA foreign_keys = ON');
$pdo->exec('PRAGMA busy_timeout = 5000');
app_init_db($pdo);
return $pdo;
}
function app_init_db(PDO $pdo): void
{
$pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT DEFAULT '',
fitness_focus TEXT DEFAULT '',
role TEXT NOT NULL DEFAULT 'member',
status TEXT NOT NULL DEFAULT 'active',
phone TEXT DEFAULT '',
birthday TEXT DEFAULT '',
height_in REAL DEFAULT 0,
training_level TEXT DEFAULT '',
emergency_contact TEXT DEFAULT '',
theme_mode TEXT NOT NULL DEFAULT 'dark',
theme_accent TEXT NOT NULL DEFAULT 'green',
updated_at TEXT DEFAULT '',
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS exercises (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
name TEXT NOT NULL,
category TEXT NOT NULL,
muscle_group TEXT DEFAULT '',
default_unit TEXT DEFAULT '',
notes TEXT DEFAULT '',
is_builtin INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS supplements (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
name TEXT NOT NULL,
timing TEXT NOT NULL,
default_dose TEXT DEFAULT '',
purpose TEXT DEFAULT '',
notes TEXT DEFAULT '',
is_builtin INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS nutrition_goals (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
label TEXT NOT NULL,
scope TEXT NOT NULL CHECK (scope IN ('daily', 'weekly', 'monthly')),
calories REAL NOT NULL DEFAULT 0,
protein_g REAL NOT NULL DEFAULT 0,
carbs_g REAL NOT NULL DEFAULT 0,
fat_g REAL NOT NULL DEFAULT 0,
water_ml REAL NOT NULL DEFAULT 0,
start_date TEXT NOT NULL,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS routines (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
nutrition_goal_id INTEGER REFERENCES nutrition_goals(id) ON DELETE SET NULL,
name TEXT NOT NULL,
workout_type TEXT NOT NULL,
planner_scope TEXT NOT NULL CHECK (planner_scope IN ('daily', 'weekly', 'monthly')),
start_date TEXT NOT NULL,
end_date TEXT DEFAULT '',
intensity TEXT DEFAULT '',
notes TEXT DEFAULT '',
active INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS workouts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
routine_id INTEGER REFERENCES routines(id) ON DELETE SET NULL,
exercise_id INTEGER REFERENCES exercises(id) ON DELETE SET NULL,
scheduled_on TEXT NOT NULL,
title TEXT NOT NULL,
workout_type TEXT NOT NULL,
sets INTEGER DEFAULT 0,
reps INTEGER DEFAULT 0,
weight_lbs REAL DEFAULT 0,
duration_min REAL DEFAULT 0,
distance_mi REAL DEFAULT 0,
intensity TEXT DEFAULT '',
status TEXT NOT NULL DEFAULT 'planned' CHECK (status IN ('planned', 'complete', 'skipped')),
notes TEXT DEFAULT '',
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS nutrition_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
logged_on TEXT NOT NULL,
calories REAL NOT NULL DEFAULT 0,
protein_g REAL NOT NULL DEFAULT 0,
carbs_g REAL NOT NULL DEFAULT 0,
fat_g REAL NOT NULL DEFAULT 0,
water_ml REAL NOT NULL DEFAULT 0,
notes TEXT DEFAULT '',
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS supplement_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
supplement_id INTEGER REFERENCES supplements(id) ON DELETE SET NULL,
taken_on TEXT NOT NULL,
timing TEXT NOT NULL,
dose TEXT DEFAULT '',
status TEXT NOT NULL DEFAULT 'planned' CHECK (status IN ('planned', 'taken', 'skipped')),
notes TEXT DEFAULT '',
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS body_metrics (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
measured_on TEXT NOT NULL,
weight_lbs REAL DEFAULT 0,
body_fat_pct REAL DEFAULT 0,
sleep_hours REAL DEFAULT 0,
resting_hr INTEGER DEFAULT 0,
mood TEXT DEFAULT '',
notes TEXT DEFAULT '',
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS medical_terms (
id INTEGER PRIMARY KEY AUTOINCREMENT,
term TEXT NOT NULL,
category TEXT NOT NULL,
plain_language_definition TEXT NOT NULL,
clinical_context TEXT DEFAULT '',
source_name TEXT NOT NULL,
source_url TEXT NOT NULL,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS medical_conditions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
category TEXT NOT NULL,
overview TEXT NOT NULL,
common_tracking TEXT DEFAULT '',
source_name TEXT NOT NULL,
source_url TEXT NOT NULL,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS medical_symptoms (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
category TEXT NOT NULL,
body_system TEXT DEFAULT '',
plain_language_description TEXT NOT NULL,
common_tracking TEXT DEFAULT '',
urgency_note TEXT DEFAULT '',
source_name TEXT NOT NULL,
source_url TEXT NOT NULL,
search_terms TEXT DEFAULT '',
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS medication_catalog (
id INTEGER PRIMARY KEY AUTOINCREMENT,
generic_name TEXT NOT NULL,
active_ingredient TEXT DEFAULT '',
chemical_name TEXT DEFAULT '',
brand_names TEXT DEFAULT '',
generic_brands TEXT DEFAULT '',
class_name TEXT NOT NULL,
drug_family TEXT DEFAULT '',
common_use TEXT NOT NULL,
tracking_note TEXT DEFAULT '',
rxnorm_url TEXT NOT NULL,
medlineplus_url TEXT DEFAULT '',
pubchem_url TEXT DEFAULT '',
dailymed_url TEXT DEFAULT '',
pubmed_url TEXT DEFAULT '',
clinicaltrials_url TEXT DEFAULT '',
source_name TEXT NOT NULL,
search_terms TEXT DEFAULT '',
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS medication_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
medication_id INTEGER REFERENCES medication_catalog(id) ON DELETE SET NULL,
taken_on TEXT NOT NULL,
medication_name TEXT NOT NULL,
dose TEXT DEFAULT '',
route TEXT DEFAULT '',
frequency TEXT DEFAULT '',
status TEXT NOT NULL DEFAULT 'planned' CHECK (status IN ('planned', 'taken', 'skipped', 'stopped')),
prescriber TEXT DEFAULT '',
notes TEXT DEFAULT '',
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS vital_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
measured_on TEXT NOT NULL,
systolic INTEGER DEFAULT 0,
diastolic INTEGER DEFAULT 0,
pulse INTEGER DEFAULT 0,
temperature_f REAL DEFAULT 0,
oxygen_pct REAL DEFAULT 0,
glucose_mg_dl REAL DEFAULT 0,
pain_score INTEGER DEFAULT 0,
symptoms TEXT DEFAULT '',
notes TEXT DEFAULT '',
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS lab_results (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
collected_on TEXT NOT NULL,
test_name TEXT NOT NULL,
result_value TEXT NOT NULL,
unit TEXT DEFAULT '',
reference_range TEXT DEFAULT '',
ordering_clinician TEXT DEFAULT '',
notes TEXT DEFAULT '',
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS symptom_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
observed_on TEXT NOT NULL,
symptom TEXT NOT NULL,
body_area TEXT DEFAULT '',
severity INTEGER DEFAULT 0,
duration TEXT DEFAULT '',
triggers TEXT DEFAULT '',
action_taken TEXT DEFAULT '',
notes TEXT DEFAULT '',
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS allergy_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
allergen TEXT NOT NULL,
reaction TEXT DEFAULT '',
severity TEXT NOT NULL DEFAULT 'unknown',
first_noted_on TEXT DEFAULT '',
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'resolved')),
notes TEXT DEFAULT '',
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS appointments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
appointment_on TEXT NOT NULL,
clinician TEXT DEFAULT '',
specialty TEXT DEFAULT '',
location TEXT DEFAULT '',
reason TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'scheduled' CHECK (status IN ('scheduled', 'completed', 'canceled')),
follow_up_notes TEXT DEFAULT '',
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS immunization_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
vaccine_name TEXT NOT NULL,
administered_on TEXT NOT NULL,
dose TEXT DEFAULT '',
lot_number TEXT DEFAULT '',
clinician_or_site TEXT DEFAULT '',
next_due_on TEXT DEFAULT '',
notes TEXT DEFAULT '',
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS research_studies (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category TEXT NOT NULL,
related_medicine TEXT DEFAULT '',
related_condition TEXT DEFAULT '',
title TEXT NOT NULL,
study_type TEXT DEFAULT '',
summary TEXT NOT NULL,
nih_source TEXT NOT NULL,
source_url TEXT NOT NULL,
pubmed_url TEXT DEFAULT '',
clinicaltrials_url TEXT DEFAULT '',
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS medication_source_updates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source TEXT NOT NULL,
status TEXT NOT NULL,
imported_count INTEGER NOT NULL DEFAULT 0,
updated_count INTEGER NOT NULL DEFAULT 0,
message TEXT DEFAULT '',
created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_workouts_user_date ON workouts(user_id, scheduled_on);
CREATE INDEX IF NOT EXISTS idx_nutrition_logs_user_date ON nutrition_logs(user_id, logged_on);
CREATE INDEX IF NOT EXISTS idx_supplement_logs_user_date ON supplement_logs(user_id, taken_on);
CREATE INDEX IF NOT EXISTS idx_body_metrics_user_date ON body_metrics(user_id, measured_on);
CREATE INDEX IF NOT EXISTS idx_medication_logs_user_date ON medication_logs(user_id, taken_on);
CREATE INDEX IF NOT EXISTS idx_vital_logs_user_date ON vital_logs(user_id, measured_on);
CREATE INDEX IF NOT EXISTS idx_lab_results_user_date ON lab_results(user_id, collected_on);
CREATE INDEX IF NOT EXISTS idx_symptom_logs_user_date ON symptom_logs(user_id, observed_on);
CREATE INDEX IF NOT EXISTS idx_allergy_records_user ON allergy_records(user_id, allergen);
CREATE INDEX IF NOT EXISTS idx_appointments_user_date ON appointments(user_id, appointment_on);
CREATE INDEX IF NOT EXISTS idx_immunization_records_user_date ON immunization_records(user_id, administered_on);
CREATE INDEX IF NOT EXISTS idx_medical_terms_term ON medical_terms(term);
CREATE INDEX IF NOT EXISTS idx_medical_symptoms_name ON medical_symptoms(name);
CREATE INDEX IF NOT EXISTS idx_medical_symptoms_category ON medical_symptoms(category);
CREATE INDEX IF NOT EXISTS idx_medication_catalog_generic ON medication_catalog(generic_name);
CREATE INDEX IF NOT EXISTS idx_medication_source_updates_created ON medication_source_updates(created_at);
SQL);
migrate_users_table($pdo);
migrate_medication_catalog_table($pdo);
seed_libraries($pdo);
seed_medical_library($pdo);
seed_demo_data($pdo);
seed_medical_demo_data($pdo);
ensure_first_user_is_admin($pdo);
}
function migrate_users_table(PDO $pdo): void
{
$columns = db_table_columns($pdo, 'users');
$definitions = [
'role' => "TEXT NOT NULL DEFAULT 'member'",
'status' => "TEXT NOT NULL DEFAULT 'active'",
'phone' => "TEXT DEFAULT ''",
'birthday' => "TEXT DEFAULT ''",
'height_in' => "REAL DEFAULT 0",
'training_level' => "TEXT DEFAULT ''",
'emergency_contact' => "TEXT DEFAULT ''",
'updated_at' => "TEXT DEFAULT ''",
];
foreach ($definitions as $column => $definition) {
if (!in_array($column, $columns, true)) {
$pdo->exec("ALTER TABLE users ADD COLUMN {$column} {$definition}");
}
}
$pdo->exec("UPDATE users SET role = 'member' WHERE role IS NULL OR role = ''");
$pdo->exec("UPDATE users SET status = 'active' WHERE status IS NULL OR status = ''");
$pdo->exec("UPDATE users SET updated_at = created_at WHERE updated_at IS NULL OR updated_at = ''");
}
function migrate_medication_catalog_table(PDO $pdo): void
{
$columns = db_table_columns($pdo, 'medication_catalog');
$definitions = [
'active_ingredient' => "TEXT DEFAULT ''",
'chemical_name' => "TEXT DEFAULT ''",
'brand_names' => "TEXT DEFAULT ''",
'generic_brands' => "TEXT DEFAULT ''",
'drug_family' => "TEXT DEFAULT ''",
'pubchem_url' => "TEXT DEFAULT ''",
'dailymed_url' => "TEXT DEFAULT ''",
'search_terms' => "TEXT DEFAULT ''",
];
foreach ($definitions as $column => $definition) {
if (!in_array($column, $columns, true)) {
$pdo->exec("ALTER TABLE medication_catalog ADD COLUMN {$column} {$definition}");
}
}
}
function db_table_columns(PDO $pdo, string $table): array
{
$rows = db_all($pdo, "PRAGMA table_info({$table})");
return array_map(static fn(array $row): string => $row['name'], $rows);
}
function ensure_first_user_is_admin(PDO $pdo): void
{
$firstUserId = (int) db_value($pdo, 'SELECT id FROM users ORDER BY id LIMIT 1');
if ($firstUserId <= 0) {
return;
}
$adminCount = (int) db_value($pdo, "SELECT COUNT(*) FROM users WHERE role = 'admin'");
if ($adminCount === 0) {
$stmt = $pdo->prepare("UPDATE users SET role = 'admin', status = 'active', updated_at = ? WHERE id = ?");
$stmt->execute([app_now(), $firstUserId]);
}
}
function seed_libraries(PDO $pdo): void
{
$exerciseCount = (int) $pdo->query("SELECT COUNT(*) FROM exercises WHERE is_builtin = 1")->fetchColumn();
if ($exerciseCount === 0) {
$exercises = [
['Barbell Back Squat', 'Weight lifting', 'Legs', 'sets x reps x lb'],
['Deadlift', 'Weight lifting', 'Posterior chain', 'sets x reps x lb'],
['Bench Press', 'Weight lifting', 'Chest', 'sets x reps x lb'],
['Overhead Press', 'Weight lifting', 'Shoulders', 'sets x reps x lb'],
['Pull-Up', 'Weight lifting', 'Back', 'sets x reps'],
['Bent-Over Row', 'Weight lifting', 'Back', 'sets x reps x lb'],
['Romanian Deadlift', 'Weight lifting', 'Hamstrings', 'sets x reps x lb'],
['Walking Lunge', 'Weight lifting', 'Legs', 'sets x reps'],
['Hip Thrust', 'Weight lifting', 'Glutes', 'sets x reps x lb'],
['Biceps Curl', 'Weight lifting', 'Arms', 'sets x reps x lb'],
['Triceps Dip', 'Weight lifting', 'Arms', 'sets x reps'],
['Core Plank', 'Core', 'Core', 'minutes'],
['Kettlebell Swing', 'Conditioning', 'Full body', 'sets x reps'],
['HIIT Circuit', 'Conditioning', 'Full body', 'minutes'],
['Indoor Cycling', 'Cycling', 'Cardio', 'minutes + miles'],
['Road Ride', 'Cycling', 'Cardio', 'minutes + miles'],
['Easy Run', 'Running', 'Cardio', 'minutes + miles'],
['Tempo Run', 'Running', 'Cardio', 'minutes + miles'],
['Sprint Intervals', 'Running', 'Cardio', 'rounds + minutes'],
['Pool Laps', 'Swimming', 'Cardio', 'minutes + distance'],
['Open-Water Swim', 'Swimming', 'Cardio', 'minutes + distance'],
['Rowing Machine', 'Conditioning', 'Cardio', 'minutes + meters'],
['Yoga Mobility', 'Mobility', 'Recovery', 'minutes'],
['Hiking', 'Outdoor', 'Cardio', 'minutes + miles'],
['Jump Rope', 'Conditioning', 'Cardio', 'minutes'],
['Elliptical', 'Conditioning', 'Cardio', 'minutes'],
];
$stmt = $pdo->prepare(
"INSERT INTO exercises (name, category, muscle_group, default_unit, notes, is_builtin, created_at)
VALUES (?, ?, ?, ?, '', 1, ?)"
);
foreach ($exercises as $exercise) {
$stmt->execute([...$exercise, app_now()]);
}
}
$supplementCount = (int) $pdo->query("SELECT COUNT(*) FROM supplements WHERE is_builtin = 1")->fetchColumn();
if ($supplementCount === 0) {
$supplements = [
['Pre-Workout Blend', 'Pre-workout', '1 scoop', 'Energy and focus'],
['Caffeine', 'Pre-workout', '100-200 mg', 'Alertness'],
['Creatine Monohydrate', 'Daily', '5 g', 'Strength and power output'],
['Beta-Alanine', 'Pre-workout', '3.2 g', 'High-intensity endurance'],
['Citrulline Malate', 'Pre-workout', '6-8 g', 'Pump and blood flow'],
['Electrolytes', 'Intra-workout', '1 serving', 'Hydration support'],
['BCAAs / EAAs', 'Intra-workout', '1 serving', 'Amino acid support'],
['Carb Powder', 'Intra-workout', '25-50 g', 'Training fuel'],
['Whey Protein', 'Post-workout', '25-35 g', 'Protein target support'],
['Casein Protein', 'Evening', '25-35 g', 'Slow-digesting protein'],
['Collagen Peptides', 'Daily', '10-20 g', 'Tendon and joint support'],
['Fish Oil / Omega-3', 'Daily', '1-2 g EPA/DHA', 'General health'],
['Magnesium Glycinate', 'Evening', '200-400 mg', 'Relaxation and sleep'],
['Vitamin D3', 'Daily', '1000-2000 IU', 'General health'],
['Multivitamin', 'Daily', '1 serving', 'Micronutrient coverage'],
['Tart Cherry', 'Post-workout', '1 serving', 'Recovery support'],
['Greens Powder', 'Daily', '1 serving', 'Micronutrient support'],
];
$stmt = $pdo->prepare(
"INSERT INTO supplements (name, timing, default_dose, purpose, notes, is_builtin, created_at)
VALUES (?, ?, ?, ?, '', 1, ?)"
);
foreach ($supplements as $supplement) {
$stmt->execute([...$supplement, app_now()]);
}
}
}
function seed_medical_library(PDO $pdo): void
{
$created = app_now();
$termCount = (int) $pdo->query('SELECT COUNT(*) FROM medical_terms')->fetchColumn();
if ($termCount === 0) {
$terms = [
['Blood pressure', 'Vitals', 'Pressure of circulating blood against artery walls, commonly recorded as systolic over diastolic.', 'Often tracked for cardiovascular risk and medication follow-up.', 'MedlinePlus', 'https://medlineplus.gov/highbloodpressure.html'],
['Hypertension', 'Condition', 'A clinical term for high blood pressure.', 'Can be tracked with home readings, clinician visits, and medication adherence.', 'MedlinePlus', 'https://medlineplus.gov/highbloodpressure.html'],
['A1C', 'Laboratory', 'A blood test that reflects average blood glucose over roughly the previous few months.', 'Commonly followed in diabetes care.', 'MedlinePlus', 'https://medlineplus.gov/a1c.html'],
['Hyperglycemia', 'Symptom/Lab', 'Higher-than-desired blood glucose.', 'May be logged with glucose readings, symptoms, meals, and medications.', 'MedlinePlus', 'https://medlineplus.gov/highbloodsugar.html'],
['LDL cholesterol', 'Laboratory', 'A cholesterol fraction often discussed as a cardiovascular risk marker.', 'Frequently reviewed with lipid panels and statin therapy.', 'MedlinePlus', 'https://medlineplus.gov/cholesterollevelswhatyouneedtoknow.html'],
['HDL cholesterol', 'Laboratory', 'A cholesterol fraction commonly included in lipid panels.', 'Used with LDL, triglycerides, and total cholesterol to understand lipid status.', 'MedlinePlus', 'https://medlineplus.gov/cholesterollevelswhatyouneedtoknow.html'],
['Oxygen saturation', 'Vitals', 'An estimate of how much oxygen hemoglobin is carrying.', 'Often recorded as SpO2 from a pulse oximeter.', 'MedlinePlus', 'https://medlineplus.gov/ency/article/003855.htm'],
['eGFR', 'Laboratory', 'Estimated glomerular filtration rate, a calculated kidney-function marker.', 'Often reviewed with kidney disease, diabetes, hypertension, and medication dosing.', 'MedlinePlus', 'https://medlineplus.gov/lab-tests/glomerular-filtration-rate-gfr-test/'],
['Adverse drug reaction', 'Medication Safety', 'An unwanted or harmful response associated with taking a medicine.', 'Useful to log with medicine, dose, timing, and symptoms.', 'MedlinePlus', 'https://medlineplus.gov/drugsafety.html'],
['Contraindication', 'Medication Safety', 'A reason a treatment may be unsuitable for a person or situation.', 'Important to discuss with a clinician or pharmacist before medication changes.', 'RxNorm/NLM', 'https://www.nlm.nih.gov/research/umls/rxnorm/index.html'],
['Randomized controlled trial', 'Research', 'A study design that assigns participants to interventions by chance.', 'Common study type in PubMed and ClinicalTrials.gov research records.', 'ClinicalTrials.gov', 'https://clinicaltrials.gov/'],
['Placebo', 'Research', 'An inactive comparison used in some clinical studies.', 'Helps separate treatment effects from expectation or background changes.', 'ClinicalTrials.gov', 'https://clinicaltrials.gov/'],
];
$stmt = $pdo->prepare(
"INSERT INTO medical_terms
(term, category, plain_language_definition, clinical_context, source_name, source_url, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)"
);
foreach ($terms as $term) {
$stmt->execute([...$term, $created]);
}
}
$additionalTerms = [
['Complete blood count (CBC)', 'Laboratory', 'A group of blood tests that count and describe red blood cells, white blood cells, platelets, hemoglobin, hematocrit, and related measures.', 'Commonly logged when tracking anemia, infection, bleeding concerns, or medication effects.', 'MedlinePlus', 'https://medlineplus.gov/lab-tests/complete-blood-count-cbc/'],
['Hemoglobin', 'Laboratory', 'An iron-rich protein in red blood cells that carries oxygen.', 'Often reviewed as part of a CBC and anemia evaluation.', 'MedlinePlus', 'https://medlineplus.gov/lab-tests/complete-blood-count-cbc/'],
['Hematocrit', 'Laboratory', 'The share of whole blood made up of red blood cells.', 'Often tracked with hemoglobin and red blood cell count in CBC results.', 'MedlinePlus', 'https://medlineplus.gov/lab-tests/complete-blood-count-cbc/'],
['White blood cell count', 'Laboratory', 'A measure of white blood cells, which are involved in infection and immune response.', 'May be followed during infections, immune conditions, and medication monitoring.', 'MedlinePlus', 'https://medlineplus.gov/lab-tests/complete-blood-count-cbc/'],
['Platelet count', 'Laboratory', 'A measure of platelets, blood components involved in clotting.', 'Useful context for bleeding risk, clotting concerns, and some medication follow-up.', 'MedlinePlus', 'https://medlineplus.gov/lab-tests/complete-blood-count-cbc/'],
['Creatinine', 'Laboratory', 'A waste product measured in blood or urine and commonly used to evaluate kidney function.', 'Often reviewed with eGFR, diabetes, hypertension, and medication dosing.', 'MedlinePlus', 'https://medlineplus.gov/lab-tests/creatinine-test/'],
['Triglycerides', 'Laboratory', 'A type of fat measured in the blood as part of cardiovascular and metabolic risk review.', 'Frequently tracked with cholesterol labs and lipid-management medicines.', 'MedlinePlus', 'https://medlineplus.gov/lab-tests/triglycerides-test/'],
['Body mass index (BMI)', 'Body Measurements', 'A calculated measure based on height and weight.', 'Can be tracked as one rough context marker, though it does not replace clinical assessment.', 'MedlinePlus', 'https://medlineplus.gov/ency/article/007196.htm'],
['INR', 'Laboratory', 'International normalized ratio, a standardized blood-clotting measure often reported with prothrombin time.', 'Important for certain anticoagulant monitoring when ordered by a clinician.', 'MedlinePlus', 'https://medlineplus.gov/lab-tests/prothrombin-time-test-and-inr-ptinr/'],
['C-reactive protein (CRP)', 'Laboratory', 'A blood marker that may rise with inflammation.', 'Can be logged with symptoms, diagnosis workups, and clinician instructions.', 'MedlinePlus', 'https://medlineplus.gov/lab-tests/c-reactive-protein-crp-test/'],
['Urinalysis', 'Laboratory', 'A urine test that can screen for substances, cells, or signs of several health conditions.', 'Commonly tracked with kidney, urinary, diabetes, or pregnancy-related care.', 'MedlinePlus', 'https://medlineplus.gov/lab-tests/urinalysis/'],
];
$termInsert = $pdo->prepare(
"INSERT INTO medical_terms
(term, category, plain_language_definition, clinical_context, source_name, source_url, created_at)
SELECT ?, ?, ?, ?, ?, ?, ?
WHERE NOT EXISTS (SELECT 1 FROM medical_terms WHERE term = ?)"
);
foreach ($additionalTerms as $term) {
$termInsert->execute([...$term, $created, $term[0]]);
}
$conditionCount = (int) $pdo->query('SELECT COUNT(*) FROM medical_conditions')->fetchColumn();
if ($conditionCount === 0) {
$conditions = [
['Hypertension', 'Cardiovascular', 'High blood pressure is a common condition tracked through repeated readings and risk-factor review.', 'Blood pressure, pulse, medications, symptoms, lifestyle notes', 'MedlinePlus', 'https://medlineplus.gov/highbloodpressure.html'],
['Type 2 Diabetes', 'Endocrine/Metabolic', 'Type 2 diabetes is commonly tracked with glucose, A1C, medications, nutrition, and activity patterns.', 'Glucose, A1C, medication adherence, symptoms, nutrition notes', 'MedlinePlus', 'https://medlineplus.gov/diabetestype2.html'],
['Asthma', 'Respiratory', 'Asthma tracking often focuses on symptoms, rescue inhaler use, triggers, and breathing measures.', 'Symptoms, medication use, peak flow if prescribed, oxygen saturation', 'MedlinePlus', 'https://medlineplus.gov/asthma.html'],
['Depression', 'Mental Health', 'Depression tracking may include mood, sleep, medication use, symptoms, and care-team notes.', 'Mood, sleep, symptoms, medication adherence, side effects', 'MedlinePlus', 'https://medlineplus.gov/depression.html'],
['High Cholesterol', 'Cardiovascular/Metabolic', 'Cholesterol management is commonly followed through lipid labs and medication response.', 'LDL, HDL, triglycerides, statin adherence, side effects', 'MedlinePlus', 'https://medlineplus.gov/cholesterol.html'],
['Pain and Inflammation', 'General Medicine', 'Pain tracking can connect severity, location, medication use, and functional impact.', 'Pain score, medication timing, symptoms, notes', 'MedlinePlus', 'https://medlineplus.gov/pain.html'],
];
$stmt = $pdo->prepare(
"INSERT INTO medical_conditions
(name, category, overview, common_tracking, source_name, source_url, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)"
);
foreach ($conditions as $condition) {
$stmt->execute([...$condition, $created]);
}
}
seed_medical_symptom_catalog($pdo, $created);
$medicineCount = (int) $pdo->query('SELECT COUNT(*) FROM medication_catalog')->fetchColumn();
if ($medicineCount === 0) {
$medicines = [
['metformin', 'Biguanide antidiabetic', 'Often used in type 2 diabetes care.', 'Track dose, meals if relevant, glucose readings, and GI symptoms.', 'https://mor.nlm.nih.gov/RxNav/search?searchBy=String&searchTerm=metformin', 'https://medlineplus.gov/druginfo/meds/a696005.html', 'https://pubmed.ncbi.nlm.nih.gov/?term=metformin+diabetes+prevention+program', 'https://clinicaltrials.gov/search?term=metformin', 'RxNorm / MedlinePlus / PubMed'],
['lisinopril', 'ACE inhibitor', 'Often used in blood pressure and cardiovascular care.', 'Track blood pressure, pulse, cough, dizziness, and lab follow-up if ordered.', 'https://mor.nlm.nih.gov/RxNav/search?searchBy=String&searchTerm=lisinopril', 'https://medlineplus.gov/druginfo/meds/a692051.html', 'https://pubmed.ncbi.nlm.nih.gov/?term=lisinopril+hypertension+trial', 'https://clinicaltrials.gov/search?term=lisinopril', 'RxNorm / MedlinePlus / PubMed'],
['atorvastatin', 'HMG-CoA reductase inhibitor statin', 'Often used for cholesterol management and cardiovascular risk reduction.', 'Track lipid labs, muscle symptoms, dose changes, and care-team notes.', 'https://mor.nlm.nih.gov/RxNav/search?searchBy=String&searchTerm=atorvastatin', 'https://medlineplus.gov/druginfo/meds/a600045.html', 'https://pubmed.ncbi.nlm.nih.gov/?term=atorvastatin+cardiovascular+trial', 'https://clinicaltrials.gov/search?term=atorvastatin', 'RxNorm / MedlinePlus / PubMed'],
['albuterol', 'Short-acting beta agonist bronchodilator', 'Often used as a rescue medicine for asthma or bronchospasm.', 'Track inhaler use, symptoms, triggers, pulse, and breathing status.', 'https://mor.nlm.nih.gov/RxNav/search?searchBy=String&searchTerm=albuterol', 'https://medlineplus.gov/druginfo/meds/a682145.html', 'https://pubmed.ncbi.nlm.nih.gov/?term=albuterol+asthma+clinical+trial', 'https://clinicaltrials.gov/search?term=albuterol', 'RxNorm / MedlinePlus / PubMed'],
['sertraline', 'Selective serotonin reuptake inhibitor', 'Often used in depression, anxiety, and related mental health care.', 'Track mood, sleep, side effects, dose changes, and follow-up notes.', 'https://mor.nlm.nih.gov/RxNav/search?searchBy=String&searchTerm=sertraline', 'https://medlineplus.gov/druginfo/meds/a697048.html', 'https://pubmed.ncbi.nlm.nih.gov/?term=sertraline+depression+trial', 'https://clinicaltrials.gov/search?term=sertraline', 'RxNorm / MedlinePlus / PubMed'],
['ibuprofen', 'Nonsteroidal anti-inflammatory drug', 'Often used for pain, fever, or inflammation.', 'Track dose, pain score, stomach symptoms, and frequency of use.', 'https://mor.nlm.nih.gov/RxNav/search?searchBy=String&searchTerm=ibuprofen', 'https://medlineplus.gov/druginfo/meds/a682159.html', 'https://pubmed.ncbi.nlm.nih.gov/?term=ibuprofen+pain+clinical+trial', 'https://clinicaltrials.gov/search?term=ibuprofen', 'RxNorm / MedlinePlus / PubMed'],
['acetaminophen', 'Analgesic and antipyretic', 'Often used for pain or fever.', 'Track total daily dose, pain score, fever, and other combination products.', 'https://mor.nlm.nih.gov/RxNav/search?searchBy=String&searchTerm=acetaminophen', 'https://medlineplus.gov/druginfo/meds/a681004.html', 'https://pubmed.ncbi.nlm.nih.gov/?term=acetaminophen+pain+clinical+trial', 'https://clinicaltrials.gov/search?term=acetaminophen', 'RxNorm / MedlinePlus / PubMed'],
['amoxicillin', 'Penicillin antibiotic', 'Often used for susceptible bacterial infections.', 'Track start/stop dates, dose schedule, allergy symptoms, and care-team instructions.', 'https://mor.nlm.nih.gov/RxNav/search?searchBy=String&searchTerm=amoxicillin', 'https://medlineplus.gov/druginfo/meds/a685001.html', 'https://pubmed.ncbi.nlm.nih.gov/?term=amoxicillin+clinical+trial', 'https://clinicaltrials.gov/search?term=amoxicillin', 'RxNorm / MedlinePlus / PubMed'],
['omeprazole', 'Proton pump inhibitor', 'Often used for acid-related gastrointestinal conditions.', 'Track symptom response, dose timing, duration, and clinician instructions.', 'https://mor.nlm.nih.gov/RxNav/search?searchBy=String&searchTerm=omeprazole', 'https://medlineplus.gov/druginfo/meds/a693050.html', 'https://pubmed.ncbi.nlm.nih.gov/?term=omeprazole+clinical+trial', 'https://clinicaltrials.gov/search?term=omeprazole', 'RxNorm / MedlinePlus / PubMed'],
];
$stmt = $pdo->prepare(
"INSERT INTO medication_catalog
(generic_name, class_name, common_use, tracking_note, rxnorm_url, medlineplus_url, pubmed_url,
clinicaltrials_url, source_name, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
foreach ($medicines as $medicine) {
$stmt->execute([...$medicine, $created]);
}
}
seed_expanded_medication_catalog($pdo);
$studyCount = (int) $pdo->query('SELECT COUNT(*) FROM research_studies')->fetchColumn();
if ($studyCount === 0) {
$studies = [
['Diabetes', 'metformin', 'Type 2 Diabetes', 'Diabetes Prevention Program and metformin research', 'NIH clinical research program', 'NIH diabetes prevention research is a major category for metformin-related prevention and metabolic-risk evidence.', 'NIDDK / PubMed / ClinicalTrials.gov', 'https://www.niddk.nih.gov/about-niddk/research-areas/diabetes/diabetes-prevention-program-dpp', 'https://pubmed.ncbi.nlm.nih.gov/?term=Diabetes+Prevention+Program+metformin', 'https://clinicaltrials.gov/search?term=metformin%20diabetes%20prevention'],
['Cardiovascular', 'lisinopril', 'Hypertension', 'Blood pressure intervention and antihypertensive treatment studies', 'NIH clinical trial category', 'NIH cardiovascular studies include blood-pressure targets, antihypertensive treatment, and outcomes research relevant to hypertension medicines.', 'NHLBI / PubMed / ClinicalTrials.gov', 'https://www.nhlbi.nih.gov/science/systolic-blood-pressure-intervention-trial-sprint-study', 'https://pubmed.ncbi.nlm.nih.gov/?term=lisinopril+hypertension+clinical+trial', 'https://clinicaltrials.gov/search?term=lisinopril%20hypertension'],
['Cardiovascular', 'atorvastatin', 'High Cholesterol', 'Statin and lipid-management outcomes research', 'Medication outcomes research', 'Statin studies evaluate lipid changes and cardiovascular outcomes; this app links the medication catalog to PubMed and ClinicalTrials.gov searches.', 'PubMed / ClinicalTrials.gov', 'https://pubmed.ncbi.nlm.nih.gov/?term=atorvastatin+cardiovascular+outcomes+trial', 'https://pubmed.ncbi.nlm.nih.gov/?term=atorvastatin+cardiovascular+outcomes+trial', 'https://clinicaltrials.gov/search?term=atorvastatin'],
['Respiratory', 'albuterol', 'Asthma', 'Asthma medication and rescue inhaler studies', 'Clinical research category', 'Asthma studies often evaluate symptom control, rescue medication use, and exacerbation outcomes.', 'NHLBI / PubMed / ClinicalTrials.gov', 'https://www.nhlbi.nih.gov/health/asthma', 'https://pubmed.ncbi.nlm.nih.gov/?term=albuterol+asthma+clinical+trial', 'https://clinicaltrials.gov/search?term=albuterol%20asthma'],
['Mental Health', 'sertraline', 'Depression', 'Depression treatment studies and medication-response research', 'NIH mental health research category', 'Depression studies evaluate symptom scores, remission, side effects, and care pathways for medicines including SSRIs.', 'NIMH / PubMed / ClinicalTrials.gov', 'https://www.nimh.nih.gov/health/topics/depression', 'https://pubmed.ncbi.nlm.nih.gov/?term=sertraline+depression+randomized+trial', 'https://clinicaltrials.gov/search?term=sertraline%20depression'],
['Pain', 'ibuprofen', 'Pain and Inflammation', 'NSAID pain and inflammation studies', 'Medication safety and outcomes category', 'Pain studies can compare pain scores, function, adverse effects, and dose schedules for NSAIDs.', 'PubMed / ClinicalTrials.gov', 'https://pubmed.ncbi.nlm.nih.gov/?term=ibuprofen+pain+randomized+trial', 'https://pubmed.ncbi.nlm.nih.gov/?term=ibuprofen+pain+randomized+trial', 'https://clinicaltrials.gov/search?term=ibuprofen%20pain'],
['Pain', 'acetaminophen', 'Pain and Fever', 'Acetaminophen pain and fever studies', 'Medication safety and outcomes category', 'Acetaminophen studies often evaluate pain relief, fever reduction, dosing, and safety monitoring.', 'PubMed / ClinicalTrials.gov', 'https://pubmed.ncbi.nlm.nih.gov/?term=acetaminophen+pain+randomized+trial', 'https://pubmed.ncbi.nlm.nih.gov/?term=acetaminophen+pain+randomized+trial', 'https://clinicaltrials.gov/search?term=acetaminophen'],
['Infectious Disease', 'amoxicillin', 'Bacterial Infection', 'Antibiotic effectiveness and safety studies', 'Medication outcomes category', 'Antibiotic studies evaluate infection outcomes, adverse reactions, resistance patterns, and dosing strategies.', 'PubMed / ClinicalTrials.gov', 'https://pubmed.ncbi.nlm.nih.gov/?term=amoxicillin+clinical+trial', 'https://pubmed.ncbi.nlm.nih.gov/?term=amoxicillin+clinical+trial', 'https://clinicaltrials.gov/search?term=amoxicillin'],
['Gastroenterology', 'omeprazole', 'Acid-related GI Conditions', 'Proton pump inhibitor studies', 'Medication outcomes category', 'PPI studies evaluate symptom control, healing outcomes, duration, and safety topics.', 'PubMed / ClinicalTrials.gov', 'https://pubmed.ncbi.nlm.nih.gov/?term=omeprazole+clinical+trial', 'https://pubmed.ncbi.nlm.nih.gov/?term=omeprazole+clinical+trial', 'https://clinicaltrials.gov/search?term=omeprazole'],
];
$stmt = $pdo->prepare(
"INSERT INTO research_studies
(category, related_medicine, related_condition, title, study_type, summary, nih_source,
source_url, pubmed_url, clinicaltrials_url, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
foreach ($studies as $study) {
$stmt->execute([...$study, $created]);
}
}
}
function seed_medical_symptom_catalog(PDO $pdo, string $created): void
{
$rows = symptom_catalog_rows();
$existing = (int) db_value($pdo, 'SELECT COUNT(*) FROM medical_symptoms');
if ($existing >= count($rows)) {
return;
}
$stmt = $pdo->prepare(
"INSERT INTO medical_symptoms
(name, category, body_system, plain_language_description, common_tracking, urgency_note,
source_name, source_url, search_terms, created_at)
SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
WHERE NOT EXISTS (SELECT 1 FROM medical_symptoms WHERE lower(name) = lower(?))"
);
$startedTransaction = !$pdo->inTransaction();
if ($startedTransaction) {
$pdo->beginTransaction();
}
try {
foreach ($rows as $row) {
$stmt->execute([...$row, $created, $row[0]]);
}
if ($startedTransaction) {
$pdo->commit();
}
} catch (Throwable $exception) {
if ($startedTransaction && $pdo->inTransaction()) {
$pdo->rollBack();
}
throw $exception;
}
}
function symptom_catalog_rows(): array
{
$groups = [
[
'General / systemic',
'Whole body',
'{symptom} is a body-wide symptom or change that can be useful to track alongside temperature, hydration, sleep, medicines, and recent exposures.',
'Record onset, duration, severity, temperature if relevant, hydration, recent illness exposure, medicines taken, and what improves or worsens it.',
['Fever', 'Chills', 'Fatigue', 'Malaise', 'General weakness', 'Unintentional weight loss', 'Unintentional weight gain', 'Night sweats', 'Excessive sweating', 'Dehydration', 'Excessive thirst', 'Loss of appetite', 'Increased appetite', 'Heat intolerance', 'Cold intolerance', 'Swelling', 'Edema', 'Swollen lymph nodes', 'Body aches'],
],
[
'Pain',
'Pain / sensory',
'{symptom} is a pain or sensory complaint that is often tracked by location, intensity, timing, triggers, and response to treatment.',
'Record pain score, location, character, onset, duration, triggers, injuries, activity limits, medicines used, and associated symptoms.',
['Pain', 'Chronic pain', 'Severe pain', 'Abdominal pain', 'Chest pain', 'Chest pressure', 'Back pain', 'Neck pain', 'Joint pain', 'Muscle pain', 'Pelvic pain', 'Flank pain', 'Ear pain', 'Eye pain', 'Sore throat', 'Headache', 'Severe headache', 'Toothache', 'Facial pain', 'Sciatica', 'Cramps', 'Tenderness', 'Burning pain', 'Numbness', 'Tingling'],
],
[
'Respiratory',
'Lungs / airways',
'{symptom} is a breathing, airway, nose, throat, or lung-related symptom that may be tracked with oxygen saturation, triggers, and medication use when relevant.',
'Record breathing effort, oxygen saturation if available, cough character, sputum, fever, triggers, inhaler or medicine use, activity tolerance, and exposure history.',
['Cough', 'Dry cough', 'Productive cough', 'Shortness of breath', 'Trouble breathing', 'Wheezing', 'Chest tightness', 'Rapid breathing', 'Shallow breathing', 'Coughing up blood', 'Noisy breathing', 'Snoring', 'Sleep apnea symptoms', 'Nasal congestion', 'Runny nose', 'Sneezing', 'Postnasal drip', 'Sinus pressure', 'Hoarseness', 'Sputum changes'],
],
[
'Cardiovascular',
'Heart / circulation',
'{symptom} is a heart, blood vessel, or circulation-related symptom that may be useful to track with pulse, blood pressure, swelling, exertion, and medication timing.',
'Record pulse, blood pressure if available, activity at onset, duration, swelling location, shortness of breath, chest symptoms, medicines, and whether symptoms occur at rest or with exertion.',
['Palpitations', 'Fainting', 'Near-fainting', 'Dizziness', 'Lightheadedness', 'Leg swelling', 'Blue lips', 'Rapid heartbeat', 'Slow heartbeat', 'Orthopnea', 'Exercise intolerance', 'Cold hands and feet', 'Clammy skin'],
],
[
'Digestive',
'Gastrointestinal',
'{symptom} is a digestive or abdominal symptom that may be tracked with meals, hydration, bowel pattern, medicines, and associated pain.',
'Record meals, hydration, bowel frequency, stool appearance, vomiting episodes, abdominal location, fever, weight changes, medicines, and possible food or infection exposures.',
['Nausea', 'Vomiting', 'Vomiting blood', 'Diarrhea', 'Constipation', 'Heartburn', 'Indigestion', 'Gas', 'Bloating', 'Belching', 'Abdominal cramps', 'Blood in stool', 'Black stools', 'Rectal bleeding', 'Difficulty swallowing', 'Loss of bowel control', 'Bowel urgency', 'Mucus in stool', 'Jaundice', 'Hiccups', 'Early fullness', 'Loss of taste for food'],
],
[
'Neurologic',
'Brain / nerves',
'{symptom} is a nervous-system symptom that may be important to track with timing, location, mental status, movement changes, and related medicines or exposures.',
'Record onset time, duration, affected body areas, speech or vision changes, balance, weakness, headache, injury, seizure-like activity, medicines, and glucose if relevant.',
['Confusion', 'Memory problems', 'Seizure', 'Tremor', 'Migraine aura', 'Vision loss', 'Sudden vision loss', 'Double vision', 'Slurred speech', 'Difficulty speaking', 'Difficulty walking', 'Balance problems', 'Facial droop', 'Weakness on one side', 'Drowsiness', 'Vertigo', 'Loss of consciousness', 'Restless legs', 'Involuntary movements', 'Poor coordination', 'Light sensitivity', 'Sound sensitivity'],
],
[
'Skin / hair / nails',
'Skin',
'{symptom} is a skin, hair, nail, or visible tissue change that can be tracked with appearance, spread, triggers, itching, pain, and exposures.',
'Record appearance, size, color, location, spread, itch or pain, fever, new medicines, foods, soaps, insect exposures, wounds, and photos if useful.',
['Rash', 'Itching', 'Hives', 'Bruises', 'Easy bruising', 'Bleeding', 'Wound', 'Blister', 'Skin redness', 'Skin warmth', 'Dry skin', 'Peeling skin', 'Skin color change', 'Cyanosis', 'Hair loss', 'Nail changes', 'Skin ulcer', 'Mole changes', 'Insect bite reaction', 'Pus or drainage', 'Cracked skin', 'Excessive hair growth'],
],
[
'Eye / ear / nose / throat',
'Head and neck',
'{symptom} is an eye, ear, nose, mouth, or throat symptom that may be tracked with pain, discharge, hearing or vision changes, and infection or allergy context.',
'Record side affected, pain, discharge, fever, vision or hearing changes, allergy triggers, recent infections, injuries, dental symptoms, and medicines used.',
['Eye redness', 'Eye discharge', 'Watery eyes', 'Blurred vision', 'Floaters', 'Ear discharge', 'Hearing loss', 'Tinnitus', 'Ear fullness', 'Nosebleed', 'Mouth sores', 'Dry mouth', 'Bad breath', 'Swollen gums', 'Gum bleeding', 'Taste changes', 'Smell changes', 'Jaw pain', 'Tongue swelling', 'Difficulty opening mouth'],
],
[
'Urinary / reproductive',
'Genitourinary',
'{symptom} is a urinary, pelvic, breast, menstrual, or reproductive symptom that may be tracked with timing, pain, bleeding, discharge, and fluid intake.',
'Record urination pattern, pain, color, bleeding, discharge, pelvic or flank pain, cycle timing, pregnancy possibility if relevant, sexual exposure concerns, fever, and medicines.',
['Painful urination', 'Frequent urination', 'Urgent urination', 'Blood in urine', 'Cloudy urine', 'Urinary incontinence', 'Urinary retention', 'Nighttime urination', 'Decreased urination', 'Vaginal bleeding', 'Vaginal discharge', 'Missed period', 'Heavy menstrual bleeding', 'Painful periods', 'Hot flashes', 'Breast pain', 'Breast lump', 'Nipple discharge', 'Testicular pain', 'Scrotal swelling', 'Erectile dysfunction', 'Pain with sex'],
],
[
'Musculoskeletal',
'Bones / joints / muscles',
'{symptom} is a muscle, joint, bone, or movement-related symptom that may be tracked with location, activity, injury, swelling, stiffness, and function.',
'Record affected area, injury or overuse, swelling, redness, stiffness, range of motion, weakness, pain score, walking changes, falls, medicines, and functional limits.',
['Joint swelling', 'Joint stiffness', 'Limited range of motion', 'Muscle weakness', 'Muscle cramps', 'Muscle twitching', 'Back stiffness', 'Gait change', 'Falls', 'Bone pain', 'Shoulder pain', 'Hip pain', 'Knee pain', 'Ankle swelling', 'Hand pain', 'Foot pain', 'Neck stiffness', 'Limping'],
],
[
'Mental health / sleep',
'Mood / sleep / behavior',
'{symptom} is a mood, behavior, thinking, or sleep-related symptom that can be tracked with timing, stressors, sleep, medicines, substance use, and safety concerns.',
'Record mood, sleep hours, triggers, stressors, medicines, substance use, appetite, energy, concentration, safety concerns, and support or care contacts.',
['Anxiety', 'Panic attack', 'Depressed mood', 'Mood swings', 'Irritability', 'Insomnia', 'Excessive sleepiness', 'Nightmares', 'Hallucinations', 'Delusions', 'Suicidal thoughts', 'Self-harm thoughts', 'Trouble concentrating', 'Behavior changes', 'Agitation', 'Social withdrawal', 'Low motivation', 'Racing thoughts'],
],
[
'Metabolic / endocrine',
'Hormones / metabolism',
'{symptom} is a metabolism, hormone, or blood-sugar-related symptom that may be tracked with food intake, glucose readings, medicines, hydration, and energy changes.',
'Record meals, hydration, glucose readings if available, medicines, sweating, shaking, urination, thirst, weight changes, energy, and timing around exercise or illness.',
['High blood sugar symptoms', 'Low blood sugar symptoms', 'Increased thirst', 'Frequent hunger', 'Excessive urination', 'Shakiness', 'Sweating episodes', 'Unintentional weight changes', 'Feeling overheated', 'Feeling cold', 'Salt craving'],
],
[
'Medication / allergy',
'Medication safety',
'{symptom} is a possible medicine, injection, allergy, or exposure-related symptom that is useful to record with timing, dose, route, and product details.',
'Record medicine or exposure name, dose, timing, route, new products, rash or swelling, breathing symptoms, dizziness, stomach symptoms, bleeding, and whether the medicine was stopped or changed.',
['Allergic reaction', 'Severe allergic reaction', 'Medication side effects', 'Injection-site reaction', 'Dizziness after medication', 'Nausea after medication', 'Rash after medication', 'Swelling after medication', 'Bleeding after medication', 'Sleepiness after medication'],
],
];
$urgentNames = array_fill_keys(array_map('strtolower', [
'Chest pain',
'Chest pressure',
'Shortness of breath',
'Trouble breathing',
'Coughing up blood',
'Blue lips',
'Fainting',
'Loss of consciousness',
'Seizure',
'Confusion',
'Facial droop',
'Weakness on one side',
'Slurred speech',
'Difficulty speaking',
'Sudden vision loss',
'Severe headache',
'Vomiting blood',
'Blood in stool',
'Black stools',
'Rectal bleeding',
'Severe allergic reaction',
'Suicidal thoughts',
'Self-harm thoughts',
]), true);
$rows = [];
foreach ($groups as [$category, $bodySystem, $descriptionTemplate, $tracking, $names]) {
foreach ($names as $name) {
$urgency = isset($urgentNames[strtolower($name)])
? 'Potential urgent symptom: seek emergency care for sudden, severe, new, or worsening symptoms, or when this appears with breathing trouble, chest symptoms, fainting, neurologic changes, severe bleeding, or safety concerns.'
: 'Track patterns and contact a clinician for persistent, worsening, unexplained, recurrent, or concerning symptoms.';
$searchTerms = strtolower($name . ' ' . $category . ' ' . $bodySystem . ' symptom sign medical tracking medlineplus');
$rows[] = [
$name,
$category,
$bodySystem,
str_replace('{symptom}', $name, $descriptionTemplate),
$tracking,
$urgency,
'MedlinePlus',
symptom_source_url($name),
$searchTerms,
];
}
}
return $rows;
}
function symptom_source_url(string $name): string
{
$map = [
'Abdominal pain' => 'https://medlineplus.gov/abdominalpain.html',
'Anxiety' => 'https://medlineplus.gov/anxiety.html',
'Back pain' => 'https://medlineplus.gov/backpain.html',
'Chest pain' => 'https://medlineplus.gov/chestpain.html',
'Constipation' => 'https://medlineplus.gov/constipation.html',
'Cough' => 'https://medlineplus.gov/cough.html',
'Dehydration' => 'https://medlineplus.gov/dehydration.html',
'Depressed mood' => 'https://medlineplus.gov/depression.html',
'Diarrhea' => 'https://medlineplus.gov/diarrhea.html',
'Dizziness' => 'https://medlineplus.gov/dizzinessandvertigo.html',
'Ear pain' => 'https://medlineplus.gov/earinfections.html',
'Fatigue' => 'https://medlineplus.gov/fatigue.html',
'Fever' => 'https://medlineplus.gov/fever.html',
'Hair loss' => 'https://medlineplus.gov/hairloss.html',
'Headache' => 'https://medlineplus.gov/headache.html',
'Heartburn' => 'https://medlineplus.gov/heartburn.html',
'Hives' => 'https://medlineplus.gov/hives.html',
'Insomnia' => 'https://medlineplus.gov/insomnia.html',
'Itching' => 'https://medlineplus.gov/itching.html',
'Jaundice' => 'https://medlineplus.gov/jaundice.html',
'Joint pain' => 'https://medlineplus.gov/jointdisorders.html',
'Nausea' => 'https://medlineplus.gov/nauseaandvomiting.html',
'Neck pain' => 'https://medlineplus.gov/neckinjuriesanddisorders.html',
'Pain' => 'https://medlineplus.gov/pain.html',
'Palpitations' => 'https://medlineplus.gov/heartdiseases.html',
'Rash' => 'https://medlineplus.gov/rashes.html',
'Shortness of breath' => 'https://medlineplus.gov/breathingproblems.html',
'Sore throat' => 'https://medlineplus.gov/sorethroat.html',
'Tinnitus' => 'https://medlineplus.gov/tinnitus.html',
'Tremor' => 'https://medlineplus.gov/tremor.html',
'Urinary incontinence' => 'https://medlineplus.gov/urinaryincontinence.html',
'Vertigo' => 'https://medlineplus.gov/dizzinessandvertigo.html',
'Vomiting' => 'https://medlineplus.gov/nauseaandvomiting.html',
'Wheezing' => 'https://medlineplus.gov/breathingproblems.html',
];
return $map[$name] ?? 'https://medlineplus.gov/search/?query=' . rawurlencode($name);
}
function medication_source_url(string $base, string $query): string
{
return $base . rawurlencode($query);
}
function seed_expanded_medication_catalog(PDO $pdo): void
{
$created = app_now();
$rows = expanded_medication_catalog_rows();
$existingCount = (int) $pdo->query('SELECT COUNT(*) FROM medication_catalog')->fetchColumn();
$needsSearchText = (int) $pdo->query("SELECT COUNT(*) FROM medication_catalog WHERE search_terms IS NULL OR search_terms = ''")->fetchColumn();
$needsChemicalNames = (int) $pdo->query("SELECT COUNT(*) FROM medication_catalog WHERE chemical_name IS NULL OR chemical_name = ''")->fetchColumn();
if ($existingCount >= count($rows) && $needsSearchText === 0 && $needsChemicalNames === 0) {
return;
}
$select = $pdo->prepare('SELECT id FROM medication_catalog WHERE lower(generic_name) = lower(?) LIMIT 1');
$insert = $pdo->prepare(
"INSERT INTO medication_catalog
(generic_name, active_ingredient, chemical_name, brand_names, generic_brands, class_name, drug_family,
common_use, tracking_note, rxnorm_url, medlineplus_url, pubchem_url, dailymed_url, pubmed_url,
clinicaltrials_url, source_name, search_terms, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
$update = $pdo->prepare(
"UPDATE medication_catalog
SET active_ingredient = ?, chemical_name = ?, brand_names = ?, generic_brands = ?,
class_name = ?, drug_family = ?, common_use = ?, tracking_note = ?, rxnorm_url = ?,
medlineplus_url = ?, pubchem_url = ?, dailymed_url = ?, pubmed_url = ?,
clinicaltrials_url = ?, source_name = ?, search_terms = ?
WHERE id = ?"
);
foreach ($rows as $row) {
[$genericName, $activeIngredient, $chemicalName, $brandNames, $genericBrands, $className, $drugFamily, $commonUse, $trackingNote, $medlinePlusUrl] = $row;
$rxnormUrl = medication_source_url('https://mor.nlm.nih.gov/RxNav/search?searchBy=String&searchTerm=', $genericName);
$pubchemUrl = medication_source_url('https://pubchem.ncbi.nlm.nih.gov/#query=', $activeIngredient ?: $genericName);
$dailymedUrl = medication_source_url('https://dailymed.nlm.nih.gov/dailymed/search.cfm?query=', $genericName);
$pubmedUrl = medication_source_url('https://pubmed.ncbi.nlm.nih.gov/?term=', $genericName . ' medication');
$clinicalTrialsUrl = medication_source_url('https://clinicaltrials.gov/search?term=', $genericName);
$sourceName = 'RxNorm / DailyMed / PubChem / MedlinePlus';
$searchTerms = implode(' ', [
$genericName,
$activeIngredient,
$chemicalName,
$brandNames,
$genericBrands,
$className,
$drugFamily,
$commonUse,
]);
$select->execute([$genericName]);
$existingId = (int) $select->fetchColumn();
if ($existingId > 0) {
$update->execute([
$activeIngredient,
$chemicalName,
$brandNames,
$genericBrands,
$className,
$drugFamily,
$commonUse,
$trackingNote,
$rxnormUrl,
$medlinePlusUrl,
$pubchemUrl,
$dailymedUrl,
$pubmedUrl,
$clinicalTrialsUrl,
$sourceName,
$searchTerms,
$existingId,
]);
continue;
}
$insert->execute([
$genericName,
$activeIngredient,
$chemicalName,
$brandNames,
$genericBrands,
$className,
$drugFamily,
$commonUse,
$trackingNote,
$rxnormUrl,
$medlinePlusUrl,
$pubchemUrl,
$dailymedUrl,
$pubmedUrl,
$clinicalTrialsUrl,
$sourceName,
$searchTerms,
$created,
]);
}
}
function expanded_medication_catalog_rows(): array
{
$defaultMedline = 'https://medlineplus.gov/druginformation.html';
return [
['acetaminophen', 'acetaminophen', 'N-(4-hydroxyphenyl)acetamide', 'Tylenol, Mapap, Panadol', 'paracetamol, APAP', 'Analgesic and antipyretic', 'Pain and fever', 'Often used for pain or fever.', 'Track total daily dose and other combination products that may also contain acetaminophen.', 'https://medlineplus.gov/druginfo/meds/a681004.html'],
['ibuprofen', 'ibuprofen', '2-(4-isobutylphenyl)propanoic acid', 'Advil, Motrin, Midol', 'ibuprofen tablets, ibuprofen suspension', 'Nonsteroidal anti-inflammatory drug', 'Pain and inflammation', 'Often used for pain, fever, or inflammation.', 'Track dose, pain score, stomach symptoms, and frequency of use.', 'https://medlineplus.gov/druginfo/meds/a682159.html'],
['naproxen', 'naproxen', '(S)-2-(6-methoxynaphthalen-2-yl)propanoic acid', 'Aleve, Naprosyn, Anaprox', 'naproxen sodium, naproxen tablets', 'Nonsteroidal anti-inflammatory drug', 'Pain and inflammation', 'Often used for pain, fever, or inflammation.', 'Track dose timing, stomach symptoms, kidney-risk context, and pain response.', $defaultMedline],
['aspirin', 'acetylsalicylic acid', '2-acetyloxybenzoic acid', 'Bayer, Bufferin, Ecotrin', 'acetylsalicylic acid, ASA', 'Salicylate antiplatelet and analgesic', 'Pain, fever, antiplatelet therapy', 'Used for pain, fever, inflammation, or antiplatelet therapy when directed.', 'Track indication, dose, bleeding symptoms, and clinician instructions.', $defaultMedline],
['diclofenac', 'diclofenac', '2-[2-(2,6-dichloroanilino)phenyl]acetic acid', 'Voltaren, Cataflam, Zipsor', 'diclofenac sodium, diclofenac potassium', 'Nonsteroidal anti-inflammatory drug', 'Pain and inflammation', 'Often used for arthritis or musculoskeletal pain.', 'Track pain score, application site for topical use, stomach symptoms, and dose schedule.', $defaultMedline],
['celecoxib', 'celecoxib', '4-[5-(4-methylphenyl)-3-(trifluoromethyl)pyrazol-1-yl]benzenesulfonamide', 'Celebrex, Elyxyb', 'celecoxib capsules', 'COX-2 selective nonsteroidal anti-inflammatory drug', 'Pain and inflammation', 'Often used for arthritis pain or inflammatory conditions.', 'Track pain relief, stomach symptoms, swelling, blood pressure, and clinician instructions.', $defaultMedline],
['meloxicam', 'meloxicam', 'meloxicam', 'Mobic, Vivlodex, Anjeso', 'meloxicam tablets, meloxicam capsules', 'Nonsteroidal anti-inflammatory drug', 'Pain and inflammation', 'Often used for arthritis and inflammatory pain.', 'Track pain response, stomach symptoms, and blood pressure or kidney follow-up if ordered.', $defaultMedline],
['tramadol', 'tramadol', 'tramadol hydrochloride', 'Ultram, ConZip, Qdolo', 'tramadol tablets, tramadol ER', 'Opioid analgesic', 'Pain', 'Used for selected pain conditions when prescribed.', 'Track dose, sedation, constipation, dizziness, pain score, and stop/start dates.', $defaultMedline],
['hydrocodone acetaminophen', 'hydrocodone bitartrate and acetaminophen', 'hydrocodone plus acetaminophen', 'Norco, Vicodin, Lortab', 'hydrocodone/APAP', 'Opioid analgesic combination', 'Pain', 'Combination pain medicine containing an opioid and acetaminophen.', 'Track total acetaminophen exposure, sedation, constipation, and prescriber instructions.', $defaultMedline],
['oxycodone', 'oxycodone', 'oxycodone hydrochloride', 'OxyContin, Roxicodone, Oxaydo', 'oxycodone IR, oxycodone ER', 'Opioid analgesic', 'Pain', 'Used for selected pain conditions when prescribed.', 'Track dose, sedation, constipation, pain score, and safety instructions.', $defaultMedline],
['morphine', 'morphine', 'morphine sulfate', 'MS Contin, Kadian, Roxanol', 'morphine IR, morphine ER', 'Opioid analgesic', 'Pain', 'Used for selected pain conditions when prescribed.', 'Track dose, sedation, constipation, respiratory symptoms, and pain score.', $defaultMedline],
['gabapentin', 'gabapentin', '1-(aminomethyl)cyclohexaneacetic acid', 'Neurontin, Gralise, Horizant', 'gabapentin capsules, gabapentin tablets', 'Anticonvulsant nerve-pain medicine', 'Neurology and pain', 'Often used for seizures or nerve-related pain.', 'Track sleepiness, dizziness, dose changes, and symptom response.', $defaultMedline],
['pregabalin', 'pregabalin', '(S)-3-(aminomethyl)-5-methylhexanoic acid', 'Lyrica', 'pregabalin capsules', 'Anticonvulsant nerve-pain medicine', 'Neurology and pain', 'Often used for nerve pain, fibromyalgia, or seizures.', 'Track dizziness, swelling, sleepiness, and symptom response.', $defaultMedline],
['cyclobenzaprine', 'cyclobenzaprine', 'cyclobenzaprine hydrochloride', 'Flexeril, Amrix, Fexmid', 'cyclobenzaprine tablets', 'Skeletal muscle relaxant', 'Musculoskeletal', 'Often used short term for muscle spasm.', 'Track sedation, dry mouth, dose timing, and symptom response.', $defaultMedline],
['amoxicillin', 'amoxicillin', '(2S,5R,6R)-6-[[(2R)-2-amino-2-(4-hydroxyphenyl)acetyl]amino]-3,3-dimethyl-7-oxo-4-thia-1-azabicyclo[3.2.0]heptane-2-carboxylic acid', 'Amoxil, Moxatag, Trimox', 'amoxicillin capsules, amoxicillin suspension', 'Penicillin antibiotic', 'Infectious disease', 'Often used for susceptible bacterial infections.', 'Track start/stop dates, dose schedule, allergy symptoms, and care-team instructions.', 'https://medlineplus.gov/druginfo/meds/a685001.html'],
['amoxicillin clavulanate', 'amoxicillin and clavulanate potassium', 'amoxicillin plus clavulanic acid', 'Augmentin', 'amoxicillin/clavulanate, co-amoxiclav', 'Penicillin antibiotic with beta-lactamase inhibitor', 'Infectious disease', 'Used for selected bacterial infections when prescribed.', 'Track dose schedule, GI symptoms, allergy symptoms, and completion date.', $defaultMedline],
['azithromycin', 'azithromycin', 'azithromycin', 'Zithromax, Z-Pak, Zmax', 'azithromycin tablets, azithromycin suspension', 'Macrolide antibiotic', 'Infectious disease', 'Used for selected bacterial infections when prescribed.', 'Track start/stop dates, dose schedule, diarrhea, rash, and symptom response.', $defaultMedline],
['doxycycline', 'doxycycline', 'doxycycline hyclate or monohydrate', 'Vibramycin, Doryx, Oracea', 'doxycycline tablets, doxycycline capsules', 'Tetracycline antibiotic', 'Infectious disease', 'Used for selected bacterial infections and some inflammatory skin conditions.', 'Track sun sensitivity, stomach symptoms, dose timing, and completion date.', $defaultMedline],
['cephalexin', 'cephalexin', 'cefalexin', 'Keflex', 'cephalexin capsules, cephalexin suspension', 'Cephalosporin antibiotic', 'Infectious disease', 'Used for selected bacterial infections when prescribed.', 'Track start/stop dates, allergy symptoms, GI symptoms, and symptom response.', $defaultMedline],
['ciprofloxacin', 'ciprofloxacin', 'ciprofloxacin hydrochloride', 'Cipro, Cipro XR', 'ciprofloxacin tablets, ciprofloxacin drops', 'Fluoroquinolone antibiotic', 'Infectious disease', 'Used for selected bacterial infections when prescribed.', 'Track tendon pain, nerve symptoms, GI symptoms, and clinician instructions.', $defaultMedline],
['levofloxacin', 'levofloxacin', 'levofloxacin', 'Levaquin', 'levofloxacin tablets', 'Fluoroquinolone antibiotic', 'Infectious disease', 'Used for selected bacterial infections when prescribed.', 'Track tendon pain, nerve symptoms, mood changes, and completion date.', $defaultMedline],
['clindamycin', 'clindamycin', 'clindamycin hydrochloride', 'Cleocin, Clindesse', 'clindamycin capsules, clindamycin topical', 'Lincosamide antibiotic', 'Infectious disease', 'Used for selected bacterial infections or topical acne treatment.', 'Track diarrhea, rash, dose timing, and symptom response.', $defaultMedline],
['metronidazole', 'metronidazole', '2-(2-methyl-5-nitroimidazol-1-yl)ethanol', 'Flagyl, Metrogel, Vandazole', 'metronidazole tablets, metronidazole gel', 'Nitroimidazole antimicrobial', 'Infectious disease', 'Used for selected bacterial or protozoal infections when prescribed.', 'Track dose schedule, GI symptoms, metallic taste, and alcohol-related instructions.', $defaultMedline],
['trimethoprim sulfamethoxazole', 'sulfamethoxazole and trimethoprim', 'trimethoprim plus sulfamethoxazole', 'Bactrim, Septra', 'TMP-SMX, co-trimoxazole', 'Sulfonamide antibiotic combination', 'Infectious disease', 'Used for selected bacterial infections when prescribed.', 'Track rash, sun sensitivity, GI symptoms, dose schedule, and completion date.', $defaultMedline],
['nitrofurantoin', 'nitrofurantoin', 'nitrofurantoin', 'Macrobid, Macrodantin, Furadantin', 'nitrofurantoin capsules, nitrofurantoin suspension', 'Nitrofuran antibiotic', 'Infectious disease', 'Often used for selected urinary tract infections.', 'Track urinary symptoms, dose schedule, GI symptoms, and completion date.', $defaultMedline],
['fluconazole', 'fluconazole', 'fluconazole', 'Diflucan', 'fluconazole tablets', 'Azole antifungal', 'Infectious disease', 'Used for selected fungal infections when prescribed.', 'Track symptom response, rash, stomach symptoms, and liver-lab follow-up if ordered.', $defaultMedline],
['acyclovir', 'acyclovir', 'acyclovir', 'Zovirax', 'acyclovir tablets, acyclovir cream', 'Antiviral nucleoside analog', 'Infectious disease', 'Used for herpes-virus infections when prescribed.', 'Track outbreak timing, dose schedule, kidney-risk context, and symptom response.', $defaultMedline],
['valacyclovir', 'valacyclovir', 'valacyclovir hydrochloride', 'Valtrex', 'valacyclovir tablets', 'Antiviral prodrug', 'Infectious disease', 'Used for herpes-virus infections when prescribed.', 'Track outbreak timing, dose schedule, symptom response, and kidney-risk context.', $defaultMedline],
['lisinopril', 'lisinopril', '(2S)-1-[(2S)-6-amino-2-[[(1S)-1-carboxy-3-phenylpropyl]amino]hexanoyl]pyrrolidine-2-carboxylic acid', 'Prinivil, Zestril, Qbrelis', 'lisinopril tablets, lisinopril solution', 'ACE inhibitor', 'Cardiovascular', 'Often used in blood pressure and cardiovascular care.', 'Track blood pressure, pulse, cough, dizziness, and lab follow-up if ordered.', 'https://medlineplus.gov/druginfo/meds/a692051.html'],
['losartan', 'losartan', 'losartan potassium', 'Cozaar', 'losartan tablets', 'Angiotensin II receptor blocker', 'Cardiovascular', 'Often used for blood pressure, kidney protection, or cardiovascular care.', 'Track blood pressure, dizziness, potassium/kidney labs if ordered, and dose changes.', $defaultMedline],
['valsartan', 'valsartan', 'valsartan', 'Diovan', 'valsartan tablets', 'Angiotensin II receptor blocker', 'Cardiovascular', 'Often used for blood pressure or heart failure care.', 'Track blood pressure, dizziness, kidney/potassium labs if ordered, and dose changes.', $defaultMedline],
['amlodipine', 'amlodipine', 'amlodipine besylate', 'Norvasc, Katerzia', 'amlodipine tablets, amlodipine solution', 'Calcium channel blocker', 'Cardiovascular', 'Often used for blood pressure or angina care.', 'Track blood pressure, swelling, dizziness, and dose changes.', $defaultMedline],
['metoprolol', 'metoprolol', 'metoprolol tartrate or succinate', 'Lopressor, Toprol XL, Kapspargo Sprinkle', 'metoprolol tartrate, metoprolol succinate ER', 'Beta blocker', 'Cardiovascular', 'Often used for blood pressure, heart rate, angina, or heart failure care.', 'Track pulse, blood pressure, dizziness, fatigue, and formulation.', $defaultMedline],
['carvedilol', 'carvedilol', 'carvedilol', 'Coreg, Coreg CR', 'carvedilol tablets, carvedilol ER', 'Alpha/beta blocker', 'Cardiovascular', 'Often used for heart failure or blood pressure care.', 'Track pulse, blood pressure, dizziness, weight changes, and dose titration.', $defaultMedline],
['atenolol', 'atenolol', 'atenolol', 'Tenormin', 'atenolol tablets', 'Beta blocker', 'Cardiovascular', 'Often used for blood pressure, heart rate, or angina care.', 'Track pulse, blood pressure, dizziness, fatigue, and dose changes.', $defaultMedline],
['hydrochlorothiazide', 'hydrochlorothiazide', '6-chloro-3,4-dihydro-2H-1,2,4-benzothiadiazine-7-sulfonamide 1,1-dioxide', 'Microzide, HydroDIURIL', 'HCTZ, hydrochlorothiazide tablets', 'Thiazide diuretic', 'Cardiovascular and renal', 'Often used for blood pressure or fluid management.', 'Track blood pressure, urination, dizziness, electrolytes if ordered, and weight trends.', $defaultMedline],
['chlorthalidone', 'chlorthalidone', 'chlorthalidone', 'Thalitone, Hygroton', 'chlorthalidone tablets', 'Thiazide-like diuretic', 'Cardiovascular and renal', 'Often used for blood pressure or fluid management.', 'Track blood pressure, dizziness, electrolytes if ordered, and urination changes.', $defaultMedline],
['furosemide', 'furosemide', '4-chloro-N-furfuryl-5-sulfamoylanthranilic acid', 'Lasix', 'furosemide tablets, furosemide solution', 'Loop diuretic', 'Cardiovascular and renal', 'Often used for fluid management when prescribed.', 'Track weight, swelling, urine output, dizziness, and electrolytes if ordered.', $defaultMedline],
['spironolactone', 'spironolactone', 'spironolactone', 'Aldactone, CaroSpir', 'spironolactone tablets, spironolactone suspension', 'Potassium-sparing diuretic and aldosterone antagonist', 'Cardiovascular and endocrine', 'Often used for heart failure, blood pressure, fluid, or hormonal indications.', 'Track blood pressure, potassium labs if ordered, dizziness, and dose changes.', $defaultMedline],
['clopidogrel', 'clopidogrel', 'clopidogrel bisulfate', 'Plavix', 'clopidogrel tablets', 'P2Y12 antiplatelet', 'Cardiovascular', 'Used to reduce clotting risk when prescribed.', 'Track bruising, bleeding symptoms, procedures, and prescriber instructions.', $defaultMedline],
['warfarin', 'warfarin', 'warfarin sodium', 'Coumadin, Jantoven', 'warfarin tablets', 'Vitamin K antagonist anticoagulant', 'Cardiovascular and hematology', 'Used for anticoagulation when prescribed.', 'Track INR, dose schedule, bleeding symptoms, diet changes, and interactions.', $defaultMedline],
['apixaban', 'apixaban', 'apixaban', 'Eliquis', 'apixaban tablets', 'Direct factor Xa inhibitor anticoagulant', 'Cardiovascular and hematology', 'Used for anticoagulation when prescribed.', 'Track dose adherence, bleeding symptoms, procedures, kidney follow-up, and refills.', $defaultMedline],
['rivaroxaban', 'rivaroxaban', 'rivaroxaban', 'Xarelto', 'rivaroxaban tablets', 'Direct factor Xa inhibitor anticoagulant', 'Cardiovascular and hematology', 'Used for anticoagulation when prescribed.', 'Track dose timing with food if directed, bleeding symptoms, procedures, and refills.', $defaultMedline],
['nitroglycerin', 'nitroglycerin', 'glyceryl trinitrate', 'Nitrostat, Nitro-Dur, NitroMist', 'nitroglycerin sublingual, nitroglycerin patch', 'Nitrate vasodilator', 'Cardiovascular', 'Used for angina or cardiovascular indications when prescribed.', 'Track symptom timing, dose use, blood pressure symptoms, and emergency instructions.', $defaultMedline],
['atorvastatin', 'atorvastatin', 'atorvastatin calcium', 'Lipitor', 'atorvastatin tablets', 'HMG-CoA reductase inhibitor statin', 'Cardiovascular and metabolic', 'Often used for cholesterol management and cardiovascular risk reduction.', 'Track lipid labs, muscle symptoms, dose changes, and care-team notes.', 'https://medlineplus.gov/druginfo/meds/a600045.html'],
['rosuvastatin', 'rosuvastatin', 'rosuvastatin calcium', 'Crestor, Ezallor Sprinkle', 'rosuvastatin tablets', 'HMG-CoA reductase inhibitor statin', 'Cardiovascular and metabolic', 'Often used for cholesterol management and cardiovascular risk reduction.', 'Track lipid labs, muscle symptoms, dose changes, and care-team notes.', $defaultMedline],
['simvastatin', 'simvastatin', 'simvastatin', 'Zocor, FloLipid', 'simvastatin tablets', 'HMG-CoA reductase inhibitor statin', 'Cardiovascular and metabolic', 'Often used for cholesterol management.', 'Track lipid labs, muscle symptoms, dose changes, and medication interactions.', $defaultMedline],
['ezetimibe', 'ezetimibe', 'ezetimibe', 'Zetia', 'ezetimibe tablets', 'Cholesterol absorption inhibitor', 'Cardiovascular and metabolic', 'Often used for cholesterol management.', 'Track lipid labs, GI symptoms, muscle symptoms, and combination therapy.', $defaultMedline],
['metformin', 'metformin', 'N,N-dimethylbiguanide', 'Glucophage, Glumetza, Fortamet, Riomet', 'metformin tablets, metformin ER', 'Biguanide antidiabetic', 'Endocrine and metabolic', 'Often used in type 2 diabetes care.', 'Track dose, meals if relevant, glucose readings, and GI symptoms.', 'https://medlineplus.gov/druginfo/meds/a696005.html'],
['insulin glargine', 'insulin glargine', 'recombinant human insulin analog insulin glargine', 'Lantus, Basaglar, Toujeo, Semglee', 'insulin glargine-yfgn, insulin glargine pens', 'Long-acting insulin', 'Endocrine and metabolic', 'Used for diabetes care when prescribed.', 'Track dose, glucose readings, hypoglycemia symptoms, injection site, and timing.', $defaultMedline],
['insulin lispro', 'insulin lispro', 'recombinant human insulin analog insulin lispro', 'Humalog, Admelog, Lyumjev', 'insulin lispro-aabc, insulin lispro pens', 'Rapid-acting insulin', 'Endocrine and metabolic', 'Used for diabetes meal or correction dosing when prescribed.', 'Track glucose readings, meals, dose timing, hypoglycemia symptoms, and injection site.', $defaultMedline],
['semaglutide', 'semaglutide', 'glucagon-like peptide-1 receptor agonist peptide analog semaglutide', 'Ozempic, Wegovy, Rybelsus', 'semaglutide injection, semaglutide tablets', 'GLP-1 receptor agonist', 'Endocrine and metabolic', 'Used for diabetes or weight-management indications when prescribed.', 'Track dose titration, GI symptoms, weight, glucose, and prescriber instructions.', $defaultMedline],
['dulaglutide', 'dulaglutide', 'glucagon-like peptide-1 receptor agonist dulaglutide', 'Trulicity', 'dulaglutide injection', 'GLP-1 receptor agonist', 'Endocrine and metabolic', 'Used for type 2 diabetes care when prescribed.', 'Track weekly dose, GI symptoms, glucose readings, and injection site.', $defaultMedline],
['liraglutide', 'liraglutide', 'glucagon-like peptide-1 receptor agonist liraglutide', 'Victoza, Saxenda', 'liraglutide injection', 'GLP-1 receptor agonist', 'Endocrine and metabolic', 'Used for diabetes or weight-management indications when prescribed.', 'Track dose titration, GI symptoms, weight, glucose, and injection site.', $defaultMedline],
['empagliflozin', 'empagliflozin', 'empagliflozin', 'Jardiance', 'empagliflozin tablets', 'SGLT2 inhibitor', 'Endocrine and metabolic', 'Used for diabetes and selected heart/kidney indications when prescribed.', 'Track glucose, urinary symptoms, hydration, weight, and kidney labs if ordered.', $defaultMedline],
['dapagliflozin', 'dapagliflozin', 'dapagliflozin propanediol', 'Farxiga', 'dapagliflozin tablets', 'SGLT2 inhibitor', 'Endocrine and metabolic', 'Used for diabetes and selected heart/kidney indications when prescribed.', 'Track glucose, urinary symptoms, hydration, weight, and kidney labs if ordered.', $defaultMedline],
['sitagliptin', 'sitagliptin', 'sitagliptin phosphate', 'Januvia', 'sitagliptin tablets', 'DPP-4 inhibitor', 'Endocrine and metabolic', 'Used for type 2 diabetes care when prescribed.', 'Track glucose readings, GI symptoms, dose changes, and kidney follow-up if ordered.', $defaultMedline],
['glipizide', 'glipizide', 'glipizide', 'Glucotrol, Glucotrol XL', 'glipizide tablets, glipizide ER', 'Sulfonylurea antidiabetic', 'Endocrine and metabolic', 'Used for type 2 diabetes care when prescribed.', 'Track glucose readings, hypoglycemia symptoms, meals, and dose timing.', $defaultMedline],
['glyburide', 'glyburide', 'glyburide', 'Diabeta, Glynase', 'glyburide tablets', 'Sulfonylurea antidiabetic', 'Endocrine and metabolic', 'Used for type 2 diabetes care when prescribed.', 'Track glucose readings, hypoglycemia symptoms, meals, and dose timing.', $defaultMedline],
['pioglitazone', 'pioglitazone', 'pioglitazone hydrochloride', 'Actos', 'pioglitazone tablets', 'Thiazolidinedione antidiabetic', 'Endocrine and metabolic', 'Used for type 2 diabetes care when prescribed.', 'Track glucose, swelling, weight, shortness of breath, and liver follow-up if ordered.', $defaultMedline],
['albuterol', 'albuterol', '4-[2-(tert-butylamino)-1-hydroxyethyl]-2-(hydroxymethyl)phenol', 'Ventolin, ProAir, Proventil', 'salbutamol, albuterol inhaler', 'Short-acting beta agonist bronchodilator', 'Respiratory', 'Often used as a rescue medicine for asthma or bronchospasm.', 'Track inhaler use, symptoms, triggers, pulse, and breathing status.', 'https://medlineplus.gov/druginfo/meds/a682145.html'],
['fluticasone', 'fluticasone', 'fluticasone propionate or furoate', 'Flonase, Flovent, Arnuity, Flonase Sensimist', 'fluticasone nasal spray, fluticasone inhaler', 'Corticosteroid', 'Respiratory and allergy', 'Used for allergic rhinitis, asthma, or inflammation depending on formulation.', 'Track formulation, symptoms, throat irritation, nosebleeds, and controller adherence.', $defaultMedline],
['budesonide formoterol', 'budesonide and formoterol fumarate', 'budesonide plus formoterol', 'Symbicort, Breyna', 'budesonide/formoterol inhaler', 'Inhaled corticosteroid and long-acting beta agonist', 'Respiratory', 'Used for asthma or COPD controller therapy when prescribed.', 'Track doses, rescue inhaler use, symptoms, oral rinsing, and exacerbations.', $defaultMedline],
['fluticasone salmeterol', 'fluticasone propionate and salmeterol', 'fluticasone plus salmeterol', 'Advair, Wixela Inhub, AirDuo', 'fluticasone/salmeterol inhaler', 'Inhaled corticosteroid and long-acting beta agonist', 'Respiratory', 'Used for asthma or COPD controller therapy when prescribed.', 'Track doses, rescue inhaler use, symptoms, oral rinsing, and exacerbations.', $defaultMedline],
['montelukast', 'montelukast', 'montelukast sodium', 'Singulair', 'montelukast tablets, montelukast chewables', 'Leukotriene receptor antagonist', 'Respiratory and allergy', 'Used for asthma or allergy indications when prescribed.', 'Track symptoms, sleep/mood changes, rescue inhaler use, and triggers.', $defaultMedline],
['cetirizine', 'cetirizine', 'cetirizine hydrochloride', 'Zyrtec, Aller-Tec', 'cetirizine tablets, cetirizine solution', 'Second-generation antihistamine', 'Allergy', 'Used for allergy symptoms or hives.', 'Track symptom response, drowsiness, and dose timing.', $defaultMedline],
['loratadine', 'loratadine', 'loratadine', 'Claritin, Alavert', 'loratadine tablets, loratadine syrup', 'Second-generation antihistamine', 'Allergy', 'Used for allergy symptoms.', 'Track symptom response and dose timing.', $defaultMedline],
['fexofenadine', 'fexofenadine', 'fexofenadine hydrochloride', 'Allegra', 'fexofenadine tablets, fexofenadine suspension', 'Second-generation antihistamine', 'Allergy', 'Used for allergy symptoms or hives.', 'Track symptom response and dose timing.', $defaultMedline],
['diphenhydramine', 'diphenhydramine', 'diphenhydramine hydrochloride', 'Benadryl, ZzzQuil', 'diphenhydramine tablets, diphenhydramine liquid', 'First-generation antihistamine', 'Allergy and sleep', 'Used for allergy symptoms, itching, or sleep aid products.', 'Track drowsiness, confusion, dry mouth, dose timing, and reason for use.', $defaultMedline],
['pseudoephedrine', 'pseudoephedrine', 'pseudoephedrine hydrochloride', 'Sudafed', 'pseudoephedrine tablets', 'Nasal decongestant', 'Respiratory and allergy', 'Used for nasal congestion.', 'Track blood pressure, heart rate symptoms, sleep effects, and dose timing.', $defaultMedline],
['epinephrine', 'epinephrine', 'adrenaline', 'EpiPen, Auvi-Q, Adrenaclick', 'epinephrine auto-injector', 'Adrenergic agonist', 'Emergency allergy and cardiovascular', 'Used for severe allergic reactions or emergency indications when prescribed.', 'Track expiration dates, use events, emergency follow-up, and refill status.', $defaultMedline],
['omeprazole', 'omeprazole', '5-methoxy-2-[(4-methoxy-3,5-dimethylpyridin-2-yl)methylsulfinyl]-1H-benzimidazole', 'Prilosec, Zegerid', 'omeprazole capsules, omeprazole tablets', 'Proton pump inhibitor', 'Gastroenterology', 'Often used for acid-related gastrointestinal conditions.', 'Track symptom response, dose timing, duration, and clinician instructions.', 'https://medlineplus.gov/druginfo/meds/a693050.html'],
['pantoprazole', 'pantoprazole', 'pantoprazole sodium', 'Protonix', 'pantoprazole tablets', 'Proton pump inhibitor', 'Gastroenterology', 'Often used for acid-related gastrointestinal conditions.', 'Track symptom response, dose timing, duration, and clinician instructions.', $defaultMedline],
['famotidine', 'famotidine', 'famotidine', 'Pepcid', 'famotidine tablets, famotidine suspension', 'Histamine H2 receptor antagonist', 'Gastroenterology', 'Often used for acid-related symptoms.', 'Track symptom response, dose timing, kidney-dose context if relevant, and duration.', $defaultMedline],
['ondansetron', 'ondansetron', 'ondansetron hydrochloride', 'Zofran, Zuplenz', 'ondansetron tablets, ondansetron ODT', '5-HT3 receptor antagonist antiemetic', 'Gastroenterology', 'Used for nausea or vomiting when prescribed.', 'Track nausea score, constipation, headache, dose timing, and hydration.', $defaultMedline],
['metoclopramide', 'metoclopramide', 'metoclopramide hydrochloride', 'Reglan, Gimoti', 'metoclopramide tablets, metoclopramide nasal spray', 'Dopamine antagonist prokinetic antiemetic', 'Gastroenterology', 'Used for selected nausea or motility conditions when prescribed.', 'Track movement symptoms, drowsiness, mood changes, and duration.', $defaultMedline],
['polyethylene glycol 3350', 'polyethylene glycol 3350', 'polyethylene glycol 3350', 'MiraLAX, GlycoLax', 'PEG 3350 powder', 'Osmotic laxative', 'Gastroenterology', 'Used for constipation.', 'Track bowel movements, hydration, bloating, and dose frequency.', $defaultMedline],
['loperamide', 'loperamide', 'loperamide hydrochloride', 'Imodium, Diamode', 'loperamide capsules, loperamide liquid', 'Antidiarrheal opioid receptor agonist', 'Gastroenterology', 'Used for diarrhea symptoms when appropriate.', 'Track stool frequency, fever/blood symptoms, dose count, and hydration.', $defaultMedline],
['dicyclomine', 'dicyclomine', 'dicyclomine hydrochloride', 'Bentyl', 'dicyclomine tablets, dicyclomine capsules', 'Anticholinergic antispasmodic', 'Gastroenterology', 'Used for intestinal spasm symptoms when prescribed.', 'Track abdominal pain, dry mouth, dizziness, constipation, and dose timing.', $defaultMedline],
['sertraline', 'sertraline', '(1S,4S)-4-(3,4-dichlorophenyl)-N-methyl-1,2,3,4-tetrahydronaphthalen-1-amine', 'Zoloft', 'sertraline tablets, sertraline solution', 'Selective serotonin reuptake inhibitor', 'Mental health', 'Often used in depression, anxiety, and related mental health care.', 'Track mood, sleep, side effects, dose changes, and follow-up notes.', 'https://medlineplus.gov/druginfo/meds/a697048.html'],
['fluoxetine', 'fluoxetine', 'fluoxetine hydrochloride', 'Prozac, Sarafem, Rapiflux', 'fluoxetine capsules, fluoxetine solution', 'Selective serotonin reuptake inhibitor', 'Mental health', 'Often used in depression, anxiety, or related mental health care.', 'Track mood, sleep, GI symptoms, activation, and dose changes.', $defaultMedline],
['escitalopram', 'escitalopram', 'escitalopram oxalate', 'Lexapro', 'escitalopram tablets, escitalopram solution', 'Selective serotonin reuptake inhibitor', 'Mental health', 'Often used in depression or anxiety care.', 'Track mood, sleep, side effects, dose changes, and follow-up notes.', $defaultMedline],
['citalopram', 'citalopram', 'citalopram hydrobromide', 'Celexa', 'citalopram tablets, citalopram solution', 'Selective serotonin reuptake inhibitor', 'Mental health', 'Often used in depression care.', 'Track mood, sleep, side effects, dose changes, and follow-up notes.', $defaultMedline],
['paroxetine', 'paroxetine', 'paroxetine hydrochloride', 'Paxil, Pexeva, Brisdelle', 'paroxetine tablets, paroxetine suspension', 'Selective serotonin reuptake inhibitor', 'Mental health', 'Used for depression, anxiety, or related indications when prescribed.', 'Track mood, sleep, side effects, dose changes, and discontinuation instructions.', $defaultMedline],
['venlafaxine', 'venlafaxine', 'venlafaxine hydrochloride', 'Effexor XR', 'venlafaxine tablets, venlafaxine ER', 'Serotonin-norepinephrine reuptake inhibitor', 'Mental health', 'Used for depression, anxiety, or related indications when prescribed.', 'Track mood, blood pressure, sleep, side effects, and dose changes.', $defaultMedline],
['duloxetine', 'duloxetine', 'duloxetine hydrochloride', 'Cymbalta, Drizalma Sprinkle', 'duloxetine capsules', 'Serotonin-norepinephrine reuptake inhibitor', 'Mental health and pain', 'Used for depression, anxiety, nerve pain, or related indications when prescribed.', 'Track mood, pain, sleep, blood pressure, and side effects.', $defaultMedline],
['bupropion', 'bupropion', 'bupropion hydrochloride', 'Wellbutrin, Wellbutrin XL, Zyban, Aplenzin', 'bupropion SR, bupropion XL', 'Norepinephrine-dopamine reuptake inhibitor', 'Mental health and smoking cessation', 'Used for depression or smoking cessation indications when prescribed.', 'Track mood, sleep, appetite, anxiety, dose formulation, and seizure-risk context.', $defaultMedline],
['mirtazapine', 'mirtazapine', 'mirtazapine', 'Remeron, Remeron SolTab', 'mirtazapine tablets, mirtazapine ODT', 'Noradrenergic and specific serotonergic antidepressant', 'Mental health', 'Used for depression care when prescribed.', 'Track mood, sleep, appetite, weight, sedation, and dose changes.', $defaultMedline],
['trazodone', 'trazodone', 'trazodone hydrochloride', 'Desyrel, Oleptro', 'trazodone tablets', 'Serotonin antagonist and reuptake inhibitor', 'Mental health and sleep', 'Used for depression or sleep-related indications when prescribed.', 'Track sleep, dizziness, next-day sedation, mood, and dose timing.', $defaultMedline],
['buspirone', 'buspirone', 'buspirone hydrochloride', 'Buspar', 'buspirone tablets', 'Anxiolytic', 'Mental health', 'Used for anxiety care when prescribed.', 'Track anxiety symptoms, dizziness, dose schedule, and time-to-effect.', $defaultMedline],
['hydroxyzine', 'hydroxyzine', 'hydroxyzine hydrochloride or pamoate', 'Atarax, Vistaril', 'hydroxyzine tablets, hydroxyzine capsules', 'First-generation antihistamine anxiolytic', 'Mental health and allergy', 'Used for anxiety, itching, or allergy indications when prescribed.', 'Track drowsiness, dry mouth, anxiety symptoms, itching, and dose timing.', $defaultMedline],
['alprazolam', 'alprazolam', 'alprazolam', 'Xanax, Xanax XR, Niravam', 'alprazolam tablets, alprazolam ER', 'Benzodiazepine', 'Mental health', 'Used for anxiety or panic indications when prescribed.', 'Track sedation, dose timing, driving cautions, refills, and taper instructions.', $defaultMedline],
['lorazepam', 'lorazepam', 'lorazepam', 'Ativan', 'lorazepam tablets, lorazepam solution', 'Benzodiazepine', 'Mental health and neurology', 'Used for anxiety, sedation, or seizure-related indications when prescribed.', 'Track sedation, dose timing, falls risk, and taper instructions.', $defaultMedline],
['clonazepam', 'clonazepam', 'clonazepam', 'Klonopin', 'clonazepam tablets, clonazepam ODT', 'Benzodiazepine', 'Mental health and neurology', 'Used for seizure, panic, or related indications when prescribed.', 'Track sedation, symptom response, dose timing, and taper instructions.', $defaultMedline],
['zolpidem', 'zolpidem', 'zolpidem tartrate', 'Ambien, Ambien CR, Edluar, Intermezzo', 'zolpidem tablets, zolpidem ER', 'Sedative-hypnotic', 'Sleep', 'Used for insomnia when prescribed.', 'Track sleep duration, next-day sedation, unusual sleep behaviors, and dose timing.', $defaultMedline],
['methylphenidate', 'methylphenidate', 'methylphenidate hydrochloride', 'Ritalin, Concerta, Metadate, Jornay PM, Quillivant XR', 'methylphenidate IR, methylphenidate ER', 'Central nervous system stimulant', 'Mental health and neurology', 'Used for ADHD or related indications when prescribed.', 'Track focus, appetite, sleep, pulse, blood pressure, and formulation.', $defaultMedline],
['amphetamine dextroamphetamine', 'mixed amphetamine salts', 'amphetamine and dextroamphetamine salts', 'Adderall, Adderall XR, Mydayis', 'mixed amphetamine salts IR, mixed amphetamine salts ER', 'Central nervous system stimulant', 'Mental health and neurology', 'Used for ADHD or narcolepsy indications when prescribed.', 'Track focus, appetite, sleep, pulse, blood pressure, and formulation.', $defaultMedline],
['atomoxetine', 'atomoxetine', 'atomoxetine hydrochloride', 'Strattera', 'atomoxetine capsules', 'Selective norepinephrine reuptake inhibitor', 'Mental health and neurology', 'Used for ADHD care when prescribed.', 'Track focus, appetite, sleep, mood, pulse, blood pressure, and dose changes.', $defaultMedline],
['lamotrigine', 'lamotrigine', 'lamotrigine', 'Lamictal, Lamictal XR', 'lamotrigine tablets, lamotrigine ODT', 'Anticonvulsant mood stabilizer', 'Neurology and mental health', 'Used for seizures or bipolar disorder indications when prescribed.', 'Track rash, mood, seizure activity, dose titration, and missed doses.', $defaultMedline],
['levetiracetam', 'levetiracetam', 'levetiracetam', 'Keppra, Keppra XR, Spritam', 'levetiracetam tablets, levetiracetam solution', 'Anticonvulsant', 'Neurology', 'Used for seizure care when prescribed.', 'Track seizure activity, mood changes, sleepiness, and dose schedule.', $defaultMedline],
['topiramate', 'topiramate', 'topiramate', 'Topamax, Trokendi XR, Qudexy XR', 'topiramate tablets, topiramate ER', 'Anticonvulsant', 'Neurology', 'Used for seizures or migraine prevention indications when prescribed.', 'Track headache frequency, seizure activity, cognitive symptoms, tingling, and hydration.', $defaultMedline],
['levothyroxine', 'levothyroxine', 'levothyroxine sodium', 'Synthroid, Levoxyl, Unithroid, Tirosint, Euthyrox', 'levothyroxine tablets, levothyroxine capsules', 'Synthetic thyroid hormone', 'Endocrine', 'Used for hypothyroidism or thyroid hormone replacement when prescribed.', 'Track dose timing, TSH/T4 labs if ordered, symptoms, and brand/formulation changes.', $defaultMedline],
['methimazole', 'methimazole', 'methimazole', 'Tapazole', 'methimazole tablets', 'Antithyroid medicine', 'Endocrine', 'Used for hyperthyroidism care when prescribed.', 'Track thyroid labs if ordered, rash, fever/sore throat, and dose changes.', $defaultMedline],
['prednisone', 'prednisone', '17,21-dihydroxypregna-1,4-diene-3,11,20-trione', 'Deltasone, Rayos', 'prednisone tablets, prednisone solution', 'Systemic corticosteroid', 'Inflammation and immune conditions', 'Used for inflammatory, allergic, or immune-related conditions when prescribed.', 'Track dose taper, mood, glucose, sleep, swelling, and infection symptoms.', $defaultMedline],
['methylprednisolone', 'methylprednisolone', 'methylprednisolone', 'Medrol, Solu-Medrol, Depo-Medrol', 'methylprednisolone dose pack, methylprednisolone injection', 'Systemic corticosteroid', 'Inflammation and immune conditions', 'Used for inflammatory, allergic, or immune-related conditions when prescribed.', 'Track dose taper, mood, glucose, sleep, swelling, and infection symptoms.', $defaultMedline],
['hydrocortisone', 'hydrocortisone', 'cortisol', 'Cortef, Solu-Cortef, Anusol-HC', 'hydrocortisone tablets, hydrocortisone cream', 'Corticosteroid', 'Endocrine, inflammation, and dermatology', 'Used for steroid replacement or inflammation depending on formulation.', 'Track formulation, dose timing, skin response for topical use, and sick-day instructions if applicable.', $defaultMedline],
['ethinyl estradiol levonorgestrel', 'ethinyl estradiol and levonorgestrel', 'ethinyl estradiol plus levonorgestrel', 'Alesse, Aviane, Lessina, Seasonale', 'levonorgestrel/ethinyl estradiol tablets', 'Combined hormonal contraceptive', 'Reproductive health', 'Used for contraception or hormonal indications when prescribed.', 'Track cycle changes, missed doses, side effects, blood pressure, and clot-warning symptoms.', $defaultMedline],
['norethindrone', 'norethindrone', 'norethisterone', 'Aygestin, Camila, Errin, Heather', 'norethindrone tablets', 'Progestin', 'Reproductive health', 'Used for contraception or hormonal indications when prescribed.', 'Track cycle changes, missed doses, headaches, mood, and bleeding pattern.', $defaultMedline],
['medroxyprogesterone', 'medroxyprogesterone', 'medroxyprogesterone acetate', 'Provera, Depo-Provera', 'medroxyprogesterone tablets, medroxyprogesterone injection', 'Progestin', 'Reproductive health', 'Used for contraception or hormonal indications when prescribed.', 'Track injection dates, bleeding pattern, mood, weight, and follow-up schedule.', $defaultMedline],
['estradiol', 'estradiol', '17 beta-estradiol', 'Estrace, Vivelle-Dot, Climara, Divigel', 'estradiol tablets, estradiol patch, estradiol cream', 'Estrogen hormone', 'Reproductive health and endocrine', 'Used for hormone therapy or related indications when prescribed.', 'Track formulation, dose changes, symptoms, bleeding, and clinician follow-up.', $defaultMedline],
['clotrimazole', 'clotrimazole', 'clotrimazole', 'Lotrimin, Mycelex, Gyne-Lotrimin', 'clotrimazole cream, clotrimazole lozenges', 'Azole antifungal', 'Dermatology and infectious disease', 'Used for selected fungal infections.', 'Track symptom response, application site, irritation, and completion date.', $defaultMedline],
['terbinafine', 'terbinafine', 'terbinafine hydrochloride', 'Lamisil', 'terbinafine tablets, terbinafine cream', 'Allylamine antifungal', 'Dermatology and infectious disease', 'Used for selected fungal infections.', 'Track skin/nail response, rash, GI symptoms, and liver-lab follow-up if ordered.', $defaultMedline],
['mupirocin', 'mupirocin', 'mupirocin', 'Bactroban, Centany', 'mupirocin ointment, mupirocin cream', 'Topical antibiotic', 'Dermatology and infectious disease', 'Used for selected skin infections when prescribed.', 'Track application schedule, skin response, irritation, and completion date.', $defaultMedline],
['triamcinolone', 'triamcinolone', 'triamcinolone acetonide', 'Kenalog, Nasacort, Triderm', 'triamcinolone cream, triamcinolone nasal spray', 'Corticosteroid', 'Dermatology, allergy, and inflammation', 'Used for inflammation depending on formulation.', 'Track formulation, application site, skin thinning/irritation, and symptom response.', $defaultMedline],
['isotretinoin', 'isotretinoin', '13-cis-retinoic acid', 'Accutane, Absorica, Claravis, Myorisan, Zenatane', 'isotretinoin capsules', 'Retinoid', 'Dermatology', 'Used for severe acne when prescribed under strict monitoring.', 'Track pregnancy-prevention program requirements, mood, dryness, labs, and dose schedule.', $defaultMedline],
['tamsulosin', 'tamsulosin', 'tamsulosin hydrochloride', 'Flomax', 'tamsulosin capsules', 'Alpha-1 blocker', 'Urology', 'Often used for urinary symptoms related to enlarged prostate.', 'Track urinary symptoms, dizziness, falls, and dose timing.', $defaultMedline],
['finasteride', 'finasteride', 'finasteride', 'Proscar, Propecia', 'finasteride tablets', '5-alpha-reductase inhibitor', 'Urology and dermatology', 'Used for enlarged prostate or hair-loss indications when prescribed.', 'Track urinary symptoms or hair goals, sexual side effects, and dose indication.', $defaultMedline],
['sildenafil', 'sildenafil', 'sildenafil citrate', 'Viagra, Revatio', 'sildenafil tablets', 'PDE-5 inhibitor', 'Urology and pulmonary vascular', 'Used for erectile dysfunction or pulmonary hypertension indications when prescribed.', 'Track dose timing, blood pressure symptoms, headache, vision symptoms, and nitrate warnings.', $defaultMedline],
['tadalafil', 'tadalafil', 'tadalafil', 'Cialis, Adcirca', 'tadalafil tablets', 'PDE-5 inhibitor', 'Urology and pulmonary vascular', 'Used for erectile dysfunction, urinary symptoms, or pulmonary hypertension indications when prescribed.', 'Track dose timing, blood pressure symptoms, headache, back pain, and nitrate warnings.', $defaultMedline],
['oxybutynin', 'oxybutynin', 'oxybutynin chloride', 'Ditropan XL, Gelnique, Oxytrol', 'oxybutynin tablets, oxybutynin patch', 'Anticholinergic bladder antispasmodic', 'Urology', 'Used for overactive bladder symptoms when prescribed.', 'Track dry mouth, constipation, confusion, urinary symptoms, and formulation.', $defaultMedline],
['allopurinol', 'allopurinol', 'allopurinol', 'Zyloprim, Aloprim', 'allopurinol tablets', 'Xanthine oxidase inhibitor', 'Rheumatology and metabolic', 'Often used for gout prevention or uric-acid management.', 'Track uric acid labs if ordered, rash, flares, kidney follow-up, and dose changes.', $defaultMedline],
['colchicine', 'colchicine', 'colchicine', 'Colcrys, Mitigare, Gloperba', 'colchicine tablets, colchicine capsules', 'Antigout anti-inflammatory', 'Rheumatology', 'Used for gout flares or prevention when prescribed.', 'Track GI symptoms, flare timing, dose schedule, and interaction cautions.', $defaultMedline],
['methotrexate', 'methotrexate', 'amethopterin', 'Trexall, Rasuvo, Otrexup, RediTrex', 'methotrexate tablets, methotrexate injection', 'Antimetabolite immunomodulator', 'Rheumatology, oncology, and immune conditions', 'Used for autoimmune or cancer indications under clinician monitoring.', 'Track weekly schedule, folic acid plan if prescribed, labs, mouth sores, infection symptoms, and pregnancy precautions.', $defaultMedline],
['adalimumab', 'adalimumab', 'recombinant human monoclonal antibody adalimumab', 'Humira, Amjevita, Hadlima, Hyrimoz, Yuflyma', 'adalimumab biosimilars', 'TNF blocker biologic', 'Immune-mediated conditions', 'Used for selected autoimmune inflammatory conditions when prescribed.', 'Track injection dates, infection symptoms, flares, site reactions, and biologic brand.', $defaultMedline],
['etanercept', 'etanercept', 'recombinant TNF receptor fusion protein etanercept', 'Enbrel, Erelzi, Eticovo', 'etanercept biosimilars', 'TNF blocker biologic', 'Immune-mediated conditions', 'Used for selected autoimmune inflammatory conditions when prescribed.', 'Track injection dates, infection symptoms, flares, site reactions, and biologic brand.', $defaultMedline],
];
}
function seed_demo_data(PDO $pdo): void
{
$userCount = (int) $pdo->query('SELECT COUNT(*) FROM users')->fetchColumn();
if ($userCount > 0) {
return;
}
$created = app_now();
$today = new DateTimeImmutable('today');
$weekStart = $today->modify('monday this week');
$monthStart = $today->modify('first day of this month');
$stmt = $pdo->prepare(
"INSERT INTO users
(name, email, fitness_focus, role, status, phone, birthday, height_in, training_level,
emergency_contact, theme_mode, theme_accent, updated_at, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
$stmt->execute([
'Alex Rivera',
'alex@example.test',
'Strength, cycling, and lean muscle',
'admin',
'active',
'',
'',
70,
'Intermediate',
'',
'dark',
'green',
$created,
$created,
]);
$alexId = (int) $pdo->lastInsertId();
$stmt->execute([
'Maya Chen',
'maya@example.test',
'Running base, swimming, and recovery',
'member',
'active',
'',
'',
64,
'Advanced',
'',
'dark',
'pink',
$created,
$created,
]);
$mayaId = (int) $pdo->lastInsertId();
$goalStmt = $pdo->prepare(
"INSERT INTO nutrition_goals
(user_id, label, scope, calories, protein_g, carbs_g, fat_g, water_ml, start_date, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
$goals = [
[$alexId, 'Lean bulk day', 'daily', 2900, 190, 330, 85, 3600, app_date($weekStart)],
[$alexId, 'Lean bulk week', 'weekly', 20300, 1330, 2310, 595, 25200, app_date($weekStart)],
[$alexId, 'Monthly build', 'monthly', 87000, 5700, 9900, 2550, 108000, app_date($monthStart)],
[$mayaId, 'Endurance day', 'daily', 2350, 135, 310, 65, 3200, app_date($weekStart)],
[$mayaId, 'Endurance week', 'weekly', 16450, 945, 2170, 455, 22400, app_date($weekStart)],
];
foreach ($goals as $goal) {
$goalStmt->execute([...$goal, $created]);
}
$alexGoalId = (int) db_value($pdo, "SELECT id FROM nutrition_goals WHERE user_id = ? AND scope = 'weekly' LIMIT 1", [$alexId]);
$mayaGoalId = (int) db_value($pdo, "SELECT id FROM nutrition_goals WHERE user_id = ? AND scope = 'weekly' LIMIT 1", [$mayaId]);
$routineStmt = $pdo->prepare(
"INSERT INTO routines
(user_id, nutrition_goal_id, name, workout_type, planner_scope, start_date, end_date, intensity, notes, active, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)"
);
$routineStmt->execute([$alexId, $alexGoalId, 'Push Pull Legs + Ride', 'Hybrid', 'weekly', app_date($weekStart), '', 'Moderate', 'Three lifting days with one cycling push.', $created]);
$alexRoutineId = (int) $pdo->lastInsertId();
$routineStmt->execute([$alexId, $alexGoalId, 'Monthly Strength Block', 'Weight lifting', 'monthly', app_date($monthStart), '', 'High', 'Progressive overload with deload flexibility.', $created]);
$routineStmt->execute([$mayaId, $mayaGoalId, '5K Base + Swim', 'Running', 'weekly', app_date($weekStart), '', 'Moderate', 'Aerobic run focus with low-impact swim.', $created]);
$mayaRoutineId = (int) $pdo->lastInsertId();
$exerciseIds = db_key_values($pdo, "SELECT name, id FROM exercises WHERE is_builtin = 1");
$workoutStmt = $pdo->prepare(
"INSERT INTO workouts
(user_id, routine_id, exercise_id, scheduled_on, title, workout_type, sets, reps, weight_lbs,
duration_min, distance_mi, intensity, status, notes, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
$workouts = [
[$alexId, $alexRoutineId, $exerciseIds['Bench Press'], app_date($weekStart), 'Push strength', 'Weight lifting', 4, 6, 185, 55, 0, 'RPE 8', 'complete', 'Add 5 lb if bar speed holds.'],
[$alexId, $alexRoutineId, $exerciseIds['Indoor Cycling'], app_date($weekStart->modify('+1 day')), 'Zone 2 ride', 'Cycling', 0, 0, 0, 45, 14, 'Easy', 'planned', 'Keep nasal breathing.'],
[$alexId, $alexRoutineId, $exerciseIds['Deadlift'], app_date($weekStart->modify('+2 days')), 'Pull strength', 'Weight lifting', 5, 3, 315, 60, 0, 'Heavy', 'planned', 'Long rest between sets.'],
[$alexId, $alexRoutineId, $exerciseIds['Barbell Back Squat'], app_date($weekStart->modify('+4 days')), 'Legs volume', 'Weight lifting', 4, 8, 225, 65, 0, 'Moderate', 'planned', 'Superset core plank.'],
[$mayaId, $mayaRoutineId, $exerciseIds['Easy Run'], app_date($weekStart), 'Easy miles', 'Running', 0, 0, 0, 35, 3.5, 'Easy', 'complete', 'Relaxed pace.'],
[$mayaId, $mayaRoutineId, $exerciseIds['Tempo Run'], app_date($weekStart->modify('+2 days')), 'Tempo intervals', 'Running', 0, 0, 0, 42, 4.2, 'Threshold', 'planned', '3 x 8 min tempo.'],
[$mayaId, $mayaRoutineId, $exerciseIds['Pool Laps'], app_date($weekStart->modify('+4 days')), 'Recovery swim', 'Swimming', 0, 0, 0, 30, 0, 'Easy', 'planned', 'Smooth form.'],
];
foreach ($workouts as $workout) {
$workoutStmt->execute([...$workout, $created]);
}
$nutritionStmt = $pdo->prepare(
"INSERT INTO nutrition_logs
(user_id, logged_on, calories, protein_g, carbs_g, fat_g, water_ml, notes, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
$nutritionLogs = [
[$alexId, app_date($today), 1540, 110, 165, 42, 1850, 'Breakfast, shake, and lunch logged.'],
[$alexId, app_date($today->modify('-1 day')), 2865, 186, 325, 82, 3700, 'Hit the target closely.'],
[$mayaId, app_date($today), 1210, 74, 164, 31, 1700, 'Morning and lunch logged.'],
];
foreach ($nutritionLogs as $log) {
$nutritionStmt->execute([...$log, $created]);
}
$supplementIds = db_key_values($pdo, "SELECT name, id FROM supplements WHERE is_builtin = 1");
$supplementLogStmt = $pdo->prepare(
"INSERT INTO supplement_logs
(user_id, supplement_id, taken_on, timing, dose, status, notes, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
);
$supplementLogs = [
[$alexId, $supplementIds['Creatine Monohydrate'], app_date($today), 'Daily', '5 g', 'taken', 'With breakfast.'],
[$alexId, $supplementIds['Pre-Workout Blend'], app_date($today), 'Pre-workout', '1 scoop', 'planned', 'Before pull session.'],
[$alexId, $supplementIds['Whey Protein'], app_date($today), 'Post-workout', '30 g', 'planned', 'After training.'],
[$mayaId, $supplementIds['Electrolytes'], app_date($today), 'Intra-workout', '1 serving', 'planned', 'For tempo run.'],
];
foreach ($supplementLogs as $log) {
$supplementLogStmt->execute([...$log, $created]);
}
$metricStmt = $pdo->prepare(
"INSERT INTO body_metrics
(user_id, measured_on, weight_lbs, body_fat_pct, sleep_hours, resting_hr, mood, notes, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
$metrics = [
[$alexId, app_date($today), 184.2, 14.8, 7.1, 58, 'Focused', 'Lower back feels good.'],
[$alexId, app_date($today->modify('-7 days')), 185.1, 15.0, 6.8, 60, 'Steady', 'Minor shoulder tightness.'],
[$mayaId, app_date($today), 132.4, 19.6, 7.8, 53, 'Fresh', 'Good recovery.'],
];
foreach ($metrics as $metric) {
$metricStmt->execute([...$metric, $created]);
}
$medicineIds = db_key_values($pdo, "SELECT generic_name AS name, id FROM medication_catalog");
$medicationStmt = $pdo->prepare(
"INSERT INTO medication_logs
(user_id, medication_id, taken_on, medication_name, dose, route, frequency, status, prescriber, notes, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
$medicationLogs = [
[$alexId, $medicineIds['metformin'] ?? null, app_date($today), 'metformin', '500 mg', 'oral', 'twice daily', 'taken', 'Primary care', 'Demo log for glucose tracking.'],
[$alexId, $medicineIds['atorvastatin'] ?? null, app_date($today), 'atorvastatin', '20 mg', 'oral', 'nightly', 'planned', 'Cardiology', 'Review lipid panel at follow-up.'],
[$mayaId, $medicineIds['albuterol'] ?? null, app_date($today), 'albuterol', '2 puffs', 'inhaled', 'as needed', 'planned', 'Pulmonology', 'Track use with symptoms and triggers.'],
];
foreach ($medicationLogs as $log) {
$medicationStmt->execute([...$log, $created]);
}
$vitalStmt = $pdo->prepare(
"INSERT INTO vital_logs
(user_id, measured_on, systolic, diastolic, pulse, temperature_f, oxygen_pct, glucose_mg_dl, pain_score, symptoms, notes, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
$vitals = [
[$alexId, app_date($today), 126, 78, 62, 98.4, 98, 112, 1, 'Mild soreness', 'Morning reading before breakfast.'],
[$mayaId, app_date($today), 112, 70, 56, 98.2, 99, 0, 0, 'None', 'Rest day baseline.'],
];
foreach ($vitals as $vital) {
$vitalStmt->execute([...$vital, $created]);
}
$labStmt = $pdo->prepare(
"INSERT INTO lab_results
(user_id, collected_on, test_name, result_value, unit, reference_range, ordering_clinician, notes, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
$labs = [
[$alexId, app_date($today->modify('-14 days')), 'A1C', '6.2', '%', 'Clinician supplied', 'Primary care', 'Demo metabolic marker.'],
[$alexId, app_date($today->modify('-14 days')), 'LDL cholesterol', '96', 'mg/dL', 'Clinician supplied', 'Cardiology', 'Review with statin plan.'],
[$mayaId, app_date($today->modify('-10 days')), 'Ferritin', '42', 'ng/mL', 'Clinician supplied', 'Primary care', 'Endurance training context.'],
];
foreach ($labs as $lab) {
$labStmt->execute([...$lab, $created]);
}
}
function seed_medical_demo_data(PDO $pdo): void
{
$users = db_all($pdo, 'SELECT id, name FROM users ORDER BY id LIMIT 2');
if (!$users) {
return;
}
$created = app_now();
$today = new DateTimeImmutable('today');
$alexId = (int) $users[0]['id'];
$mayaId = isset($users[1]) ? (int) $users[1]['id'] : $alexId;
$medicineIds = db_key_values($pdo, "SELECT generic_name AS name, id FROM medication_catalog");
if ((int) $pdo->query('SELECT COUNT(*) FROM medication_logs')->fetchColumn() === 0) {
$medicationStmt = $pdo->prepare(
"INSERT INTO medication_logs
(user_id, medication_id, taken_on, medication_name, dose, route, frequency, status, prescriber, notes, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
$medicationLogs = [
[$alexId, $medicineIds['metformin'] ?? null, app_date($today), 'metformin', '500 mg', 'oral', 'twice daily', 'taken', 'Primary care', 'Demo log for glucose tracking.'],
[$alexId, $medicineIds['atorvastatin'] ?? null, app_date($today), 'atorvastatin', '20 mg', 'oral', 'nightly', 'planned', 'Cardiology', 'Review lipid panel at follow-up.'],
[$mayaId, $medicineIds['albuterol'] ?? null, app_date($today), 'albuterol', '2 puffs', 'inhaled', 'as needed', 'planned', 'Pulmonology', 'Track use with symptoms and triggers.'],
];
foreach ($medicationLogs as $log) {
$medicationStmt->execute([...$log, $created]);
}
}
if ((int) $pdo->query('SELECT COUNT(*) FROM vital_logs')->fetchColumn() === 0) {
$vitalStmt = $pdo->prepare(
"INSERT INTO vital_logs
(user_id, measured_on, systolic, diastolic, pulse, temperature_f, oxygen_pct, glucose_mg_dl, pain_score, symptoms, notes, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
$vitals = [
[$alexId, app_date($today), 126, 78, 62, 98.4, 98, 112, 1, 'Mild soreness', 'Morning reading before breakfast.'],
[$mayaId, app_date($today), 112, 70, 56, 98.2, 99, 0, 0, 'None', 'Rest day baseline.'],
];
foreach ($vitals as $vital) {
$vitalStmt->execute([...$vital, $created]);
}
}
if ((int) $pdo->query('SELECT COUNT(*) FROM lab_results')->fetchColumn() === 0) {
$labStmt = $pdo->prepare(
"INSERT INTO lab_results
(user_id, collected_on, test_name, result_value, unit, reference_range, ordering_clinician, notes, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
$labs = [
[$alexId, app_date($today->modify('-14 days')), 'A1C', '6.2', '%', 'Clinician supplied', 'Primary care', 'Demo metabolic marker.'],
[$alexId, app_date($today->modify('-14 days')), 'LDL cholesterol', '96', 'mg/dL', 'Clinician supplied', 'Cardiology', 'Review with statin plan.'],
[$mayaId, app_date($today->modify('-10 days')), 'Ferritin', '42', 'ng/mL', 'Clinician supplied', 'Primary care', 'Endurance training context.'],
];
foreach ($labs as $lab) {
$labStmt->execute([...$lab, $created]);
}
}
if ((int) $pdo->query('SELECT COUNT(*) FROM symptom_logs')->fetchColumn() === 0) {
$symptomStmt = $pdo->prepare(
"INSERT INTO symptom_logs
(user_id, observed_on, symptom, body_area, severity, duration, triggers, action_taken, notes, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
$symptoms = [
[$alexId, app_date($today), 'Headache', 'Head', 3, '2 hours', 'Long screen session', 'Hydration and rest', 'Monitor if it returns.'],
[$alexId, app_date($today->modify('-1 day')), 'Dizziness', 'General', 2, '15 minutes', 'Standing quickly', 'Sat down and checked BP', 'No recurrence after breakfast.'],
[$mayaId, app_date($today), 'Wheezing', 'Chest', 4, '20 minutes', 'Cold air run', 'Used rescue inhaler as directed', 'Track with activity and weather.'],
];
foreach ($symptoms as $symptom) {
$symptomStmt->execute([...$symptom, $created]);
}
}
if ((int) $pdo->query('SELECT COUNT(*) FROM allergy_records')->fetchColumn() === 0) {
$allergyStmt = $pdo->prepare(
"INSERT INTO allergy_records
(user_id, allergen, reaction, severity, first_noted_on, status, notes, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
);
$allergies = [
[$alexId, 'Penicillin', 'Rash', 'moderate', app_date($today->modify('-5 years')), 'active', 'Demo allergy record. Confirm with clinician.'],
[$mayaId, 'Peanuts', 'Hives and throat tightness', 'severe', app_date($today->modify('-12 years')), 'active', 'Carries emergency plan per clinician.'],
];
foreach ($allergies as $allergy) {
$allergyStmt->execute([...$allergy, $created]);
}
}
if ((int) $pdo->query('SELECT COUNT(*) FROM appointments')->fetchColumn() === 0) {
$appointmentStmt = $pdo->prepare(
"INSERT INTO appointments
(user_id, appointment_on, clinician, specialty, location, reason, status, follow_up_notes, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
$appointments = [
[$alexId, app_date($today->modify('+3 days')), 'Dr. Morgan', 'Primary care', 'Downtown Clinic', 'Medication and blood pressure follow-up', 'scheduled', 'Bring home BP readings.'],
[$alexId, app_date($today->modify('+21 days')), 'Cardiology team', 'Cardiology', 'Heart Center', 'Lipid panel review', 'scheduled', 'Ask about statin tolerance.'],
[$mayaId, app_date($today->modify('+8 days')), 'Pulmonary clinic', 'Pulmonology', 'Respiratory Care', 'Asthma action plan review', 'scheduled', 'Discuss cold-weather symptoms.'],
];
foreach ($appointments as $appointment) {
$appointmentStmt->execute([...$appointment, $created]);
}
}
if ((int) $pdo->query('SELECT COUNT(*) FROM immunization_records')->fetchColumn() === 0) {
$immunizationStmt = $pdo->prepare(
"INSERT INTO immunization_records
(user_id, vaccine_name, administered_on, dose, lot_number, clinician_or_site, next_due_on, notes, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
$immunizations = [
[$alexId, 'Influenza', app_date($today->modify('-4 months')), '0.5 mL', '', 'Local pharmacy', app_date($today->modify('+8 months')), 'Annual reminder demo.'],
[$alexId, 'COVID-19 booster', app_date($today->modify('-7 months')), 'Booster', '', 'Community clinic', '', 'Record source: demo profile.'],
[$mayaId, 'Tdap', app_date($today->modify('-2 years')), 'Adult dose', '', 'Primary care', app_date($today->modify('+8 years')), 'Next due date depends on clinician guidance.'],
];
foreach ($immunizations as $immunization) {
$immunizationStmt->execute([...$immunization, $created]);
}
}
}
function app_now(): string
{
return (new DateTimeImmutable())->format('Y-m-d H:i:s');
}
function app_date(DateTimeImmutable $date): string
{
return $date->format('Y-m-d');
}
function parse_app_date(?string $value = null): DateTimeImmutable
{
if (!$value) {
return new DateTimeImmutable('today');
}
$parsed = DateTimeImmutable::createFromFormat('!Y-m-d', $value);
return $parsed ?: new DateTimeImmutable('today');
}
function range_for_view(string $view, DateTimeImmutable $anchor): array
{
if ($view === 'weekly') {
$start = $anchor->modify('monday this week');
return [$start, $start->modify('+6 days')];
}
if ($view === 'monthly') {
$start = $anchor->modify('first day of this month');
return [$start, $anchor->modify('last day of this month')];
}
return [$anchor, $anchor];
}
function db_all(PDO $pdo, string $sql, array $params = []): array
{
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll();
}
function db_one(PDO $pdo, string $sql, array $params = []): ?array
{
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$row = $stmt->fetch();
return $row ?: null;
}
function db_value(PDO $pdo, string $sql, array $params = []): mixed
{
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
return $stmt->fetchColumn();
}
function medicine_detail_path(array $medicine): string
{
return '/medicine.php?id=' . rawurlencode((string) ($medicine['id'] ?? ''));
}
function term_detail_path(array $term): string
{
return '/term.php?id=' . rawurlencode((string) ($term['id'] ?? ''));
}
function symptom_detail_path(array $symptom): string
{
return '/symptom.php?id=' . rawurlencode((string) ($symptom['id'] ?? ''));
}
function find_medication_catalog_item(PDO $pdo, array $query): ?array
{
$id = filter_var($query['id'] ?? null, FILTER_VALIDATE_INT);
if ($id !== false && $id > 0) {
return db_one($pdo, 'SELECT * FROM medication_catalog WHERE id = ?', [$id]);
}
$name = trim((string) ($query['name'] ?? $query['q'] ?? ''));
if ($name === '') {
return null;
}
return db_one(
$pdo,
"SELECT *
FROM medication_catalog
WHERE lower(generic_name) = lower(?)
OR lower(active_ingredient) = lower(?)
OR lower(chemical_name) = lower(?)
ORDER BY
CASE
WHEN lower(generic_name) = lower(?) THEN 0
WHEN lower(active_ingredient) = lower(?) THEN 1
ELSE 2
END,
generic_name
LIMIT 1",
[$name, $name, $name, $name, $name]
);
}
function find_medical_term_item(PDO $pdo, array $query): ?array
{
$id = filter_var($query['id'] ?? null, FILTER_VALIDATE_INT);
if ($id !== false && $id > 0) {
return db_one($pdo, 'SELECT * FROM medical_terms WHERE id = ?', [$id]);
}
$term = trim((string) ($query['term'] ?? $query['q'] ?? ''));
if ($term === '') {
return null;
}
return db_one(
$pdo,
"SELECT *
FROM medical_terms
WHERE lower(term) = lower(?)
ORDER BY term
LIMIT 1",
[$term]
);
}
function find_medical_symptom_item(PDO $pdo, array $query): ?array
{
$id = filter_var($query['id'] ?? null, FILTER_VALIDATE_INT);
if ($id !== false && $id > 0) {
return db_one($pdo, 'SELECT * FROM medical_symptoms WHERE id = ?', [$id]);
}
$name = trim((string) ($query['name'] ?? $query['symptom'] ?? $query['q'] ?? ''));
if ($name === '') {
return null;
}
return db_one(
$pdo,
"SELECT *
FROM medical_symptoms
WHERE lower(name) = lower(?)
ORDER BY name
LIMIT 1",
[$name]
);
}
function medicine_reference_links(array $medicine): array
{
$links = [
'RxNorm' => $medicine['rxnorm_url'] ?? '',
'PubChem' => $medicine['pubchem_url'] ?? '',
'DailyMed' => $medicine['dailymed_url'] ?? '',
'MedlinePlus' => $medicine['medlineplus_url'] ?? '',
'PubMed' => $medicine['pubmed_url'] ?? '',
'Studies' => $medicine['clinicaltrials_url'] ?? '',
];
return array_filter($links, static fn ($url) => trim((string) $url) !== '');
}
function db_key_values(PDO $pdo, string $sql, array $params = []): array
{
$rows = db_all($pdo, $sql, $params);
$values = [];
foreach ($rows as $row) {
$values[$row['name']] = (int) $row['id'];
}
return $values;
}
function text_value(array $data, string $key, string $default = ''): string
{
if (!array_key_exists($key, $data) || $data[$key] === null) {
return $default;
}
$value = trim((string) $data[$key]);
return $value === '' ? $default : $value;
}
function number_value(array $data, string $key, float $default = 0): float
{
if (!array_key_exists($key, $data) || $data[$key] === '' || $data[$key] === null) {
return $default;
}
return is_numeric($data[$key]) ? (float) $data[$key] : $default;
}
function int_value(array $data, string $key, int $default = 0): int
{
return (int) number_value($data, $key, $default);
}
function user_id_value(array $data): int
{
$userId = int_value($data, 'user_id');
if ($userId <= 0) {
throw new InvalidArgumentException('A valid profile is required.');
}
return $userId;
}
function clamp_choice(string $value, array $choices, string $fallback): string
{
return in_array($value, $choices, true) ? $value : $fallback;
}
function nutrition_goal_for(PDO $pdo, int $userId, string $view, DateTimeImmutable $start, DateTimeImmutable $end): array
{
$goal = db_one(
$pdo,
"SELECT * FROM nutrition_goals
WHERE user_id = ? AND scope = ? AND start_date <= ?
ORDER BY start_date DESC, id DESC
LIMIT 1",
[$userId, $view, app_date($end)]
);
if ($goal) {
$goal['scaled_from'] = $goal['scope'];
return $goal;
}
$daily = db_one(
$pdo,
"SELECT * FROM nutrition_goals
WHERE user_id = ? AND scope = 'daily' AND start_date <= ?
ORDER BY start_date DESC, id DESC
LIMIT 1",
[$userId, app_date($end)]
);
if ($daily) {
$days = ((int) $start->diff($end)->format('%a')) + 1;
foreach (['calories', 'protein_g', 'carbs_g', 'fat_g', 'water_ml'] as $field) {
$daily[$field] = (float) $daily[$field] * $days;
}
$daily['label'] = $daily['label'] . ' x ' . $days . ' days';
$daily['scope'] = $view;
$daily['scaled_from'] = 'daily';
return $daily;
}
return [
'id' => null,
'label' => 'No target',
'scope' => $view,
'calories' => 0,
'protein_g' => 0,
'carbs_g' => 0,
'fat_g' => 0,
'water_ml' => 0,
'start_date' => app_date($start),
'scaled_from' => 'none',
];
}
function nutrition_totals(PDO $pdo, int $userId, DateTimeImmutable $start, DateTimeImmutable $end): array
{
$row = db_one(
$pdo,
"SELECT
COALESCE(SUM(calories), 0) AS calories,
COALESCE(SUM(protein_g), 0) AS protein_g,
COALESCE(SUM(carbs_g), 0) AS carbs_g,
COALESCE(SUM(fat_g), 0) AS fat_g,
COALESCE(SUM(water_ml), 0) AS water_ml
FROM nutrition_logs
WHERE user_id = ? AND logged_on BETWEEN ? AND ?",
[$userId, app_date($start), app_date($end)]
) ?? [];
foreach (['calories', 'protein_g', 'carbs_g', 'fat_g', 'water_ml'] as $field) {
$row[$field] = (float) ($row[$field] ?? 0);
}
return $row;
}
function workout_summary(array $workouts): array
{
$planned = count($workouts);
$completed = 0;
$skipped = 0;
$minutes = 0.0;
$distance = 0.0;
$volume = 0.0;
foreach ($workouts as $workout) {
if ($workout['status'] === 'complete') {
$completed++;
}
if ($workout['status'] === 'skipped') {
$skipped++;
}
$minutes += (float) $workout['duration_min'];
$distance += (float) $workout['distance_mi'];
$volume += (float) $workout['sets'] * (float) $workout['reps'] * (float) $workout['weight_lbs'];
}
return [
'planned' => $planned,
'completed' => $completed,
'skipped' => $skipped,
'minutes' => round($minutes, 1),
'distance_mi' => round($distance, 2),
'weight_volume_lbs' => round($volume, 1),
'completion_rate' => $planned > 0 ? round(($completed / $planned) * 100, 1) : 0,
];
}
function is_admin(PDO $pdo, int $userId): bool
{
if ($userId <= 0) {
return false;
}
return (bool) db_value(
$pdo,
"SELECT COUNT(*) FROM users WHERE id = ? AND role = 'admin' AND status = 'active'",
[$userId]
);
}
function admin_user_rows(PDO $pdo): array
{
return db_all(
$pdo,
"SELECT
users.*,
(SELECT COUNT(*) FROM routines WHERE routines.user_id = users.id) AS routine_count,
(SELECT COUNT(*) FROM workouts WHERE workouts.user_id = users.id) AS workout_count,
(SELECT COUNT(*) FROM nutrition_logs WHERE nutrition_logs.user_id = users.id) AS nutrition_log_count,
(SELECT COUNT(*) FROM supplement_logs WHERE supplement_logs.user_id = users.id) AS supplement_log_count,
(SELECT COUNT(*) FROM body_metrics WHERE body_metrics.user_id = users.id) AS metric_count,
(SELECT COUNT(*) FROM medication_logs WHERE medication_logs.user_id = users.id) AS medication_log_count,
(SELECT COUNT(*) FROM vital_logs WHERE vital_logs.user_id = users.id) AS vital_log_count,
(SELECT COUNT(*) FROM lab_results WHERE lab_results.user_id = users.id) AS lab_result_count,
(SELECT COUNT(*) FROM symptom_logs WHERE symptom_logs.user_id = users.id) AS symptom_log_count,
(SELECT COUNT(*) FROM allergy_records WHERE allergy_records.user_id = users.id) AS allergy_count,
(SELECT COUNT(*) FROM appointments WHERE appointments.user_id = users.id) AS appointment_count,
(SELECT COUNT(*) FROM immunization_records WHERE immunization_records.user_id = users.id) AS immunization_count,
(SELECT MAX(measured_on) FROM body_metrics WHERE body_metrics.user_id = users.id) AS last_metric_on
FROM users
ORDER BY
CASE users.status WHEN 'active' THEN 0 ELSE 1 END,
CASE users.role WHEN 'admin' THEN 0 ELSE 1 END,
users.name"
);
}
function admin_summary(PDO $pdo): array
{
return [
'total_users' => (int) db_value($pdo, 'SELECT COUNT(*) FROM users'),
'active_users' => (int) db_value($pdo, "SELECT COUNT(*) FROM users WHERE status = 'active'"),
'inactive_users' => (int) db_value($pdo, "SELECT COUNT(*) FROM users WHERE status <> 'active'"),
'admins' => (int) db_value($pdo, "SELECT COUNT(*) FROM users WHERE role = 'admin'"),
'members' => (int) db_value($pdo, "SELECT COUNT(*) FROM users WHERE role = 'member'"),
'medication_logs' => (int) db_value($pdo, 'SELECT COUNT(*) FROM medication_logs'),
'vital_logs' => (int) db_value($pdo, 'SELECT COUNT(*) FROM vital_logs'),
'lab_results' => (int) db_value($pdo, 'SELECT COUNT(*) FROM lab_results'),
'appointments' => (int) db_value($pdo, 'SELECT COUNT(*) FROM appointments'),
];
}
function require_admin(PDO $pdo, array $data): int
{
$actingUserId = int_value($data, 'acting_user_id') ?: int_value($data, 'user_id');
if (!is_admin($pdo, $actingUserId)) {
throw new InvalidArgumentException('Admin access is required.');
}
return $actingUserId;
}
function last_admin_guard(PDO $pdo, int $targetUserId, string $nextRole, string $nextStatus): void
{
$target = db_one($pdo, 'SELECT id, role, status FROM users WHERE id = ?', [$targetUserId]);
if (!$target || $target['role'] !== 'admin' || $target['status'] !== 'active') {
return;
}
$wouldRemainActiveAdmin = $nextRole === 'admin' && $nextStatus === 'active';
if ($wouldRemainActiveAdmin) {
return;
}
$activeAdmins = (int) db_value($pdo, "SELECT COUNT(*) FROM users WHERE role = 'admin' AND status = 'active'");
if ($activeAdmins <= 1) {
throw new InvalidArgumentException('At least one active admin profile is required.');
}
}
function state_payload(array $query): array
{
$pdo = app_db();
$view = clamp_choice((string) ($query['view'] ?? 'daily'), ['daily', 'weekly', 'monthly'], 'daily');
$anchor = parse_app_date(isset($query['date']) ? (string) $query['date'] : null);
[$start, $end] = range_for_view($view, $anchor);
$users = db_all($pdo, "SELECT * FROM users WHERE status = 'active' ORDER BY id");
if (!$users) {
seed_demo_data($pdo);
ensure_first_user_is_admin($pdo);
$users = db_all($pdo, "SELECT * FROM users WHERE status = 'active' ORDER BY id");
}
if (!$users) {
$users = db_all($pdo, 'SELECT * FROM users ORDER BY id');
}
$requestedUserId = isset($query['user_id']) ? (int) $query['user_id'] : (int) $users[0]['id'];
$currentUser = $users[0];
foreach ($users as $user) {
if ((int) $user['id'] === $requestedUserId) {
$currentUser = $user;
break;
}
}
$userId = (int) $currentUser['id'];
$currentUserIsAdmin = is_admin($pdo, $userId);
$exercises = db_all(
$pdo,
"SELECT * FROM exercises
WHERE is_builtin = 1 OR user_id = ?
ORDER BY is_builtin DESC, category, name",
[$userId]
);
$supplements = db_all(
$pdo,
"SELECT * FROM supplements
WHERE is_builtin = 1 OR user_id = ?
ORDER BY timing, name",
[$userId]
);
$nutritionGoals = db_all(
$pdo,
"SELECT * FROM nutrition_goals
WHERE user_id = ?
ORDER BY start_date DESC, scope, label",
[$userId]
);
$routines = db_all(
$pdo,
"SELECT routines.*, nutrition_goals.label AS nutrition_goal_label
FROM routines
LEFT JOIN nutrition_goals ON nutrition_goals.id = routines.nutrition_goal_id
WHERE routines.user_id = ?
ORDER BY routines.active DESC, routines.start_date DESC, routines.name",
[$userId]
);
$workouts = db_all(
$pdo,
"SELECT workouts.*, exercises.name AS exercise_name, routines.name AS routine_name
FROM workouts
LEFT JOIN exercises ON exercises.id = workouts.exercise_id
LEFT JOIN routines ON routines.id = workouts.routine_id
WHERE workouts.user_id = ? AND workouts.scheduled_on BETWEEN ? AND ?
ORDER BY workouts.scheduled_on, workouts.id",
[$userId, app_date($start), app_date($end)]
);
$nutritionLogs = db_all(
$pdo,
"SELECT * FROM nutrition_logs
WHERE user_id = ? AND logged_on BETWEEN ? AND ?
ORDER BY logged_on DESC, id DESC",
[$userId, app_date($start), app_date($end)]
);
$supplementLogs = db_all(
$pdo,
"SELECT supplement_logs.*, supplements.name AS supplement_name, supplements.purpose AS supplement_purpose
FROM supplement_logs
LEFT JOIN supplements ON supplements.id = supplement_logs.supplement_id
WHERE supplement_logs.user_id = ? AND supplement_logs.taken_on BETWEEN ? AND ?
ORDER BY supplement_logs.taken_on DESC, supplement_logs.id DESC",
[$userId, app_date($start), app_date($end)]
);
$bodyMetrics = db_all(
$pdo,
"SELECT * FROM body_metrics
WHERE user_id = ? AND measured_on BETWEEN ? AND ?
ORDER BY measured_on DESC, id DESC",
[$userId, app_date($start), app_date($end)]
);
$latestMetric = db_one(
$pdo,
"SELECT * FROM body_metrics
WHERE user_id = ?
ORDER BY measured_on DESC, id DESC
LIMIT 1",
[$userId]
);
$medicalTerms = db_all($pdo, 'SELECT * FROM medical_terms ORDER BY category, term');
$medicalConditions = db_all($pdo, 'SELECT * FROM medical_conditions ORDER BY category, name');
$medicalSymptoms = db_all($pdo, 'SELECT * FROM medical_symptoms ORDER BY category, name');
$medicationCatalog = db_all($pdo, 'SELECT * FROM medication_catalog ORDER BY generic_name');
$researchStudies = db_all($pdo, 'SELECT * FROM research_studies ORDER BY category, related_medicine, title');
$medicationLogs = db_all(
$pdo,
"SELECT medication_logs.*, medication_catalog.class_name, medication_catalog.rxnorm_url, medication_catalog.pubmed_url
FROM medication_logs
LEFT JOIN medication_catalog ON medication_catalog.id = medication_logs.medication_id
WHERE medication_logs.user_id = ? AND medication_logs.taken_on BETWEEN ? AND ?
ORDER BY medication_logs.taken_on DESC, medication_logs.id DESC",
[$userId, app_date($start), app_date($end)]
);
$vitalLogs = db_all(
$pdo,
"SELECT * FROM vital_logs
WHERE user_id = ? AND measured_on BETWEEN ? AND ?
ORDER BY measured_on DESC, id DESC",
[$userId, app_date($start), app_date($end)]
);
$labResults = db_all(
$pdo,
"SELECT * FROM lab_results
WHERE user_id = ? AND collected_on BETWEEN ? AND ?
ORDER BY collected_on DESC, id DESC",
[$userId, app_date($start), app_date($end)]
);
$symptomLogs = db_all(
$pdo,
"SELECT * FROM symptom_logs
WHERE user_id = ? AND observed_on BETWEEN ? AND ?
ORDER BY observed_on DESC, severity DESC, id DESC",
[$userId, app_date($start), app_date($end)]
);
$allergyRecords = db_all(
$pdo,
"SELECT * FROM allergy_records
WHERE user_id = ?
ORDER BY CASE status WHEN 'active' THEN 0 ELSE 1 END, allergen",
[$userId]
);
$appointments = db_all(
$pdo,
"SELECT * FROM appointments
WHERE user_id = ? AND appointment_on BETWEEN ? AND ?
ORDER BY appointment_on, id",
[$userId, app_date($start), app_date($end)]
);
$immunizationRecords = db_all(
$pdo,
"SELECT * FROM immunization_records
WHERE user_id = ?
ORDER BY administered_on DESC, id DESC",
[$userId]
);
$latestVitals = db_one(
$pdo,
"SELECT * FROM vital_logs
WHERE user_id = ?
ORDER BY measured_on DESC, id DESC
LIMIT 1",
[$userId]
);
return [
'users' => $users,
'adminUsers' => $currentUserIsAdmin ? admin_user_rows($pdo) : [],
'adminSummary' => $currentUserIsAdmin ? admin_summary($pdo) : null,
'currentUser' => $currentUser,
'currentUserIsAdmin' => $currentUserIsAdmin,
'view' => $view,
'range' => [
'anchor' => app_date($anchor),
'start' => app_date($start),
'end' => app_date($end),
'days' => ((int) $start->diff($end)->format('%a')) + 1,
],
'exercises' => $exercises,
'supplements' => $supplements,
'nutritionGoals' => $nutritionGoals,
'routines' => $routines,
'workouts' => $workouts,
'nutritionLogs' => $nutritionLogs,
'supplementLogs' => $supplementLogs,
'bodyMetrics' => $bodyMetrics,
'latestMetric' => $latestMetric,
'medicalTerms' => $medicalTerms,
'medicalConditions' => $medicalConditions,
'medicalSymptoms' => $medicalSymptoms,
'medicationCatalog' => $medicationCatalog,
'researchStudies' => $researchStudies,
'medicationLogs' => $medicationLogs,
'vitalLogs' => $vitalLogs,
'labResults' => $labResults,
'symptomLogs' => $symptomLogs,
'allergyRecords' => $allergyRecords,
'appointments' => $appointments,
'immunizationRecords' => $immunizationRecords,
'latestVitals' => $latestVitals,
'nutritionSummary' => [
'target' => nutrition_goal_for($pdo, $userId, $view, $start, $end),
'actual' => nutrition_totals($pdo, $userId, $start, $end),
],
'workoutSummary' => workout_summary($workouts),
];
}
function library_search_payload(array $query): array
{
$pdo = app_db();
$search = trim((string) ($query['q'] ?? ''));
$limit = max(10, min(500, (int) ($query['limit'] ?? 250)));
if ($search === '') {
return [
'query' => '',
'medicalTerms' => db_all($pdo, 'SELECT * FROM medical_terms ORDER BY category, term LIMIT ?', [$limit]),
'medicalConditions' => db_all($pdo, 'SELECT * FROM medical_conditions ORDER BY category, name LIMIT ?', [$limit]),
'medicalSymptoms' => db_all($pdo, 'SELECT * FROM medical_symptoms ORDER BY category, name LIMIT ?', [$limit]),
'medicationCatalog' => db_all($pdo, 'SELECT * FROM medication_catalog ORDER BY generic_name LIMIT ?', [$limit]),
'researchStudies' => db_all($pdo, 'SELECT * FROM research_studies ORDER BY category, related_medicine, title LIMIT ?', [$limit]),
];
}
$like = '%' . $search . '%';
$termParams = array_fill(0, 5, $like);
$conditionParams = array_fill(0, 4, $like);
$symptomParams = [...array_fill(0, 8, $like), $search, $search . '%', $like];
$medicineParams = array_fill(0, 10, $like);
$studyParams = array_fill(0, 6, $like);
return [
'query' => $search,
'medicalTerms' => db_all(
$pdo,
"SELECT * FROM medical_terms
WHERE term LIKE ? OR category LIKE ? OR plain_language_definition LIKE ?
OR clinical_context LIKE ? OR source_name LIKE ?
ORDER BY category, term
LIMIT {$limit}",
$termParams
),
'medicalConditions' => db_all(
$pdo,
"SELECT * FROM medical_conditions
WHERE name LIKE ? OR category LIKE ? OR overview LIKE ? OR common_tracking LIKE ?
ORDER BY category, name
LIMIT {$limit}",
$conditionParams
),
'medicalSymptoms' => db_all(
$pdo,
"SELECT * FROM medical_symptoms
WHERE name LIKE ? OR category LIKE ? OR body_system LIKE ?
OR plain_language_description LIKE ? OR common_tracking LIKE ?
OR urgency_note LIKE ? OR source_name LIKE ? OR search_terms LIKE ?
ORDER BY
CASE
WHEN lower(name) = lower(?) THEN 0
WHEN name LIKE ? THEN 1
WHEN search_terms LIKE ? THEN 2
ELSE 3
END,
category, name
LIMIT {$limit}",
$symptomParams
),
'medicationCatalog' => db_all(
$pdo,
"SELECT * FROM medication_catalog
WHERE generic_name LIKE ? OR active_ingredient LIKE ? OR chemical_name LIKE ?
OR brand_names LIKE ? OR generic_brands LIKE ? OR class_name LIKE ?
OR drug_family LIKE ? OR common_use LIKE ? OR tracking_note LIKE ?
OR search_terms LIKE ?
ORDER BY generic_name
LIMIT {$limit}",
$medicineParams
),
'researchStudies' => db_all(
$pdo,
"SELECT * FROM research_studies
WHERE category LIKE ? OR related_medicine LIKE ? OR related_condition LIKE ?
OR title LIKE ? OR study_type LIKE ? OR summary LIKE ?
ORDER BY category, related_medicine, title
LIMIT {$limit}",
$studyParams
),
];
}
function http_json(string $url, int $timeout = 20): ?array
{
$context = stream_context_create([
'http' => [
'method' => 'GET',
'timeout' => $timeout,
'header' => "Accept: application/json\r\nUser-Agent: NeonMedicalTracker/1.0\r\n",
],
]);
$body = @file_get_contents($url, false, $context);
if (($body === false || $body === '') && function_exists('shell_exec')) {
$curl = trim((string) shell_exec('command -v curl 2>/dev/null'));
if ($curl === '' && is_executable('/usr/bin/curl')) {
$curl = '/usr/bin/curl';
}
if ($curl !== '') {
$command = escapeshellarg($curl)
. ' -fsSL --max-time ' . (int) $timeout
. ' -H ' . escapeshellarg('Accept: application/json')
. ' -A ' . escapeshellarg('NeonMedicalTracker/1.0')
. ' ' . escapeshellarg($url)
. ' 2>/dev/null';
$body = shell_exec($command);
}
}
if (!is_string($body) || $body === '') {
return null;
}
$json = json_decode($body, true);
return is_array($json) ? $json : null;
}
function medication_import_row(string $name, string $source, string $brandNames = '', string $className = 'Imported medicine concept'): array
{
$cleanName = preg_replace('/\s+/', ' ', trim($name));
if ($cleanName === '') {
return [];
}
$sourceLabel = match ($source) {
'RxNorm' => 'RxNorm / NLM',
'DailyMed' => 'DailyMed / NLM',
'openFDA' => 'openFDA / FDA',
default => $source,
};
return [
'generic_name' => strtolower($cleanName),
'active_ingredient' => $cleanName,
'chemical_name' => $cleanName,
'brand_names' => $brandNames,
'generic_brands' => $cleanName,
'class_name' => $className,
'drug_family' => 'Imported reference',
'common_use' => "Imported from {$sourceLabel}. Review official labels and clinician guidance before use.",
'tracking_note' => 'Use this entry as a reference/search starting point; verify dose, indication, and safety details with official labeling or a clinician.',
'rxnorm_url' => medication_source_url('https://mor.nlm.nih.gov/RxNav/search?searchBy=String&searchTerm=', $cleanName),
'medlineplus_url' => 'https://medlineplus.gov/druginformation.html',
'pubchem_url' => medication_source_url('https://pubchem.ncbi.nlm.nih.gov/#query=', $cleanName),
'dailymed_url' => medication_source_url('https://dailymed.nlm.nih.gov/dailymed/search.cfm?query=', $cleanName),
'pubmed_url' => medication_source_url('https://pubmed.ncbi.nlm.nih.gov/?term=', $cleanName . ' medication'),
'clinicaltrials_url' => medication_source_url('https://clinicaltrials.gov/search?term=', $cleanName),
'source_name' => $sourceLabel,
'search_terms' => implode(' ', [$cleanName, $brandNames, $className, $sourceLabel]),
];
}
function fetch_rxnorm_medication_rows(int $limit): array
{
$rows = [];
foreach (['IN', 'PIN'] as $tty) {
$json = http_json('https://rxnav.nlm.nih.gov/REST/allconcepts.json?tty=' . $tty);
$concepts = $json['minConceptGroup']['minConcept'] ?? [];
if (!is_array($concepts)) {
continue;
}
foreach ($concepts as $concept) {
if (count($rows) >= $limit) {
break 2;
}
$name = (string) ($concept['name'] ?? '');
$row = medication_import_row($name, 'RxNorm');
if ($row) {
$rows[] = $row;
}
}
}
return $rows;
}
function fetch_openfda_medication_rows(int $limit): array
{
$url = 'https://api.fda.gov/drug/label.json?count=openfda.generic_name.exact&limit=' . $limit;
$json = http_json($url);
$results = $json['results'] ?? [];
if (!is_array($results)) {
return [];
}
$rows = [];
foreach ($results as $result) {
if (count($rows) >= $limit) {
break;
}
$term = (string) ($result['term'] ?? '');
$row = medication_import_row($term, 'openFDA');
if ($row) {
$rows[] = $row;
}
}
return $rows;
}
function fetch_dailymed_medication_rows(int $limit): array
{
$url = 'https://dailymed.nlm.nih.gov/dailymed/services/v2/drugnames.json';
$json = http_json($url);
$data = $json['data'] ?? $json['drugNames'] ?? $json['results'] ?? [];
if (!is_array($data)) {
return [];
}
$rows = [];
foreach ($data as $item) {
if (count($rows) >= $limit) {
break;
}
$name = is_array($item)
? (string) ($item['drug_name'] ?? $item['name'] ?? $item['term'] ?? '')
: (string) $item;
$row = medication_import_row($name, 'DailyMed');
if ($row) {
$rows[] = $row;
}
}
return $rows;
}
function merge_text_values(string $current, string $incoming): string
{
$parts = [];
foreach (explode(',', $current . ',' . $incoming) as $part) {
$value = trim($part);
if ($value === '') {
continue;
}
$parts[strtolower($value)] = $value;
}
return implode(', ', array_values($parts));
}
function upsert_imported_medication_rows(PDO $pdo, array $rows): array
{
$created = app_now();
$select = $pdo->prepare('SELECT * FROM medication_catalog WHERE lower(generic_name) = lower(?) LIMIT 1');
$insert = $pdo->prepare(
"INSERT INTO medication_catalog
(generic_name, active_ingredient, chemical_name, brand_names, generic_brands, class_name, drug_family,
common_use, tracking_note, rxnorm_url, medlineplus_url, pubchem_url, dailymed_url, pubmed_url,
clinicaltrials_url, source_name, search_terms, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
$update = $pdo->prepare(
"UPDATE medication_catalog
SET active_ingredient = ?, chemical_name = ?, brand_names = ?, generic_brands = ?,
class_name = ?, drug_family = ?, common_use = ?, tracking_note = ?, rxnorm_url = ?,
medlineplus_url = ?, pubchem_url = ?, dailymed_url = ?, pubmed_url = ?,
clinicaltrials_url = ?, source_name = ?, search_terms = ?
WHERE id = ?"
);
$seen = [];
$inserted = 0;
$updated = 0;
foreach ($rows as $row) {
if (!$row || empty($row['generic_name'])) {
continue;
}
$key = strtolower((string) $row['generic_name']);
if (isset($seen[$key])) {
continue;
}
$seen[$key] = true;
$select->execute([$row['generic_name']]);
$existing = $select->fetch();
if (!$existing) {
$insert->execute([
$row['generic_name'],
$row['active_ingredient'],
$row['chemical_name'],
$row['brand_names'],
$row['generic_brands'],
$row['class_name'],
$row['drug_family'],
$row['common_use'],
$row['tracking_note'],
$row['rxnorm_url'],
$row['medlineplus_url'],
$row['pubchem_url'],
$row['dailymed_url'],
$row['pubmed_url'],
$row['clinicaltrials_url'],
$row['source_name'],
$row['search_terms'],
$created,
]);
$inserted++;
continue;
}
$next = [];
foreach (['active_ingredient', 'chemical_name', 'class_name', 'drug_family', 'common_use', 'tracking_note', 'rxnorm_url', 'medlineplus_url', 'pubchem_url', 'dailymed_url', 'pubmed_url', 'clinicaltrials_url'] as $field) {
$next[$field] = trim((string) ($existing[$field] ?? '')) !== '' ? $existing[$field] : $row[$field];
}
$next['brand_names'] = merge_text_values((string) ($existing['brand_names'] ?? ''), (string) $row['brand_names']);
$next['generic_brands'] = merge_text_values((string) ($existing['generic_brands'] ?? ''), (string) $row['generic_brands']);
$next['source_name'] = merge_text_values((string) ($existing['source_name'] ?? ''), (string) $row['source_name']);
$next['search_terms'] = trim((string) ($existing['search_terms'] ?? '') . ' ' . (string) $row['search_terms']);
$update->execute([
$next['active_ingredient'],
$next['chemical_name'],
$next['brand_names'],
$next['generic_brands'],
$next['class_name'],
$next['drug_family'],
$next['common_use'],
$next['tracking_note'],
$next['rxnorm_url'],
$next['medlineplus_url'],
$next['pubchem_url'],
$next['dailymed_url'],
$next['pubmed_url'],
$next['clinicaltrials_url'],
$next['source_name'],
$next['search_terms'],
(int) $existing['id'],
]);
$updated++;
}
return ['inserted' => $inserted, 'updated' => $updated, 'seen' => count($seen)];
}
function record_medication_source_update(PDO $pdo, string $source, string $status, int $inserted, int $updated, string $message): void
{
$stmt = $pdo->prepare(
"INSERT INTO medication_source_updates
(source, status, imported_count, updated_count, message, created_at)
VALUES (?, ?, ?, ?, ?, ?)"
);
$stmt->execute([$source, $status, $inserted, $updated, $message, app_now()]);
}
function refresh_medication_sources(array $data): array
{
$pdo = app_db();
if (array_key_exists('acting_user_id', $data) || array_key_exists('user_id', $data)) {
require_admin($pdo, $data);
}
$limit = max(25, min(5000, int_value($data, 'limit', 1000)));
$sources = [
'RxNorm' => 'fetch_rxnorm_medication_rows',
'DailyMed' => 'fetch_dailymed_medication_rows',
'openFDA' => 'fetch_openfda_medication_rows',
];
$requestedSource = text_value($data, 'source', 'all');
if ($requestedSource !== 'all') {
$sources = array_intersect_key($sources, [$requestedSource => true]);
}
$summary = [];
$totalInserted = 0;
$totalUpdated = 0;
foreach ($sources as $source => $fetcher) {
try {
$rows = $fetcher($limit);
if (!$rows) {
record_medication_source_update($pdo, $source, 'empty', 0, 0, 'No rows returned. The source may be unavailable or rate-limited.');
$summary[] = ['source' => $source, 'status' => 'empty', 'inserted' => 0, 'updated' => 0, 'message' => 'No rows returned.'];
continue;
}
$result = upsert_imported_medication_rows($pdo, $rows);
$totalInserted += $result['inserted'];
$totalUpdated += $result['updated'];
$message = "Fetched {$result['seen']} unique rows.";
record_medication_source_update($pdo, $source, 'ok', $result['inserted'], $result['updated'], $message);
$summary[] = ['source' => $source, 'status' => 'ok', 'inserted' => $result['inserted'], 'updated' => $result['updated'], 'message' => $message];
} catch (Throwable $exception) {
record_medication_source_update($pdo, $source, 'error', 0, 0, $exception->getMessage());
$summary[] = ['source' => $source, 'status' => 'error', 'inserted' => 0, 'updated' => 0, 'message' => $exception->getMessage()];
}
}
return [
'inserted' => $totalInserted,
'updated' => $totalUpdated,
'catalog_count' => (int) db_value($pdo, 'SELECT COUNT(*) FROM medication_catalog'),
'sources' => $summary,
'recent_updates' => db_all($pdo, 'SELECT * FROM medication_source_updates ORDER BY created_at DESC, id DESC LIMIT 10'),
];
}
function create_user(array $data): array
{
$pdo = app_db();
$name = text_value($data, 'name');
if ($name === '') {
throw new InvalidArgumentException('Profile name is required.');
}
$userCount = (int) db_value($pdo, 'SELECT COUNT(*) FROM users');
if ($userCount > 0) {
require_admin($pdo, $data);
}
$role = $userCount === 0
? 'admin'
: clamp_choice(text_value($data, 'role', 'member'), ['admin', 'member'], 'member');
$status = clamp_choice(text_value($data, 'status', 'active'), ['active', 'inactive'], 'active');
$mode = clamp_choice(text_value($data, 'theme_mode', 'dark'), ['dark', 'light'], 'dark');
$accent = clamp_choice(text_value($data, 'theme_accent', 'green'), ['green', 'blue', 'red', 'pink', 'orange'], 'green');
$stmt = $pdo->prepare(
"INSERT INTO users
(name, email, fitness_focus, role, status, phone, birthday, height_in, training_level,
emergency_contact, theme_mode, theme_accent, updated_at, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
$stmt->execute([
$name,
text_value($data, 'email'),
text_value($data, 'fitness_focus'),
$role,
$status,
text_value($data, 'phone'),
text_value($data, 'birthday'),
number_value($data, 'height_in'),
text_value($data, 'training_level'),
text_value($data, 'emergency_contact'),
$mode,
$accent,
app_now(),
app_now(),
]);
$userId = (int) $pdo->lastInsertId();
$goalStmt = $pdo->prepare(
"INSERT INTO nutrition_goals
(user_id, label, scope, calories, protein_g, carbs_g, fat_g, water_ml, start_date, created_at)
VALUES (?, 'Starter day', 'daily', 2400, 150, 260, 75, 3000, ?, ?)"
);
$goalStmt->execute([$userId, app_date(new DateTimeImmutable('today')), app_now()]);
return ['user' => db_one($pdo, 'SELECT * FROM users WHERE id = ?', [$userId])];
}
function update_user(array $data): array
{
$pdo = app_db();
require_admin($pdo, $data);
$targetUserId = int_value($data, 'target_user_id');
if ($targetUserId <= 0) {
throw new InvalidArgumentException('Target profile is required.');
}
$current = db_one($pdo, 'SELECT * FROM users WHERE id = ?', [$targetUserId]);
if (!$current) {
throw new InvalidArgumentException('Profile not found.');
}
$nextRole = array_key_exists('role', $data)
? clamp_choice(text_value($data, 'role', $current['role']), ['admin', 'member'], $current['role'])
: $current['role'];
$nextStatus = array_key_exists('status', $data)
? clamp_choice(text_value($data, 'status', $current['status']), ['active', 'inactive'], $current['status'])
: $current['status'];
last_admin_guard($pdo, $targetUserId, $nextRole, $nextStatus);
$fields = [
'name' => array_key_exists('name', $data) ? text_value($data, 'name', $current['name']) : $current['name'],
'email' => array_key_exists('email', $data) ? text_value($data, 'email') : $current['email'],
'fitness_focus' => array_key_exists('fitness_focus', $data) ? text_value($data, 'fitness_focus') : $current['fitness_focus'],
'role' => $nextRole,
'status' => $nextStatus,
'phone' => array_key_exists('phone', $data) ? text_value($data, 'phone') : $current['phone'],
'birthday' => array_key_exists('birthday', $data) ? text_value($data, 'birthday') : $current['birthday'],
'height_in' => array_key_exists('height_in', $data) ? number_value($data, 'height_in') : (float) $current['height_in'],
'training_level' => array_key_exists('training_level', $data) ? text_value($data, 'training_level') : $current['training_level'],
'emergency_contact' => array_key_exists('emergency_contact', $data) ? text_value($data, 'emergency_contact') : $current['emergency_contact'],
'theme_mode' => array_key_exists('theme_mode', $data)
? clamp_choice(text_value($data, 'theme_mode', $current['theme_mode']), ['dark', 'light'], $current['theme_mode'])
: $current['theme_mode'],
'theme_accent' => array_key_exists('theme_accent', $data)
? clamp_choice(text_value($data, 'theme_accent', $current['theme_accent']), ['green', 'blue', 'red', 'pink', 'orange'], $current['theme_accent'])
: $current['theme_accent'],
];
if ($fields['name'] === '') {
throw new InvalidArgumentException('Profile name is required.');
}
$stmt = $pdo->prepare(
"UPDATE users
SET name = ?, email = ?, fitness_focus = ?, role = ?, status = ?, phone = ?, birthday = ?,
height_in = ?, training_level = ?, emergency_contact = ?, theme_mode = ?, theme_accent = ?, updated_at = ?
WHERE id = ?"
);
$stmt->execute([
$fields['name'],
$fields['email'],
$fields['fitness_focus'],
$fields['role'],
$fields['status'],
$fields['phone'],
$fields['birthday'],
$fields['height_in'],
$fields['training_level'],
$fields['emergency_contact'],
$fields['theme_mode'],
$fields['theme_accent'],
app_now(),
$targetUserId,
]);
return ['user' => db_one($pdo, 'SELECT * FROM users WHERE id = ?', [$targetUserId])];
}
function delete_user(array $data): array
{
$pdo = app_db();
$actingUserId = require_admin($pdo, $data);
$targetUserId = int_value($data, 'target_user_id');
if ($targetUserId <= 0) {
throw new InvalidArgumentException('Target profile is required.');
}
if ($targetUserId === $actingUserId) {
throw new InvalidArgumentException('You cannot delete the profile you are currently using.');
}
$target = db_one($pdo, 'SELECT * FROM users WHERE id = ?', [$targetUserId]);
if (!$target) {
throw new InvalidArgumentException('Profile not found.');
}
last_admin_guard($pdo, $targetUserId, 'member', 'inactive');
$stmt = $pdo->prepare('DELETE FROM users WHERE id = ?');
$stmt->execute([$targetUserId]);
return ['deleted' => true, 'target_user_id' => $targetUserId];
}
function update_theme(array $data): array
{
$pdo = app_db();
$userId = user_id_value($data);
$mode = clamp_choice(text_value($data, 'theme_mode', 'dark'), ['dark', 'light'], 'dark');
$accent = clamp_choice(text_value($data, 'theme_accent', 'green'), ['green', 'blue', 'red', 'pink', 'orange'], 'green');
$stmt = $pdo->prepare('UPDATE users SET theme_mode = ?, theme_accent = ? WHERE id = ?');
$stmt->execute([$mode, $accent, $userId]);
return ['user' => db_one($pdo, 'SELECT * FROM users WHERE id = ?', [$userId])];
}
function create_exercise(array $data): array
{
$pdo = app_db();
$userId = user_id_value($data);
$name = text_value($data, 'name');
if ($name === '') {
throw new InvalidArgumentException('Exercise name is required.');
}
$stmt = $pdo->prepare(
"INSERT INTO exercises (user_id, name, category, muscle_group, default_unit, notes, is_builtin, created_at)
VALUES (?, ?, ?, ?, ?, ?, 0, ?)"
);
$stmt->execute([
$userId,
$name,
text_value($data, 'category', 'Custom'),
text_value($data, 'muscle_group'),
text_value($data, 'default_unit'),
text_value($data, 'notes'),
app_now(),
]);
return ['exercise' => db_one($pdo, 'SELECT * FROM exercises WHERE id = ?', [(int) $pdo->lastInsertId()])];
}
function create_supplement(array $data): array
{
$pdo = app_db();
$userId = user_id_value($data);
$name = text_value($data, 'name');
if ($name === '') {
throw new InvalidArgumentException('Supplement name is required.');
}
$stmt = $pdo->prepare(
"INSERT INTO supplements (user_id, name, timing, default_dose, purpose, notes, is_builtin, created_at)
VALUES (?, ?, ?, ?, ?, ?, 0, ?)"
);
$stmt->execute([
$userId,
$name,
text_value($data, 'timing', 'Daily'),
text_value($data, 'default_dose'),
text_value($data, 'purpose'),
text_value($data, 'notes'),
app_now(),
]);
return ['supplement' => db_one($pdo, 'SELECT * FROM supplements WHERE id = ?', [(int) $pdo->lastInsertId()])];
}
function create_nutrition_goal(array $data): array
{
$pdo = app_db();
$userId = user_id_value($data);
$scope = clamp_choice(text_value($data, 'scope', 'daily'), ['daily', 'weekly', 'monthly'], 'daily');
$label = text_value($data, 'label', ucfirst($scope) . ' target');
$stmt = $pdo->prepare(
"INSERT INTO nutrition_goals
(user_id, label, scope, calories, protein_g, carbs_g, fat_g, water_ml, start_date, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
$stmt->execute([
$userId,
$label,
$scope,
number_value($data, 'calories'),
number_value($data, 'protein_g'),
number_value($data, 'carbs_g'),
number_value($data, 'fat_g'),
number_value($data, 'water_ml'),
app_date(parse_app_date(text_value($data, 'start_date'))),
app_now(),
]);
return ['nutritionGoal' => db_one($pdo, 'SELECT * FROM nutrition_goals WHERE id = ?', [(int) $pdo->lastInsertId()])];
}
function create_routine(array $data): array
{
$pdo = app_db();
$userId = user_id_value($data);
$name = text_value($data, 'name');
if ($name === '') {
throw new InvalidArgumentException('Routine name is required.');
}
$scope = clamp_choice(text_value($data, 'planner_scope', 'weekly'), ['daily', 'weekly', 'monthly'], 'weekly');
$goalId = int_value($data, 'nutrition_goal_id') ?: null;
$stmt = $pdo->prepare(
"INSERT INTO routines
(user_id, nutrition_goal_id, name, workout_type, planner_scope, start_date, end_date, intensity, notes, active, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)"
);
$stmt->execute([
$userId,
$goalId,
$name,
text_value($data, 'workout_type', 'Hybrid'),
$scope,
app_date(parse_app_date(text_value($data, 'start_date'))),
text_value($data, 'end_date'),
text_value($data, 'intensity'),
text_value($data, 'notes'),
app_now(),
]);
return ['routine' => db_one($pdo, 'SELECT * FROM routines WHERE id = ?', [(int) $pdo->lastInsertId()])];
}
function create_workout(array $data): array
{
$pdo = app_db();
$userId = user_id_value($data);
$title = text_value($data, 'title');
if ($title === '') {
throw new InvalidArgumentException('Workout title is required.');
}
$status = clamp_choice(text_value($data, 'status', 'planned'), ['planned', 'complete', 'skipped'], 'planned');
$stmt = $pdo->prepare(
"INSERT INTO workouts
(user_id, routine_id, exercise_id, scheduled_on, title, workout_type, sets, reps, weight_lbs,
duration_min, distance_mi, intensity, status, notes, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
$stmt->execute([
$userId,
int_value($data, 'routine_id') ?: null,
int_value($data, 'exercise_id') ?: null,
app_date(parse_app_date(text_value($data, 'scheduled_on'))),
$title,
text_value($data, 'workout_type', 'Weight lifting'),
int_value($data, 'sets'),
int_value($data, 'reps'),
number_value($data, 'weight_lbs'),
number_value($data, 'duration_min'),
number_value($data, 'distance_mi'),
text_value($data, 'intensity'),
$status,
text_value($data, 'notes'),
app_now(),
]);
return ['workout' => db_one($pdo, 'SELECT * FROM workouts WHERE id = ?', [(int) $pdo->lastInsertId()])];
}
function update_workout(array $data): array
{
$pdo = app_db();
$workoutId = int_value($data, 'id');
if ($workoutId <= 0) {
throw new InvalidArgumentException('Workout id is required.');
}
$allowed = [
'title' => 'text',
'workout_type' => 'text',
'intensity' => 'text',
'notes' => 'text',
'scheduled_on' => 'date',
'status' => 'status',
'sets' => 'int',
'reps' => 'int',
'weight_lbs' => 'float',
'duration_min' => 'float',
'distance_mi' => 'float',
];
$fields = [];
$values = [];
foreach ($allowed as $field => $type) {
if (!array_key_exists($field, $data)) {
continue;
}
$fields[] = $field . ' = ?';
if ($type === 'date') {
$values[] = app_date(parse_app_date(text_value($data, $field)));
} elseif ($type === 'status') {
$values[] = clamp_choice(text_value($data, $field, 'planned'), ['planned', 'complete', 'skipped'], 'planned');
} elseif ($type === 'int') {
$values[] = int_value($data, $field);
} elseif ($type === 'float') {
$values[] = number_value($data, $field);
} else {
$values[] = text_value($data, $field);
}
}
if (!$fields) {
throw new InvalidArgumentException('No workout updates supplied.');
}
$values[] = $workoutId;
$stmt = $pdo->prepare('UPDATE workouts SET ' . implode(', ', $fields) . ' WHERE id = ?');
$stmt->execute($values);
return ['workout' => db_one($pdo, 'SELECT * FROM workouts WHERE id = ?', [$workoutId])];
}
function create_nutrition_log(array $data): array
{
$pdo = app_db();
$userId = user_id_value($data);
$stmt = $pdo->prepare(
"INSERT INTO nutrition_logs
(user_id, logged_on, calories, protein_g, carbs_g, fat_g, water_ml, notes, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
$stmt->execute([
$userId,
app_date(parse_app_date(text_value($data, 'logged_on'))),
number_value($data, 'calories'),
number_value($data, 'protein_g'),
number_value($data, 'carbs_g'),
number_value($data, 'fat_g'),
number_value($data, 'water_ml'),
text_value($data, 'notes'),
app_now(),
]);
return ['nutritionLog' => db_one($pdo, 'SELECT * FROM nutrition_logs WHERE id = ?', [(int) $pdo->lastInsertId()])];
}
function create_supplement_log(array $data): array
{
$pdo = app_db();
$userId = user_id_value($data);
$status = clamp_choice(text_value($data, 'status', 'planned'), ['planned', 'taken', 'skipped'], 'planned');
$stmt = $pdo->prepare(
"INSERT INTO supplement_logs
(user_id, supplement_id, taken_on, timing, dose, status, notes, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
);
$stmt->execute([
$userId,
int_value($data, 'supplement_id') ?: null,
app_date(parse_app_date(text_value($data, 'taken_on'))),
text_value($data, 'timing', 'Daily'),
text_value($data, 'dose'),
$status,
text_value($data, 'notes'),
app_now(),
]);
return ['supplementLog' => db_one($pdo, 'SELECT * FROM supplement_logs WHERE id = ?', [(int) $pdo->lastInsertId()])];
}
function create_body_metric(array $data): array
{
$pdo = app_db();
$userId = user_id_value($data);
$stmt = $pdo->prepare(
"INSERT INTO body_metrics
(user_id, measured_on, weight_lbs, body_fat_pct, sleep_hours, resting_hr, mood, notes, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
$stmt->execute([
$userId,
app_date(parse_app_date(text_value($data, 'measured_on'))),
number_value($data, 'weight_lbs'),
number_value($data, 'body_fat_pct'),
number_value($data, 'sleep_hours'),
int_value($data, 'resting_hr'),
text_value($data, 'mood'),
text_value($data, 'notes'),
app_now(),
]);
return ['bodyMetric' => db_one($pdo, 'SELECT * FROM body_metrics WHERE id = ?', [(int) $pdo->lastInsertId()])];
}
function create_medication_log(array $data): array
{
$pdo = app_db();
$userId = user_id_value($data);
$medicationId = int_value($data, 'medication_id') ?: null;
$catalog = $medicationId ? db_one($pdo, 'SELECT * FROM medication_catalog WHERE id = ?', [$medicationId]) : null;
$medicationName = text_value($data, 'medication_name', $catalog['generic_name'] ?? '');
if ($medicationName === '') {
throw new InvalidArgumentException('Medication name is required.');
}
$status = clamp_choice(text_value($data, 'status', 'planned'), ['planned', 'taken', 'skipped', 'stopped'], 'planned');
$stmt = $pdo->prepare(
"INSERT INTO medication_logs
(user_id, medication_id, taken_on, medication_name, dose, route, frequency, status, prescriber, notes, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
$stmt->execute([
$userId,
$medicationId,
app_date(parse_app_date(text_value($data, 'taken_on'))),
$medicationName,
text_value($data, 'dose'),
text_value($data, 'route'),
text_value($data, 'frequency'),
$status,
text_value($data, 'prescriber'),
text_value($data, 'notes'),
app_now(),
]);
return ['medicationLog' => db_one($pdo, 'SELECT * FROM medication_logs WHERE id = ?', [(int) $pdo->lastInsertId()])];
}
function create_vital_log(array $data): array
{
$pdo = app_db();
$userId = user_id_value($data);
$painScore = max(0, min(10, int_value($data, 'pain_score')));
$stmt = $pdo->prepare(
"INSERT INTO vital_logs
(user_id, measured_on, systolic, diastolic, pulse, temperature_f, oxygen_pct,
glucose_mg_dl, pain_score, symptoms, notes, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
$stmt->execute([
$userId,
app_date(parse_app_date(text_value($data, 'measured_on'))),
int_value($data, 'systolic'),
int_value($data, 'diastolic'),
int_value($data, 'pulse'),
number_value($data, 'temperature_f'),
number_value($data, 'oxygen_pct'),
number_value($data, 'glucose_mg_dl'),
$painScore,
text_value($data, 'symptoms'),
text_value($data, 'notes'),
app_now(),
]);
return ['vitalLog' => db_one($pdo, 'SELECT * FROM vital_logs WHERE id = ?', [(int) $pdo->lastInsertId()])];
}
function create_lab_result(array $data): array
{
$pdo = app_db();
$userId = user_id_value($data);
$testName = text_value($data, 'test_name');
$resultValue = text_value($data, 'result_value');
if ($testName === '' || $resultValue === '') {
throw new InvalidArgumentException('Lab test name and result are required.');
}
$stmt = $pdo->prepare(
"INSERT INTO lab_results
(user_id, collected_on, test_name, result_value, unit, reference_range, ordering_clinician, notes, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
$stmt->execute([
$userId,
app_date(parse_app_date(text_value($data, 'collected_on'))),
$testName,
$resultValue,
text_value($data, 'unit'),
text_value($data, 'reference_range'),
text_value($data, 'ordering_clinician'),
text_value($data, 'notes'),
app_now(),
]);
return ['labResult' => db_one($pdo, 'SELECT * FROM lab_results WHERE id = ?', [(int) $pdo->lastInsertId()])];
}
function create_symptom_log(array $data): array
{
$pdo = app_db();
$userId = user_id_value($data);
$symptom = text_value($data, 'symptom');
if ($symptom === '') {
throw new InvalidArgumentException('Symptom is required.');
}
$severity = max(0, min(10, int_value($data, 'severity')));
$stmt = $pdo->prepare(
"INSERT INTO symptom_logs
(user_id, observed_on, symptom, body_area, severity, duration, triggers, action_taken, notes, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
$stmt->execute([
$userId,
app_date(parse_app_date(text_value($data, 'observed_on'))),
$symptom,
text_value($data, 'body_area'),
$severity,
text_value($data, 'duration'),
text_value($data, 'triggers'),
text_value($data, 'action_taken'),
text_value($data, 'notes'),
app_now(),
]);
return ['symptomLog' => db_one($pdo, 'SELECT * FROM symptom_logs WHERE id = ?', [(int) $pdo->lastInsertId()])];
}
function create_allergy_record(array $data): array
{
$pdo = app_db();
$userId = user_id_value($data);
$allergen = text_value($data, 'allergen');
if ($allergen === '') {
throw new InvalidArgumentException('Allergen is required.');
}
$severity = clamp_choice(text_value($data, 'severity', 'unknown'), ['unknown', 'mild', 'moderate', 'severe', 'life-threatening'], 'unknown');
$status = clamp_choice(text_value($data, 'status', 'active'), ['active', 'resolved'], 'active');
$firstNoted = text_value($data, 'first_noted_on');
$stmt = $pdo->prepare(
"INSERT INTO allergy_records
(user_id, allergen, reaction, severity, first_noted_on, status, notes, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
);
$stmt->execute([
$userId,
$allergen,
text_value($data, 'reaction'),
$severity,
$firstNoted === '' ? '' : app_date(parse_app_date($firstNoted)),
$status,
text_value($data, 'notes'),
app_now(),
]);
return ['allergyRecord' => db_one($pdo, 'SELECT * FROM allergy_records WHERE id = ?', [(int) $pdo->lastInsertId()])];
}
function create_appointment(array $data): array
{
$pdo = app_db();
$userId = user_id_value($data);
$reason = text_value($data, 'reason');
if ($reason === '') {
throw new InvalidArgumentException('Appointment reason is required.');
}
$status = clamp_choice(text_value($data, 'status', 'scheduled'), ['scheduled', 'completed', 'canceled'], 'scheduled');
$stmt = $pdo->prepare(
"INSERT INTO appointments
(user_id, appointment_on, clinician, specialty, location, reason, status, follow_up_notes, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
$stmt->execute([
$userId,
app_date(parse_app_date(text_value($data, 'appointment_on'))),
text_value($data, 'clinician'),
text_value($data, 'specialty'),
text_value($data, 'location'),
$reason,
$status,
text_value($data, 'follow_up_notes'),
app_now(),
]);
return ['appointment' => db_one($pdo, 'SELECT * FROM appointments WHERE id = ?', [(int) $pdo->lastInsertId()])];
}
function create_immunization_record(array $data): array
{
$pdo = app_db();
$userId = user_id_value($data);
$vaccineName = text_value($data, 'vaccine_name');
if ($vaccineName === '') {
throw new InvalidArgumentException('Vaccine name is required.');
}
$nextDue = text_value($data, 'next_due_on');
$stmt = $pdo->prepare(
"INSERT INTO immunization_records
(user_id, vaccine_name, administered_on, dose, lot_number, clinician_or_site, next_due_on, notes, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
$stmt->execute([
$userId,
$vaccineName,
app_date(parse_app_date(text_value($data, 'administered_on'))),
text_value($data, 'dose'),
text_value($data, 'lot_number'),
text_value($data, 'clinician_or_site'),
$nextDue === '' ? '' : app_date(parse_app_date($nextDue)),
text_value($data, 'notes'),
app_now(),
]);
return ['immunizationRecord' => db_one($pdo, 'SELECT * FROM immunization_records WHERE id = ?', [(int) $pdo->lastInsertId()])];
}
function json_input(): array
{
$raw = file_get_contents('php://input');
if (!$raw) {
return [];
}
$data = json_decode($raw, true);
if (!is_array($data)) {
throw new InvalidArgumentException('Invalid JSON payload.');
}
return $data;
}
function json_response(array $payload, int $status = 200): void
{
http_response_code($status);
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
echo json_encode($payload, JSON_THROW_ON_ERROR);
}