- The medicine database is now much bigger and has a real update path.
Added: Live source updater for RxNorm, DailyMed, and openFDA. Admin dashboard panel to refresh medicine sources. CLI updater for periodic jobs: [scripts/update_medicine_catalog.php](/Users/tyemeclifford/Documents/GH/workout/scripts/update_medicine_catalog.php) Update history table: medication_source_updates. Curl fallback for external fetches when PHP stream DNS fails. README instructions for periodic updates. I also ran the imports: Current medication catalog count: 6,002. Latest source imports:RxNorm: 3,875 inserted, 1,125 updated. openFDA: 912 inserted, 88 updated. DailyMed: 95 inserted. Periodic update command: /Users/tyemeclifford/frankenphp php-cli scripts/update_medicine_catalog.php 5000 all
This commit is contained in:
@@ -35,12 +35,13 @@ SQLite is created automatically at:
|
|||||||
data/health_tracker.sqlite
|
data/health_tracker.sqlite
|
||||||
```
|
```
|
||||||
|
|
||||||
The app seeds sample profiles, medical terminology, condition summaries, a medication catalog, NIH/NLM study-link categories, medication logs, vitals, labs, symptoms, allergies, appointments, immunizations, and the older optional wellness/fitness data.
|
The app seeds sample profiles, medical terminology, condition summaries, an expanded medication catalog, NIH/NLM study-link categories, medication logs, vitals, labs, symptoms, allergies, appointments, immunizations, and the older optional wellness/fitness data.
|
||||||
|
|
||||||
## Included
|
## Included
|
||||||
|
|
||||||
- Multi-user profiles with separate records and theme preferences.
|
- Multi-user profiles with separate records and theme preferences.
|
||||||
- Admin dashboard for creating, editing, activating/deactivating, and deleting user profiles.
|
- Admin dashboard for creating, editing, activating/deactivating, and deleting user profiles.
|
||||||
|
- Admin medicine-source updater for refreshing the catalog from RxNorm, DailyMed, and openFDA.
|
||||||
- First user created in a database is automatically promoted to `admin`; later users default to `member`.
|
- First user created in a database is automatically promoted to `admin`; later users default to `member`.
|
||||||
- Daily, weekly, and monthly views for medical logs.
|
- Daily, weekly, and monthly views for medical logs.
|
||||||
- Medication logging by date, dose, route, frequency, status, prescriber, and notes.
|
- Medication logging by date, dose, route, frequency, status, prescriber, and notes.
|
||||||
@@ -48,14 +49,31 @@ The app seeds sample profiles, medical terminology, condition summaries, a medic
|
|||||||
- Lab result records with units, reference ranges, clinician/source notes, and collection dates.
|
- Lab result records with units, reference ranges, clinician/source notes, and collection dates.
|
||||||
- Symptom journal, allergy list, appointments, and immunization records.
|
- Symptom journal, allergy list, appointments, and immunization records.
|
||||||
- Medical terminology and condition database with source links.
|
- Medical terminology and condition database with source links.
|
||||||
- Medicine catalog with RxNorm, MedlinePlus, PubMed, and ClinicalTrials.gov links.
|
- Searchable medicine catalog by generic name, active ingredient, chemical name, common brand names, and generic/alternate names.
|
||||||
|
- Medicine links to RxNorm, DailyMed, PubChem, MedlinePlus, PubMed, and ClinicalTrials.gov.
|
||||||
- NIH/NLM research-link categories related to the listed medicines.
|
- NIH/NLM research-link categories related to the listed medicines.
|
||||||
- Neon dark theme by default, light mode option, and green, blue, red, pink, and orange accents.
|
- Neon dark theme by default, light mode option, and green, blue, red, pink, and orange accents.
|
||||||
- Optional legacy fitness, nutrition, and supplement tracking remains available as wellness context.
|
- Optional legacy fitness, nutrition, and supplement tracking remains available as wellness context.
|
||||||
|
|
||||||
|
## Medicine Catalog Updates
|
||||||
|
|
||||||
|
The catalog ships with a curated starter set, the current SQLite file includes thousands of imported medicine concepts, and it can be refreshed from public medicine sources.
|
||||||
|
|
||||||
|
From the admin dashboard, use the medicine source updater and choose `All sources`, `RxNorm`, `DailyMed`, or `openFDA`.
|
||||||
|
|
||||||
|
From the command line:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
/Users/tyemeclifford/frankenphp php-cli scripts/update_medicine_catalog.php 1000 all
|
||||||
|
```
|
||||||
|
|
||||||
|
For a periodic cron-style update, 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`.
|
||||||
|
|
||||||
## Reference Sources
|
## Reference Sources
|
||||||
|
|
||||||
- RxNorm and RxNav from the U.S. National Library of Medicine: `https://www.nlm.nih.gov/research/umls/rxnorm/index.html`
|
- RxNorm and RxNav from the U.S. National Library of Medicine: `https://www.nlm.nih.gov/research/umls/rxnorm/index.html`
|
||||||
|
- DailyMed label search from the U.S. National Library of Medicine: `https://dailymed.nlm.nih.gov/dailymed/`
|
||||||
|
- PubChem chemical/compound search from NCBI: `https://pubchem.ncbi.nlm.nih.gov/`
|
||||||
- MedlinePlus condition and medication pages: `https://medlineplus.gov/`
|
- MedlinePlus condition and medication pages: `https://medlineplus.gov/`
|
||||||
- PubMed search links for medicine and condition research: `https://pubmed.ncbi.nlm.nih.gov/`
|
- PubMed search links for medicine and condition research: `https://pubmed.ncbi.nlm.nih.gov/`
|
||||||
- ClinicalTrials.gov search links for study discovery: `https://clinicaltrials.gov/`
|
- ClinicalTrials.gov search links for study discovery: `https://clinicaltrials.gov/`
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ function app_db(): PDO
|
|||||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
||||||
$pdo->exec('PRAGMA foreign_keys = ON');
|
$pdo->exec('PRAGMA foreign_keys = ON');
|
||||||
|
$pdo->exec('PRAGMA busy_timeout = 5000');
|
||||||
|
|
||||||
app_init_db($pdo);
|
app_init_db($pdo);
|
||||||
|
|
||||||
@@ -315,6 +316,16 @@ CREATE TABLE IF NOT EXISTS research_studies (
|
|||||||
created_at TEXT NOT NULL
|
created_at TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS medication_source_updates (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
source TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL,
|
||||||
|
imported_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
updated_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
message TEXT DEFAULT '',
|
||||||
|
created_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_workouts_user_date ON workouts(user_id, scheduled_on);
|
CREATE INDEX IF NOT EXISTS idx_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_nutrition_logs_user_date ON nutrition_logs(user_id, logged_on);
|
||||||
CREATE INDEX IF NOT EXISTS idx_supplement_logs_user_date ON supplement_logs(user_id, taken_on);
|
CREATE INDEX IF NOT EXISTS idx_supplement_logs_user_date ON supplement_logs(user_id, taken_on);
|
||||||
@@ -328,6 +339,7 @@ CREATE INDEX IF NOT EXISTS idx_appointments_user_date ON appointments(user_id, a
|
|||||||
CREATE INDEX IF NOT EXISTS idx_immunization_records_user_date ON immunization_records(user_id, administered_on);
|
CREATE INDEX IF NOT EXISTS idx_immunization_records_user_date ON immunization_records(user_id, administered_on);
|
||||||
CREATE INDEX IF NOT EXISTS idx_medical_terms_term ON medical_terms(term);
|
CREATE INDEX IF NOT EXISTS idx_medical_terms_term ON medical_terms(term);
|
||||||
CREATE INDEX IF NOT EXISTS idx_medication_catalog_generic ON medication_catalog(generic_name);
|
CREATE INDEX IF NOT EXISTS idx_medication_catalog_generic ON medication_catalog(generic_name);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_medication_source_updates_created ON medication_source_updates(created_at);
|
||||||
SQL);
|
SQL);
|
||||||
|
|
||||||
migrate_users_table($pdo);
|
migrate_users_table($pdo);
|
||||||
@@ -611,6 +623,12 @@ function seed_expanded_medication_catalog(PDO $pdo): void
|
|||||||
{
|
{
|
||||||
$created = app_now();
|
$created = app_now();
|
||||||
$rows = expanded_medication_catalog_rows();
|
$rows = expanded_medication_catalog_rows();
|
||||||
|
$existingCount = (int) $pdo->query('SELECT COUNT(*) FROM medication_catalog')->fetchColumn();
|
||||||
|
$needsSearchText = (int) $pdo->query("SELECT COUNT(*) FROM medication_catalog WHERE search_terms IS NULL OR search_terms = ''")->fetchColumn();
|
||||||
|
$needsChemicalNames = (int) $pdo->query("SELECT COUNT(*) FROM medication_catalog WHERE chemical_name IS NULL OR chemical_name = ''")->fetchColumn();
|
||||||
|
if ($existingCount >= count($rows) && $needsSearchText === 0 && $needsChemicalNames === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$select = $pdo->prepare('SELECT id FROM medication_catalog WHERE lower(generic_name) = lower(?) LIMIT 1');
|
$select = $pdo->prepare('SELECT id FROM medication_catalog WHERE lower(generic_name) = lower(?) LIMIT 1');
|
||||||
$insert = $pdo->prepare(
|
$insert = $pdo->prepare(
|
||||||
@@ -1705,6 +1723,312 @@ function library_search_payload(array $query): array
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function http_json(string $url, int $timeout = 20): ?array
|
||||||
|
{
|
||||||
|
$context = stream_context_create([
|
||||||
|
'http' => [
|
||||||
|
'method' => 'GET',
|
||||||
|
'timeout' => $timeout,
|
||||||
|
'header' => "Accept: application/json\r\nUser-Agent: NeonMedicalTracker/1.0\r\n",
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
$body = @file_get_contents($url, false, $context);
|
||||||
|
if (($body === false || $body === '') && function_exists('shell_exec')) {
|
||||||
|
$curl = trim((string) shell_exec('command -v curl 2>/dev/null'));
|
||||||
|
if ($curl === '' && is_executable('/usr/bin/curl')) {
|
||||||
|
$curl = '/usr/bin/curl';
|
||||||
|
}
|
||||||
|
if ($curl !== '') {
|
||||||
|
$command = escapeshellarg($curl)
|
||||||
|
. ' -fsSL --max-time ' . (int) $timeout
|
||||||
|
. ' -H ' . escapeshellarg('Accept: application/json')
|
||||||
|
. ' -A ' . escapeshellarg('NeonMedicalTracker/1.0')
|
||||||
|
. ' ' . escapeshellarg($url)
|
||||||
|
. ' 2>/dev/null';
|
||||||
|
$body = shell_exec($command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!is_string($body) || $body === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$json = json_decode($body, true);
|
||||||
|
return is_array($json) ? $json : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function medication_import_row(string $name, string $source, string $brandNames = '', string $className = 'Imported medicine concept'): array
|
||||||
|
{
|
||||||
|
$cleanName = preg_replace('/\s+/', ' ', trim($name));
|
||||||
|
if ($cleanName === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$sourceLabel = match ($source) {
|
||||||
|
'RxNorm' => 'RxNorm / NLM',
|
||||||
|
'DailyMed' => 'DailyMed / NLM',
|
||||||
|
'openFDA' => 'openFDA / FDA',
|
||||||
|
default => $source,
|
||||||
|
};
|
||||||
|
|
||||||
|
return [
|
||||||
|
'generic_name' => strtolower($cleanName),
|
||||||
|
'active_ingredient' => $cleanName,
|
||||||
|
'chemical_name' => $cleanName,
|
||||||
|
'brand_names' => $brandNames,
|
||||||
|
'generic_brands' => $cleanName,
|
||||||
|
'class_name' => $className,
|
||||||
|
'drug_family' => 'Imported reference',
|
||||||
|
'common_use' => "Imported from {$sourceLabel}. Review official labels and clinician guidance before use.",
|
||||||
|
'tracking_note' => 'Use this entry as a reference/search starting point; verify dose, indication, and safety details with official labeling or a clinician.',
|
||||||
|
'rxnorm_url' => medication_source_url('https://mor.nlm.nih.gov/RxNav/search?searchBy=String&searchTerm=', $cleanName),
|
||||||
|
'medlineplus_url' => 'https://medlineplus.gov/druginformation.html',
|
||||||
|
'pubchem_url' => medication_source_url('https://pubchem.ncbi.nlm.nih.gov/#query=', $cleanName),
|
||||||
|
'dailymed_url' => medication_source_url('https://dailymed.nlm.nih.gov/dailymed/search.cfm?query=', $cleanName),
|
||||||
|
'pubmed_url' => medication_source_url('https://pubmed.ncbi.nlm.nih.gov/?term=', $cleanName . ' medication'),
|
||||||
|
'clinicaltrials_url' => medication_source_url('https://clinicaltrials.gov/search?term=', $cleanName),
|
||||||
|
'source_name' => $sourceLabel,
|
||||||
|
'search_terms' => implode(' ', [$cleanName, $brandNames, $className, $sourceLabel]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetch_rxnorm_medication_rows(int $limit): array
|
||||||
|
{
|
||||||
|
$rows = [];
|
||||||
|
foreach (['IN', 'PIN'] as $tty) {
|
||||||
|
$json = http_json('https://rxnav.nlm.nih.gov/REST/allconcepts.json?tty=' . $tty);
|
||||||
|
$concepts = $json['minConceptGroup']['minConcept'] ?? [];
|
||||||
|
if (!is_array($concepts)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($concepts as $concept) {
|
||||||
|
if (count($rows) >= $limit) {
|
||||||
|
break 2;
|
||||||
|
}
|
||||||
|
$name = (string) ($concept['name'] ?? '');
|
||||||
|
$row = medication_import_row($name, 'RxNorm');
|
||||||
|
if ($row) {
|
||||||
|
$rows[] = $row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetch_openfda_medication_rows(int $limit): array
|
||||||
|
{
|
||||||
|
$url = 'https://api.fda.gov/drug/label.json?count=openfda.generic_name.exact&limit=' . $limit;
|
||||||
|
$json = http_json($url);
|
||||||
|
$results = $json['results'] ?? [];
|
||||||
|
if (!is_array($results)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = [];
|
||||||
|
foreach ($results as $result) {
|
||||||
|
if (count($rows) >= $limit) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$term = (string) ($result['term'] ?? '');
|
||||||
|
$row = medication_import_row($term, 'openFDA');
|
||||||
|
if ($row) {
|
||||||
|
$rows[] = $row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetch_dailymed_medication_rows(int $limit): array
|
||||||
|
{
|
||||||
|
$url = 'https://dailymed.nlm.nih.gov/dailymed/services/v2/drugnames.json';
|
||||||
|
$json = http_json($url);
|
||||||
|
$data = $json['data'] ?? $json['drugNames'] ?? $json['results'] ?? [];
|
||||||
|
if (!is_array($data)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = [];
|
||||||
|
foreach ($data as $item) {
|
||||||
|
if (count($rows) >= $limit) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$name = is_array($item)
|
||||||
|
? (string) ($item['drug_name'] ?? $item['name'] ?? $item['term'] ?? '')
|
||||||
|
: (string) $item;
|
||||||
|
$row = medication_import_row($name, 'DailyMed');
|
||||||
|
if ($row) {
|
||||||
|
$rows[] = $row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
function merge_text_values(string $current, string $incoming): string
|
||||||
|
{
|
||||||
|
$parts = [];
|
||||||
|
foreach (explode(',', $current . ',' . $incoming) as $part) {
|
||||||
|
$value = trim($part);
|
||||||
|
if ($value === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$parts[strtolower($value)] = $value;
|
||||||
|
}
|
||||||
|
return implode(', ', array_values($parts));
|
||||||
|
}
|
||||||
|
|
||||||
|
function upsert_imported_medication_rows(PDO $pdo, array $rows): array
|
||||||
|
{
|
||||||
|
$created = app_now();
|
||||||
|
$select = $pdo->prepare('SELECT * FROM medication_catalog WHERE lower(generic_name) = lower(?) LIMIT 1');
|
||||||
|
$insert = $pdo->prepare(
|
||||||
|
"INSERT INTO medication_catalog
|
||||||
|
(generic_name, active_ingredient, chemical_name, brand_names, generic_brands, class_name, drug_family,
|
||||||
|
common_use, tracking_note, rxnorm_url, medlineplus_url, pubchem_url, dailymed_url, pubmed_url,
|
||||||
|
clinicaltrials_url, source_name, search_terms, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||||
|
);
|
||||||
|
$update = $pdo->prepare(
|
||||||
|
"UPDATE medication_catalog
|
||||||
|
SET active_ingredient = ?, chemical_name = ?, brand_names = ?, generic_brands = ?,
|
||||||
|
class_name = ?, drug_family = ?, common_use = ?, tracking_note = ?, rxnorm_url = ?,
|
||||||
|
medlineplus_url = ?, pubchem_url = ?, dailymed_url = ?, pubmed_url = ?,
|
||||||
|
clinicaltrials_url = ?, source_name = ?, search_terms = ?
|
||||||
|
WHERE id = ?"
|
||||||
|
);
|
||||||
|
|
||||||
|
$seen = [];
|
||||||
|
$inserted = 0;
|
||||||
|
$updated = 0;
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
if (!$row || empty($row['generic_name'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$key = strtolower((string) $row['generic_name']);
|
||||||
|
if (isset($seen[$key])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$seen[$key] = true;
|
||||||
|
|
||||||
|
$select->execute([$row['generic_name']]);
|
||||||
|
$existing = $select->fetch();
|
||||||
|
if (!$existing) {
|
||||||
|
$insert->execute([
|
||||||
|
$row['generic_name'],
|
||||||
|
$row['active_ingredient'],
|
||||||
|
$row['chemical_name'],
|
||||||
|
$row['brand_names'],
|
||||||
|
$row['generic_brands'],
|
||||||
|
$row['class_name'],
|
||||||
|
$row['drug_family'],
|
||||||
|
$row['common_use'],
|
||||||
|
$row['tracking_note'],
|
||||||
|
$row['rxnorm_url'],
|
||||||
|
$row['medlineplus_url'],
|
||||||
|
$row['pubchem_url'],
|
||||||
|
$row['dailymed_url'],
|
||||||
|
$row['pubmed_url'],
|
||||||
|
$row['clinicaltrials_url'],
|
||||||
|
$row['source_name'],
|
||||||
|
$row['search_terms'],
|
||||||
|
$created,
|
||||||
|
]);
|
||||||
|
$inserted++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$next = [];
|
||||||
|
foreach (['active_ingredient', 'chemical_name', 'class_name', 'drug_family', 'common_use', 'tracking_note', 'rxnorm_url', 'medlineplus_url', 'pubchem_url', 'dailymed_url', 'pubmed_url', 'clinicaltrials_url'] as $field) {
|
||||||
|
$next[$field] = trim((string) ($existing[$field] ?? '')) !== '' ? $existing[$field] : $row[$field];
|
||||||
|
}
|
||||||
|
$next['brand_names'] = merge_text_values((string) ($existing['brand_names'] ?? ''), (string) $row['brand_names']);
|
||||||
|
$next['generic_brands'] = merge_text_values((string) ($existing['generic_brands'] ?? ''), (string) $row['generic_brands']);
|
||||||
|
$next['source_name'] = merge_text_values((string) ($existing['source_name'] ?? ''), (string) $row['source_name']);
|
||||||
|
$next['search_terms'] = trim((string) ($existing['search_terms'] ?? '') . ' ' . (string) $row['search_terms']);
|
||||||
|
|
||||||
|
$update->execute([
|
||||||
|
$next['active_ingredient'],
|
||||||
|
$next['chemical_name'],
|
||||||
|
$next['brand_names'],
|
||||||
|
$next['generic_brands'],
|
||||||
|
$next['class_name'],
|
||||||
|
$next['drug_family'],
|
||||||
|
$next['common_use'],
|
||||||
|
$next['tracking_note'],
|
||||||
|
$next['rxnorm_url'],
|
||||||
|
$next['medlineplus_url'],
|
||||||
|
$next['pubchem_url'],
|
||||||
|
$next['dailymed_url'],
|
||||||
|
$next['pubmed_url'],
|
||||||
|
$next['clinicaltrials_url'],
|
||||||
|
$next['source_name'],
|
||||||
|
$next['search_terms'],
|
||||||
|
(int) $existing['id'],
|
||||||
|
]);
|
||||||
|
$updated++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['inserted' => $inserted, 'updated' => $updated, 'seen' => count($seen)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function record_medication_source_update(PDO $pdo, string $source, string $status, int $inserted, int $updated, string $message): void
|
||||||
|
{
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"INSERT INTO medication_source_updates
|
||||||
|
(source, status, imported_count, updated_count, message, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)"
|
||||||
|
);
|
||||||
|
$stmt->execute([$source, $status, $inserted, $updated, $message, app_now()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function refresh_medication_sources(array $data): array
|
||||||
|
{
|
||||||
|
$pdo = app_db();
|
||||||
|
if (array_key_exists('acting_user_id', $data) || array_key_exists('user_id', $data)) {
|
||||||
|
require_admin($pdo, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
$limit = max(25, min(5000, int_value($data, 'limit', 1000)));
|
||||||
|
$sources = [
|
||||||
|
'RxNorm' => 'fetch_rxnorm_medication_rows',
|
||||||
|
'DailyMed' => 'fetch_dailymed_medication_rows',
|
||||||
|
'openFDA' => 'fetch_openfda_medication_rows',
|
||||||
|
];
|
||||||
|
$requestedSource = text_value($data, 'source', 'all');
|
||||||
|
if ($requestedSource !== 'all') {
|
||||||
|
$sources = array_intersect_key($sources, [$requestedSource => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$summary = [];
|
||||||
|
$totalInserted = 0;
|
||||||
|
$totalUpdated = 0;
|
||||||
|
foreach ($sources as $source => $fetcher) {
|
||||||
|
try {
|
||||||
|
$rows = $fetcher($limit);
|
||||||
|
if (!$rows) {
|
||||||
|
record_medication_source_update($pdo, $source, 'empty', 0, 0, 'No rows returned. The source may be unavailable or rate-limited.');
|
||||||
|
$summary[] = ['source' => $source, 'status' => 'empty', 'inserted' => 0, 'updated' => 0, 'message' => 'No rows returned.'];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$result = upsert_imported_medication_rows($pdo, $rows);
|
||||||
|
$totalInserted += $result['inserted'];
|
||||||
|
$totalUpdated += $result['updated'];
|
||||||
|
$message = "Fetched {$result['seen']} unique rows.";
|
||||||
|
record_medication_source_update($pdo, $source, 'ok', $result['inserted'], $result['updated'], $message);
|
||||||
|
$summary[] = ['source' => $source, 'status' => 'ok', 'inserted' => $result['inserted'], 'updated' => $result['updated'], 'message' => $message];
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
record_medication_source_update($pdo, $source, 'error', 0, 0, $exception->getMessage());
|
||||||
|
$summary[] = ['source' => $source, 'status' => 'error', 'inserted' => 0, 'updated' => 0, 'message' => $exception->getMessage()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'inserted' => $totalInserted,
|
||||||
|
'updated' => $totalUpdated,
|
||||||
|
'catalog_count' => (int) db_value($pdo, 'SELECT COUNT(*) FROM medication_catalog'),
|
||||||
|
'sources' => $summary,
|
||||||
|
'recent_updates' => db_all($pdo, 'SELECT * FROM medication_source_updates ORDER BY created_at DESC, id DESC LIMIT 10'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
function create_user(array $data): array
|
function create_user(array $data): array
|
||||||
{
|
{
|
||||||
$pdo = app_db();
|
$pdo = app_db();
|
||||||
|
|||||||
Binary file not shown.
@@ -31,6 +31,21 @@ require __DIR__ . '/partials/site-header.php';
|
|||||||
|
|
||||||
<section class="admin-panel site-admin-panel" id="adminPanel" hidden>
|
<section class="admin-panel site-admin-panel" id="adminPanel" hidden>
|
||||||
<div class="admin-stats" id="adminStats"></div>
|
<div class="admin-stats" id="adminStats"></div>
|
||||||
|
|
||||||
|
<section class="admin-source-panel">
|
||||||
|
<div class="site-section-heading">
|
||||||
|
<p class="site-eyebrow">Medicine source updater</p>
|
||||||
|
<h2>Refresh the medication catalog from public sources.</h2>
|
||||||
|
<p>Pull medicine concepts from RxNorm, DailyMed, and openFDA. Use a smaller limit for quick checks or a larger limit for periodic maintenance.</p>
|
||||||
|
</div>
|
||||||
|
<form class="admin-source-form" id="medicineSourceRefreshForm">
|
||||||
|
<label class="site-field"><span>Source</span><select name="source"><option value="all">All sources</option><option>RxNorm</option><option>DailyMed</option><option>openFDA</option></select></label>
|
||||||
|
<label class="site-field"><span>Max rows per source</span><input name="limit" type="number" min="25" max="5000" step="25" value="1000"></label>
|
||||||
|
<button class="site-primary" type="submit">Refresh medicines</button>
|
||||||
|
</form>
|
||||||
|
<div class="source-refresh-result" id="medicineSourceRefreshResult" role="status" aria-live="polite"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div class="admin-layout">
|
<div class="admin-layout">
|
||||||
<div class="admin-user-list" id="adminUserList"></div>
|
<div class="admin-user-list" id="adminUserList"></div>
|
||||||
<form class="admin-editor" id="adminUserForm">
|
<form class="admin-editor" id="adminUserForm">
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ try {
|
|||||||
'create_allergy_record' => 'create_allergy_record',
|
'create_allergy_record' => 'create_allergy_record',
|
||||||
'create_appointment' => 'create_appointment',
|
'create_appointment' => 'create_appointment',
|
||||||
'create_immunization_record' => 'create_immunization_record',
|
'create_immunization_record' => 'create_immunization_record',
|
||||||
|
'refresh_medication_sources' => 'refresh_medication_sources',
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!isset($routes[$action])) {
|
if (!isset($routes[$action])) {
|
||||||
|
|||||||
@@ -186,6 +186,22 @@ function adminFormPayload(form) {
|
|||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderRefreshResult(result) {
|
||||||
|
const target = admin$("#medicineSourceRefreshResult");
|
||||||
|
if (!target) return;
|
||||||
|
const rows = (result.sources || []).map((source) => `
|
||||||
|
<article>
|
||||||
|
<strong>${adminEscape(source.source)}: ${adminEscape(source.status)}</strong>
|
||||||
|
<span>${adminNumber(source.inserted)} inserted / ${adminNumber(source.updated)} updated</span>
|
||||||
|
<small>${adminEscape(source.message || "")}</small>
|
||||||
|
</article>
|
||||||
|
`).join("");
|
||||||
|
target.innerHTML = `
|
||||||
|
<p>${adminNumber(result.catalog_count)} medicines in catalog after refresh.</p>
|
||||||
|
<div class="source-refresh-list">${rows}</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", async () => {
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
admin$("#adminProfileSelect").addEventListener("change", async (event) => {
|
admin$("#adminProfileSelect").addEventListener("change", async (event) => {
|
||||||
adminState.userId = event.target.value;
|
adminState.userId = event.target.value;
|
||||||
@@ -248,6 +264,27 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const refreshForm = admin$("#medicineSourceRefreshForm");
|
||||||
|
if (refreshForm) {
|
||||||
|
refreshForm.addEventListener("submit", async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const payload = adminFormPayload(refreshForm);
|
||||||
|
const button = refreshForm.querySelector("button[type='submit']");
|
||||||
|
if (button) button.disabled = true;
|
||||||
|
admin$("#medicineSourceRefreshResult").textContent = "Refreshing medicine sources...";
|
||||||
|
try {
|
||||||
|
const result = await adminApi("refresh_medication_sources", payload);
|
||||||
|
renderRefreshResult(result);
|
||||||
|
adminToast("Medicine sources refreshed");
|
||||||
|
} catch (error) {
|
||||||
|
admin$("#medicineSourceRefreshResult").textContent = error.message;
|
||||||
|
adminToast(error.message);
|
||||||
|
} finally {
|
||||||
|
if (button) button.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await loadAdminState();
|
await loadAdminState();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
+106
-1
@@ -394,6 +394,64 @@ figcaption a {
|
|||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.library-search {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: end;
|
||||||
|
margin: 12px 0 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-search label {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-search input {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 48px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: color-mix(in srgb, var(--panel-2), transparent 6%);
|
||||||
|
color: var(--text);
|
||||||
|
padding: 12px 14px;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-search input:focus {
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent), transparent 76%);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-stats article {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: color-mix(in srgb, var(--panel-2), transparent 6%);
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-stats strong {
|
||||||
|
display: block;
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-stats span {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
.site-split,
|
.site-split,
|
||||||
.detail-hero,
|
.detail-hero,
|
||||||
.signup-layout {
|
.signup-layout {
|
||||||
@@ -555,6 +613,47 @@ textarea:focus {
|
|||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-source-panel {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: color-mix(in srgb, var(--panel-2), transparent 5%);
|
||||||
|
padding: 14px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-source-form {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 190px auto;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-refresh-result {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.88rem;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-refresh-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-refresh-list article {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: color-mix(in srgb, var(--panel), transparent 10%);
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-refresh-list span,
|
||||||
|
.source-refresh-list small {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
.admin-layout {
|
.admin-layout {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1.2fr) minmax(360px, 0.8fr);
|
grid-template-columns: minmax(0, 1.2fr) minmax(360px, 0.8fr);
|
||||||
@@ -718,7 +817,8 @@ textarea:focus {
|
|||||||
.site-split,
|
.site-split,
|
||||||
.detail-hero,
|
.detail-hero,
|
||||||
.signup-layout,
|
.signup-layout,
|
||||||
.admin-layout {
|
.admin-layout,
|
||||||
|
.admin-source-form {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -753,11 +853,16 @@ textarea:focus {
|
|||||||
.feature-grid,
|
.feature-grid,
|
||||||
.detail-grid,
|
.detail-grid,
|
||||||
.admin-stats,
|
.admin-stats,
|
||||||
|
.library-stats,
|
||||||
.signup-row,
|
.signup-row,
|
||||||
.form-grid.two {
|
.form-grid.two {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.library-search {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.admin-access-row,
|
.admin-access-row,
|
||||||
.admin-user-main,
|
.admin-user-main,
|
||||||
.panel-heading {
|
.panel-heading {
|
||||||
|
|||||||
+35
-8
@@ -6,19 +6,40 @@ $pdo = app_db();
|
|||||||
|
|
||||||
$pageTitle = 'Medical Terminology Database | Neon Medical Tracker';
|
$pageTitle = 'Medical Terminology Database | Neon Medical Tracker';
|
||||||
$activePage = 'terminology';
|
$activePage = 'terminology';
|
||||||
$terms = db_all($pdo, 'SELECT * FROM medical_terms ORDER BY category, term');
|
$searchQuery = trim((string) ($_GET['q'] ?? ''));
|
||||||
$conditions = db_all($pdo, 'SELECT * FROM medical_conditions ORDER BY category, name');
|
$library = library_search_payload(['q' => $searchQuery, 'limit' => 500]);
|
||||||
$medicines = db_all($pdo, 'SELECT * FROM medication_catalog ORDER BY generic_name');
|
$terms = $library['medicalTerms'];
|
||||||
$studies = db_all($pdo, 'SELECT * FROM research_studies ORDER BY category, related_medicine, title');
|
$conditions = $library['medicalConditions'];
|
||||||
|
$medicines = $library['medicationCatalog'];
|
||||||
|
$studies = $library['researchStudies'];
|
||||||
|
$totalMedicines = (int) db_value($pdo, 'SELECT COUNT(*) FROM medication_catalog');
|
||||||
|
|
||||||
require __DIR__ . '/partials/site-header.php';
|
require __DIR__ . '/partials/site-header.php';
|
||||||
?>
|
?>
|
||||||
<main>
|
<main>
|
||||||
<section class="admin-public-shell">
|
<section class="admin-public-shell">
|
||||||
<div class="site-section-heading">
|
<div class="site-section-heading">
|
||||||
<p class="site-eyebrow">Medical terminology database</p>
|
<p class="site-eyebrow">Searchable medical library</p>
|
||||||
<h1>Terms, conditions, medicines, and NIH/NLM study links.</h1>
|
<h1>Search terms, conditions, medicines, chemical names, and brands.</h1>
|
||||||
<p>Entries are concise app-authored summaries with links to NIH/NLM systems such as MedlinePlus, RxNorm, PubMed, and ClinicalTrials.gov. This is a tracking/reference aid, not medical advice.</p>
|
<p>Entries are concise app-authored summaries with links to NIH/NLM systems such as MedlinePlus, RxNorm, DailyMed, PubChem, PubMed, and ClinicalTrials.gov. This is a tracking/reference aid, not medical advice.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form class="library-search" method="get" action="/medical.php">
|
||||||
|
<label>
|
||||||
|
<span>Search medical library</span>
|
||||||
|
<input name="q" value="<?= htmlspecialchars($searchQuery, ENT_QUOTES) ?>" placeholder="Try Lipitor, atorvastatin, blood pressure, CBC, metformin">
|
||||||
|
</label>
|
||||||
|
<button class="site-primary" type="submit">Search</button>
|
||||||
|
<?php if ($searchQuery !== ''): ?>
|
||||||
|
<a class="site-secondary" href="/medical.php">Clear</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="library-stats" aria-label="Search result counts">
|
||||||
|
<article><strong><?= count($medicines) ?></strong><span><?= $searchQuery === '' ? 'medicines shown of ' . number_format($totalMedicines) : 'medicine results' ?></span></article>
|
||||||
|
<article><strong><?= count($terms) ?></strong><span>terms</span></article>
|
||||||
|
<article><strong><?= count($conditions) ?></strong><span>conditions</span></article>
|
||||||
|
<article><strong><?= count($studies) ?></strong><span>study links</span></article>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="detail-grid">
|
<div class="detail-grid">
|
||||||
@@ -57,17 +78,23 @@ require __DIR__ . '/partials/site-header.php';
|
|||||||
<section class="admin-public-shell">
|
<section class="admin-public-shell">
|
||||||
<div class="site-section-heading">
|
<div class="site-section-heading">
|
||||||
<p class="site-eyebrow">Medication catalog</p>
|
<p class="site-eyebrow">Medication catalog</p>
|
||||||
<h2>Medicines with RxNorm, PubMed, and ClinicalTrials.gov links</h2>
|
<h2>Medicines with chemical, brand, RxNorm, DailyMed, and PubChem links</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-grid">
|
<div class="detail-grid">
|
||||||
<?php foreach ($medicines as $medicine): ?>
|
<?php foreach ($medicines as $medicine): ?>
|
||||||
<article>
|
<article>
|
||||||
<h2><?= htmlspecialchars(ucfirst($medicine['generic_name']), ENT_QUOTES) ?></h2>
|
<h2><?= htmlspecialchars(ucfirst($medicine['generic_name']), ENT_QUOTES) ?></h2>
|
||||||
<p><strong><?= htmlspecialchars($medicine['class_name'], ENT_QUOTES) ?></strong></p>
|
<p><strong><?= htmlspecialchars($medicine['class_name'], ENT_QUOTES) ?></strong></p>
|
||||||
|
<p><strong>Active ingredient:</strong> <?= htmlspecialchars($medicine['active_ingredient'] ?: $medicine['generic_name'], ENT_QUOTES) ?></p>
|
||||||
|
<p><strong>Chemical name:</strong> <?= htmlspecialchars($medicine['chemical_name'] ?: $medicine['active_ingredient'] ?: $medicine['generic_name'], ENT_QUOTES) ?></p>
|
||||||
|
<p><strong>Common brands:</strong> <?= htmlspecialchars($medicine['brand_names'] ?: 'Brand names vary by market', ENT_QUOTES) ?></p>
|
||||||
|
<p><strong>Generic/alternate names:</strong> <?= htmlspecialchars($medicine['generic_brands'] ?: $medicine['generic_name'], ENT_QUOTES) ?></p>
|
||||||
<p><?= htmlspecialchars($medicine['common_use'], ENT_QUOTES) ?></p>
|
<p><?= htmlspecialchars($medicine['common_use'], ENT_QUOTES) ?></p>
|
||||||
<p><?= htmlspecialchars($medicine['tracking_note'], ENT_QUOTES) ?></p>
|
<p><?= htmlspecialchars($medicine['tracking_note'], ENT_QUOTES) ?></p>
|
||||||
<div class="source-links">
|
<div class="source-links">
|
||||||
<a href="<?= htmlspecialchars($medicine['rxnorm_url'], ENT_QUOTES) ?>">RxNorm</a>
|
<a href="<?= htmlspecialchars($medicine['rxnorm_url'], ENT_QUOTES) ?>">RxNorm</a>
|
||||||
|
<a href="<?= htmlspecialchars($medicine['pubchem_url'], ENT_QUOTES) ?>">PubChem</a>
|
||||||
|
<a href="<?= htmlspecialchars($medicine['dailymed_url'], ENT_QUOTES) ?>">DailyMed</a>
|
||||||
<a href="<?= htmlspecialchars($medicine['medlineplus_url'], ENT_QUOTES) ?>">MedlinePlus</a>
|
<a href="<?= htmlspecialchars($medicine['medlineplus_url'], ENT_QUOTES) ?>">MedlinePlus</a>
|
||||||
<a href="<?= htmlspecialchars($medicine['pubmed_url'], ENT_QUOTES) ?>">PubMed</a>
|
<a href="<?= htmlspecialchars($medicine['pubmed_url'], ENT_QUOTES) ?>">PubMed</a>
|
||||||
<a href="<?= htmlspecialchars($medicine['clinicaltrials_url'], ENT_QUOTES) ?>">Studies</a>
|
<a href="<?= htmlspecialchars($medicine['clinicaltrials_url'], ENT_QUOTES) ?>">Studies</a>
|
||||||
|
|||||||
+28
-3
@@ -6,7 +6,10 @@ $pdo = app_db();
|
|||||||
|
|
||||||
$pageTitle = 'Medicine Catalog | Neon Medical Tracker';
|
$pageTitle = 'Medicine Catalog | Neon Medical Tracker';
|
||||||
$activePage = 'medicines';
|
$activePage = 'medicines';
|
||||||
$medicines = db_all($pdo, 'SELECT * FROM medication_catalog ORDER BY generic_name');
|
$searchQuery = trim((string) ($_GET['q'] ?? ''));
|
||||||
|
$library = library_search_payload(['q' => $searchQuery, 'limit' => 500]);
|
||||||
|
$medicines = $library['medicationCatalog'];
|
||||||
|
$totalMedicines = (int) db_value($pdo, 'SELECT COUNT(*) FROM medication_catalog');
|
||||||
$photo = 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/4f/A_large_mixed_salad.jpg/1024px-A_large_mixed_salad.jpg';
|
$photo = 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/4f/A_large_mixed_salad.jpg/1024px-A_large_mixed_salad.jpg';
|
||||||
require __DIR__ . '/partials/site-header.php';
|
require __DIR__ . '/partials/site-header.php';
|
||||||
?>
|
?>
|
||||||
@@ -21,21 +24,43 @@ require __DIR__ . '/partials/site-header.php';
|
|||||||
</figure>
|
</figure>
|
||||||
<div>
|
<div>
|
||||||
<p class="site-eyebrow">Medicine catalog</p>
|
<p class="site-eyebrow">Medicine catalog</p>
|
||||||
<h1>Track medicines with source links beside every catalog entry.</h1>
|
<h1>Search medicines by generic, chemical, brand, and alternate names.</h1>
|
||||||
<p>The starter catalog includes common medicines and links each one to RxNorm, MedlinePlus, PubMed, and ClinicalTrials.gov searches.</p>
|
<p>The catalog includes common medicines and links each one to RxNorm, DailyMed, PubChem, MedlinePlus, PubMed, and ClinicalTrials.gov searches.</p>
|
||||||
<a class="site-primary" href="/tracker.php">Open medical tracker</a>
|
<a class="site-primary" href="/tracker.php">Open medical tracker</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="admin-public-shell">
|
||||||
|
<form class="library-search" method="get" action="/nutrition.php">
|
||||||
|
<label>
|
||||||
|
<span>Search medicine catalog</span>
|
||||||
|
<input name="q" value="<?= htmlspecialchars($searchQuery, ENT_QUOTES) ?>" placeholder="Try Zoloft, sertraline, insulin, acetaminophen, blood pressure">
|
||||||
|
</label>
|
||||||
|
<button class="site-primary" type="submit">Search</button>
|
||||||
|
<?php if ($searchQuery !== ''): ?>
|
||||||
|
<a class="site-secondary" href="/nutrition.php">Clear</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</form>
|
||||||
|
<div class="library-stats" aria-label="Medicine result count">
|
||||||
|
<article><strong><?= count($medicines) ?></strong><span><?= $searchQuery === '' ? 'shown of ' . number_format($totalMedicines) . ' medicines' : 'medicine results' ?></span></article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="detail-grid">
|
<section class="detail-grid">
|
||||||
<?php foreach ($medicines as $medicine): ?>
|
<?php foreach ($medicines as $medicine): ?>
|
||||||
<article>
|
<article>
|
||||||
<h2><?= htmlspecialchars(ucfirst($medicine['generic_name']), ENT_QUOTES) ?></h2>
|
<h2><?= htmlspecialchars(ucfirst($medicine['generic_name']), ENT_QUOTES) ?></h2>
|
||||||
<p><strong><?= htmlspecialchars($medicine['class_name'], ENT_QUOTES) ?></strong></p>
|
<p><strong><?= htmlspecialchars($medicine['class_name'], ENT_QUOTES) ?></strong></p>
|
||||||
|
<p><strong>Active ingredient:</strong> <?= htmlspecialchars($medicine['active_ingredient'] ?: $medicine['generic_name'], ENT_QUOTES) ?></p>
|
||||||
|
<p><strong>Chemical name:</strong> <?= htmlspecialchars($medicine['chemical_name'] ?: $medicine['active_ingredient'] ?: $medicine['generic_name'], ENT_QUOTES) ?></p>
|
||||||
|
<p><strong>Common brands:</strong> <?= htmlspecialchars($medicine['brand_names'] ?: 'Brand names vary by market', ENT_QUOTES) ?></p>
|
||||||
|
<p><strong>Generic/alternate names:</strong> <?= htmlspecialchars($medicine['generic_brands'] ?: $medicine['generic_name'], ENT_QUOTES) ?></p>
|
||||||
<p><?= htmlspecialchars($medicine['common_use'], ENT_QUOTES) ?></p>
|
<p><?= htmlspecialchars($medicine['common_use'], ENT_QUOTES) ?></p>
|
||||||
<p><?= htmlspecialchars($medicine['tracking_note'], ENT_QUOTES) ?></p>
|
<p><?= htmlspecialchars($medicine['tracking_note'], ENT_QUOTES) ?></p>
|
||||||
<div class="source-links">
|
<div class="source-links">
|
||||||
<a href="<?= htmlspecialchars($medicine['rxnorm_url'], ENT_QUOTES) ?>">RxNorm</a>
|
<a href="<?= htmlspecialchars($medicine['rxnorm_url'], ENT_QUOTES) ?>">RxNorm</a>
|
||||||
|
<a href="<?= htmlspecialchars($medicine['pubchem_url'], ENT_QUOTES) ?>">PubChem</a>
|
||||||
|
<a href="<?= htmlspecialchars($medicine['dailymed_url'], ENT_QUOTES) ?>">DailyMed</a>
|
||||||
<a href="<?= htmlspecialchars($medicine['medlineplus_url'], ENT_QUOTES) ?>">MedlinePlus</a>
|
<a href="<?= htmlspecialchars($medicine['medlineplus_url'], ENT_QUOTES) ?>">MedlinePlus</a>
|
||||||
<a href="<?= htmlspecialchars($medicine['pubmed_url'], ENT_QUOTES) ?>">PubMed</a>
|
<a href="<?= htmlspecialchars($medicine['pubmed_url'], ENT_QUOTES) ?>">PubMed</a>
|
||||||
<a href="<?= htmlspecialchars($medicine['clinicaltrials_url'], ENT_QUOTES) ?>">Studies</a>
|
<a href="<?= htmlspecialchars($medicine['clinicaltrials_url'], ENT_QUOTES) ?>">Studies</a>
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require __DIR__ . '/../app/bootstrap.php';
|
||||||
|
|
||||||
|
if (PHP_SAPI !== 'cli') {
|
||||||
|
fwrite(STDERR, "This script is intended for CLI use.\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$limit = isset($argv[1]) ? (int) $argv[1] : 1000;
|
||||||
|
$source = $argv[2] ?? 'all';
|
||||||
|
|
||||||
|
$result = refresh_medication_sources([
|
||||||
|
'limit' => $limit,
|
||||||
|
'source' => $source,
|
||||||
|
]);
|
||||||
|
|
||||||
|
echo "Medication catalog refresh complete\n";
|
||||||
|
echo "Catalog count: {$result['catalog_count']}\n";
|
||||||
|
echo "Inserted: {$result['inserted']}\n";
|
||||||
|
echo "Updated: {$result['updated']}\n";
|
||||||
|
|
||||||
|
foreach ($result['sources'] as $sourceResult) {
|
||||||
|
echo "- {$sourceResult['source']}: {$sourceResult['status']} ";
|
||||||
|
echo "({$sourceResult['inserted']} inserted, {$sourceResult['updated']} updated) ";
|
||||||
|
echo "{$sourceResult['message']}\n";
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user