feat: DH-HIS Web Giao Ban Go v1.0.1 (Favicon, Exe icon, System Tray, Gitea auto-update)
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"web-giao-ban-go/config"
|
||||
"web-giao-ban-go/db"
|
||||
"web-giao-ban-go/logger"
|
||||
"web-giao-ban-go/updater"
|
||||
)
|
||||
|
||||
// GET /api/admin/config
|
||||
func HandleGetAdminConfig(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
cfg := config.Get()
|
||||
|
||||
// Mask sensitive fields
|
||||
respCfg := *cfg
|
||||
if respCfg.Server.AdminPassword != "" {
|
||||
respCfg.Server.AdminPassword = "••••••••"
|
||||
}
|
||||
if respCfg.Database.Password != "" {
|
||||
respCfg.Database.Password = "••••••••"
|
||||
}
|
||||
if respCfg.Updater.Token != "" {
|
||||
respCfg.Updater.Token = "••••••••"
|
||||
}
|
||||
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": true,
|
||||
"data": respCfg,
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/admin/config
|
||||
func HandleSaveAdminConfig(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
var input config.AppConfig
|
||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{"success": false, "message": "JSON không hợp lệ: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
current := config.Get()
|
||||
|
||||
// Handle Admin Password
|
||||
if input.Server.AdminPassword == "" || input.Server.AdminPassword == "••••••••" {
|
||||
input.Server.AdminPassword = current.Server.AdminPassword
|
||||
input.DecryptedAdminPassword = current.DecryptedAdminPassword
|
||||
} else {
|
||||
input.DecryptedAdminPassword = input.Server.AdminPassword
|
||||
enc, _ := config.Encrypt(input.Server.AdminPassword)
|
||||
input.Server.AdminPassword = enc
|
||||
}
|
||||
|
||||
// Handle DB Password
|
||||
if input.Database.Password == "" || input.Database.Password == "••••••••" {
|
||||
input.Database.Password = current.Database.Password
|
||||
input.DecryptedDBPassword = current.DecryptedDBPassword
|
||||
} else {
|
||||
input.DecryptedDBPassword = input.Database.Password
|
||||
enc, _ := config.Encrypt(input.Database.Password)
|
||||
input.Database.Password = enc
|
||||
}
|
||||
|
||||
// Handle Updater Token
|
||||
if input.Updater.Token == "" || input.Updater.Token == "••••••••" {
|
||||
input.Updater.Token = current.Updater.Token
|
||||
input.DecryptedUpdaterToken = current.DecryptedUpdaterToken
|
||||
} else {
|
||||
input.DecryptedUpdaterToken = input.Updater.Token
|
||||
enc, _ := config.Encrypt(input.Updater.Token)
|
||||
input.Updater.Token = enc
|
||||
}
|
||||
|
||||
if err := config.SaveConfig(&input); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{"success": false, "message": "Lưu file thất bại: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Reset DB pool to apply new connection parameters
|
||||
db.CloseAll()
|
||||
logger.Info("Đã lưu cấu hình mới và làm mới kết nối CSDL")
|
||||
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "Đã lưu cấu hình và mã hóa mật khẩu thành công!",
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/admin/test-db
|
||||
func HandleTestDBConnection(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
var body struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
User string `json:"user"`
|
||||
Password string `json:"password"`
|
||||
Database string `json:"database"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{"success": false, "message": "Dữ liệu không hợp lệ"})
|
||||
return
|
||||
}
|
||||
|
||||
cfg := config.Get()
|
||||
pass := body.Password
|
||||
if pass == "" || pass == "••••••••" {
|
||||
pass = cfg.DecryptedDBPassword
|
||||
}
|
||||
|
||||
err := db.TestConnection(body.Host, body.Port, body.User, pass, body.Database)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "Kết nối thất bại: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "Kết nối PostgreSQL thành công!",
|
||||
})
|
||||
}
|
||||
|
||||
// GET /api/admin/logs
|
||||
func HandleGetLogs(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
limitStr := r.URL.Query().Get("limit")
|
||||
level := r.URL.Query().Get("level")
|
||||
|
||||
limit := 100
|
||||
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 {
|
||||
limit = l
|
||||
}
|
||||
|
||||
entries := logger.Get().GetEntries(limit, strings.ToUpper(level))
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": true,
|
||||
"data": entries,
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/admin/logs/clear
|
||||
func HandleClearLogs(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
logger.Get().Clear()
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "Đã làm trống bộ nhớ log!",
|
||||
})
|
||||
}
|
||||
|
||||
// GET /api/update/check
|
||||
func HandleCheckUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
res, err := updater.CheckUpdate()
|
||||
if err != nil {
|
||||
logger.Error("Kiểm tra cập nhật lỗi: %v", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": true,
|
||||
"data": res,
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/update/apply
|
||||
func HandleApplyUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
var body struct {
|
||||
AssetURL string `json:"asset_url"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.AssetURL == "" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{"success": false, "message": "Thiếu thông tin URL bản cập nhật"})
|
||||
return
|
||||
}
|
||||
|
||||
err := updater.ApplyUpdate(body.AssetURL)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "Cập nhật thất bại: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Update current version in config if provided
|
||||
if body.Version != "" {
|
||||
cfg := config.Get()
|
||||
cfg.Updater.CurrentVersion = body.Version
|
||||
_ = config.SaveConfig(cfg)
|
||||
}
|
||||
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "Đã cập nhật phiên bản mới thành công! Vui lòng khởi động lại phần mềm để áp dụng.",
|
||||
})
|
||||
}
|
||||
+681
@@ -0,0 +1,681 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"web-giao-ban-go/config"
|
||||
"web-giao-ban-go/db"
|
||||
)
|
||||
|
||||
// Helper to generate partition filters: namkt IN (...) AND thangkt IN (...)
|
||||
func GetThangNamFilter(fromStr, toStr, prefix string) string {
|
||||
f, err1 := time.Parse("2006-01-02 15:04:05", fromStr)
|
||||
t, err2 := time.Parse("2006-01-02 15:04:05", toStr)
|
||||
if err1 != nil || err2 != nil {
|
||||
// Try date only
|
||||
f, err1 = time.Parse("2006-01-02", fromStr[:10])
|
||||
t, err2 = time.Parse("2006-01-02", toStr[:10])
|
||||
if err1 != nil || err2 != nil {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
thangsMap := make(map[string]bool)
|
||||
namsMap := make(map[string]bool)
|
||||
|
||||
curr := time.Date(f.Year(), f.Month(), 1, 0, 0, 0, 0, time.UTC)
|
||||
end := time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, time.UTC)
|
||||
count := 0
|
||||
|
||||
for !curr.After(end) && count < 24 {
|
||||
thangsMap[fmt.Sprintf("'%02d'", curr.Month())] = true
|
||||
namsMap[fmt.Sprintf("'%d'", curr.Year())] = true
|
||||
curr = curr.AddDate(0, 1, 0)
|
||||
count++
|
||||
}
|
||||
|
||||
var thangsArr, namsArr []string
|
||||
for k := range thangsMap {
|
||||
thangsArr = append(thangsArr, k)
|
||||
}
|
||||
for k := range namsMap {
|
||||
namsArr = append(namsArr, k)
|
||||
}
|
||||
|
||||
if len(thangsArr) == 0 || len(namsArr) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
p := ""
|
||||
if prefix != "" {
|
||||
p = prefix + "."
|
||||
}
|
||||
return fmt.Sprintf(" AND %snamkt IN (%s) AND %sthangkt IN (%s) ",
|
||||
p, strings.Join(namsArr, ","),
|
||||
p, strings.Join(thangsArr, ","),
|
||||
)
|
||||
}
|
||||
|
||||
// GET /api/databases
|
||||
func HandleGetDatabases(w http.ResponseWriter, r *http.Request) {
|
||||
cfg := config.Get()
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": true,
|
||||
"data": cfg.Database.Databases,
|
||||
})
|
||||
}
|
||||
|
||||
type OutpatientData struct {
|
||||
Khoa []map[string]interface{} `json:"khoa"`
|
||||
BANT map[string]interface{} `json:"bant"`
|
||||
YDCT map[string]interface{} `json:"ydct"`
|
||||
CLS []map[string]interface{} `json:"cls"`
|
||||
Top10 []map[string]interface{} `json:"top10"`
|
||||
TiemNgua int `json:"tiemngua"`
|
||||
}
|
||||
|
||||
func queryOutpatient(database, fromDate, toDate string) (*OutpatientData, error) {
|
||||
conn, err := db.GetDB(database)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fnKb := GetThangNamFilter(fromDate, toDate, "kb")
|
||||
fnLs := GetThangNamFilter(fromDate, toDate, "ls")
|
||||
fnNone := GetThangNamFilter(fromDate, toDate, "")
|
||||
|
||||
out := &OutpatientData{
|
||||
Khoa: make([]map[string]interface{}, 0),
|
||||
BANT: map[string]interface{}{"moi": 0, "taikham": 0, "tong": 0},
|
||||
YDCT: map[string]interface{}{"dien_cham": 0, "vltl": 0, "sac_thuoc": 0, "cay_chi": 0},
|
||||
CLS: make([]map[string]interface{}, 0),
|
||||
Top10: make([]map[string]interface{}, 0),
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex
|
||||
|
||||
// 1. Khoa KCB
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
sqlKhoa := fmt.Sprintf(`
|
||||
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 %s
|
||||
AND COALESCE(kb.xoa, 0) = 0
|
||||
AND COALESCE(kb.dakham, 0) != 0
|
||||
GROUP BY dv.madv, dv.tendv
|
||||
ORDER BY tong_kham DESC;
|
||||
`, fnKb)
|
||||
|
||||
rows, err := conn.Query(sqlKhoa, fromDate, toDate)
|
||||
if err == nil {
|
||||
defer rows.Close()
|
||||
var list []map[string]interface{}
|
||||
for rows.Next() {
|
||||
var madv, tendv string
|
||||
var tong, bhyt, thuphi, cv int
|
||||
_ = rows.Scan(&madv, &tendv, &tong, &bhyt, &thuphi, &cv)
|
||||
list = append(list, map[string]interface{}{
|
||||
"madv": madv, "tendv": tendv, "tong_kham": tong,
|
||||
"bhyt": bhyt, "thuphi": thuphi, "chuyenvien": cv,
|
||||
})
|
||||
}
|
||||
mu.Lock()
|
||||
out.Khoa = list
|
||||
mu.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
// 2. BANT
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
sqlBant := `
|
||||
SELECT
|
||||
COALESCE(SUM(CASE WHEN COALESCE(dk.taikham, 0) = 0 THEN 1 ELSE 0 END), 0) AS moi,
|
||||
COALESCE(SUM(CASE WHEN COALESCE(dk.taikham, 0) = 1 THEN 1 ELSE 0 END), 0) 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;
|
||||
`
|
||||
var moi, taikham, tong int
|
||||
err := conn.QueryRow(sqlBant, fromDate, toDate).Scan(&moi, &taikham, &tong)
|
||||
if err == nil {
|
||||
mu.Lock()
|
||||
out.BANT = map[string]interface{}{"moi": moi, "taikham": taikham, "tong": tong}
|
||||
mu.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
// 3. YDCT
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
sqlYdct := fmt.Sprintf(`
|
||||
SELECT
|
||||
COALESCE(SUM(CASE WHEN cls.tencls ILIKE '%%Điện châm%%' THEN ls.soluong ELSE 0 END), 0) AS dien_cham,
|
||||
COALESCE(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), 0) AS vltl,
|
||||
COALESCE(SUM(CASE WHEN cls.tencls ILIKE '%%Sắc thuốc%%' THEN ls.soluong ELSE 0 END), 0) AS sac_thuoc,
|
||||
COALESCE(SUM(CASE WHEN cls.tencls ILIKE '%%Cấy chỉ%%' OR cls.tencls ILIKE '%%Chôn chỉ%%' THEN ls.soluong ELSE 0 END), 0) AS cay_chi
|
||||
FROM current.chidinhcls ls
|
||||
JOIN current.dmcls cls ON ls.macls = cls.macls
|
||||
WHERE ls.ngaykcb BETWEEN $1 AND $2 %s
|
||||
AND COALESCE(ls.xoa, 0) = 0;
|
||||
`, fnLs)
|
||||
var dc, vltl, st, cc float64
|
||||
err := conn.QueryRow(sqlYdct, fromDate, toDate).Scan(&dc, &vltl, &st, &cc)
|
||||
if err == nil {
|
||||
mu.Lock()
|
||||
out.YDCT = map[string]interface{}{
|
||||
"dien_cham": int(dc), "vltl": int(vltl), "sac_thuoc": int(st), "cay_chi": int(cc),
|
||||
}
|
||||
mu.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
// 4. CLS
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
sqlCls := fmt.Sprintf(`
|
||||
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 %s
|
||||
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;
|
||||
`, fnLs)
|
||||
rows, err := conn.Query(sqlCls, fromDate, toDate)
|
||||
if err == nil {
|
||||
defer rows.Close()
|
||||
var list []map[string]interface{}
|
||||
for rows.Next() {
|
||||
var kho, maloai, tenloai string
|
||||
var soBn, soLuot int
|
||||
_ = rows.Scan(&kho, &maloai, &tenloai, &soBn, &soLuot)
|
||||
list = append(list, map[string]interface{}{
|
||||
"kho": kho, "maloai": maloai, "tenloai": tenloai, "so_bn": soBn, "so_luot": soLuot,
|
||||
})
|
||||
}
|
||||
mu.Lock()
|
||||
out.CLS = list
|
||||
mu.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
// 5. Top 10
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
sqlTop10 := fmt.Sprintf(`
|
||||
SELECT maicd, kqcdoan, COUNT(*) AS sl
|
||||
FROM current.khambenh
|
||||
WHERE ngaykcb BETWEEN $1 AND $2 %s
|
||||
AND COALESCE(xoa, 0) = 0
|
||||
AND maicd IS NOT NULL AND maicd != ''
|
||||
GROUP BY maicd, kqcdoan
|
||||
ORDER BY sl DESC
|
||||
LIMIT 10;
|
||||
`, fnNone)
|
||||
rows, err := conn.Query(sqlTop10, fromDate, toDate)
|
||||
if err == nil {
|
||||
defer rows.Close()
|
||||
var list []map[string]interface{}
|
||||
for rows.Next() {
|
||||
var maicd, kqcdoan string
|
||||
var sl int
|
||||
_ = rows.Scan(&maicd, &kqcdoan, &sl)
|
||||
list = append(list, map[string]interface{}{
|
||||
"maicd": maicd, "kqcdoan": kqcdoan, "sl": sl,
|
||||
})
|
||||
}
|
||||
mu.Lock()
|
||||
out.Top10 = list
|
||||
mu.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
// 6. Tiêm ngừa
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
sqlTn := fmt.Sprintf(`
|
||||
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 %s
|
||||
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%%'
|
||||
);
|
||||
`, fnKb)
|
||||
var sl int
|
||||
err := conn.QueryRow(sqlTn, fromDate, toDate).Scan(&sl)
|
||||
if err == nil {
|
||||
mu.Lock()
|
||||
out.TiemNgua = sl
|
||||
mu.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// GET /api/giao-ban-ngoai-tru
|
||||
func HandleGiaoBanNgoaiTru(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
q := r.URL.Query()
|
||||
database := q.Get("database")
|
||||
from := q.Get("from")
|
||||
to := q.Get("to")
|
||||
compareFrom := q.Get("compareFrom")
|
||||
compareTo := q.Get("compareTo")
|
||||
|
||||
if from == "" {
|
||||
from = "2026-03-01 00:00:00"
|
||||
}
|
||||
if to == "" {
|
||||
to = "2026-03-31 23:59:59"
|
||||
}
|
||||
|
||||
curData, err := queryOutpatient(database, from, to)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var compData *OutpatientData
|
||||
if compareFrom != "" && compareTo != "" {
|
||||
compData, _ = queryOutpatient(database, compareFrom, compareTo)
|
||||
}
|
||||
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": true,
|
||||
"data": map[string]interface{}{
|
||||
"current": curData,
|
||||
"compare": compData,
|
||||
"periodCurrent": map[string]string{
|
||||
"from": from, "to": to,
|
||||
},
|
||||
"periodCompare": func() interface{} {
|
||||
if compareFrom != "" && compareTo != "" {
|
||||
return map[string]string{"from": compareFrom, "to": compareTo}
|
||||
}
|
||||
return nil
|
||||
}(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
type InpatientData struct {
|
||||
Khoa []map[string]interface{} `json:"khoa"`
|
||||
}
|
||||
|
||||
func queryInpatient(database, fromDate, toDate string) (*InpatientData, error) {
|
||||
conn, err := db.GetDB(database)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sqlKhoa := `
|
||||
SELECT
|
||||
dv.madv,
|
||||
dv.tendv,
|
||||
SUM(CASE WHEN nt.ngayvv < $1 AND (nt.ravien = 0 OR nt.ngayrv >= $1) THEN 1 ELSE 0 END) AS benh_cu,
|
||||
SUM(CASE WHEN nt.ngayvv BETWEEN $1 AND $2 THEN 1 ELSE 0 END) AS benh_moi,
|
||||
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,
|
||||
SUM(CASE WHEN nt.ngayvv <= $2 AND (nt.ravien = 0 OR nt.ngayrv > $2) THEN 1 ELSE 0 END) AS benh_hien_con,
|
||||
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,
|
||||
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;
|
||||
`
|
||||
|
||||
rows, err := conn.Query(sqlKhoa, fromDate, toDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
list := make([]map[string]interface{}, 0)
|
||||
for rows.Next() {
|
||||
var madv, tendv string
|
||||
var cu, moi, on, nang, tron, cv, tv, hiencon, td, cs1 int
|
||||
_ = rows.Scan(&madv, &tendv, &cu, &moi, &on, &nang, &tron, &cv, &tv, &hiencon, &td, &cs1)
|
||||
list = append(list, map[string]interface{}{
|
||||
"madv": madv, "tendv": tendv,
|
||||
"benh_cu": cu, "benh_moi": moi, "on_xin_ve": on, "nang_xin_ve": nang,
|
||||
"tron_vien": tron, "chuyen_vien": cv, "tu_vong": tv, "benh_hien_con": hiencon,
|
||||
"benh_theo_doi": td, "chamsoc_cap1": cs1,
|
||||
})
|
||||
}
|
||||
|
||||
return &InpatientData{Khoa: list}, nil
|
||||
}
|
||||
|
||||
// GET /api/giao-ban-noi-tru
|
||||
func HandleGiaoBanNoiTru(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
q := r.URL.Query()
|
||||
database := q.Get("database")
|
||||
from := q.Get("from")
|
||||
to := q.Get("to")
|
||||
compareFrom := q.Get("compareFrom")
|
||||
compareTo := q.Get("compareTo")
|
||||
|
||||
if from == "" {
|
||||
from = "2026-03-01 00:00:00"
|
||||
}
|
||||
if to == "" {
|
||||
to = "2026-03-31 23:59:59"
|
||||
}
|
||||
|
||||
db.EnsureTable(database)
|
||||
|
||||
curData, err := queryInpatient(database, from, to)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var compData *InpatientData
|
||||
if compareFrom != "" && compareTo != "" {
|
||||
compData, _ = queryInpatient(database, compareFrom, compareTo)
|
||||
}
|
||||
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": true,
|
||||
"data": map[string]interface{}{
|
||||
"current": curData,
|
||||
"compare": compData,
|
||||
"periodCurrent": map[string]string{
|
||||
"from": from, "to": to,
|
||||
},
|
||||
"periodCompare": func() interface{} {
|
||||
if compareFrom != "" && compareTo != "" {
|
||||
return map[string]string{"from": compareFrom, "to": compareTo}
|
||||
}
|
||||
return nil
|
||||
}(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GET /api/danh-sach-chuyen-vien
|
||||
func HandleDanhSachChuyenVien(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
q := r.URL.Query()
|
||||
database := q.Get("database")
|
||||
from := q.Get("from")
|
||||
to := q.Get("to")
|
||||
madv := q.Get("madv")
|
||||
|
||||
if from == "" {
|
||||
from = "2026-03-01 00:00:00"
|
||||
}
|
||||
if to == "" {
|
||||
to = "2026-03-31 23:59:59"
|
||||
}
|
||||
|
||||
conn, err := db.GetDB(database)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
db.EnsureTable(database)
|
||||
|
||||
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')
|
||||
`
|
||||
args := []interface{}{from, to}
|
||||
if madv != "" && madv != "ALL" {
|
||||
args = append(args, madv)
|
||||
sqlWhere += fmt.Sprintf(" AND nt.madv = $%d ", len(args))
|
||||
}
|
||||
|
||||
sqlStr := fmt.Sprintf(`
|
||||
SELECT
|
||||
nt.mabn,
|
||||
nt.maba,
|
||||
bn.holot || ' ' || bn.ten AS hoten,
|
||||
COALESCE(bn.diachi, '') AS 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,
|
||||
COALESCE(nt.kqcdoan, '') AS chandoan_chinh,
|
||||
COALESCE(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
|
||||
%s
|
||||
ORDER BY nt.ngayrv DESC
|
||||
LIMIT 100;
|
||||
`, sqlWhere)
|
||||
|
||||
rows, err := conn.Query(sqlStr, args...)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
list := make([]map[string]interface{}, 0)
|
||||
for rows.Next() {
|
||||
var mabn, maba, hoten, diachi, ngayvv, ngayrv, tendv, cdChinh, cdPhu, cls, ghichu string
|
||||
_ = rows.Scan(&mabn, &maba, &hoten, &diachi, &ngayvv, &ngayrv, &tendv, &cdChinh, &cdPhu, &cls, &ghichu)
|
||||
list = append(list, map[string]interface{}{
|
||||
"mabn": mabn, "maba": maba, "hoten": hoten, "diachi": diachi,
|
||||
"ngayvv": ngayvv, "ngayrv": ngayrv, "tendv": tendv,
|
||||
"chandoan_chinh": cdChinh, "chandoan_phu": cdPhu,
|
||||
"ketqua_cls": db.CleanRTF(cls), "ghichu": ghichu,
|
||||
})
|
||||
}
|
||||
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": true,
|
||||
"data": list,
|
||||
})
|
||||
}
|
||||
|
||||
// GET /api/danh-sach-theo-doi
|
||||
func HandleDanhSachTheoDoi(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
q := r.URL.Query()
|
||||
database := q.Get("database")
|
||||
from := q.Get("from")
|
||||
to := q.Get("to")
|
||||
madv := q.Get("madv")
|
||||
|
||||
if from == "" {
|
||||
from = "2026-03-01 00:00:00"
|
||||
}
|
||||
if to == "" {
|
||||
to = "2026-03-31 23:59:59"
|
||||
}
|
||||
|
||||
conn, err := db.GetDB(database)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
db.EnsureTable(database)
|
||||
|
||||
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')
|
||||
)
|
||||
`
|
||||
args := []interface{}{from, to}
|
||||
if madv != "" && madv != "ALL" {
|
||||
args = append(args, madv)
|
||||
sqlWhere += fmt.Sprintf(" AND nt.madv = $%d ", len(args))
|
||||
}
|
||||
|
||||
sqlStr := fmt.Sprintf(`
|
||||
SELECT
|
||||
nt.mabn,
|
||||
nt.maba,
|
||||
bn.holot || ' ' || bn.ten AS hoten,
|
||||
COALESCE(bn.diachi, '') AS 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,
|
||||
COALESCE(nt.kqcdoan, '') AS chandoan_chinh,
|
||||
COALESCE(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
|
||||
%s
|
||||
ORDER BY nt.ngayvv DESC
|
||||
LIMIT 100;
|
||||
`, sqlWhere)
|
||||
|
||||
rows, err := conn.Query(sqlStr, args...)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
list := make([]map[string]interface{}, 0)
|
||||
for rows.Next() {
|
||||
var mabn, maba, hoten, diachi, ngayvv, ngayrv, tendv, cdChinh, cdPhu, chamsoc, cls, ghichu string
|
||||
_ = rows.Scan(&mabn, &maba, &hoten, &diachi, &ngayvv, &ngayrv, &tendv, &cdChinh, &cdPhu, &chamsoc, &cls, &ghichu)
|
||||
list = append(list, map[string]interface{}{
|
||||
"mabn": mabn, "maba": maba, "hoten": hoten, "diachi": diachi,
|
||||
"ngayvv": ngayvv, "ngayrv": ngayrv, "tendv": tendv,
|
||||
"chandoan_chinh": cdChinh, "chandoan_phu": cdPhu,
|
||||
"chamsoc": chamsoc, "ketqua_cls": db.CleanRTF(cls), "ghichu": ghichu,
|
||||
})
|
||||
}
|
||||
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": true,
|
||||
"data": list,
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/cap-nhat-ghi-chu
|
||||
func HandleCapNhatGhiChu(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
var body struct {
|
||||
Maba string `json:"maba"`
|
||||
Mabn string `json:"mabn"`
|
||||
Loai string `json:"loai"`
|
||||
Ghichu string `json:"ghichu"`
|
||||
Database string `json:"database"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.Maba == "" || body.Loai == "" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{"success": false, "message": "Thiếu thông tin maba hoặc loai danh sách"})
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := db.GetDB(body.Database)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
db.EnsureTable(body.Database)
|
||||
|
||||
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();
|
||||
`
|
||||
_, err = conn.Exec(sqlUpsert, body.Maba, body.Mabn, body.Loai, body.Ghichu)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Try sync to bnnoitru.ghichu
|
||||
_, _ = conn.Exec("UPDATE current.bnnoitru SET ghichu = $1 WHERE maba = $2;", body.Ghichu, body.Maba)
|
||||
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "Đã lưu ghi chú thành công!",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"web-giao-ban-go/logger"
|
||||
)
|
||||
|
||||
type AttachmentInfo struct {
|
||||
Filename string `json:"filename"`
|
||||
OriginalName string `json:"originalname"`
|
||||
Size int64 `json:"size"`
|
||||
MimeType string `json:"mimetype"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
type Issue struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Category string `json:"category"`
|
||||
Module string `json:"module"`
|
||||
Priority string `json:"priority"`
|
||||
Database string `json:"database"`
|
||||
DatabaseName string `json:"databaseName"`
|
||||
Period string `json:"period"`
|
||||
Reporter string `json:"reporter"`
|
||||
Department string `json:"department"`
|
||||
Description string `json:"description"`
|
||||
Status string `json:"status"` // CHO_XU_LY, DANG_XU_LY, DA_XU_LY, DONG
|
||||
HandlerNotes string `json:"handlerNotes"`
|
||||
ResolvedBy string `json:"resolvedBy"`
|
||||
ResolvedAt *string `json:"resolvedAt"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Attachment *AttachmentInfo `json:"attachment,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
issuesMu sync.RWMutex
|
||||
dataFile = "data/issues.json"
|
||||
uploadsDir = "uploads/issues"
|
||||
)
|
||||
|
||||
func EnsureDirs() {
|
||||
_ = os.MkdirAll("data", 0755)
|
||||
_ = os.MkdirAll(uploadsDir, 0755)
|
||||
if _, err := os.Stat(dataFile); os.IsNotExist(err) {
|
||||
_ = os.WriteFile(dataFile, []byte("[]"), 0644)
|
||||
}
|
||||
}
|
||||
|
||||
func getAllIssues() []Issue {
|
||||
EnsureDirs()
|
||||
issuesMu.RLock()
|
||||
defer issuesMu.RUnlock()
|
||||
|
||||
raw, err := os.ReadFile(dataFile)
|
||||
if err != nil {
|
||||
return []Issue{}
|
||||
}
|
||||
var list []Issue
|
||||
if err := json.Unmarshal(raw, &list); err != nil {
|
||||
return []Issue{}
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
func saveIssues(list []Issue) error {
|
||||
EnsureDirs()
|
||||
issuesMu.Lock()
|
||||
defer issuesMu.Unlock()
|
||||
|
||||
data, err := json.MarshalIndent(list, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmp := dataFile + ".tmp"
|
||||
if err := os.WriteFile(tmp, data, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmp, dataFile)
|
||||
}
|
||||
|
||||
func generateIssueID(all []Issue) string {
|
||||
now := time.Now()
|
||||
prefix := fmt.Sprintf("ISS-%s", now.Format("20060102"))
|
||||
count := 1
|
||||
for _, item := range all {
|
||||
if strings.HasPrefix(item.ID, prefix) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("%s-%03d", prefix, count)
|
||||
}
|
||||
|
||||
// GET /api/issues
|
||||
func HandleGetIssues(w http.ResponseWriter, r *http.Request) {
|
||||
list := getAllIssues()
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": true,
|
||||
"data": list,
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/issues
|
||||
func HandleCreateIssue(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
// Limit 25MB
|
||||
if err := r.ParseMultipartForm(25 << 20); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{"success": false, "message": "File quá lớn (tối đa 25MB)"})
|
||||
return
|
||||
}
|
||||
|
||||
title := strings.TrimSpace(r.FormValue("title"))
|
||||
if title == "" {
|
||||
title = "Sự cố không có tiêu đề"
|
||||
}
|
||||
|
||||
all := getAllIssues()
|
||||
id := generateIssueID(all)
|
||||
nowStr := time.Now().Format(time.RFC3339)
|
||||
|
||||
var attach *AttachmentInfo
|
||||
file, header, err := r.FormFile("attachment")
|
||||
if err == nil && file != nil {
|
||||
defer file.Close()
|
||||
ext := filepath.Ext(header.Filename)
|
||||
cleanBase := strings.Map(func(r rune) rune {
|
||||
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' || r == '-' {
|
||||
return r
|
||||
}
|
||||
return '_'
|
||||
}, strings.TrimSuffix(header.Filename, ext))
|
||||
|
||||
safeFilename := fmt.Sprintf("%s_%d%s", cleanBase, time.Now().UnixNano()/1e6, ext)
|
||||
destPath := filepath.Join(uploadsDir, safeFilename)
|
||||
|
||||
dst, err := os.Create(destPath)
|
||||
if err == nil {
|
||||
defer dst.Close()
|
||||
_, _ = io.Copy(dst, file)
|
||||
attach = &AttachmentInfo{
|
||||
Filename: safeFilename,
|
||||
OriginalName: header.Filename,
|
||||
Size: header.Size,
|
||||
MimeType: header.Header.Get("Content-Type"),
|
||||
URL: "/uploads/issues/" + safeFilename,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newIssue := Issue{
|
||||
ID: id,
|
||||
Title: title,
|
||||
Category: r.FormValue("category"),
|
||||
Module: r.FormValue("module"),
|
||||
Priority: r.FormValue("priority"),
|
||||
Database: r.FormValue("database"),
|
||||
DatabaseName: r.FormValue("databaseName"),
|
||||
Period: r.FormValue("period"),
|
||||
Reporter: strings.TrimSpace(r.FormValue("reporter")),
|
||||
Department: strings.TrimSpace(r.FormValue("department")),
|
||||
Description: strings.TrimSpace(r.FormValue("description")),
|
||||
Status: "CHO_XU_LY",
|
||||
CreatedAt: nowStr,
|
||||
UpdatedAt: nowStr,
|
||||
Attachment: attach,
|
||||
}
|
||||
|
||||
all = append([]Issue{newIssue}, all...)
|
||||
_ = saveIssues(all)
|
||||
|
||||
logger.Info("Đã ghi nhận Issue mới: [%s] %s từ %s", id, title, newIssue.Reporter)
|
||||
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": true,
|
||||
"data": newIssue,
|
||||
"message": "Đã ghi nhận báo lỗi thành công!",
|
||||
})
|
||||
}
|
||||
|
||||
// PUT /api/issues/{id}
|
||||
func HandleUpdateIssue(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
pathParts := strings.Split(r.URL.Path, "/")
|
||||
id := pathParts[len(pathParts)-1]
|
||||
|
||||
var body struct {
|
||||
Status string `json:"status"`
|
||||
ResolvedBy string `json:"resolvedBy"`
|
||||
HandlerNotes string `json:"handlerNotes"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{"success": false, "message": "Dữ liệu không hợp lệ"})
|
||||
return
|
||||
}
|
||||
|
||||
all := getAllIssues()
|
||||
found := false
|
||||
var updated Issue
|
||||
nowStr := time.Now().Format(time.RFC3339)
|
||||
|
||||
for i := range all {
|
||||
if all[i].ID == id {
|
||||
if body.Status != "" {
|
||||
all[i].Status = body.Status
|
||||
}
|
||||
all[i].ResolvedBy = body.ResolvedBy
|
||||
all[i].HandlerNotes = body.HandlerNotes
|
||||
all[i].UpdatedAt = nowStr
|
||||
|
||||
if (body.Status == "DA_XU_LY" || body.Status == "DONG") && all[i].ResolvedAt == nil {
|
||||
all[i].ResolvedAt = &nowStr
|
||||
}
|
||||
updated = all[i]
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{"success": false, "message": "Không tìm thấy issue"})
|
||||
return
|
||||
}
|
||||
|
||||
_ = saveIssues(all)
|
||||
logger.Info("Cập nhật Issue [%s]: Trạng thái=%s, Kỹ thuật=%s", id, updated.Status, updated.ResolvedBy)
|
||||
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": true,
|
||||
"data": updated,
|
||||
"message": "Cập nhật issue thành công!",
|
||||
})
|
||||
}
|
||||
|
||||
// DELETE /api/issues/{id}
|
||||
func HandleDeleteIssue(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
pathParts := strings.Split(r.URL.Path, "/")
|
||||
id := pathParts[len(pathParts)-1]
|
||||
|
||||
all := getAllIssues()
|
||||
filtered := make([]Issue, 0, len(all))
|
||||
var removed *Issue
|
||||
|
||||
for _, item := range all {
|
||||
if item.ID == id {
|
||||
cpy := item
|
||||
removed = &cpy
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, item)
|
||||
}
|
||||
|
||||
if removed == nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{"success": false, "message": "Không tìm thấy issue"})
|
||||
return
|
||||
}
|
||||
|
||||
if removed.Attachment != nil && removed.Attachment.Filename != "" {
|
||||
_ = os.Remove(filepath.Join(uploadsDir, removed.Attachment.Filename))
|
||||
}
|
||||
|
||||
_ = saveIssues(filtered)
|
||||
logger.Info("Đã xóa Issue [%s]", id)
|
||||
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "Đã xóa issue thành công!",
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user