<!-- 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> </div> <footer class="border-top pt-4 mt-4"> <p class="text-muted">Results sourced from the PBS database as at January 2023. 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> <!--<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)); // Initialise search bar const mp_preferred_terms = execAsScalars(db.prepare('SELECT preferred_term FROM pbs_mp 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 pbs_mp_brand_name LEFT JOIN pbs_mp ON pbs_mp_brand_name.mp_code = pbs_mp.code 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['preferred_term'] + ')</span>', 'value': tpp_brand_name['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 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 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'); 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); // Create popover for criteria new bootstrap.Popover(span, { trigger: 'hover focus', title: restriction['indication'], content: restriction['criteria_rendered'], 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['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]+$/.compile(); function comparePBSItems(item1, item2) { // 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') { num1 /= 1000000; } if (bits1.length > i + 1 && bits1[i + 1] == 'mg') { num1 /= 1000; } if (bits2.length > i + 1 && bits2[i + 1] == 'microgram') { num2 /= 1000000; } if (bits2.length > i + 1 && bits2[i + 1] == '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>