// 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 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(); 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'); quickPreset.addEventListener('change', () => { const val = quickPreset.value; 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; default: return; } dateFrom.value = f; dateTo.value = t; loadData(); }); } // 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(); }); }); // 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`; } 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); } } // 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 { cur.khoa.forEach(row => { const k1 = parseInt(row.tong_kham || 0, 10); let compCols = ''; if (isCompare) { const k2 = parseInt(compMap[row.madv]?.tong_kham || 0, 10); 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 : []; 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 { 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; 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ú 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; if (chartNoiTruInstance) { chartNoiTruInstance.destroy(); chartNoiTruInstance = null; } const khoaList = Array.isArray(cur.khoa) ? cur.khoa : []; 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!'); }