<!-- 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"> </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> </nav> <div class="container pt-4"> <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 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 labels = execAsScalars(db.prepare('SELECT DISTINCT mp_pt FROM pbs_drug ORDER BY LOWER(mp_pt)')); const data = labels.map(label => ({'label': label})); const autocomplete = new Autocomplete(document.getElementById('search-input'), { data: data, maximumItems: 20, threshold: 2, onSelectItem: onClickSearchItem }); } function onClickSearchItem(item) { // Find matching PBS items const stmt = db.prepare('SELECT *, (SELECT COUNT(1) FROM pbs_streamlined WHERE pbs_drug.item_code = pbs_streamlined.item_code) AS streamlined_authorities FROM pbs_drug LEFT JOIN pbs_prescriber_type ON pbs_drug.item_code = pbs_prescriber_type.item_code WHERE LOWER(mp_pt) = ? AND prescriber_type = "M"'); stmt.bind([item.label.toLowerCase()]); const items = execAsObjects(stmt); items.sort(comparePBSItems); // Update table const tbody = document.querySelector('#search-results tbody'); tbody.innerHTML = ''; for (let item of items) { const tr = document.createElement('tr'); let td = document.createElement('td'); td.innerHTML = '<a href="https://www.pbs.gov.au/medicine/item/' + item['item_code'] + '" target="_blank">' + item['item_code'] + '</a>'; tr.appendChild(td); td = document.createElement('td'); td.innerText = item['tpuu_or_mpp_pt']; tr.appendChild(td); td = document.createElement('td'); td.innerText = item['mq']; tr.appendChild(td); td = document.createElement('td'); td.innerText = item['repeats']; tr.appendChild(td); if (item['restriction_flag'] === 'U') { td = document.createElement('td'); tr.appendChild(td); } else if (item['restriction_flag'] === 'R') { td = document.createElement('td'); td.innerHTML = '<a href="https://www.pbs.gov.au/medicine/item/' + item['item_code'] + '" target="_blank">Restricted</a>'; tr.appendChild(td); tr.classList.add('table-warning'); } else if (item['restriction_flag'] === 'A') { if (item['streamlined_authorities'] > 0) { td = document.createElement('td'); td.innerHTML = '<a href="https://www.pbs.gov.au/medicine/item/' + item['item_code'] + '" target="_blank">Streamlined</a>'; tr.appendChild(td); tr.classList.add('table-warning'); } else { td = document.createElement('td'); td.innerHTML = '<a href="https://www.pbs.gov.au/medicine/item/' + item['item_code'] + '" target="_blank">Authority</a>'; tr.appendChild(td); tr.classList.add('table-danger'); } } else { td = document.createElement('td'); td.innerText = item['restriction_flag']; tr.appendChild(td); } tbody.appendChild(tr); } } const regexIsNumber = /^[0-9]+$/.compile(); function comparePBSItems(item1, item2) { // Sort tablets/capsules before other forms if ((item1['tpuu_or_mpp_pt'].indexOf(' tablet, ') >= 0 || item1['tpuu_or_mpp_pt'].indexOf(' capsule, ') >= 0) && !(item2['tpuu_or_mpp_pt'].indexOf(' tablet, ') >= 0 || item2['tpuu_or_mpp_pt'].indexOf(' capsule, ') >= 0)) { return -1; } if ((item2['tpuu_or_mpp_pt'].indexOf(' tablet, ') >= 0 || item2['tpuu_or_mpp_pt'].indexOf(' capsule, ') >= 0) && !(item1['tpuu_or_mpp_pt'].indexOf(' tablet, ') >= 0 || item1['tpuu_or_mpp_pt'].indexOf(' capsule, ') >= 0)) { return 1; } // Compare tpuu_or_mpp_pt word-by-word accounting for numbers const bits1 = item1['tpuu_or_mpp_pt'].split(' '); const bits2 = item2['tpuu_or_mpp_pt'].split(' '); for (let i = 0; i < bits1.length && i < bits2.length; i++) { if (regexIsNumber.test(bits1[i]) && regexIsNumber.test(bits2[i])) { // Numeric compare const num1 = parseInt(bits1[i]); const num2 = parseInt(bits2[i]); 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; } } } return 0; } 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; } main(); </script> </body> </html>