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:
@@ -51,6 +51,10 @@
|
||||
<i class="fa-solid fa-terminal"></i>
|
||||
<span>3. Nhật Ký Hoạt Động (Live Logs)</span>
|
||||
</button>
|
||||
<button class="admin-tab px-5 py-2.5 text-sm font-bold border-b-2 border-transparent text-slate-500 hover:text-slate-800 flex items-center gap-2 transition-all" data-tab="tab-license">
|
||||
<i class="fa-solid fa-shield-halved"></i>
|
||||
<span>4. Quản Lý Bản Quyền (License)</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -332,6 +336,99 @@
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ========================================== -->
|
||||
<!-- TAB 4: QUẢN LÝ BẢN QUYỀN (LICENSE) -->
|
||||
<!-- ========================================== -->
|
||||
<div id="tab-license" class="admin-content hidden space-y-6">
|
||||
|
||||
<!-- License Status Overview -->
|
||||
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden">
|
||||
<div class="px-5 py-4 bg-slate-50 border-b border-slate-200 flex items-center justify-between">
|
||||
<h2 class="text-sm font-bold text-slate-800 flex items-center gap-2">
|
||||
<i class="fa-solid fa-certificate text-amber-500"></i>
|
||||
<span>Thông Tin Bản Quyền Web Giao Ban (DH-HIS)</span>
|
||||
</h2>
|
||||
<span id="licenseStatusBadge" class="text-xs px-2.5 py-1 rounded-full font-bold bg-slate-100 text-slate-600">Đang kiểm tra...</span>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
|
||||
<div class="bg-slate-50 p-4 rounded-xl border border-slate-200">
|
||||
<span class="text-xs text-slate-500 block font-medium mb-1">Tên Bệnh Viện / Đơn Vị:</span>
|
||||
<span id="licTenBV" class="text-sm font-bold text-slate-800 block">--</span>
|
||||
<span class="text-[11px] text-slate-400 font-mono mt-0.5 block">Mã BVBH: <b id="licMabvbh" class="text-indigo-600">--</b></span>
|
||||
</div>
|
||||
|
||||
<div class="bg-slate-50 p-4 rounded-xl border border-slate-200">
|
||||
<span class="text-xs text-slate-500 block font-medium mb-1">Thời Hạn Sử Dụng:</span>
|
||||
<span id="licDateExpires" class="text-sm font-bold text-slate-800 font-mono block">--</span>
|
||||
<span id="licRemainingDays" class="text-[11px] text-slate-500 font-semibold mt-0.5 block">--</span>
|
||||
</div>
|
||||
|
||||
<div class="bg-slate-50 p-4 rounded-xl border border-slate-200">
|
||||
<span class="text-xs text-slate-500 block font-medium mb-1">Chức Năng Được Cấp:</span>
|
||||
<span id="licTitle" class="text-sm font-bold text-slate-800 block">Sử dụng Web giao ban</span>
|
||||
<span class="text-[11px] text-slate-400 font-mono mt-0.5 block">Key: <b id="licKey" class="text-slate-600">web_giao_ban</b></span>
|
||||
</div>
|
||||
|
||||
<div class="bg-slate-50 p-4 rounded-xl border border-slate-200">
|
||||
<span class="text-xs text-slate-500 block font-medium mb-1">Đối Tác & Ghi Chú:</span>
|
||||
<span id="licDoiTac" class="text-sm font-bold text-slate-800 block">--</span>
|
||||
<span id="licGhiChu" class="text-[11px] text-slate-500 mt-0.5 block italic">--</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="licenseAlertBox" class="mt-4 p-3.5 rounded-xl border text-xs flex items-center gap-2.5 bg-blue-50 border-blue-200 text-blue-800">
|
||||
<i class="fa-solid fa-circle-info text-base"></i>
|
||||
<span id="licenseAlertMsg">Đang tải trạng thái kích hoạt bản quyền...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- License Activation Form -->
|
||||
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden">
|
||||
<div class="px-5 py-4 bg-slate-50 border-b border-slate-200 flex items-center justify-between">
|
||||
<h2 class="text-sm font-bold text-slate-800 flex items-center gap-2">
|
||||
<i class="fa-solid fa-key text-indigo-600"></i>
|
||||
<span>Nhập & Kích Hoạt Key Bản Quyền</span>
|
||||
</h2>
|
||||
<span class="text-xs text-slate-500 font-medium">Quy tắc chuẩn OTH.Adapter PrivateCode</span>
|
||||
</div>
|
||||
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-amber-50 border border-amber-200 rounded-xl p-3.5 text-xs text-amber-800 flex items-start gap-2.5">
|
||||
<i class="fa-solid fa-lightbulb text-amber-600 text-base mt-0.5"></i>
|
||||
<div class="leading-relaxed">
|
||||
<b>Hướng dẫn:</b> Quý khách chỉ dán chuỗi mã hóa bản quyền (phần ciphertext bắt đầu bằng <code class="bg-amber-100 px-1 py-0.5 rounded font-mono font-bold">U2FsdGVkX1...</code>). Hệ thống sẽ tự động giải mã bằng thuật toán OpenSSL AES-256-CBC, xác thực mã đơn vị, chức năng và kiểm tra thời hạn sử dụng trước khi kích hoạt.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block font-semibold text-slate-700 text-xs mb-1.5 flex items-center justify-between">
|
||||
<span>Chuỗi Key Bản Quyền (DECODE Token):</span>
|
||||
<span class="text-slate-400 font-normal">Định dạng Base64 Salted__</span>
|
||||
</label>
|
||||
<textarea id="txtLicenseKey" rows="4" class="w-full rounded-xl border border-slate-300 p-3 bg-slate-50 font-mono text-xs outline-none focus:ring-2 focus:ring-indigo-500 text-slate-700 leading-relaxed resize-y" placeholder="Dán chuỗi token bản quyền (bắt đầu bằng U2FsdGVkX1...) vào đây..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between pt-2">
|
||||
<button id="btnPasteSampleKey" type="button" class="text-xs text-indigo-600 hover:text-indigo-800 font-semibold flex items-center gap-1.5 py-2">
|
||||
<i class="fa-solid fa-clipboard-check"></i>
|
||||
<span>Dán Key Mẫu (Trà Cú)</span>
|
||||
</button>
|
||||
|
||||
<button id="btnActivateLicense" type="button" class="bg-gradient-to-r from-indigo-600 to-blue-600 hover:from-indigo-700 hover:to-blue-700 text-white text-xs font-bold px-6 py-2.5 rounded-xl shadow-md flex items-center gap-2 transition-all">
|
||||
<i class="fa-solid fa-check-double"></i>
|
||||
<span>Kích Hoạt Key Bản Quyền</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- TOAST -->
|
||||
|
||||
+117
@@ -16,7 +16,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
checkUpdate();
|
||||
} else if (hash === '#tab-logs') {
|
||||
switchTab('tab-logs');
|
||||
} else if (hash === '#tab-license') {
|
||||
switchTab('tab-license');
|
||||
}
|
||||
|
||||
initLicenseManager();
|
||||
});
|
||||
|
||||
function showToast(msg) {
|
||||
@@ -446,3 +450,116 @@ function escapeHtml(str) {
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// LICENSE MANAGEMENT
|
||||
// -------------------------------------------------------------------
|
||||
const SAMPLE_LICENSE_KEY = "U2FsdGVkX18Zsq52FPbB7FS1QM7CwWh+t1Esi96gUMFILJD0vZXM69uQBC4sP7TjLVOiJPZt0KIq17iql+hXehLCVSbCEhwKKm+zNab+u2Swah63A52hz0KMJXddLRZ6vanmP2goLTzY7749HsFVRDq+3bdbi8fdUZCpAsSTEiqC2cRTpF5WVh0SHt1wX/i42YSQgOYPeXQtg+lRx/URk4p7lYSVQOYWXglp99kO+Va1ADoNcTgrfz3aaAxAkj0S";
|
||||
|
||||
function initLicenseManager() {
|
||||
loadLicenseStatus();
|
||||
|
||||
const btnPaste = document.getElementById('btnPasteSampleKey');
|
||||
if (btnPaste) {
|
||||
btnPaste.addEventListener('click', () => {
|
||||
const txt = document.getElementById('txtLicenseKey');
|
||||
if (txt) {
|
||||
txt.value = SAMPLE_LICENSE_KEY;
|
||||
showToast('Đã dán mã bản quyền mẫu');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const btnActivate = document.getElementById('btnActivateLicense');
|
||||
if (btnActivate) {
|
||||
btnActivate.addEventListener('click', activateLicense);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLicenseStatus() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/license/status`);
|
||||
const json = await res.json();
|
||||
if (json.success && json.data) {
|
||||
renderLicenseInfo(json.data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Lỗi lấy thông tin bản quyền:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function renderLicenseInfo(lic) {
|
||||
const badge = document.getElementById('licenseStatusBadge');
|
||||
const alertBox = document.getElementById('licenseAlertBox');
|
||||
const alertMsg = document.getElementById('licenseAlertMsg');
|
||||
|
||||
document.getElementById('licTenBV').innerText = lic.TenBV || 'Chưa xác định';
|
||||
document.getElementById('licMabvbh').innerText = lic.mabvbh || '--';
|
||||
document.getElementById('licDateExpires').innerText = lic.DateExpires || '--';
|
||||
document.getElementById('licTitle').innerText = lic.title || 'Sử dụng Web giao ban';
|
||||
document.getElementById('licKey').innerText = lic.key || 'web_giao_ban';
|
||||
document.getElementById('licDoiTac').innerText = lic['Đối tác'] || '--';
|
||||
document.getElementById('licGhiChu').innerText = lic['Ghi chú'] || 'Không có';
|
||||
|
||||
if (lic.is_valid) {
|
||||
badge.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.5';
|
||||
badge.innerHTML = '<i class="fa-solid fa-circle-check text-emerald-600"></i> Hợp Lệ';
|
||||
|
||||
document.getElementById('licRemainingDays').innerHTML = `<span class="text-emerald-600 font-bold"><i class="fa-solid fa-clock"></i> Còn ${lic.remaining_days} ngày</span>`;
|
||||
|
||||
alertBox.className = 'mt-4 p-3.5 rounded-xl border text-xs flex items-center gap-2.5 bg-emerald-50 border-emerald-200 text-emerald-800';
|
||||
alertBox.innerHTML = `<i class="fa-solid fa-circle-check text-emerald-600 text-base"></i><span>${escapeHtml(lic.status_message || 'Bản quyền đang hoạt động bình thường.')}</span>`;
|
||||
} else {
|
||||
badge.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.5';
|
||||
badge.innerHTML = '<i class="fa-solid fa-triangle-exclamation text-rose-600"></i> Chưa Hợp Lệ';
|
||||
|
||||
document.getElementById('licRemainingDays').innerHTML = `<span class="text-rose-600 font-bold"><i class="fa-solid fa-triangle-exclamation"></i> Không khả dụng</span>`;
|
||||
|
||||
alertBox.className = 'mt-4 p-3.5 rounded-xl border text-xs flex items-center gap-2.5 bg-rose-50 border-rose-200 text-rose-800';
|
||||
alertBox.innerHTML = `<i class="fa-solid fa-circle-xmark text-rose-600 text-base"></i><span>${escapeHtml(lic.status_message || 'Chưa cập nhật key bản quyền hợp lệ.')}</span>`;
|
||||
}
|
||||
|
||||
// Pre-fill textarea if key exists and textarea is empty
|
||||
const txt = document.getElementById('txtLicenseKey');
|
||||
if (txt && !txt.value && lic.raw_encrypted) {
|
||||
txt.value = lic.raw_encrypted;
|
||||
}
|
||||
}
|
||||
|
||||
async function activateLicense() {
|
||||
const txt = document.getElementById('txtLicenseKey');
|
||||
if (!txt) return;
|
||||
|
||||
const keyVal = txt.value.trim();
|
||||
if (!keyVal) {
|
||||
alert('Vui lòng nhập chuỗi key bản quyền (bắt đầu bằng U2FsdGVkX1...)');
|
||||
txt.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('btnActivateLicense');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin mr-1"></i> Đang xác thực...';
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/license/activate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ key: keyVal })
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json.success && json.data) {
|
||||
showToast(json.message);
|
||||
renderLicenseInfo(json.data);
|
||||
alert('Kích hoạt thành công!\n\n' + json.message + '\nĐơn vị: ' + json.data.TenBV + '\nHạn sử dụng: ' + json.data.DateExpires);
|
||||
} else {
|
||||
alert('Kích hoạt không thành công: ' + (json.message || 'Lỗi không xác định'));
|
||||
loadLicenseStatus();
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Lỗi kết nối máy chủ: ' + err.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fa-solid fa-check-double"></i> <span>Kích Hoạt Key Bản Quyền</span>';
|
||||
}
|
||||
}
|
||||
|
||||
+767
-2
@@ -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.');
|
||||
}
|
||||
}
|
||||
|
||||
+263
-27
@@ -25,10 +25,13 @@
|
||||
<div>
|
||||
<h1 class="text-xl font-bold tracking-tight uppercase flex items-center gap-2 flex-wrap">
|
||||
Phần Mềm Quản Lý Giao Ban Bệnh Viện
|
||||
<span class="text-xs bg-cyan-500/30 text-cyan-200 border border-cyan-400/40 px-2 py-0.5 rounded-full font-normal">DH-HIS Web</span>
|
||||
<span class="text-xs bg-cyan-500/30 text-cyan-200 border border-cyan-400/40 px-2 py-0.5 rounded-full font-normal">DH-HIS Web (Go)</span>
|
||||
<span id="currentDbBadge" class="text-xs bg-emerald-500/20 text-emerald-300 border border-emerald-400/30 px-2.5 py-0.5 rounded-full font-medium flex items-center gap-1.5">
|
||||
<span class="w-2 h-2 rounded-full bg-emerald-400 animate-pulse"></span> BVĐK Ô Môn
|
||||
</span>
|
||||
<span id="headerLicenseBadge" class="text-[11px] px-2 py-0.5 rounded-full font-bold bg-amber-500/30 text-amber-200 border border-amber-400/30 hidden">
|
||||
<i class="fa-solid fa-shield-halved"></i> <span id="headerLicenseText">Kiểm tra bản quyền</span>
|
||||
</span>
|
||||
</h1>
|
||||
<p class="text-xs text-sky-200">Báo cáo Giao ban Ngoại trú & Nội trú hằng ngày - Phân tích đối chiếu đa chu kỳ</p>
|
||||
</div>
|
||||
@@ -58,6 +61,19 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- LICENSE WARNING BANNER -->
|
||||
<div id="licenseBanner" class="hidden bg-gradient-to-r from-amber-600 to-rose-600 text-white shadow-md">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 py-2.5 flex flex-wrap items-center justify-between gap-3 text-xs">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fa-solid fa-triangle-exclamation text-amber-200 text-base"></i>
|
||||
<span id="licenseBannerText" class="font-bold">Hệ thống chưa cập nhật key bản quyền tính năng Web Giao Ban</span>
|
||||
</div>
|
||||
<a href="admin.html#tab-license" class="bg-white text-rose-700 font-bold px-3 py-1 rounded-lg shadow-sm hover:bg-rose-50 transition-all flex items-center gap-1.5">
|
||||
<i class="fa-solid fa-key"></i> Kích Hoạt Bản Quyền Ngay
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CONTROL PANEL / FILTER TOOLBAR -->
|
||||
<section class="max-w-7xl mx-auto px-4 sm:px-6 mt-4">
|
||||
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-4 transition-all">
|
||||
@@ -85,12 +101,18 @@
|
||||
<!-- Kỳ chính: Từ ngày - Đến ngày -->
|
||||
<div class="md:col-span-4 flex items-center gap-2">
|
||||
<div class="w-1/2">
|
||||
<label class="block text-xs font-semibold text-slate-600 mb-1">Từ ngày:</label>
|
||||
<input type="date" id="dateFrom" class="w-full text-sm rounded-lg border border-slate-300 px-2.5 py-1.5 bg-slate-50 outline-none focus:ring-2 focus:ring-blue-500" value="2026-03-01">
|
||||
<label class="block text-xs font-semibold text-slate-600 mb-1 flex items-center justify-between">
|
||||
<span>Từ ngày:</span>
|
||||
<span id="lockStatusFrom" class="text-[10px] text-slate-400 font-normal"><i class="fa-solid fa-lock text-[10px]"></i> Cố định</span>
|
||||
</label>
|
||||
<input type="date" id="dateFrom" class="w-full text-sm rounded-lg border border-slate-300 px-2.5 py-1.5 bg-slate-100 text-slate-500 cursor-not-allowed outline-none font-medium" value="2026-03-01" readonly>
|
||||
</div>
|
||||
<div class="w-1/2">
|
||||
<label class="block text-xs font-semibold text-slate-600 mb-1">Đến ngày:</label>
|
||||
<input type="date" id="dateTo" class="w-full text-sm rounded-lg border border-slate-300 px-2.5 py-1.5 bg-slate-50 outline-none focus:ring-2 focus:ring-blue-500" value="2026-03-31">
|
||||
<label class="block text-xs font-semibold text-slate-600 mb-1 flex items-center justify-between">
|
||||
<span>Đến ngày:</span>
|
||||
<span id="lockStatusTo" class="text-[10px] text-slate-400 font-normal"><i class="fa-solid fa-lock text-[10px]"></i> Cố định</span>
|
||||
</label>
|
||||
<input type="date" id="dateTo" class="w-full text-sm rounded-lg border border-slate-300 px-2.5 py-1.5 bg-slate-100 text-slate-500 cursor-not-allowed outline-none font-medium" value="2026-03-31" readonly>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -123,19 +145,35 @@
|
||||
|
||||
<!-- KỲ SO SÁNH (Hiển thị khi bật enableCompare) -->
|
||||
<div id="compareContainer" class="hidden mt-3 pt-3 border-t border-dashed border-indigo-200 bg-indigo-50/50 p-3 rounded-lg">
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
<span class="text-xs font-bold text-indigo-900 flex items-center gap-1.5">
|
||||
<i class="fa-solid fa-code-compare text-indigo-600"></i> Kỳ so sánh (Ví dụ: Tháng 01/2026):
|
||||
</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-xs text-slate-600 font-medium">Từ:</label>
|
||||
<input type="date" id="compareFrom" class="text-sm rounded border border-indigo-300 px-2 py-1 bg-white outline-none" value="2026-01-01">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<span class="text-xs font-bold text-indigo-900 flex items-center gap-1.5">
|
||||
<i class="fa-solid fa-code-compare text-indigo-600"></i> Kỳ so sánh 2:
|
||||
</span>
|
||||
<!-- Chọn nhanh mốc so sánh (ISS-20261027-003) -->
|
||||
<select id="quickComparePreset" class="text-xs rounded-lg border border-indigo-300 px-2.5 py-1.5 bg-white outline-none font-medium text-slate-700">
|
||||
<option value="month-01-2026" selected>Tháng 01/2026 (Mặc định)</option>
|
||||
<option value="month-02-2026">Tháng 02/2026</option>
|
||||
<option value="prev-period">Kỳ liền trước (khoảng thời gian tương đương)</option>
|
||||
<option value="prev-month">Cùng kỳ tháng trước</option>
|
||||
<option value="prev-year">Cùng kỳ năm trước</option>
|
||||
<option value="custom">Tùy chọn khoảng ngày...</option>
|
||||
</select>
|
||||
<div class="flex items-center gap-1.5 text-xs">
|
||||
<label class="text-slate-600 font-medium">Từ:</label>
|
||||
<input type="date" id="compareFrom" class="text-xs rounded border border-indigo-300 px-2 py-1 bg-slate-100 text-slate-500 cursor-not-allowed outline-none font-medium" value="2026-01-01" readonly>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 text-xs">
|
||||
<label class="text-slate-600 font-medium">Đến:</label>
|
||||
<input type="date" id="compareTo" class="text-xs rounded border border-indigo-300 px-2 py-1 bg-slate-100 text-slate-500 cursor-not-allowed outline-none font-medium" value="2026-01-31" readonly>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-xs text-slate-600 font-medium">Đến:</label>
|
||||
<input type="date" id="compareTo" class="text-sm rounded border border-indigo-300 px-2 py-1 bg-white outline-none" value="2026-01-31">
|
||||
</div>
|
||||
<span class="text-xs text-slate-500 italic">* Hệ thống sẽ đối chiếu song song và vẽ biểu đồ cột tăng giảm tự động.</span>
|
||||
|
||||
<!-- Tùy chọn ẩn dòng = 0 (ISS-20261027-004) -->
|
||||
<label class="inline-flex items-center gap-1.5 text-xs font-semibold text-indigo-900 cursor-pointer bg-white border border-indigo-200 px-2.5 py-1 rounded-lg hover:bg-indigo-50 transition-colors shadow-2xs">
|
||||
<input type="checkbox" id="chkHideZeroRows" class="rounded text-indigo-600 focus:ring-indigo-500">
|
||||
<span>Ẩn dòng không có dữ liệu (2 kỳ = 0)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -152,9 +190,13 @@
|
||||
<i class="fa-solid fa-bed-pulse"></i>
|
||||
<span>2. Báo Cáo Giao Ban Nội Trú</span>
|
||||
</button>
|
||||
<button class="tab-btn px-4 py-2.5 text-sm font-bold border-b-2 border-transparent text-slate-500 hover:text-slate-800 flex items-center gap-2 transition-all" data-tab="tab-bienban">
|
||||
<i class="fa-solid fa-file-signature text-indigo-600"></i>
|
||||
<span>3. Biên Bản Giao Ban</span>
|
||||
</button>
|
||||
<button class="tab-btn px-4 py-2.5 text-sm font-bold border-b-2 border-transparent text-slate-500 hover:text-slate-800 flex items-center gap-2 transition-all" data-tab="tab-issues">
|
||||
<i class="fa-solid fa-clipboard-list text-rose-500"></i>
|
||||
<span>3. Quản Lý Issue & Báo Lỗi</span>
|
||||
<span>4. Quản Lý Issue & Báo Lỗi</span>
|
||||
<span id="badgeIssueCount" class="ml-1 px-1.5 py-0.2 text-[10px] font-bold rounded-full bg-rose-100 text-rose-700 border border-rose-300">0</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -359,8 +401,10 @@
|
||||
<i class="fa-solid fa-chart-column text-indigo-600"></i>
|
||||
<span>Biểu Đồ Cột So Sánh Số Liệu</span>
|
||||
</h2>
|
||||
<div class="flex gap-1 text-xs">
|
||||
<button class="chart-type-btn active px-2.5 py-1 rounded bg-blue-100 text-blue-700 font-semibold" data-chart="khoa">Theo Khoa</button>
|
||||
<div class="flex gap-1 text-xs flex-wrap">
|
||||
<button class="chart-type-btn active px-2.5 py-1 rounded bg-blue-100 text-blue-700 font-semibold" data-chart="khoa">Cột Khoa</button>
|
||||
<button class="chart-type-btn px-2.5 py-1 rounded bg-slate-100 text-slate-600 font-medium" data-chart="khoa-pie"><i class="fa-solid fa-chart-pie mr-1"></i>% Tròn Khoa</button>
|
||||
<button class="chart-type-btn px-2.5 py-1 rounded bg-slate-100 text-slate-600 font-medium" data-chart="bhyt-group"><i class="fa-solid fa-chart-bar mr-1"></i>BHYT / Thu phí</button>
|
||||
<button class="chart-type-btn px-2.5 py-1 rounded bg-slate-100 text-slate-600 font-medium" data-chart="cls">Theo CLS</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -475,12 +519,20 @@
|
||||
|
||||
<!-- BIỂU ĐỒ CỘT SO SÁNH NỘI TRÚ -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-4">
|
||||
<div class="flex items-center justify-between border-b border-slate-100 pb-2 mb-3">
|
||||
<h2 class="text-sm font-bold text-slate-800 flex items-center gap-2">
|
||||
<i class="fa-solid fa-chart-column text-indigo-600"></i>
|
||||
<span>Biểu Đồ Lưu Lượng Bệnh Nhân & So Sánh Đa Chu Kỳ</span>
|
||||
</h2>
|
||||
<span class="text-xs text-slate-500">So sánh số bệnh mới nhập viện giữa các khoa</span>
|
||||
<div class="flex flex-wrap items-center justify-between border-b border-slate-100 pb-2 mb-3 gap-2">
|
||||
<div>
|
||||
<h2 class="text-sm font-bold text-slate-800 flex items-center gap-2">
|
||||
<i class="fa-solid fa-chart-column text-indigo-600"></i>
|
||||
<span>Biểu Đồ Phân Tích Bệnh Nhân Nội Trú</span>
|
||||
</h2>
|
||||
<span class="text-xs text-slate-500" id="subTitleChartNoiTru">Phân bố lưu lượng và cơ cấu bệnh nhân điều trị</span>
|
||||
</div>
|
||||
<div class="flex gap-1 text-xs flex-wrap">
|
||||
<button class="chart-nt-btn active px-2.5 py-1 rounded bg-blue-100 text-blue-700 font-semibold" data-ntchart="noitru-col">Cột Vào Viện</button>
|
||||
<button class="chart-nt-btn px-2.5 py-1 rounded bg-slate-100 text-slate-600 font-medium" data-ntchart="noitru-pie-tong"><i class="fa-solid fa-chart-pie mr-1"></i>% Tròn 4 Tiêu Chí</button>
|
||||
<button class="chart-nt-btn px-2.5 py-1 rounded bg-slate-100 text-slate-600 font-medium" data-ntchart="noitru-pie-khoa"><i class="fa-solid fa-chart-pie mr-1"></i>% Hiện Còn Theo Khoa</button>
|
||||
<button class="chart-nt-btn px-2.5 py-1 rounded bg-slate-100 text-slate-600 font-medium" data-ntchart="noitru-group">Cột Nhóm Tổng Hợp</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative w-full h-80">
|
||||
<canvas id="chartNoiTru"></canvas>
|
||||
@@ -574,7 +626,159 @@
|
||||
</div>
|
||||
|
||||
<!-- ========================================== -->
|
||||
<!-- TAB 3: QUẢN LÝ ISSUE & BÁO LỖI -->
|
||||
<!-- TAB 3: BIÊN BẢN GIAO BAN (MẪU A4 & PHÂN QUYỀN 3 CẤP) -->
|
||||
<!-- ========================================== -->
|
||||
<div id="tab-bienban" class="tab-content hidden space-y-4">
|
||||
|
||||
<!-- Top Action Toolbar -->
|
||||
<div class="bg-white p-3.5 rounded-xl border border-slate-200 shadow-sm flex flex-wrap items-center justify-between gap-3 print:hidden">
|
||||
|
||||
<!-- Role Switcher -->
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs font-bold text-slate-700 flex items-center gap-1.5">
|
||||
<i class="fa-solid fa-user-shield text-indigo-600"></i> Vai trò đăng nhập:
|
||||
</span>
|
||||
<select id="selectBienBanRole" class="text-xs font-bold rounded-lg border border-indigo-300 px-3 py-1.5 bg-indigo-50 text-indigo-900 outline-none">
|
||||
<option value="cap1">Cấp 1: Bác sĩ / Nhân viên (Chỉ xem)</option>
|
||||
<option value="cap2" selected>Cấp 2: Thư ký giao ban / Trưởng khoa (Ghi chép)</option>
|
||||
<option value="cap3">Cấp 3: Chủ trì / Ban Giám đốc (Kết luận & Khóa)</option>
|
||||
</select>
|
||||
<span id="badgeLockStatus" class="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">
|
||||
<i class="fa-solid fa-lock-open text-emerald-600"></i> Đang mở soạn thảo
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<button id="btnSync24hData" class="bg-blue-50 hover:bg-blue-100 text-blue-700 border border-blue-300 text-xs font-semibold px-3 py-1.5 rounded-lg flex items-center gap-1.5 transition-all" title="Lấy nhanh số liệu KCB 24h điền vào biên bản">
|
||||
<i class="fa-solid fa-arrows-rotate text-blue-600"></i> Đồng bộ số liệu 24h
|
||||
</button>
|
||||
<button id="btnSaveBienBan" class="bg-indigo-600 hover:bg-indigo-700 text-white text-xs font-semibold px-3 py-1.5 rounded-lg shadow-sm flex items-center gap-1.5 transition-all">
|
||||
<i class="fa-solid fa-floppy-disk"></i> Lưu biên bản
|
||||
</button>
|
||||
<button id="btnToggleLockBienBan" class="bg-amber-600 hover:bg-amber-700 text-white text-xs font-semibold px-3 py-1.5 rounded-lg shadow-sm flex items-center gap-1.5 transition-all">
|
||||
<i class="fa-solid fa-lock"></i> Khóa biên bản
|
||||
</button>
|
||||
<button id="btnPrintBienBan" class="bg-emerald-600 hover:bg-emerald-700 text-white text-xs font-semibold px-3.5 py-1.5 rounded-lg shadow-sm flex items-center gap-1.5 transition-all">
|
||||
<i class="fa-solid fa-print"></i> In Biên Bản A4
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Printable A4 Paper Document Container -->
|
||||
<div id="bienBanA4Sheet" class="bg-white rounded-xl shadow-lg border border-slate-300 max-w-4xl mx-auto p-8 sm:p-12 text-slate-900 font-serif leading-relaxed print:shadow-none print:border-none print:p-0 print:max-w-none">
|
||||
|
||||
<!-- Header Quốc Hiệu & Đơn Vị -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-12 gap-4 pb-4 border-b-2 border-slate-900 items-start">
|
||||
<div class="sm:col-span-5 text-center">
|
||||
<p class="text-xs uppercase font-bold tracking-wider text-slate-800" id="bbTenSoYTe">SỞ Y TẾ THÀNH PHỐ</p>
|
||||
<input type="text" id="bbTenBenhVien" class="text-sm uppercase font-black text-center w-full outline-none border-b border-transparent hover:border-slate-300 focus:border-indigo-500 bg-transparent" value="BỆNH VIỆN ĐA KHOA Ô MÔN">
|
||||
<p class="text-[11px] italic text-slate-600">Số: <input type="text" id="bbSoBienBan" class="w-24 text-center font-mono border-b border-dotted border-slate-400 bg-transparent outline-none" value=".../BB-GB"></p>
|
||||
</div>
|
||||
<div class="sm:col-span-7 text-center">
|
||||
<p class="text-xs uppercase font-bold tracking-wider">CỘNG HÒA XÃ HỘI CHỦ NGHĨA VIỆT NAM</p>
|
||||
<p class="text-xs font-bold underline underline-offset-4 tracking-normal pb-1">Độc lập - Tự do - Hạnh phúc</p>
|
||||
<p class="text-xs italic text-slate-600 mt-2">
|
||||
<input type="text" id="bbNgayThangNam" class="text-center w-64 border-b border-dotted border-slate-400 bg-transparent outline-none italic" value="..., ngày ... tháng ... năm 2026">
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Document Title -->
|
||||
<div class="text-center my-6">
|
||||
<h1 class="text-xl font-black uppercase tracking-wide text-slate-900">BIÊN BẢN HỌP GIAO BAN BỆNH VIỆN</h1>
|
||||
<p class="text-xs italic text-slate-600 mt-1">(V/v Đánh giá công tác khám chữa bệnh 24h & Triển khai nhiệm vụ trọng tâm)</p>
|
||||
</div>
|
||||
|
||||
<!-- Metadata Section -->
|
||||
<div class="space-y-2 text-xs text-slate-800 bg-slate-50 p-4 rounded-lg border border-slate-200 mb-6 font-sans">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<span class="font-bold">1. Thời gian bắt đầu:</span>
|
||||
<input type="text" id="bbThoiGian" class="border-b border-slate-300 bg-transparent px-1 font-medium outline-none w-48" value="07 giờ 30 phút">
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-bold">2. Địa điểm:</span>
|
||||
<input type="text" id="bbDiaDiem" class="border-b border-slate-300 bg-transparent px-1 font-medium outline-none w-48" value="Hội trường giao ban">
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<span class="font-bold">3. Chủ trì cuộc họp:</span>
|
||||
<input type="text" id="bbChuTri" class="border-b border-slate-300 bg-transparent px-1 font-bold text-indigo-900 outline-none w-48" value="BS.CKII - Giám đốc trực">
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-bold">4. Thư ký ghi biên bản:</span>
|
||||
<input type="text" id="bbThuKy" class="border-b border-slate-300 bg-transparent px-1 font-medium outline-none w-48" value="BS. Trực tổng hợp">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-bold">5. Thành phần tham dự:</span>
|
||||
<input type="text" id="bbThanhPhan" class="w-full border-b border-slate-300 bg-transparent px-1 font-medium outline-none mt-1" value="Ban Giám đốc, Trưởng/Phó các khoa lâm sàng & cận lâm sàng, Phòng Kế hoạch tổng hợp, Điều dưỡng trưởng bệnh viện và các Bác sĩ trực.">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 1: Tình hình khám chữa bệnh 24h qua -->
|
||||
<div class="space-y-3 mb-6">
|
||||
<h2 class="text-sm font-bold uppercase tracking-wider text-slate-900 flex items-center justify-between border-b border-slate-300 pb-1">
|
||||
<span>I. TÌNH HÌNH HOẠT ĐỘNG KHÁM CHỮA BỆNH 24 GIỜ QUA</span>
|
||||
</h2>
|
||||
<textarea id="bbNoiDungKcb" rows="5" class="w-full p-3 rounded-lg border border-slate-300 font-sans text-xs outline-none focus:ring-1 focus:ring-indigo-500 leading-relaxed bg-white" placeholder="Bấm nút 'Đồng bộ số liệu 24h' để nạp tự động hoặc nhập tóm tắt số liệu khám ngoại trú, nội trú hiện còn, nhập viện mới, chuyển viện..."></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Section 2: Báo cáo công tác trực & Chuyên môn khoa phòng -->
|
||||
<div class="space-y-3 mb-6">
|
||||
<h2 class="text-sm font-bold uppercase tracking-wider text-slate-900 border-b border-slate-300 pb-1">
|
||||
II. BÁO CÁO CÔNG TÁC TRỰC & DIỄN BIẾN CHUYÊN MÔN
|
||||
</h2>
|
||||
<textarea id="bbNoiDungChuyenMon" rows="5" class="w-full p-3 rounded-lg border border-slate-300 font-sans text-xs outline-none focus:ring-1 focus:ring-indigo-500 leading-relaxed bg-white" placeholder="- Tình hình cấp cứu, phẫu thuật cấp cứu trong ca trực. - Bệnh nhân nặng, theo dõi đặc biệt (chăm sóc cấp I). - Các trường hợp chuyển viện an toàn, tai biến y khoa (nếu có): Không có diễn biến bất thường."></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Section 3: Công tác Dược, Vật tư & Trang thiết bị y tế -->
|
||||
<div class="space-y-3 mb-6">
|
||||
<h2 class="text-sm font-bold uppercase tracking-wider text-slate-900 border-b border-slate-300 pb-1">
|
||||
III. CÔNG TÁC DƯỢC, VẬT TƯ & TRANG THIẾT BỊ Y TẾ
|
||||
</h2>
|
||||
<textarea id="bbNoiDungDuocTrangThietBi" rows="4" class="w-full p-3 rounded-lg border border-slate-300 font-sans text-xs outline-none focus:ring-1 focus:ring-indigo-500 leading-relaxed bg-white" placeholder="- Cung ứng thuốc thang, dịch truyền, hóa chất xét nghiệm đầy đủ cho các khoa. - Trang thiết bị chẩn đoán hình ảnh (X-quang, Siêu âm, CT) và máy xét nghiệm hoạt động ổn định."></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Section 4: Ý kiến thảo luận & Kiến nghị khoa phòng -->
|
||||
<div class="space-y-3 mb-6">
|
||||
<h2 class="text-sm font-bold uppercase tracking-wider text-slate-900 border-b border-slate-300 pb-1">
|
||||
IV. Ý KIẾN THẢO LUẬN & KIẾN NGHỊ CỦA CÁC KHOA PHÒNG
|
||||
</h2>
|
||||
<textarea id="bbNoiDungYkienKhoa" rows="4" class="w-full p-3 rounded-lg border border-slate-300 font-sans text-xs outline-none focus:ring-1 focus:ring-indigo-500 leading-relaxed bg-white" placeholder="- Khoa Khám bệnh: Đề xuất tăng cường bàn khám vào khung giờ cao điểm 7h30 - 9h00. - Khoa Hồi sức cấp cứu: Đề nghị bảo dưỡng định kỳ hệ thống khí y tế trung tâm."></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Section 5: Kết luận & Chỉ đạo của Ban Giám Đốc -->
|
||||
<div class="space-y-3 mb-8 bg-amber-50/50 p-4 rounded-xl border border-amber-200">
|
||||
<h2 class="text-sm font-bold uppercase tracking-wider text-indigo-950 border-b border-amber-300 pb-1 flex items-center justify-between">
|
||||
<span>V. KẾT LUẬN & CHỈ ĐẠO CỦA BAN GIÁM ĐỐC (CHỦ TRÌ GIAO BAN)</span>
|
||||
<span class="text-[10px] font-sans bg-amber-200/80 text-amber-900 px-2 py-0.5 rounded font-bold">Phân quyền Cấp 3</span>
|
||||
</h2>
|
||||
<textarea id="bbNoiDungKetLuan" rows="5" class="w-full p-3 rounded-lg border border-amber-300 font-sans text-xs outline-none focus:ring-1 focus:ring-amber-500 leading-relaxed bg-white font-medium" placeholder="Chỉ đạo của Ban Giám đốc: 1. Các khoa lâm sàng tiếp tục theo dõi sát người bệnh nặng, hội chẩn kịp thời. 2. Đảm bảo quy chế kê đơn, bảng kê chi phí BHYT và hồ sơ bệnh án chuẩn hóa. 3. Phòng KHTH phối hợp tổ CNTT duy trì ổn định hệ thống phần mềm và cập nhật số liệu."></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Footer Chữ Ký A4 -->
|
||||
<div class="grid grid-cols-2 gap-8 text-center pt-4 font-sans text-xs">
|
||||
<div>
|
||||
<p class="font-bold uppercase tracking-wider text-slate-800">THƯ KÝ GHI BIÊN BẢN</p>
|
||||
<p class="text-[11px] italic text-slate-500 mb-16">(Ký và ghi rõ họ tên)</p>
|
||||
<p id="bbSignThuKy" class="font-bold text-slate-900">BS. Trực tổng hợp</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-bold uppercase tracking-wider text-slate-800">CHỦ TRÌ GIAO BAN</p>
|
||||
<p class="text-[11px] italic text-slate-500 mb-16">(Ký duyệt và ghi rõ họ tên)</p>
|
||||
<p id="bbSignChuTri" class="font-bold text-indigo-950">BS.CKII - Ban Giám Đốc</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ========================================== -->
|
||||
<!-- TAB 4: QUẢN LÝ ISSUE & BÁO LỖI -->
|
||||
<!-- ========================================== -->
|
||||
<div id="tab-issues" class="tab-content hidden space-y-4">
|
||||
|
||||
@@ -896,6 +1100,38 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ======================================================== -->
|
||||
<!-- MODAL 4: CẢNH BÁO CHƯA KÍCH HOẠT BẢN QUYỀN -->
|
||||
<!-- ======================================================== -->
|
||||
<div id="modalLicenseBlock" class="fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm flex items-center justify-center p-4 hidden">
|
||||
<div class="bg-white rounded-2xl shadow-2xl max-w-md w-full overflow-hidden border border-rose-200">
|
||||
<div class="bg-gradient-to-r from-rose-600 to-amber-600 p-5 text-white flex items-center gap-3">
|
||||
<div class="w-12 h-12 rounded-xl bg-white/20 flex items-center justify-center text-2xl">
|
||||
<i class="fa-solid fa-shield-halved"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-base uppercase">Yêu Cầu Bản Quyền Tính Năng</h3>
|
||||
<p class="text-xs text-rose-100">Hệ thống DH-HIS Web Giao Ban</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6 space-y-4 text-xs leading-relaxed text-slate-700">
|
||||
<div class="p-3 bg-rose-50 border border-rose-200 rounded-xl text-rose-800 font-medium" id="modalLicenseBlockMsg">
|
||||
Chưa cập nhật key bản quyền hoặc key đã hết hạn sử dụng. Vui lòng liên hệ bộ phận hỗ trợ kỹ thuật để được cấp mã kích hoạt.
|
||||
</div>
|
||||
<p>Để tiếp tục trích xuất số liệu và sử dụng các phân hệ báo cáo giao ban, vui lòng mở trang Quản Trị và dán chuỗi key bản quyền (phần DECODE token do DH cấp).</p>
|
||||
<div class="flex items-center justify-end gap-2 pt-2 border-t border-slate-200">
|
||||
<button type="button" id="btnCloseLicenseModal" class="px-3 py-2 font-semibold text-slate-600 hover:bg-slate-100 rounded-lg">
|
||||
Đóng
|
||||
</button>
|
||||
<a href="admin.html#tab-license" class="bg-gradient-to-r from-indigo-600 to-blue-600 hover:from-indigo-700 hover:to-blue-700 text-white px-4 py-2 rounded-lg font-bold shadow flex items-center gap-1.5 transition-all">
|
||||
<i class="fa-solid fa-key"></i>
|
||||
<span>Mở Quản Trị Nhập Key</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- NOTIFICATION TOAST -->
|
||||
<div id="toast" class="fixed bottom-4 right-4 z-50 bg-slate-900 text-white text-xs px-4 py-2.5 rounded-lg shadow-xl flex items-center gap-2 transform translate-y-20 opacity-0 transition-all duration-300">
|
||||
<i class="fa-solid fa-circle-check text-emerald-400"></i>
|
||||
|
||||
Reference in New Issue
Block a user