328 lines
14 KiB
HTML
328 lines
14 KiB
HTML
<!--
|
|
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">
|
|
<style type="text/css">
|
|
#search-results ul { margin-top: 0.5em; margin-bottom: 0 }
|
|
.popover { max-width: 400px }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<nav class="navbar navbar-expand-sm navbar-light bg-light d-print-none">
|
|
<div class="container">
|
|
<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>
|
|
</div>
|
|
</nav>
|
|
<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>
|
|
</div>
|
|
<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>
|
|
<footer class="border-top pt-4 mt-4">
|
|
<p class="text-muted">Results sourced from the PBS database as at <span id="pbs-date"></span>. 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>
|
|
</footer>
|
|
</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>
|
|
<script src="https://cdn.jsdelivr.net/npm/sql.js@1.8.0/dist/sql-wasm.min.js"></script>
|
|
|
|
<script>
|
|
var db; // Keep in global namespace for debugging
|
|
|
|
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));
|
|
|
|
// 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];
|
|
|
|
// Initialise search bar
|
|
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)'));
|
|
let data = mp_preferred_terms.map(mp_preferred_term => ({'label': mp_preferred_term, 'preview': mp_preferred_term, 'value': mp_preferred_term}));
|
|
|
|
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']})));
|
|
|
|
const autocomplete = new Autocomplete(document.getElementById('search-input'), {
|
|
data: data,
|
|
maximumItems: 20,
|
|
threshold: 2,
|
|
onSelectItem: onClickSearchItem
|
|
});
|
|
|
|
// Hide loading spinner
|
|
document.getElementById('loading-content').style.display = 'none';
|
|
document.getElementById('app-content').style.display = 'block';
|
|
}
|
|
|
|
function onClickSearchItem(item) {
|
|
// Override label if clicked on a trade name
|
|
document.getElementById('search-input').value = item.value;
|
|
|
|
// Find matching PBS items
|
|
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) = ?'
|
|
);
|
|
stmt.bind([item.value.toLowerCase()]);
|
|
const items = execAsObjects(stmt);
|
|
|
|
items.sort(comparePBSItems);
|
|
|
|
// Update table
|
|
const tbody = document.querySelector('#search-results tbody');
|
|
tbody.innerHTML = '';
|
|
for (let item of items) {
|
|
// 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);
|
|
|
|
const tr = document.createElement('tr');
|
|
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);
|
|
|
|
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');
|
|
ulRestrictions.appendChild(li);
|
|
|
|
li.innerHTML = restriction['indication'];
|
|
if (item['benefit_type'] === 'streamlined') {
|
|
li.innerHTML += ' (' + restriction['treatment_of'] + ')';
|
|
}
|
|
if (restriction['num_criteria'] > 0) {
|
|
li.innerHTML += ' ';
|
|
|
|
let span = document.createElement('span');
|
|
span.classList.add('text-muted');
|
|
span.innerText = '(' + restriction['num_criteria'] + ' criteria)';
|
|
li.appendChild(span);
|
|
|
|
// Render restriction content
|
|
let content = '';
|
|
if (restriction['treatment_phase']) {
|
|
content += '<p><i>' + restriction['treatment_phase'] + '</i></p>'
|
|
}
|
|
content += restriction['criteria_rendered'];
|
|
|
|
// Create popover for criteria
|
|
new bootstrap.Popover(span, {
|
|
trigger: 'hover focus',
|
|
title: restriction['indication'],
|
|
content: content,
|
|
html: true
|
|
});
|
|
}
|
|
}
|
|
td.appendChild(ulRestrictions);
|
|
}
|
|
tr.appendChild(td);
|
|
|
|
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);
|
|
|
|
if (item['program'] !== 'GE') {
|
|
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);
|
|
tr.classList.add('table-secondary');
|
|
} else if (item['benefit_type'] === 'unrestricted') {
|
|
td = document.createElement('td'); tr.appendChild(td);
|
|
} 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);
|
|
tr.classList.add('table-warning');
|
|
} 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);
|
|
tr.classList.add('table-warning');
|
|
} 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');
|
|
} else {
|
|
alert('Unknown benefit type: ' + item['benefit_type']);
|
|
throw 'Unknown benefit type: ' + item['benefit_type'];
|
|
}
|
|
|
|
tbody.appendChild(tr);
|
|
}
|
|
}
|
|
|
|
const regexIsNumber = /^[0-9.]+$/;
|
|
|
|
function comparePBSItems(item1, item2) {
|
|
// 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;
|
|
}
|
|
|
|
// Sort tablets/capsules before other forms
|
|
|
|
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)) {
|
|
return -1;
|
|
}
|
|
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)) {
|
|
return 1;
|
|
}
|
|
|
|
// Compare mpp_preferred_term word-by-word accounting for numbers and units
|
|
|
|
const bits1 = item1['mpp_preferred_term'].split(' ');
|
|
const bits2 = item2['mpp_preferred_term'].split(' ');
|
|
|
|
for (let i = 0; i < bits1.length && i < bits2.length; i++) {
|
|
if (regexIsNumber.test(bits1[i]) && regexIsNumber.test(bits2[i])) {
|
|
// Numeric compare
|
|
let num1 = parseInt(bits1[i]);
|
|
let num2 = parseInt(bits2[i]);
|
|
|
|
// Check for units - convert to grams
|
|
if (bits1.length > i + 1 && (bits1[i + 1] == 'microgram' || bits1[i + 1].startsWith('microgram/'))) {
|
|
num1 /= 1000000;
|
|
}
|
|
if (bits1.length > i + 1 && (bits1[i + 1] == 'mg' || bits1[i + 1].startsWith('mg/'))) {
|
|
num1 /= 1000;
|
|
}
|
|
|
|
if (bits2.length > i + 1 && (bits2[i + 1] == 'microgram' || bits2[i + 1].startsWith('microgram/'))) {
|
|
num2 /= 1000000;
|
|
}
|
|
if (bits2.length > i + 1 && (bits2[i + 1] == 'mg' || bits2[i + 1].startsWith('mg/'))) {
|
|
num2 /= 1000;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sort unrestricted, then restricted/streamlined, then authority required
|
|
|
|
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;
|
|
return type1 - type2;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}, {});
|
|
};
|
|
main();
|
|
</script>
|
|
</body>
|
|
</html>
|