diff --git a/CHANGELOG.md b/CHANGELOG.md index b7c2e48..fd71c4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,46 @@ All notable changes to Neon Medical Tracker are documented here. +## [Unreleased] - 2026-07-03 + +### Added + +- Added generated medicine-to-condition and medicine-to-symptom relationship tables. +- Added dynamic medicine detail sections for linked conditions and linked symptom contexts. +- Added dynamic condition and symptom sections for commonly linked medicines. +- Added therapeutic-use enrichment rules so imported medicine rows can show practical common-use context when a clear category match exists. +- Added expanded condition and symptom seed coverage across cardiovascular, respiratory, endocrine, GI, kidney, neurologic, mental health, dermatology, infectious disease, immune, ENT, reproductive, and general medical topics. +- Added openFDA brand-name and substance-name buckets to the medicine source updater. +- Added official medicine-use fields and source links populated from openFDA drug label `indications_and_usage` or `purpose` text. +- Added computed `used_for` values to medicine API payloads so clients can display official label use text when available. + +### Changed + +- Increased the current SQLite snapshot to 7,646 medicines, 283 conditions, and 308 symptoms, including 200 openFDA label-derived use summaries. +- Rebuilt generated relationship data to 8,077 medicine-to-condition links and 10,590 medicine-to-symptom links. +- Updated medical library search so medicine searches can match linked conditions/symptoms, and condition/symptom searches can match linked medicines. +- Updated medicine catalog, medicine detail, condition, symptom, terminology, and combined-library pages to show "what it is used for" text through one shared helper. +- Updated relationship generation so official medicine-use label text participates in condition and symptom linking. +- Updated openFDA refreshes to fetch label documents first, then fall back to generic, brand, and substance buckets for broader coverage. +- Batched imported medicine upserts inside a SQLite transaction for faster source refreshes. +- Increased SQLite busy timeout for safer first-run migrations and source refreshes. +- Updated `README.md` with current counts, relationship data, and source-refresh behavior. + +### Fixed + +- Cleaned generated search terms when therapeutic relationship rules change so removed links do not linger in search. +- Tightened relationship rules to avoid broad steroid and chemistry keyword false positives. +- Fixed medicine pages that previously depended on `common_use` only, so medicines now show official "used for" text whenever it exists. + +### Verified + +- Refreshed public medicine sources with a 500-row-per-source run across RxNorm, DailyMed, and openFDA. +- Refreshed openFDA label-use data with a 200-row run and verified 200 medicine rows have official use summaries. +- Verified `/medicine.php?name=albuterol`, `/condition.php?name=Asthma`, `/symptom.php?name=Wheezing`, and `api.php?action=library_search&q=asthma&limit=10` return `200`. +- Verified official-use medicine pages expose "What It Is Used For", `Used for:` label text, and an openFDA drug label source link. +- Verified albuterol medicine pages show linked Asthma/Wheezing records and Asthma/Wheezing pages show linked albuterol medicines. +- Verified stale adalimumab-to-asthma and celecoxib-to-UTI false positives are removed. + ## [Unreleased] - 2026-06-30 ### Added @@ -58,7 +98,7 @@ All notable changes to Neon Medical Tracker are documented here. - Verified dynamic medicine, symptom, condition, and terminology routes return `200` for representative records. - Verified legacy `/signup.php` returns `302` to `/tracker.php`. - Verified `api.php?action=public_signup` returns `404`. -- Verified current SQLite snapshot counts: 6,002 medicines, 242 symptoms, 203 conditions, 23 terms, and 9 research-link categories. +- Verified then-current SQLite snapshot counts: 6,002 medicines, 242 symptoms, 203 conditions, 23 terms, and 9 research-link categories. - Verified browser layout checks for medicine, symptom, and terminology pages at desktop and mobile widths with no horizontal overflow. ## Earlier Scope diff --git a/README.md b/README.md index 39ece18..047ff8d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Neon Medical Tracker -A dependency-free PHP, HTML5, JavaScript, and SQLite web app for multi-user medical tracking, searchable medical reference data, medication catalog lookup, terminology, symptoms, conditions, NIH/NLM study links, and admin-managed profiles. +A dependency-free PHP, HTML5, JavaScript, and SQLite web app for multi-user medical tracking, searchable medical reference data, medication catalog lookup, common-use and official-label medicine context, terminology, symptoms, conditions, NIH/NLM study links, and admin-managed profiles. This app is a personal tracking and reference tool. It is not medical advice, diagnosis, treatment guidance, emergency guidance, or a substitute for a clinician or pharmacist. @@ -42,11 +42,14 @@ data/health_tracker.sqlite The current SQLite snapshot includes: -- 6,002 medicine catalog records. -- 242 symptom reference records. -- 203 condition reference records. +- 7,646 medicine catalog records. +- 200 openFDA label-derived medicine use summaries. +- 308 symptom reference records. +- 283 condition reference records. - 23 medical terminology records. - 9 NIH/NLM research-link categories. +- 8,077 medicine-to-condition links. +- 10,590 medicine-to-symptom links. The database also stores user profiles, medication logs, vitals, lab results, symptom logs, allergies, appointments, immunizations, optional legacy workout/nutrition/supplement data, theme preferences, and admin state. @@ -57,10 +60,11 @@ The database also stores user profiles, medication logs, vitals, lab results, sy - Admin dashboard for creating, editing, activating/deactivating, and deleting profiles. - Admin medicine-source updater for refreshing catalog data from RxNorm, DailyMed, and openFDA. - Profile-scoped tracker for medication logs, vitals, lab results, symptoms, allergies, appointments, immunizations, notes, and older wellness data. -- Searchable medicine catalog by generic name, active ingredient, chemical name, common brand names, generic/alternate names, class, family, and source terms. -- Dynamic medicine pages with active ingredient, chemical name, brand names, tracking notes, RxNorm, DailyMed, PubChem, MedlinePlus, PubMed, and ClinicalTrials.gov links. +- Searchable medicine catalog by generic name, active ingredient, chemical name, common brand names, generic/alternate names, class, family, what the medicine is used for, linked conditions, linked symptoms, and source terms. +- Dynamic medicine pages with active ingredient, chemical name, brand names, what the medicine is used for, openFDA label-use source links when available, linked conditions, linked symptom contexts, tracking notes, RxNorm, DailyMed, PubChem, MedlinePlus, PubMed, and ClinicalTrials.gov links. - Searchable symptom database with body-system categories, tracking notes, urgency-context notes, MedlinePlus links, and dynamic detail pages. - Searchable condition database with category summaries, tracking notes, MedlinePlus links, and dynamic detail pages. +- Dynamic condition and symptom pages with commonly linked medicines generated from therapeutic-use categories. - Medical terminology database with plain-language definitions, clinical context, source links, and dynamic detail pages. - Combined medical library search across symptoms, conditions, terminology, medicines, and research links. - NIH/NLM study-link categories related to listed medicines and conditions. @@ -68,7 +72,7 @@ The database also stores user profiles, medication logs, vitals, lab results, sy ## Medicine Catalog Updates -The medicine catalog ships with a curated starter set and the current SQLite snapshot includes thousands of imported medicine concepts. It can be refreshed from public sources. +The medicine catalog ships with a curated starter set and the current SQLite snapshot includes thousands of imported medicine concepts. It can be refreshed from public sources, then re-linked to conditions and symptom contexts. From the admin dashboard, use the medicine source updater and choose `All sources`, `RxNorm`, `DailyMed`, or `openFDA`. @@ -80,6 +84,8 @@ frankenphp php-cli scripts/update_medicine_catalog.php 1000 all For periodic maintenance, run the same command nightly or weekly. The first argument is the max rows per source, from `25` to `5000`; the second argument is `all`, `RxNorm`, `DailyMed`, or `openFDA`. +The updater imports RxNorm ingredient concepts, DailyMed drug names, and openFDA label records. openFDA rows prefer official label `indications_and_usage` text and fall back to label `purpose` text when available, then continue filling generic, brand, and substance buckets for broader catalog coverage. Imported rows are also enriched with rule-based common-use context when the medicine name, class, family, official use text, or source terms clearly match a therapeutic category. + ## API Main API endpoint: @@ -108,6 +114,7 @@ The old `public_signup` action has been removed. - PubMed research search: `https://pubmed.ncbi.nlm.nih.gov/` - ClinicalTrials.gov study search: `https://clinicaltrials.gov/` - openFDA drug label data: `https://open.fda.gov/apis/drug/label/` +- openFDA drug label searchable fields, including `indications_and_usage` and `purpose`: `https://open.fda.gov/apis/drug/label/searchable-fields/` ## Notes diff --git a/app/bootstrap.php b/app/bootstrap.php index afed7bd..5707043 100644 --- a/app/bootstrap.php +++ b/app/bootstrap.php @@ -23,7 +23,7 @@ function app_db(): PDO $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'); + $pdo->exec('PRAGMA busy_timeout = 30000'); app_init_db($pdo); @@ -207,6 +207,9 @@ CREATE TABLE IF NOT EXISTS medication_catalog ( class_name TEXT NOT NULL, drug_family TEXT DEFAULT '', common_use TEXT NOT NULL, + official_use TEXT DEFAULT '', + official_use_source_name TEXT DEFAULT '', + official_use_source_url TEXT DEFAULT '', tracking_note TEXT DEFAULT '', rxnorm_url TEXT NOT NULL, medlineplus_url TEXT DEFAULT '', @@ -330,6 +333,30 @@ CREATE TABLE IF NOT EXISTS research_studies ( created_at TEXT NOT NULL ); +CREATE TABLE IF NOT EXISTS medication_condition_links ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + medication_id INTEGER NOT NULL REFERENCES medication_catalog(id) ON DELETE CASCADE, + condition_id INTEGER NOT NULL REFERENCES medical_conditions(id) ON DELETE CASCADE, + relationship TEXT NOT NULL DEFAULT 'common use', + note TEXT DEFAULT '', + source_name TEXT NOT NULL, + source_url TEXT NOT NULL, + created_at TEXT NOT NULL, + UNIQUE(medication_id, condition_id, relationship) +); + +CREATE TABLE IF NOT EXISTS medication_symptom_links ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + medication_id INTEGER NOT NULL REFERENCES medication_catalog(id) ON DELETE CASCADE, + symptom_id INTEGER NOT NULL REFERENCES medical_symptoms(id) ON DELETE CASCADE, + relationship TEXT NOT NULL DEFAULT 'symptom context', + note TEXT DEFAULT '', + source_name TEXT NOT NULL, + source_url TEXT NOT NULL, + created_at TEXT NOT NULL, + UNIQUE(medication_id, symptom_id, relationship) +); + CREATE TABLE IF NOT EXISTS medication_source_updates ( id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL, @@ -340,6 +367,12 @@ CREATE TABLE IF NOT EXISTS medication_source_updates ( created_at TEXT NOT NULL ); +CREATE TABLE IF NOT EXISTS app_metadata ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updated_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); @@ -355,6 +388,10 @@ 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_condition_links_medication ON medication_condition_links(medication_id); +CREATE INDEX IF NOT EXISTS idx_medication_condition_links_condition ON medication_condition_links(condition_id); +CREATE INDEX IF NOT EXISTS idx_medication_symptom_links_medication ON medication_symptom_links(medication_id); +CREATE INDEX IF NOT EXISTS idx_medication_symptom_links_symptom ON medication_symptom_links(symptom_id); CREATE INDEX IF NOT EXISTS idx_medication_source_updates_created ON medication_source_updates(created_at); SQL); @@ -362,6 +399,7 @@ SQL); migrate_medication_catalog_table($pdo); seed_libraries($pdo); seed_medical_library($pdo); + seed_medical_relationships($pdo); seed_demo_data($pdo); seed_medical_demo_data($pdo); ensure_first_user_is_admin($pdo); @@ -404,6 +442,9 @@ function migrate_medication_catalog_table(PDO $pdo): void 'pubchem_url' => "TEXT DEFAULT ''", 'dailymed_url' => "TEXT DEFAULT ''", 'search_terms' => "TEXT DEFAULT ''", + 'official_use' => "TEXT DEFAULT ''", + 'official_use_source_name' => "TEXT DEFAULT ''", + 'official_use_source_url' => "TEXT DEFAULT ''", ]; foreach ($definitions as $column => $definition) { @@ -677,79 +718,79 @@ function condition_catalog_rows(): array 'Cardiovascular', '{condition} is a heart, blood vessel, blood-pressure, clotting, or circulation-related condition.', 'Track blood pressure, pulse, weight, swelling, chest symptoms, shortness of breath, medicines, labs, procedures, and care-team instructions.', - ['Hypertension', 'High Cholesterol', 'Coronary Artery Disease', 'Angina', 'Heart Attack', 'Heart Failure', 'Atrial Fibrillation', 'Arrhythmia', 'Stroke', 'Transient Ischemic Attack', 'Peripheral Artery Disease', 'Deep Vein Thrombosis', 'Pulmonary Embolism', 'Aortic Aneurysm', 'Raynaud Disease'], + ['Hypertension', 'High Cholesterol', 'Coronary Artery Disease', 'Angina', 'Heart Attack', 'Heart Failure', 'Atrial Fibrillation', 'Arrhythmia', 'Stroke', 'Transient Ischemic Attack', 'Peripheral Artery Disease', 'Deep Vein Thrombosis', 'Pulmonary Embolism', 'Aortic Aneurysm', 'Raynaud Disease', 'Cardiomyopathy', 'Myocarditis', 'Pericarditis', 'Valvular Heart Disease', 'Pulmonary Hypertension', 'Chronic Venous Insufficiency', 'Edema'], ], [ 'Respiratory', '{condition} is a lung, airway, breathing, or oxygen-related condition.', 'Track cough, breathing effort, oxygen saturation, wheezing, triggers, fever, inhaler use, smoking or exposure history, and activity tolerance.', - ['Asthma', 'Chronic Obstructive Pulmonary Disease', 'Pneumonia', 'Bronchitis', 'Sleep Apnea', 'Pulmonary Fibrosis', 'Tuberculosis', 'Cystic Fibrosis', 'Pleural Effusion', 'Respiratory Syncytial Virus Infection', 'COVID-19', 'Influenza', 'Allergic Rhinitis', 'Sinusitis'], + ['Asthma', 'Chronic Obstructive Pulmonary Disease', 'Pneumonia', 'Bronchitis', 'Sleep Apnea', 'Pulmonary Fibrosis', 'Tuberculosis', 'Cystic Fibrosis', 'Pleural Effusion', 'Respiratory Syncytial Virus Infection', 'COVID-19', 'Influenza', 'Allergic Rhinitis', 'Sinusitis', 'Bronchiectasis', 'Acute Respiratory Distress Syndrome', 'Lung Nodule', 'Sarcoidosis', 'Respiratory Failure', 'Vocal Cord Dysfunction'], ], [ 'Endocrine / metabolic', '{condition} is a hormone, blood-sugar, thyroid, weight, or metabolism-related condition.', 'Track glucose if relevant, A1C, weight, blood pressure, medicines, energy, thirst, urination, labs, nutrition context, and clinician goals.', - ['Type 1 Diabetes', 'Type 2 Diabetes', 'Prediabetes', 'Gestational Diabetes', 'Hypoglycemia', 'Hyperthyroidism', 'Hypothyroidism', 'Hashimoto Thyroiditis', 'Graves Disease', 'Cushing Syndrome', 'Addison Disease', 'Polycystic Ovary Syndrome', 'Obesity', 'Metabolic Syndrome', 'Vitamin D Deficiency', 'Osteoporosis'], + ['Type 1 Diabetes', 'Type 2 Diabetes', 'Prediabetes', 'Gestational Diabetes', 'Hypoglycemia', 'Hyperthyroidism', 'Hypothyroidism', 'Hashimoto Thyroiditis', 'Graves Disease', 'Cushing Syndrome', 'Addison Disease', 'Polycystic Ovary Syndrome', 'Obesity', 'Metabolic Syndrome', 'Vitamin D Deficiency', 'Osteoporosis', 'Diabetic Ketoacidosis', 'Thyroid Nodules', 'Hyperparathyroidism', 'Hypoparathyroidism', 'Vitamin B12 Deficiency', 'Hyperlipidemia', 'Hypercalcemia', 'Hyponatremia'], ], [ 'Digestive / liver', '{condition} is a digestive tract, liver, gallbladder, pancreas, or bowel-related condition.', 'Track abdominal pain location, bowel pattern, nausea, vomiting, bleeding, diet triggers, hydration, weight changes, medicines, procedures, and lab or imaging follow-up.', - ['Gastroesophageal Reflux Disease', 'Peptic Ulcer Disease', 'Gastritis', 'Irritable Bowel Syndrome', 'Crohn Disease', 'Ulcerative Colitis', 'Celiac Disease', 'Diverticulitis', 'Gallstones', 'Pancreatitis', 'Hepatitis A', 'Hepatitis B', 'Hepatitis C', 'Cirrhosis', 'Fatty Liver Disease', 'Appendicitis', 'Hemorrhoids', 'Constipation', 'Diarrhea'], + ['Gastroesophageal Reflux Disease', 'Peptic Ulcer Disease', 'Gastritis', 'Irritable Bowel Syndrome', 'Crohn Disease', 'Ulcerative Colitis', 'Celiac Disease', 'Diverticulitis', 'Gallstones', 'Pancreatitis', 'Hepatitis A', 'Hepatitis B', 'Hepatitis C', 'Cirrhosis', 'Fatty Liver Disease', 'Appendicitis', 'Hemorrhoids', 'Constipation', 'Diarrhea', 'Gastrointestinal Bleeding', 'Colon Polyps', 'Lactose Intolerance', 'Nausea and Vomiting', 'Biliary Colic', 'Gastroenteritis', 'Dysphagia', 'Fecal Incontinence', 'Bacterial Vaginosis', 'Vaginal Yeast Infection'], ], [ 'Kidney / urinary', '{condition} is a kidney, bladder, urinary tract, or fluid-balance-related condition.', 'Track urination pattern, pain, fever, fluid intake, urine test results, kidney labs, blood pressure, swelling, medicines, and procedure notes.', - ['Chronic Kidney Disease', 'Kidney Stones', 'Urinary Tract Infection', 'Bladder Infection', 'Kidney Infection', 'Urinary Incontinence', 'Overactive Bladder', 'Benign Prostatic Hyperplasia', 'Interstitial Cystitis', 'Glomerulonephritis', 'Nephrotic Syndrome'], + ['Chronic Kidney Disease', 'Kidney Stones', 'Urinary Tract Infection', 'Bladder Infection', 'Kidney Infection', 'Urinary Incontinence', 'Overactive Bladder', 'Benign Prostatic Hyperplasia', 'Interstitial Cystitis', 'Glomerulonephritis', 'Nephrotic Syndrome', 'Acute Kidney Injury', 'Proteinuria', 'Hematuria', 'Hydronephrosis', 'Urinary Retention'], ], [ 'Neurologic', '{condition} is a brain, spinal cord, nerve, movement, headache, seizure, or cognition-related condition.', 'Track onset, symptom episodes, weakness, speech or vision changes, headaches, seizures, falls, medicines, triggers, sleep, and safety events.', - ['Migraine', 'Epilepsy', 'Multiple Sclerosis', 'Parkinson Disease', 'Alzheimer Disease', 'Dementia', 'Neuropathy', 'Bell Palsy', 'Meningitis', 'Concussion', 'Traumatic Brain Injury', 'Vertigo', 'Amyotrophic Lateral Sclerosis', 'Restless Legs Syndrome', 'Carpal Tunnel Syndrome'], + ['Migraine', 'Epilepsy', 'Multiple Sclerosis', 'Parkinson Disease', 'Alzheimer Disease', 'Dementia', 'Neuropathy', 'Peripheral Neuropathy', 'Bell Palsy', 'Meningitis', 'Concussion', 'Traumatic Brain Injury', 'Vertigo', 'Amyotrophic Lateral Sclerosis', 'Restless Legs Syndrome', 'Carpal Tunnel Syndrome', 'Essential Tremor', 'Cluster Headache', 'Tension Headache', 'Brain Aneurysm', 'Guillain-Barre Syndrome', 'Myasthenia Gravis'], ], [ 'Mental health', '{condition} is a mood, anxiety, attention, behavior, substance-use, sleep, or psychiatric condition.', 'Track mood, sleep, appetite, concentration, safety concerns, triggers, medicines, therapy notes, substance use, and follow-up plans.', - ['Depression', 'Anxiety Disorders', 'Generalized Anxiety Disorder', 'Panic Disorder', 'Bipolar Disorder', 'Post-Traumatic Stress Disorder', 'Obsessive-Compulsive Disorder', 'Attention Deficit Hyperactivity Disorder', 'Autism Spectrum Disorder', 'Schizophrenia', 'Eating Disorders', 'Alcohol Use Disorder', 'Substance Use Disorder', 'Insomnia'], + ['Depression', 'Anxiety Disorders', 'Generalized Anxiety Disorder', 'Panic Disorder', 'Bipolar Disorder', 'Post-Traumatic Stress Disorder', 'Obsessive-Compulsive Disorder', 'Attention Deficit Hyperactivity Disorder', 'Autism Spectrum Disorder', 'Schizophrenia', 'Eating Disorders', 'Alcohol Use Disorder', 'Substance Use Disorder', 'Insomnia', 'Seasonal Affective Disorder', 'Premenstrual Dysphoric Disorder', 'Social Anxiety Disorder', 'Specific Phobia', 'Borderline Personality Disorder', 'Opioid Use Disorder'], ], [ 'Musculoskeletal / rheumatology', '{condition} is a bone, joint, muscle, spine, autoimmune, or inflammation-related condition.', 'Track pain score, stiffness, swelling, range of motion, flares, activity limits, injuries, medicines, injections, imaging, and function.', - ['Osteoarthritis', 'Rheumatoid Arthritis', 'Gout', 'Fibromyalgia', 'Lupus', 'Ankylosing Spondylitis', 'Psoriatic Arthritis', 'Bursitis', 'Tendinitis', 'Back Pain', 'Sciatica', 'Scoliosis', 'Fracture', 'Sprain', 'Carpal Tunnel Syndrome', 'Osteomyelitis'], + ['Osteoarthritis', 'Rheumatoid Arthritis', 'Gout', 'Fibromyalgia', 'Lupus', 'Ankylosing Spondylitis', 'Psoriatic Arthritis', 'Bursitis', 'Tendinitis', 'Back Pain', 'Sciatica', 'Scoliosis', 'Fracture', 'Sprain', 'Carpal Tunnel Syndrome', 'Osteomyelitis', 'Osteopenia', 'Rotator Cuff Injury', 'Meniscus Tear', 'Plantar Fasciitis', 'Tennis Elbow', 'Frozen Shoulder'], ], [ 'Skin / dermatology', '{condition} is a skin, hair, nail, wound, allergy, or visible-tissue condition.', 'Track location, appearance, spread, itch, pain, fever, drainage, new medicines, exposures, photos, treatment response, and recurrence.', - ['Acne', 'Eczema', 'Psoriasis', 'Rosacea', 'Hives', 'Cellulitis', 'Shingles', 'Cold Sores', 'Fungal Skin Infection', 'Warts', 'Vitiligo', 'Alopecia Areata', 'Pressure Ulcers', 'Melanoma', 'Skin Cancer', 'Seborrheic Dermatitis'], + ['Acne', 'Eczema', 'Psoriasis', 'Rosacea', 'Hives', 'Cellulitis', 'Shingles', 'Cold Sores', 'Fungal Skin Infection', 'Warts', 'Vitiligo', 'Alopecia Areata', 'Pressure Ulcers', 'Melanoma', 'Skin Cancer', 'Seborrheic Dermatitis', 'Contact Dermatitis', 'Impetigo', 'Athlete Foot', 'Nail Fungus', 'Burns', 'Abscess', 'Actinic Keratosis', 'Candidiasis'], ], [ 'Infectious disease', '{condition} is an infection or infectious-disease-related condition.', 'Track fever, symptom onset, exposures, test results, isolation guidance, medicines, completion dates, hydration, and worsening symptoms.', - ['Common Cold', 'Strep Throat', 'Mononucleosis', 'Lyme Disease', 'Sepsis', 'HIV', 'Herpes', 'Chickenpox', 'Measles', 'Mumps', 'Whooping Cough', 'Food Poisoning', 'MRSA Infection', 'C. difficile Infection'], + ['Common Cold', 'Strep Throat', 'Mononucleosis', 'Lyme Disease', 'Sepsis', 'HIV', 'Herpes', 'Chickenpox', 'Measles', 'Mumps', 'Whooping Cough', 'Food Poisoning', 'MRSA Infection', 'C. difficile Infection', 'Norovirus', 'Respiratory Syncytial Virus', 'Dental Abscess', 'Pelvic Inflammatory Disease', 'Bacterial Infection', 'Parasitic Infection'], ], [ 'Blood / immune', '{condition} is a blood, clotting, anemia, immune, or allergy-related condition.', 'Track CBC results, bleeding or bruising, fatigue, infections, triggers, exposures, medicines, transfusions, and specialist follow-up.', - ['Anemia', 'Iron Deficiency Anemia', 'Sickle Cell Disease', 'Hemophilia', 'Thrombocytopenia', 'Leukemia', 'Lymphoma', 'Multiple Myeloma', 'Allergies', 'Anaphylaxis', 'Autoimmune Diseases', 'Immunodeficiency Disorders'], + ['Anemia', 'Iron Deficiency Anemia', 'Sickle Cell Disease', 'Hemophilia', 'Thrombocytopenia', 'Leukemia', 'Lymphoma', 'Multiple Myeloma', 'Allergies', 'Anaphylaxis', 'Autoimmune Diseases', 'Immunodeficiency Disorders', 'Neutropenia', 'Blood Clotting Disorders', 'Mast Cell Activation Syndrome'], ], [ 'Eye / ear / nose / throat', '{condition} is an eye, ear, sinus, mouth, dental, hearing, or throat condition.', 'Track pain, side affected, vision or hearing changes, discharge, fever, allergies, dental symptoms, medicines, and procedure notes.', - ['Conjunctivitis', 'Cataracts', 'Glaucoma', 'Macular Degeneration', 'Diabetic Eye Disease', 'Dry Eye', 'Ear Infection', 'Tinnitus', 'Hearing Loss', 'Tonsillitis', 'Dental Cavities', 'Gum Disease'], + ['Conjunctivitis', 'Cataracts', 'Glaucoma', 'Macular Degeneration', 'Diabetic Eye Disease', 'Dry Eye', 'Ear Infection', 'Tinnitus', 'Hearing Loss', 'Tonsillitis', 'Dental Cavities', 'Gum Disease', 'Otitis Externa', 'Meniere Disease', 'Temporomandibular Joint Disorder', 'Oral Thrush'], ], [ 'Reproductive / pregnancy', '{condition} is a reproductive, menstrual, pregnancy, breast, prostate, or sexual-health-related condition.', 'Track cycle timing, bleeding, pelvic pain, discharge, pregnancy context, urinary symptoms, test results, medicines, and clinician instructions.', - ['Endometriosis', 'Uterine Fibroids', 'Ovarian Cysts', 'Menopause', 'Pregnancy', 'Preeclampsia', 'Breast Cancer', 'Prostate Cancer', 'Prostatitis', 'Erectile Dysfunction', 'Sexually Transmitted Infections', 'Infertility'], + ['Endometriosis', 'Uterine Fibroids', 'Ovarian Cysts', 'Menopause', 'Pregnancy', 'Preeclampsia', 'Breast Cancer', 'Prostate Cancer', 'Prostatitis', 'Erectile Dysfunction', 'Sexually Transmitted Infections', 'Infertility', 'Premenstrual Syndrome', 'Heavy Menstrual Bleeding', 'Vaginitis', 'Testicular Torsion'], ], [ 'Cancer / oncology', @@ -879,91 +920,91 @@ function symptom_catalog_rows(): array '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'], + ['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', 'Medication reaction', 'Poor appetite', 'Low energy', 'Feeling faint', 'Weight fluctuation'], ], [ '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'], + ['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', 'Nerve pain', 'Pain with movement', 'Pain at rest', 'Sharp pain', 'Dull ache'], ], [ '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'], + ['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', 'Labored breathing', 'Apnea', 'Coughing at night', 'Exercise-induced wheezing', 'Throat clearing'], ], [ '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'], + ['Palpitations', 'Fainting', 'Near-fainting', 'Dizziness', 'Lightheadedness', 'Leg swelling', 'Blue lips', 'Rapid heartbeat', 'Slow heartbeat', 'Orthopnea', 'Exercise intolerance', 'Cold hands and feet', 'Clammy skin', 'High blood pressure reading', 'Low blood pressure reading', 'Irregular heartbeat', 'Calf pain with walking', 'Sudden swelling'], ], [ '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'], + ['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', 'Acid reflux', 'Regurgitation', 'Change in stool color', 'Greasy stool', 'Food intolerance symptoms'], ], [ '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'], + ['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', 'Facial numbness', 'Arm weakness', 'Leg weakness', 'Brain fog', 'Burning feet'], ], [ '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'], + ['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', 'Cold sores', 'Skin scaling', 'Open sore', 'Skin infection signs', 'Nail discoloration', 'Acne flare'], ], [ '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'], + ['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', 'Dry eyes', 'Sinus headache', 'Dental pain', 'Gum swelling', 'Ear ringing'], ], [ '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'], + ['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', 'Burning with urination', 'Foamy urine', 'Pelvic pressure', 'Irregular periods', 'Vaginal itching', 'Genital sores'], ], [ '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'], + ['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', 'Morning stiffness', 'Muscle spasm', 'Tendon pain', 'Heel pain', 'Reduced grip strength'], ], [ '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'], + ['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', 'Compulsions', 'Flashbacks', 'Poor sleep quality', 'Loss of interest', 'Appetite changes'], ], [ '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'], + ['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', 'Sugar cravings', 'Low blood pressure symptoms', 'Thyroid symptom changes', 'Increased urination at night'], ], [ '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'], + ['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', 'Bruising after medication', 'Dry mouth after medication', 'Constipation after medication', 'Injection swelling', 'Medication rash'], ], ]; @@ -1079,14 +1120,16 @@ function seed_expanded_medication_catalog(PDO $pdo): void $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, + common_use, official_use, official_use_source_name, official_use_source_url, tracking_note, + rxnorm_url, medlineplus_url, pubchem_url, dailymed_url, pubmed_url, clinicaltrials_url, source_name, search_terms, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" + 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 = ?, + class_name = ?, drug_family = ?, common_use = ?, official_use = ?, + official_use_source_name = ?, official_use_source_url = ?, tracking_note = ?, rxnorm_url = ?, medlineplus_url = ?, pubchem_url = ?, dailymed_url = ?, pubmed_url = ?, clinicaltrials_url = ?, source_name = ?, search_terms = ? WHERE id = ?" @@ -1122,6 +1165,9 @@ function seed_expanded_medication_catalog(PDO $pdo): void $className, $drugFamily, $commonUse, + '', + '', + '', $trackingNote, $rxnormUrl, $medlinePlusUrl, @@ -1145,6 +1191,9 @@ function seed_expanded_medication_catalog(PDO $pdo): void $className, $drugFamily, $commonUse, + '', + '', + '', $trackingNote, $rxnormUrl, $medlinePlusUrl, @@ -1796,6 +1845,596 @@ function medicine_reference_links(array $medicine): array return array_filter($links, static fn ($url) => trim((string) $url) !== ''); } +function medicine_used_for_text(array $medicine): string +{ + $officialUse = trim((string) ($medicine['official_use'] ?? '')); + if ($officialUse !== '') { + return 'Used for: ' . $officialUse; + } + + $commonUse = trim((string) ($medicine['common_use'] ?? '')); + if ($commonUse !== '') { + return $commonUse; + } + + return 'Use information has not been imported yet. Review the official label or ask a clinician/pharmacist for patient-specific guidance.'; +} + +function medicine_use_source_link(array $medicine): array +{ + $label = trim((string) ($medicine['official_use_source_name'] ?? '')); + $url = trim((string) ($medicine['official_use_source_url'] ?? '')); + if ($label !== '' && $url !== '') { + return [$label => $url]; + } + + return []; +} + +function medicine_with_use_summary(array $medicine): array +{ + $medicine['used_for'] = medicine_used_for_text($medicine); + foreach (medicine_use_source_link($medicine) as $label => $url) { + $medicine['used_for_source_name'] = $label; + $medicine['used_for_source_url'] = $url; + break; + } + return $medicine; +} + +function medicine_rows_with_use_summary(array $rows): array +{ + return array_map(static fn(array $medicine): array => medicine_with_use_summary($medicine), $rows); +} + +function therapeutic_relationship_source_name(): string +{ + return 'Neon therapeutic-use relationships'; +} + +function therapeutic_relationship_source_url(): string +{ + return 'https://medlineplus.gov/druginformation.html'; +} + +function therapeutic_relationship_rules(): array +{ + return [ + [ + 'family' => 'Pain and fever', + 'medicine_class' => 'Analgesic, antipyretic, or anti-inflammatory medicine', + 'keywords' => ['analgesic', 'antipyretic', 'nonsteroidal', 'nsaid', 'salicylate', 'acetaminophen', 'paracetamol', 'ibuprofen', 'naproxen', 'diclofenac', 'celecoxib', 'meloxicam', 'aspirin', 'opioid analgesic', 'tramadol', 'hydrocodone', 'oxycodone', 'morphine'], + 'conditions' => ['Pain and Inflammation', 'Chronic Pain', 'Fever', 'Osteoarthritis', 'Back Pain', 'Migraine'], + 'symptoms' => ['Pain', 'Severe pain', 'Fever', 'Headache', 'Severe headache', 'Joint pain', 'Muscle pain', 'Back pain', 'Neck pain', 'Toothache', 'Cramps'], + 'note' => 'Commonly used in pain, fever, or inflammation care depending on formulation and patient-specific indication.', + ], + [ + 'family' => 'Antibiotic and antibacterial', + 'medicine_class' => 'Antibacterial or antibiotic medicine', + 'keywords' => ['antibiotic', 'antibacterial', 'penicillin', 'cephalosporin', 'macrolide', 'tetracycline', 'fluoroquinolone', 'nitrofuran', 'amoxicillin', 'azithromycin', 'doxycycline', 'cephalexin', 'ciprofloxacin', 'levofloxacin', 'clindamycin', 'metronidazole', 'trimethoprim', 'sulfamethoxazole', 'nitrofurantoin', 'mupirocin'], + 'conditions' => ['Urinary Tract Infection', 'Bladder Infection', 'Kidney Infection', 'Pneumonia', 'Bronchitis', 'Sinusitis', 'Strep Throat', 'Cellulitis', 'MRSA Infection', 'Bacterial Vaginosis', 'Dental Abscess'], + 'symptoms' => ['Fever', 'Sore throat', 'Cough', 'Productive cough', 'Painful urination', 'Frequent urination', 'Skin redness', 'Skin warmth', 'Pus or drainage', 'Toothache'], + 'note' => 'Commonly used for selected bacterial infections when prescribed and matched to the suspected or confirmed organism.', + ], + [ + 'family' => 'Antiviral', + 'medicine_class' => 'Antiviral medicine', + 'keywords' => ['antiviral', 'acyclovir', 'valacyclovir', 'famciclovir', 'oseltamivir', 'zanamivir', 'tenofovir', 'emtricitabine', 'dolutegravir', 'bictegravir', 'ritonavir', 'remdesivir', 'nirmatrelvir'], + 'conditions' => ['Herpes', 'Shingles', 'Influenza', 'HIV', 'Hepatitis B', 'Hepatitis C', 'COVID-19', 'Cold Sores'], + 'symptoms' => ['Fever', 'Chills', 'Body aches', 'Rash', 'Blister', 'Cold Sores', 'Fatigue', 'Sore throat'], + 'note' => 'Commonly used for selected viral infections depending on virus, timing, formulation, and indication.', + ], + [ + 'family' => 'Antifungal', + 'medicine_class' => 'Antifungal medicine', + 'keywords' => ['antifungal', 'azole antifungal', 'fluconazole', 'clotrimazole', 'miconazole', 'ketoconazole', 'terbinafine', 'nystatin', 'voriconazole', 'itraconazole'], + 'conditions' => ['Fungal Skin Infection', 'Candidiasis', 'Vaginal Yeast Infection', 'Athlete Foot', 'Nail Fungus'], + 'symptoms' => ['Itching', 'Rash', 'Skin redness', 'Peeling skin', 'Vaginal discharge', 'Cracked skin', 'Nail changes'], + 'note' => 'Commonly used for selected fungal infections depending on site, organism, and formulation.', + ], + [ + 'family' => 'Blood pressure and heart medicines', + 'medicine_class' => 'Cardiovascular medicine', + 'keywords' => ['ace inhibitor', 'angiotensin', 'receptor blocker', 'beta blocker', 'calcium channel blocker', 'thiazide', 'diuretic', 'loop diuretic', 'vasodilator', 'nitrate', 'lisinopril', 'losartan', 'valsartan', 'amlodipine', 'metoprolol', 'carvedilol', 'atenolol', 'hydrochlorothiazide', 'chlorthalidone', 'furosemide', 'spironolactone', 'nitroglycerin'], + 'conditions' => ['Hypertension', 'Heart Failure', 'Coronary Artery Disease', 'Angina', 'Chronic Kidney Disease', 'Peripheral Artery Disease', 'Edema'], + 'symptoms' => ['Chest pain', 'Chest pressure', 'Shortness of breath', 'Leg swelling', 'Edema', 'Palpitations', 'Dizziness', 'Exercise intolerance'], + 'note' => 'Commonly used in blood pressure, heart failure, angina, kidney-protection, or fluid-management care depending on the medicine.', + ], + [ + 'family' => 'Cholesterol and lipid medicines', + 'medicine_class' => 'Lipid-lowering medicine', + 'keywords' => ['statin', 'hmg-coa', 'cholesterol absorption inhibitor', 'pcsk9', 'lipid', 'atorvastatin', 'rosuvastatin', 'simvastatin', 'pravastatin', 'ezetimibe', 'alirocumab', 'evolocumab', 'fenofibrate', 'gemfibrozil'], + 'conditions' => ['High Cholesterol', 'Coronary Artery Disease', 'Stroke', 'Peripheral Artery Disease', 'Type 2 Diabetes', 'Metabolic Syndrome'], + 'symptoms' => ['Muscle pain', 'Muscle weakness', 'Medication side effects', 'Fatigue'], + 'note' => 'Commonly used for cholesterol management and cardiovascular risk reduction when prescribed.', + ], + [ + 'family' => 'Anticoagulant and antiplatelet', + 'medicine_class' => 'Blood thinner or antiplatelet medicine', + 'keywords' => ['anticoagulant', 'antiplatelet', 'factor xa', 'vitamin k antagonist', 'p2y12', 'warfarin', 'apixaban', 'rivaroxaban', 'dabigatran', 'edoxaban', 'clopidogrel', 'prasugrel', 'ticagrelor', 'heparin', 'enoxaparin'], + 'conditions' => ['Atrial Fibrillation', 'Deep Vein Thrombosis', 'Pulmonary Embolism', 'Stroke', 'Transient Ischemic Attack', 'Coronary Artery Disease', 'Heart Attack'], + 'symptoms' => ['Bleeding after medication', 'Easy bruising', 'Bleeding', 'Black stools', 'Blood in stool', 'Nosebleed', 'Leg swelling', 'Chest pain'], + 'note' => 'Commonly used to reduce clotting risk or treat clots when prescribed; bleeding symptoms are important tracking context.', + ], + [ + 'family' => 'Diabetes and metabolic medicines', + 'medicine_class' => 'Diabetes or metabolic medicine', + 'keywords' => ['antidiabetic', 'insulin', 'glp-1', 'sglt2', 'dpp-4', 'sulfonylurea', 'biguanide', 'metformin', 'glargine', 'lispro', 'semaglutide', 'dulaglutide', 'liraglutide', 'empagliflozin', 'dapagliflozin', 'canagliflozin', 'sitagliptin', 'glipizide', 'glyburide'], + 'conditions' => ['Type 2 Diabetes', 'Type 1 Diabetes', 'Prediabetes', 'Gestational Diabetes', 'Obesity', 'Metabolic Syndrome', 'Chronic Kidney Disease', 'Heart Failure'], + 'symptoms' => ['High blood sugar symptoms', 'Low blood sugar symptoms', 'Excessive thirst', 'Frequent urination', 'Frequent hunger', 'Shakiness', 'Sweating episodes', 'Unintentional weight changes'], + 'note' => 'Commonly used in diabetes, metabolic, weight, kidney, or heart-risk care depending on the specific medicine and indication.', + ], + [ + 'family' => 'Respiratory and asthma medicines', + 'medicine_class' => 'Respiratory medicine', + 'keywords' => ['bronchodilator', 'beta agonist', 'inhaled corticosteroid', 'leukotriene', 'anticholinergic inhaler', 'albuterol', 'levalbuterol', 'salmeterol', 'formoterol', 'budesonide', 'fluticasone', 'montelukast', 'ipratropium', 'tiotropium', 'umeclidinium'], + 'conditions' => ['Asthma', 'Chronic Obstructive Pulmonary Disease', 'Allergic Rhinitis', 'Bronchitis', 'Cystic Fibrosis'], + 'symptoms' => ['Wheezing', 'Shortness of breath', 'Trouble breathing', 'Cough', 'Dry cough', 'Chest tightness', 'Rapid breathing', 'Nasal congestion'], + 'note' => 'Commonly used for asthma, COPD, bronchospasm, allergy-airway, or breathing-symptom care depending on formulation.', + ], + [ + 'family' => 'Allergy and anaphylaxis medicines', + 'medicine_class' => 'Allergy medicine', + 'keywords' => ['antihistamine', 'decongestant', 'epinephrine', 'nasal corticosteroid', 'allergy', 'cetirizine', 'loratadine', 'fexofenadine', 'diphenhydramine', 'hydroxyzine', 'fluticasone', 'mometasone', 'pseudoephedrine'], + 'conditions' => ['Allergies', 'Allergic Rhinitis', 'Hives', 'Anaphylaxis', 'Eczema'], + 'symptoms' => ['Sneezing', 'Runny nose', 'Nasal congestion', 'Itching', 'Hives', 'Watery eyes', 'Severe allergic reaction', 'Swelling', 'Tongue swelling'], + 'note' => 'Commonly used for allergy symptoms or emergency allergic-reaction care depending on medicine and route.', + ], + [ + 'family' => 'Gastrointestinal medicines', + 'medicine_class' => 'Gastrointestinal medicine', + 'keywords' => ['proton pump inhibitor', 'h2 blocker', 'antiemetic', 'laxative', 'stool softener', 'antidiarrheal', 'antacid', 'omeprazole', 'pantoprazole', 'esomeprazole', 'famotidine', 'ondansetron', 'promethazine', 'metoclopramide', 'docusate', 'polyethylene glycol', 'senna', 'bisacodyl', 'loperamide'], + 'conditions' => ['Gastroesophageal Reflux Disease', 'Peptic Ulcer Disease', 'Gastritis', 'Constipation', 'Diarrhea', 'Irritable Bowel Syndrome', 'Nausea and Vomiting'], + 'symptoms' => ['Heartburn', 'Indigestion', 'Nausea', 'Vomiting', 'Constipation', 'Diarrhea', 'Abdominal pain', 'Bloating', 'Gas'], + 'note' => 'Commonly used for acid-related, nausea/vomiting, constipation, diarrhea, or bowel-symptom care depending on the medicine.', + ], + [ + 'family' => 'Mental health medicines', + 'medicine_class' => 'Mental health medicine', + 'keywords' => ['ssri', 'snri', 'antidepressant', 'benzodiazepine', 'anxiolytic', 'antipsychotic', 'mood stabilizer', 'stimulant', 'norepinephrine reuptake', 'sertraline', 'fluoxetine', 'escitalopram', 'citalopram', 'paroxetine', 'venlafaxine', 'duloxetine', 'bupropion', 'trazodone', 'alprazolam', 'lorazepam', 'clonazepam', 'quetiapine', 'risperidone', 'olanzapine', 'lithium', 'amphetamine', 'methylphenidate', 'atomoxetine'], + 'conditions' => ['Depression', 'Anxiety Disorders', 'Generalized Anxiety Disorder', 'Panic Disorder', 'Bipolar Disorder', 'Post-Traumatic Stress Disorder', 'Obsessive-Compulsive Disorder', 'Attention Deficit Hyperactivity Disorder', 'Schizophrenia', 'Insomnia'], + 'symptoms' => ['Depressed mood', 'Anxiety', 'Panic attack', 'Mood swings', 'Insomnia', 'Trouble concentrating', 'Irritability', 'Racing thoughts', 'Hallucinations'], + 'note' => 'Commonly used for mental-health or sleep-related care depending on diagnosis, formulation, and clinician plan.', + ], + [ + 'family' => 'Seizure, migraine, and nerve-pain medicines', + 'medicine_class' => 'Neurology medicine', + 'keywords' => ['anticonvulsant', 'antiepileptic', 'nerve-pain', 'migraine', 'gabapentin', 'pregabalin', 'lamotrigine', 'levetiracetam', 'topiramate', 'carbamazepine', 'valproate', 'divalproex', 'sumatriptan', 'rizatriptan'], + 'conditions' => ['Epilepsy', 'Migraine', 'Neuropathy', 'Fibromyalgia', 'Restless Legs Syndrome', 'Bipolar Disorder'], + 'symptoms' => ['Seizure', 'Headache', 'Severe headache', 'Migraine aura', 'Numbness', 'Tingling', 'Burning pain', 'Restless legs', 'Light sensitivity', 'Sound sensitivity'], + 'note' => 'Commonly used for seizure, migraine, mood-stabilization, or nerve-pain care depending on medicine and indication.', + ], + [ + 'family' => 'Thyroid and endocrine medicines', + 'medicine_class' => 'Endocrine medicine', + 'keywords' => ['thyroid hormone', 'antithyroid', 'levothyroxine', 'liothyronine', 'methimazole', 'propylthiouracil', 'hydrocortisone', 'prednisone', 'dexamethasone'], + 'conditions' => ['Hypothyroidism', 'Hyperthyroidism', 'Hashimoto Thyroiditis', 'Graves Disease', 'Addison Disease', 'Cushing Syndrome'], + 'symptoms' => ['Fatigue', 'Cold intolerance', 'Heat intolerance', 'Unintentional weight changes', 'Excessive sweating', 'Palpitations', 'General weakness'], + 'note' => 'Commonly used for thyroid, adrenal, or endocrine-inflammatory care depending on the medicine.', + ], + [ + 'family' => 'Corticosteroid and immune medicines', + 'medicine_class' => 'Anti-inflammatory or immunomodulating medicine', + 'keywords' => ['systemic corticosteroid', 'immunomodulator', 'immunosuppressant', 'tnf blocker', 'biologic', 'monoclonal antibody', 'prednisone', 'methylprednisolone', 'methotrexate', 'adalimumab', 'etanercept', 'infliximab', 'ustekinumab', 'secukinumab'], + 'conditions' => ['Rheumatoid Arthritis', 'Psoriasis', 'Psoriatic Arthritis', 'Crohn Disease', 'Ulcerative Colitis', 'Lupus', 'Eczema', 'Autoimmune Diseases'], + 'symptoms' => ['Joint pain', 'Joint swelling', 'Rash', 'Itching', 'Wheezing', 'Abdominal pain', 'Fatigue', 'Medication side effects'], + 'note' => 'Commonly used for inflammatory, autoimmune, allergy, skin, lung, or immune-mediated care depending on formulation and diagnosis.', + ], + [ + 'family' => 'Dermatology medicines', + 'medicine_class' => 'Dermatology medicine', + 'keywords' => ['retinoid', 'topical antibiotic', 'topical corticosteroid', 'acne', 'isotretinoin', 'tretinoin', 'adapalene', 'benzoyl peroxide', 'mupirocin', 'clindamycin topical', 'triamcinolone cream', 'hydrocortisone cream'], + 'conditions' => ['Acne', 'Eczema', 'Psoriasis', 'Rosacea', 'Fungal Skin Infection', 'Cellulitis', 'Seborrheic Dermatitis'], + 'symptoms' => ['Rash', 'Itching', 'Skin redness', 'Dry skin', 'Peeling skin', 'Pus or drainage', 'Mole changes'], + 'note' => 'Commonly used for skin inflammation, acne, infection, or visible skin-symptom care depending on formulation.', + ], + [ + 'family' => 'Urology and reproductive medicines', + 'medicine_class' => 'Urology or reproductive-health medicine', + 'keywords' => ['alpha-1 blocker', '5-alpha-reductase', 'pde-5', 'anticholinergic bladder', 'contraceptive', 'estrogen', 'progestin', 'tamsulosin', 'finasteride', 'sildenafil', 'tadalafil', 'oxybutynin', 'norethindrone', 'estradiol', 'medroxyprogesterone', 'ethinyl estradiol'], + 'conditions' => ['Benign Prostatic Hyperplasia', 'Erectile Dysfunction', 'Overactive Bladder', 'Urinary Incontinence', 'Menopause', 'Endometriosis', 'Polycystic Ovary Syndrome'], + 'symptoms' => ['Erectile dysfunction', 'Frequent urination', 'Urgent urination', 'Urinary retention', 'Urinary incontinence', 'Nighttime urination', 'Hot flashes', 'Heavy menstrual bleeding'], + 'note' => 'Commonly used in urinary, prostate, erectile, menstrual, menopause, or hormonal care depending on the medicine.', + ], + [ + 'family' => 'Gout and uric-acid medicines', + 'medicine_class' => 'Gout or uric-acid medicine', + 'keywords' => ['antigout', 'xanthine oxidase', 'allopurinol', 'febuxostat', 'colchicine', 'probenecid'], + 'conditions' => ['Gout', 'Kidney Stones', 'Chronic Kidney Disease'], + 'symptoms' => ['Joint pain', 'Joint swelling', 'Tenderness', 'Severe pain', 'Flank pain'], + 'note' => 'Commonly used for gout flares, gout prevention, or uric-acid management depending on the medicine.', + ], + [ + 'family' => 'Bone health medicines', + 'medicine_class' => 'Bone health medicine', + 'keywords' => ['bisphosphonate', 'vitamin d', 'calcium', 'osteoporosis', 'alendronate', 'risedronate', 'ibandronate', 'zoledronic', 'denosumab', 'cholecalciferol', 'ergocalciferol'], + 'conditions' => ['Osteoporosis', 'Vitamin D Deficiency', 'Fracture', 'Malnutrition'], + 'symptoms' => ['Bone pain', 'Back pain', 'Falls', 'Muscle weakness'], + 'note' => 'Commonly used for bone density, fracture-risk, vitamin D, or calcium-related care depending on the medicine or supplement.', + ], + [ + 'family' => 'Iron, anemia, and blood-support medicines', + 'medicine_class' => 'Anemia or blood-support medicine', + 'keywords' => ['iron', 'ferrous', 'folic acid', 'cyanocobalamin', 'vitamin b12', 'epoetin', 'darbepoetin'], + 'conditions' => ['Anemia', 'Iron Deficiency Anemia', 'Vitamin B12 Deficiency', 'Chronic Kidney Disease', 'Malnutrition'], + 'symptoms' => ['Fatigue', 'General weakness', 'Dizziness', 'Shortness of breath', 'Palpitations', 'Cold hands and feet'], + 'note' => 'Commonly used for selected anemia, deficiency, or blood-count support when prescribed or recommended.', + ], + ]; +} + +function relationship_lookup(PDO $pdo, string $table, string $nameColumn): array +{ + $rows = db_all($pdo, "SELECT id, {$nameColumn} AS name FROM {$table}"); + $lookup = []; + foreach ($rows as $row) { + $lookup[strtolower((string) $row['name'])] = (int) $row['id']; + } + return $lookup; +} + +function relationship_haystack(array $medicine): string +{ + return strtolower(implode(' ', [ + $medicine['generic_name'] ?? '', + $medicine['active_ingredient'] ?? '', + $medicine['chemical_name'] ?? '', + $medicine['brand_names'] ?? '', + $medicine['generic_brands'] ?? '', + $medicine['class_name'] ?? '', + $medicine['drug_family'] ?? '', + $medicine['common_use'] ?? '', + $medicine['official_use'] ?? '', + $medicine['tracking_note'] ?? '', + $medicine['official_use_source_name'] ?? '', + $medicine['search_terms'] ?? '', + ])); +} + +function relationship_rule_matches(string $haystack, array $rule): bool +{ + foreach ($rule['keywords'] as $keyword) { + $needle = strtolower((string) $keyword); + if ($needle !== '' && str_contains($haystack, $needle)) { + return true; + } + } + return false; +} + +function unique_relationship_names(array $rules, string $field, int $limit = 20): array +{ + $names = []; + foreach ($rules as $rule) { + foreach ($rule[$field] ?? [] as $name) { + $key = strtolower((string) $name); + if ($key === '' || isset($names[$key])) { + continue; + } + $names[$key] = (string) $name; + if (count($names) >= $limit) { + break 2; + } + } + } + return array_values($names); +} + +function generic_import_common_use(string $value): bool +{ + $lower = strtolower(trim($value)); + return $lower === '' + || str_starts_with($lower, 'imported from') + || str_contains($lower, 'review official labels'); +} + +function generated_relationship_common_use(string $value): bool +{ + $lower = strtolower(trim($value)); + return str_starts_with($lower, 'commonly used for') + && str_contains($lower, 'confirm exact indication'); +} + +function relationship_search_phrases(): array +{ + $phrases = []; + foreach (therapeutic_relationship_rules() as $rule) { + foreach (['conditions', 'symptoms'] as $field) { + foreach ($rule[$field] ?? [] as $phrase) { + $phrases[(string) $phrase] = true; + } + } + $phrases[(string) $rule['family']] = true; + $phrases[(string) $rule['medicine_class']] = true; + } + + $values = array_keys($phrases); + usort($values, static fn(string $left, string $right): int => strlen($right) <=> strlen($left)); + return $values; +} + +function strip_relationship_search_phrases(string $current): string +{ + $next = $current; + foreach (relationship_search_phrases() as $phrase) { + $next = str_ireplace($phrase, ' ', $next); + } + return trim((string) preg_replace('/\s+/', ' ', $next)); +} + +function merge_search_phrases(string $current, array $phrases): string +{ + $next = trim($current); + $lower = strtolower($next); + foreach ($phrases as $phrase) { + $value = trim((string) $phrase); + if ($value === '' || str_contains($lower, strtolower($value))) { + continue; + } + $next = trim($next . ' ' . $value); + $lower = strtolower($next); + } + return $next; +} + +function relationship_common_use(array $rules): string +{ + $conditions = array_slice(unique_relationship_names($rules, 'conditions', 8), 0, 6); + $symptoms = array_slice(unique_relationship_names($rules, 'symptoms', 8), 0, 4); + + if ($conditions) { + $text = 'Commonly used for ' . implode(', ', $conditions) . ' care depending on formulation and patient-specific indication.'; + } else { + $text = 'Commonly used for symptom contexts such as ' . implode(', ', $symptoms) . ' depending on formulation and patient-specific indication.'; + } + + if ($symptoms) { + $text .= ' Track related symptoms such as ' . implode(', ', $symptoms) . '.'; + } + + return $text . ' Confirm exact indication, dosing, and safety information with official labeling or a clinician/pharmacist.'; +} + +function seed_medical_relationships(PDO $pdo, bool $force = false): void +{ + $version = '2026-07-03-therapeutic-links-v5'; + $medicineCount = (int) db_value($pdo, 'SELECT COUNT(*) FROM medication_catalog'); + $conditionCount = (int) db_value($pdo, 'SELECT COUNT(*) FROM medical_conditions'); + $symptomCount = (int) db_value($pdo, 'SELECT COUNT(*) FROM medical_symptoms'); + $signature = "{$version}:{$medicineCount}:{$conditionCount}:{$symptomCount}"; + $metaKey = 'medical_relationship_ruleset'; + $currentSignature = (string) (db_value($pdo, 'SELECT value FROM app_metadata WHERE key = ?', [$metaKey]) ?: ''); + if (!$force && $currentSignature === $signature) { + return; + } + + $rules = therapeutic_relationship_rules(); + $conditionLookup = relationship_lookup($pdo, 'medical_conditions', 'name'); + $symptomLookup = relationship_lookup($pdo, 'medical_symptoms', 'name'); + if (!$medicineCount || !$conditionLookup || !$symptomLookup) { + return; + } + + $sourceName = therapeutic_relationship_source_name(); + $sourceUrl = therapeutic_relationship_source_url(); + $created = app_now(); + $medicines = db_all($pdo, 'SELECT * FROM medication_catalog ORDER BY generic_name'); + + $conditionInsert = $pdo->prepare( + "INSERT OR IGNORE INTO medication_condition_links + (medication_id, condition_id, relationship, note, source_name, source_url, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?)" + ); + $symptomInsert = $pdo->prepare( + "INSERT OR IGNORE INTO medication_symptom_links + (medication_id, symptom_id, relationship, note, source_name, source_url, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?)" + ); + $medicineUpdate = $pdo->prepare( + "UPDATE medication_catalog + SET class_name = ?, drug_family = ?, common_use = ?, search_terms = ? + WHERE id = ?" + ); + $metadataUpsert = $pdo->prepare( + "INSERT INTO app_metadata (key, value, updated_at) + VALUES (?, ?, ?) + ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at" + ); + + $startedTransaction = !$pdo->inTransaction(); + if ($startedTransaction) { + $pdo->beginTransaction(); + } + + try { + $pdo->prepare('DELETE FROM medication_condition_links WHERE source_name = ?')->execute([$sourceName]); + $pdo->prepare('DELETE FROM medication_symptom_links WHERE source_name = ?')->execute([$sourceName]); + + foreach ($medicines as $medicine) { + $haystack = relationship_haystack($medicine); + $matchedRules = []; + foreach ($rules as $rule) { + if (relationship_rule_matches($haystack, $rule)) { + $matchedRules[] = $rule; + } + } + $className = (string) ($medicine['class_name'] ?? ''); + $drugFamily = (string) ($medicine['drug_family'] ?? ''); + $commonUse = (string) ($medicine['common_use'] ?? ''); + $searchTerms = strip_relationship_search_phrases((string) ($medicine['search_terms'] ?? '')); + if (!$matchedRules) { + $nextCommonUse = generated_relationship_common_use($commonUse) + ? 'Imported reference entry. Review official labels and clinician guidance before use.' + : $commonUse; + if ($searchTerms !== (string) ($medicine['search_terms'] ?? '') || $nextCommonUse !== $commonUse) { + $medicineUpdate->execute([$className, $drugFamily, $nextCommonUse, $searchTerms, (int) $medicine['id']]); + } + continue; + } + + $medicineId = (int) $medicine['id']; + foreach (unique_relationship_names($matchedRules, 'conditions') as $conditionName) { + $conditionId = $conditionLookup[strtolower($conditionName)] ?? 0; + if ($conditionId <= 0) { + continue; + } + $conditionInsert->execute([ + $medicineId, + $conditionId, + 'common use', + $matchedRules[0]['note'], + $sourceName, + $sourceUrl, + $created, + ]); + } + + foreach (unique_relationship_names($matchedRules, 'symptoms') as $symptomName) { + $symptomId = $symptomLookup[strtolower($symptomName)] ?? 0; + if ($symptomId <= 0) { + continue; + } + $symptomInsert->execute([ + $medicineId, + $symptomId, + 'symptom context', + $matchedRules[0]['note'], + $sourceName, + $sourceUrl, + $created, + ]); + } + + $firstRule = $matchedRules[0]; + $nextClassName = strtolower($className) === 'imported medicine concept' + ? (string) $firstRule['medicine_class'] + : $className; + $nextDrugFamily = strtolower($drugFamily) === 'imported reference' + ? (string) $firstRule['family'] + : $drugFamily; + $nextCommonUse = generic_import_common_use($commonUse) || generated_relationship_common_use($commonUse) + ? relationship_common_use($matchedRules) + : $commonUse; + $nextSearchTerms = merge_search_phrases( + $searchTerms, + [ + ...unique_relationship_names($matchedRules, 'conditions'), + ...unique_relationship_names($matchedRules, 'symptoms'), + $firstRule['family'], + $firstRule['medicine_class'], + ] + ); + + if ($nextClassName !== $className || $nextDrugFamily !== $drugFamily || $nextCommonUse !== $commonUse || $nextSearchTerms !== $searchTerms) { + $medicineUpdate->execute([$nextClassName, $nextDrugFamily, $nextCommonUse, $nextSearchTerms, $medicineId]); + } + } + + $metadataUpsert->execute([$metaKey, $signature, app_now()]); + + if ($startedTransaction) { + $pdo->commit(); + } + } catch (Throwable $exception) { + if ($startedTransaction && $pdo->inTransaction()) { + $pdo->rollBack(); + } + throw $exception; + } +} + +function linked_conditions_for_medicine(PDO $pdo, int $medicationId, int $limit = 16): array +{ + return db_all( + $pdo, + "SELECT medical_conditions.*, + medication_condition_links.relationship AS link_relationship, + medication_condition_links.note AS link_note, + medication_condition_links.source_name AS link_source_name, + medication_condition_links.source_url AS link_source_url + FROM medication_condition_links + INNER JOIN medical_conditions ON medical_conditions.id = medication_condition_links.condition_id + WHERE medication_condition_links.medication_id = ? + ORDER BY medical_conditions.category, medical_conditions.name + LIMIT {$limit}", + [$medicationId] + ); +} + +function linked_symptoms_for_medicine(PDO $pdo, int $medicationId, int $limit = 16): array +{ + return db_all( + $pdo, + "SELECT medical_symptoms.*, + medication_symptom_links.relationship AS link_relationship, + medication_symptom_links.note AS link_note, + medication_symptom_links.source_name AS link_source_name, + medication_symptom_links.source_url AS link_source_url + FROM medication_symptom_links + INNER JOIN medical_symptoms ON medical_symptoms.id = medication_symptom_links.symptom_id + WHERE medication_symptom_links.medication_id = ? + ORDER BY medical_symptoms.category, medical_symptoms.name + LIMIT {$limit}", + [$medicationId] + ); +} + +function linked_medicines_for_condition(PDO $pdo, int $conditionId, int $limit = 16): array +{ + return db_all( + $pdo, + "SELECT medication_catalog.*, + medication_condition_links.relationship AS link_relationship, + medication_condition_links.note AS link_note, + medication_condition_links.source_name AS link_source_name, + medication_condition_links.source_url AS link_source_url + FROM medication_condition_links + INNER JOIN medication_catalog ON medication_catalog.id = medication_condition_links.medication_id + WHERE medication_condition_links.condition_id = ? + ORDER BY + CASE + WHEN medication_catalog.source_name LIKE '%MedlinePlus%' THEN 0 + WHEN medication_catalog.source_name LIKE '%DailyMed%' THEN 1 + ELSE 2 + END, + CASE + WHEN medication_catalog.generic_name GLOB '[A-Za-z]*' THEN 0 + ELSE 1 + END, + medication_catalog.generic_name + LIMIT {$limit}", + [$conditionId] + ); +} + +function linked_medicines_for_symptom(PDO $pdo, int $symptomId, int $limit = 16): array +{ + return db_all( + $pdo, + "SELECT medication_catalog.*, + medication_symptom_links.relationship AS link_relationship, + medication_symptom_links.note AS link_note, + medication_symptom_links.source_name AS link_source_name, + medication_symptom_links.source_url AS link_source_url + FROM medication_symptom_links + INNER JOIN medication_catalog ON medication_catalog.id = medication_symptom_links.medication_id + WHERE medication_symptom_links.symptom_id = ? + ORDER BY + CASE + WHEN medication_catalog.source_name LIKE '%MedlinePlus%' THEN 0 + WHEN medication_catalog.source_name LIKE '%DailyMed%' THEN 1 + ELSE 2 + END, + CASE + WHEN medication_catalog.generic_name GLOB '[A-Za-z]*' THEN 0 + ELSE 1 + END, + medication_catalog.generic_name + LIMIT {$limit}", + [$symptomId] + ); +} + function db_key_values(PDO $pdo, string $sql, array $params = []): array { $rows = db_all($pdo, $sql, $params); @@ -2135,7 +2774,7 @@ function state_payload(array $query): array $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'); + $medicationCatalog = medicine_rows_with_use_summary(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, @@ -2252,16 +2891,16 @@ function library_search_payload(array $query): array '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]), + 'medicationCatalog' => medicine_rows_with_use_summary(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), $search, $search . '%', $like]; - $symptomParams = [...array_fill(0, 8, $like), $search, $search . '%', $like]; - $medicineParams = array_fill(0, 10, $like); + $conditionParams = [...array_fill(0, 4, $like), ...array_fill(0, 6, $like), $search, $search . '%', $like]; + $symptomParams = [...array_fill(0, 8, $like), ...array_fill(0, 6, $like), $search, $search . '%', $like]; + $medicineParams = [...array_fill(0, 12, $like), ...array_fill(0, 4, $like), ...array_fill(0, 5, $like)]; $studyParams = array_fill(0, 6, $like); return [ @@ -2279,6 +2918,20 @@ function library_search_payload(array $query): array $pdo, "SELECT * FROM medical_conditions WHERE name LIKE ? OR category LIKE ? OR overview LIKE ? OR common_tracking LIKE ? + OR EXISTS ( + SELECT 1 + FROM medication_condition_links + INNER JOIN medication_catalog ON medication_catalog.id = medication_condition_links.medication_id + WHERE medication_condition_links.condition_id = medical_conditions.id + AND ( + medication_catalog.generic_name LIKE ? + OR medication_catalog.active_ingredient LIKE ? + OR medication_catalog.brand_names LIKE ? + OR medication_catalog.common_use LIKE ? + OR medication_catalog.official_use LIKE ? + OR medication_catalog.search_terms LIKE ? + ) + ) ORDER BY CASE WHEN lower(name) = lower(?) THEN 0 @@ -2296,6 +2949,20 @@ function library_search_payload(array $query): array 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 ? + OR EXISTS ( + SELECT 1 + FROM medication_symptom_links + INNER JOIN medication_catalog ON medication_catalog.id = medication_symptom_links.medication_id + WHERE medication_symptom_links.symptom_id = medical_symptoms.id + AND ( + medication_catalog.generic_name LIKE ? + OR medication_catalog.active_ingredient LIKE ? + OR medication_catalog.brand_names LIKE ? + OR medication_catalog.common_use LIKE ? + OR medication_catalog.official_use LIKE ? + OR medication_catalog.search_terms LIKE ? + ) + ) ORDER BY CASE WHEN lower(name) = lower(?) THEN 0 @@ -2307,17 +2974,42 @@ function library_search_payload(array $query): array LIMIT {$limit}", $symptomParams ), - 'medicationCatalog' => db_all( + 'medicationCatalog' => medicine_rows_with_use_summary(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 ? + OR official_use LIKE ? OR official_use_source_name LIKE ? OR search_terms LIKE ? + OR EXISTS ( + SELECT 1 + FROM medication_condition_links + INNER JOIN medical_conditions ON medical_conditions.id = medication_condition_links.condition_id + WHERE medication_condition_links.medication_id = medication_catalog.id + AND ( + medical_conditions.name LIKE ? + OR medical_conditions.category LIKE ? + OR medical_conditions.overview LIKE ? + OR medical_conditions.common_tracking LIKE ? + ) + ) + OR EXISTS ( + SELECT 1 + FROM medication_symptom_links + INNER JOIN medical_symptoms ON medical_symptoms.id = medication_symptom_links.symptom_id + WHERE medication_symptom_links.medication_id = medication_catalog.id + AND ( + medical_symptoms.name LIKE ? + OR medical_symptoms.category LIKE ? + OR medical_symptoms.body_system LIKE ? + OR medical_symptoms.plain_language_description LIKE ? + OR medical_symptoms.search_terms LIKE ? + ) + ) ORDER BY generic_name LIMIT {$limit}", $medicineParams - ), + )), 'researchStudies' => db_all( $pdo, "SELECT * FROM research_studies @@ -2362,7 +3054,73 @@ function http_json(string $url, int $timeout = 20): ?array return is_array($json) ? $json : null; } -function medication_import_row(string $name, string $source, string $brandNames = '', string $className = 'Imported medicine concept'): array +function clean_label_use_text(array|string|null $value, int $maxLength = 520): string +{ + if (is_array($value)) { + $value = implode(' ', array_filter(array_map(static fn($item): string => trim((string) $item), $value))); + } + + $text = trim((string) $value); + if ($text === '') { + return ''; + } + + $text = html_entity_decode(strip_tags($text), ENT_QUOTES | ENT_HTML5); + $text = preg_replace('/\s+/', ' ', $text) ?? $text; + $text = preg_replace('/^\s*(\d+(\.\d+)?\s*)?(INDICATIONS AND USAGE|INDICATIONS|USES?|USAGE|PURPOSE)\s*[:.&-]?\s*/i', '', $text) ?? $text; + $text = preg_replace('/^\s*&\s*Usage\s*/i', '', $text) ?? $text; + $text = trim($text, " \t\n\r\0\x0B:-"); + if ($text === '') { + return ''; + } + + if (strlen($text) <= $maxLength) { + return $text; + } + + $short = substr($text, 0, $maxLength); + $lastSentence = max(strrpos($short, '. '), strrpos($short, '; ')); + if ($lastSentence !== false && $lastSentence > 160) { + $short = substr($short, 0, $lastSentence + 1); + } + + return rtrim($short, " \t\n\r\0\x0B,;:.") . '.'; +} + +function label_use_from_openfda_result(array $result): string +{ + $indications = clean_label_use_text($result['indications_and_usage'] ?? null); + if ($indications !== '') { + return $indications; + } + + return clean_label_use_text($result['purpose'] ?? null); +} + +function openfda_label_source_url(array $result): string +{ + $setId = trim((string) ($result['set_id'] ?? '')); + if ($setId !== '') { + return 'https://api.fda.gov/drug/label.json?search=set_id:' . rawurlencode('"' . $setId . '"'); + } + + $id = trim((string) ($result['id'] ?? '')); + if ($id !== '') { + return 'https://api.fda.gov/drug/label.json?search=id:' . rawurlencode('"' . $id . '"'); + } + + return 'https://open.fda.gov/apis/drug/label/'; +} + +function medication_import_row( + string $name, + string $source, + string $brandNames = '', + string $className = 'Imported medicine concept', + string $officialUse = '', + string $officialUseSourceName = '', + string $officialUseSourceUrl = '' +): array { $cleanName = preg_replace('/\s+/', ' ', trim($name)); if ($cleanName === '') { @@ -2375,6 +3133,9 @@ function medication_import_row(string $name, string $source, string $brandNames 'openFDA' => 'openFDA / FDA', default => $source, }; + $useSummary = clean_label_use_text($officialUse); + $sourceUseName = $officialUseSourceName !== '' ? $officialUseSourceName : ($useSummary !== '' ? $sourceLabel . ' label' : ''); + $sourceUseUrl = $officialUseSourceUrl !== '' ? $officialUseSourceUrl : ''; return [ 'generic_name' => strtolower($cleanName), @@ -2384,7 +3145,12 @@ function medication_import_row(string $name, string $source, string $brandNames 'generic_brands' => $cleanName, 'class_name' => $className, 'drug_family' => 'Imported reference', - 'common_use' => "Imported from {$sourceLabel}. Review official labels and clinician guidance before use.", + 'common_use' => $useSummary !== '' + ? 'Used for: ' . $useSummary + : "Imported from {$sourceLabel}. Review official labels and clinician guidance before use.", + 'official_use' => $useSummary, + 'official_use_source_name' => $sourceUseName, + 'official_use_source_url' => $sourceUseUrl, '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', @@ -2393,7 +3159,7 @@ function medication_import_row(string $name, string $source, string $brandNames '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]), + 'search_terms' => implode(' ', [$cleanName, $brandNames, $className, $sourceLabel, $useSummary]), ]; } @@ -2421,24 +3187,154 @@ function fetch_rxnorm_medication_rows(int $limit): array return $rows; } -function fetch_openfda_medication_rows(int $limit): array +function openfda_text_values(array $openfda, string $field): 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)) { + $values = $openfda[$field] ?? []; + if (!is_array($values)) { + $values = [$values]; + } + + $clean = []; + foreach ($values as $value) { + $text = preg_replace('/\s+/', ' ', trim((string) $value)); + if ($text === '') { + continue; + } + $clean[strtolower($text)] = $text; + } + return array_values($clean); +} + +function openfda_label_medication_rows_from_result(array $result): array +{ + $officialUse = label_use_from_openfda_result($result); + if ($officialUse === '') { + return []; + } + + $openfda = is_array($result['openfda'] ?? null) ? $result['openfda'] : []; + $genericNames = openfda_text_values($openfda, 'generic_name'); + $brandNames = openfda_text_values($openfda, 'brand_name'); + $substances = openfda_text_values($openfda, 'substance_name'); + $names = $genericNames ?: ($substances ?: $brandNames); + if (!$names) { return []; } $rows = []; - foreach ($results as $result) { - if (count($rows) >= $limit) { + $brandText = implode(', ', $brandNames); + $activeText = implode(', ', $substances ?: $genericNames); + $genericText = implode(', ', $genericNames ?: $substances); + $sourceUrl = openfda_label_source_url($result); + + foreach ($names as $name) { + $row = medication_import_row( + $name, + 'openFDA', + $brandText, + 'Official label medicine', + $officialUse, + 'openFDA drug label', + $sourceUrl + ); + if (!$row) { + continue; + } + if ($activeText !== '') { + $row['active_ingredient'] = $activeText; + $row['chemical_name'] = $activeText; + } + if ($genericText !== '') { + $row['generic_brands'] = $genericText; + } + $row['drug_family'] = 'Official label use'; + $row['dailymed_url'] = medication_source_url('https://dailymed.nlm.nih.gov/dailymed/search.cfm?query=', $name); + $rows[] = $row; + } + + return $rows; +} + +function fetch_openfda_label_medication_rows(int $limit): array +{ + $rows = []; + $seen = []; + $searches = ['_exists_:indications_and_usage', '_exists_:purpose']; + foreach ($searches as $search) { + $skip = 0; + while (count($rows) < $limit && $skip <= 25000) { + $remaining = $limit - count($rows); + $pageLimit = min(1000, max(25, $remaining)); + $url = 'https://api.fda.gov/drug/label.json?search=' . rawurlencode($search) + . '&limit=' . $pageLimit + . '&skip=' . $skip; + $json = http_json($url); + $results = $json['results'] ?? []; + if (!is_array($results) || !$results) { + break; + } + + foreach ($results as $result) { + if (!is_array($result)) { + continue; + } + foreach (openfda_label_medication_rows_from_result($result) as $row) { + $key = strtolower((string) $row['generic_name']); + if (isset($seen[$key])) { + continue; + } + $seen[$key] = true; + $rows[] = $row; + if (count($rows) >= $limit) { + break 2; + } + } + } + + if (count($results) < $pageLimit) { + break; + } + $skip += $pageLimit; + } + } + + return $rows; +} + +function fetch_openfda_medication_rows(int $limit): array +{ + $rows = fetch_openfda_label_medication_rows($limit); + $seen = array_fill_keys(array_map(static fn(array $row): string => strtolower((string) $row['generic_name']), $rows), true); + $fields = [ + 'openfda.generic_name.exact' => ['Imported medicine concept', ''], + 'openfda.brand_name.exact' => ['Imported brand medicine concept', 'brand'], + 'openfda.substance_name.exact' => ['Imported active substance', 'substance'], + ]; + + foreach ($fields as $field => [$className, $kind]) { + $remaining = $limit - count($rows); + if ($remaining <= 0) { break; } - $term = (string) ($result['term'] ?? ''); - $row = medication_import_row($term, 'openFDA'); - if ($row) { - $rows[] = $row; + $url = 'https://api.fda.gov/drug/label.json?count=' . rawurlencode($field) . '&limit=' . $remaining; + $json = http_json($url); + $results = $json['results'] ?? []; + if (!is_array($results)) { + continue; + } + + foreach ($results as $result) { + if (count($rows) >= $limit) { + break 2; + } + $term = (string) ($result['term'] ?? ''); + $brandNames = $kind === 'brand' ? $term : ''; + $row = medication_import_row($term, 'openFDA', $brandNames, $className); + $key = strtolower((string) ($row['generic_name'] ?? '')); + if ($row && !isset($seen[$key])) { + $seen[$key] = true; + $rows[] = $row; + } } } return $rows; @@ -2489,14 +3385,16 @@ function upsert_imported_medication_rows(PDO $pdo, array $rows): array $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, + common_use, official_use, official_use_source_name, official_use_source_url, tracking_note, + rxnorm_url, medlineplus_url, pubchem_url, dailymed_url, pubmed_url, clinicaltrials_url, source_name, search_terms, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" + 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 = ?, + class_name = ?, drug_family = ?, common_use = ?, official_use = ?, + official_use_source_name = ?, official_use_source_url = ?, tracking_note = ?, rxnorm_url = ?, medlineplus_url = ?, pubchem_url = ?, dailymed_url = ?, pubmed_url = ?, clinicaltrials_url = ?, source_name = ?, search_terms = ? WHERE id = ?" @@ -2505,72 +3403,116 @@ function upsert_imported_medication_rows(PDO $pdo, array $rows): array $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, + $startedTransaction = !$pdo->inTransaction(); + if ($startedTransaction) { + $pdo->beginTransaction(); + } + + try { + 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['official_use'], + $row['official_use_source_name'], + $row['official_use_source_url'], + $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', '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]; + } + $incomingOfficialUse = trim((string) ($row['official_use'] ?? '')); + $next['official_use'] = $incomingOfficialUse !== '' + ? $incomingOfficialUse + : (string) ($existing['official_use'] ?? ''); + $next['official_use_source_name'] = trim((string) ($row['official_use_source_name'] ?? '')) !== '' + ? (string) $row['official_use_source_name'] + : (string) ($existing['official_use_source_name'] ?? ''); + $next['official_use_source_url'] = trim((string) ($row['official_use_source_url'] ?? '')) !== '' + ? (string) $row['official_use_source_url'] + : (string) ($existing['official_use_source_url'] ?? ''); + $existingCommonUse = (string) ($existing['common_use'] ?? ''); + $existingCommonUseLower = strtolower(trim($existingCommonUse)); + $shouldReplaceCommonUseWithOfficial = $incomingOfficialUse !== '' + && ( + generic_import_common_use($existingCommonUse) + || generated_relationship_common_use($existingCommonUse) + || str_starts_with($existingCommonUseLower, 'used for:') + ); + $next['common_use'] = $shouldReplaceCommonUseWithOfficial + ? (string) $row['common_use'] + : (trim($existingCommonUse) !== '' ? $existingCommonUse : (string) $row['common_use']); + $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['official_use'], + $next['official_use_source_name'], + $next['official_use_source_url'], + $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'], ]); - $inserted++; - continue; + $updated++; } - $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]; + if ($startedTransaction) { + $pdo->commit(); } - $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++; + } catch (Throwable $exception) { + if ($startedTransaction && $pdo->inTransaction()) { + $pdo->rollBack(); + } + throw $exception; } return ['inserted' => $inserted, 'updated' => $updated, 'seen' => count($seen)]; @@ -2627,6 +3569,8 @@ function refresh_medication_sources(array $data): array } } + seed_medical_relationships($pdo, true); + return [ 'inserted' => $totalInserted, 'updated' => $totalUpdated, diff --git a/data/health_tracker.sqlite b/data/health_tracker.sqlite index 2e977f1..4ca4c9d 100644 Binary files a/data/health_tracker.sqlite and b/data/health_tracker.sqlite differ diff --git a/public/condition.php b/public/condition.php index 2bd8aae..4364f4c 100644 --- a/public/condition.php +++ b/public/condition.php @@ -14,12 +14,21 @@ $relatedSymptoms = []; $relatedTerms = []; $relatedMedicines = []; $relatedStudies = []; +$linkedMedicines = []; if ($condition) { + $linkedMedicines = linked_medicines_for_condition($pdo, (int) $condition['id']); $related = library_search_payload(['q' => (string) $condition['name'], 'limit' => 8]); $relatedSymptoms = $related['medicalSymptoms']; $relatedTerms = $related['medicalTerms']; $relatedMedicines = $related['medicationCatalog']; $relatedStudies = $related['researchStudies']; + if ($linkedMedicines) { + $linkedIds = array_fill_keys(array_map(static fn(array $row): int => (int) $row['id'], $linkedMedicines), true); + $relatedMedicines = array_values(array_filter( + $relatedMedicines, + static fn(array $row): bool => !isset($linkedIds[(int) $row['id']]) + )); + } } else { http_response_code(404); } @@ -69,6 +78,32 @@ require __DIR__ . '/partials/site-header.php'; + +
+
+

Medicine links

+

Commonly linked medicines

+
+
+ + + +
+
+ +
@@ -111,7 +146,7 @@ require __DIR__ . '/partials/site-header.php';

-

+

diff --git a/public/medical.php b/public/medical.php index 0473eae..b013d12 100644 --- a/public/medical.php +++ b/public/medical.php @@ -149,7 +149,7 @@ require __DIR__ . '/partials/site-header.php';

-

+

Active: diff --git a/public/medicine.php b/public/medicine.php index e0eb6d6..1835541 100644 --- a/public/medicine.php +++ b/public/medicine.php @@ -12,9 +12,14 @@ $activePage = 'medicines'; $relatedStudies = []; $referenceLinks = []; +$linkedConditions = []; +$linkedSymptoms = []; +$useSourceLinks = []; if ($medicine) { $genericName = (string) $medicine['generic_name']; $like = '%' . $genericName . '%'; + $linkedConditions = linked_conditions_for_medicine($pdo, (int) $medicine['id']); + $linkedSymptoms = linked_symptoms_for_medicine($pdo, (int) $medicine['id']); $relatedStudies = db_all( $pdo, "SELECT * @@ -25,6 +30,7 @@ if ($medicine) { [$like, $like, $like] ); $referenceLinks = medicine_reference_links($medicine); + $useSourceLinks = medicine_use_source_link($medicine); } else { http_response_code(404); } @@ -45,7 +51,7 @@ require __DIR__ . '/partials/site-header.php';

Medicine detail

-

+

Open tracker @@ -62,6 +68,17 @@ require __DIR__ . '/partials/site-header.php';
+
+ What It Is Used For +

+ + + +
Active Ingredient

@@ -93,6 +110,48 @@ require __DIR__ . '/partials/site-header.php';
+ +
+
+

Clinical links

+

Linked conditions and symptoms

+
+
+ + + + + + + +
+
+ +
diff --git a/public/nutrition.php b/public/nutrition.php index 9b585d6..2dc907a 100644 --- a/public/nutrition.php +++ b/public/nutrition.php @@ -63,7 +63,7 @@ require __DIR__ . '/partials/site-header.php';

-

+

Active: diff --git a/public/symptom.php b/public/symptom.php index 5d85c21..0b84b13 100644 --- a/public/symptom.php +++ b/public/symptom.php @@ -14,12 +14,21 @@ $relatedTerms = []; $relatedConditions = []; $relatedMedicines = []; $relatedStudies = []; +$linkedMedicines = []; if ($symptom) { + $linkedMedicines = linked_medicines_for_symptom($pdo, (int) $symptom['id']); $related = library_search_payload(['q' => (string) $symptom['name'], 'limit' => 8]); $relatedTerms = $related['medicalTerms']; $relatedConditions = $related['medicalConditions']; $relatedMedicines = $related['medicationCatalog']; $relatedStudies = $related['researchStudies']; + if ($linkedMedicines) { + $linkedIds = array_fill_keys(array_map(static fn(array $row): int => (int) $row['id'], $linkedMedicines), true); + $relatedMedicines = array_values(array_filter( + $relatedMedicines, + static fn(array $row): bool => !isset($linkedIds[(int) $row['id']]) + )); + } } else { http_response_code(404); } @@ -74,6 +83,32 @@ require __DIR__ . '/partials/site-header.php';
+ +
+
+

Medicine links

+

Commonly linked medicines

+
+
+ + + +
+
+ +
@@ -115,7 +150,7 @@ require __DIR__ . '/partials/site-header.php';

-

+

diff --git a/public/term.php b/public/term.php index 7cc5ffb..b72bae2 100644 --- a/public/term.php +++ b/public/term.php @@ -93,7 +93,7 @@ require __DIR__ . '/partials/site-header.php';

-

+