1/** 2 * maint_settings_tables.js 3 * Handles Area, Details, Break Time tables in Maintenance Settings modal. 4 * - Pagination (100 rows/page) thay vì virtual scroll â ÄÆ¡n giản, không bug scroll 5 * - Pure JS DOM manipulation (no Dash callbacks for add/edit/delete) 6 * - Dirty tracking â warn before close 7 * - Progress/status feedback on save 8 * - All code inside IIFE (no global scope leaks) 9 */ 10(function () { 11 'use strict'; 12 13 var PAGE_SIZE = 100; 14 15 // ââ i18n âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 16 // Reads translations from the window._maintI18n object, which is set by 17 // a clientside callback (see cb_clientside.py) whenever lang-store changes. 18 // Fallback defaults are Vietnamese so the UI is never blank. 19 var _i18nDefaults = { 20 maint_js_del_tooltip: 'Xóa dòng', 21 maint_js_loading: 'â³ Äang tải dữ liá»u...', 22 maint_js_pag_info: '{s}â{e} / {total} (tr.{page}/{pages})', 23 maint_js_prev_page: 'â', 24 maint_js_next_page: 'â¶', 25 maint_js_first_page: 'Trang Äầu', 26 maint_js_last_page: 'Trang cuá»i', 27 maint_js_goto_page: 'Nháºp sá» trang rá»i Enter Äá» Äến trang Äó', 28 maint_settings_unsaved_title: 'Thay Äá»i chưa ÄÆ°á»£c lưu', 29 maint_settings_unsaved_body: 'Bạn có thay Äá»i chưa ÄÆ°á»£c lưu. Thoát mà không lưu?', 30 maint_settings_confirm_back: 'Quay lại', 31 maint_settings_confirm_exit: 'Thoát không lưu', 32 maint_js_no_data: 'Chưa có dữ liá»u. Nhấn "Thêm dòng" hoặc Upload File.', 33 maint_js_saving_db: 'Äang lưu và o database...', 34 maint_js_no_shift_employees: 'Không có nhân viên Äang hoạt Äá»ng trong Shift Schedule.', 35 maint_js_area_empty: 'Chưa có cấu hình. Nhấn "Thêm System" Äá» bắt Äầu.', 36 maint_js_component_select: '-- Chá»n Component --', 37 maint_js_combo_search_ph: 'Tìm...', 38 maint_js_combo_no_match: 'Không tìm thấy', 39 // (FIX <4>) 4 dòng "trá»ng bảng" sau Äây trưá»c Äây hard-code tiếng Viá»t 40 // thẳng trong JS (không qua _t()) nên không bao giá» Äá»i theo ngôn ngữ â 41 // nay ÄÆ°a và o i18n giá»ng các key maint_js_* khác. 42 maint_js_bt_empty: 'Chưa có khung giá». Nhấn "Thêm khung giá»" Äá» bắt Äầu.', 43 maint_js_type_empty: 'Chưa có loại. Nhấn "Thêm loại" Äá» bắt Äầu.', 44 maint_js_status_empty: 'Chưa có trạng thái. Nhấn "Thêm trạng thái" Äá» bắt Äầu.', 45 maint_js_period_empty: 'Chưa có loại kỳ. Nhấn "Thêm loại kỳ" Äá» bắt Äầu.', 46 }; 47 48 // (FIX) Trưá»c Äây _t() chá» nháºn 1 tham sá» nên các chá» gá»i _t(key, defaultText) 49 // (VD: dropdown Component) bá» bá» qua defaultText, và khi key Äó chưa có trong 50 // window._maintI18n / _i18nDefaults thì trả vá» nguyên literal "key" lên UI 51 // (VD: chữ "maint_js_component_select" hiá»n trong dropdown). 52 function _t(key, fallback) { 53 var store = window._maintI18n || {}; 54 return store[key] || _i18nDefaults[key] || fallback || key; 55 } 56 57 function _fmt(tpl, vars) { 58 return tpl.replace(/\{(\w+)\}/g, function(_, k) { 59 return vars[k] !== undefined ? vars[k] : '{' + k + '}'; 60 }); 61 } 62 63 // ââ State ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 64 var _areaData = []; var _areaPage = 0; var _areaLocations = []; var _areaCurrentLocationId = ''; 65 // (2026-08 FIX <filter Area theo há» máy sidebar>) Mặc Äá»nh lá»c BẬT â chá» 66 // hiá»n dòng thuá»c Location Äang chá»n á» sidebar (Äá»ng bá» hà nh vi vá»i tab 67 // "Tần suất PM"). _areaData luôn giữ FULL dữ liá»u (má»i há» máy) Äá» Save 68 // không là m mất dữ liá»u cá»§a há» máy khác â chá» phần HIá»N THá»/phân trang bá» 69 // lá»c qua _areaFilteredIndices(). 70 var _areaFilterByLocation = true; 71 72 function _areaFilteredIndices() { 73 if (!_areaFilterByLocation || !_areaCurrentLocationId) { 74 var all = []; for (var k = 0; k < _areaData.length; k++) all.push(k); 75 return all; 76 } 77 var out = []; 78 _areaData.forEach(function(row, idx) { 79 var locId = row.location_id || _areaCurrentLocationId; 80 if (locId === _areaCurrentLocationId) out.push(idx); 81 }); 82 return out; 83 } 84 var _detData = []; var _detPage = 0; 85 var _btData = []; var _btPage = 0; 86 var _staffData = []; 87 var _typeData = []; 88 var _statusData = []; 89 var _periodData = []; 90 91 // ââ Helpers âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 92 function esc(s) { 93 return String(s == null ? '' : s) 94 .replace(/&/g,'&').replace(/</g,'<') 95 .replace(/>/g,'>').replace(/"/g,'"'); 96 } 97 98 function syncHidden(id, data) { 99 var el = document.getElementById(id); 100 if (!el) return; 101 var json = JSON.stringify(data); 102 if (el.value === json) return; 103 el.value = json; 104 el.dispatchEvent(new Event('input', { bubbles: true })); 105 } 106 107 function isDark() { 108 return document.documentElement.getAttribute('data-theme') === 'dark'; 109 } 110 111 function isMobile() { 112 return document.body.classList.contains('is-mobile-device'); 113 } 114 115 function textColor() { return isDark() ? '#e2e8f0' : '#1e293b'; } 116 function inputBg() { return isDark() ? '#1e293b' : '#ffffff'; } 117 function inputBorder() { return isDark() ? 'rgba(255,255,255,0.15)' : 'rgba(0,0,0,0.15)'; } 118 function borderColor() { return isDark() ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.08)'; } 119 function mutedColor() { return isDark() ? '#94a3b8' : '#64748b'; } 120 121 function applyInputStyle(el, isTextarea) { 122 el.className = (el.className ? el.className + ' ' : '') + 123 (isTextarea ? 'maint-settings-textarea' : 'maint-settings-input'); 124 if (isMobile()) { 125 el.style.fontSize = '0.60rem'; 126 if (!isTextarea) el.style.height = '22px'; 127 else el.style.minHeight = '32px'; 128 } 129 } 130 131 function cellStyle(td, extraCss) { 132 td.className = (td.className ? td.className + ' ' : '') + 'maint-settings-cell'; 133 if (isMobile()) td.style.padding = '2px 4px'; 134 if (extraCss) { 135 extraCss.split(';').forEach(function(part) { 136 part = part.trim(); if (!part) return; 137 var colon = part.indexOf(':'); if (colon < 0) return; 138 var prop = part.slice(0, colon).trim(); 139 var val = part.slice(colon + 1).trim(); 140 var jsProp = prop.replace(/-([a-z])/g, function(_, c) { return c.toUpperCase(); }); 141 td.style[jsProp] = val; 142 }); 143 } 144 } 145
146 // ââ Per-tab dirty tracking (FIX 2026-08) ââââââââââââââââââââââââââââââââ 147 // Trưá»c Äây dùng 1 cá» "_dirty" DÃNG CHUNG cho cả 7 tab, set = true Và ÄIá»U 148 // KIá»N má»i lần gõ (không so sánh vá»i baseline) â 2 bug: 149 // <1> Không có dòng thông báo "Có thay Äá»i chưa lưu" (chỠẩn/hiá»n 2 nút). 150 // <2> Sá»a xong rá»i tá»± sá»a lại vá» Äúng giá trá» cÅ©, 2 nút vẫn không ẩn (vì 151 // _dirty chá» set true, không bao giá» tá»± tắt khi khá»p lại baseline). 152 // <3> Upload file chá» Äá»i data-init (nạp dữ liá»u má»i và o bảng) nhưng 153 // KHÃNG há» gá»i markDirty() á» Äâu cả â 2 nút Save/Cancel + badge không 154 // xuất hiá»n dù dữ liá»u Äã Äá»i và chưa ÄÆ°á»£c lưu và o DB. 155 // 156 // Fix (giá»ng kiến trúc asm_settings_tables.js â 1 baseline Äá»c láºp má»i 157 // tab, so sánh JSON Äá» tá»± báºt/tắt, không phụ thuá»c cá» boolean thá»§ công): 158 // - Má»i tab có 1 baseline riêng "_original[key]" â snapshot Äà LƯU gần 159 // nhất, lấy từ attribute "data-original" (Python set, KHÃNG Äá»i khi 160 // Upload â chá» Äá»i khi Save thà nh công) â xem ui_maintenance.py. 161 // - markDirty(key): so sánh dữ liá»u hiá»n tại cá»§a tab "key" vá»i baseline 162 // â tá»± báºt/tắt Save/Cancel + badge, không set cá» vô Äiá»u kiá»n. 163 // - Upload chá» Äá»i "data-init" (không Äá»i "data-original") â dữ liá»u lá»ch 164 // baseline â dirty tá»± báºt, không cần gá»i markDirty() thá»§ công á» upload. 165 // - Cancel/Lưu-thà nh-công Äá»u là m dữ liá»u khá»p lại "data-original" â 166 // dirty tá»± tắt. 167 var _KEYS = ['area', 'details', 'bt', 'staff', 'type', 'status', 'period']; 168 169 var _TBODY_ID = { 170 area: 'maint-area-js-tbody', details: 'maint-details-js-tbody', bt: 'maint-bt-js-tbody', 171 staff: 'maint-staff-js-tbody', type: 'maint-type-js-tbody', status: 'maint-status-js-tbody', 172 period: 'maint-period-js-tbody', 173 }; 174 var _KEY_BY_TBODY = {}; 175 Object.keys(_TBODY_ID).forEach(function (k) { _KEY_BY_TBODY[_TBODY_ID[k]] = k; }); 176 177 var _BTN_IDS = { 178 area: ['maint-area-save-btn', 'maint-area-cancel-btn'], 179 details: ['maint-details-save-btn', 'maint-details-cancel-btn'], 180 bt: ['maint-bt-save-btn', 'maint-bt-cancel-btn'], 181 staff: ['maint-staff-save-btn', 'maint-staff-cancel-btn'], 182 type: ['maint-type-save-btn', 'maint-type-cancel-btn'], 183 status: ['maint-status-save-btn', 'maint-status-cancel-btn'], 184 period: ['maint-period-save-btn', 'maint-period-cancel-btn'], 185 }; 186 var _BADGE_ID = { 187 area: 'maint-area-unsaved-badge', details: 'maint-details-unsaved-badge', bt: 'maint-bt-unsaved-badge', 188 staff: 'maint-staff-unsaved-badge', type: 'maint-type-unsaved-badge', status: 'maint-status-unsaved-badge', 189 period: 'maint-period-unsaved-badge', 190 }; 191 192 var _original = {}; // key -> JSON string baseline (dữ liá»u Äã lưu gần nhất) 193 194 function _dataFor(key) { 195 switch (key) { 196 case 'area': return _areaData; 197 case 'details': return _detData; 198 case 'bt': return _btData; 199 case 'staff': return _staffData; 200 case 'type': return _typeData; 201 case 'status': return _statusData; 202 case 'period': return _periodData; 203 } 204 return []; 205 } 206 207 function _setKeyDirtyUI(key, dirty) { 208 (_BTN_IDS[key] || []).forEach(function (id) { 209 var btn = document.getElementById(id); 210 if (btn) btn.style.display = dirty ? '' : 'none'; 211 }); 212 var badge = document.getElementById(_BADGE_ID[key]); 213 if (badge) badge.style.display = dirty ? '' : 'none'; 214 } 215 216 // markDirty(key) â gá»i SAU KHI Äã sá»a mảng dữ liá»u cá»§a tab "key" (VD sau 217 // khi gõ input / xóa dòng / thêm dòng) â so sánh vá»i baseline Äá» tá»± báºt 218 // hoặc tắt Save/Cancel + badge, KHÃNG set dirty=true vô Äiá»u kiá»n. 219 function markDirty(key) { 220 if (!key) return; 221 _refreshDirtyUI(key); 222 } 223
224 function _refreshDirtyUI(key) { 225 var same = JSON.stringify(_dataFor(key)) === (_original[key] !== undefined ? _original[key] : '[]'); 226 _setKeyDirtyUI(key, !same); 227 } 228 229 function _refreshAllDirtyUI() { _KEYS.forEach(_refreshDirtyUI); } 230 231 // _captureBaseline(key) â nạp lại baseline từ attribute "data-original" 232 // cá»§a tbody (fallback "data-init" nếu chưa có, VD lần Äầu deploy trưá»c khi 233 // Python ÄÆ°á»£c cáºp nháºt Äá» set data-original). 234 function _captureBaseline(key) { 235 var tb = document.getElementById(_TBODY_ID[key]); 236 if (!tb) return; 237 var raw = tb.getAttribute('data-original'); 238 if (raw === null || raw === undefined) raw = tb.getAttribute('data-init') || '[]'; 239 try { _original[key] = JSON.stringify(JSON.parse(raw)); } 240 catch (e) { _original[key] = '[]'; } 241 } 242 243 // clearDirty(key?) â coi dữ liá»u hiá»n tại cá»§a tab "key" là baseline má»i 244 // (dùng khi bấm Save â ẩn Save/Cancel/badge NGAY tại thá»i Äiá»m click, thay 245 // vì Äợi Dash round-trip xong. data-original tháºt sá»± do Python set lại 246 // sau Äó sẽ ÄÆ°á»£c _captureBaseline() nạp lại â kết quả giá»ng nhau). 247 // Không truyá»n key â clear tất cả (dùng khi Äóng modal không lưu). 248 function clearDirty(key) { 249 if (!key) { _KEYS.forEach(function (k) { clearDirty(k); }); return; } 250 _original[key] = JSON.stringify(_dataFor(key)); 251 _setKeyDirtyUI(key, false); 252 } 253 254 function _anyDirty() { 255 var tableDirty = _KEYS.some(function (k) { 256 return JSON.stringify(_dataFor(k)) !== (_original[k] !== undefined ? _original[k] : '[]'); 257 }); 258 if (tableDirty) return true; 259 // (Fix 2 â 2026-08) Tab "Tá»± Äá»ng PM" (auto) là 1 form ÄÆ¡n giản, dirty- 260 // check cá»§a nó ÄÆ°á»£c tÃnh hoà n toà n phÃa Dash/Python (xem 261 // cb_auto_planning.py::_auto_planning_dirty_check) â không dùng há» 262 // thá»ng _dataFor/_TBODY_ID (chá» dà nh cho các tab dạng bảng). á» Äây chá» 263 // cần Äá»c lại kết quả (badge Äang hiá»n hay ẩn) Äá» gá»p và o _anyDirty() 264 // chung, cho phép nút "Äóng" á» footer modal cÅ©ng chặn + há»i xác nháºn 265 // khi tab Auto Äang có thay Äá»i chưa lưu â y há»t các tab khác. 266 var autoBadge = document.getElementById('maint-auto-unsaved-badge'); 267 if (autoBadge && autoBadge.style.display !== 'none') return true; 268 // (Feature 2026-08 <Tần suất PM dirty-check>) Tab "ð Tần suất PM" cÅ©ng 269 // tÃnh dirty hoà n toà n phÃa Dash/Python (xem cb_pm_frequency.py) â gá»p 270 // và o _anyDirty() chung y há»t tab "auto" á» trên. 271 var pmfreqBadge = document.getElementById('maint-pmfreq-unsaved-badge'); 272 return !!(pmfreqBadge && pmfreqBadge.style.display !== 'none'); 273 } 274 275 // ââ Pagination bar â rendered into inline div next to Save/Cancel âââââââââ 276 // Div IDs: maint-area-pag, maint-det-pag, maint-bt-pag 277 // These are inside modal-body but the observer explicitly ignores changes to them. 278 var _PAG_IDS = ['maint-area-pag', 'maint-det-pag', 'maint-bt-pag']; 279 var _pagStates = {}; // pagId â { currentPage, totalRows, onPageChange } 280 281 function renderPagBar(pagId, currentPage, totalRows, onPageChange) { 282 var c = document.getElementById(pagId); 283 if (!c) return; 284 285 _pagStates[pagId] = { currentPage: currentPage, totalRows: totalRows, onPageChange: onPageChange }; 286 287 var totalPages = Math.max(1, Math.ceil(totalRows / PAGE_SIZE)); 288 if (totalPages <= 1) { c.innerHTML = ''; return; } 289 290 var start = currentPage * PAGE_SIZE + 1; 291 var end = Math.min((currentPage + 1) * PAGE_SIZE, totalRows); 292 293 // Reuse existing buttons â only update text/disabled, no add/remove DOM nodes 294 var first = c.querySelector('._pag-first'); 295 var prev = c.querySelector('._pag-prev'); 296 var pageInput = c.querySelector('._pag-page-input'); 297 var pageTotal = c.querySelector('._pag-page-total'); 298 var next = c.querySelector('._pag-next'); 299 var last = c.querySelector('._pag-last'); 300 var info = c.querySelector('._pag-info'); 301 302 // FIX (2026-08) <First/Last + nháºp sá» trang>: thêm 2 nút First/Last và 1 303 // ô input cho phép gõ thẳng sá» trang muá»n Äến, thay vì chá» Prev/Next Äi 304 // từng trang má»t â hữu Ãch khi dữ liá»u nhiá»u trang (VD Area 1450 dòng / 305 // 100 dòng-trang = 15 trang, phải bấm Next 14 lần má»i tá»i trang cuá»i). 306 // Cùng nguyên tắc "build 1 lần, chá» update giá trá»" như prev/info/next 307 // sẵn có â tránh tạo lại DOM node má»i lần render (giữ focus/con trỠô 308 // input nếu Äang gõ dá», không bá» mất khi renderPagBar() gá»i lại). 309 function goToPage(pageInput1Based) { 310 var st = _pagStates[pagId]; 311 if (!st) return; 312 var tp = Math.max(1, Math.ceil(st.totalRows / PAGE_SIZE)); 313 var p = parseInt(pageInput1Based, 10); 314 if (isNaN(p)) p = st.currentPage + 1; 315 p = Math.max(1, Math.min(tp, p)); 316 st.onPageChange(p - 1); 317 } 318 319 if (!first) { 320 // Build once 321 first = document.createElement('button'); 322 first.className = 'btn btn-sm btn-outline-secondary _pag-first'; 323 first.textContent = 'â®'; 324 first.title = _t('maint_js_first_page', 'Trang Äầu'); 325 first.addEventListener('click', function(e) { 326 e.preventDefault(); e.stopPropagation(); 327 var st = _pagStates[pagId]; 328 if (st && st.currentPage > 0) st.onPageChange(0); 329 }); 330 c.appendChild(first); 331 332 prev = document.createElement('button'); 333 prev.className = 'btn btn-sm btn-outline-secondary _pag-prev'; 334 prev.textContent = 'â'; 335 prev.title = _t('maint_js_prev_page'); 336 prev.addEventListener('click', function(e) { 337 e.preventDefault(); e.stopPropagation(); 338 var st = _pagStates[pagId]; 339 if (st && st.currentPage > 0) st.onPageChange(st.currentPage - 1); 340 }); 341 c.appendChild(prev); 342 343 var pageWrap = document.createElement('span'); 344 pageWrap.className = '_pag-page-wrap'; 345 pageWrap.style.cssText = 'display:inline-flex;align-items:center;gap:3px;margin:0 4px;'; 346 347 pageInput = document.createElement('input'); 348 pageInput.type = 'number'; pageInput.min = '1'; 349 pageInput.className = 'form-control form-control-sm _pag-page-input'; 350 pageInput.style.cssText = 'width:52px;padding:2px 4px;text-align:center;font-size:0.78rem;'; 351 pageInput.title = _t('maint_js_goto_page', 'Nháºp sá» trang rá»i Enter Äá» Äến trang Äó'); 352 // Enter â nhảy trang; blur â snap lại vá» trang hiá»n tại nếu bá» dá»/không hợp lá» 353 pageInput.addEventListener('keydown', function(e) { 354 e.stopPropagation(); 355 if (e.key === 'Enter') { e.preventDefault(); goToPage(pageInput.value); pageInput.blur(); } 356 }); 357 pageInput.addEventListener('click', function(e) { e.stopPropagation(); }); 358 pageInput.addEventListener('blur', function() { 359 var st = _pagStates[pagId]; 360 if (st) pageInput.value = st.currentPage + 1; 361 }); 362 pageWrap.appendChild(pageInput); 363 364 pageTotal = document.createElement('span'); 365 pageTotal.className = '_pag-page-total'; 366 pageTotal.style.cssText = 'font-size:0.78rem;color:#6c757d;white-space:nowrap;'; 367 pageWrap.appendChild(pageTotal); 368 369 c.appendChild(pageWrap); 370 371 next = document.createElement('button'); 372 next.className = 'btn btn-sm btn-outline-secondary _pag-next'; 373 next.textContent = 'â¶'; 374 next.title = _t('maint_js_next_page'); 375 next.addEventListener('click', function(e) { 376 e.preventDefault(); e.stopPropagation(); 377 var st = _pagStates[pagId]; 378 if (st && st.currentPage < Math.ceil(st.totalRows / PAGE_SIZE) - 1) st.onPageChange(st.currentPage + 1); 379 }); 380 c.appendChild(next); 381 382 last = document.createElement('button'); 383 last.className = 'btn btn-sm btn-outline-secondary _pag-last'; 384 last.textContent = 'â'; 385 last.title = _t('maint_js_last_page', 'Trang cuá»i'); 386 last.addEventListener('click', function(e) { 387 e.preventDefault(); e.stopPropagation(); 388 var st = _pagStates[pagId]; 389 if (!st) return; 390 var tp = Math.max(1, Math.ceil(st.totalRows / PAGE_SIZE)); 391 if (st.currentPage < tp - 1) st.onPageChange(tp - 1); 392 }); 393 c.appendChild(last); 394 395 info = document.createElement('span'); 396 info.className = '_pag-info'; 397 info.style.cssText = 'font-size:0.78rem;color:#6c757d;white-space:nowrap;font-weight:500;margin-left:6px;'; 398 c.appendChild(info); 399 } 400 401 // Update values only â no DOM structure change 402 first.disabled = currentPage === 0; 403 prev.disabled = currentPage === 0; 404 next.disabled = currentPage >= totalPages - 1; 405 last.disabled = currentPage >= totalPages - 1; 406 pageInput.max = totalPages; 407 // Không ghi Äè giá trỠô input nếu user Äang gõ dá» (Äang focus) â tránh 408 // giáºt con trá»/xoá mất sá» Äang nháºp khi renderPagBar() bá» gá»i lại do 409 // thao tác khác (VD sá»a 1 ô input trên bảng cÅ©ng trigger markDirty â 410 // không re-render pag, nhưng Äá» an toà n vẫn check document.activeElement). 411 if (document.activeElement !== pageInput) { pageInput.value = currentPage + 1; } 412 pageTotal.textContent = '/ ' + totalPages; 413 info.textContent = _fmt(_t('maint_js_pag_info'), {s: start, e: end, total: totalRows, page: currentPage+1, pages: totalPages}); 414 } 415 416 // (Component dropdown) Danh sách tên Component duy nhất, lấy từ dữ liá»u tab 417 // Details (_detData) â dùng là m option cho dropdown Component á» tab Area. 418 function _componentNameOptions() { 419 var seen = {}, out = [];
420 (_detData || []).forEach(function(r) { 421 var name = (r && r.component_name) ? String(r.component_name).trim() : ''; 422 if (name && !seen[name]) { seen[name] = true; out.push(name); } 423 }); 424 out.sort(function(a, b) { return a.localeCompare(b); }); 425 return out; 426 } 427 428 // ââ Custom Component Combo ââââââââââââââââââââââââââââââââââââââââââââââ 429 // Panel ná»i dùng CHUNG cho má»i dòng (thay vì tạo 1 <select> riêng má»i 430 // dòng) â nhẹ hÆ¡n, và cho phép style hoà n toà n bằng CSS (height, font, 431 // dark mode), có ô tìm kiếm Äá» dá» chá»n khi danh sách Component dà i. 432 var _comboPanel = null, _comboSearchInput = null, _comboList = null; 433 var _comboOnSelect = null, _comboTrigger = null, _comboOptions = [], _comboCurVal = ''; 434 435 function _ensureComboPanel() { 436 if (_comboPanel) return; 437 _comboPanel = document.createElement('div'); 438 _comboPanel.className = 'maint-combo-panel'; 439 _comboPanel.style.display = 'none'; 440 441 var searchWrap = document.createElement('div'); 442 searchWrap.className = 'maint-combo-search-wrap'; 443 _comboSearchInput = document.createElement('input'); 444 _comboSearchInput.type = 'text'; 445 _comboSearchInput.className = 'maint-combo-search'; 446 searchWrap.appendChild(_comboSearchInput); 447 448 _comboList = document.createElement('div'); 449 _comboList.className = 'maint-combo-list'; 450 451 _comboPanel.appendChild(searchWrap); 452 _comboPanel.appendChild(_comboList); 453 document.body.appendChild(_comboPanel); 454 455 _comboSearchInput.addEventListener('input', function () { 456 _renderComboItems(_comboSearchInput.value); 457 }); 458 _comboSearchInput.addEventListener('keydown', function (e) { 459 if (e.key === 'Escape') { _closeCombo(); } 460 }); 461 document.addEventListener('mousedown', function (e) { 462 if (!_comboPanel || _comboPanel.style.display === 'none') return; 463 if (_comboPanel.contains(e.target) || (_comboTrigger && _comboTrigger.contains(e.target))) return; 464 _closeCombo(); 465 }); 466 // Äóng panel khi cuá»n (bảng, modal, trang...) Äá» tránh panel bá» lá»ch vá» 467 // trà so vá»i trigger (panel dùng position:fixed, tÃnh 1 lần lúc má»). 468 document.addEventListener('scroll', function (e) { 469 if (!_comboPanel || _comboPanel.style.display === 'none') return; 470 if (_comboPanel.contains(e.target)) return; 471 _closeCombo(); 472 }, true); 473 window.addEventListener('resize', function () { _closeCombo(); }); 474 } 475 476 function _renderComboItems(filterText) { 477 var ft = (filterText || '').trim().toLowerCase(); 478 _comboList.innerHTML = ''; 479 480 var blank = document.createElement('div'); 481 blank.className = 'maint-combo-item maint-combo-item-blank'; 482 blank.textContent = _t('maint_js_component_select', '-- Chá»n Component --'); 483 if (!ft) { 484 if (!_comboCurVal) blank.classList.add('active'); 485 blank.addEventListener('click', function () { _selectCombo(''); }); 486 _comboList.appendChild(blank); 487 } 488 489 var any = false; 490 _comboOptions.forEach(function (name) { 491 if (ft && name.toLowerCase().indexOf(ft) === -1) return; 492 any = true; 493 var it = document.createElement('div'); 494 it.className = 'maint-combo-item'; 495 it.textContent = name; 496 if (name === _comboCurVal) it.classList.add('active'); 497 it.addEventListener('click', function () { _selectCombo(name); }); 498 _comboList.appendChild(it); 499 }); 500 if (!any && ft) { 501 var empty = document.createElement('div'); 502 empty.className = 'maint-combo-empty'; 503 empty.textContent = _t('maint_js_combo_no_match', 'Không tìm thấy'); 504 _comboList.appendChild(empty); 505 } 506 } 507 508 function _selectCombo(val) { 509 var cb = _comboOnSelect; 510 _closeCombo(); 511 if (cb) cb(val); 512 } 513 514 function _positionCombo() { 515 if (!_comboTrigger) return; 516 var r = _comboTrigger.getBoundingClientRect(); 517 var panelMaxH = 240; 518 var spaceBelow = window.innerHeight - r.bottom; 519 var openUp = spaceBelow < panelMaxH && r.top > spaceBelow; 520 _comboPanel.style.left = Math.max(4, r.left) + 'px'; 521 _comboPanel.style.width = Math.max(r.width, 200) + 'px'; 522 if (openUp) { 523 var h = Math.min(panelMaxH, r.top - 8); 524 _comboPanel.style.maxHeight = h + 'px'; 525 _comboPanel.style.top = (r.top - h) + 'px'; 526 } else { 527 var h2 = Math.min(panelMaxH, spaceBelow - 8); 528 _comboPanel.style.maxHeight = Math.max(h2, 120) + 'px'; 529 _comboPanel.style.top = r.bottom + 'px'; 530 } 531 } 532 533 function _closeCombo() { 534 if (!_comboPanel) return; 535 _comboPanel.style.display = 'none'; 536 _comboTrigger = null; 537 _comboOnSelect = null; 538 } 539 540 function _openCombo(triggerEl, options, curVal, onSelect) { 541 _ensureComboPanel(); 542 if (_comboTrigger === triggerEl && _comboPanel.style.display !== 'none') { 543 _closeCombo(); 544 return; 545 } 546 _comboTrigger = triggerEl; 547 _comboOptions = options; 548 _comboCurVal = curVal; 549 _comboOnSelect = onSelect; 550 _comboSearchInput.value = ''; 551 _comboSearchInput.placeholder = _t('maint_js_combo_search_ph', 'Tìm...'); 552 _renderComboItems(''); 553 _comboPanel.style.display = 'flex'; 554 _positionCombo(); 555 setTimeout(function () { _comboSearchInput.focus(); }, 0); 556 } 557 558 // ââ AREA TABLE ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 559 function renderAreaTable() { 560 _closeCombo(); 561 var tbody = document.getElementById('maint-area-js-tbody'); 562 if (!tbody) return; 563 var ro = tbody.getAttribute('data-readonly') === 'true'; 564 tbody.innerHTML = ''; 565 566 var visibleIdx = _areaFilteredIndices(); 567 568 if (!_areaData.length) { 569 var tr = document.createElement('tr'); 570 var td = document.createElement('td'); 571 td.colSpan = 6; td.className = 'maint-settings-empty-cell'; 572 td.textContent = _t('maint_js_area_empty'); 573 tr.appendChild(td); tbody.appendChild(tr); 574 syncHidden('maint-area-store-hidden', []); 575 renderPagBar('maint-area-pag', 0, 0, function(){}); 576 return; 577 } 578 579 if (!visibleIdx.length) { 580 // Có dữ liá»u nhưng há» máy Äang chá»n không có dòng nà o â khác thông 581 // báo "chưa có cấu hình gì" Äá» không gây hiá»u lầm là mất dữ liá»u. 582 var tr2 = document.createElement('tr'); 583 var td2 = document.createElement('td'); 584 td2.colSpan = 6; td2.className = 'maint-settings-empty-cell';
585 td2.textContent = _t('maint_js_area_empty_filtered', 'Há» máy Äang chá»n chưa có cấu hình Area. Bá» tick "Chá» hiá»n há» máy Äang chá»n" Äá» xem tất cả.'); 586 tr2.appendChild(td2); tbody.appendChild(tr2); 587 renderPagBar('maint-area-pag', 0, 0, function(){}); 588 syncHidden('maint-area-store-hidden', _areaData); 589 return; 590 } 591 592 var frag = document.createDocumentFragment(); 593 var start = _areaPage * PAGE_SIZE; 594 visibleIdx.slice(start, start + PAGE_SIZE).forEach(function(i) { 595 var row = _areaData[i]; 596 var tr = document.createElement('tr'); 597 tr.className = 'maint-settings-row'; 598 599 var fields = ['system_name','equipment_name','area_name','component_id']; 600 fields.forEach(function(f) { 601 var td = document.createElement('td'); 602 cellStyle(td); 603 if (ro) { 604 td.textContent = row[f] || ''; 605 } else if (f === 'component_id') { 606 // (Component dropdown) value lấy từ danh sách component_name cá»§a 607 // tab Details, thay vì cho gõ tá»± do â tránh sai chÃnh tả/không 608 // khá»p vá»i Details khi tÃnh checklist bảo trì. 609 // (FIX) Trưá»c Äây dùng <select> gá»c cá»§a trình duyá»t â popup xấu, 610 // không thá» chá»nh height/font, không theo dark mode. Nay thay bằng 611 // custom combo (trigger + panel ná»i dùng chung, xem _openCombo()). 612 var curVal = row[f] || ''; 613 var trigger = document.createElement('button'); 614 trigger.type = 'button'; 615 trigger.className = 'maint-combo-trigger'; 616 var lbl = document.createElement('span'); 617 lbl.className = 'maint-combo-trigger-label'; 618 var phText = _t('maint_js_component_select', '-- Chá»n Component --'); 619 lbl.textContent = curVal || phText; 620 if (!curVal) lbl.classList.add('is-placeholder'); 621 trigger.appendChild(lbl); 622 var caret = document.createElement('i'); 623 caret.className = 'fas fa-chevron-down maint-combo-caret'; 624 trigger.appendChild(caret); 625 (function(idx, field, triggerEl, labelEl){ 626 triggerEl.addEventListener('click', function(e){ 627 e.preventDefault(); e.stopPropagation(); 628 var cv = _areaData[idx][field] || ''; 629 // Giữ lại giá trá» cÅ© trong dropdown dù nó không (còn) nằm trong 630 // danh sách Details hiá»n tại (VD: Details Äã Äá»i tên) â tránh 631 // mất dữ liá»u / âm thầm Äá»i giá trá» cá»§a dòng khi má»i má» lại. 632 var curOpts = _componentNameOptions(); 633 if (cv && curOpts.indexOf(cv) === -1) curOpts = [cv].concat(curOpts); 634 _openCombo(triggerEl, curOpts, cv, function(val){ 635 _areaData[idx][field] = val; 636 var ph = _t('maint_js_component_select', '-- Chá»n Component --'); 637 labelEl.textContent = val || ph; 638 labelEl.classList.toggle('is-placeholder', !val); 639 syncHidden('maint-area-store-hidden', _areaData); 640 markDirty('area'); 641 }); 642 }); 643 })(i, f, trigger, lbl); 644 td.appendChild(trigger); 645 } else { 646 var inp = document.createElement('input'); 647 inp.type = 'text'; inp.value = row[f] || ''; 648 applyInputStyle(inp, false); 649 (function(idx, field){ 650 inp.addEventListener('input', function(){ 651 _areaData[idx][field] = this.value; 652 syncHidden('maint-area-store-hidden', _areaData); 653 markDirty('area'); 654 }); 655 })(i, f); 656 td.appendChild(inp); 657 } 658 tr.appendChild(td); 659 }); 660 661 // (2026-07-29) Cá»t Location (nhà máy) â KHOÃ, luôn hiá»n thá» text tÄ©nh 662 // (2026-07-30 FIX) tránh data má» côi khi Äá»i há» máy tùy tiá»n. Dòng cÅ© 663 // giữ nguyên location_id Äã gán; dòng má»i (chưa có location_id) sẽ 664 // mặc Äá»nh = ASM Äang chá»n á» sidebar (_areaCurrentLocationId). 665 var tdLoc = document.createElement('td'); 666 cellStyle(tdLoc); 667 var curLocId = row.location_id || _areaCurrentLocationId || (_areaLocations[0] && _areaLocations[0].id) || ''; 668 var foundLoc = _areaLocations.filter(function(l){ return l.id === curLocId; })[0]; 669 tdLoc.textContent = foundLoc ? (foundLoc.name ? (foundLoc.code + ' - ' + foundLoc.name) : foundLoc.code) : ''; 670 if (!row.location_id) { _areaData[i].location_id = curLocId; } 671 tr.appendChild(tdLoc); 672 673 var tdDel = document.createElement('td'); 674 cellStyle(tdDel, 'text-align:center;width:40px'); 675 if (!ro) { 676 var btn = document.createElement('button');
677 btn.innerHTML = '<i class="fas fa-trash"></i>'; 678 btn.className = 'btn btn-sm btn-link p-0'; 679 btn.style.color = '#ef4444'; btn.title = _t('maint_js_del_tooltip'); 680 (function(idx){ 681 btn.addEventListener('click', function(){ 682 _areaData.splice(idx, 1); 683 var visLen = _areaFilteredIndices().length; 684 if (_areaPage > 0 && _areaPage * PAGE_SIZE >= visLen) _areaPage--; 685 syncHidden('maint-area-store-hidden', _areaData); 686 markDirty('area'); renderAreaTable(); 687 }); 688 })(i); 689 tdDel.appendChild(btn); 690 } 691 tr.appendChild(tdDel); 692 frag.appendChild(tr); 693 }); 694 tbody.appendChild(frag); 695 696 renderPagBar('maint-area-pag', _areaPage, visibleIdx.length, function(p) { 697 _areaPage = p; renderAreaTable(); 698 }); 699 syncHidden('maint-area-store-hidden', _areaData); 700 } 701 702 // ââ DETAILS TABLE âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 703 function renderDetailsTable() { 704 var tbody = document.getElementById('maint-details-js-tbody'); 705 if (!tbody) return; 706 var ro = tbody.getAttribute('data-readonly') === 'true'; 707 tbody.innerHTML = ''; 708 709 if (!_detData.length) { 710 var tr = document.createElement('tr'); 711 var td = document.createElement('td'); 712 td.colSpan = 4; td.className = 'maint-settings-empty-cell'; 713 td.textContent = _t('maint_js_no_data'); 714 tr.appendChild(td); tbody.appendChild(tr); 715 syncHidden('maint-details-store-hidden', []); 716 renderPagBar('maint-det-pag', 0, 0, function(){}); 717 return; 718 } 719 720 var frag = document.createDocumentFragment(); 721 var start = _detPage * PAGE_SIZE; 722 _detData.slice(start, start + PAGE_SIZE).forEach(function(row, li) { 723 var i = start + li; 724 var tr = document.createElement('tr'); 725 tr.className = 'maint-settings-row'; 726 727 var td1 = document.createElement('td'); 728 cellStyle(td1, 'width:25%;vertical-align:top'); 729 if (ro) { 730 td1.textContent = row.component_name || ''; 731 td1.classList.add('fw-semibold'); 732 } else { 733 var inp = document.createElement('input'); 734 inp.type = 'text'; inp.value = row.component_name || ''; 735 applyInputStyle(inp, false); 736 (function(idx){ 737 inp.addEventListener('input', function(){ 738 _detData[idx].component_name = this.value; 739 syncHidden('maint-details-store-hidden', _detData); markDirty('details'); 740 }); 741 })(i); 742 td1.appendChild(inp); 743 } 744 tr.appendChild(td1); 745 746 var td2 = document.createElement('td'); 747 cellStyle(td2, 'vertical-align:top'); 748 if (ro) { 749 var pre = document.createElement('pre'); 750 pre.className = 'maint-settings-pre'; 751 if (isMobile()) pre.style.fontSize = '0.60rem'; 752 pre.textContent = row.detail_text || ''; 753 td2.appendChild(pre); 754 } else { 755 var ta = document.createElement('textarea'); 756 ta.value = row.detail_text || ''; 757 applyInputStyle(ta, true); 758 (function(idx){ 759 ta.addEventListener('input', function(){ 760 _detData[idx].detail_text = this.value; 761 syncHidden('maint-details-store-hidden', _detData); markDirty('details'); 762 }); 763 ta.addEventListener('keydown', function(e) { 764 if (e.altKey && e.key === 'Enter') { 765 e.preventDefault(); 766 var s = this.selectionStart; 767 this.value = this.value.substring(0,s)+'\n'+this.value.substring(this.selectionEnd); 768 this.selectionStart = this.selectionEnd = s+1; 769 _detData[idx].detail_text = this.value; 770 syncHidden('maint-details-store-hidden', _detData); markDirty('details'); 771 } 772 }); 773 })(i); 774 td2.appendChild(ta); 775 } 776 tr.appendChild(td2); 777 778 // ââ Cá»t "ð¡ï¸ Tá»§ Äiá»n" (checklist_type) ââââââââââââââââââââââââââââââ 779 var td3 = document.createElement('td'); 780 cellStyle(td3, 'text-align:center;vertical-align:top'); 781 var cb = document.createElement('input'); 782 cb.type = 'checkbox'; 783 cb.checked = (row.checklist_type === 'electrical_cabinet'); 784 cb.disabled = ro; 785 cb.title = _t('maint_details_col_cabinet_hint'); 786 cb.style.width = '18px'; cb.style.height = '18px'; cb.style.cursor = ro ? 'default' : 'pointer'; 787 if (!ro) { 788 (function(idx){ 789 cb.addEventListener('change', function(){ 790 _detData[idx].checklist_type = this.checked ? 'electrical_cabinet' : 'standard'; 791 syncHidden('maint-details-store-hidden', _detData); markDirty('details'); 792 }); 793 })(i); 794 } 795 td3.appendChild(cb); 796 tr.appendChild(td3); 797 798 var tdDel = document.createElement('td'); 799 cellStyle(tdDel, 'text-align:center;width:40px;vertical-align:top'); 800 if (!ro) { 801 var btn = document.createElement('button');
802 btn.innerHTML = '<i class="fas fa-trash"></i>'; 803 btn.className = 'btn btn-sm btn-link p-0'; 804 btn.style.color = '#ef4444'; btn.title = _t('maint_js_del_tooltip'); 805 (function(idx){ 806 btn.addEventListener('click', function(){ 807 _detData.splice(idx, 1); 808 if (_detPage > 0 && _detPage * PAGE_SIZE >= _detData.length) _detPage--; 809 syncHidden('maint-details-store-hidden', _detData); 810 markDirty('details'); renderDetailsTable(); 811 }); 812 })(i); 813 tdDel.appendChild(btn); 814 } 815 tr.appendChild(tdDel); 816 frag.appendChild(tr); 817 }); 818 tbody.appendChild(frag); 819 820 renderPagBar('maint-det-pag', _detPage, _detData.length, function(p) { 821 _detPage = p; renderDetailsTable(); 822 }); 823 syncHidden('maint-details-store-hidden', _detData); 824 } 825 826 // ââ BREAK TIME TABLE ââââââââââââââââââââââââââââââââââââââââââââââââââââââ 827 function renderBtTable() { 828 var tbody = document.getElementById('maint-bt-js-tbody'); 829 if (!tbody) return; 830 var ro = tbody.getAttribute('data-readonly') === 'true'; 831 tbody.innerHTML = ''; 832 833 if (!_btData.length) { 834 var tr = document.createElement('tr'); 835 var td = document.createElement('td'); 836 td.colSpan = 3; td.className = 'maint-settings-empty-cell'; 837 td.textContent = _t('maint_js_bt_empty', 'Chưa có khung giá». Nhấn "Thêm khung giá»" Äá» bắt Äầu.'); 838 tr.appendChild(td); tbody.appendChild(tr); 839 syncHidden('maint-bt-store-hidden', []); 840 renderPagBar('maint-bt-pag', 0, 0, function(){}); 841 return; 842 } 843 844 var frag = document.createDocumentFragment(); 845 var start = _btPage * PAGE_SIZE; 846 _btData.slice(start, start + PAGE_SIZE).forEach(function(row, li) { 847 var i = start + li; 848 var tr = document.createElement('tr'); 849 tr.className = 'maint-settings-row'; 850 851 ['start_time','end_time'].forEach(function(f) { 852 var td = document.createElement('td'); 853 cellStyle(td); 854 if (ro) { 855 td.textContent = row[f] || ''; 856 } else { 857 var inp = document.createElement('input'); 858 inp.type = 'text'; inp.value = row[f] || ''; 859 inp.placeholder = 'HH:MM'; 860 applyInputStyle(inp, false); 861 (function(idx, field){ 862 inp.addEventListener('input', function(){ 863 _btData[idx][field] = this.value; 864 syncHidden('maint-bt-store-hidden', _btData); markDirty('bt'); 865 }); 866 })(i, f); 867 td.appendChild(inp); 868 } 869 tr.appendChild(td); 870 }); 871 872 var tdDel = document.createElement('td'); 873 cellStyle(tdDel, 'text-align:center;width:40px'); 874 if (!ro) { 875 var btn = document.createElement('button'); 876 btn.innerHTML = '<i class="fas fa-trash"></i>'; 877 btn.className = 'btn btn-sm btn-link p-0'; 878 btn.style.color = '#ef4444'; 879 (function(idx){ 880 btn.addEventListener('click', function(){ 881 _btData.splice(idx, 1); 882 if (_btPage > 0 && _btPage * PAGE_SIZE >= _btData.length) _btPage--; 883 syncHidden('maint-bt-store-hidden', _btData); 884 markDirty('bt'); renderBtTable(); 885 }); 886 })(i); 887 tdDel.appendChild(btn); 888 } 889 tr.appendChild(tdDel); 890 frag.appendChild(tr); 891 }); 892 tbody.appendChild(frag); 893 894 renderPagBar('maint-bt-pag', _btPage, _btData.length, function(p) { 895 _btPage = p; renderBtTable(); 896 }); 897 syncHidden('maint-bt-store-hidden', _btData); 898 } 899 900 // ââ styleHeaders: no-op (CSS handles it) ââââââââââââââââââââââââââââââââââ 901 function styleHeaders() {} 902 903 // ââ Spinner overlay âââââââââââââââââââââââââââââââââââââââââââââââââââââââ 904 function _getOrCreateSpinner(msgId) { 905 var spinnerId = msgId + '-js-spinner'; 906 var el = document.getElementById(spinnerId); 907 if (el) return el; 908 var dashDiv = document.getElementById(msgId); 909 if (!dashDiv) return null; 910 el = document.createElement('div'); 911 el.id = spinnerId; 912 el.style.cssText = 'margin-bottom:6px;min-height:22px'; 913 dashDiv.parentNode.insertBefore(el, dashDiv); 914 return el; 915 } 916 917 function showSaving(msgId) { 918 var sp = _getOrCreateSpinner(msgId); 919 if (!sp) return; 920 sp.innerHTML = '<span style="color:#f59e0b;font-weight:600">' + 921 '<i class="fas fa-spinner fa-spin me-1"></i>' + _t('maint_js_saving_db') + '</span>'; 922 sp._watching = true; 923 var dashDiv = document.getElementById(msgId); 924 if (!dashDiv || dashDiv._spinObs) return; 925 dashDiv._spinObs = true; 926 new MutationObserver(function() { 927 if (sp._watching) { sp.innerHTML = ''; sp._watching = false; } 928 }).observe(dashDiv, { childList: true, subtree: true }); 929 } 930 931 function setupSaveProgress() { 932 // area/details/bt: force syncHidden BEFORE Dash reads State(store-hidden) 933 // This prevents race condition where Dash snapshots store before JS writes it. 934 var syncMap = [ 935 { btnId: 'maint-area-save-btn', msgId: 'maint-area-msg', key: 'area', 936 storeId: 'maint-area-store-hidden', getData: function(){ return _areaData; } }, 937 { btnId: 'maint-details-save-btn', msgId: 'maint-details-msg', key: 'details', 938 storeId: 'maint-details-store-hidden', getData: function(){ return _detData; } }, 939 { btnId: 'maint-bt-save-btn', msgId: 'maint-bt-msg', key: 'bt', 940 storeId: 'maint-bt-store-hidden', getData: function(){ return _btData; } }, 941 // staff/type/status also get explicit sync on save 942 { btnId: 'maint-staff-save-btn', msgId: 'maint-staff-msg', key: 'staff', 943 storeId: 'maint-staff-store-hidden', getData: function(){ return _staffData; } }, 944 { btnId: 'maint-type-save-btn', msgId: 'maint-type-msg', key: 'type', 945 storeId: 'maint-type-store-hidden', getData: function(){ return _typeData; } }, 946 { btnId: 'maint-status-save-btn', msgId: 'maint-status-msg', key: 'status', 947 storeId: 'maint-status-store-hidden', getData: function(){ return _statusData;} }, 948 { btnId: 'maint-period-save-btn', msgId: 'maint-period-msg', key: 'period', 949 storeId: 'maint-period-store-hidden', getData: function(){ return _periodData;} }, 950 ];
951 syncMap.forEach(function(cfg) { 952 var btn = document.getElementById(cfg.btnId); 953 if (!btn || btn._progressBound) return; 954 btn._progressBound = true; 955 btn.addEventListener('click', function() { 956 // Force-sync store FIRST (synchronous) so Dash State reads the latest value 957 syncHidden(cfg.storeId, cfg.getData()); 958 if (cfg.msgId.indexOf('bt') < 0 && cfg.msgId.indexOf('staff') < 0 && 959 cfg.msgId.indexOf('type') < 0 && cfg.msgId.indexOf('status') < 0 && 960 cfg.msgId.indexOf('period') < 0) { 961 setTimeout(function() { showSaving(cfg.msgId); }, 80); 962 } 963 // Ẩn Save/Cancel/badge NGAY lúc bấm (không Äợi server round-trip) â 964 // data-original tháºt do Python trả vá» sau Äó sẽ tá»± Äá»ng bá» lại qua 965 // MutationObserver (xem watch data-original bên dưá»i). 966 clearDirty(cfg.key); 967 }); 968 }); 969 } 970 971 // ââ Auto-dismiss ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 972 // (FIX 2026-08 <React removeChild crash>) TRƯá»C ÄÃY hà m nà y tá»± ý xoá DOM 973 // bằng el.innerHTML='' sau khi hết giá» â nhưng React (Dash) vẫn coi các 974 // node bên trong `el` là do Nà quản lý (không biết bá» JS xoá tay). Äến khi 975 // React re-render lại `el` (VD Lưu lần tiếp theo, banner có key má»i), nó cá» 976 // gỡ (removeChild) 1 node Äã bá» JS xoá mất từ trưá»c -> ném lá»i 977 // "NotFoundError: Failed to execute 'removeChild' on 'Node'" ra console. 978 // Sá»a: KHÃNG xoá DOM nữa â chỠẩn trá»±c quan bằng opacity+visibility, giữ 979 // nguyên node Äá» React tá»± thay thế Äúng cách á» lần render kế tiếp. 980 function setupAutoDismiss(msgId) { 981 var el = document.getElementById(msgId); 982 if (!el || el._adObs) return; 983 el._adObs = true; 984 new MutationObserver(function() { 985 var inner = el.querySelector('[data-autohide]'); 986 if (!inner || inner._adTimerSet) return; 987 inner._adTimerSet = true; 988 var ms = parseInt(inner.getAttribute('data-autohide') || '4000'); 989 setTimeout(function() { 990 inner.style.transition = 'opacity 0.5s'; 991 inner.style.opacity = '0'; 992 setTimeout(function() { 993 // ChỠẩn trá»±c quan (không tương tác, không chiếm chá») â KHÃNG gá»i 994 // removeChild/innerHTML='' Äá» tránh xung Äá»t vá»i React reconciler. 995 inner.style.visibility = 'hidden'; 996 inner.style.pointerEvents = 'none'; 997 inner.style.height = '0'; 998 inner.style.overflow = 'hidden'; 999 inner.style.margin = '0'; 1000 }, 520); 1001 }, ms); 1002 }).observe(el, { childList: true, subtree: true }); 1003 } 1004 1005 // ââ Custom confirm modal ââââââââââââââââââââââââââââââââââââââââââââââââââ 1006 // (FIX 2026-08 <item 3>) Má» modal xác nháºn qua ÄÃNG ÄÆ°á»ng Dash (click nút ẩn 1007 // "maint-settings-confirm-open-trigger" -> callback Python set is_open=True), 1008 // KHÃNG còn gá»i window.bootstrap.Modal(...).show() thẳng lên DOM cá»§a dbc.Modal 1009 // nữa. dbc.Modal là component React (react-bootstrap), không phải bootstrap.js 1010 // gá»c â ép má» bằng bootstrap.js khiến state is_open ná»i bá» cá»§a Dash/React 1011 // không bao giá» Äá»ng bá» True, Äá» lại backdrop/overlay kẹt lại phÃa trên sau khi 1012 // Äóng, khiến modal Settings chÃnh trông như "không Äóng ÄÆ°á»£c" dù thá»±c ra nó 1013 // Äã Äóng phÃa dưá»i lá»p backdrop kẹt lại Äó. 1014 var _maintConfirmCallback = null; 1015 1016 function showMaintConfirm(onConfirm) { 1017 _maintConfirmCallback = onConfirm; 1018 var trigger = document.getElementById('maint-settings-confirm-open-trigger'); 1019 if (trigger) { trigger.click(); return; } 1020 // Fallback cá»±c hiếm (trigger không có trong DOM vì lý do gì Äó) â giữ lại 1021 // overlay tá»± vẽ như phương án dá»± phòng cuá»i cùng.
1022 var overlay = document.createElement('div'); 1023 overlay.id = 'maint-dirty-confirm-overlay'; 1024 overlay.style.cssText = 'position:fixed;top:0;left:0;width:100vw;height:100vh;background:rgba(0,0,0,0.5);z-index:99999;display:flex;align-items:center;justify-content:center'; 1025 var dark = isDark(); 1026 var bg = dark?'#1e293b':'#fff', tc = dark?'#e2e8f0':'#1e293b', bord = dark?'rgba(255,255,255,0.12)':'rgba(0,0,0,0.12)'; 1027 var box = document.createElement('div'); 1028 box.style.cssText = 'background:'+bg+';color:'+tc+';border:1px solid '+bord+';border-radius:8px;padding:24px 28px;min-width:320px;max-width:440px;box-shadow:0 20px 60px rgba(0,0,0,0.4);font-family:inherit'; 1029 box.innerHTML = '<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px;font-size:1.05rem;font-weight:600"><i class="fas fa-exclamation-triangle" style="color:#f59e0b"></i>' + _t('maint_settings_unsaved_title') + '</div><p style="margin:0 0 20px;font-size:0.9rem;opacity:0.85">' + _t('maint_settings_unsaved_body') + '</p><div style="display:flex;justify-content:flex-end;gap:10px"><button id="_maint-confirm-cancel" style="padding:6px 18px;border-radius:5px;border:1px solid '+bord+';background:transparent;color:'+tc+';cursor:pointer;font-size:0.88rem">' + _t('maint_settings_confirm_back') + '</button><button id="_maint-confirm-ok" style="padding:6px 18px;border-radius:5px;border:none;background:#ef4444;color:#fff;cursor:pointer;font-size:0.88rem;font-weight:600">' + _t('maint_settings_confirm_exit') + '</button></div>'; 1030 overlay.appendChild(box); document.body.appendChild(overlay); 1031 document.getElementById('_maint-confirm-cancel').addEventListener('click', function(){ overlay.remove(); _maintConfirmCallback = null; }); 1032 document.getElementById('_maint-confirm-ok').addEventListener('click', function(){ overlay.remove(); if (_maintConfirmCallback){ _maintConfirmCallback(); _maintConfirmCallback = null; } }); 1033 } 1034 1035 // (FIX 2026-08 <item 3>) Khi bấm nút OK tháºt trong modal (id 'maint-settings- 1036 // confirm-ok-btn'), Dash Tá»° Äóng cả 'maint-settings-confirm-modal' LẪN 1037 // 'maint-settings-modal' qua 2 callback Python riêng (Äã ÄÄng ký Input Äúng 1038 // trên nút nà y) â JS á» Äây CHá» cần chạy phần "revert dữ liá»u vá» bản Äã lưu", 1039 // không cần tá»± tay ẩn modal/backdrop hay giả láºp click lại nút Äóng nữa 1040 // (là m váºy trưá»c Äây chÃnh là nguá»n gây ra tình trạng Äóng "chưa dứt Äiá»m"). 1041 document.addEventListener('click', function(e) { 1042 var okBtn = e.target.closest('#maint-settings-confirm-ok-btn'); 1043 if (okBtn && _maintConfirmCallback) { 1044 var cb = _maintConfirmCallback; _maintConfirmCallback = null; 1045 setTimeout(cb, 80); 1046 } 1047 var cancelBtn = e.target.closest('#maint-settings-confirm-cancel-btn'); 1048 if (cancelBtn) { _maintConfirmCallback = null; } 1049 }); 1050 1051 // ââ Dirty-check âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1052 // (FIX 2026-08) "Thoát không lưu" trưá»c Äây gá»i clearDirty() â hà m nà y chá» 1053 // coi dữ liá»u HIá»N TẠI (có thá» Äang là ná»i dung vừa Upload nhưng CHƯA lưu) 1054 // là baseline má»i, KHÃNG há» revert lại dữ liá»u/att "data-init" vá» Äúng bản 1055 // Äã lưu trong DB (giá»ng há»t bug Äã sá»a á» nút Cancel â xem comment á» Äó). 1056 // Háºu quả: Äóng modal qua ÄÆ°á»ng "Thoát không lưu" (khác vá»i bấm Cancel 1057 // từng tab) Äá» lại "data-init" bá» kẹt sai lá»ch, gây hiá»n tượng má» lại modal 1058 // thấy "Chưa có cấu hình" + badge "chưa lưu" dù không sá»a gì. Sá»a: thay vì 1059 // tá»± tÃnh lại baseline, giả láºp click nút Cancel cá»§a TỪNG tab (nếu Äang 1060 // hiá»n, tức Äang dirty) â tái dùng Äúng logic revert-vá»-data-original + 1061 // Äá»ng bá» lại data-init Äã có sẵn á» Äó. 1062 function _revertAllTabsToOriginal() { 1063 ['maint-area-cancel-btn', 'maint-details-cancel-btn', 'maint-bt-cancel-btn', 'maint-auto-cancel-btn', 'maint-pmfreq-cancel-btn'].forEach(function(id){ 1064 var btn = document.getElementById(id); 1065 if (btn && btn.style.display !== 'none') btn.click(); 1066 }); 1067 clearDirty(); // dá»n ná»t các tab không có nút Cancel riêng (staff/type/status/period) 1068 } 1069
1070 // (FIX 2026-08 <item 3>) closeBtn/nút-X giá» CHá» cần gá»i showMaintConfirm() + 1071 // revert dữ liá»u khi ngưá»i dùng xác nháºn â KHÃNG còn phải tá»± re-click 1072 // closeBtn/dc theo kiá»u "giả láºp" nữa, vì cả 2 callback Python (Äóng confirm- 1073 // modal Và Äóng settings-modal) Äã lắng nghe TRá»°C TIẾP trên nút OK tháºt 1074 // ('maint-settings-confirm-ok-btn') rá»i â chá» cần Dash nháºn Äúng click Äó 1075 // (qua round-trip is_open bình thưá»ng) là cả 2 modal tá»± Äóng Äúng cách. 1076 function setupDirtyCheck() { 1077 var closeBtn = document.getElementById('maint-settings-close-btn'); 1078 if (closeBtn && !closeBtn._dirtyBound) { 1079 closeBtn._dirtyBound = true; 1080 closeBtn.addEventListener('click', function(e) { 1081 if (_anyDirty()) { e.preventDefault(); e.stopImmediatePropagation(); 1082 showMaintConfirm(function(){ _revertAllTabsToOriginal(); }); } 1083 }, true); 1084 } 1085 var modalEl = document.getElementById('maint-settings-modal'); 1086 if (!modalEl || modalEl._xDirtyBound) return; 1087 modalEl._xDirtyBound = true; 1088 modalEl.addEventListener('click', function(e) { 1089 var xBtn = e.target.closest('.modal-header .btn-close, .modal-header [data-bs-dismiss="modal"]'); 1090 if (!xBtn || !_anyDirty()) return; 1091 e.preventDefault(); e.stopImmediatePropagation(); 1092 showMaintConfirm(function(){ _revertAllTabsToOriginal(); }); 1093 }, true); 1094 } 1095 1096 // ââ ADD buttons âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1097 function bindAddButtons() { 1098 var areaFilterChk = document.getElementById('maint-area-filter-loc-chk'); 1099 if (areaFilterChk && !areaFilterChk._bound) { 1100 areaFilterChk._bound = true; 1101 areaFilterChk.addEventListener('change', function() { 1102 _areaFilterByLocation = !!this.checked; 1103 _areaPage = 0; 1104 renderAreaTable(); 1105 }); 1106 } 1107 var aBtn = document.getElementById('maint-area-add-system-btn'); 1108 if (aBtn && !aBtn._bound) { 1109 aBtn._bound = true; 1110 aBtn.addEventListener('click', function(e) { 1111 e.preventDefault(); e.stopImmediatePropagation(); 1112 _areaData.push({ system_name:'', equipment_name:'', area_name:'', component_id:'', 1113 location_id: _areaCurrentLocationId || (_areaLocations[0] && _areaLocations[0].id) || '' }); 1114 // Dòng má»i luôn thuá»c há» máy Äang chá»n nên luôn nằm trong danh sách 1115 // Äang lá»c â nhảy tá»i trang cuá»i cá»§a danh sách Äà Lá»C (không phải 1116 // _areaData.length, nếu không sẽ nhảy sai trang khi Äang báºt lá»c). 1117 _areaPage = Math.floor((_areaFilteredIndices().length - 1) / PAGE_SIZE); 1118 syncHidden('maint-area-store-hidden', _areaData); 1119 markDirty('area'); renderAreaTable(); 1120 // Focus last input on new row 1121 setTimeout(function(){ 1122 var tbody = document.getElementById('maint-area-js-tbody'); 1123 if (!tbody) return; 1124 var rows = tbody.querySelectorAll('tr.maint-settings-row'); 1125 var last = rows[rows.length - 1]; 1126 if (last) { var inp = last.querySelector('input'); if (inp) inp.focus(); } 1127 }, 30); 1128 }); 1129 } 1130 var dBtn = document.getElementById('maint-details-add-btn'); 1131 if (dBtn && !dBtn._bound) { 1132 dBtn._bound = true; 1133 dBtn.addEventListener('click', function(e) { 1134 e.preventDefault(); e.stopImmediatePropagation(); 1135 _detData.push({ component_name:'', detail_text:'' }); 1136 _detPage = Math.floor((_detData.length - 1) / PAGE_SIZE); 1137 syncHidden('maint-details-store-hidden', _detData); 1138 markDirty('details'); renderDetailsTable(); 1139 setTimeout(function(){ 1140 var tbody = document.getElementById('maint-details-js-tbody'); 1141 if (!tbody) return; 1142 var rows = tbody.querySelectorAll('tr.maint-settings-row'); 1143 var last = rows[rows.length - 1]; 1144 if (last) { var inp = last.querySelector('input'); if (inp) inp.focus(); } 1145 }, 30); 1146 }); 1147 }
1148 var bBtn = document.getElementById('maint-bt-add-btn'); 1149 if (bBtn && !bBtn._bound) { 1150 bBtn._bound = true; 1151 bBtn.addEventListener('click', function(e) { 1152 e.preventDefault(); e.stopImmediatePropagation(); 1153 _btData.push({ start_time:'', end_time:'' }); 1154 _btPage = Math.floor((_btData.length - 1) / PAGE_SIZE); 1155 syncHidden('maint-bt-store-hidden', _btData); 1156 markDirty('bt'); renderBtTable(); 1157 setTimeout(function(){ 1158 var tbody = document.getElementById('maint-bt-js-tbody'); 1159 if (!tbody) return; 1160 var rows = tbody.querySelectorAll('tr.maint-settings-row'); 1161 var last = rows[rows.length - 1]; 1162 if (last) { var inp = last.querySelector('input'); if (inp) inp.focus(); } 1163 }, 30); 1164 }); 1165 } 1166 1167 // ââ Cancel buttons â Area/Details/Break Time. FIX (2026-08): revert lại 1168 // "data-original" (snapshot Äà LƯU gần nhất trong DB â Python quản lý, 1169 // KHÃNG Äá»i khi Upload) thay vì "data-init" (có thá» Äang là dữ liá»u vừa 1170 // Upload nhưng CHƯA lưu â nếu revert theo data-init thì Cancel sau khi 1171 // Upload sẽ giữ nguyên dữ liá»u upload thay vì há»§y nó, sai vá»i ý nghÄ©a 1172 // "Há»§y" cá»§a ngưá»i dùng). 1173 var aCancelBtn = document.getElementById('maint-area-cancel-btn'); 1174 if (aCancelBtn && !aCancelBtn._cancelBound) { 1175 aCancelBtn._cancelBound = true; 1176 aCancelBtn.addEventListener('click', function(e) { 1177 e.preventDefault(); e.stopImmediatePropagation(); 1178 var tbody = document.getElementById('maint-area-js-tbody'); 1179 if (tbody) { 1180 var raw = tbody.getAttribute('data-original'); 1181 if (raw === null) raw = tbody.getAttribute('data-init') || '[]'; 1182 try { _areaData = JSON.parse(raw); } catch(e2) { _areaData = []; } 1183 // FIX (2026-08): Äá»ng bá» lại "data-init" khá»p vá»i dữ liá»u vừa 1184 // revert. Trưá»c Äây chá» set _areaData trong bá» nhá» mà KHÃNG cáºp 1185 // nháºt attribute "data-init" trên DOM â nếu trưá»c Äó user Äã 1186 // Upload file (data-init Äang là dữ liá»u upload, KHÃC data-original), 1187 // attribute nà y bá» "kẹt" sai lá»ch. Lần má» modal kế tiếp, Python 1188 // re-render Tbody vá»i data-init="[]" (Area luôn nạp async) â DOM 1189 // patch coi Äây là 1 thay Äá»i tháºt (từ giá trá» upload cÅ© â "[]"), 1190 // kÃch hoạt observer "data-init" tưá»ng nhầm là dữ liá»u THẬT (rá»ng) 1191 // thay vì hiá»u Äây là tÃn hiá»u "cần fetch lại" â hiá»n nhầm "Chưa 1192 // có cấu hình" + badge "chưa lưu" (baseline cÅ© không khá»p) cho 1193 // Äến khi Äóng/má» lại modal lần 2. Set lại data-init á» Äây giữ 2 1194 // attribute luôn Äá»ng bá», tránh mutation giả á» lần má» kế tiếp. 1195 tbody.setAttribute('data-init', raw); 1196 syncHidden('maint-area-store-hidden', _areaData); 1197 _areaPage = 0; 1198 renderAreaTable(); 1199 } 1200 // FIX <1>: Cancel phải xoá luôn dòng thông báo Upload ("â Äá»c N 1201 // dòng thà nh công...") â trưá»c Äây chỠẩn 2 nút Save/Cancel + badge 1202 // (qua clearDirty) nhưng Äá» nguyên ná»i dung #maint-area-msg. 1203 var msg = document.getElementById('maint-area-msg'); 1204 if (msg) msg.innerHTML = ''; 1205 clearDirty('area'); 1206 }); 1207 } 1208 var dCancelBtn = document.getElementById('maint-details-cancel-btn'); 1209 if (dCancelBtn && !dCancelBtn._cancelBound) { 1210 dCancelBtn._cancelBound = true; 1211 dCancelBtn.addEventListener('click', function(e) { 1212 e.preventDefault(); e.stopImmediatePropagation(); 1213 var tbody = document.getElementById('maint-details-js-tbody'); 1214 if (tbody) { 1215 var raw = tbody.getAttribute('data-original'); 1216 if (raw === null) raw = tbody.getAttribute('data-init') || '[]'; 1217 try { _detData = JSON.parse(raw); } catch(e2) { _detData = []; } 1218 tbody.setAttribute('data-init', raw); // xem giải thÃch á» nút Cancel Area phÃa trên 1219 syncHidden('maint-details-store-hidden', _detData); 1220 _detPage = 0; 1221 renderDetailsTable(); 1222 } 1223 var msg = document.getElementById('maint-details-msg'); 1224 if (msg) msg.innerHTML = ''; 1225 clearDirty('details'); 1226 }); 1227 } 1228 var bCancelBtn = document.getElementById('maint-bt-cancel-btn'); 1229 if (bCancelBtn && !bCancelBtn._cancelBound) { 1230 bCancelBtn._cancelBound = true; 1231 bCancelBtn.addEventListener('click', function(e) { 1232 e.preventDefault(); e.stopImmediatePropagation(); 1233 var tbody = document.getElementById('maint-bt-js-tbody'); 1234 if (tbody) { 1235 var raw = tbody.getAttribute('data-original'); 1236 if (raw === null) raw = tbody.getAttribute('data-init') || '[]'; 1237 try { _btData = JSON.parse(raw); } catch(e2) { _btData = []; } 1238 tbody.setAttribute('data-init', raw); // xem giải thÃch á» nút Cancel Area phÃa trên 1239 syncHidden('maint-bt-store-hidden', _btData); 1240 _btPage = 0; 1241 renderBtTable(); 1242 } 1243 var msg = document.getElementById('maint-bt-msg'); 1244 if (msg) msg.innerHTML = ''; 1245 clearDirty('bt'); 1246 }); 1247 } 1248 } 1249 1250 // ââ Area tab: fetch authoritative config from server âââââââââââââââââââââââ 1251 // Extracted to module scope (previously an inner closure of initTables()) 1252 // so it can ALSO be called from the "data-open-nonce" MutationObserver 1253 // below â needed to fix the "phải Äóng/má» modal 2 lần má»i thấy config" 1254 // bug (see full explanation on the data-open-nonce observer). 1255 var _areaFetchInFlight = false;
1256 function _fetchAreaConfig(retryLeft) { 1257 var at = document.getElementById('maint-area-js-tbody'); 1258 if (!at) return; 1259 if (retryLeft === undefined) retryLeft = 1; 1260 if (_areaFetchInFlight) { 1261 // FIX (2026-08) <duplicate fetch>: initTables() (qua childList observer 1262 // khi Dash tạo tbody Area Má»I) Và observer theo dõi "data-open-nonce" 1263 // (bắt cả trưá»ng hợp Dash chá» PATCH lại node cÅ©) Äá»u có thá» gá»i hà m 1264 // nà y gần như Äá»ng thá»i má»i lần má» modal. Trưá»c Äây cá» nà y chá» chặn 1265 // viá»c ghi Äè innerHTML "loading" 2 lần, KHÃNG chặn gá»i fetch() lần 2 1266 // â 2 request chạy song song, tuy cùng trả kết quả giá»ng nhau nhưng 1267 // tá»n API call thừa và tÄng khả nÄng Äụng Äá» thá»i Äiá»m cáºp nháºt DOM. 1268 // Giá»: nếu Äã có 1 request Äang chạy, bá» qua hẳn lần gá»i nà y (request 1269 // Äang chạy khi xong sẽ tá»± cáºp nháºt Äầy Äá»§ data-init/data-original/ 1270 // baseline/dirty UI cho TẤT CẢ các trigger, không cần gá»i thêm). 1271 return; 1272 } 1273 _areaFetchInFlight = true; 1274 at.innerHTML = '<tr><td colspan="5" class="maint-settings-empty-cell">' + _t('maint_js_loading') + '</td></tr>'; 1275 fetch('/api/maint_area_config') 1276 .then(function(r){ if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); }) 1277 .then(function(data){ 1278 _areaFetchInFlight = false; 1279 _areaData = Array.isArray(data) ? data : []; 1280 var _json = JSON.stringify(_areaData); 1281 at.setAttribute('data-init', _json); 1282 // FIX: tab Area nạp dữ liá»u KHÃNG Äá»ng bá» (fetch riêng, không 1283 // qua data-init lúc Dash render modal â xem comment Äầu hà m). 1284 // Baseline (_captureBaseline) Äã chạy TRƯá»C KHI fetch nà y trả 1285 // vá» (async), nên nếu không cáºp nháºt lại data-original á» Äây, 1286 // baseline sẽ kẹt á» "[]" ban Äầu â tab Area bá» coi là "dirty" 1287 // ngay cả khi chưa sá»a gì. LUÃN ghi Äè data-original bằng kết 1288 // quả fetch má»i nhất (KHÃNG chá» khi chưa có, như bản cÅ©) â vì 1289 // má»i lần hà m nà y chạy nghÄ©a là 1 lần Modal thá»±c sá»± (re)má» và 1290 // xin dữ liá»u THẬT từ server, nên Äây luôn là baseline Äúng 1291 // nhất tại thá»i Äiá»m má», bất ká» tbody có bá» DOM-reuse giữ lại 1292 // giá trá» "data-original" cÅ© từ phiên má» trưá»c hay không. 1293 at.setAttribute('data-original', _json); 1294 syncHidden('maint-area-store-hidden', _areaData); 1295 _areaPage = 0; renderAreaTable(); 1296 _captureBaseline('area'); _refreshDirtyUI('area'); 1297 }) 1298 .catch(function(e){ 1299 if (retryLeft > 0) { 1300 setTimeout(function(){ _fetchAreaConfig(retryLeft - 1); }, 300); 1301 } else { 1302 _areaFetchInFlight = false; 1303 console.error('[maint_settings] fetch error:', e); 1304 _areaData = []; renderAreaTable(); 1305 } 1306 }); 1307 } 1308 1309 // ââ INIT ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1310 function initTables() { 1311 // (Component dropdown) Äá»c dữ liá»u tab Details TRƯá»C tab Area, vì cá»t 1312 // Component cá»§a Area giá» là dropdown lấy value từ danh sách component 1313 // cá»§a tab Details â cần _detData sẵn sà ng trưá»c khi renderAreaTable(). 1314 var dtEarly = document.getElementById('maint-details-js-tbody'); 1315 if (dtEarly) { 1316 try { _detData = JSON.parse(dtEarly.getAttribute('data-init') || '[]'); } catch(e) { _detData = []; } 1317 } 1318 var at = document.getElementById('maint-area-js-tbody'); 1319 if (at) { 1320 // (2026-07-29) GIAI ÄOẠN 1b má» rá»ng Äa nhà máy â danh sách Location Äá» 1321 // render dropdown chá»n nhà máy cho từng dòng Khu vá»±c bảo trì. 1322 try { _areaLocations = JSON.parse(at.getAttribute('data-locations') || '[]'); } catch(e) { _areaLocations = []; } 1323 _areaCurrentLocationId = at.getAttribute('data-current-location') || ''; 1324 // Má»i lần modal Area má» lại: reset vá» mặc Äá»nh "chá» hiá»n há» máy Äang 1325 // chá»n" (không giữ trạng thái bá»-lá»c cá»§a phiên trưá»c, tránh gây nhầm 1326 // lẫn "sao giá» thấy toà n bá» há» máy khác"). 1327 _areaFilterByLocation = true; 1328 var _areaFilterChk = document.getElementById('maint-area-filter-loc-chk'); 1329 if (_areaFilterChk) _areaFilterChk.checked = true; 1330 var cachedInit = at.getAttribute('data-init') || '[]'; 1331 var hasCachedData = false;
1332 try { var cached = JSON.parse(cachedInit); if (cached.length > 0) { _areaData = cached; hasCachedData = true; } } catch(e) {} 1333 if (!hasCachedData) { 1334 // (2026-07-30 FIX) 2 lá»i trong fetch cÅ©: 1335 // 1) không check r.ok â response 500 (body {"error":...}) bá» coi như 1336 // data hợp lá» (không phải mảng) -> render rá»ng NGAY, không rÆ¡i 1337 // và o .catch() nên không há» retry. 1338 // 2) không có retry â nếu request Äầu ngay sau khi Save (server có 1339 // backoff riêng, xem app.py) vẫn trượt do timing, JS bá» cuá»c 1340 // luôn -> user phải tá»± Äóng/má» modal lần 2 má»i thấy dữ liá»u. 1341 // Giá»: check r.ok, và nếu fail thì tá»± retry 1 lần sau 300ms trưá»c khi 1342 // má»i tháºt sá»± coi là rá»ng. 1343 // FIX (2026-08) <flash bug>: Bảng Area nạp async (fetch riêng, xong 1344 // sau initTables() 1 khoảng). Trưá»c Äây, dòng "_KEYS.forEach 1345 // (_captureBaseline); _refreshAllDirtyUI();" á» CUá»I initTables() vẫn 1346 // chạy NGAY khi tbody vừa ÄÆ°á»£c tạo má»i hoà n toà n (element má»i hoà n 1347 // toà n KHÃNG có sẵn attribute "data-original" từ phiên trưá»c) â 1348 // baseline tạm rÆ¡i vá» "[]", trong khi biến _areaData trong bá» nhá» JS 1349 // (persist xuyên suá»t session trang, không mất khi Äóng/má» modal) 1350 // vẫn còn Äang giữ dữ liá»u THẬT từ lần má» trưá»c â so sánh lá»ch â 1351 // hiá»n nhầm badge "chưa lưu" + nút Save/Cancel trong ~1-2s cho tá»i 1352 // khi fetch xong (_fetchAreaConfig tá»± tÃnh lại baseline ÄÃNG và ẩn 1353 // Äi). Ãp UI "sạch" ngay láºp tức trong lúc chá» fetch â không Äá» 1354 // Äoạn code cuá»i initTables() tÃnh dirty cho tab 'area' bằng dữ liá»u 1355 // tạm/chưa Äá»§ tin cáºy nà y (xem Äoạn loại trừ 'area' phÃa dưá»i). 1356 _setKeyDirtyUI('area', false); 1357 _fetchAreaConfig(1); 1358 } else { 1359 syncHidden('maint-area-store-hidden', _areaData); 1360 _areaPage = 0; renderAreaTable(); 1361 // hasCachedData=true nghÄ©a là data-init Äang giữ dữ liá»u THẬT (VD 1362 // vừa Upload, chưa Äóng modal) â tÃnh baseline/dirty ngay tại Äây 1363 // (KHÃNG dá»±a và o Äoạn "_KEYS.forEach" cuá»i initTables(), vì Äoạn Äó 1364 // giá» loại trừ 'area' â xem lý do á» nhánh fetch phÃa trên). 1365 _captureBaseline('area'); _refreshDirtyUI('area'); 1366 } 1367 } 1368 var dt = document.getElementById('maint-details-js-tbody'); 1369 if (dt) { 1370 try { _detData = JSON.parse(dt.getAttribute('data-init') || '[]'); } catch(e){ _detData=[]; } 1371 syncHidden('maint-details-store-hidden', _detData); _detPage = 0; renderDetailsTable(); 1372 } 1373 var bt = document.getElementById('maint-bt-js-tbody'); 1374 if (bt) { 1375 try { _btData = JSON.parse(bt.getAttribute('data-init') || '[]'); } catch(e){ _btData=[]; } 1376 syncHidden('maint-bt-store-hidden', _btData); _btPage = 0; renderBtTable(); 1377 } 1378 var stf = document.getElementById('maint-staff-js-tbody'); 1379 if (stf) { 1380 try { _staffData = JSON.parse(stf.getAttribute('data-init') || '[]'); } catch(e){ _staffData=[]; } 1381 syncHidden('maint-staff-store-hidden', _staffData); renderStaffTable(); 1382 } 1383 var typ = document.getElementById('maint-type-js-tbody'); 1384 if (typ) { 1385 try { _typeData = JSON.parse(typ.getAttribute('data-init') || '[]'); } catch(e){ _typeData=[]; } 1386 syncHidden('maint-type-store-hidden', _typeData); renderTypeTable(); 1387 } 1388 var sts = document.getElementById('maint-status-js-tbody'); 1389 if (sts) { 1390 try { _statusData = JSON.parse(sts.getAttribute('data-init') || '[]'); } catch(e){ _statusData=[]; } 1391 syncHidden('maint-status-store-hidden', _statusData); renderStatusTable(); 1392 } 1393 var prd = document.getElementById('maint-period-js-tbody'); 1394 if (prd) { 1395 try { _periodData = JSON.parse(prd.getAttribute('data-init') || '[]'); } catch(e){ _periodData=[]; } 1396 syncHidden('maint-period-store-hidden', _periodData); renderPeriodTable(); 1397 } 1398 bindAddButtons(); setupSaveProgress(); setupDirtyCheck(); 1399 // Nạp baseline (data-original) cho 6 tab còn lại (trừ 'area') rá»i tá»± 1400 // tÃnh lại trạng thái dirty â an toà n hÆ¡n ép vá» "sạch" như trưá»c: nếu 1401 // vì lý do nà o Äó data-init != data-original ngay lúc má» modal, badge/ 1402 // nút vẫn hiá»n Äúng thay vì bỠẩn oan. 1403 // FIX (2026-08) <flash bug tái phát>: TRƯá»C ÄÃY dòng dưá»i Äây chạy
1404 // "_KEYS.forEach(...)" cho CẢ 'area' â dù comment phÃa trên (nhánh 1405 // fetch, ~dòng 1141) khẳng Äá»nh Äã loại trừ, code thá»±c tế thì KHÃNG. 1406 // Tab 'area' không ÄÆ°á»£c Python render sẵn "data-original" (baseline 1407 // tab nà y chá» do JS set SAU KHI _fetchAreaConfig() trả vá» â xem hà m 1408 // Äó), nên vá»i 1 tbody Area Má»I hoà n toà n (Dash tạo lại khi má» modal), 1409 // _captureBaseline('area') Äá»c "data-original" = null â baseline tạm 1410 // = "[]", trong khi _areaData trong bá» nhá» JS (persist xuyên session) 1411 // vẫn giữ dữ liá»u THẬT từ lần má» trưá»c â _refreshDirtyUI('area') so 1412 // sánh lá»ch â báºt nhầm Save/Cancel/badge trong lúc chá» fetch, y há»t 1413 // bug ban Äầu. Giá» loại 'area' ra khá»i vòng lặp nà y tháºt sá»± â baseline/ 1414 // dirty cá»§a 'area' CHá» ÄÆ°á»£c tÃnh bá»i: (a) nhánh hasCachedData trong 1415 // initTables() phÃa trên, (b) _fetchAreaConfig() khi fetch xong, hoặc 1416 // (c) observer "data-original" Äá»i (sau Save) â cả 3 nÆ¡i Äó Äá»u dùng 1417 // Äúng dữ liá»u Äáng tin cáºy tại thá»i Äiá»m gá»i. 1418 _KEYS.forEach(function(k) { if (k !== 'area') _captureBaseline(k); }); 1419 _KEYS.forEach(function(k) { if (k !== 'area') _refreshDirtyUI(k); }); 1420 ['maint-area-msg','maint-details-msg','maint-bt-msg','maint-staff-msg','maint-type-msg','maint-status-msg','maint-period-msg','maint-auto-msg','maint-pmfreq-msg'].forEach(setupAutoDismiss); 1421 1422 // FIX: modal body ÄÆ°á»£c inject bá»i Dash lazy-load AFTER applyScale() Äã chạy lần Äầu. 1423 // Lúc Äó #maint-settings-modal-body chưa có â applyModalStyle() không detect ÄÆ°á»£c 1424 // â modal bá» route sai và o applyLargeModalStyle (fixed height) thay vì 1425 // applyAutoHeightXlModalStyle (max-height theo viewport). 1426 // Dispatch resize Äá» utils_js.py applyScale() chạy lại â bây giá» #maint-settings-modal-body 1427 // Äã có trong DOM nên detection Äúng và height ÄÆ°á»£c set chÃnh xác. 1428 setTimeout(function() { window.dispatchEvent(new Event('resize')); }, 60); 1429 setTimeout(function() { window.dispatchEvent(new Event('resize')); }, 300); 1430 } 1431 1432 // ââ Observers âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1433 var _initPending = false; 1434 1435 function _tryInit() { 1436 var at = document.getElementById('maint-area-js-tbody'); 1437 var dt = document.getElementById('maint-details-js-tbody'); 1438 var bt = document.getElementById('maint-bt-js-tbody'); 1439 if (at && dt && bt) { _initPending = false; initTables(); } 1440 else if (_initPending) { setTimeout(_tryInit, 60); } 1441 } 1442 1443 var _WATCHED = ['maint-area-js-tbody','maint-details-js-tbody','maint-bt-js-tbody','maint-staff-js-tbody','maint-type-js-tbody','maint-status-js-tbody','maint-period-js-tbody']; 1444 1445 // FIX (2026-08) <banner tá»± auto-dismiss không hoạt Äá»ng á» tab Tần suất PM 1446 // + tab Tá»± Äá»ng PM>: 2 tab nà y KHÃNG dùng cÆ¡ chế bảng JS (_js_tbody) như 1447 // 7 tab trên â chúng là bảng Dash render server-side thuần tuý, và các 1448 // sub-tab nà y ÄÆ°á»£c Dash MOUNT LAZY (chá» tạo DOM khi user bấm và o tab Äó 1449 // lần Äầu). Vì váºy, lúc initTables() chạy lần Äầu (ngay khi má» modal, á» 1450 // tab Area) thì #maint-pmfreq-msg / #maint-auto-msg CHƯA tá»n tại trong 1451 // DOM -> setupAutoDismiss(id) gá»i document.getElementById() trả vá» null 1452 // -> return sá»m, KHÃNG gắn observer -> banner "Äã Äá»ng bá» / Äã lưu" hiá»n 1453 // ra vÄ©nh viá» n, không bao giá» tá»± ẩn (không có bất kỳ nÆ¡i nà o gá»i lại 1454 // setupAutoDismiss cho 2 id nà y sau Äó, vì chúng không nằm trong 1455 // _WATCHED á» trên). Sá»a: theo dõi riêng 2 id nà y trong CÃNG observer 1456 // document.body bên dưá»i â ngay khi DOM cá»§a chúng ÄÆ°á»£c Dash tạo ra lần 1457 // Äầu (mount lazy khi click tab), gắn observer auto-dismiss ngay láºp tức. 1458 var _LAZY_MSG_IDS = ['maint-pmfreq-msg', 'maint-auto-msg']; 1459 1460 // ââ Observer 1: watch for Dash injecting our specific tbody IDs ââââââââââââ 1461 // CRITICAL: only match addedNodes with our exact tbody IDs.
1462 // Do NOT check if target is inside modal â that fires on every DOM change 1463 // inside the modal (including our own renderPagBar/renderAreaTable calls) 1464 // causing an infinite re-render loop. 1465 new MutationObserver(function(muts) { 1466 var foundIds = {}; 1467 muts.forEach(function(m) { 1468 m.addedNodes.forEach(function(n) { 1469 if (n.nodeType !== 1) return; 1470 // Direct match 1471 if (_WATCHED.indexOf(n.id) >= 0) { foundIds[n.id] = n; return; } 1472 // Subtree match (Dash wraps in container div) 1473 if (n.querySelector) { 1474 _WATCHED.forEach(function(id) { 1475 var found = n.querySelector('#' + id); 1476 if (found) foundIds[id] = found; 1477 }); 1478 } 1479 // (FIX <banner auto-dismiss lazy tab>) Bắt riêng lúc #maint-pmfreq-msg 1480 // / #maint-auto-msg lần Äầu xuất hiá»n trong DOM (mount lazy khi user 1481 // bấm sang tab Äó) -> gắn observer auto-dismiss ngay, không chá» 1482 // initTables() (lúc Äó Äã chạy xong từ trưá»c, không chạy lại nữa). 1483 _LAZY_MSG_IDS.forEach(function(msgId) { 1484 if (n.id === msgId) { setupAutoDismiss(msgId); return; } 1485 if (n.querySelector && n.querySelector('#' + msgId)) setupAutoDismiss(msgId); 1486 }); 1487 }); 1488 }); 1489 1490 var ids = Object.keys(foundIds); 1491 if (!ids.length) return; // nothing relevant â ignore 1492 1493 // Extra guard: if ALL mutations are inside pag divs, ignore them entirely 1494 // (renderPagBar updating button text/disabled triggers observer otherwise) 1495 var allInPag = muts.every(function(m) { 1496 var t = m.target; 1497 while (t) { 1498 if (_PAG_IDS.indexOf(t.id) >= 0) return true; 1499 t = t.parentElement; 1500 } 1501 return false; 1502 }); 1503 if (allInPag) return; 1504 1505 // Check if primary tbodies are all present â full init 1506 var at = foundIds['maint-area-js-tbody'] || document.getElementById('maint-area-js-tbody'); 1507 var dt = foundIds['maint-details-js-tbody'] || document.getElementById('maint-details-js-tbody'); 1508 var bt = foundIds['maint-bt-js-tbody'] || document.getElementById('maint-bt-js-tbody'); 1509 1510 if (at && dt && bt && !_initPending) { 1511 _initPending = true; 1512 setTimeout(function() { _initPending = false; initTables(); }, 80); 1513 } else { 1514 // Secondary tabs: only init the ones just added 1515 if (foundIds['maint-staff-js-tbody']) { 1516 var el = foundIds['maint-staff-js-tbody']; 1517 try{_staffData=JSON.parse(el.getAttribute('data-init')||'[]');}catch(e){_staffData=[];} 1518 syncHidden('maint-staff-store-hidden',_staffData); renderStaffTable(); 1519 } 1520 if (foundIds['maint-type-js-tbody']) { 1521 var el2 = foundIds['maint-type-js-tbody']; 1522 try{_typeData=JSON.parse(el2.getAttribute('data-init')||'[]');}catch(e){_typeData=[];} 1523 syncHidden('maint-type-store-hidden',_typeData); renderTypeTable(); 1524 } 1525 if (foundIds['maint-status-js-tbody']) { 1526 var el3 = foundIds['maint-status-js-tbody']; 1527 try{_statusData=JSON.parse(el3.getAttribute('data-init')||'[]');}catch(e){_statusData=[];} 1528 syncHidden('maint-status-store-hidden',_statusData); renderStatusTable(); 1529 } 1530 if (foundIds['maint-period-js-tbody']) { 1531 var el4 = foundIds['maint-period-js-tbody']; 1532 try{_periodData=JSON.parse(el4.getAttribute('data-init')||'[]');}catch(e){_periodData=[];} 1533 syncHidden('maint-period-store-hidden',_periodData); renderPeriodTable(); 1534 } 1535 bindAddButtons(); setupDirtyCheck(); 1536 // Nạp baseline cho các tab vừa ÄÆ°á»£c Dash inject và o DOM (tab phụ má» 1537 // lazy khi click qua) rá»i tÃnh lại dirty UI cho Äúng tab Äó. 1538 ['staff', 'type', 'status', 'period'].forEach(function (k) { 1539 if (foundIds[_TBODY_ID[k]]) { _captureBaseline(k); _refreshDirtyUI(k); } 1540 }); 1541 } 1542 }).observe(document.body, { childList: true, subtree: true }); 1543 1544 // FIX (2026-08) <3>: theo dõi CẢ "data-init" LẪN "data-original". 1545 // - "data-init" Äá»i (VD sau Upload file, hoặc sau Save Python trả vá» dữ 1546 // liá»u vừa lưu) â nạp lại dữ liá»u và o bảng + render lại. 1547 // - "data-original" Äá»i (CHá» xảy ra khi Save thà nh công â Python cỠý 1548 // KHÃNG Äá»i data-original khi Upload, xem cb_maintenance.py) â nạp lại
1549 // baseline cho tab Äó rá»i tÃnh lại dirty UI. 1550 // Nhá» tách 2 attribute nà y, Upload (chá» Äá»i data-init) sẽ tá»± Äá»ng là m dữ 1551 // liá»u lá»ch baseline â Save/Cancel + badge "chưa lưu" tá»± xuất hiá»n, không 1552 // cần gá»i markDirty() thá»§ công á» Äâu cả. 1553 // FIX (2026-08) <2><3>: bug "phải Äóng/má» modal Maintenance Settings 2 lần 1554 // má»i thấy config Area load Äúng" (kèm badge 'Có thay Äá»i chưa lưu' không 1555 // biến mất). Root cause tháºt sá»± (khác vá»i các fix Cancel/close phÃa trên 1556 // â những fix Äó xá» lý 1 nguá»n gây lá»ch data, còn Äây là lá»i XẢY RA Má»I 1557 // LẦN Má» LẠI MODAL, ká» cả khi KHÃNG há» Upload/Cancel gì trưá»c Äó): 1558 // - Tab Area luôn ÄÆ°á»£c Python render vá»i data-init="[]" (cỠý â dữ liá»u 1559 // tháºt lấy qua fetch('/api/maint_area_config') riêng, xem comment á» 1560 // _fetchAreaConfig()), coi "[]" chá» là placeholder chá» fetch. 1561 // - Khi bấm nút má» lại Modal, Dash re-render <Tbody id="maint-area-js- 1562 // tbody">. NẾU React/Dash chá» PATCH lại node cÅ© (thay vì tạo má»i hẳn) 1563 // â hoà n toà n có thá» xảy ra vì cùng id + cùng vá» trà trong cây â thì 1564 // observer "childList" (theo dõi node Má»I ÄÆ°á»£c thêm và o DOM) sẽ KHÃNG 1565 // bắn, vì tbody không há» bá» remove/add lại. Chá» có observer "attributes" 1566 // bên dưá»i bắn (vì data-init Äá»i giá trá») â và code CŨ á» Äây hiá»u lầm 1567 // "[]" là dữ liá»u THẬT (rá»ng), tá»± render "Chưa có cấu hình" + tÃnh dirty 1568 // sai (baseline cÅ© trong _original['area'] không khá»p []) â hiá»n badge. 1569 // - Do Äó user phải Äóng/má» LẦN 2: lần nà y DOM có thá» bá» tạo má»i tháºt sá»± 1570 // (hoặc do timing khác) â childList observer bắt ÄÆ°á»£c â initTables() 1571 // chạy Äúng luá»ng fetch. 1572 // Sá»a triá»t Äá»: Python thêm 1 attribute "data-open-nonce" LUÃN Äá»i giá trá» 1573 // má»i lần modal má» (xem _build_area_tab trong ui_maintenance.py) â bất ká» 1574 // node bá» patch hay tạo má»i, thuá»c tÃnh nà y LUÃN ÄÆ°á»£c ghi lại nên LUÃN kÃch 1575 // hoạt observer. Khi nonce Äá»i, coi Äây là tÃn hiá»u "Modal Area tab vừa má» 1576 // lại" và LUÃN gá»i _fetchAreaConfig() Äá» lấy dữ liá»u THẬT â bá» qua hoà n 1577 // toà n giá trá» "[]" tạm thá»i mà Python vừa ghi và o data-init trong cùng 1578 // batch mutation nà y (không tá»± render rá»ng theo nó). 1579 new MutationObserver(function(muts) { 1580 var changedByKey = {}; // key -> { init:bool, original:bool, nonce:bool } 1581 muts.forEach(function(m) { 1582 var el = m.target; 1583 var key = _KEY_BY_TBODY[el.id]; 1584 if (!key) return; 1585 var info = changedByKey[key] || (changedByKey[key] = {}); 1586 if (m.attributeName === 'data-init') info.init = true; 1587 else if (m.attributeName === 'data-original') info.original = true; 1588 else if (m.attributeName === 'data-open-nonce') info.nonce = true; 1589 }); 1590 Object.keys(changedByKey).forEach(function(key) { 1591 var info = changedByKey[key]; 1592 if (key === 'area' && info.nonce) { 1593 // Modal Area tab vừa (re)má» â luôn fetch lại dữ liá»u tháºt, KHÃNG 1594 // render trá»±c tiếp theo data-init="[]" tạm thá»i (dù nó có Äá»i 1595 // trong cùng batch mutation nà y hay không). 1596 _fetchAreaConfig(1); 1597 return; 1598 } 1599 if (info.init) { 1600 var tb = document.getElementById(_TBODY_ID[key]); 1601 var map = { 1602 area: function(d){ _areaData=d; _areaPage=0; renderAreaTable(); }, 1603 details: function(d){ _detData=d; _detPage=0; renderDetailsTable(); }, 1604 bt: function(d){ _btData=d; _btPage=0; renderBtTable(); }, 1605 staff: function(d){ _staffData=d; renderStaffTable(); }, 1606 type: function(d){ _typeData=d; renderTypeTable(); }, 1607 status: function(d){ _statusData=d; renderStatusTable(); }, 1608 period: function(d){ _periodData=d; renderPeriodTable(); }, 1609 }; 1610 var fn = map[key]; 1611 if (fn && tb) { 1612 try { fn(JSON.parse(tb.getAttribute('data-init')||'[]')); } catch(e) { fn([]); }
1613 _refreshDirtyUI(key); 1614 } 1615 } 1616 if (info.original) { 1617 _captureBaseline(key); 1618 _refreshDirtyUI(key); 1619 } 1620 }); 1621 }).observe(document.body, { subtree: true, attributes: true, attributeFilter: ['data-init', 'data-original', 'data-open-nonce'] }); 1622 1623 new MutationObserver(function() { styleHeaders(); }) 1624 .observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] }); 1625 1626 // ââ STAFF TAB âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1627 var _staffData = []; 1628 1629 function renderStaffTable() { 1630 // Staff tab giá» luôn READ-ONLY (live từ Shift Schedule) â không còn 1631 // input editable, không còn nút xoá dòng, không còn Thêm/Lưu/Há»§y. 1632 var tb = document.getElementById('maint-staff-js-tbody'); 1633 if (!tb) return; 1634 var html = ''; 1635 if (!_staffData.length) { 1636 html = '<tr><td colspan="3" class="maint-settings-empty-cell">' + _t('maint_js_no_shift_employees') + '</td></tr>'; 1637 } else { 1638 _staffData.forEach(function(row) { 1639 html += '<tr class="maint-settings-row">'; 1640 html += '<td class="maint-settings-cell">'+esc(row.name||'')+'</td>'; 1641 html += '<td class="maint-settings-cell">'+esc(row.email||'')+'</td>'; 1642 html += '<td class="maint-settings-cell">'+esc(row.role||'')+'</td></tr>'; 1643 }); 1644 } 1645 tb.innerHTML = html; 1646 } 1647 1648 // ââ TYPE TAB ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1649 var _typeData = []; 1650 1651 function renderTypeTable() { 1652 var tb = document.getElementById('maint-type-js-tbody'); 1653 if (!tb) return; 1654 var ro = tb.getAttribute('data-readonly') === 'true'; 1655 var html = ''; 1656 if (!_typeData.length) { 1657 html = '<tr><td colspan="2" class="maint-settings-empty-cell">' + esc(_t('maint_js_type_empty', 'Chưa có loại. Nhấn "Thêm loại" Äá» bắt Äầu.')) + '</td></tr>'; 1658 } else { 1659 _typeData.forEach(function(row, i){ 1660 html += '<tr class="maint-settings-row" data-idx="'+i+'">'; 1661 html += '<td class="maint-settings-cell"><input class="maint-settings-input" data-field="type_name" value="'+esc(row.type_name||'')+'" '+(ro?'disabled':'')+'/></td>'; 1662 html += '<td class="maint-settings-cell text-center">'+((!ro)?'<button class="btn btn-sm btn-outline-danger py-0 px-1 type-del-btn" data-idx="'+i+'">×</button>':'')+'</td></tr>'; 1663 }); 1664 } 1665 tb.innerHTML = html; 1666 tb.querySelectorAll('input').forEach(function(inp){ 1667 inp.addEventListener('input', function(){ 1668 var idx = parseInt(inp.closest('tr').dataset.idx);
1669 _typeData[idx][inp.dataset.field] = inp.value; 1670 syncHidden('maint-type-store-hidden', _typeData); 1671 markDirty('type'); 1672 }); 1673 }); 1674 tb.querySelectorAll('.type-del-btn').forEach(function(btn){ 1675 btn.addEventListener('click', function(){ 1676 _typeData.splice(parseInt(btn.dataset.idx), 1); 1677 syncHidden('maint-type-store-hidden', _typeData); 1678 markDirty('type'); renderTypeTable(); 1679 }); 1680 }); 1681 } 1682 1683 document.addEventListener('click', function(e) { 1684 var t = e.target; 1685 if (t.closest && t.closest('#maint-type-add-btn')) { 1686 _typeData.push({id:'new_'+Date.now(), type_name:''}); 1687 syncHidden('maint-type-store-hidden', _typeData); 1688 markDirty('type'); renderTypeTable(); 1689 setTimeout(function(){ 1690 var tb = document.getElementById('maint-type-js-tbody'); 1691 if (!tb) return; 1692 var last = tb.querySelectorAll('tr.maint-settings-row'); 1693 last = last[last.length-1]; 1694 if (last) { var inp = last.querySelector('input'); if (inp) inp.focus(); } 1695 }, 30); 1696 } 1697 if (t.closest && t.closest('#maint-type-cancel-btn')) { 1698 var tb = document.getElementById('maint-type-js-tbody'); 1699 if (tb) { 1700 var _raw = tb.getAttribute('data-original'); if (_raw === null) _raw = tb.getAttribute('data-init') || '[]'; try { _typeData = JSON.parse(_raw); } catch(e2) { _typeData = []; } 1701 syncHidden('maint-type-store-hidden', _typeData); 1702 renderTypeTable(); 1703 } 1704 clearDirty('type'); 1705 } 1706 }); 1707 1708 // ââ STATUS TAB ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1709 var _statusData = []; 1710
1711 function renderStatusTable() { 1712 var tb = document.getElementById('maint-status-js-tbody'); 1713 if (!tb) return; 1714 var ro = tb.getAttribute('data-readonly') === 'true'; 1715 var html = ''; 1716 if (!_statusData.length) { 1717 html = '<tr><td colspan="2" class="maint-settings-empty-cell">' + esc(_t('maint_js_status_empty', 'Chưa có trạng thái. Nhấn "Thêm trạng thái" Äá» bắt Äầu.')) + '</td></tr>'; 1718 } else { 1719 _statusData.forEach(function(row, i){ 1720 html += '<tr class="maint-settings-row" data-idx="'+i+'">'; 1721 html += '<td class="maint-settings-cell"><input class="maint-settings-input" data-field="status_name" value="'+esc(row.status_name||'')+'" '+(ro?'disabled':'')+'/></td>'; 1722 html += '<td class="maint-settings-cell text-center">'+((!ro)?'<button class="btn btn-sm btn-outline-danger py-0 px-1 status-del-btn" data-idx="'+i+'">×</button>':'')+'</td></tr>'; 1723 }); 1724 } 1725 tb.innerHTML = html; 1726 tb.querySelectorAll('input').forEach(function(inp){ 1727 inp.addEventListener('input', function(){ 1728 var idx = parseInt(inp.closest('tr').dataset.idx);
1729 _statusData[idx][inp.dataset.field] = inp.value; 1730 syncHidden('maint-status-store-hidden', _statusData); 1731 markDirty('status'); 1732 }); 1733 }); 1734 tb.querySelectorAll('.status-del-btn').forEach(function(btn){ 1735 btn.addEventListener('click', function(){ 1736 _statusData.splice(parseInt(btn.dataset.idx), 1); 1737 syncHidden('maint-status-store-hidden', _statusData); 1738 markDirty('status'); renderStatusTable(); 1739 }); 1740 }); 1741 } 1742 1743 document.addEventListener('click', function(e) { 1744 var t = e.target; 1745 if (t.closest && t.closest('#maint-status-add-btn')) { 1746 _statusData.push({id:'new_'+Date.now(), status_name:''}); 1747 syncHidden('maint-status-store-hidden', _statusData); 1748 markDirty('status'); renderStatusTable(); 1749 setTimeout(function(){ 1750 var tb = document.getElementById('maint-status-js-tbody'); 1751 if (!tb) return; 1752 var last = tb.querySelectorAll('tr.maint-settings-row'); 1753 last = last[last.length-1]; 1754 if (last) { var inp = last.querySelector('input'); if (inp) inp.focus(); } 1755 }, 30); 1756 } 1757 if (t.closest && t.closest('#maint-status-cancel-btn')) { 1758 var tb = document.getElementById('maint-status-js-tbody'); 1759 if (tb) { 1760 var _raw = tb.getAttribute('data-original'); if (_raw === null) _raw = tb.getAttribute('data-init') || '[]'; try { _statusData = JSON.parse(_raw); } catch(e2) { _statusData = []; } 1761 syncHidden('maint-status-store-hidden', _statusData); 1762 renderStatusTable(); 1763 } 1764 clearDirty('status'); 1765 } 1766 }); 1767 1768 // ââ PERIOD TYPE TAB âââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1769 var _periodData = []; 1770 1771 function renderPeriodTable() { 1772 var tb = document.getElementById('maint-period-js-tbody'); 1773 if (!tb) return; 1774 var ro = tb.getAttribute('data-readonly') === 'true'; 1775 var html = ''; 1776 if (!_periodData.length) { 1777 html = '<tr><td colspan="3" class="maint-settings-empty-cell">' + esc(_t('maint_js_period_empty', 'Chưa có loại kỳ. Nhấn "Thêm loại kỳ" Äá» bắt Äầu.')) + '</td></tr>'; 1778 } else { 1779 _periodData.forEach(function(row, i){ 1780 html += '<tr class="maint-settings-row" data-idx="'+i+'">'; 1781 html += '<td class="maint-settings-cell"><input class="maint-settings-input" data-field="period_name" value="'+esc(row.period_name||'')+'" '+(ro?'disabled':'')+'/></td>'; 1782 html += '<td class="maint-settings-cell"><input class="maint-settings-input" data-field="period_value" value="'+esc(row.period_value||'')+'" '+(ro?'disabled':'')+' placeholder="vd: monthly"/></td>'; 1783 html += '<td class="maint-settings-cell text-center">'+((!ro)?'<button class="btn btn-sm btn-outline-danger py-0 px-1 period-del-btn" data-idx="'+i+'">×</button>':'')+'</td></tr>'; 1784 }); 1785 } 1786 tb.innerHTML = html; 1787 tb.querySelectorAll('input').forEach(function(inp){ 1788 inp.addEventListener('input', function(){ 1789 var idx = parseInt(inp.closest('tr').dataset.idx);
1790 _periodData[idx][inp.dataset.field] = inp.value; 1791 syncHidden('maint-period-store-hidden', _periodData); 1792 markDirty('period'); 1793 }); 1794 }); 1795 tb.querySelectorAll('.period-del-btn').forEach(function(btn){ 1796 btn.addEventListener('click', function(){ 1797 _periodData.splice(parseInt(btn.dataset.idx), 1); 1798 syncHidden('maint-period-store-hidden', _periodData); 1799 markDirty('period'); renderPeriodTable(); 1800 }); 1801 }); 1802 } 1803 1804 document.addEventListener('click', function(e) { 1805 var t = e.target; 1806 if (t.closest && t.closest('#maint-period-add-btn')) { 1807 _periodData.push({id:'new_'+Date.now(), period_name:'', period_value:''}); 1808 syncHidden('maint-period-store-hidden', _periodData); 1809 markDirty('period'); renderPeriodTable(); 1810 setTimeout(function(){ 1811 var tb = document.getElementById('maint-period-js-tbody'); 1812 if (!tb) return; 1813 var last = tb.querySelectorAll('tr.maint-settings-row'); 1814 last = last[last.length-1]; 1815 if (last) { var inp = last.querySelector('input'); if (inp) inp.focus(); } 1816 }, 30); 1817 } 1818 if (t.closest && t.closest('#maint-period-cancel-btn')) { 1819 var tb = document.getElementById('maint-period-js-tbody'); 1820 if (tb) { 1821 var _raw = tb.getAttribute('data-original'); if (_raw === null) _raw = tb.getAttribute('data-init') || '[]'; try { _periodData = JSON.parse(_raw); } catch(e2) { _periodData = []; } 1822 syncHidden('maint-period-store-hidden', _periodData); 1823 renderPeriodTable(); 1824 } 1825 clearDirty('period'); 1826 } 1827 }); 1828 1829})();
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.