feat: Tích hợp Bản quyền License AES-256 (OTH.Adapter), Biên bản giao ban A4, Biểu đồ tròn và Tài liệu triển khai demo

This commit is contained in:
ongtrieuhau
2026-10-27 02:04:23 -07:00
parent 6d54ca20f6
commit 358cfbfde1
13 changed files with 2149 additions and 32 deletions
+767 -2
View File
@@ -3,6 +3,8 @@ const API_BASE = window.location.pathname.startsWith('/web-giao-ban-go') ? '/web
let chartNgoaiTruInstance = null;
let chartNoiTruInstance = null;
let currentChartTypeNgoaiTru = 'khoa';
let currentChartTypeNoiTru = 'noitru-col';
let isBienBanLocked = false;
let rawNgoaiTruData = null;
let rawNoiTruData = null;
@@ -17,6 +19,8 @@ document.addEventListener('DOMContentLoaded', async () => {
initTabs();
initEvents();
initIssueFeatures();
initBienBan();
await checkLicenseStatus();
await loadDatabases();
loadData();
loadIssues(false);
@@ -81,9 +85,38 @@ 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 = '<i class="fa-solid fa-lock-open text-emerald-500 text-[10px]"></i> Mở khóa';
if (lockStatusTo) lockStatusTo.innerHTML = '<i class="fa-solid fa-lock-open text-emerald-500 text-[10px]"></i> 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 = '<i class="fa-solid fa-lock text-slate-400 text-[10px]"></i> Cố định';
if (lockStatusTo) lockStatusTo.innerHTML = '<i class="fa-solid fa-lock text-slate-400 text-[10px]"></i> Cố định';
}
}
quickPreset.addEventListener('change', () => {
const val = quickPreset.value;
updateLockState();
let f = '', t = '';
switch (val) {
@@ -123,13 +156,94 @@ function initPresets() {
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
@@ -203,6 +317,28 @@ function initEvents() {
});
});
// 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);
@@ -341,6 +477,12 @@ async function loadData() {
if (body) body.innerHTML = `<tr><td colspan="9" class="text-center py-6 text-rose-500 font-medium"><i class="fa-solid fa-triangle-exclamation mr-1"></i> Không tải được danh sách theo dõi</td></tr>`;
}
// 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}`);
@@ -357,6 +499,54 @@ async function loadData() {
}
}
// 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 = `<i class="fa-solid fa-triangle-exclamation mr-1.5"></i> ${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 = `<i class="fa-solid fa-circle-check text-emerald-300"></i> ${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 = `<i class="fa-solid fa-triangle-exclamation text-rose-300"></i> 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);
@@ -420,11 +610,16 @@ function renderNgoaiTru() {
if (!cur.khoa || cur.khoa.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="text-center py-8 text-slate-400">Không có dữ liệu khám chữa bệnh trong khoảng thời gian này</td></tr>';
} 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 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');
@@ -532,6 +727,106 @@ function renderChartNgoaiTru() {
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 = [];
@@ -624,6 +919,7 @@ function renderNoiTru() {
if (khoaList.length === 0) {
tbody.innerHTML = '<tr><td colspan="12" class="text-center py-8 text-slate-400">Không có dữ liệu nội trú trong khoảng thời gian này</td></tr>';
} 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);
@@ -640,6 +936,11 @@ function renderNoiTru() {
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 += `
<tr class="hover:bg-slate-50 transition-colors">
<td class="py-2 px-3 font-mono text-slate-500">${row.madv}</td>
@@ -690,7 +991,7 @@ function renderNoiTru() {
renderChartNoiTru();
}
// Render Chart Nội Trú
// Render Chart Nội Trú (ISS-20261027-001)
function renderChartNoiTru() {
if (!rawNoiTruData) return;
const canvas = document.getElementById('chartNoiTru');
@@ -698,6 +999,7 @@ function renderChartNoiTru() {
const ctx = canvas.getContext('2d');
const cur = rawNoiTruData.current || {};
const comp = rawNoiTruData.compare;
const subTitle = document.getElementById('subTitleChartNoiTru');
if (chartNoiTruInstance) {
chartNoiTruInstance.destroy();
@@ -705,6 +1007,159 @@ function renderChartNoiTru() {
}
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));
@@ -1542,3 +1997,313 @@ function exportIssuesExcel() {
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 = '<i class="fa-solid fa-lock text-rose-600"></i> Đã khóa biên bản (Chỉ xem)';
if (btnLock) btnLock.innerHTML = '<i class="fa-solid fa-lock-open mr-1"></i> 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 = '<i class="fa-solid fa-lock-open text-emerald-600"></i> Đang mở soạn thảo';
if (btnLock) btnLock.innerHTML = '<i class="fa-solid fa-lock mr-1"></i> 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.');
}
}