2023-01-17 19:34:37 +11:00
<!--
Copyright © 2023 Lee Yingtong Li (RunasSudo)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see < https: / / www . gnu . org / licenses / > .
-->
<!DOCTYPE html>
< html >
< head >
< meta charset = "utf-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1" >
< title > PBS medicine search< / title >
< link rel = "stylesheet" href = "https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" integrity = "sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin = "anonymous" >
< link rel = "stylesheet" href = "https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.2/font/bootstrap-icons.css" integrity = "sha256-4RctOgogjPAdwGbwq+rxfwAmSpZhWaafcZR9btzUk18=" crossorigin = "anonymous" >
2023-01-22 21:18:26 +11:00
< style type = "text/css" >
2023-01-26 20:09:56 +11:00
#search-results ul { margin-top: 0.5em; margin-bottom: 0 }
2023-02-11 20:24:22 +11:00
tr.bg-orange { border-color: #e4cfb3; color: #000 }
tr.bg-orange td { background-color: #ffe9cd }
2023-01-23 20:31:20 +11:00
.popover { max-width: 400px }
2023-01-22 21:18:26 +11:00
< / style >
2023-01-17 19:34:37 +11:00
< / head >
< body >
< nav class = "navbar navbar-expand-sm navbar-light bg-light d-print-none" >
< div class = "container" >
2023-01-26 20:09:56 +11:00
< a class = "navbar-brand" href = "#" > PBS medicine search< / a >
< div class = "collapse navbar-collapse justify-content-end" >
< ul class = "navbar-nav" >
< li class = "nav-item" >
< a class = "nav-link" href = "https://yingtongli.me/git/MedicineSearch" > Source code< / a >
< / li >
< / ul >
< / div >
2023-01-17 19:34:37 +11:00
< / div >
< / nav >
2023-01-26 20:03:07 +11:00
< div class = "container pt-4" id = "loading-page" >
< div class = "text-center" id = "loading-content" >
< div class = "spinner-border" role = "status" >
< span class = "visually-hidden" > Loading...< / span >
< / div >
< div class = "mt-2" >
Loading…
< / div >
2023-01-17 19:34:37 +11:00
< / div >
2023-01-26 20:03:07 +11:00
< div id = "app-content" style = "display:none" >
< div >
< input class = "form-control" id = "search-input" placeholder = "Search…" autocomplete = "off" >
< / div >
< div >
< table id = "search-results" class = "table mt-2" >
< thead >
< tr >
< th class = "col-1" > Item code< / th >
< th class = "col-8" > Drug< / th >
< th class = "col-1" > Quantity< / th >
< th class = "col-1" > Repeats< / th >
< th class = "col-1" > Restriction< / th >
< / tr >
< / thead >
< tbody >
< td colspan = "5" class = "text-center" > Enter a search term above to show results< / th >
< / tbody >
< / table >
< / div >
2023-02-04 14:28:11 +11:00
< footer class = "border-top pt-4 mt-4" >
2023-02-18 13:47:00 +11:00
< p class = "text-muted" > Results sourced from the PBS database as at < span id = "pbs-date" > < / span > . Only items from selected PBS programs, and selected non-PBS medications, are displayed. This tool is made available in the hope that it will be useful, but < b > WITHOUT ANY WARRANTY< / b > ; without even the implied warranty of < b > MERCHANTABILITY< / b > or < b > FITNESS FOR A PARTICULAR PURPOSE< / b > . Information provided in this tool is intended for reference by medical professionals. Nothing in this tool is intended to constitute medical advice.< / p >
2023-02-04 14:28:11 +11:00
< / footer >
2023-01-17 19:34:37 +11:00
< / div >
< / div >
<!-- <script src="https://cdn.jsdelivr.net/npm/jquery@3.6.3/dist/jquery.min.js" integrity="sha256 - pvPw+upLPUjgMXY0G+8O0xUf+/Im1MZjXxxgOcBQBXU=" crossorigin="anonymous"></script> -->
< script src = "https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" integrity = "sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin = "anonymous" > < / script >
< script src = "autocomplete.js" > < / script >
2023-01-17 20:17:45 +11:00
< script src = "https://cdn.jsdelivr.net/npm/sql.js@1.8.0/dist/sql-wasm.min.js" > < / script >
2023-01-17 19:34:37 +11:00
< script >
2023-01-17 20:17:45 +11:00
var db; // Keep in global namespace for debugging
2023-01-17 19:34:37 +11:00
2023-01-17 20:17:45 +11:00
async function main() {
// Load SQLite database
const sqlPromise = initSqlJs({
locateFile: file => ('https://cdn.jsdelivr.net/npm/sql.js@1.8.0/dist/' + file)
});
const dataPromise = fetch('database.db').then(res => res.arrayBuffer());
const [SQL, buf] = await Promise.all([sqlPromise, dataPromise])
db = new SQL.Database(new Uint8Array(buf));
2023-01-17 19:34:37 +11:00
2023-02-04 14:28:11 +11:00
// Display date
const pbs_date_bits = execAsScalars(db.prepare('SELECT value FROM meta WHERE key = "pbs_date"'))[0].split('-');
document.getElementById('pbs-date').innerText = {'01': 'January', '02': 'February', '03': 'March', '04': 'April', '05': 'May', '06': 'June', '07': 'July', '08': 'August', '09': 'September', '10': 'October', '11': 'November', '12': 'December'}[pbs_date_bits[1]] + ' ' + pbs_date_bits[0];
2023-01-17 19:34:37 +11:00
// Initialise search bar
2023-02-04 15:48:44 +11:00
const mp_preferred_terms = execAsScalars(db.prepare('SELECT * FROM (SELECT preferred_term FROM pbs_mp UNION SELECT mp_preferred_term AS preferred_term FROM non_pbs_tpp) ORDER BY LOWER(preferred_term)'));
2023-01-24 19:56:54 +11:00
let data = mp_preferred_terms.map(mp_preferred_term => ({'label': mp_preferred_term, 'preview': mp_preferred_term, 'value': mp_preferred_term}));
2023-02-04 15:48:44 +11:00
const tpp_brand_names = execAsObjects(db.prepare('SELECT * FROM mp_brand_name ORDER BY LOWER(brand_name)'));
data = data.concat(tpp_brand_names.map(tpp_brand_name => ({'label': tpp_brand_name['brand_name'], 'preview': tpp_brand_name['brand_name'] + ' < span class = "text-muted" > (' + tpp_brand_name['mp_preferred_term'] + ')< / span > ', 'value': tpp_brand_name['mp_preferred_term']})));
2023-01-24 19:56:54 +11:00
2023-01-17 19:34:37 +11:00
const autocomplete = new Autocomplete(document.getElementById('search-input'), {
data: data,
maximumItems: 20,
threshold: 2,
onSelectItem: onClickSearchItem
});
2023-01-26 20:03:07 +11:00
// Hide loading spinner
document.getElementById('loading-content').style.display = 'none';
document.getElementById('app-content').style.display = 'block';
2023-01-17 20:17:45 +11:00
}
2023-01-17 19:34:37 +11:00
function onClickSearchItem(item) {
2023-01-24 19:56:54 +11:00
// Override label if clicked on a trade name
document.getElementById('search-input').value = item.value;
2023-01-17 19:34:37 +11:00
// Find matching PBS items
2023-02-04 15:48:44 +11:00
let stmt = db.prepare(
'SELECT * FROM (' +
' SELECT code, mp_preferred_term, mpp_preferred_term, benefit_type, maximum_prescribable_units, number_repeats, program FROM pbs_item' +
' LEFT JOIN (SELECT code AS mpp_code, preferred_term AS mpp_preferred_term, mp_code FROM pbs_mpp) AS pbs_mpp ON pbs_item.mpp_code = pbs_mpp.mpp_code' +
' LEFT JOIN (SELECT code AS mp_code, preferred_term AS mp_preferred_term FROM pbs_mp) AS pbs_mp ON pbs_mpp.mp_code = pbs_mp.mp_code' +
' UNION SELECT DISTINCT NULL AS code, mp_preferred_term, mpp_preferred_term, "unrestricted" AS benefit_type, NULL AS maximum_prescribable_units, NULL AS number_repeats, "NA" AS program FROM non_pbs_tpp' +
' )' +
' WHERE LOWER(mp_preferred_term) = ?'
);
2023-01-24 19:56:54 +11:00
stmt.bind([item.value.toLowerCase()]);
2023-01-17 20:17:45 +11:00
const items = execAsObjects(stmt);
2023-01-17 19:34:37 +11:00
items.sort(comparePBSItems);
// Update table
2023-01-17 20:17:45 +11:00
const tbody = document.querySelector('#search-results tbody');
2023-01-17 19:34:37 +11:00
tbody.innerHTML = '';
for (let item of items) {
2023-01-22 21:18:26 +11:00
// Get restrictions
stmt = db.prepare('SELECT *, (SELECT COUNT(*) FROM pbs_restriction_criteria WHERE pbs_restriction_criteria.restriction_code = pbs_restriction.code) AS num_criteria FROM pbs_restriction LEFT JOIN pbs_item_restriction ON pbs_restriction.code = pbs_item_restriction.restriction_code WHERE pbs_item_restriction.item_code = ?');
stmt.bind([item['code']]);
const restrictions = execAsObjects(stmt);
2023-01-17 19:34:37 +11:00
const tr = document.createElement('tr');
2023-02-04 15:48:44 +11:00
let td = document.createElement('td');
if (item['code']) {
td.innerHTML = '< a href = "https://www.pbs.gov.au/medicine/item/' + item['code'] + '" target = "_blank" > ' + item['code'] + '< / a > ';
}
tr.appendChild(td);
2023-01-22 21:18:26 +11:00
td = document.createElement('td');
let div = document.createElement('div'); div.innerText = item['mpp_preferred_term']; td.appendChild(div);
if (restrictions.length > 0) {
let ulRestrictions = document.createElement('ul');
for (let restriction of restrictions) {
let li = document.createElement('li');
2023-01-23 20:31:20 +11:00
ulRestrictions.appendChild(li);
2023-01-22 21:18:26 +11:00
li.innerHTML = restriction['indication'];
if (item['benefit_type'] === 'streamlined') {
li.innerHTML += ' (' + restriction['treatment_of'] + ')';
}
if (restriction['num_criteria'] > 0) {
2023-01-23 20:31:20 +11:00
li.innerHTML += ' ';
let span = document.createElement('span');
span.classList.add('text-muted');
span.innerText = '(' + restriction['num_criteria'] + ' criteria)';
li.appendChild(span);
2023-02-11 18:17:19 +11:00
// Render restriction content
let content = '';
if (restriction['treatment_phase']) {
content += '< p > < i > ' + restriction['treatment_phase'] + '< / i > < / p > '
}
content += restriction['criteria_rendered'];
2023-01-23 20:31:20 +11:00
// Create popover for criteria
new bootstrap.Popover(span, {
trigger: 'hover focus',
title: restriction['indication'],
2023-02-11 18:17:19 +11:00
content: content,
2023-01-23 20:31:20 +11:00
html: true
});
2023-01-22 21:18:26 +11:00
}
}
td.appendChild(ulRestrictions);
}
tr.appendChild(td);
2023-01-22 21:02:59 +11:00
td = document.createElement('td'); td.innerText = item['maximum_prescribable_units']; tr.appendChild(td);
td = document.createElement('td'); td.innerText = item['number_repeats']; tr.appendChild(td);
2023-01-17 19:34:37 +11:00
2023-02-18 13:47:00 +11:00
if (item['program'] !== 'GE' & & item['program'] !== 'HB') {
2023-02-04 15:48:44 +11:00
td = document.createElement('td');
if (item['program'] === 'R1') {
td.innerHTML = '< a href = "https://www.pbs.gov.au/medicine/item/' + item['code'] + '" target = "_blank" > RPBS< / a > ';
} else if (item['program'] === 'NA') {
td.innerHTML = 'Non-PBS';
} else {
alert('Unknown program: ' + item['program']);
throw 'Unknown program: ' + item['program'];
}
tr.appendChild(td);
2023-02-04 14:09:58 +11:00
tr.classList.add('table-secondary');
} else if (item['benefit_type'] === 'unrestricted') {
2023-01-17 19:34:37 +11:00
td = document.createElement('td'); tr.appendChild(td);
2023-01-22 21:02:59 +11:00
} else if (item['benefit_type'] === 'restricted') {
td = document.createElement('td'); td.innerHTML = '< a href = "https://www.pbs.gov.au/medicine/item/' + item['code'] + '" target = "_blank" > Restricted< / a > '; tr.appendChild(td);
2023-01-17 19:34:37 +11:00
tr.classList.add('table-warning');
2023-01-22 21:02:59 +11:00
} else if (item['benefit_type'] === 'streamlined') {
td = document.createElement('td'); td.innerHTML = '< a href = "https://www.pbs.gov.au/medicine/item/' + item['code'] + '" target = "_blank" > Streamlined< / a > '; tr.appendChild(td);
2023-02-11 20:24:22 +11:00
tr.classList.add('bg-orange');
2023-01-22 21:02:59 +11:00
} else if (item['benefit_type'] === 'authority') {
td = document.createElement('td'); td.innerHTML = '< a href = "https://www.pbs.gov.au/medicine/item/' + item['code'] + '" target = "_blank" > Authority< / a > '; tr.appendChild(td);
tr.classList.add('table-danger');
2023-01-17 19:34:37 +11:00
} else {
2023-01-22 21:02:59 +11:00
alert('Unknown benefit type: ' + item['benefit_type']);
throw 'Unknown benefit type: ' + item['benefit_type'];
2023-01-17 19:34:37 +11:00
}
tbody.appendChild(tr);
}
}
2023-02-10 23:46:08 +11:00
const regexIsNumber = /^[0-9.]+$/;
2023-01-17 19:34:37 +11:00
function comparePBSItems(item1, item2) {
2023-02-04 14:09:58 +11:00
// Sort RPBS, etc. benefits after other items
if (item1['program'] === 'GE' & & item2['program'] !== 'GE') {
return -1;
}
if (item2['program'] === 'GE' & & item1['program'] !== 'GE') {
return 1;
}
2023-01-17 19:34:37 +11:00
// Sort tablets/capsules before other forms
2023-01-22 21:02:59 +11:00
if ((item1['mpp_preferred_term'].indexOf(' tablet, ') >= 0 || item1['mpp_preferred_term'].indexOf(' capsule, ') >= 0) & & !(item2['mpp_preferred_term'].indexOf(' tablet, ') >= 0 || item2['mpp_preferred_term'].indexOf(' capsule, ') >= 0)) {
2023-01-17 19:34:37 +11:00
return -1;
}
2023-01-22 21:02:59 +11:00
if ((item2['mpp_preferred_term'].indexOf(' tablet, ') >= 0 || item2['mpp_preferred_term'].indexOf(' capsule, ') >= 0) & & !(item1['mpp_preferred_term'].indexOf(' tablet, ') >= 0 || item1['mpp_preferred_term'].indexOf(' capsule, ') >= 0)) {
2023-01-17 19:34:37 +11:00
return 1;
}
2023-01-23 20:53:59 +11:00
// Compare mpp_preferred_term word-by-word accounting for numbers and units
2023-01-17 19:34:37 +11:00
2023-01-22 21:02:59 +11:00
const bits1 = item1['mpp_preferred_term'].split(' ');
const bits2 = item2['mpp_preferred_term'].split(' ');
2023-01-17 19:34:37 +11:00
for (let i = 0; i < bits1.length & & i < bits2 . length ; i + + ) {
if (regexIsNumber.test(bits1[i]) & & regexIsNumber.test(bits2[i])) {
// Numeric compare
2023-01-23 20:53:59 +11:00
let num1 = parseInt(bits1[i]);
let num2 = parseInt(bits2[i]);
// Check for units - convert to grams
2023-02-10 23:46:36 +11:00
if (bits1.length > i + 1 & & (bits1[i + 1] == 'microgram' || bits1[i + 1].startsWith('microgram/'))) {
2023-01-23 20:53:59 +11:00
num1 /= 1000000;
}
2023-02-10 23:46:36 +11:00
if (bits1.length > i + 1 & & (bits1[i + 1] == 'mg' || bits1[i + 1].startsWith('mg/'))) {
2023-01-23 20:53:59 +11:00
num1 /= 1000;
}
2023-02-10 23:46:36 +11:00
if (bits2.length > i + 1 & & (bits2[i + 1] == 'microgram' || bits2[i + 1].startsWith('microgram/'))) {
2023-01-23 20:53:59 +11:00
num2 /= 1000000;
}
2023-02-10 23:46:36 +11:00
if (bits2.length > i + 1 & & (bits2[i + 1] == 'mg' || bits2[i + 1].startsWith('mg/'))) {
2023-01-23 20:53:59 +11:00
num2 /= 1000;
}
2023-01-17 19:34:37 +11:00
if (num1 < num2 ) {
return -1;
}
if (num1 > num2) {
return 1;
}
} else {
// String compare
if (bits1[i] < bits2 [ i ] ) {
return -1;
}
if (bits1[i] > bits2[i]) {
return 1;
}
}
}
2023-01-17 20:17:54 +11:00
// Sort unrestricted, then restricted/streamlined, then authority required
2023-01-22 21:02:59 +11:00
const type1 = item1['benefit_type'] === 'unrestricted' ? 0 : item1['benefit_type'] === 'restricted' ? 1 : item1['benefit_type'] === 'streamlined' ? 1 : 2;
const type2 = item2['benefit_type'] === 'unrestricted' ? 0 : item2['benefit_type'] === 'restricted' ? 1 : item2['benefit_type'] === 'streamlined' ? 1 : 2;
2023-01-17 20:17:54 +11:00
return type1 - type2;
2023-01-17 19:34:37 +11:00
}
2023-01-17 20:17:45 +11:00
function execAsScalars(stmt) {
let results = [];
while (stmt.step()) {
results.push(stmt.get()[0]);
}
stmt.free();
return results;
}
function execAsObjects(stmt) {
let results = [];
while (stmt.step()) {
results.push(stmt.getAsObject());
}
stmt.free();
return results;
}
2023-01-23 20:31:20 +11:00
function groupBy(array, grouper) {
// Array.prototype.group not yet supported in most (any) browsers
return array.reduce(function(result, item) {
(result[grouper(item)] = result[grouper(item)] || []).push(item);
return result;
}, {});
};
2023-01-17 20:17:45 +11:00
main();
2023-01-17 19:34:37 +11:00
< / script >
< / body >
< / html >