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!", }) }