1/** 2 * asset_hist_pagination.js v3 3 * Client-side pagination + search + filter cho asset detail modal history tabs. 4 * 5 * Vì dbc.Input/dcc.Input không nháºn data-* attrs, JS tá»± parse section từ element id: 6 * id "asset-hist-search-inc" â section = "inc" 7 * id "asset-hist-filter-year-inc" â section = "inc", filterKey = "year" 8 * id "asset-hist-filter-type-maint" â section = "maint", filterKey = "type" 9 * id "asset-hist-first-parts" (btn) â section = "parts" 10 * 11 * html.Select dropdowns vẫn dùng data-section/data-filterkey (html.Select há» trợ). 12 * 13 * State lưu trên pager div data attrs: 14 * data-page, data-pagesize, data-total, data-cols, data-center, data-filters 15 */ 16(function () { 17 'use strict'; 18 19 var PREFIX = 'asset-hist-'; 20 21 /* ââ Parse section từ element id ââââââââââââââââââââââââââââââââââââ */ 22 function sectionFromId(id, part) { 23 // id = "asset-hist-{part}-{section}" 24 // part examples: "search", "first", "prev", "next", "last", "page", "count" 25 var pfx = PREFIX + part + '-'; 26 return id && id.startsWith(pfx) ? id.slice(pfx.length) : null; 27 } 28 29 // For filter dropdowns: id = "asset-hist-filter-{filterKey}-{section}" 30 // section may itself contain "-" only if section_id has "-", which we avoid. 31 // filterKey is always the second-to-last segment, section is the last. 32 function parseFilterId(id) { 33 var pfx = PREFIX + 'filter-'; 34 if (!id || !id.startsWith(pfx)) return null; 35 var rest = id.slice(pfx.length); // "year-inc" or "type-maint" 36 var dash = rest.lastIndexOf('-'); 37 if (dash < 0) return null; 38 return { filterKey: rest.slice(0, dash), section: rest.slice(dash + 1) }; 39 } 40 41 /* ââ Helpers ââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 42 function escHtml(s) { 43 return String(s === null || s === undefined ? '' : s) 44 .replace(/&/g,'&').replace(/</g,'<') 45 .replace(/>/g,'>').replace(/"/g,'"'); 46 } 47 48 function getAllRows(section) { 49 var div = document.getElementById(PREFIX + 'data-' + section); 50 if (!div) return []; 51 try { return JSON.parse(div.textContent || '[]'); } catch(e) { return []; } 52 } 53 54 function getPager(section) { 55 return document.getElementById(PREFIX + 'pager-' + section); 56 } 57 58 function readFilters(section) { 59 var p = getPager(section); 60 if (!p) return {}; 61 try { return JSON.parse(p.dataset.filters || '{}'); } catch(e) { return {}; } 62 } 63 64 function readSearch(section) { 65 var el = document.getElementById(PREFIX + 'search-' + section); 66 return el ? (el.value || '') : ''; 67 } 68 69 /* ââ Apply search + filter ââââââââââââââââââââââââââââââââââââââââ */ 70 function applyFilters(rows, searchStr, filters) { 71 var q = (searchStr || '').trim().toLowerCase(); 72 var hasFilter = Object.keys(filters).some(function(k){ return !!filters[k]; }); 73 if (!q && !hasFilter) return rows; 74 return rows.filter(function(r) { 75 if (hasFilter) { 76 for (var k in filters) { 77 if (!filters[k]) continue; 78 var v = String(r[k] !== null && r[k] !== undefined ? r[k] : '').trim(); 79 if (v !== filters[k]) return false; 80 } 81 } 82 if (q) { 83 var found = false; 84 for (var key in r) { 85 if (String(r[key] !== null && r[key] !== undefined ? r[key] : '').toLowerCase().indexOf(q) !== -1) { 86 found = true; break; 87 } 88 } 89 if (!found) return false; 90 } 91 return true; 92 }); 93 } 94 95 /* ââ Render one page ââââââââââââââââââââââââââââââââââââââââââââââ */ 96 function renderPage(section, filteredRows, curPage) { 97 var pager = getPager(section); 98 var tbody = document.getElementById(PREFIX + 'tbody-' + section); 99 var pageLbl = document.getElementById(PREFIX + 'page-' + section); 100 var cntLbl = document.getElementById(PREFIX + 'count-' + section); 101 if (!pager || !tbody) return; 102 103 var pageSize = parseInt(pager.dataset.pagesize, 10) || 10; 104 var cols = JSON.parse(pager.dataset.cols || '[]'); 105 var centers = JSON.parse(pager.dataset.center || '[]'); 106 var total = filteredRows.length; 107 var totalPages = Math.max(1, Math.ceil(total / pageSize)); 108 109 curPage = Math.max(0, Math.min(curPage, totalPages - 1)); 110 pager.dataset.page = curPage; 111 pager.dataset.total = total; 112 113 var start = curPage * pageSize; 114 var slice = filteredRows.slice(start, start + pageSize); 115 var html = ''; 116 if (!slice.length) { 117 var nodataMsg = (pager && pager.dataset.nodata) ? pager.dataset.nodata : 'Không có dữ liá»u phù hợp'; 118 html = '<tr><td colspan="' + cols.length + 119 '" class="text-center text-muted small py-3">' + escHtml(nodataMsg) + '</td></tr>'; 120 } else { 121 slice.forEach(function(r, i) { 122 var cls = i % 2 === 0 ? 'asset-hist-odd' : 'asset-hist-even'; 123 html += '<tr class="asset-hist-row ' + cls + '">';
124 cols.forEach(function(key, j) { 125 var center = centers[j] ? ' text-center' : ''; 126 var val = (r[key] !== null && r[key] !== undefined) ? String(r[key]) : ''; 127 html += '<td class="asset-hist-td' + center + '">' + escHtml(val) + '</td>'; 128 }); 129 html += '</tr>'; 130 }); 131 } 132 tbody.innerHTML = html; 133 134 if (pageLbl) pageLbl.textContent = (curPage + 1) + ' / ' + totalPages; 135 var rowsTpl = (pager && pager.dataset.rowslabel) ? pager.dataset.rowslabel : '{n} dòng'; 136 if (cntLbl) cntLbl.textContent = rowsTpl.replace('{n}', total); 137 138 ['first','prev'].forEach(function(a) { 139 var b = document.getElementById(PREFIX + a + '-' + section); 140 if (b) b.disabled = (curPage <= 0); 141 }); 142 ['next','last'].forEach(function(a) { 143 var b = document.getElementById(PREFIX + a + '-' + section); 144 if (b) b.disabled = (curPage >= totalPages - 1); 145 }); 146 } 147 148 function refresh(section, newPage) { 149 var pager = getPager(section); 150 if (!pager) return; 151 var curPage = (newPage !== undefined) ? newPage : parseInt(pager.dataset.page, 10) || 0; 152 var filtered = applyFilters(getAllRows(section), readSearch(section), readFilters(section)); 153 renderPage(section, filtered, curPage); 154 // (filtered rows không cần sync lên Python store - export callback dùng DB fallback) 155 } 156 157 /* ââ Click: pagination buttons ââââââââââââââââââââââââââââââââââââ */ 158 document.addEventListener('click', function(e) { 159 var btn = e.target.closest('button[id^="' + PREFIX + '"]'); 160 if (!btn) return; 161 var id = btn.id; 162 var section = null, action = null; 163 ['first','prev','next','last'].forEach(function(a) { 164 var s = sectionFromId(id, a); 165 if (s) { section = s; action = a; } 166 }); 167 if (!section) return; 168 169 var pager = getPager(section); 170 if (!pager) return; 171 var curPage = parseInt(pager.dataset.page, 10) || 0; 172 var total = parseInt(pager.dataset.total, 10) || 0; 173 var pageSize = parseInt(pager.dataset.pagesize, 10) || 10; 174 var totalPages = Math.max(1, Math.ceil(total / pageSize)); 175 176 if (action === 'first') curPage = 0; 177 else if (action === 'prev') curPage = Math.max(0, curPage - 1); 178 else if (action === 'next') curPage = Math.min(totalPages - 1, curPage + 1); 179 else if (action === 'last') curPage = totalPages - 1; 180 181 refresh(section, curPage); 182 }); 183 184 /* ââ Change: filter dropdowns (html.Select â support data-* natively) */ 185 document.addEventListener('change', function(e) { 186 var el = e.target; 187 // html.Select: có data-action="filter" 188 if (el.dataset && el.dataset.action === 'filter' && el.dataset.section && el.dataset.filterkey) { 189 var pager = getPager(el.dataset.section); 190 if (!pager) return; 191 var filters = readFilters(el.dataset.section); 192 filters[el.dataset.filterkey] = el.value || ''; 193 pager.dataset.filters = JSON.stringify(filters); 194 refresh(el.dataset.section, 0); 195 return; 196 } 197 // Fallback: parse from id (asset-hist-filter-{key}-{section}) 198 var parsed = el.id ? parseFilterId(el.id) : null; 199 if (parsed) { 200 var pager2 = getPager(parsed.section); 201 if (!pager2) return; 202 var filters2 = readFilters(parsed.section); 203 filters2[parsed.filterKey] = el.value || ''; 204 pager2.dataset.filters = JSON.stringify(filters2); 205 refresh(parsed.section, 0); 206 } 207 }); 208 209 /* ââ Input/change: search box (dbc.Input â id only, no data-*) ââââ */ 210 function onSearch(e) { 211 var el = e.target; 212 var id = el.id || ''; 213 var section = sectionFromId(id, 'search'); 214 if (!section) return; 215 refresh(section, 0); 216 } 217 document.addEventListener('input', onSearch); 218 document.addEventListener('change', onSearch); 219 220})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.