feat: DH-HIS Web Giao Ban (Node.js & Express) complete codebase
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
PORT=3000
|
||||||
|
PG_HOST=192.168.50.79
|
||||||
|
PG_PORT=5437
|
||||||
|
PG_USER=postgres
|
||||||
|
PG_PASSWORD=123
|
||||||
|
PG_DATABASE=an_omon_full_04052026
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules/
|
||||||
|
*.log
|
||||||
|
.app.pid
|
||||||
|
uploads/issues/*
|
||||||
|
!uploads/issues/.gitkeep
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Phần Mềm Quản Lý Giao Ban Hằng Ngày (DH-HIS Web)
|
||||||
|
|
||||||
|
Ứng dụng Node.js + Express + PostgreSQL phục vụ công tác giao ban bệnh viện hằng ngày, hỗ trợ phân tích đối chiếu đa chu kỳ và tương tác nhập liệu ghi chú trực tiếp.
|
||||||
|
|
||||||
|
## 1. Cấu trúc thư mục
|
||||||
|
```
|
||||||
|
dh-giaoban-web/
|
||||||
|
├── server.js # API Server Express (REST APIs cho Ngoại trú, Nội trú, Bảng 3 & 4)
|
||||||
|
├── db.js # Quản lý kết nối PostgreSQL Pool & chuẩn hóa văn bản RTF
|
||||||
|
├── .env # Cấu hình CSDL PostgreSQL DH37 (Host, Port, User, DB)
|
||||||
|
├── package.json # Dependencies: express, pg, cors, dotenv
|
||||||
|
└── public/
|
||||||
|
├── index.html # Giao diện Dashboard giao ban bệnh viện
|
||||||
|
├── app.js # Xử lý lọc kỳ, so sánh đa kỳ, vẽ biểu đồ Chart.js & auto-save ghi chú
|
||||||
|
└── styles.css # Định dạng giao diện & hỗ trợ in ấn (Print CSS)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Cách khởi động ứng dụng
|
||||||
|
```bash
|
||||||
|
cd /home/othau/dh-coding/dh-giaoban-web
|
||||||
|
node server.js
|
||||||
|
```
|
||||||
|
Mở trình duyệt truy cập: **http://localhost:3000** (hoặc `http://<IP_MÁY>:3000`).
|
||||||
|
|
||||||
|
## 3. Các tính năng chính
|
||||||
|
1. **Giao ban Ngoại trú**:
|
||||||
|
- Thống kê lượt khám BHYT, Thu phí, Chuyển viện theo các khoa (Khám bệnh, Ngoại, Nội, CSSKSS, Nhi, YDCT,...).
|
||||||
|
- Thống kê Bệnh án ngoại trú (Mới vs Tái khám) và chuyên khoa YDCT (Điện châm, VLTL, Sắc thuốc, Cấy chỉ).
|
||||||
|
- Tổng hợp số liệu Cận lâm sàng: Xét nghiệm (Huyết học, Sinh hóa, Nước tiểu, Miễn dịch, Đông máu) & CĐHA (Nội soi, Xquang, Siêu âm, CT, ECG).
|
||||||
|
- Top 10 chẩn đoán bệnh ngoại trú theo mã ICD-10.
|
||||||
|
- So sánh số liệu giữa 2 khoảng thời gian không liên tục (Ví dụ: Tháng 1 vs Tháng 3/10) và biểu diễn bằng biểu đồ cột.
|
||||||
|
|
||||||
|
2. **Giao ban Nội trú**:
|
||||||
|
- Tổng hợp lưu lượng bệnh nhân theo các khoa: Bệnh cũ (tồn đầu), Bệnh mới (nhập viện), Xuất viện (Ổn xin về, Nặng xin về, Trốn, Chuyển viện, Tử vong), Bệnh hiện còn, Bệnh theo dõi, Chăm sóc Cấp I.
|
||||||
|
- Biểu đồ cột phân tích lưu lượng và so sánh đa chu kỳ.
|
||||||
|
- **Bảng 3 (Danh sách bệnh nhân Chuyển viện)** & **Bảng 4 (Danh sách bệnh nhân Theo dõi & CS Cấp I)**:
|
||||||
|
- Hiển thị đầy đủ: Họ tên, Địa chỉ, Ngày VV/RV, Khoa, Chẩn đoán chính/phụ, Tóm tắt kết quả CLS.
|
||||||
|
- **Cột Ghi chú thông minh**: Các khoa tự nhập liệu trực tiếp trên lưới; hệ thống tự động lưu vào CSDL ngay khi chuyển ô.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "ISS-20261025-001",
|
||||||
|
"title": "Kiểm tra đối chiếu số liệu tháng 03/2026",
|
||||||
|
"category": "SAI_LECH_SO_LIEU",
|
||||||
|
"module": "NGOAI_TRU",
|
||||||
|
"priority": "CAO",
|
||||||
|
"database": "an_omon_full_04052026",
|
||||||
|
"databaseName": "BVĐK Ô Môn - Cần Thơ",
|
||||||
|
"period": "Từ 2026-03-01 đến 2026-03-31",
|
||||||
|
"reporter": "BS. Trưởng Khoa KHTH",
|
||||||
|
"department": "Phòng Kế Hoạch Tổng Hợp",
|
||||||
|
"description": "Số liệu khám chữa bệnh BHYT tại Khoa Khám Bệnh đối chiếu rất tốt, gửi file test tổng hợp.",
|
||||||
|
"status": "DA_XU_LY",
|
||||||
|
"handlerNotes": "Đã kiểm tra đối chiếu khớp 100% với HIS Ô Môn.",
|
||||||
|
"resolvedBy": "Dev HIS",
|
||||||
|
"resolvedAt": "2026-10-25T10:26:47.059Z",
|
||||||
|
"createdAt": "2026-10-25T10:26:38.259Z",
|
||||||
|
"updatedAt": "2026-10-25T10:26:47.059Z",
|
||||||
|
"attachment": {
|
||||||
|
"filename": "sample_log_1792923998248-734023.txt",
|
||||||
|
"originalname": "sample_log.txt",
|
||||||
|
"size": 32,
|
||||||
|
"mimetype": "text/plain",
|
||||||
|
"url": "/uploads/issues/sample_log_1792923998248-734023.txt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
const { Pool } = require('pg');
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
const pools = {};
|
||||||
|
|
||||||
|
function getPool(dbName) {
|
||||||
|
const database = dbName || process.env.PG_DATABASE || 'an_omon_full_04052026';
|
||||||
|
if (!pools[database]) {
|
||||||
|
pools[database] = new Pool({
|
||||||
|
host: process.env.PG_HOST || '192.168.50.79',
|
||||||
|
port: parseInt(process.env.PG_PORT || '5437', 10),
|
||||||
|
user: process.env.PG_USER || 'postgres',
|
||||||
|
password: process.env.PG_PASSWORD || '123',
|
||||||
|
database: database,
|
||||||
|
max: 10,
|
||||||
|
idleTimeoutMillis: 30000,
|
||||||
|
connectionTimeoutMillis: 5000,
|
||||||
|
statement_timeout: 15000, // 15s timeout to prevent hanging queries
|
||||||
|
});
|
||||||
|
|
||||||
|
pools[database].on('error', (err) => {
|
||||||
|
console.error(`Unexpected error on idle client for db ${database}`, err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return pools[database];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function query(sql, params = [], dbName = null) {
|
||||||
|
const pool = getPool(dbName);
|
||||||
|
const start = Date.now();
|
||||||
|
const res = await pool.query(sql, params);
|
||||||
|
const duration = Date.now() - start;
|
||||||
|
// console.log(`[SQL ${duration}ms] executed on ${dbName || 'default'}`);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean RTF string helper
|
||||||
|
function cleanRtf(text) {
|
||||||
|
if (!text) return '';
|
||||||
|
if (!text.includes('{\\rtf')) return text.trim();
|
||||||
|
// Strip RTF control codes
|
||||||
|
let clean = text
|
||||||
|
.replace(/\\par[d]?/g, '\n')
|
||||||
|
.replace(/\\u([0-9]{3,5})\?/g, (match, dec) => String.fromCharCode(dec))
|
||||||
|
.replace(/\\'([0-9a-fA-F]{2})/g, (match, hex) => String.fromCharCode(parseInt(hex, 16)))
|
||||||
|
.replace(/\{[^{}]*\}/g, '')
|
||||||
|
.replace(/\\[a-zA-Z0-9\-]+/g, '')
|
||||||
|
.replace(/[\{\}]/g, '')
|
||||||
|
.trim();
|
||||||
|
return clean;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getPool,
|
||||||
|
query,
|
||||||
|
cleanRtf,
|
||||||
|
};
|
||||||
+147
@@ -0,0 +1,147 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const DATA_DIR = path.join(__dirname, 'data');
|
||||||
|
const UPLOADS_DIR = path.join(__dirname, 'uploads', 'issues');
|
||||||
|
const ISSUES_FILE = path.join(DATA_DIR, 'issues.json');
|
||||||
|
|
||||||
|
function ensureDirs() {
|
||||||
|
if (!fs.existsSync(DATA_DIR)) {
|
||||||
|
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(UPLOADS_DIR)) {
|
||||||
|
fs.mkdirSync(UPLOADS_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(ISSUES_FILE)) {
|
||||||
|
fs.writeFileSync(ISSUES_FILE, JSON.stringify([], null, 2), 'utf-8');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAllIssues() {
|
||||||
|
ensureDirs();
|
||||||
|
try {
|
||||||
|
const raw = fs.readFileSync(ISSUES_FILE, 'utf-8');
|
||||||
|
const list = JSON.parse(raw);
|
||||||
|
return Array.isArray(list) ? list : [];
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error reading issues.json:', err);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveIssues(issues) {
|
||||||
|
ensureDirs();
|
||||||
|
const tmpFile = `${ISSUES_FILE}.tmp`;
|
||||||
|
fs.writeFileSync(tmpFile, JSON.stringify(issues, null, 2), 'utf-8');
|
||||||
|
fs.renameSync(tmpFile, ISSUES_FILE);
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateIssueId() {
|
||||||
|
const now = new Date();
|
||||||
|
const y = now.getFullYear();
|
||||||
|
const m = String(now.getMonth() + 1).padStart(2, '0');
|
||||||
|
const d = String(now.getDate()).padStart(2, '0');
|
||||||
|
const prefix = `ISS-${y}${m}${d}`;
|
||||||
|
|
||||||
|
const all = getAllIssues();
|
||||||
|
const todayCount = all.filter(item => item.id && item.id.startsWith(prefix)).length + 1;
|
||||||
|
const seq = String(todayCount).padStart(3, '0');
|
||||||
|
return `${prefix}-${seq}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createIssue(data, file) {
|
||||||
|
ensureDirs();
|
||||||
|
const all = getAllIssues();
|
||||||
|
const id = generateIssueId();
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
let attachment = null;
|
||||||
|
if (file) {
|
||||||
|
attachment = {
|
||||||
|
filename: file.filename,
|
||||||
|
originalname: file.originalname,
|
||||||
|
size: file.size,
|
||||||
|
mimetype: file.mimetype,
|
||||||
|
url: `/uploads/issues/${file.filename}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const newIssue = {
|
||||||
|
id,
|
||||||
|
title: (data.title || 'Không có tiêu đề').trim(),
|
||||||
|
category: data.category || 'SAI_LECH_SO_LIEU', // SAI_LECH_SO_LIEU, LOI_GIAO_DIEN, LOI_KET_NOI, DE_XUAT, YEU_CAU_MOI, KHAC
|
||||||
|
module: data.module || 'CHUNG', // NGOAI_TRU, NOI_TRU, CHUYEN_VIEN, THEO_DOI, CHUNG
|
||||||
|
priority: data.priority || 'BINH_THUONG', // THAP, BINH_THUONG, CAO, KHAN_CAP
|
||||||
|
database: data.database || '',
|
||||||
|
databaseName: data.databaseName || data.database || '',
|
||||||
|
period: data.period || '',
|
||||||
|
reporter: (data.reporter || 'Cán bộ giao ban').trim(),
|
||||||
|
department: (data.department || '').trim(),
|
||||||
|
description: (data.description || '').trim(),
|
||||||
|
status: 'CHO_XU_LY', // CHO_XU_LY, DANG_XU_LY, DA_XU_LY, DONG
|
||||||
|
handlerNotes: '',
|
||||||
|
resolvedBy: '',
|
||||||
|
resolvedAt: null,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
attachment,
|
||||||
|
};
|
||||||
|
|
||||||
|
all.unshift(newIssue);
|
||||||
|
saveIssues(all);
|
||||||
|
return newIssue;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateIssue(id, updates) {
|
||||||
|
const all = getAllIssues();
|
||||||
|
const idx = all.findIndex(item => item.id === id);
|
||||||
|
if (idx === -1) {
|
||||||
|
throw new Error(`Không tìm thấy Issue với mã: ${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const cur = all[idx];
|
||||||
|
|
||||||
|
const updated = {
|
||||||
|
...cur,
|
||||||
|
...updates,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (updates.status === 'DA_XU_LY' && cur.status !== 'DA_XU_LY') {
|
||||||
|
updated.resolvedAt = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
all[idx] = updated;
|
||||||
|
saveIssues(all);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteIssue(id) {
|
||||||
|
const all = getAllIssues();
|
||||||
|
const idx = all.findIndex(item => item.id === id);
|
||||||
|
if (idx === -1) {
|
||||||
|
throw new Error(`Không tìm thấy Issue với mã: ${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = all[idx];
|
||||||
|
if (item.attachment && item.attachment.filename) {
|
||||||
|
const fPath = path.join(UPLOADS_DIR, item.attachment.filename);
|
||||||
|
if (fs.existsSync(fPath)) {
|
||||||
|
try { fs.unlinkSync(fPath); } catch (e) { /* ignore */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
all.splice(idx, 1);
|
||||||
|
saveIssues(all);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
UPLOADS_DIR,
|
||||||
|
ensureDirs,
|
||||||
|
getAllIssues,
|
||||||
|
createIssue,
|
||||||
|
updateIssue,
|
||||||
|
deleteIssue,
|
||||||
|
};
|
||||||
Generated
+1048
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "dh-giaoban-web",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"cors": "^2.8.6",
|
||||||
|
"dotenv": "^18.0.9",
|
||||||
|
"express": "^5.3.0",
|
||||||
|
"multer": "^2.5.0",
|
||||||
|
"pg": "^8.23.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
+1544
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
@@ -0,0 +1,899 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="vi">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>DH-HIS | Quản Lý Giao Ban Hằng Ngày</title>
|
||||||
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
|
<!-- Tailwind CSS via CDN -->
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<!-- Chart.js via CDN -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<!-- Font Awesome -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
</head>
|
||||||
|
<body class="bg-slate-100 text-slate-800 font-sans antialiased">
|
||||||
|
|
||||||
|
<!-- TOP HEADER -->
|
||||||
|
<header class="bg-gradient-to-r from-blue-700 via-sky-700 to-indigo-800 text-white shadow-lg sticky top-0 z-50">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 py-3 flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="bg-white/10 p-2 rounded-lg backdrop-blur-sm border border-white/20">
|
||||||
|
<i class="fa-solid fa-hospital-user text-2xl text-cyan-300"></i>
|
||||||
|
</div>
|
||||||
|
<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 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>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Database Selector -->
|
||||||
|
<div class="flex items-center gap-2 text-xs bg-slate-900/40 px-3 py-1.5 rounded-lg border border-white/10">
|
||||||
|
<label for="selectDb" class="text-sky-200 font-medium flex items-center gap-1 whitespace-nowrap">
|
||||||
|
<i class="fa-solid fa-database text-amber-400"></i> CSDL:
|
||||||
|
</label>
|
||||||
|
<select id="selectDb" class="bg-slate-800 text-white rounded px-2 py-1 outline-none border border-slate-600 focus:border-cyan-400 text-xs font-medium cursor-pointer">
|
||||||
|
<option value="an_omon_full_04052026">BVĐK Ô Môn - Cần Thơ (33 GB - Chuẩn khoa phòng)</option>
|
||||||
|
<option value="tuan_dbgovap_full">BVĐK Gò Vấp - TP.HCM (65 GB - Dữ liệu lớn)</option>
|
||||||
|
<option value="binh_bvq12_062026052026042026032026022026">BV Quận 12 - TP.HCM (21 GB)</option>
|
||||||
|
<option value="bvq12_697">BV Quận 12 - Bản 697 (17 GB)</option>
|
||||||
|
<option value="an_dalieu_full_05082026">BV Da Liễu (31 GB)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- 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">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-12 gap-3 items-end">
|
||||||
|
|
||||||
|
<!-- Mốc thời gian chọn nhanh -->
|
||||||
|
<div class="md:col-span-3">
|
||||||
|
<label class="block text-xs font-semibold text-slate-600 mb-1">
|
||||||
|
<i class="fa-solid fa-calendar-check text-blue-600"></i> Chọn nhanh mốc thời gian:
|
||||||
|
</label>
|
||||||
|
<select id="quickPreset" class="w-full text-sm rounded-lg border border-slate-300 bg-slate-50 px-3 py-2 outline-none focus:ring-2 focus:ring-blue-500 font-medium">
|
||||||
|
<option value="month-03-2026" selected>Tháng 03/2026 (Dữ liệu đầy đủ đối chiếu)</option>
|
||||||
|
<option value="month-04-2026">Tháng 04/2026</option>
|
||||||
|
<option value="month-01-2026">Tháng 01/2026</option>
|
||||||
|
<option value="month-02-2026">Tháng 02/2026</option>
|
||||||
|
<option value="today">Hôm nay</option>
|
||||||
|
<option value="yesterday">Hôm qua</option>
|
||||||
|
<option value="this-month">Tháng hiện tại</option>
|
||||||
|
<option value="quarter-1">Quý I/2026</option>
|
||||||
|
<option value="year-2026">Cả năm 2026</option>
|
||||||
|
<option value="custom">Tùy chọn khoảng ngày...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 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">
|
||||||
|
</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">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Checkbox so sánh -->
|
||||||
|
<div class="md:col-span-2 flex flex-col justify-end pb-1.5">
|
||||||
|
<label class="inline-flex items-center gap-2 cursor-pointer select-none text-xs font-semibold text-indigo-700 bg-indigo-50 border border-indigo-200 px-3 py-2 rounded-lg hover:bg-indigo-100 transition-colors">
|
||||||
|
<input type="checkbox" id="enableCompare" class="rounded text-indigo-600 focus:ring-indigo-500 h-4 w-4">
|
||||||
|
<span>So sánh 2 kỳ</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="md:col-span-3 flex items-center gap-2">
|
||||||
|
<button id="btnFilter" class="flex-1 bg-blue-600 hover:bg-blue-700 text-white text-sm font-semibold py-2 px-3 rounded-lg shadow-sm flex items-center justify-center gap-1.5 transition-all">
|
||||||
|
<i class="fa-solid fa-magnifying-glass"></i>
|
||||||
|
<span>Xem Báo Cáo</span>
|
||||||
|
</button>
|
||||||
|
<button id="btnOpenIssueModal" class="bg-rose-600 hover:bg-rose-700 text-white text-sm font-semibold py-2 px-3 rounded-lg shadow-sm flex items-center gap-1.5 transition-all" title="Báo cáo lỗi / Góp ý">
|
||||||
|
<i class="fa-solid fa-bug"></i>
|
||||||
|
<span class="hidden sm:inline">Báo Lỗi</span>
|
||||||
|
</button>
|
||||||
|
<button id="btnPrint" class="bg-slate-100 hover:bg-slate-200 text-slate-700 text-sm font-semibold py-2 px-2.5 rounded-lg border border-slate-300" title="In báo cáo">
|
||||||
|
<i class="fa-solid fa-print"></i>
|
||||||
|
</button>
|
||||||
|
<button id="btnExport" class="bg-emerald-600 hover:bg-emerald-700 text-white text-sm font-semibold py-2 px-2.5 rounded-lg shadow-sm" title="Xuất dữ liệu Excel">
|
||||||
|
<i class="fa-solid fa-file-excel"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- NAVIGATION TABS -->
|
||||||
|
<section class="max-w-7xl mx-auto px-4 sm:px-6 mt-4">
|
||||||
|
<div class="flex border-b border-slate-200 gap-2 flex-wrap">
|
||||||
|
<button class="tab-btn active px-4 py-2.5 text-sm font-bold border-b-2 border-blue-600 text-blue-600 flex items-center gap-2 transition-all" data-tab="tab-ngoaitru">
|
||||||
|
<i class="fa-solid fa-user-doctor"></i>
|
||||||
|
<span>1. Báo Cáo Giao Ban Ngoạ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-noitru">
|
||||||
|
<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-issues">
|
||||||
|
<i class="fa-solid fa-clipboard-list text-rose-500"></i>
|
||||||
|
<span>3. 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>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- MAIN CONTENT CONTAINER -->
|
||||||
|
<main class="max-w-7xl mx-auto px-4 sm:px-6 py-4">
|
||||||
|
|
||||||
|
<!-- ========================================== -->
|
||||||
|
<!-- TAB 1: BÁO CÁO GIAO BAN NGOẠI TRÚ -->
|
||||||
|
<!-- ========================================== -->
|
||||||
|
<div id="tab-ngoaitru" class="tab-content space-y-4">
|
||||||
|
|
||||||
|
<!-- KPI METRIC CARDS -->
|
||||||
|
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-3">
|
||||||
|
<div class="bg-white p-3.5 rounded-xl border border-slate-200 shadow-sm flex flex-col justify-between">
|
||||||
|
<div class="flex items-center justify-between text-slate-500">
|
||||||
|
<span class="text-xs font-semibold">Tổng Khám</span>
|
||||||
|
<i class="fa-solid fa-stethoscope text-blue-500"></i>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 text-2xl font-extrabold text-blue-700" id="kpiTongKham">0</div>
|
||||||
|
<div class="text-[11px] text-slate-400 mt-1">Lượt khám hoàn tất</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white p-3.5 rounded-xl border border-slate-200 shadow-sm flex flex-col justify-between">
|
||||||
|
<div class="flex items-center justify-between text-slate-500">
|
||||||
|
<span class="text-xs font-semibold">Khám BHYT</span>
|
||||||
|
<i class="fa-solid fa-id-card text-emerald-500"></i>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 text-2xl font-extrabold text-emerald-600" id="kpiBhyt">0</div>
|
||||||
|
<div class="text-[11px] text-emerald-600/80 font-medium mt-1" id="kpiBhytPct">0% tổng lượt</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white p-3.5 rounded-xl border border-slate-200 shadow-sm flex flex-col justify-between">
|
||||||
|
<div class="flex items-center justify-between text-slate-500">
|
||||||
|
<span class="text-xs font-semibold">Khám Thu Phí</span>
|
||||||
|
<i class="fa-solid fa-receipt text-amber-500"></i>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 text-2xl font-extrabold text-amber-600" id="kpiThuPhi">0</div>
|
||||||
|
<div class="text-[11px] text-amber-600/80 font-medium mt-1" id="kpiThuPhiPct">0% tổng lượt</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white p-3.5 rounded-xl border border-slate-200 shadow-sm flex flex-col justify-between">
|
||||||
|
<div class="flex items-center justify-between text-slate-500">
|
||||||
|
<span class="text-xs font-semibold">Chuyển Viện</span>
|
||||||
|
<i class="fa-solid fa-truck-medical text-rose-500"></i>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 text-2xl font-extrabold text-rose-600" id="kpiChuyenVien">0</div>
|
||||||
|
<div class="text-[11px] text-rose-600/80 font-medium mt-1">Tuyến trên & ngang</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white p-3.5 rounded-xl border border-slate-200 shadow-sm flex flex-col justify-between">
|
||||||
|
<div class="flex items-center justify-between text-slate-500">
|
||||||
|
<span class="text-xs font-semibold">Tiêm Ngừa</span>
|
||||||
|
<i class="fa-solid fa-syringe text-purple-500"></i>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 text-2xl font-extrabold text-purple-600" id="kpiTiemNgua">0</div>
|
||||||
|
<div class="text-[11px] text-slate-400 mt-1">Lượt tiêm chủng</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white p-3.5 rounded-xl border border-slate-200 shadow-sm flex flex-col justify-between">
|
||||||
|
<div class="flex items-center justify-between text-slate-500">
|
||||||
|
<span class="text-xs font-semibold">Bệnh Án Ngoại Trú</span>
|
||||||
|
<i class="fa-solid fa-folder-open text-sky-500"></i>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 text-2xl font-extrabold text-sky-700" id="kpiBantTong">0</div>
|
||||||
|
<div class="text-[11px] text-slate-500 mt-1 flex justify-between">
|
||||||
|
<span>Mới: <b class="text-sky-600" id="kpiBantMoi">0</b></span>
|
||||||
|
<span>Tái khám: <b class="text-sky-600" id="kpiBantTk">0</b></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ROW 1: BẢNG KHÁM THEO KHOA & SỐ LIỆU CHUYÊN KHOA YDCT -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-12 gap-4">
|
||||||
|
|
||||||
|
<!-- Bảng khám chữa bệnh theo Khoa -->
|
||||||
|
<div class="lg:col-span-8 bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
||||||
|
<div class="px-4 py-3 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-table-list text-blue-600"></i>
|
||||||
|
<span>1.1. Số Liệu Khám Chữa Bệnh Theo Các Khoa</span>
|
||||||
|
</h2>
|
||||||
|
<span class="text-xs text-slate-500" id="subKhoaKcb">Khám bệnh, Nội, Ngoại, CSSKSS, Nhi, YDCT</span>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-left text-sm border-collapse" id="tableKhoaKcb">
|
||||||
|
<thead class="bg-slate-100 text-slate-700 font-semibold text-xs uppercase tracking-wider border-b border-slate-200">
|
||||||
|
<tr>
|
||||||
|
<th class="py-2.5 px-3">Mã</th>
|
||||||
|
<th class="py-2.5 px-3">Khoa Khám Bệnh</th>
|
||||||
|
<th class="py-2.5 px-3 text-right">BHYT</th>
|
||||||
|
<th class="py-2.5 px-3 text-right">Thu Phí</th>
|
||||||
|
<th class="py-2.5 px-3 text-right">Chuyển Viện</th>
|
||||||
|
<th class="py-2.5 px-3 text-right">Tổng Khám</th>
|
||||||
|
<!-- Cột so sánh nếu bật -->
|
||||||
|
<th class="py-2.5 px-3 text-right th-compare hidden">Kỳ So Sánh</th>
|
||||||
|
<th class="py-2.5 px-3 text-right th-compare hidden">Chênh Lệch</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-200 font-normal text-slate-800" id="bodyKhoaKcb">
|
||||||
|
<tr><td colspan="6" class="text-center py-6 text-slate-400">Đang tải dữ liệu...</td></tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot class="bg-slate-100 font-bold text-slate-800 text-xs border-t-2 border-slate-300" id="footKhoaKcb">
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chuyên khoa YDCT & Bệnh án ngoại trú Card -->
|
||||||
|
<div class="lg:col-span-4 space-y-4">
|
||||||
|
<!-- Card YDCT -->
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
||||||
|
<div class="px-4 py-3 bg-amber-50/70 border-b border-amber-200 flex items-center justify-between">
|
||||||
|
<h2 class="text-sm font-bold text-amber-900 flex items-center gap-2">
|
||||||
|
<i class="fa-solid fa-leaf text-amber-600"></i>
|
||||||
|
<span>1.2. Chuyên Khoa Y Dược Cổ Truyền</span>
|
||||||
|
</h2>
|
||||||
|
<span class="text-xs bg-amber-200/60 text-amber-800 px-2 py-0.5 rounded font-semibold">YDCT</span>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 space-y-3">
|
||||||
|
<div class="flex items-center justify-between border-b border-slate-100 pb-2">
|
||||||
|
<span class="text-sm text-slate-600 font-medium flex items-center gap-2">
|
||||||
|
<i class="fa-solid fa-bolt text-amber-500"></i> Điện châm
|
||||||
|
</span>
|
||||||
|
<span class="text-base font-bold text-slate-800" id="ydctDienCham">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between border-b border-slate-100 pb-2">
|
||||||
|
<span class="text-sm text-slate-600 font-medium flex items-center gap-2">
|
||||||
|
<i class="fa-solid fa-person-walking text-blue-500"></i> Vật lý trị liệu (VLTL)
|
||||||
|
</span>
|
||||||
|
<span class="text-base font-bold text-slate-800" id="ydctVltl">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between border-b border-slate-100 pb-2">
|
||||||
|
<span class="text-sm text-slate-600 font-medium flex items-center gap-2">
|
||||||
|
<i class="fa-solid fa-mug-hot text-emerald-500"></i> Sắc thuốc thang
|
||||||
|
</span>
|
||||||
|
<span class="text-base font-bold text-slate-800" id="ydctSacThuoc">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-sm text-slate-600 font-medium flex items-center gap-2">
|
||||||
|
<i class="fa-solid fa-certificate text-purple-500"></i> Cấy chỉ (Chôn chỉ)
|
||||||
|
</span>
|
||||||
|
<span class="text-base font-bold text-slate-800" id="ydctCayChi">0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Top 10 Bệnh Ngoại Trú -->
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
||||||
|
<div class="px-4 py-3 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-ranking-star text-rose-500"></i>
|
||||||
|
<span>Top 10 Chẩn Đoán Ngoại Trú</span>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="p-2 overflow-y-auto max-h-72">
|
||||||
|
<table class="w-full text-left text-xs border-collapse">
|
||||||
|
<tbody class="divide-y divide-slate-100 font-medium" id="bodyTop10">
|
||||||
|
<tr><td class="text-center py-4 text-slate-400">Đang tải dữ liệu...</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ROW 2: TỔNG HỢP CẬN LÂM SÀNG & BIỂU ĐỒ CỘT SO SÁNH -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-12 gap-4">
|
||||||
|
|
||||||
|
<!-- Bảng Cận Lâm Sàng -->
|
||||||
|
<div class="lg:col-span-6 bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
||||||
|
<div class="px-4 py-3 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-flask-vial text-cyan-600"></i>
|
||||||
|
<span>1.3. Tổng Hợp Số Liệu Cận Lâm Sàng</span>
|
||||||
|
</h2>
|
||||||
|
<span class="text-xs text-slate-500">Xét nghiệm & CĐHA</span>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-x-auto max-h-96">
|
||||||
|
<table class="w-full text-left text-xs border-collapse">
|
||||||
|
<thead class="bg-slate-100 text-slate-700 font-semibold uppercase border-b border-slate-200 sticky top-0">
|
||||||
|
<tr>
|
||||||
|
<th class="py-2 px-3">Phân nhóm</th>
|
||||||
|
<th class="py-2 px-3">Loại Kỹ Thuật</th>
|
||||||
|
<th class="py-2 px-3 text-right">Số Bệnh Nhân</th>
|
||||||
|
<th class="py-2 px-3 text-right">Số Lượt Thực Hiện</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-100" id="bodyCls">
|
||||||
|
<tr><td colspan="4" class="text-center py-4 text-slate-400">Đang tải dữ liệu...</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Biểu đồ cột so sánh -->
|
||||||
|
<div class="lg:col-span-6 bg-white rounded-xl shadow-sm border border-slate-200 p-4 flex flex-col justify-between">
|
||||||
|
<div class="flex items-center justify-between border-b border-slate-100 pb-2 mb-2">
|
||||||
|
<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 Đồ 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>
|
||||||
|
<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>
|
||||||
|
<div class="relative w-full h-80">
|
||||||
|
<canvas id="chartNgoaiTru"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ========================================== -->
|
||||||
|
<!-- TAB 2: BÁO CÁO GIAO BAN NỘI TRÚ -->
|
||||||
|
<!-- ========================================== -->
|
||||||
|
<div id="tab-noitru" class="tab-content hidden space-y-4">
|
||||||
|
|
||||||
|
<!-- KPI METRICS NỘI TRÚ -->
|
||||||
|
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-3">
|
||||||
|
<div class="bg-white p-3.5 rounded-xl border border-slate-200 shadow-sm flex flex-col justify-between">
|
||||||
|
<div class="flex items-center justify-between text-slate-500">
|
||||||
|
<span class="text-xs font-semibold">Bệnh Cũ (Tồn Đầu)</span>
|
||||||
|
<i class="fa-solid fa-clock-rotate-left text-slate-500"></i>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 text-2xl font-extrabold text-slate-700" id="kpiNtBenhCu">0</div>
|
||||||
|
<div class="text-[11px] text-slate-400 mt-1">Chuyển từ kỳ trước</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white p-3.5 rounded-xl border border-slate-200 shadow-sm flex flex-col justify-between">
|
||||||
|
<div class="flex items-center justify-between text-slate-500">
|
||||||
|
<span class="text-xs font-semibold">Bệnh Mới (Vào Viện)</span>
|
||||||
|
<i class="fa-solid fa-user-plus text-blue-500"></i>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 text-2xl font-extrabold text-blue-600" id="kpiNtBenhMoi">0</div>
|
||||||
|
<div class="text-[11px] text-blue-600/80 font-medium mt-1">Nhập viện trong kỳ</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white p-3.5 rounded-xl border border-slate-200 shadow-sm flex flex-col justify-between">
|
||||||
|
<div class="flex items-center justify-between text-slate-500">
|
||||||
|
<span class="text-xs font-semibold">Tổng Xuất Viện</span>
|
||||||
|
<i class="fa-solid fa-person-walking-arrow-right text-emerald-500"></i>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 text-2xl font-extrabold text-emerald-600" id="kpiNtXuatVien">0</div>
|
||||||
|
<div class="text-[11px] text-emerald-600/80 font-medium mt-1" id="kpiNtOnPct">Ổn về: 0 ca</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white p-3.5 rounded-xl border border-slate-200 shadow-sm flex flex-col justify-between">
|
||||||
|
<div class="flex items-center justify-between text-slate-500">
|
||||||
|
<span class="text-xs font-semibold">Bệnh Hiện Còn</span>
|
||||||
|
<i class="fa-solid fa-bed text-indigo-500"></i>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 text-2xl font-extrabold text-indigo-700" id="kpiNtHienCon">0</div>
|
||||||
|
<div class="text-[11px] text-indigo-700/80 font-medium mt-1">Đang nằm điều trị</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white p-3.5 rounded-xl border border-slate-200 shadow-sm flex flex-col justify-between">
|
||||||
|
<div class="flex items-center justify-between text-slate-500">
|
||||||
|
<span class="text-xs font-semibold">Bệnh Theo Dõi</span>
|
||||||
|
<i class="fa-solid fa-heart-pulse text-rose-500"></i>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 text-2xl font-extrabold text-rose-600" id="kpiNtTheoDoi">0</div>
|
||||||
|
<div class="text-[11px] text-rose-600/80 font-medium mt-1">Bệnh nhân nặng</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white p-3.5 rounded-xl border border-slate-200 shadow-sm flex flex-col justify-between">
|
||||||
|
<div class="flex items-center justify-between text-slate-500">
|
||||||
|
<span class="text-xs font-semibold">Chăm Sóc Cấp I</span>
|
||||||
|
<i class="fa-solid fa-hand-holding-medical text-amber-500"></i>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 text-2xl font-extrabold text-amber-600" id="kpiNtCsCap1">0</div>
|
||||||
|
<div class="text-[11px] text-amber-600/80 font-medium mt-1">Chế độ chăm sóc I</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- BẢNG 1: LƯU LƯỢNG BỆNH NHÂN THEO CÁC KHOA -->
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
||||||
|
<div class="px-4 py-3 bg-slate-50 border-b border-slate-200 flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-sm font-bold text-slate-800 flex items-center gap-2">
|
||||||
|
<i class="fa-solid fa-hospital text-blue-600"></i>
|
||||||
|
<span>2.1. Tổng Hợp Lưu Lượng Bệnh Nhân Theo Các Khoa Nội Trú</span>
|
||||||
|
</h2>
|
||||||
|
<p class="text-xs text-slate-500">Cấp cứu HS, Nội, Nhi, Ngoại TH, CSSKSS, YDCT,... (Khớp công thức: Tồn đầu + Vào viện - Ra viện = Hiện còn)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-left text-xs border-collapse" id="tableNoiTru">
|
||||||
|
<thead class="bg-slate-100 text-slate-700 font-semibold uppercase tracking-wider border-b border-slate-200">
|
||||||
|
<tr>
|
||||||
|
<th class="py-2.5 px-3">Mã</th>
|
||||||
|
<th class="py-2.5 px-3">Khoa Điều Trị</th>
|
||||||
|
<th class="py-2.5 px-3 text-right">Bệnh Cũ</th>
|
||||||
|
<th class="py-2.5 px-3 text-right">Bệnh Mới</th>
|
||||||
|
<th class="py-2.5 px-3 text-right text-emerald-700">Ổn xin về</th>
|
||||||
|
<th class="py-2.5 px-3 text-right text-amber-700">Nặng về</th>
|
||||||
|
<th class="py-2.5 px-3 text-right text-slate-600">Trốn</th>
|
||||||
|
<th class="py-2.5 px-3 text-right text-rose-700">Chuyển viện</th>
|
||||||
|
<th class="py-2.5 px-3 text-right text-rose-900">Tử vong</th>
|
||||||
|
<th class="py-2.5 px-3 text-right font-bold text-blue-700 bg-blue-50/50">Hiện Còn</th>
|
||||||
|
<th class="py-2.5 px-3 text-right">Bệnh Theo Dõi</th>
|
||||||
|
<th class="py-2.5 px-3 text-right">CS Cấp I</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-200 font-normal text-slate-800" id="bodyNoiTru">
|
||||||
|
<tr><td colspan="12" class="text-center py-6 text-slate-400">Đang tải dữ liệu...</td></tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot class="bg-slate-100 font-bold text-slate-800 text-xs border-t-2 border-slate-300" id="footNoiTru">
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
|
<div class="relative w-full h-80">
|
||||||
|
<canvas id="chartNoiTru"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- BẢNG 3: DANH SÁCH BỆNH NHÂN CHUYỂN VIỆN -->
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
||||||
|
<div class="px-4 py-3 bg-rose-50/80 border-b border-rose-200 flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-sm font-bold text-rose-900 flex items-center gap-2">
|
||||||
|
<i class="fa-solid fa-ambulance text-rose-600"></i>
|
||||||
|
<span>BẢNG 3: Danh Sách Bệnh Nhân Chuyển Viện</span>
|
||||||
|
</h2>
|
||||||
|
<p class="text-xs text-rose-700/80">Cho phép các khoa tự nhập liệu trực tiếp vào cột Ghi chú trên lưới trước giờ họp giao ban</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 text-xs">
|
||||||
|
<span class="text-slate-600 font-medium">Khoa:</span>
|
||||||
|
<select id="filterKhoaCv" class="rounded border border-slate-300 px-2 py-1 bg-white outline-none">
|
||||||
|
<option value="ALL">Tất cả khoa</option>
|
||||||
|
</select>
|
||||||
|
<span class="bg-rose-200 text-rose-800 px-2 py-0.5 rounded-full font-bold text-[11px]" id="countBang3">0 ca</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-x-auto max-h-[500px]">
|
||||||
|
<table class="w-full text-left text-xs border-collapse">
|
||||||
|
<thead class="bg-slate-100 text-slate-700 font-semibold uppercase sticky top-0 border-b border-slate-200 z-10">
|
||||||
|
<tr>
|
||||||
|
<th class="py-2.5 px-2.5 w-10 text-center">STT</th>
|
||||||
|
<th class="py-2.5 px-2.5 w-24">Mã BN / BA</th>
|
||||||
|
<th class="py-2.5 px-3 w-40">Họ và Tên</th>
|
||||||
|
<th class="py-2.5 px-3 w-48">Địa Chỉ</th>
|
||||||
|
<th class="py-2.5 px-3 w-32">Ngày Vào / Ra</th>
|
||||||
|
<th class="py-2.5 px-3 w-32">Khoa Chuyển</th>
|
||||||
|
<th class="py-2.5 px-3 w-56">Chẩn Đoán (Chính/Phụ)</th>
|
||||||
|
<th class="py-2.5 px-3 w-64">Kết Quả CLS Tóm Tắt</th>
|
||||||
|
<th class="py-2.5 px-3 w-64 bg-amber-50 text-amber-900 font-bold border-l border-amber-200">
|
||||||
|
<i class="fa-solid fa-pen-to-square"></i> Ghi Chú (Khoa tự nhập)
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-200" id="bodyBang3">
|
||||||
|
<tr><td colspan="9" class="text-center py-6 text-slate-400">Đang tải danh sách chuyển viện...</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- BẢNG 4: DANH SÁCH BỆNH NHÂN THEO DÕI & CHĂM SÓC CẤP I -->
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
||||||
|
<div class="px-4 py-3 bg-amber-50/80 border-b border-amber-200 flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-sm font-bold text-amber-900 flex items-center gap-2">
|
||||||
|
<i class="fa-solid fa-eye text-amber-600"></i>
|
||||||
|
<span>BẢNG 4: Danh Sách Bệnh Nhân Theo Dõi & Chăm Sóc Cấp I</span>
|
||||||
|
</h2>
|
||||||
|
<p class="text-xs text-amber-700/80">Theo dõi diễn biến nặng, can thiệp đặc biệt và chỉ định chăm sóc cấp I</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 text-xs">
|
||||||
|
<span class="text-slate-600 font-medium">Khoa:</span>
|
||||||
|
<select id="filterKhoaTd" class="rounded border border-slate-300 px-2 py-1 bg-white outline-none">
|
||||||
|
<option value="ALL">Tất cả khoa</option>
|
||||||
|
</select>
|
||||||
|
<span class="bg-amber-200 text-amber-800 px-2 py-0.5 rounded-full font-bold text-[11px]" id="countBang4">0 ca</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-x-auto max-h-[500px]">
|
||||||
|
<table class="w-full text-left text-xs border-collapse">
|
||||||
|
<thead class="bg-slate-100 text-slate-700 font-semibold uppercase sticky top-0 border-b border-slate-200 z-10">
|
||||||
|
<tr>
|
||||||
|
<th class="py-2.5 px-2.5 w-10 text-center">STT</th>
|
||||||
|
<th class="py-2.5 px-2.5 w-24">Mã BN / BA</th>
|
||||||
|
<th class="py-2.5 px-3 w-40">Họ và Tên</th>
|
||||||
|
<th class="py-2.5 px-3 w-48">Địa Chỉ</th>
|
||||||
|
<th class="py-2.5 px-3 w-32">Ngày Vào Viện</th>
|
||||||
|
<th class="py-2.5 px-3 w-32">Khoa Điều Trị</th>
|
||||||
|
<th class="py-2.5 px-3 w-56">Chẩn Đoán (Chính/Phụ)</th>
|
||||||
|
<th class="py-2.5 px-3 w-64">Kết Quả CLS Tóm Tắt</th>
|
||||||
|
<th class="py-2.5 px-3 w-64 bg-amber-50 text-amber-900 font-bold border-l border-amber-200">
|
||||||
|
<i class="fa-solid fa-pen-to-square"></i> Ghi Chú (Khoa tự nhập)
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-200" id="bodyBang4">
|
||||||
|
<tr><td colspan="9" class="text-center py-6 text-slate-400">Đang tải danh sách theo dõi...</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ========================================== -->
|
||||||
|
<!-- TAB 3: QUẢN LÝ ISSUE & BÁO LỖI -->
|
||||||
|
<!-- ========================================== -->
|
||||||
|
<div id="tab-issues" class="tab-content hidden space-y-4">
|
||||||
|
|
||||||
|
<!-- KPI METRIC CARDS CHO ISSUE -->
|
||||||
|
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||||
|
<div class="bg-white p-3.5 rounded-xl border border-slate-200 shadow-sm flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="text-xs font-semibold text-slate-500">Tổng Số Báo Lỗi</div>
|
||||||
|
<div class="mt-1 text-2xl font-black text-slate-800" id="issueKpiTotal">0</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-10 h-10 rounded-lg bg-slate-100 flex items-center justify-center text-slate-600">
|
||||||
|
<i class="fa-solid fa-list-check text-lg"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white p-3.5 rounded-xl border border-amber-200 shadow-sm flex items-center justify-between bg-amber-50/40">
|
||||||
|
<div>
|
||||||
|
<div class="text-xs font-semibold text-amber-700">Chờ Xử Lý</div>
|
||||||
|
<div class="mt-1 text-2xl font-black text-amber-600" id="issueKpiPending">0</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-10 h-10 rounded-lg bg-amber-100 flex items-center justify-center text-amber-600">
|
||||||
|
<i class="fa-solid fa-clock text-lg"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white p-3.5 rounded-xl border border-blue-200 shadow-sm flex items-center justify-between bg-blue-50/40">
|
||||||
|
<div>
|
||||||
|
<div class="text-xs font-semibold text-blue-700">Đang Xử Lý</div>
|
||||||
|
<div class="mt-1 text-2xl font-black text-blue-600" id="issueKpiProcessing">0</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-10 h-10 rounded-lg bg-blue-100 flex items-center justify-center text-blue-600">
|
||||||
|
<i class="fa-solid fa-screwdriver-wrench text-lg"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white p-3.5 rounded-xl border border-emerald-200 shadow-sm flex items-center justify-between bg-emerald-50/40">
|
||||||
|
<div>
|
||||||
|
<div class="text-xs font-semibold text-emerald-700">Đã Giải Quyết</div>
|
||||||
|
<div class="mt-1 text-2xl font-black text-emerald-600" id="issueKpiResolved">0</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-10 h-10 rounded-lg bg-emerald-100 flex items-center justify-center text-emerald-600">
|
||||||
|
<i class="fa-solid fa-circle-check text-lg"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- FILTER & ACTION TOOLBAR -->
|
||||||
|
<div class="bg-white p-3 rounded-xl border border-slate-200 shadow-sm flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<div class="flex flex-wrap items-center gap-2 text-xs">
|
||||||
|
<span class="font-bold text-slate-700 flex items-center gap-1">
|
||||||
|
<i class="fa-solid fa-filter text-blue-600"></i> Bộ lọc:
|
||||||
|
</span>
|
||||||
|
<select id="filterIssueStatus" class="rounded-lg border border-slate-300 px-2.5 py-1.5 bg-slate-50 outline-none">
|
||||||
|
<option value="ALL">Tất cả trạng thái</option>
|
||||||
|
<option value="CHO_XU_LY">Chờ xử lý</option>
|
||||||
|
<option value="DANG_XU_LY">Đang xử lý</option>
|
||||||
|
<option value="DA_XU_LY">Đã xử lý</option>
|
||||||
|
<option value="DONG">Đã đóng</option>
|
||||||
|
</select>
|
||||||
|
<select id="filterIssueCategory" class="rounded-lg border border-slate-300 px-2.5 py-1.5 bg-slate-50 outline-none">
|
||||||
|
<option value="ALL">Tất cả phân loại</option>
|
||||||
|
<option value="SAI_LECH_SO_LIEU">Sai lệch số liệu</option>
|
||||||
|
<option value="LOI_GIAO_DIEN">Lỗi giao diện / chức năng</option>
|
||||||
|
<option value="LOI_KET_NOI">Lỗi kết nối / timeout</option>
|
||||||
|
<option value="DE_XUAT_CAI_TIEN">Đề xuất cải tiến</option>
|
||||||
|
<option value="YEU_CAU_MOI">Yêu cầu mới</option>
|
||||||
|
<option value="KHAC">Khác</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" id="filterIssueKeyword" placeholder="Tìm theo mã, tiêu đề, người báo..." class="rounded-lg border border-slate-300 px-2.5 py-1.5 bg-slate-50 outline-none w-56">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button id="btnExportIssuesExcel" class="bg-emerald-600 hover:bg-emerald-700 text-white text-xs font-semibold py-1.5 px-3 rounded-lg shadow-sm flex items-center gap-1.5 transition-all">
|
||||||
|
<i class="fa-solid fa-file-excel"></i>
|
||||||
|
<span>Xuất Excel Tổng Hợp</span>
|
||||||
|
</button>
|
||||||
|
<button id="btnOpenIssueModal2" class="bg-rose-600 hover:bg-rose-700 text-white text-xs font-semibold py-1.5 px-3 rounded-lg shadow-sm flex items-center gap-1.5 transition-all">
|
||||||
|
<i class="fa-solid fa-plus"></i>
|
||||||
|
<span>Tạo Báo Lỗi Mới</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- BẢNG TỔNG HỢP ISSUE -->
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-left text-xs border-collapse" id="tableIssues">
|
||||||
|
<thead class="bg-slate-100 text-slate-700 font-semibold uppercase border-b border-slate-200">
|
||||||
|
<tr>
|
||||||
|
<th class="py-2.5 px-3 w-28">Mã Issue</th>
|
||||||
|
<th class="py-2.5 px-3">Tiêu Đề & Chi Tiết Sai Lệch</th>
|
||||||
|
<th class="py-2.5 px-3 w-32">Phân Loại / Mức Độ</th>
|
||||||
|
<th class="py-2.5 px-3 w-44">CSDL & Kỳ Xem</th>
|
||||||
|
<th class="py-2.5 px-3 w-32">Người Báo</th>
|
||||||
|
<th class="py-2.5 px-3 w-24 text-center">Tệp Đính Kèm</th>
|
||||||
|
<th class="py-2.5 px-3 w-32 text-center">Trạng Thái</th>
|
||||||
|
<th class="py-2.5 px-3 w-28 text-center">Thao Tác</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-200" id="bodyIssuesList">
|
||||||
|
<tr><td colspan="8" class="text-center py-8 text-slate-400">Đang tải danh sách issue...</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- ======================================================== -->
|
||||||
|
<!-- MODAL 1: TẠO BÁO LỖI / ISSUE MỚI -->
|
||||||
|
<!-- ======================================================== -->
|
||||||
|
<div id="modalIssue" class="fixed inset-0 z-50 bg-slate-900/60 backdrop-blur-sm flex items-center justify-center p-4 hidden">
|
||||||
|
<div class="bg-white rounded-2xl shadow-2xl max-w-2xl w-full max-h-[92vh] flex flex-col overflow-hidden animate-in fade-in zoom-in-95 duration-150">
|
||||||
|
|
||||||
|
<!-- Modal Header -->
|
||||||
|
<div class="px-6 py-4 bg-gradient-to-r from-rose-600 to-pink-600 text-white flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-2.5">
|
||||||
|
<div class="w-8 h-8 rounded-lg bg-white/20 flex items-center justify-center">
|
||||||
|
<i class="fa-solid fa-bug text-lg"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-base font-bold">Gửi Báo Lỗi / Sự Cố Hệ Thống</h3>
|
||||||
|
<p class="text-xs text-rose-100">Dữ liệu và file đính kèm sẽ được lưu lại để kỹ thuật tổng hợp xử lý</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button id="btnCloseIssueModal" class="text-white/80 hover:text-white p-1 rounded-lg hover:bg-white/10 text-lg transition-colors">
|
||||||
|
<i class="fa-solid fa-xmark"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Form Body -->
|
||||||
|
<form id="formCreateIssue" class="p-6 overflow-y-auto space-y-4 flex-1 text-xs">
|
||||||
|
|
||||||
|
<!-- Tiêu đề -->
|
||||||
|
<div>
|
||||||
|
<label class="block font-bold text-slate-700 mb-1">
|
||||||
|
Tiêu đề vấn đề / lỗi <span class="text-rose-500">*</span>:
|
||||||
|
</label>
|
||||||
|
<input type="text" id="issueTitle" required placeholder="Ví dụ: Sai lệch số lượt khám BHYT Khoa Khám Bệnh so với HIS gốc" class="w-full text-sm rounded-lg border border-slate-300 px-3 py-2 bg-slate-50 outline-none focus:ring-2 focus:ring-rose-500 font-medium">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Phân loại & Phân hệ & Mức độ -->
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||||
|
<div>
|
||||||
|
<label class="block font-semibold text-slate-700 mb-1">Phân loại sự cố:</label>
|
||||||
|
<select id="issueCategory" class="w-full rounded-lg border border-slate-300 px-2.5 py-2 bg-slate-50 outline-none focus:ring-2 focus:ring-rose-500">
|
||||||
|
<option value="SAI_LECH_SO_LIEU">Sai lệch số liệu</option>
|
||||||
|
<option value="LOI_GIAO_DIEN">Lỗi giao diện / chức năng</option>
|
||||||
|
<option value="LOI_KET_NOI">Lỗi kết nối CSDL / timeout</option>
|
||||||
|
<option value="DE_XUAT_CAI_TIEN">Đề xuất cải tiến</option>
|
||||||
|
<option value="YEU_CAU_MOI">Yêu cầu báo cáo mới</option>
|
||||||
|
<option value="KHAC">Khác</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block font-semibold text-slate-700 mb-1">Phân hệ liên quan:</label>
|
||||||
|
<select id="issueModule" class="w-full rounded-lg border border-slate-300 px-2.5 py-2 bg-slate-50 outline-none focus:ring-2 focus:ring-rose-500">
|
||||||
|
<option value="NGOAI_TRU">1. Báo cáo Ngoại trú</option>
|
||||||
|
<option value="NOI_TRU">2. Báo cáo Nội trú</option>
|
||||||
|
<option value="CHUYEN_VIEN">Bảng 3. Chuyển viện</option>
|
||||||
|
<option value="THEO_DOI">Bảng 4. BN Theo dõi</option>
|
||||||
|
<option value="CHUNG">Chung / Toàn hệ thống</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block font-semibold text-slate-700 mb-1">Mức độ ưu tiên:</label>
|
||||||
|
<select id="issuePriority" class="w-full rounded-lg border border-slate-300 px-2.5 py-2 bg-slate-50 outline-none focus:ring-2 focus:ring-rose-500 font-bold text-amber-700">
|
||||||
|
<option value="BINH_THUONG" class="text-slate-700">Bình thường</option>
|
||||||
|
<option value="CAO" class="text-amber-700">Cao</option>
|
||||||
|
<option value="KHAN_CAP" class="text-rose-700">Khẩn cấp (Họp giao ban gấp)</option>
|
||||||
|
<option value="THAP" class="text-slate-500">Thấp</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CSDL & Mốc thời gian tự động -->
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 bg-slate-50 p-2.5 rounded-lg border border-slate-200">
|
||||||
|
<div>
|
||||||
|
<label class="block text-slate-500 mb-0.5">CSDL đang xem:</label>
|
||||||
|
<input type="text" id="issueDatabase" readonly class="w-full rounded border border-slate-300 px-2 py-1 bg-white text-slate-700 font-mono text-[11px]">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-slate-500 mb-0.5">Khoảng ngày đang xem:</label>
|
||||||
|
<input type="text" id="issuePeriod" readonly class="w-full rounded border border-slate-300 px-2 py-1 bg-white text-slate-700 font-mono text-[11px]">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Người báo & Khoa phòng -->
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label class="block font-semibold text-slate-700 mb-1">Người báo cáo:</label>
|
||||||
|
<input type="text" id="issueReporter" placeholder="Ví dụ: BS. Nguyễn Văn A" class="w-full rounded-lg border border-slate-300 px-3 py-2 bg-slate-50 outline-none focus:ring-2 focus:ring-rose-500 font-medium">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block font-semibold text-slate-700 mb-1">Khoa / Phòng ban:</label>
|
||||||
|
<input type="text" id="issueDepartment" placeholder="Ví dụ: Khoa Khám Bệnh / KHTH" class="w-full rounded-lg border border-slate-300 px-3 py-2 bg-slate-50 outline-none focus:ring-2 focus:ring-rose-500 font-medium">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Nội dung mô tả chi tiết -->
|
||||||
|
<div>
|
||||||
|
<label class="block font-bold text-slate-700 mb-1">
|
||||||
|
Mô tả chi tiết sai lệch / lỗi <span class="text-rose-500">*</span>:
|
||||||
|
</label>
|
||||||
|
<textarea id="issueDescription" rows="4" required placeholder="- Số liệu kỳ vọng trên HIS gốc: ... - Số liệu hiển thị trên Web: ... - Thao tác thực hiện / lỗi xuất hiện:" class="w-full rounded-lg border border-slate-300 p-2.5 bg-slate-50 outline-none focus:ring-2 focus:ring-rose-500 text-xs leading-relaxed font-sans"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Đính kèm file / Ảnh chụp màn hình -->
|
||||||
|
<div>
|
||||||
|
<label class="block font-bold text-slate-700 mb-1 flex items-center justify-between">
|
||||||
|
<span>Đính kèm tệp / Ảnh chụp màn hình:</span>
|
||||||
|
<span class="text-slate-400 font-normal">Hỗ trợ PNG, JPG, PDF, XLSX, Log (Tối đa 25MB)</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div id="issuePasteZone" class="border-2 border-dashed border-slate-300 hover:border-rose-400 rounded-xl p-3 text-center bg-slate-50/50 hover:bg-rose-50/20 transition-all cursor-pointer">
|
||||||
|
<input type="file" id="issueFileInput" class="hidden" accept="image/*,.pdf,.xlsx,.xls,.doc,.docx,.txt,.csv">
|
||||||
|
<div id="pasteZonePrompt" class="flex flex-col items-center justify-center gap-1 text-slate-500">
|
||||||
|
<i class="fa-solid fa-cloud-arrow-up text-2xl text-rose-500 mb-1"></i>
|
||||||
|
<div class="text-xs font-semibold text-slate-700">Nhấp để chọn tệp hoặc <span class="text-rose-600 underline">dán ảnh chụp màn hình (Ctrl+V)</span></div>
|
||||||
|
<div class="text-[11px] text-slate-400">Có thể dán trực tiếp ảnh từ phím tắt PrintScreen / Snipping Tool</div>
|
||||||
|
</div>
|
||||||
|
<!-- File Preview Area -->
|
||||||
|
<div id="issuePreviewArea" class="hidden mt-2 flex items-center justify-between bg-white p-2.5 rounded-lg border border-slate-200">
|
||||||
|
<div class="flex items-center gap-2 overflow-hidden">
|
||||||
|
<img id="issueImgPreview" src="" class="hidden w-12 h-12 object-cover rounded border border-slate-200">
|
||||||
|
<i id="issueFileIcon" class="fa-solid fa-file text-2xl text-slate-400 hidden"></i>
|
||||||
|
<div class="text-left truncate">
|
||||||
|
<div id="issueFileName" class="font-bold text-slate-800 text-xs truncate">ten_file.png</div>
|
||||||
|
<div id="issueFileSize" class="text-[10px] text-slate-400">0 KB</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" id="btnRemoveFile" class="text-rose-500 hover:text-rose-700 text-sm p-1.5 rounded hover:bg-rose-50">
|
||||||
|
<i class="fa-solid fa-trash-can"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action buttons -->
|
||||||
|
<div class="pt-2 flex items-center justify-end gap-2 border-t border-slate-200">
|
||||||
|
<button type="button" id="btnCancelIssue" class="px-4 py-2 text-xs font-semibold text-slate-600 hover:bg-slate-100 rounded-lg transition-colors">
|
||||||
|
Hủy bỏ
|
||||||
|
</button>
|
||||||
|
<button type="submit" id="btnSubmitIssue" class="bg-gradient-to-r from-rose-600 to-pink-600 hover:from-rose-700 hover:to-pink-700 text-white px-5 py-2 rounded-lg font-bold shadow-md flex items-center gap-2 transition-all">
|
||||||
|
<i class="fa-solid fa-paper-plane"></i>
|
||||||
|
<span>Gửi Báo Lỗi & Lưu File</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ======================================================== -->
|
||||||
|
<!-- MODAL 2: CẬP NHẬT TRẠNG THÁI & XỬ LÝ ISSUE -->
|
||||||
|
<!-- ======================================================== -->
|
||||||
|
<div id="modalUpdateIssue" class="fixed inset-0 z-50 bg-slate-900/60 backdrop-blur-sm flex items-center justify-center p-4 hidden">
|
||||||
|
<div class="bg-white rounded-2xl shadow-2xl max-w-lg w-full flex flex-col overflow-hidden animate-in fade-in zoom-in-95 duration-150">
|
||||||
|
|
||||||
|
<div class="px-5 py-3.5 bg-slate-800 text-white flex items-center justify-between">
|
||||||
|
<h3 class="text-sm font-bold flex items-center gap-2">
|
||||||
|
<i class="fa-solid fa-pen-nib text-amber-400"></i>
|
||||||
|
<span>Cập Nhật Xử Lý Issue: <span id="updateModalIssueId" class="font-mono text-cyan-300"></span></span>
|
||||||
|
</h3>
|
||||||
|
<button id="btnCloseUpdateModal" class="text-white/80 hover:text-white p-1 rounded-lg hover:bg-white/10 text-base">
|
||||||
|
<i class="fa-solid fa-xmark"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="formUpdateIssue" class="p-5 space-y-3.5 text-xs">
|
||||||
|
<div>
|
||||||
|
<label class="block font-semibold text-slate-700 mb-1">Trạng thái xử lý:</label>
|
||||||
|
<select id="updateStatus" class="w-full rounded-lg border border-slate-300 px-3 py-2 bg-slate-50 outline-none font-bold">
|
||||||
|
<option value="CHO_XU_LY" class="text-amber-700 font-bold">Chờ xử lý (Mới tiếp nhận)</option>
|
||||||
|
<option value="DANG_XU_LY" class="text-blue-700 font-bold">Đang xử lý (Đội ngũ kỹ thuật đang sửa)</option>
|
||||||
|
<option value="DA_XU_LY" class="text-emerald-700 font-bold">Đã xử lý (Đã fix lỗi / cập nhật số liệu)</option>
|
||||||
|
<option value="DONG" class="text-slate-500 font-bold">Đóng issue (Không phải lỗi / Đã xong)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block font-semibold text-slate-700 mb-1">Cán bộ / Kỹ thuật xử lý:</label>
|
||||||
|
<input type="text" id="updateResolvedBy" placeholder="Ví dụ: Dev IT / Othau" class="w-full rounded-lg border border-slate-300 px-3 py-2 bg-slate-50 outline-none font-medium">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block font-semibold text-slate-700 mb-1">Ghi chú giải pháp / Phản hồi xử lý:</label>
|
||||||
|
<textarea id="updateHandlerNotes" rows="3" placeholder="Ví dụ: Đã bổ sung điều kiện lọc theo mã đơn vị, số liệu đã khớp 100% với report HIS gốc." class="w-full rounded-lg border border-slate-300 p-2.5 bg-slate-50 outline-none leading-relaxed"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pt-2 flex items-center justify-end gap-2 border-t border-slate-200">
|
||||||
|
<button type="button" id="btnCancelUpdate" class="px-4 py-2 font-semibold text-slate-600 hover:bg-slate-100 rounded-lg">
|
||||||
|
Đóng
|
||||||
|
</button>
|
||||||
|
<button type="submit" id="btnSaveUpdateIssue" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-bold shadow flex items-center gap-1.5">
|
||||||
|
<i class="fa-solid fa-floppy-disk"></i>
|
||||||
|
<span>Lưu Cập Nhật</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ======================================================== -->
|
||||||
|
<!-- MODAL 3: XEM ẢNH PHÓNG TO (LIGHTBOX) -->
|
||||||
|
<!-- ======================================================== -->
|
||||||
|
<div id="modalImagePreview" class="fixed inset-0 z-50 bg-slate-950/80 backdrop-blur-md flex items-center justify-center p-4 hidden">
|
||||||
|
<div class="relative max-w-4xl max-h-[90vh] flex flex-col items-center">
|
||||||
|
<button id="btnCloseLightbox" class="absolute -top-10 right-0 text-white/90 hover:text-white text-2xl p-1">
|
||||||
|
<i class="fa-solid fa-xmark"></i>
|
||||||
|
</button>
|
||||||
|
<img id="lightboxImg" src="" class="max-w-full max-h-[85vh] rounded-lg shadow-2xl object-contain border border-white/20">
|
||||||
|
<div id="lightboxCaption" class="mt-2 text-xs text-white/80 font-medium"></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>
|
||||||
|
<span id="toastMsg">Thông báo</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/* Custom styles for DH-HIS Daily Briefing Web */
|
||||||
|
@layer base {
|
||||||
|
body {
|
||||||
|
background-color: #f8fafc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-cell-edit {
|
||||||
|
background-color: #fffbeb;
|
||||||
|
transition: all 0.2s;
|
||||||
|
outline: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px 6px;
|
||||||
|
width: 100%;
|
||||||
|
border: 1px dashed #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-cell-edit:focus {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid #2563eb;
|
||||||
|
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-cell-edit.saving {
|
||||||
|
background-color: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-cell-edit.saved {
|
||||||
|
background-color: #ecfdf5;
|
||||||
|
border-color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Print styling */
|
||||||
|
@media print {
|
||||||
|
header, section, .tab-btn, .chart-type-btn, #btnFilter, #btnPrint, #btnExport, #toast {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background: white !important;
|
||||||
|
font-size: 10pt;
|
||||||
|
}
|
||||||
|
.tab-content {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
border: 1px solid #ccc !important;
|
||||||
|
page-break-inside: auto;
|
||||||
|
}
|
||||||
|
tr {
|
||||||
|
page-break-inside: avoid;
|
||||||
|
page-break-after: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+6
@@ -0,0 +1,6 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Script khởi động lại dịch vụ web-giao-ban
|
||||||
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
|
||||||
|
"$DIR/stop.sh"
|
||||||
|
sleep 1
|
||||||
|
"$DIR/start.sh"
|
||||||
@@ -0,0 +1,533 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const cors = require('cors');
|
||||||
|
const path = require('path');
|
||||||
|
const multer = require('multer');
|
||||||
|
const db = require('./db');
|
||||||
|
const issueService = require('./issueService');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
router.use(express.static(path.join(__dirname, 'public')));
|
||||||
|
router.use('/uploads', express.static(path.join(__dirname, 'uploads')));
|
||||||
|
|
||||||
|
// Multer storage for issue attachments
|
||||||
|
const storage = multer.diskStorage({
|
||||||
|
destination: function (req, file, cb) {
|
||||||
|
issueService.ensureDirs();
|
||||||
|
cb(null, issueService.UPLOADS_DIR);
|
||||||
|
},
|
||||||
|
filename: function (req, file, cb) {
|
||||||
|
const ext = path.extname(file.originalname);
|
||||||
|
const safeBase = path.basename(file.originalname, ext).replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||||
|
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e6);
|
||||||
|
cb(null, `${safeBase}_${uniqueSuffix}${ext}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const upload = multer({
|
||||||
|
storage: storage,
|
||||||
|
limits: { fileSize: 25 * 1024 * 1024 }, // 25MB max
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure giaoban_ghichu table exists
|
||||||
|
async function ensureTable(dbName) {
|
||||||
|
try {
|
||||||
|
await db.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS current.giaoban_ghichu (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
maba VARCHAR(50) NOT NULL,
|
||||||
|
mabn VARCHAR(20) NOT NULL,
|
||||||
|
loai_danhsach VARCHAR(20) NOT NULL,
|
||||||
|
ghichu TEXT,
|
||||||
|
ngay_capnhat TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT unq_giaoban_item_${dbName ? dbName.replace(/[^a-zA-Z0-9]/g, '_') : 'default'} UNIQUE(maba, loai_danhsach)
|
||||||
|
);
|
||||||
|
`, [], dbName);
|
||||||
|
} catch (err) {
|
||||||
|
// If permission or table exists, ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Danh sách database
|
||||||
|
router.get('/api/databases', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const list = [
|
||||||
|
{ id: 'an_omon_full_04052026', name: 'BVĐK Ô Môn - Cần Thơ (33 GB - Đầy đủ khoa phòng chuẩn)' },
|
||||||
|
{ id: 'tuan_dbgovap_full', name: 'BVĐK Gò Vấp - TP.HCM (65 GB - Dữ liệu lớn)' },
|
||||||
|
{ id: 'binh_bvq12_062026052026042026032026022026', name: 'BV Quận 12 - TP.HCM (21 GB)' },
|
||||||
|
{ id: 'bvq12_697', name: 'BV Quận 12 - Bản 697 (17 GB)' },
|
||||||
|
{ id: 'an_dalieu_full_05082026', name: 'BV Da Liễu (31 GB)' },
|
||||||
|
];
|
||||||
|
res.json({ success: true, data: list });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper function to build namkt and thangkt partition filter
|
||||||
|
function getThangNamFilter(fromDateStr, toDateStr, prefix = '') {
|
||||||
|
try {
|
||||||
|
const f = new Date(fromDateStr);
|
||||||
|
const t = new Date(toDateStr);
|
||||||
|
if (isNaN(f.getTime()) || isNaN(t.getTime())) return '';
|
||||||
|
|
||||||
|
const thangs = new Set();
|
||||||
|
const nams = new Set();
|
||||||
|
let curr = new Date(f.getFullYear(), f.getMonth(), 1);
|
||||||
|
const end = new Date(t.getFullYear(), t.getMonth(), 1);
|
||||||
|
let count = 0;
|
||||||
|
while (curr <= end && count < 24) {
|
||||||
|
thangs.add(String(curr.getMonth() + 1).padStart(2, '0'));
|
||||||
|
nams.add(String(curr.getFullYear()));
|
||||||
|
curr.setMonth(curr.getMonth() + 1);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
const p = prefix ? `${prefix}.` : '';
|
||||||
|
const thangsArr = Array.from(thangs).map(m => `'${m}'`).join(',');
|
||||||
|
const namsArr = Array.from(nams).map(y => `'${y}'`).join(',');
|
||||||
|
return ` AND ${p}namkt IN (${namsArr}) AND ${p}thangkt IN (${thangsArr}) `;
|
||||||
|
} catch (e) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper for Outpatient query
|
||||||
|
async function getOutpatientData(database, fromDate, toDate) {
|
||||||
|
const params = [fromDate, toDate];
|
||||||
|
const fnKb = getThangNamFilter(fromDate, toDate, 'kb');
|
||||||
|
const fnLs = getThangNamFilter(fromDate, toDate, 'ls');
|
||||||
|
const fnNone = getThangNamFilter(fromDate, toDate, '');
|
||||||
|
|
||||||
|
// A. Thống kê theo Khoa khám bệnh
|
||||||
|
const sqlKhoa = `
|
||||||
|
SELECT
|
||||||
|
COALESCE(dv.madv, 'KHAC') AS madv,
|
||||||
|
COALESCE(dv.tendv, 'Khoa khác') AS tendv,
|
||||||
|
COUNT(kb.makb) AS tong_kham,
|
||||||
|
SUM(CASE WHEN dt.bhyt IN (1, 2) THEN 1 ELSE 0 END) AS bhyt,
|
||||||
|
SUM(CASE WHEN dt.bhyt IN (0, 4) THEN 1 ELSE 0 END) AS thuphi,
|
||||||
|
SUM(CASE WHEN kb.dakham = 6 THEN 1 ELSE 0 END) AS chuyenvien
|
||||||
|
FROM current.khambenh kb
|
||||||
|
LEFT JOIN current.psdangky dk ON kb.mabn = dk.mabn AND kb.makb = dk.makb AND COALESCE(dk.xoa, 0) = 0
|
||||||
|
LEFT JOIN current.dmdoituong dt ON dk.madt = dt.madt
|
||||||
|
LEFT JOIN current.dmdonvi dv ON kb.madv = dv.madv
|
||||||
|
WHERE kb.ngaykcb BETWEEN $1 AND $2 ${fnKb}
|
||||||
|
AND COALESCE(kb.xoa, 0) = 0
|
||||||
|
AND COALESCE(kb.dakham, 0) != 0
|
||||||
|
GROUP BY dv.madv, dv.tendv
|
||||||
|
ORDER BY tong_kham DESC;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// B. Bệnh án ngoại trú (Mới vs Tái khám)
|
||||||
|
const sqlBANT = `
|
||||||
|
SELECT
|
||||||
|
SUM(CASE WHEN COALESCE(dk.taikham, 0) = 0 THEN 1 ELSE 0 END) AS moi,
|
||||||
|
SUM(CASE WHEN COALESCE(dk.taikham, 0) = 1 THEN 1 ELSE 0 END) AS taikham,
|
||||||
|
COUNT(*) AS tong
|
||||||
|
FROM current.bnnoitru nt
|
||||||
|
JOIN current.psdangky dk ON nt.mabn = dk.mabn AND nt.makb = dk.makb AND COALESCE(dk.xoa, 0) = 0
|
||||||
|
WHERE nt.namvien = 0 AND COALESCE(nt.xoa, 0) = 0
|
||||||
|
AND nt.ngayvv BETWEEN $1 AND $2;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// C. Chuyên khoa YDCT
|
||||||
|
const sqlYDCT = `
|
||||||
|
SELECT
|
||||||
|
SUM(CASE WHEN cls.tencls ILIKE '%Điện châm%' THEN ls.soluong ELSE 0 END) AS dien_cham,
|
||||||
|
SUM(CASE WHEN cls.tencls ILIKE '%Vật lý trị liệu%' OR cls.tencls ILIKE '%VLTL%' OR cls.macls IN ('DI131','DI135') THEN ls.soluong ELSE 0 END) AS vltl,
|
||||||
|
SUM(CASE WHEN cls.tencls ILIKE '%Sắc thuốc%' THEN ls.soluong ELSE 0 END) AS sac_thuoc,
|
||||||
|
SUM(CASE WHEN cls.tencls ILIKE '%Cấy chỉ%' OR cls.tencls ILIKE '%Chôn chỉ%' THEN ls.soluong ELSE 0 END) AS cay_chi
|
||||||
|
FROM current.chidinhcls ls
|
||||||
|
JOIN current.dmcls cls ON ls.macls = cls.macls
|
||||||
|
WHERE ls.ngaykcb BETWEEN $1 AND $2 ${fnLs}
|
||||||
|
AND COALESCE(ls.xoa, 0) = 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// D. Cận lâm sàng
|
||||||
|
const sqlCLS = `
|
||||||
|
SELECT
|
||||||
|
cls.kho,
|
||||||
|
cls.maloai,
|
||||||
|
COALESCE(loaicls.tenloai, cls.maloai) AS tenloai,
|
||||||
|
COUNT(DISTINCT ls.makb) AS so_bn,
|
||||||
|
COUNT(*) AS so_luot
|
||||||
|
FROM current.chidinhcls ls
|
||||||
|
JOIN current.dmcls cls ON ls.macls = cls.macls
|
||||||
|
LEFT JOIN current.dmloaicls loaicls ON cls.maloai = loaicls.maloai
|
||||||
|
WHERE ls.ngaykcb BETWEEN $1 AND $2 ${fnLs}
|
||||||
|
AND COALESCE(ls.xoa, 0) = 0
|
||||||
|
AND cls.kho IN ('XN', 'HA', 'CN')
|
||||||
|
GROUP BY cls.kho, cls.maloai, loaicls.tenloai
|
||||||
|
ORDER BY cls.kho, count(*) DESC;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// E. Top 10 chẩn đoán
|
||||||
|
const sqlTop10 = `
|
||||||
|
SELECT maicd, kqcdoan, COUNT(*) AS sl
|
||||||
|
FROM current.khambenh
|
||||||
|
WHERE ngaykcb BETWEEN $1 AND $2 ${fnNone}
|
||||||
|
AND COALESCE(xoa, 0) = 0
|
||||||
|
AND maicd IS NOT NULL AND maicd != ''
|
||||||
|
GROUP BY maicd, kqcdoan
|
||||||
|
ORDER BY sl DESC
|
||||||
|
LIMIT 10;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// F. Tiêm ngừa
|
||||||
|
const sqlTiemNgua = `
|
||||||
|
SELECT COUNT(kb.makb) AS sl
|
||||||
|
FROM current.khambenh kb
|
||||||
|
LEFT JOIN current.dmphong ph ON kb.maphong = ph.maphong
|
||||||
|
LEFT JOIN current.dmdonvi dv ON kb.madv = dv.madv
|
||||||
|
WHERE kb.ngaykcb BETWEEN $1 AND $2 ${fnKb}
|
||||||
|
AND COALESCE(kb.xoa, 0) = 0
|
||||||
|
AND COALESCE(kb.dakham, 0) != 0
|
||||||
|
AND (
|
||||||
|
COALESCE(ph.loai_bc_giaoban, 0) = 2 OR
|
||||||
|
ph.maphong = 'TN' OR
|
||||||
|
dv.madv = 'PTN' OR
|
||||||
|
ph.tenphong ILIKE '%Tiêm ngừa%' OR
|
||||||
|
ph.tenphong ILIKE '%Tiêm chủng%'
|
||||||
|
);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const [resKhoa, resBANT, resYDCT, resCLS, resTop10, resTiemNgua] = await Promise.all([
|
||||||
|
db.query(sqlKhoa, params, database),
|
||||||
|
db.query(sqlBANT, params, database),
|
||||||
|
db.query(sqlYDCT, params, database),
|
||||||
|
db.query(sqlCLS, params, database),
|
||||||
|
db.query(sqlTop10, params, database),
|
||||||
|
db.query(sqlTiemNgua, params, database),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
khoa: resKhoa.rows,
|
||||||
|
bant: resBANT.rows[0] || { moi: 0, taikham: 0, tong: 0 },
|
||||||
|
ydct: resYDCT.rows[0] || { dien_cham: 0, vltl: 0, sac_thuoc: 0, cay_chi: 0 },
|
||||||
|
cls: resCLS.rows,
|
||||||
|
top10: resTop10.rows,
|
||||||
|
tiemngua: parseInt(resTiemNgua.rows[0]?.sl || 0, 10),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. API Báo cáo Giao ban Ngoại trú
|
||||||
|
router.get('/api/giao-ban-ngoai-tru', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { database, from, to, compareFrom, compareTo } = req.query;
|
||||||
|
const dbName = database || process.env.PG_DATABASE;
|
||||||
|
const fromDate = from || '2026-03-01 00:00:00';
|
||||||
|
const toDate = to || '2026-03-31 23:59:59';
|
||||||
|
|
||||||
|
const currentData = await getOutpatientData(dbName, fromDate, toDate);
|
||||||
|
|
||||||
|
let compareData = null;
|
||||||
|
if (compareFrom && compareTo) {
|
||||||
|
compareData = await getOutpatientData(dbName, compareFrom, compareTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
current: currentData,
|
||||||
|
compare: compareData,
|
||||||
|
periodCurrent: { from: fromDate, to: toDate },
|
||||||
|
periodCompare: compareFrom && compareTo ? { from: compareFrom, to: compareTo } : null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in /api/giao-ban-ngoai-tru:', error);
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper for Inpatient query
|
||||||
|
async function getInpatientData(database, fromDate, toDate) {
|
||||||
|
const params = [fromDate, toDate];
|
||||||
|
|
||||||
|
const sqlKhoa = `
|
||||||
|
SELECT
|
||||||
|
dv.madv,
|
||||||
|
dv.tendv,
|
||||||
|
-- 1. Bệnh cũ (tồn đầu kỳ)
|
||||||
|
SUM(CASE WHEN nt.ngayvv < $1 AND (nt.ravien = 0 OR nt.ngayrv >= $1) THEN 1 ELSE 0 END) AS benh_cu,
|
||||||
|
-- 2. Bệnh mới (vào viện trong kỳ)
|
||||||
|
SUM(CASE WHEN nt.ngayvv BETWEEN $1 AND $2 THEN 1 ELSE 0 END) AS benh_moi,
|
||||||
|
-- 3. Xuất viện:
|
||||||
|
SUM(CASE WHEN nt.ngayrv BETWEEN $1 AND $2 AND nt.ravien = 1 AND COALESCE(nt.makq, '01') NOT IN ('04','05','06') THEN 1 ELSE 0 END) AS on_xin_ve,
|
||||||
|
SUM(CASE WHEN nt.ngayrv BETWEEN $1 AND $2 AND (nt.makq IN ('04','06') OR (nt.maxt = '09' AND nt.makq = '04')) THEN 1 ELSE 0 END) AS nang_xin_ve,
|
||||||
|
SUM(CASE WHEN nt.ngayrv BETWEEN $1 AND $2 AND (nt.ravien = 2 OR nt.maxt = '08') THEN 1 ELSE 0 END) AS tron_vien,
|
||||||
|
SUM(CASE WHEN nt.ngayrv BETWEEN $1 AND $2 AND (nt.ravien = 4 OR nt.maxt = '03') THEN 1 ELSE 0 END) AS chuyen_vien,
|
||||||
|
SUM(CASE WHEN nt.ngayrv BETWEEN $1 AND $2 AND (nt.ravien = 3 OR nt.maxt = '06') THEN 1 ELSE 0 END) AS tu_vong,
|
||||||
|
-- 4. Bệnh hiện còn (tồn cuối kỳ)
|
||||||
|
SUM(CASE WHEN nt.ngayvv <= $2 AND (nt.ravien = 0 OR nt.ngayrv > $2) THEN 1 ELSE 0 END) AS benh_hien_con,
|
||||||
|
-- 5. Bệnh theo dõi nặng
|
||||||
|
SUM(CASE WHEN nt.ngayvv <= $2 AND (nt.ravien = 0 OR nt.ngayrv >= $1) AND (COALESCE(nt.benhnang, 0) = 1 OR COALESCE(nt.mabatheodoi, '') != '' OR dv.madv IN ('17','29')) THEN 1 ELSE 0 END) AS benh_theo_doi,
|
||||||
|
-- 6. Chăm sóc Cấp I
|
||||||
|
SUM(CASE WHEN nt.ngayvv <= $2 AND (nt.ravien = 0 OR nt.ngayrv >= $1) AND (nt.chamsoc ILIKE '%cấp I%' OR nt.chamsoc ILIKE '%cấp 1%' OR nt.chamsoc IN ('1', 'CS I', 'CSI')) THEN 1 ELSE 0 END) AS chamsoc_cap1
|
||||||
|
FROM current.bnnoitru nt
|
||||||
|
JOIN current.dmdonvi dv ON nt.madv = dv.madv
|
||||||
|
WHERE nt.namvien = 1 AND COALESCE(nt.xoa, 0) = 0
|
||||||
|
AND nt.ngayvv >= ($1::date - INTERVAL '180 days')
|
||||||
|
GROUP BY dv.madv, dv.tendv
|
||||||
|
HAVING SUM(CASE WHEN nt.ngayvv <= $2 AND (nt.ravien = 0 OR nt.ngayrv >= $1) THEN 1 ELSE 0 END) > 0
|
||||||
|
ORDER BY benh_moi DESC;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const resKhoa = await db.query(sqlKhoa, params, database);
|
||||||
|
return {
|
||||||
|
khoa: resKhoa.rows,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. API Báo cáo Giao ban Nội trú
|
||||||
|
router.get('/api/giao-ban-noi-tru', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { database, from, to, compareFrom, compareTo } = req.query;
|
||||||
|
const dbName = database || process.env.PG_DATABASE;
|
||||||
|
const fromDate = from || '2026-03-01 00:00:00';
|
||||||
|
const toDate = to || '2026-03-31 23:59:59';
|
||||||
|
|
||||||
|
await ensureTable(dbName);
|
||||||
|
const currentData = await getInpatientData(dbName, fromDate, toDate);
|
||||||
|
|
||||||
|
let compareData = null;
|
||||||
|
if (compareFrom && compareTo) {
|
||||||
|
compareData = await getInpatientData(dbName, compareFrom, compareTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
current: currentData,
|
||||||
|
compare: compareData,
|
||||||
|
periodCurrent: { from: fromDate, to: toDate },
|
||||||
|
periodCompare: compareFrom && compareTo ? { from: compareFrom, to: compareTo } : null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in /api/giao-ban-noi-tru:', error);
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. API Bảng 3: Danh sách Bệnh nhân Chuyển viện
|
||||||
|
router.get('/api/danh-sach-chuyen-vien', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { database, from, to, madv } = req.query;
|
||||||
|
const dbName = database || process.env.PG_DATABASE;
|
||||||
|
const fromDate = from || '2026-03-01 00:00:00';
|
||||||
|
const toDate = to || '2026-03-31 23:59:59';
|
||||||
|
|
||||||
|
await ensureTable(dbName);
|
||||||
|
|
||||||
|
let sqlWhere = `
|
||||||
|
WHERE nt.namvien = 1 AND COALESCE(nt.xoa, 0) = 0
|
||||||
|
AND nt.ngayrv BETWEEN $1 AND $2
|
||||||
|
AND (nt.ravien = 4 OR nt.maxt = '03')
|
||||||
|
`;
|
||||||
|
const params = [fromDate, toDate];
|
||||||
|
if (madv && madv !== 'ALL') {
|
||||||
|
params.push(madv);
|
||||||
|
sqlWhere += ` AND nt.madv = $${params.length} `;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sql = `
|
||||||
|
SELECT
|
||||||
|
nt.mabn,
|
||||||
|
nt.maba,
|
||||||
|
bn.holot || ' ' || bn.ten AS hoten,
|
||||||
|
bn.diachi,
|
||||||
|
to_char(nt.ngayvv, 'DD/MM/YYYY HH24:MI') AS ngayvv,
|
||||||
|
to_char(nt.ngayrv, 'DD/MM/YYYY HH24:MI') AS ngayrv,
|
||||||
|
COALESCE(dv.tendv, nt.madv) AS tendv,
|
||||||
|
nt.kqcdoan AS chandoan_chinh,
|
||||||
|
nt.kqcdoanp AS chandoan_phu,
|
||||||
|
COALESCE(cls_sub.ketqua_cls, 'Chưa có kết quả') AS ketqua_cls,
|
||||||
|
COALESCE(gb.ghichu, nt.ghichu, '') AS ghichu
|
||||||
|
FROM current.bnnoitru nt
|
||||||
|
JOIN current.dmbenhnhan bn ON nt.mabn = bn.mabn
|
||||||
|
LEFT JOIN current.dmdonvi dv ON nt.madv = dv.madv
|
||||||
|
LEFT JOIN current.giaoban_ghichu gb ON nt.maba = gb.maba AND gb.loai_danhsach = 'CHUYEN_VIEN'
|
||||||
|
LEFT JOIN LATERAL (
|
||||||
|
SELECT string_agg(DISTINCT cls.tencls || COALESCE(': ' || NULLIF(ha.ketluan,''), ''), '; ') AS ketqua_cls
|
||||||
|
FROM current.chidinhcls ls
|
||||||
|
JOIN current.dmcls cls ON ls.macls = cls.macls
|
||||||
|
LEFT JOIN current.pskhamha ha ON ls.mabn = ha.mabn AND ls.macls = ha.macls AND ls.makb = ha.makb
|
||||||
|
WHERE ls.mabn = nt.mabn AND ls.makb = nt.makb AND COALESCE(ls.xoa, 0) = 0
|
||||||
|
) cls_sub ON true
|
||||||
|
${sqlWhere}
|
||||||
|
ORDER BY nt.ngayrv DESC
|
||||||
|
LIMIT 100;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await db.query(sql, params, dbName);
|
||||||
|
const cleanedRows = result.rows.map(row => ({
|
||||||
|
...row,
|
||||||
|
ketqua_cls: db.cleanRtf(row.ketqua_cls)
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.json({ success: true, data: cleanedRows });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in /api/danh-sach-chuyen-vien:', error);
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5. API Bảng 4: Danh sách Bệnh nhân Theo dõi & Chăm sóc Cấp I
|
||||||
|
router.get('/api/danh-sach-theo-doi', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { database, from, to, madv } = req.query;
|
||||||
|
const dbName = database || process.env.PG_DATABASE;
|
||||||
|
const fromDate = from || '2026-03-01 00:00:00';
|
||||||
|
const toDate = to || '2026-03-31 23:59:59';
|
||||||
|
|
||||||
|
await ensureTable(dbName);
|
||||||
|
|
||||||
|
let sqlWhere = `
|
||||||
|
WHERE nt.namvien = 1 AND COALESCE(nt.xoa, 0) = 0
|
||||||
|
AND (nt.ngayvv <= $2 AND (nt.ravien = 0 OR nt.ngayrv >= $1))
|
||||||
|
AND (
|
||||||
|
COALESCE(nt.benhnang, 0) = 1 OR
|
||||||
|
COALESCE(nt.mabatheodoi, '') != '' OR
|
||||||
|
nt.chamsoc ILIKE '%cấp I%' OR
|
||||||
|
nt.chamsoc ILIKE '%cấp 1%' OR
|
||||||
|
nt.chamsoc IN ('1', 'CS I', 'CSI') OR
|
||||||
|
nt.madv IN ('17', '29')
|
||||||
|
)
|
||||||
|
`;
|
||||||
|
const params = [fromDate, toDate];
|
||||||
|
if (madv && madv !== 'ALL') {
|
||||||
|
params.push(madv);
|
||||||
|
sqlWhere += ` AND nt.madv = $${params.length} `;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sql = `
|
||||||
|
SELECT
|
||||||
|
nt.mabn,
|
||||||
|
nt.maba,
|
||||||
|
bn.holot || ' ' || bn.ten AS hoten,
|
||||||
|
bn.diachi,
|
||||||
|
to_char(nt.ngayvv, 'DD/MM/YYYY HH24:MI') AS ngayvv,
|
||||||
|
to_char(nt.ngayrv, 'DD/MM/YYYY HH24:MI') AS ngayrv,
|
||||||
|
COALESCE(dv.tendv, nt.madv) AS tendv,
|
||||||
|
nt.kqcdoan AS chandoan_chinh,
|
||||||
|
nt.kqcdoanp AS chandoan_phu,
|
||||||
|
COALESCE(nt.chamsoc, 'Theo dõi đặc biệt') AS chamsoc,
|
||||||
|
COALESCE(cls_sub.ketqua_cls, 'Chưa có kết quả') AS ketqua_cls,
|
||||||
|
COALESCE(gb.ghichu, nt.ghichu, '') AS ghichu
|
||||||
|
FROM current.bnnoitru nt
|
||||||
|
JOIN current.dmbenhnhan bn ON nt.mabn = bn.mabn
|
||||||
|
LEFT JOIN current.dmdonvi dv ON nt.madv = dv.madv
|
||||||
|
LEFT JOIN current.giaoban_ghichu gb ON nt.maba = gb.maba AND gb.loai_danhsach = 'THEO_DOI'
|
||||||
|
LEFT JOIN LATERAL (
|
||||||
|
SELECT string_agg(DISTINCT cls.tencls || COALESCE(': ' || NULLIF(ha.ketluan,''), ''), '; ') AS ketqua_cls
|
||||||
|
FROM current.chidinhcls ls
|
||||||
|
JOIN current.dmcls cls ON ls.macls = cls.macls
|
||||||
|
LEFT JOIN current.pskhamha ha ON ls.mabn = ha.mabn AND ls.macls = ha.macls AND ls.makb = ha.makb
|
||||||
|
WHERE ls.mabn = nt.mabn AND ls.makb = nt.makb AND COALESCE(ls.xoa, 0) = 0
|
||||||
|
) cls_sub ON true
|
||||||
|
${sqlWhere}
|
||||||
|
ORDER BY nt.ngayvv DESC
|
||||||
|
LIMIT 100;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await db.query(sql, params, dbName);
|
||||||
|
const cleanedRows = result.rows.map(row => ({
|
||||||
|
...row,
|
||||||
|
ketqua_cls: db.cleanRtf(row.ketqua_cls)
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.json({ success: true, data: cleanedRows });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in /api/danh-sach-theo-doi:', error);
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 6. API Cập nhật Ghi chú trực tiếp từ lưới (Bảng 3 & Bảng 4)
|
||||||
|
router.post('/api/cap-nhat-ghi-chu', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { maba, mabn, loai, ghichu, database } = req.body;
|
||||||
|
if (!maba || !loai) {
|
||||||
|
return res.status(400).json({ success: false, message: 'Thiếu maba hoặc loại danh sách' });
|
||||||
|
}
|
||||||
|
const dbName = database || process.env.PG_DATABASE;
|
||||||
|
await ensureTable(dbName);
|
||||||
|
|
||||||
|
const sqlUpsert = `
|
||||||
|
INSERT INTO current.giaoban_ghichu (maba, mabn, loai_danhsach, ghichu, ngay_capnhat)
|
||||||
|
VALUES ($1, $2, $3, $4, NOW())
|
||||||
|
ON CONFLICT (maba, loai_danhsach)
|
||||||
|
DO UPDATE SET ghichu = EXCLUDED.ghichu, ngay_capnhat = NOW();
|
||||||
|
`;
|
||||||
|
await db.query(sqlUpsert, [maba, mabn || '', loai, ghichu || ''], dbName);
|
||||||
|
|
||||||
|
// Đồng bộ vào bnnoitru.ghichu nếu cần
|
||||||
|
try {
|
||||||
|
await db.query(`UPDATE current.bnnoitru SET ghichu = $1 WHERE maba = $2;`, [ghichu || '', maba], dbName);
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true, message: 'Đã lưu ghi chú thành công!' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in /api/cap-nhat-ghi-chu:', error);
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 7. API Quản lý Issue & Báo lỗi (lưu file và tổng hợp)
|
||||||
|
router.get('/api/issues', (req, res) => {
|
||||||
|
try {
|
||||||
|
const list = issueService.getAllIssues();
|
||||||
|
res.json({ success: true, data: list });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/api/issues', upload.single('attachment'), (req, res) => {
|
||||||
|
try {
|
||||||
|
const issue = issueService.createIssue(req.body, req.file);
|
||||||
|
res.json({ success: true, data: issue, message: 'Đã ghi nhận báo lỗi thành công!' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating issue:', error);
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put('/api/issues/:id', (req, res) => {
|
||||||
|
try {
|
||||||
|
const updated = issueService.updateIssue(req.params.id, req.body);
|
||||||
|
res.json({ success: true, data: updated, message: 'Cập nhật trạng thái issue thành công!' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating issue:', error);
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete('/api/issues/:id', (req, res) => {
|
||||||
|
try {
|
||||||
|
issueService.deleteIssue(req.params.id);
|
||||||
|
res.json({ success: true, message: 'Đã xóa issue thành công!' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting issue:', error);
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mount router on both /web-giao-ban and /
|
||||||
|
app.use('/web-giao-ban', router);
|
||||||
|
app.use('/', router);
|
||||||
|
|
||||||
|
app.listen(PORT, '0.0.0.0', () => {
|
||||||
|
console.log(`=======================================================`);
|
||||||
|
console.log(`[DH-HIS] Phần mềm Quản lý Giao ban hằng ngày (Node.js)`);
|
||||||
|
console.log(`Server is running at: http://localhost:${PORT}`);
|
||||||
|
console.log(`=======================================================`);
|
||||||
|
});
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Script khởi động dịch vụ web-giao-ban
|
||||||
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
|
||||||
|
cd "$DIR"
|
||||||
|
|
||||||
|
if systemctl is-enabled web-giao-ban.service >/dev/null 2>&1; then
|
||||||
|
echo "Starting web-giao-ban via systemd..."
|
||||||
|
sudo systemctl start web-giao-ban
|
||||||
|
sleep 1
|
||||||
|
sudo systemctl status web-giao-ban --no-pager
|
||||||
|
else
|
||||||
|
PID_FILE="$DIR/.app.pid"
|
||||||
|
if [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
|
||||||
|
echo "web-giao-ban is already running (PID: $(cat "$PID_FILE"))"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Starting web-giao-ban in background..."
|
||||||
|
nohup /usr/bin/node server.js > app.log 2> error.log &
|
||||||
|
echo $! > "$PID_FILE"
|
||||||
|
echo "Started with PID: $!"
|
||||||
|
fi
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Script kiểm tra trạng thái web-giao-ban
|
||||||
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
|
||||||
|
cd "$DIR"
|
||||||
|
|
||||||
|
echo "=== TRẠNG THÁI DỊCH VỤ WEB-GIAO-BAN ==="
|
||||||
|
if systemctl is-active web-giao-ban.service >/dev/null 2>&1; then
|
||||||
|
sudo systemctl status web-giao-ban --no-pager
|
||||||
|
else
|
||||||
|
PID_FILE="$DIR/.app.pid"
|
||||||
|
if [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
|
||||||
|
echo "Status: RUNNING (PID: $(cat "$PID_FILE"))"
|
||||||
|
else
|
||||||
|
echo "Status: STOPPED"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== KIỂM TRA KẾT NỐI PORT 3000 ==="
|
||||||
|
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:3000/web-giao-ban/ || echo "ERR")
|
||||||
|
echo "HTTP Status: $HTTP_CODE (http://127.0.0.1:3000/web-giao-ban/)"
|
||||||
|
|
||||||
|
if [ "$HTTP_CODE" == "200" ]; then
|
||||||
|
echo "Dịch vụ web-giao-ban đang hoạt động tốt!"
|
||||||
|
else
|
||||||
|
echo "Cảnh báo: Không thể truy cập cổng 3000 hoặc mã trả về không phải 200."
|
||||||
|
fi
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Script dừng dịch vụ web-giao-ban
|
||||||
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
|
||||||
|
cd "$DIR"
|
||||||
|
|
||||||
|
if systemctl is-active web-giao-ban.service >/dev/null 2>&1; then
|
||||||
|
echo "Stopping web-giao-ban via systemd..."
|
||||||
|
sudo systemctl stop web-giao-ban
|
||||||
|
echo "Stopped."
|
||||||
|
else
|
||||||
|
PID_FILE="$DIR/.app.pid"
|
||||||
|
if [ -f "$PID_FILE" ]; then
|
||||||
|
PID="$(cat "$PID_FILE")"
|
||||||
|
if kill -0 "$PID" 2>/dev/null; then
|
||||||
|
echo "Stopping web-giao-ban (PID: $PID)..."
|
||||||
|
kill "$PID" 2>/dev/null || true
|
||||||
|
sleep 1
|
||||||
|
fi
|
||||||
|
rm -f "$PID_FILE"
|
||||||
|
echo "Stopped."
|
||||||
|
else
|
||||||
|
# Kill by port 3000 if running
|
||||||
|
PID=$(lsof -t -i:3000 2>/dev/null || fuser 3000/tcp 2>/dev/null)
|
||||||
|
if [ -n "$PID" ]; then
|
||||||
|
echo "Killing process on port 3000 (PID: $PID)..."
|
||||||
|
kill -9 $PID 2>/dev/null || true
|
||||||
|
echo "Stopped."
|
||||||
|
else
|
||||||
|
echo "web-giao-ban is not running."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
Reference in New Issue
Block a user