// DH-HIS Quản Lý Giao Ban Web Client const API_BASE = window.location.pathname.startsWith('/web-giao-ban-go') ? '/web-giao-ban-go' : (window.location.pathname.startsWith('/web-giao-ban') ? '/web-giao-ban' : ''); let chartNgoaiTruInstance = null; let chartNoiTruInstance = null; let currentChartTypeNgoaiTru = 'khoa'; let currentChartTypeNoiTru = 'noitru-col'; let isBienBanLocked = false; let rawNgoaiTruData = null; let rawNoiTruData = null; let rawBang3Data = []; let rawBang4Data = []; let rawIssuesData = []; let currentIssueFile = null; let editingIssueId = null; document.addEventListener('DOMContentLoaded', async () => { initPresets(); initTabs(); initEvents(); initIssueFeatures(); initBienBan(); await checkLicenseStatus(); await loadDatabases(); loadData(); loadIssues(false); }); // Format Number with Vietnamese Thousand Separator function fmt(num) { if (num === null || num === undefined || isNaN(num)) return '0'; return Number(num).toLocaleString('vi-VN'); } // Show Toast function showToast(msg) { const toast = document.getElementById('toast'); const toastMsg = document.getElementById('toastMsg'); if (!toast || !toastMsg) return; toastMsg.innerText = msg; toast.classList.remove('translate-y-20', 'opacity-0'); toast.classList.add('translate-y-0', 'opacity-100'); setTimeout(() => { toast.classList.remove('translate-y-0', 'opacity-100'); toast.classList.add('translate-y-20', 'opacity-0'); }, 2500); } // Load dynamic databases list async function loadDatabases() { const selectDb = document.getElementById('selectDb'); if (!selectDb) return; try { const res = await fetch(`${API_BASE}/api/databases`); const json = await res.json(); if (json.success && Array.isArray(json.data) && json.data.length > 0) { const savedDb = localStorage.getItem('dh_giaoban_db') || selectDb.value || 'an_omon_full_04052026'; selectDb.innerHTML = ''; json.data.forEach(db => { const opt = document.createElement('option'); opt.value = db.id; opt.textContent = db.name; if (db.id === savedDb) opt.selected = true; selectDb.appendChild(opt); }); updateDbBadge(); } } catch (err) { console.warn('Không tải được danh sách CSDL từ server:', err); } } // Update Database Badge on Header function updateDbBadge() { const selectDb = document.getElementById('selectDb'); const badge = document.getElementById('currentDbBadge'); if (badge && selectDb && selectDb.selectedOptions[0]) { const text = selectDb.selectedOptions[0].textContent.split('(')[0].trim(); badge.innerHTML = ` ${text}`; } } // Preset Handler function initPresets() { const quickPreset = document.getElementById('quickPreset'); const dateFrom = document.getElementById('dateFrom'); const dateTo = document.getElementById('dateTo'); const lockStatusFrom = document.getElementById('lockStatusFrom'); const lockStatusTo = document.getElementById('lockStatusTo'); const quickComparePreset = document.getElementById('quickComparePreset'); const compareFrom = document.getElementById('compareFrom'); const compareTo = document.getElementById('compareTo'); const chkHideZeroRows = document.getElementById('chkHideZeroRows'); function updateLockState() { const isCustom = quickPreset.value === 'custom'; dateFrom.readOnly = !isCustom; dateTo.readOnly = !isCustom; if (isCustom) { dateFrom.classList.remove('bg-slate-100', 'cursor-not-allowed', 'text-slate-500'); dateFrom.classList.add('bg-white', 'text-slate-800'); dateTo.classList.remove('bg-slate-100', 'cursor-not-allowed', 'text-slate-500'); dateTo.classList.add('bg-white', 'text-slate-800'); if (lockStatusFrom) lockStatusFrom.innerHTML = ' Mở khóa'; if (lockStatusTo) lockStatusTo.innerHTML = ' Mở khóa'; } else { dateFrom.classList.add('bg-slate-100', 'cursor-not-allowed', 'text-slate-500'); dateFrom.classList.remove('bg-white', 'text-slate-800'); dateTo.classList.add('bg-slate-100', 'cursor-not-allowed', 'text-slate-500'); dateTo.classList.remove('bg-white', 'text-slate-800'); if (lockStatusFrom) lockStatusFrom.innerHTML = ' Cố định'; if (lockStatusTo) lockStatusTo.innerHTML = ' Cố định'; } } quickPreset.addEventListener('change', () => { const val = quickPreset.value; updateLockState(); let f = '', t = ''; switch (val) { case 'month-03-2026': f = '2026-03-01'; t = '2026-03-31'; break; case 'month-04-2026': f = '2026-04-01'; t = '2026-04-30'; break; case 'month-01-2026': f = '2026-01-01'; t = '2026-01-31'; break; case 'month-02-2026': f = '2026-02-01'; t = '2026-02-28'; break; case 'today': f = '2026-03-31'; t = '2026-03-31'; break; case 'yesterday': f = '2026-03-30'; t = '2026-03-30'; break; case 'this-month': f = '2026-03-01'; t = '2026-03-31'; break; case 'quarter-1': f = '2026-01-01'; t = '2026-03-31'; break; case 'year-2026': f = '2026-01-01'; t = '2026-12-31'; break; case 'custom': return; default: return; } dateFrom.value = f; dateTo.value = t; // Auto-update compare preset if relative if (quickComparePreset && quickComparePreset.value !== 'custom') { updateComparePresetDates(); } loadData(); }); function updateComparePresetDates() { if (!quickComparePreset || !compareFrom || !compareTo) return; const val = quickComparePreset.value; const isCustom = val === 'custom'; compareFrom.readOnly = !isCustom; compareTo.readOnly = !isCustom; if (isCustom) { compareFrom.classList.remove('bg-slate-100', 'cursor-not-allowed', 'text-slate-500'); compareFrom.classList.add('bg-white', 'text-slate-800'); compareTo.classList.remove('bg-slate-100', 'cursor-not-allowed', 'text-slate-500'); compareTo.classList.add('bg-white', 'text-slate-800'); return; } compareFrom.classList.add('bg-slate-100', 'cursor-not-allowed', 'text-slate-500'); compareFrom.classList.remove('bg-white', 'text-slate-800'); compareTo.classList.add('bg-slate-100', 'cursor-not-allowed', 'text-slate-500'); compareTo.classList.remove('bg-white', 'text-slate-800'); if (val === 'month-01-2026') { compareFrom.value = '2026-01-01'; compareTo.value = '2026-01-31'; } else if (val === 'month-02-2026') { compareFrom.value = '2026-02-01'; compareTo.value = '2026-02-28'; } else if (val === 'prev-month') { try { const d1 = new Date(dateFrom.value); d1.setMonth(d1.getMonth() - 1); const d2 = new Date(dateTo.value); d2.setMonth(d2.getMonth() - 1); compareFrom.value = d1.toISOString().slice(0, 10); compareTo.value = d2.toISOString().slice(0, 10); } catch (e) {} } else if (val === 'prev-year') { try { const d1 = new Date(dateFrom.value); d1.setFullYear(d1.getFullYear() - 1); const d2 = new Date(dateTo.value); d2.setFullYear(d2.getFullYear() - 1); compareFrom.value = d1.toISOString().slice(0, 10); compareTo.value = d2.toISOString().slice(0, 10); } catch (e) {} } else if (val === 'prev-period') { try { const d1 = new Date(dateFrom.value); const d2 = new Date(dateTo.value); const diff = d2.getTime() - d1.getTime(); const prevEnd = new Date(d1.getTime() - 24 * 3600 * 1000); const prevStart = new Date(prevEnd.getTime() - diff); compareFrom.value = prevStart.toISOString().slice(0, 10); compareTo.value = prevEnd.toISOString().slice(0, 10); } catch (e) {} } } if (quickComparePreset) { quickComparePreset.addEventListener('change', () => { updateComparePresetDates(); loadData(); }); } if (chkHideZeroRows) { chkHideZeroRows.addEventListener('change', () => { renderNgoaiTru(); renderNoiTru(); }); } updateLockState(); updateComparePresetDates(); } // Tab Switching function initTabs() { const tabBtns = document.querySelectorAll('.tab-btn'); const tabContents = document.querySelectorAll('.tab-content'); tabBtns.forEach(btn => { btn.addEventListener('click', () => { tabBtns.forEach(b => { b.classList.remove('active', 'border-blue-600', 'text-blue-600'); b.classList.add('border-transparent', 'text-slate-500'); }); btn.classList.add('active', 'border-blue-600', 'text-blue-600'); btn.classList.remove('border-transparent', 'text-slate-500'); const target = btn.getAttribute('data-tab'); tabContents.forEach(c => { c.classList.toggle('hidden', c.id !== target); }); // Resize charts or load issues if (target === 'tab-ngoaitru' && chartNgoaiTruInstance) { chartNgoaiTruInstance.resize(); } else if (target === 'tab-noitru' && chartNoiTruInstance) { chartNoiTruInstance.resize(); } else if (target === 'tab-issues') { loadIssues(true); } }); }); } // Events function initEvents() { const enableCompare = document.getElementById('enableCompare'); const compareContainer = document.getElementById('compareContainer'); const btnFilter = document.getElementById('btnFilter'); const selectDb = document.getElementById('selectDb'); const btnPrint = document.getElementById('btnPrint'); const btnExport = document.getElementById('btnExport'); enableCompare.addEventListener('change', () => { compareContainer.classList.toggle('hidden', !enableCompare.checked); document.querySelectorAll('.th-compare').forEach(el => { el.classList.toggle('hidden', !enableCompare.checked); }); loadData(); }); btnFilter.addEventListener('click', () => loadData()); selectDb.addEventListener('change', () => { updateDbBadge(); loadData(); }); btnPrint.addEventListener('click', () => window.print()); btnExport.addEventListener('click', () => exportExcel()); // Chart Type toggle for Outpatient document.querySelectorAll('.chart-type-btn').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.chart-type-btn').forEach(b => { b.classList.remove('active', 'bg-blue-100', 'text-blue-700', 'font-semibold'); b.classList.add('bg-slate-100', 'text-slate-600', 'font-medium'); }); btn.classList.add('active', 'bg-blue-100', 'text-blue-700', 'font-semibold'); btn.classList.remove('bg-slate-100', 'text-slate-600', 'font-medium'); currentChartTypeNgoaiTru = btn.getAttribute('data-chart'); renderChartNgoaiTru(); }); }); // Chart Type toggle for Inpatient document.querySelectorAll('.chart-nt-btn').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.chart-nt-btn').forEach(b => { b.classList.remove('active', 'bg-blue-100', 'text-blue-700', 'font-semibold'); b.classList.add('bg-slate-100', 'text-slate-600', 'font-medium'); }); btn.classList.add('active', 'bg-blue-100', 'text-blue-700', 'font-semibold'); btn.classList.remove('bg-slate-100', 'text-slate-600', 'font-medium'); currentChartTypeNoiTru = btn.getAttribute('data-ntchart'); renderChartNoiTru(); }); }); // License Modal close button const btnCloseLicense = document.getElementById('btnCloseLicenseModal'); if (btnCloseLicense) { btnCloseLicense.addEventListener('click', () => { document.getElementById('modalLicenseBlock')?.classList.add('hidden'); }); } // Filters for Table 3 & 4 document.getElementById('filterKhoaCv').addEventListener('change', (e) => { renderBang3(e.target.value); }); document.getElementById('filterKhoaTd').addEventListener('change', (e) => { renderBang4(e.target.value); }); } // Get Request Params function getParams() { const database = document.getElementById('selectDb').value; const from = document.getElementById('dateFrom').value + ' 00:00:00'; const to = document.getElementById('dateTo').value + ' 23:59:59'; const isCompare = document.getElementById('enableCompare').checked; let urlParams = `database=${database}&from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`; if (isCompare) { const compareFrom = document.getElementById('compareFrom').value + ' 00:00:00'; const compareTo = document.getElementById('compareTo').value + ' 23:59:59'; urlParams += `&compareFrom=${encodeURIComponent(compareFrom)}&compareTo=${encodeURIComponent(compareTo)}`; } return { database, from, to, isCompare, urlParams }; } // Set Loading State function setLoading(isLoading) { const btnFilter = document.getElementById('btnFilter'); const selectDb = document.getElementById('selectDb'); const quickPreset = document.getElementById('quickPreset'); if (isLoading) { if (btnFilter) { btnFilter.disabled = true; btnFilter.innerHTML = ' Đang truy vấn...'; btnFilter.classList.add('opacity-75', 'cursor-not-allowed'); } if (selectDb) selectDb.disabled = true; if (quickPreset) quickPreset.disabled = true; // Show skeletons/loading on tables const bodyKhoa = document.getElementById('bodyKhoaKcb'); if (bodyKhoa) bodyKhoa.innerHTML = ' Đang tải số liệu khám chữa bệnh...'; const bodyCls = document.getElementById('bodyCls'); if (bodyCls) bodyCls.innerHTML = ' Đang tải Cận lâm sàng...'; const bodyTop10 = document.getElementById('bodyTop10'); if (bodyTop10) bodyTop10.innerHTML = ' Đang tải Top 10 bệnh...'; const bodyNoiTru = document.getElementById('bodyNoiTru'); if (bodyNoiTru) bodyNoiTru.innerHTML = ' Đang tải số liệu nội trú...'; const bodyBang3 = document.getElementById('bodyBang3'); if (bodyBang3) bodyBang3.innerHTML = ' Đang tải danh sách chuyển viện...'; const bodyBang4 = document.getElementById('bodyBang4'); if (bodyBang4) bodyBang4.innerHTML = ' Đang tải danh sách theo dõi & CS Cấp I...'; } else { if (btnFilter) { btnFilter.disabled = false; btnFilter.innerHTML = ' Xem Báo Cáo'; btnFilter.classList.remove('opacity-75', 'cursor-not-allowed'); } if (selectDb) selectDb.disabled = false; if (quickPreset) quickPreset.disabled = false; } } // Master Load Data async function loadData() { const { urlParams, database, from, to } = getParams(); localStorage.setItem('dh_giaoban_db', database); updateDbBadge(); setLoading(true); try { const [resNt, resNoiTru, resB3, resB4] = await Promise.allSettled([ fetch(`${API_BASE}/api/giao-ban-ngoai-tru?${urlParams}`).then(r => r.json()), fetch(`${API_BASE}/api/giao-ban-noi-tru?${urlParams}`).then(r => r.json()), fetch(`${API_BASE}/api/danh-sach-chuyen-vien?database=${database}&from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`).then(r => r.json()), fetch(`${API_BASE}/api/danh-sach-theo-doi?database=${database}&from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`).then(r => r.json()), ]); let hasSuccess = false; let errors = []; // 1. Ngoại trú if (resNt.status === 'fulfilled' && resNt.value && resNt.value.success) { rawNgoaiTruData = resNt.value.data; renderNgoaiTru(); hasSuccess = true; } else { const err = resNt.status === 'rejected' ? resNt.reason?.message : resNt.value?.message; errors.push(`Ngoại trú: ${err || 'Lỗi kết nối'}`); const body = document.getElementById('bodyKhoaKcb'); if (body) body.innerHTML = ` Không tải được số liệu ngoại trú: ${err || 'Lỗi'}`; } // 2. Nội trú if (resNoiTru.status === 'fulfilled' && resNoiTru.value && resNoiTru.value.success) { rawNoiTruData = resNoiTru.value.data; renderNoiTru(); hasSuccess = true; } else { const err = resNoiTru.status === 'rejected' ? resNoiTru.reason?.message : resNoiTru.value?.message; errors.push(`Nội trú: ${err || 'Lỗi kết nối'}`); const body = document.getElementById('bodyNoiTru'); if (body) body.innerHTML = ` Không tải được số liệu nội trú: ${err || 'Lỗi'}`; } // 3. Bảng 3: Chuyển viện if (resB3.status === 'fulfilled' && resB3.value && resB3.value.success) { rawBang3Data = resB3.value.data; populateKhoaDropdown('filterKhoaCv', rawBang3Data); renderBang3('ALL'); hasSuccess = true; } else { const err = resB3.status === 'rejected' ? resB3.reason?.message : resB3.value?.message; errors.push(`Chuyển viện: ${err || 'Lỗi kết nối'}`); const body = document.getElementById('bodyBang3'); if (body) body.innerHTML = ` Không tải được danh sách chuyển viện`; } // 4. Bảng 4: Theo dõi if (resB4.status === 'fulfilled' && resB4.value && resB4.value.success) { rawBang4Data = resB4.value.data; populateKhoaDropdown('filterKhoaTd', rawBang4Data); renderBang4('ALL'); hasSuccess = true; } else { const err = resB4.status === 'rejected' ? resB4.reason?.message : resB4.value?.message; errors.push(`Theo dõi: ${err || 'Lỗi kết nối'}`); const body = document.getElementById('bodyBang4'); if (body) body.innerHTML = ` Không tải được danh sách theo dõi`; } // Kiểm tra chặn bản quyền từ backend const licBlocked = [resNt, resNoiTru, resB3, resB4].find(r => r.status === 'fulfilled' && r.value && r.value.license_valid === false); if (licBlocked) { showLicenseBlockModal(licBlocked.value.message); } if (hasSuccess) { const selName = document.getElementById('selectDb').selectedOptions[0]?.textContent.split('(')[0].trim() || database; showToast(`Đã nạp báo cáo CSDL: ${selName}`); } if (errors.length > 0) { console.warn('Errors loading some report sections:', errors); showToast(`Cảnh báo: ${errors.join('; ')}`); } } catch (err) { console.error('Fatal load error:', err); showToast('Lỗi nghiêm trọng khi tải dữ liệu: ' + err.message); } finally { setLoading(false); } } // Hiển thị Modal thông báo yêu cầu kích hoạt bản quyền function showLicenseBlockModal(msg) { const modal = document.getElementById('modalLicenseBlock'); const msgEl = document.getElementById('modalLicenseBlockMsg'); if (msgEl && msg) { msgEl.innerHTML = ` ${msg}`; } if (modal) { modal.classList.remove('hidden'); } } // Kiểm tra trạng thái bản quyền từ API async function checkLicenseStatus() { const badge = document.getElementById('headerLicenseBadge'); const text = document.getElementById('headerLicenseText'); const banner = document.getElementById('licenseBanner'); const bannerText = document.getElementById('licenseBannerText'); try { const res = await fetch(`${API_BASE}/api/license/status`); const json = await res.json(); if (json.success && json.data) { const d = json.data; if (d.is_valid) { if (badge) { badge.classList.remove('hidden'); badge.className = 'text-[11px] px-2.5 py-0.5 rounded-full font-bold bg-emerald-500/20 text-emerald-200 border border-emerald-400/30 flex items-center gap-1.5'; if (text) text.innerHTML = ` ${d.hospital || d.title || 'Bản quyền hợp lệ'} (HSD: ${d.expires || 'Vô thời hạn'})`; } if (banner) banner.classList.add('hidden'); } else { if (badge) { badge.classList.remove('hidden'); badge.className = 'text-[11px] px-2.5 py-0.5 rounded-full font-bold bg-rose-500/30 text-rose-200 border border-rose-400/30 flex items-center gap-1.5'; if (text) text.innerHTML = ` Chưa có bản quyền`; } if (banner) { banner.classList.remove('hidden'); if (bannerText) bannerText.innerText = d.reason || 'Hệ thống chưa cập nhật key bản quyền tính năng Web Giao Ban'; } } } } catch (err) { console.warn('Không kiểm tra được bản quyền:', err); } } // Populate Khoa Dropdown function populateKhoaDropdown(selectId, dataList) { const sel = document.getElementById(selectId); const currentVal = sel.value; const depts = Array.from(new Set(dataList.map(item => item.tendv).filter(Boolean))); sel.innerHTML = ''; depts.forEach(d => { sel.innerHTML += ``; }); if (depts.includes(currentVal)) { sel.value = currentVal; } } // ======================================================== // RENDER NGOẠI TRÚ // ======================================================== function renderNgoaiTru() { if (!rawNgoaiTruData) return; const cur = rawNgoaiTruData.current; const comp = rawNgoaiTruData.compare; const isCompare = !!comp; // KPIs let totalKham = 0, totalBhyt = 0, totalThuPhi = 0, totalChuyenVien = 0; cur.khoa.forEach(k => { totalKham += parseInt(k.tong_kham || 0, 10); totalBhyt += parseInt(k.bhyt || 0, 10); totalThuPhi += parseInt(k.thuphi || 0, 10); totalChuyenVien += parseInt(k.chuyenvien || 0, 10); }); document.getElementById('kpiTongKham').innerText = fmt(totalKham); document.getElementById('kpiBhyt').innerText = fmt(totalBhyt); document.getElementById('kpiBhytPct').innerText = totalKham > 0 ? (totalBhyt / totalKham * 100).toFixed(1) + '% tổng lượt' : '0%'; document.getElementById('kpiThuPhi').innerText = fmt(totalThuPhi); document.getElementById('kpiThuPhiPct').innerText = totalKham > 0 ? (totalThuPhi / totalKham * 100).toFixed(1) + '% tổng lượt' : '0%'; document.getElementById('kpiChuyenVien').innerText = fmt(totalChuyenVien); document.getElementById('kpiTiemNgua').innerText = fmt(cur.tiemngua); // BANT document.getElementById('kpiBantTong').innerText = fmt(cur.bant.tong); document.getElementById('kpiBantMoi').innerText = fmt(cur.bant.moi); document.getElementById('kpiBantTk').innerText = fmt(cur.bant.taikham); // YDCT document.getElementById('ydctDienCham').innerText = fmt(cur.ydct.dien_cham); document.getElementById('ydctVltl').innerText = fmt(cur.ydct.vltl); document.getElementById('ydctSacThuoc').innerText = fmt(cur.ydct.sac_thuoc); document.getElementById('ydctCayChi').innerText = fmt(cur.ydct.cay_chi); // Table Khoa const tbody = document.getElementById('bodyKhoaKcb'); tbody.innerHTML = ''; const compMap = {}; if (comp && comp.khoa) { comp.khoa.forEach(k => compMap[k.madv] = k); } if (!cur.khoa || cur.khoa.length === 0) { tbody.innerHTML = 'Không có dữ liệu khám chữa bệnh trong khoảng thời gian này'; } else { const hideZero = document.getElementById('chkHideZeroRows')?.checked; cur.khoa.forEach(row => { const k1 = parseInt(row.tong_kham || 0, 10); const k2 = parseInt(compMap[row.madv]?.tong_kham || 0, 10); if (hideZero) { if (isCompare && k1 === 0 && k2 === 0) return; if (!isCompare && k1 === 0) return; } let compCols = ''; if (isCompare) { const diff = k1 - k2; const pct = k2 > 0 ? ((diff / k2) * 100).toFixed(1) + '%' : (k1 > 0 ? '+100%' : '0%'); const diffColor = diff > 0 ? 'text-emerald-600 font-bold' : (diff < 0 ? 'text-rose-600 font-bold' : 'text-slate-500'); compCols = ` ${fmt(k2)} ${diff > 0 ? '+' : ''}${fmt(diff)} (${pct}) `; } tbody.innerHTML += ` ${row.madv} ${row.tendv} ${fmt(row.bhyt)} ${fmt(row.thuphi)} ${fmt(row.chuyenvien)} ${fmt(row.tong_kham)} ${compCols} `; }); } // Table Khoa Footer const tfoot = document.getElementById('footKhoaKcb'); let compFoot = ''; if (isCompare && comp && comp.khoa) { let compTotalKham = 0; comp.khoa.forEach(k => compTotalKham += parseInt(k.tong_kham || 0, 10)); const totalDiff = totalKham - compTotalKham; const totalPct = compTotalKham > 0 ? ((totalDiff / compTotalKham) * 100).toFixed(1) + '%' : '0%'; compFoot = ` ${fmt(compTotalKham)} ${totalDiff > 0 ? '+' : ''}${fmt(totalDiff)} (${totalPct}) `; } tfoot.innerHTML = ` TỔNG CỘNG ${fmt(totalBhyt)} ${fmt(totalThuPhi)} ${fmt(totalChuyenVien)} ${fmt(totalKham)} ${compFoot} `; // CLS Table const bodyCls = document.getElementById('bodyCls'); bodyCls.innerHTML = ''; if (!cur.cls || cur.cls.length === 0) { bodyCls.innerHTML = 'Không có chỉ định Cận lâm sàng nào trong kỳ'; } else { cur.cls.forEach(c => { const nhomBadge = c.kho === 'XN' ? 'Xét nghiệm' : 'CĐHA / TDCN'; bodyCls.innerHTML += ` ${nhomBadge} ${c.tenloai} (${c.maloai}) ${fmt(c.so_bn)} ${fmt(c.so_luot)} `; }); } // Top 10 Table const bodyTop10 = document.getElementById('bodyTop10'); bodyTop10.innerHTML = ''; if (!cur.top10 || cur.top10.length === 0) { bodyTop10.innerHTML = 'Không có dữ liệu chẩn đoán'; } else { cur.top10.forEach((item, idx) => { bodyTop10.innerHTML += ` ${item.maicd} ${item.kqcdoan} ${fmt(item.sl)} `; }); } // Render Outpatient Chart renderChartNgoaiTru(); } // Render Chart Ngoại Trú (Chart.js) function renderChartNgoaiTru() { if (!rawNgoaiTruData) return; const canvas = document.getElementById('chartNgoaiTru'); if (!canvas) return; const ctx = canvas.getContext('2d'); const cur = rawNgoaiTruData.current || {}; const comp = rawNgoaiTruData.compare; if (chartNgoaiTruInstance) { chartNgoaiTruInstance.destroy(); chartNgoaiTruInstance = null; } const khoaList = Array.isArray(cur.khoa) ? cur.khoa : []; const clsList = Array.isArray(cur.cls) ? cur.cls : []; // 1. Biểu đồ tròn % khám theo từng khoa (ISS-20261026-002) if (currentChartTypeNgoaiTru === 'khoa-pie') { const top6 = khoaList.slice(0, 6); const otherSum = khoaList.slice(6).reduce((acc, k) => acc + parseInt(k.tong_kham || 0, 10), 0); const labels = top6.map(k => k.tendv); const data = top6.map(k => parseInt(k.tong_kham || 0, 10)); if (otherSum > 0) { labels.push('Các khoa khác'); data.push(otherSum); } const colors = [ 'rgba(37, 99, 235, 0.85)', 'rgba(16, 185, 129, 0.85)', 'rgba(245, 158, 11, 0.85)', 'rgba(236, 72, 153, 0.85)', 'rgba(99, 102, 241, 0.85)', 'rgba(6, 182, 212, 0.85)', 'rgba(148, 163, 184, 0.85)' ]; chartNgoaiTruInstance = new Chart(ctx, { type: 'doughnut', data: { labels: labels, datasets: [{ data: data, backgroundColor: colors.slice(0, labels.length), borderWidth: 2, borderColor: '#ffffff' }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right', labels: { font: { size: 11, family: 'Inter, sans-serif' }, boxWidth: 14 } }, tooltip: { callbacks: { label: function(context) { const total = context.dataset.data.reduce((a, b) => a + b, 0); const val = context.raw || 0; const pct = total > 0 ? ((val / total) * 100).toFixed(1) : 0; return ` ${context.label}: ${fmt(val)} lượt (${pct}%)`; } } } } } }); return; } // 2. Biểu đồ cột nhóm BHYT vs Thu phí (ISS-20261026-002) if (currentChartTypeNgoaiTru === 'bhyt-group') { const topKhoa = khoaList.slice(0, 7); const labels = topKhoa.map(k => k.tendv); const dataBhyt = topKhoa.map(k => parseInt(k.bhyt || 0, 10)); const dataThuPhi = topKhoa.map(k => parseInt(k.thuphi || 0, 10)); chartNgoaiTruInstance = new Chart(ctx, { type: 'bar', data: { labels: labels, datasets: [ { label: 'Khám BHYT', data: dataBhyt, backgroundColor: 'rgba(16, 185, 129, 0.85)', borderRadius: 4 }, { label: 'Khám Thu Phí / Viện Phí', data: dataThuPhi, backgroundColor: 'rgba(245, 158, 11, 0.85)', borderRadius: 4 } ] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'top', labels: { font: { size: 11, family: 'Inter, sans-serif' } } } }, scales: { y: { beginAtZero: true, ticks: { font: { size: 10 } } }, x: { ticks: { font: { size: 10 }, maxRotation: 25, minRotation: 0 } } } } }); return; } // 3. Cột Khoa hoặc CLS thông thường let labels = []; let data1 = []; let data2 = []; if (currentChartTypeNgoaiTru === 'khoa') { labels = khoaList.slice(0, 7).map(k => k.tendv); data1 = khoaList.slice(0, 7).map(k => parseInt(k.tong_kham || 0, 10)); if (comp && Array.isArray(comp.khoa)) { const compMap = {}; comp.khoa.forEach(k => compMap[k.madv] = k); data2 = khoaList.slice(0, 7).map(k => parseInt(compMap[k.madv]?.tong_kham || 0, 10)); } } else { labels = clsList.slice(0, 8).map(c => c.tenloai); data1 = clsList.slice(0, 8).map(c => parseInt(c.so_luot || 0, 10)); if (comp && Array.isArray(comp.cls)) { const compMap = {}; comp.cls.forEach(c => compMap[c.maloai] = c); data2 = clsList.slice(0, 8).map(c => parseInt(compMap[c.maloai]?.so_luot || 0, 10)); } } const datasets = [ { label: comp ? 'Kỳ hiện tại' : 'Lượt thực hiện', data: data1, backgroundColor: 'rgba(37, 99, 235, 0.85)', borderRadius: 4, } ]; if (comp) { datasets.push({ label: 'Kỳ so sánh', data: data2, backgroundColor: 'rgba(234, 88, 12, 0.85)', borderRadius: 4, }); } chartNgoaiTruInstance = new Chart(ctx, { type: 'bar', data: { labels: labels, datasets: datasets, }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'top', labels: { font: { size: 11, family: 'Inter, sans-serif' } }, }, }, scales: { y: { beginAtZero: true, ticks: { font: { size: 10 } }, }, x: { ticks: { font: { size: 10 }, maxRotation: 30, minRotation: 0, }, }, }, }, }); } // ======================================================== // RENDER NỘI TRÚ // ======================================================== function renderNoiTru() { if (!rawNoiTruData) return; const cur = rawNoiTruData.current || {}; const comp = rawNoiTruData.compare; let totalCu = 0, totalMoi = 0, totalOn = 0, totalNang = 0, totalTron = 0, totalCv = 0, totalTv = 0, totalHienCon = 0, totalTd = 0, totalCs1 = 0; const tbody = document.getElementById('bodyNoiTru'); tbody.innerHTML = ''; const khoaList = Array.isArray(cur.khoa) ? cur.khoa : []; if (khoaList.length === 0) { tbody.innerHTML = 'Không có dữ liệu nội trú trong khoảng thời gian này'; } else { const hideZero = document.getElementById('chkHideZeroRows')?.checked; khoaList.forEach(row => { const cu = parseInt(row.benh_cu || 0, 10); const moi = parseInt(row.benh_moi || 0, 10); const on = parseInt(row.on_xin_ve || 0, 10); const nang = parseInt(row.nang_xin_ve || 0, 10); const tron = parseInt(row.tron_vien || 0, 10); const cv = parseInt(row.chuyen_vien || 0, 10); const tv = parseInt(row.tu_vong || 0, 10); const hiencon = parseInt(row.benh_hien_con || 0, 10); const td = parseInt(row.benh_theo_doi || 0, 10); const cs1 = parseInt(row.chamsoc_cap1 || 0, 10); totalCu += cu; totalMoi += moi; totalOn += on; totalNang += nang; totalTron += tron; totalCv += cv; totalTv += tv; totalHienCon += hiencon; totalTd += td; totalCs1 += cs1; if (hideZero) { const rowSum = cu + moi + on + nang + tron + cv + tv + hiencon + td + cs1; if (rowSum === 0) return; } tbody.innerHTML += ` ${row.madv} ${row.tendv} ${fmt(cu)} ${fmt(moi)} ${fmt(on)} ${fmt(nang)} ${fmt(tron)} ${fmt(cv)} ${fmt(tv)} ${fmt(hiencon)} ${fmt(td)} ${fmt(cs1)} `; }); } // KPI Nội Trú const totalXuatVien = totalOn + totalNang + totalTron + totalCv + totalTv; document.getElementById('kpiNtBenhCu').innerText = fmt(totalCu); document.getElementById('kpiNtBenhMoi').innerText = fmt(totalMoi); document.getElementById('kpiNtXuatVien').innerText = fmt(totalXuatVien); document.getElementById('kpiNtOnPct').innerText = `Ổn về: ${fmt(totalOn)} ca`; document.getElementById('kpiNtHienCon').innerText = fmt(totalHienCon); document.getElementById('kpiNtTheoDoi').innerText = fmt(totalTd); document.getElementById('kpiNtCsCap1').innerText = fmt(totalCs1); // Footer NoiTru Table document.getElementById('footNoiTru').innerHTML = ` TỔNG CỘNG ${fmt(totalCu)} ${fmt(totalMoi)} ${fmt(totalOn)} ${fmt(totalNang)} ${fmt(totalTron)} ${fmt(totalCv)} ${fmt(totalTv)} ${fmt(totalHienCon)} ${fmt(totalTd)} ${fmt(totalCs1)} `; // Render Inpatient Chart renderChartNoiTru(); } // Render Chart Nội Trú (ISS-20261027-001) function renderChartNoiTru() { if (!rawNoiTruData) return; const canvas = document.getElementById('chartNoiTru'); if (!canvas) return; const ctx = canvas.getContext('2d'); const cur = rawNoiTruData.current || {}; const comp = rawNoiTruData.compare; const subTitle = document.getElementById('subTitleChartNoiTru'); if (chartNoiTruInstance) { chartNoiTruInstance.destroy(); chartNoiTruInstance = null; } const khoaList = Array.isArray(cur.khoa) ? cur.khoa : []; // 1. Biểu đồ tròn % 4 tiêu chí toàn viện (Bệnh cũ, Mới, Xuất viện, Hiện còn) if (currentChartTypeNoiTru === 'noitru-pie-tong') { if (subTitle) subTitle.innerText = 'Tỷ lệ cơ cấu 4 tiêu chí luân chuyển bệnh nhân toàn viện'; let cu = 0, moi = 0, xuat = 0, hiencon = 0; khoaList.forEach(k => { cu += parseInt(k.benh_cu || 0, 10); moi += parseInt(k.benh_moi || 0, 10); xuat += ( parseInt(k.on_xin_ve || 0, 10) + parseInt(k.nang_xin_ve || 0, 10) + parseInt(k.tron_vien || 0, 10) + parseInt(k.chuyen_vien || 0, 10) + parseInt(k.tu_vong || 0, 10) ); hiencon += parseInt(k.benh_hien_con || 0, 10); }); chartNoiTruInstance = new Chart(ctx, { type: 'doughnut', data: { labels: ['Bệnh cũ đầu kỳ', 'Bệnh mới vào viện', 'Tổng xuất viện', 'Hiện còn điều trị'], datasets: [{ data: [cu, moi, xuat, hiencon], backgroundColor: [ 'rgba(148, 163, 184, 0.85)', 'rgba(37, 99, 235, 0.85)', 'rgba(16, 185, 129, 0.85)', 'rgba(99, 102, 241, 0.85)' ], borderWidth: 2, borderColor: '#ffffff' }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right', labels: { font: { size: 11, family: 'Inter, sans-serif' }, boxWidth: 14 } }, tooltip: { callbacks: { label: function(context) { const total = context.dataset.data.reduce((a, b) => a + b, 0); const val = context.raw || 0; const pct = total > 0 ? ((val / total) * 100).toFixed(1) : 0; return ` ${context.label}: ${fmt(val)} ca (${pct}%)`; } } } } } }); return; } // 2. Biểu đồ tròn % bệnh hiện còn theo từng khoa if (currentChartTypeNoiTru === 'noitru-pie-khoa') { if (subTitle) subTitle.innerText = '% Phân bố bệnh nhân hiện còn đang điều trị theo từng khoa'; const sorted = [...khoaList].sort((a, b) => parseInt(b.benh_hien_con || 0, 10) - parseInt(a.benh_hien_con || 0, 10)); const top6 = sorted.slice(0, 6); const otherSum = sorted.slice(6).reduce((acc, k) => acc + parseInt(k.benh_hien_con || 0, 10), 0); const labels = top6.map(k => k.tendv); const data = top6.map(k => parseInt(k.benh_hien_con || 0, 10)); if (otherSum > 0) { labels.push('Các khoa khác'); data.push(otherSum); } const colors = [ 'rgba(99, 102, 241, 0.85)', 'rgba(37, 99, 235, 0.85)', 'rgba(16, 185, 129, 0.85)', 'rgba(245, 158, 11, 0.85)', 'rgba(236, 72, 153, 0.85)', 'rgba(6, 182, 212, 0.85)', 'rgba(148, 163, 184, 0.85)' ]; chartNoiTruInstance = new Chart(ctx, { type: 'doughnut', data: { labels: labels, datasets: [{ data: data, backgroundColor: colors.slice(0, labels.length), borderWidth: 2, borderColor: '#ffffff' }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right', labels: { font: { size: 11, family: 'Inter, sans-serif' }, boxWidth: 14 } }, tooltip: { callbacks: { label: function(context) { const total = context.dataset.data.reduce((a, b) => a + b, 0); const val = context.raw || 0; const pct = total > 0 ? ((val / total) * 100).toFixed(1) : 0; return ` ${context.label}: ${fmt(val)} người (${pct}%)`; } } } } } }); return; } // 3. Biểu đồ cột nhóm 4 tiêu chí theo khoa (Bệnh cũ, Bệnh mới, Xuất viện, Hiện còn) if (currentChartTypeNoiTru === 'noitru-group') { if (subTitle) subTitle.innerText = 'Cột nhóm đối chiếu 4 tiêu chí chi tiết theo từng khoa'; const topKhoa = khoaList.slice(0, 6); const labels = topKhoa.map(k => k.tendv); const dataCu = topKhoa.map(k => parseInt(k.benh_cu || 0, 10)); const dataMoi = topKhoa.map(k => parseInt(k.benh_moi || 0, 10)); const dataXuat = topKhoa.map(k => ( parseInt(k.on_xin_ve || 0, 10) + parseInt(k.nang_xin_ve || 0, 10) + parseInt(k.tron_vien || 0, 10) + parseInt(k.chuyen_vien || 0, 10) + parseInt(k.tu_vong || 0, 10) )); const dataHienCon = topKhoa.map(k => parseInt(k.benh_hien_con || 0, 10)); chartNoiTruInstance = new Chart(ctx, { type: 'bar', data: { labels: labels, datasets: [ { label: 'Bệnh cũ', data: dataCu, backgroundColor: 'rgba(148, 163, 184, 0.85)', borderRadius: 3 }, { label: 'Bệnh mới', data: dataMoi, backgroundColor: 'rgba(37, 99, 235, 0.85)', borderRadius: 3 }, { label: 'Xuất viện', data: dataXuat, backgroundColor: 'rgba(16, 185, 129, 0.85)', borderRadius: 3 }, { label: 'Hiện còn', data: dataHienCon, backgroundColor: 'rgba(99, 102, 241, 0.85)', borderRadius: 3 } ] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'top', labels: { font: { size: 11, family: 'Inter, sans-serif' } } } }, scales: { y: { beginAtZero: true }, x: { ticks: { maxRotation: 25, minRotation: 0, font: { size: 10 } } } } } }); return; } // 4. Mặc định: Cột vào viện & ra viện (noitru-col) if (subTitle) subTitle.innerText = 'Phân bố lưu lượng và cơ cấu bệnh nhân điều trị'; const topKhoa = khoaList.slice(0, 8); const labels = topKhoa.map(k => k.tendv); const dataMoi = topKhoa.map(k => parseInt(k.benh_moi || 0, 10)); const dataRaVien = topKhoa.map(k => ( parseInt(k.on_xin_ve || 0, 10) + parseInt(k.nang_xin_ve || 0, 10) + parseInt(k.tron_vien || 0, 10) + parseInt(k.chuyen_vien || 0, 10) + parseInt(k.tu_vong || 0, 10) )); const dataHienCon = topKhoa.map(k => parseInt(k.benh_hien_con || 0, 10)); const datasets = [ { label: 'Bệnh mới vào viện', data: dataMoi, backgroundColor: 'rgba(37, 99, 235, 0.85)', borderRadius: 4, }, { label: 'Tổng xuất viện', data: dataRaVien, backgroundColor: 'rgba(16, 185, 129, 0.85)', borderRadius: 4, }, { label: 'Hiện còn đang điều trị', data: dataHienCon, backgroundColor: 'rgba(99, 102, 241, 0.85)', borderRadius: 4, } ]; if (comp && Array.isArray(comp.khoa)) { const compMap = {}; comp.khoa.forEach(k => compMap[k.madv] = k); const dataMoiComp = topKhoa.map(k => parseInt(compMap[k.madv]?.benh_moi || 0, 10)); datasets.push({ label: 'Bệnh mới (Kỳ so sánh)', data: dataMoiComp, backgroundColor: 'rgba(234, 88, 12, 0.85)', borderRadius: 4, }); } chartNoiTruInstance = new Chart(ctx, { type: 'bar', data: { labels: labels, datasets: datasets, }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'top', labels: { font: { size: 11, family: 'Inter, sans-serif' } }, }, }, scales: { y: { beginAtZero: true }, x: { ticks: { maxRotation: 25, minRotation: 0 } }, }, }, }); } // ======================================================== // RENDER BẢNG 3: BỆNH NHÂN CHUYỂN VIỆN // ======================================================== function renderBang3(filterDept) { const tbody = document.getElementById('bodyBang3'); tbody.innerHTML = ''; const filtered = filterDept === 'ALL' ? rawBang3Data : rawBang3Data.filter(item => item.tendv === filterDept); document.getElementById('countBang3').innerText = `${filtered.length} ca`; if (filtered.length === 0) { tbody.innerHTML = `Không có bệnh nhân chuyển viện trong thời gian này`; return; } filtered.forEach((row, index) => { tbody.innerHTML += ` ${index + 1}
${row.mabn}
${row.maba}
${row.hoten} ${row.diachi}
${row.ngayvv}
${row.ngayrv}
${row.tendv}
${row.chandoan_chinh}
${row.chandoan_phu ? `
${row.chandoan_phu}
` : ''} ${row.ketqua_cls} `; }); attachNoteHandlers(); } // ======================================================== // RENDER BẢNG 4: BỆNH NHÂN THEO DÕI & CS CẤP I // ======================================================== function renderBang4(filterDept) { const tbody = document.getElementById('bodyBang4'); tbody.innerHTML = ''; const filtered = filterDept === 'ALL' ? rawBang4Data : rawBang4Data.filter(item => item.tendv === filterDept); document.getElementById('countBang4').innerText = `${filtered.length} ca`; if (filtered.length === 0) { tbody.innerHTML = `Không có bệnh nhân cần theo dõi hoặc CS Cấp I`; return; } filtered.forEach((row, index) => { tbody.innerHTML += ` ${index + 1}
${row.mabn}
${row.maba}
${row.hoten} ${row.diachi} ${row.ngayvv}
${row.tendv}
${row.chamsoc}
${row.chandoan_chinh}
${row.chandoan_phu ? `
${row.chandoan_phu}
` : ''} ${row.ketqua_cls} `; }); attachNoteHandlers(); } // INLINE NOTE AUTO-SAVE HANDLER function attachNoteHandlers() { document.querySelectorAll('.table-cell-edit').forEach(el => { el.removeEventListener('change', handleNoteChange); el.addEventListener('change', handleNoteChange); }); } async function handleNoteChange(e) { const el = e.target; const maba = el.getAttribute('data-maba'); const mabn = el.getAttribute('data-mabn'); const loai = el.getAttribute('data-loai'); const ghichu = el.value.trim(); const database = document.getElementById('selectDb').value; el.classList.add('saving'); try { const res = await fetch(`${API_BASE}/api/cap-nhat-ghi-chu`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ maba, mabn, loai, ghichu, database }), }); const result = await res.json(); if (result.success) { el.classList.remove('saving'); el.classList.add('saved'); showToast(`Đã lưu ghi chú BN ${mabn}`); setTimeout(() => el.classList.remove('saved'), 2000); } else { throw new Error(result.message); } } catch (err) { el.classList.remove('saving'); alert('Lỗi lưu ghi chú: ' + err.message); } } // Export Excel Helper function exportExcel() { const activeTab = document.querySelector('.tab-btn.active').getAttribute('data-tab'); const table = activeTab === 'tab-ngoaitru' ? document.getElementById('tableKhoaKcb') : document.getElementById('tableNoiTru'); let html = table.outerHTML; let blob = new Blob(['\ufeff' + html], { type: 'application/vnd.ms-excel' }); let url = URL.createObjectURL(blob); let a = document.createElement('a'); a.href = url; a.download = `BaoCaoGiaoBan_${activeTab}_${Date.now()}.xls`; document.body.appendChild(a); a.click(); document.body.removeChild(a); showToast('Đã xuất tập tin Excel thành công!'); } // ======================================================== // CHỨC NĂNG QUẢN LÝ ISSUE, BÁO LỖI & LƯU FILE TỔNG HỢP // ======================================================== function initIssueFeatures() { // Modal Open / Close const btnOpen1 = document.getElementById('btnOpenIssueModal'); const btnOpen2 = document.getElementById('btnOpenIssueModal2'); const btnClose = document.getElementById('btnCloseIssueModal'); const btnCancel = document.getElementById('btnCancelIssue'); if (btnOpen1) btnOpen1.addEventListener('click', openCreateIssueModal); if (btnOpen2) btnOpen2.addEventListener('click', openCreateIssueModal); if (btnClose) btnClose.addEventListener('click', closeCreateIssueModal); if (btnCancel) btnCancel.addEventListener('click', closeCreateIssueModal); // Form Submit const formCreate = document.getElementById('formCreateIssue'); if (formCreate) formCreate.addEventListener('submit', handleCreateIssue); // Update Modal const btnCloseUp = document.getElementById('btnCloseUpdateModal'); const btnCancelUp = document.getElementById('btnCancelUpdate'); if (btnCloseUp) btnCloseUp.addEventListener('click', closeUpdateModal); if (btnCancelUp) btnCancelUp.addEventListener('click', closeUpdateModal); const formUpdate = document.getElementById('formUpdateIssue'); if (formUpdate) formUpdate.addEventListener('submit', handleUpdateIssue); // Lightbox const btnCloseLb = document.getElementById('btnCloseLightbox'); const modalLb = document.getElementById('modalImagePreview'); if (btnCloseLb) btnCloseLb.addEventListener('click', closeLightbox); if (modalLb) { modalLb.addEventListener('click', (e) => { if (e.target === modalLb) closeLightbox(); }); } // File Upload & Paste Zone const pasteZone = document.getElementById('issuePasteZone'); const fileInput = document.getElementById('issueFileInput'); const btnRemove = document.getElementById('btnRemoveFile'); if (pasteZone) { pasteZone.addEventListener('click', (e) => { if (e.target.closest('#btnRemoveFile')) return; fileInput.click(); }); } if (fileInput) { fileInput.addEventListener('change', () => { if (fileInput.files && fileInput.files[0]) { setIssueFile(fileInput.files[0]); } }); } if (btnRemove) { btnRemove.addEventListener('click', (e) => { e.stopPropagation(); clearIssueFile(); }); } // Clipboard Paste (Ctrl+V) anywhere inside the modal or page document.addEventListener('paste', (e) => { const modal = document.getElementById('modalIssue'); if (!modal || modal.classList.contains('hidden')) return; const items = e.clipboardData?.items; if (!items) return; for (let i = 0; i < items.length; i++) { if (items[i].type.indexOf('image') !== -1) { const blob = items[i].getAsFile(); if (blob) { const ext = blob.type.split('/')[1] || 'png'; const file = new File([blob], `screenshot_${Date.now()}.${ext}`, { type: blob.type }); setIssueFile(file); showToast('Đã dán ảnh chụp màn hình từ Clipboard!'); break; } } } }); // Filters const fStatus = document.getElementById('filterIssueStatus'); const fCat = document.getElementById('filterIssueCategory'); const fKeyword = document.getElementById('filterIssueKeyword'); if (fStatus) fStatus.addEventListener('change', renderIssuesTable); if (fCat) fCat.addEventListener('change', renderIssuesTable); if (fKeyword) fKeyword.addEventListener('input', renderIssuesTable); // Export Excel const btnExportIssues = document.getElementById('btnExportIssuesExcel'); if (btnExportIssues) btnExportIssues.addEventListener('click', exportIssuesExcel); } function setIssueFile(file) { currentIssueFile = file; const prompt = document.getElementById('pasteZonePrompt'); const preview = document.getElementById('issuePreviewArea'); const nameEl = document.getElementById('issueFileName'); const sizeEl = document.getElementById('issueFileSize'); const imgEl = document.getElementById('issueImgPreview'); const fileIcon = document.getElementById('issueFileIcon'); prompt.classList.add('hidden'); preview.classList.remove('hidden'); nameEl.innerText = file.name; sizeEl.innerText = `${(file.size / 1024).toFixed(1)} KB`; if (file.type.startsWith('image/')) { imgEl.classList.remove('hidden'); fileIcon.classList.add('hidden'); const reader = new FileReader(); reader.onload = (e) => { imgEl.src = e.target.result; }; reader.readAsDataURL(file); } else { imgEl.classList.add('hidden'); fileIcon.classList.remove('hidden'); } } function clearIssueFile() { currentIssueFile = null; const fileInput = document.getElementById('issueFileInput'); if (fileInput) fileInput.value = ''; const prompt = document.getElementById('pasteZonePrompt'); const preview = document.getElementById('issuePreviewArea'); prompt.classList.remove('hidden'); preview.classList.add('hidden'); } function openCreateIssueModal() { const selectDb = document.getElementById('selectDb'); const dbName = selectDb.selectedOptions[0]?.textContent || selectDb.value; const dateFrom = document.getElementById('dateFrom').value; const dateTo = document.getElementById('dateTo').value; document.getElementById('issueDatabase').value = `${dbName} (${selectDb.value})`; document.getElementById('issuePeriod').value = `Từ ${dateFrom} đến ${dateTo}`; document.getElementById('issueTitle').value = ''; document.getElementById('issueDescription').value = ''; clearIssueFile(); const modal = document.getElementById('modalIssue'); modal.classList.remove('hidden'); } function closeCreateIssueModal() { const modal = document.getElementById('modalIssue'); modal.classList.add('hidden'); } async function handleCreateIssue(e) { e.preventDefault(); const btnSubmit = document.getElementById('btnSubmitIssue'); btnSubmit.disabled = true; btnSubmit.innerHTML = ' Đang lưu...'; try { const selectDb = document.getElementById('selectDb'); const dbValue = selectDb.value; const dbName = selectDb.selectedOptions[0]?.textContent || dbValue; const formData = new FormData(); formData.append('title', document.getElementById('issueTitle').value); formData.append('category', document.getElementById('issueCategory').value); formData.append('module', document.getElementById('issueModule').value); formData.append('priority', document.getElementById('issuePriority').value); formData.append('database', dbValue); formData.append('databaseName', dbName); formData.append('period', document.getElementById('issuePeriod').value); formData.append('reporter', document.getElementById('issueReporter').value); formData.append('department', document.getElementById('issueDepartment').value); formData.append('description', document.getElementById('issueDescription').value); if (currentIssueFile) { formData.append('attachment', currentIssueFile); } const res = await fetch(`${API_BASE}/api/issues`, { method: 'POST', body: formData, }); const result = await res.json(); if (result.success) { showToast('Đã lưu báo lỗi & tệp thành công!'); closeCreateIssueModal(); await loadIssues(true); } else { throw new Error(result.message); } } catch (err) { alert('Lỗi gửi báo lỗi: ' + err.message); } finally { btnSubmit.disabled = false; btnSubmit.innerHTML = ' Gửi Báo Lỗi & Lưu File'; } } async function loadIssues(shouldShowToast = false) { try { const res = await fetch(`${API_BASE}/api/issues`); const result = await res.json(); if (result.success && Array.isArray(result.data)) { rawIssuesData = result.data; updateIssueKpis(); renderIssuesTable(); if (shouldShowToast) { showToast(`Đã nạp ${rawIssuesData.length} issue`); } } } catch (err) { console.error('Error loading issues:', err); } } function updateIssueKpis() { const total = rawIssuesData.length; const pending = rawIssuesData.filter(i => i.status === 'CHO_XU_LY').length; const processing = rawIssuesData.filter(i => i.status === 'DANG_XU_LY').length; const resolved = rawIssuesData.filter(i => i.status === 'DA_XU_LY' || i.status === 'DONG').length; const totalEl = document.getElementById('issueKpiTotal'); const pendingEl = document.getElementById('issueKpiPending'); const procEl = document.getElementById('issueKpiProcessing'); const resEl = document.getElementById('issueKpiResolved'); if (totalEl) totalEl.innerText = total; if (pendingEl) pendingEl.innerText = pending; if (procEl) procEl.innerText = processing; if (resEl) resEl.innerText = resolved; const badge = document.getElementById('badgeIssueCount'); if (badge) { badge.innerText = pending; badge.classList.toggle('bg-rose-100', pending > 0); badge.classList.toggle('text-rose-700', pending > 0); badge.classList.toggle('bg-slate-100', pending === 0); badge.classList.toggle('text-slate-500', pending === 0); } } function renderIssuesTable() { const tbody = document.getElementById('bodyIssuesList'); if (!tbody) return; const fStatus = document.getElementById('filterIssueStatus')?.value || 'ALL'; const fCat = document.getElementById('filterIssueCategory')?.value || 'ALL'; const keyword = (document.getElementById('filterIssueKeyword')?.value || '').toLowerCase().trim(); const filtered = rawIssuesData.filter(item => { if (fStatus !== 'ALL' && item.status !== fStatus) return false; if (fCat !== 'ALL' && item.category !== fCat) return false; if (keyword) { const matchId = (item.id || '').toLowerCase().includes(keyword); const matchTitle = (item.title || '').toLowerCase().includes(keyword); const matchDesc = (item.description || '').toLowerCase().includes(keyword); const matchReporter = (item.reporter || '').toLowerCase().includes(keyword); const matchDept = (item.department || '').toLowerCase().includes(keyword); if (!matchId && !matchTitle && !matchDesc && !matchReporter && !matchDept) return false; } return true; }); if (filtered.length === 0) { tbody.innerHTML = 'Không có issue nào phù hợp với bộ lọc.'; return; } tbody.innerHTML = ''; filtered.forEach(row => { // Badges const catBadge = getCategoryBadge(row.category); const priBadge = getPriorityBadge(row.priority); const statusBadge = getStatusBadge(row.status); const createdStr = formatDateTime(row.createdAt); // Attachment let attachHtml = '-'; if (row.attachment) { const attachUrl = `${API_BASE}${row.attachment.url}`; if (row.attachment.mimetype && row.attachment.mimetype.startsWith('image/')) { attachHtml = ` `; } else { attachHtml = ` Tải file `; } } // Handler notes let notesHtml = ''; if (row.handlerNotes) { notesHtml = `
Xử lý (${escapeHtml(row.resolvedBy || 'IT')}): ${escapeHtml(row.handlerNotes)}
`; } tbody.innerHTML += `
${row.id}
${createdStr}
${escapeHtml(row.title)}
${escapeHtml(row.description)}
${notesHtml}
${catBadge}
${getModuleLabel(row.module)}
${priBadge}
${escapeHtml(row.databaseName || row.database)}
${escapeHtml(row.period || '-')}
${escapeHtml(row.reporter || 'Cán bộ')}
${escapeHtml(row.department || '-')}
${attachHtml} ${statusBadge}
`; }); } function getCategoryBadge(cat) { switch (cat) { case 'SAI_LECH_SO_LIEU': return 'Sai lệch số liệu'; case 'LOI_GIAO_DIEN': return 'Lỗi giao diện'; case 'LOI_KET_NOI': return 'Lỗi kết nối'; case 'DE_XUAT_CAI_TIEN': return 'Đề xuất cải tiến'; case 'YEU_CAU_MOI': return 'Yêu cầu mới'; default: return 'Khác'; } } function getPriorityBadge(pri) { switch (pri) { case 'KHAN_CAP': return 'Khẩn cấp'; case 'CAO': return 'Cao'; case 'THAP': return 'Thấp'; default: return 'Bình thường'; } } function getStatusBadge(st) { switch (st) { case 'CHO_XU_LY': return ' Chờ xử lý'; case 'DANG_XU_LY': return ' Đang xử lý'; case 'DA_XU_LY': return ' Đã xử lý'; case 'DONG': return ' Đã đóng'; default: return st; } } function getModuleLabel(mod) { switch (mod) { case 'NGOAI_TRU': return '1. Ngoại trú'; case 'NOI_TRU': return '2. Nội trú'; case 'CHUYEN_VIEN': return 'Bảng 3. Chuyển viện'; case 'THEO_DOI': return 'Bảng 4. BN Theo dõi'; default: return 'Toàn hệ thống'; } } function formatDateTime(iso) { if (!iso) return '-'; const d = new Date(iso); if (isNaN(d.getTime())) return iso; const day = String(d.getDate()).padStart(2, '0'); const month = String(d.getMonth() + 1).padStart(2, '0'); const year = d.getFullYear(); const hours = String(d.getHours()).padStart(2, '0'); const minutes = String(d.getMinutes()).padStart(2, '0'); return `${day}/${month}/${year} ${hours}:${minutes}`; } function escapeHtml(str) { if (!str) return ''; return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function openUpdateModal(issueId) { const item = rawIssuesData.find(i => i.id === issueId); if (!item) return; editingIssueId = issueId; document.getElementById('updateModalIssueId').innerText = issueId; document.getElementById('updateStatus').value = item.status || 'CHO_XU_LY'; document.getElementById('updateResolvedBy').value = item.resolvedBy || ''; document.getElementById('updateHandlerNotes').value = item.handlerNotes || ''; const modal = document.getElementById('modalUpdateIssue'); modal.classList.remove('hidden'); } function closeUpdateModal() { const modal = document.getElementById('modalUpdateIssue'); modal.classList.add('hidden'); editingIssueId = null; } async function handleUpdateIssue(e) { e.preventDefault(); if (!editingIssueId) return; const btn = document.getElementById('btnSaveUpdateIssue'); btn.disabled = true; btn.innerHTML = ' Đang lưu...'; try { const status = document.getElementById('updateStatus').value; const resolvedBy = document.getElementById('updateResolvedBy').value.trim(); const handlerNotes = document.getElementById('updateHandlerNotes').value.trim(); const res = await fetch(`${API_BASE}/api/issues/${editingIssueId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status, resolvedBy, handlerNotes }), }); const result = await res.json(); if (result.success) { showToast(`Đã cập nhật issue ${editingIssueId}`); closeUpdateModal(); await loadIssues(false); } else { throw new Error(result.message); } } catch (err) { alert('Lỗi cập nhật issue: ' + err.message); } finally { btn.disabled = false; btn.innerHTML = ' Lưu Cập Nhật'; } } async function handleDeleteIssue(issueId) { if (!confirm(`Bạn có chắc muốn xóa Issue [${issueId}] này không?`)) return; try { const res = await fetch(`${API_BASE}/api/issues/${issueId}`, { method: 'DELETE', }); const result = await res.json(); if (result.success) { showToast(`Đã xóa issue ${issueId}`); await loadIssues(false); } else { throw new Error(result.message); } } catch (err) { alert('Lỗi xóa issue: ' + err.message); } } function openImageLightbox(url, caption) { const modal = document.getElementById('modalImagePreview'); const img = document.getElementById('lightboxImg'); const cap = document.getElementById('lightboxCaption'); img.src = url; cap.innerText = caption || 'Hình ảnh sự cố'; modal.classList.remove('hidden'); } function closeLightbox() { const modal = document.getElementById('modalImagePreview'); modal.classList.add('hidden'); } function exportIssuesExcel() { if (rawIssuesData.length === 0) { alert('Chưa có issue nào để xuất file!'); return; } let tableHtml = ` `; rawIssuesData.forEach(row => { tableHtml += ` `; }); tableHtml += '
Mã Issue Ngày Báo Cáo Tiêu Đề Phân Loại Phân Hệ Mức Độ CSDL Kỳ Thời Gian Người Báo Khoa / Phòng Nội Dung Chi Tiết Trạng Thái Kỹ Thuật Xử Lý Ghi Chú Giải Pháp Ngày Giải Quyết Tệp Đính Kèm
${escapeHtml(row.id)} ${formatDateTime(row.createdAt)} ${escapeHtml(row.title)} ${escapeHtml(row.category)} ${escapeHtml(row.module)} ${escapeHtml(row.priority)} ${escapeHtml(row.databaseName || row.database)} ${escapeHtml(row.period)} ${escapeHtml(row.reporter)} ${escapeHtml(row.department)} ${escapeHtml(row.description)} ${escapeHtml(row.status)} ${escapeHtml(row.resolvedBy || '')} ${escapeHtml(row.handlerNotes || '')} ${formatDateTime(row.resolvedAt)} ${row.attachment ? escapeHtml(row.attachment.originalname) : ''}
'; const blob = new Blob(['\ufeff' + tableHtml], { type: 'application/vnd.ms-excel;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `DanhSach_Issues_GiaoBan_${Date.now()}.xls`; document.body.appendChild(a); a.click(); document.body.removeChild(a); showToast('Đã xuất file Excel tổng hợp issue thành công!'); } // ======================================================== // BIÊN BẢN GIAO BAN (ISS-20261027-002: MẪU A4 & 3 CẤP PHÂN QUYỀN) // ======================================================== function initBienBan() { const roleSelect = document.getElementById('selectBienBanRole'); const btnSync = document.getElementById('btnSync24hData'); const btnSave = document.getElementById('btnSaveBienBan'); const btnLock = document.getElementById('btnToggleLockBienBan'); const btnPrint = document.getElementById('btnPrintBienBan'); if (!roleSelect) return; // Restore saved role const savedRole = localStorage.getItem('dh_giaoban_bb_role') || 'cap2'; roleSelect.value = savedRole; // Check saved lock state isBienBanLocked = localStorage.getItem('dh_giaoban_bb_locked') === 'true'; // Load saved content loadBienBan(); // Apply permission applyBienBanPermissions(); roleSelect.addEventListener('change', () => { localStorage.setItem('dh_giaoban_bb_role', roleSelect.value); applyBienBanPermissions(); }); if (btnSync) { btnSync.addEventListener('click', sync24hData); } if (btnSave) { btnSave.addEventListener('click', saveBienBan); } if (btnLock) { btnLock.addEventListener('click', toggleLockBienBan); } if (btnPrint) { btnPrint.addEventListener('click', () => window.print()); } // Update hospital name when DB changes const selectDb = document.getElementById('selectDb'); if (selectDb) { selectDb.addEventListener('change', () => { const hospitalName = selectDb.selectedOptions[0]?.textContent.split('(')[0].trim() || 'BỆNH VIỆN ĐA KHOA'; const bbTenBv = document.getElementById('bbTenBenhVien'); if (bbTenBv && (!bbTenBv.value || bbTenBv.value === 'BỆNH VIỆN ĐA KHOA Ô MÔN')) { bbTenBv.value = hospitalName.toUpperCase(); } }); } } function applyBienBanPermissions() { const role = document.getElementById('selectBienBanRole')?.value || 'cap2'; const badgeLock = document.getElementById('badgeLockStatus'); const btnSync = document.getElementById('btnSync24hData'); const btnSave = document.getElementById('btnSaveBienBan'); const btnLock = document.getElementById('btnToggleLockBienBan'); // Input fields const generalInputs = [ 'bbTenBenhVien', 'bbSoBienBan', 'bbNgayThangNam', 'bbThoiGian', 'bbDiaDiem', 'bbChuTri', 'bbThuKy', 'bbThanhPhan', 'bbNoiDungKcb', 'bbNoiDungChuyenMon', 'bbNoiDungDuocTrangThietBi', 'bbNoiDungYkienKhoa' ]; const leadershipInput = 'bbNoiDungKetLuan'; if (badgeLock) { if (isBienBanLocked) { badgeLock.className = 'text-xs px-2.5 py-1 rounded-full font-bold bg-rose-100 text-rose-800 border border-rose-300 flex items-center gap-1'; badgeLock.innerHTML = ' Đã khóa biên bản (Chỉ xem)'; if (btnLock) btnLock.innerHTML = ' Mở khóa biên bản'; } else { badgeLock.className = 'text-xs px-2.5 py-1 rounded-full font-bold bg-emerald-100 text-emerald-800 border border-emerald-300 flex items-center gap-1'; badgeLock.innerHTML = ' Đang mở soạn thảo'; if (btnLock) btnLock.innerHTML = ' Khóa biên bản'; } } // Cap 1: Chỉ xem (ReadOnly hết) if (role === 'cap1') { generalInputs.forEach(id => { const el = document.getElementById(id); if (el) el.readOnly = true; }); const elLead = document.getElementById(leadershipInput); if (elLead) elLead.readOnly = true; if (btnSync) btnSync.classList.add('hidden'); if (btnSave) btnSave.classList.add('hidden'); if (btnLock) btnLock.classList.add('hidden'); return; } // If locked, nobody can edit except cap3 who can toggle unlock if (isBienBanLocked) { generalInputs.forEach(id => { const el = document.getElementById(id); if (el) el.readOnly = true; }); const elLead = document.getElementById(leadershipInput); if (elLead) elLead.readOnly = true; if (btnSync) btnSync.classList.add('hidden'); if (btnSave) btnSave.classList.add('hidden'); if (btnLock) { btnLock.classList.toggle('hidden', role !== 'cap3'); } return; } // Unlocked state: if (btnSync) btnSync.classList.remove('hidden'); if (btnSave) btnSave.classList.remove('hidden'); if (role === 'cap2') { // Thư ký: có thể sửa mục chung, KHÔNG thể sửa mục V (Kết luận BGĐ), KHÔNG được khóa generalInputs.forEach(id => { const el = document.getElementById(id); if (el) el.readOnly = false; }); const elLead = document.getElementById(leadershipInput); if (elLead) { elLead.readOnly = true; elLead.title = 'Chỉ Ban Giám Đốc (Cấp 3) mới có quyền nhập kết luận chỉ đạo.'; } if (btnLock) btnLock.classList.add('hidden'); } else if (role === 'cap3') { // BGĐ: sửa được tất cả và có nút khóa generalInputs.forEach(id => { const el = document.getElementById(id); if (el) el.readOnly = false; }); const elLead = document.getElementById(leadershipInput); if (elLead) { elLead.readOnly = false; elLead.removeAttribute('title'); } if (btnLock) btnLock.classList.remove('hidden'); } } function sync24hData() { const dFrom = document.getElementById('dateFrom')?.value || ''; const dTo = document.getElementById('dateTo')?.value || ''; const selectDb = document.getElementById('selectDb'); const hospitalName = selectDb?.selectedOptions[0]?.textContent.split('(')[0].trim() || 'BỆNH VIỆN'; // Fill Hospital Name const bbTenBv = document.getElementById('bbTenBenhVien'); if (bbTenBv) bbTenBv.value = hospitalName.toUpperCase(); // Fill Date const bbNgay = document.getElementById('bbNgayThangNam'); if (bbNgay) { const today = new Date(); bbNgay.value = `Ngày ${today.getDate()} tháng ${today.getMonth() + 1} năm ${today.getFullYear()}`; } let textKcb = `TỔNG HỢP HOẠT ĐỘNG KHÁM CHỮA BỆNH (Kỳ báo cáo: ${dFrom} đến ${dTo})\n\n`; // 1. Ngoại trú if (rawNgoaiTruData && rawNgoaiTruData.current) { const cur = rawNgoaiTruData.current; let totalKham = 0, totalBhyt = 0, totalThuPhi = 0, totalCv = 0; const khoaList = cur.khoa || []; khoaList.forEach(k => { totalKham += parseInt(k.tong_kham || 0, 10); totalBhyt += parseInt(k.bhyt || 0, 10); totalThuPhi += parseInt(k.thuphi || 0, 10); totalCv += parseInt(k.chuyenvien || 0, 10); }); textKcb += `1. Khám Bệnh Ngoại Trú:\n`; textKcb += `- Tổng lượt khám: ${fmt(totalKham)} lượt (BHYT: ${fmt(totalBhyt)} lượt, Thu phí: ${fmt(totalThuPhi)} lượt, Chuyển viện: ${fmt(totalCv)} lượt).\n`; const topKhoa = khoaList.slice(0, 3).map(k => `${k.tendv} (${fmt(k.tong_kham)})`).join(', '); if (topKhoa) { textKcb += `- Các phòng/khoa khám đông nhất: ${topKhoa}.\n`; } if (cur.cls && cur.cls.length > 0) { let xn = 0, cdha = 0; cur.cls.forEach(c => { if (c.kho === 'XN') xn += parseInt(c.so_luot || 0, 10); else cdha += parseInt(c.so_luot || 0, 10); }); textKcb += `- Cận lâm sàng: Xét nghiệm ${fmt(xn)} lượt; Chẩn đoán hình ảnh & TDCN: ${fmt(cdha)} lượt.\n`; } textKcb += `\n`; } // 2. Nội trú if (rawNoiTruData && rawNoiTruData.current) { const cur = rawNoiTruData.current; let cu = 0, moi = 0, on = 0, nang = 0, tron = 0, cv = 0, tv = 0, hiencon = 0, td = 0, cs1 = 0; (cur.khoa || []).forEach(k => { cu += parseInt(k.benh_cu || 0, 10); moi += parseInt(k.benh_moi || 0, 10); on += parseInt(k.on_xin_ve || 0, 10); nang += parseInt(k.nang_xin_ve || 0, 10); tron += parseInt(k.tron_vien || 0, 10); cv += parseInt(k.chuyen_vien || 0, 10); tv += parseInt(k.tu_vong || 0, 10); hiencon += parseInt(k.benh_hien_con || 0, 10); td += parseInt(k.benh_theo_doi || 0, 10); cs1 += parseInt(k.chamsoc_cap1 || 0, 10); }); const xuatVien = on + nang + tron + cv + tv; textKcb += `2. Điều Trị Nội Trú:\n`; textKcb += `- Bệnh nhân cũ đầu kỳ: ${fmt(cu)} người.\n`; textKcb += `- Bệnh nhân mới vào viện trong kỳ: ${fmt(moi)} người.\n`; textKcb += `- Tổng xuất viện: ${fmt(xuatVien)} người (Trong đó: Ổn cho về: ${fmt(on)}, Xin về nặng: ${fmt(nang)}, Trốn viện: ${fmt(tron)}, Chuyển tuyến: ${fmt(cv)}, Tử vong: ${fmt(tv)}).\n`; textKcb += `- Hiện còn đang điều trị tại các khoa: ${fmt(hiencon)} người.\n`; textKcb += `- Bệnh nhân nặng cần theo dõi đặc biệt: ${fmt(td)} ca; Chăm sóc Cấp I: ${fmt(cs1)} ca.\n\n`; } // 3. Chuyển viện if (rawBang3Data && rawBang3Data.length > 0) { textKcb += `3. Bệnh Nhân Chuyển Tuyến / Chuyển Viện:\n`; textKcb += `- Tổng số: ${rawBang3Data.length} bệnh nhân được làm thủ tục chuyển viện an toàn theo đúng quy trình chuyên môn (Chi tiết đính kèm Bảng 3).\n`; } const bbKcbEl = document.getElementById('bbNoiDungKcb'); if (bbKcbEl) { bbKcbEl.value = textKcb.trim(); } showToast('Đã đồng bộ số liệu 24h vào biên bản thành công!'); } function saveBienBan() { const fields = [ 'bbTenBenhVien', 'bbSoBienBan', 'bbNgayThangNam', 'bbThoiGian', 'bbDiaDiem', 'bbChuTri', 'bbThuKy', 'bbThanhPhan', 'bbNoiDungKcb', 'bbNoiDungChuyenMon', 'bbNoiDungDuocTrangThietBi', 'bbNoiDungYkienKhoa', 'bbNoiDungKetLuan' ]; const data = {}; fields.forEach(id => { const el = document.getElementById(id); if (el) data[id] = el.value; }); const selectDb = document.getElementById('selectDb')?.value || 'default'; localStorage.setItem(`dh_bienban_data_${selectDb}`, JSON.stringify(data)); // Sync sign names const signThuKy = document.getElementById('bbSignThuKy'); const bbThuKy = document.getElementById('bbThuKy'); if (signThuKy && bbThuKy) signThuKy.innerText = bbThuKy.value || 'BS. Trực tổng hợp'; const signChuTri = document.getElementById('bbSignChuTri'); const bbChuTri = document.getElementById('bbChuTri'); if (signChuTri && bbChuTri) signChuTri.innerText = bbChuTri.value || 'BS.CKII - Ban Giám Đốc'; showToast('Đã lưu biên bản giao ban thành công!'); } function loadBienBan() { const selectDb = document.getElementById('selectDb')?.value || 'default'; const raw = localStorage.getItem(`dh_bienban_data_${selectDb}`); if (!raw) return; try { const data = JSON.parse(raw); Object.keys(data).forEach(id => { const el = document.getElementById(id); if (el) el.value = data[id]; }); const signThuKy = document.getElementById('bbSignThuKy'); const bbThuKy = document.getElementById('bbThuKy'); if (signThuKy && bbThuKy && bbThuKy.value) signThuKy.innerText = bbThuKy.value; const signChuTri = document.getElementById('bbSignChuTri'); const bbChuTri = document.getElementById('bbChuTri'); if (signChuTri && bbChuTri && bbChuTri.value) signChuTri.innerText = bbChuTri.value; } catch (e) { console.warn('Lỗi đọc biên bản lưu trữ:', e); } } function toggleLockBienBan() { const role = document.getElementById('selectBienBanRole')?.value; if (role !== 'cap3') { alert('Chỉ Ban Giám Đốc / Chủ trì giao ban (Cấp 3) mới có thẩm quyền Khóa hoặc Mở khóa biên bản!'); return; } isBienBanLocked = !isBienBanLocked; localStorage.setItem('dh_giaoban_bb_locked', isBienBanLocked ? 'true' : 'false'); applyBienBanPermissions(); if (isBienBanLocked) { saveBienBan(); showToast('Biên bản đã được KHÓA và chuyển sang chế độ CHỈ XEM.'); } else { showToast('Biên bản đã được MỞ KHÓA để tiếp tục hiệu chỉnh.'); } }