Initial commit
This commit is contained in:
125
cpone-dashboard/menu/abnormal/handler.go
Normal file
125
cpone-dashboard/menu/abnormal/handler.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package abnormal
|
||||
|
||||
import (
|
||||
"cpone-dashboard/menu/auth"
|
||||
"cpone-dashboard/menu/projects"
|
||||
"encoding/json"
|
||||
"html/template"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var tmpl *template.Template
|
||||
var basePath string
|
||||
|
||||
func SetTemplates(t *template.Template) { tmpl = t }
|
||||
func SetBasePath(p string) { basePath = p }
|
||||
|
||||
type pageData struct {
|
||||
Username string
|
||||
CurrentProject projects.ProjectItem
|
||||
Group string
|
||||
Groups []string
|
||||
Summary AbnormalSummary
|
||||
StaffJSON template.JS
|
||||
AgeJSON template.JS
|
||||
GenderJSON template.JS
|
||||
DeptJSON template.JS
|
||||
}
|
||||
|
||||
func toJS(v any) template.JS {
|
||||
b, _ := json.Marshal(v)
|
||||
return template.JS(b)
|
||||
}
|
||||
|
||||
func Index(w http.ResponseWriter, r *http.Request) {
|
||||
username := auth.Username(r)
|
||||
mcuID := auth.SelectedProjectID(r)
|
||||
if mcuID == 0 {
|
||||
http.Redirect(w, r, basePath+"/projects", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
project, ok, err := projects.GetUserProject(username, mcuID)
|
||||
if err != nil || !ok {
|
||||
http.Redirect(w, r, basePath+"/projects", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
group := r.URL.Query().Get("group")
|
||||
|
||||
groups, err := GetAbnormalGroups(mcuID)
|
||||
if err != nil {
|
||||
http.Error(w, "query error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
total, err := GetTotalPatients(mcuID)
|
||||
if err != nil {
|
||||
http.Error(w, "query error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
abnormalCount, err := GetAbnormalCount(mcuID, group)
|
||||
if err != nil {
|
||||
http.Error(w, "query error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
normalCount := total - abnormalCount
|
||||
if normalCount < 0 {
|
||||
normalCount = 0
|
||||
}
|
||||
rate := 0
|
||||
if total > 0 {
|
||||
rate = abnormalCount * 100 / total
|
||||
}
|
||||
|
||||
ageData, err := GetAgeChartData(mcuID, group)
|
||||
if err != nil {
|
||||
http.Error(w, "query error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
genderData, err := GetGenderChartData(mcuID, group)
|
||||
if err != nil {
|
||||
http.Error(w, "query error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
deptData, err := GetDeptChartData(mcuID, group)
|
||||
if err != nil {
|
||||
http.Error(w, "query error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
t := tmpl
|
||||
if t == nil {
|
||||
http.Error(w, "template not ready", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if err := t.ExecuteTemplate(w, "base", pageData{
|
||||
Username: username,
|
||||
CurrentProject: project,
|
||||
Group: group,
|
||||
Groups: groups,
|
||||
Summary: AbnormalSummary{
|
||||
Total: total,
|
||||
Abnormal: abnormalCount,
|
||||
Normal: normalCount,
|
||||
AbnormalRate: rate,
|
||||
},
|
||||
StaffJSON: toJS(map[string]any{
|
||||
"normal": normalCount,
|
||||
"abnormal": abnormalCount,
|
||||
}),
|
||||
AgeJSON: toJS(map[string]any{
|
||||
"labels": ageData.Labels,
|
||||
"abnormal": ageData.Abnormal,
|
||||
}),
|
||||
GenderJSON: toJS(map[string]any{
|
||||
"labels": genderData.Labels,
|
||||
"abnormal": genderData.Abnormal,
|
||||
}),
|
||||
DeptJSON: toJS(map[string]any{
|
||||
"labels": deptData.Labels,
|
||||
"abnormal": deptData.Abnormal,
|
||||
}),
|
||||
}); err != nil {
|
||||
http.Error(w, "template error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
257
cpone-dashboard/menu/abnormal/query.go
Normal file
257
cpone-dashboard/menu/abnormal/query.go
Normal file
@@ -0,0 +1,257 @@
|
||||
package abnormal
|
||||
|
||||
import (
|
||||
"cpone-dashboard/db"
|
||||
)
|
||||
|
||||
type AbnormalSummary struct {
|
||||
Total int
|
||||
Abnormal int
|
||||
Normal int
|
||||
AbnormalRate int // percentage, 0-100
|
||||
}
|
||||
|
||||
type AgeChartData struct {
|
||||
Labels []string
|
||||
Abnormal []int
|
||||
}
|
||||
|
||||
type GenderChartData struct {
|
||||
Labels []string
|
||||
Abnormal []int
|
||||
}
|
||||
|
||||
type DeptChartData struct {
|
||||
Labels []string
|
||||
Abnormal []int
|
||||
}
|
||||
|
||||
// GetTotalPatients returns total active patients for the project.
|
||||
func GetTotalPatients(mcuID int) (int, error) {
|
||||
var total int
|
||||
err := db.DB.QueryRow(`
|
||||
SELECT COUNT(*)
|
||||
FROM mcu_patient
|
||||
WHERE Mcu_PatientMcuID = ?
|
||||
AND Mcu_PatientIsActive = 'Y'
|
||||
`, mcuID).Scan(&total)
|
||||
return total, err
|
||||
}
|
||||
|
||||
// GetAbnormalCount returns distinct patients with at least one kelainan.
|
||||
// If group is non-empty, counts only patients with kelainan in that group.
|
||||
func GetAbnormalCount(mcuID int, group string) (int, error) {
|
||||
var count int
|
||||
var err error
|
||||
if group == "" {
|
||||
err = db.DB.QueryRow(`
|
||||
SELECT COUNT(DISTINCT M_PatientID)
|
||||
FROM kelainan_details
|
||||
WHERE Mgm_McuID = ?
|
||||
`, mcuID).Scan(&count)
|
||||
} else {
|
||||
err = db.DB.QueryRow(`
|
||||
SELECT COUNT(DISTINCT M_PatientID)
|
||||
FROM kelainan_details
|
||||
WHERE Mgm_McuID = ?
|
||||
AND Mcu_KelainanGroupSummaryName = ?
|
||||
`, mcuID, group).Scan(&count)
|
||||
}
|
||||
return count, err
|
||||
}
|
||||
|
||||
// GetAbnormalGroups returns distinct kelainan group names for a project.
|
||||
func GetAbnormalGroups(mcuID int) ([]string, error) {
|
||||
rows, err := db.DB.Query(`
|
||||
SELECT DISTINCT Mcu_KelainanGroupSummaryName
|
||||
FROM kelainan_details
|
||||
WHERE Mgm_McuID = ?
|
||||
AND Mcu_KelainanGroupSummaryName IS NOT NULL
|
||||
AND Mcu_KelainanGroupSummaryName != ''
|
||||
ORDER BY Mcu_KelainanGroupSummaryName
|
||||
`, mcuID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var groups []string
|
||||
for rows.Next() {
|
||||
var g string
|
||||
if err := rows.Scan(&g); err != nil {
|
||||
continue
|
||||
}
|
||||
groups = append(groups, g)
|
||||
}
|
||||
return groups, rows.Err()
|
||||
}
|
||||
|
||||
// GetAgeChartData returns abnormal patient counts by age bucket.
|
||||
func GetAgeChartData(mcuID int, group string) (AgeChartData, error) {
|
||||
buckets := []string{"<30", "30-39", "40-49", "50+"}
|
||||
counts := make(map[string]int)
|
||||
|
||||
var query string
|
||||
var args []any
|
||||
if group == "" {
|
||||
query = `
|
||||
SELECT
|
||||
CASE
|
||||
WHEN AgePatient < 30 THEN '<30'
|
||||
WHEN AgePatient < 40 THEN '30-39'
|
||||
WHEN AgePatient < 50 THEN '40-49'
|
||||
ELSE '50+'
|
||||
END AS age_group,
|
||||
COUNT(DISTINCT M_PatientID) AS cnt
|
||||
FROM kelainan_details
|
||||
WHERE Mgm_McuID = ?
|
||||
AND AgePatient IS NOT NULL
|
||||
GROUP BY age_group`
|
||||
args = []any{mcuID}
|
||||
} else {
|
||||
query = `
|
||||
SELECT
|
||||
CASE
|
||||
WHEN AgePatient < 30 THEN '<30'
|
||||
WHEN AgePatient < 40 THEN '30-39'
|
||||
WHEN AgePatient < 50 THEN '40-49'
|
||||
ELSE '50+'
|
||||
END AS age_group,
|
||||
COUNT(DISTINCT M_PatientID) AS cnt
|
||||
FROM kelainan_details
|
||||
WHERE Mgm_McuID = ?
|
||||
AND Mcu_KelainanGroupSummaryName = ?
|
||||
AND AgePatient IS NOT NULL
|
||||
GROUP BY age_group`
|
||||
args = []any{mcuID, group}
|
||||
}
|
||||
|
||||
rows, err := db.DB.Query(query, args...)
|
||||
if err != nil {
|
||||
return AgeChartData{}, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var label string
|
||||
var cnt int
|
||||
if err := rows.Scan(&label, &cnt); err != nil {
|
||||
continue
|
||||
}
|
||||
counts[label] = cnt
|
||||
}
|
||||
|
||||
data := AgeChartData{Labels: buckets, Abnormal: make([]int, len(buckets))}
|
||||
for i, b := range buckets {
|
||||
data.Abnormal[i] = counts[b]
|
||||
}
|
||||
return data, rows.Err()
|
||||
}
|
||||
|
||||
// GetGenderChartData returns abnormal patient counts by gender.
|
||||
func GetGenderChartData(mcuID int, group string) (GenderChartData, error) {
|
||||
labels := []string{"Male", "Female"}
|
||||
counts := make(map[string]int)
|
||||
|
||||
var query string
|
||||
var args []any
|
||||
if group == "" {
|
||||
query = `
|
||||
SELECT
|
||||
CASE WHEN LOWER(M_PatientGender) = 'male' THEN 'Male' ELSE 'Female' END AS gender,
|
||||
COUNT(DISTINCT M_PatientID) AS cnt
|
||||
FROM kelainan_details
|
||||
WHERE Mgm_McuID = ?
|
||||
AND M_PatientGender IS NOT NULL
|
||||
GROUP BY gender`
|
||||
args = []any{mcuID}
|
||||
} else {
|
||||
query = `
|
||||
SELECT
|
||||
CASE WHEN LOWER(M_PatientGender) = 'male' THEN 'Male' ELSE 'Female' END AS gender,
|
||||
COUNT(DISTINCT M_PatientID) AS cnt
|
||||
FROM kelainan_details
|
||||
WHERE Mgm_McuID = ?
|
||||
AND Mcu_KelainanGroupSummaryName = ?
|
||||
AND M_PatientGender IS NOT NULL
|
||||
GROUP BY gender`
|
||||
args = []any{mcuID, group}
|
||||
}
|
||||
|
||||
rows, err := db.DB.Query(query, args...)
|
||||
if err != nil {
|
||||
return GenderChartData{}, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var label string
|
||||
var cnt int
|
||||
if err := rows.Scan(&label, &cnt); err != nil {
|
||||
continue
|
||||
}
|
||||
counts[label] = cnt
|
||||
}
|
||||
|
||||
data := GenderChartData{Labels: labels, Abnormal: make([]int, len(labels))}
|
||||
for i, l := range labels {
|
||||
data.Abnormal[i] = counts[l]
|
||||
}
|
||||
return data, rows.Err()
|
||||
}
|
||||
|
||||
// GetDeptChartData returns top-10 departments by abnormal patient count.
|
||||
func GetDeptChartData(mcuID int, group string) (DeptChartData, error) {
|
||||
var query string
|
||||
var args []any
|
||||
if group == "" {
|
||||
query = `
|
||||
SELECT
|
||||
COALESCE(
|
||||
NULLIF(TRIM(M_PatientDepartement), ''),
|
||||
NULLIF(TRIM(M_PatientDivisi), ''),
|
||||
NULLIF(TRIM(M_PatientPosisi), ''),
|
||||
'-'
|
||||
) AS dept,
|
||||
COUNT(DISTINCT M_PatientID) AS cnt
|
||||
FROM kelainan_details
|
||||
WHERE Mgm_McuID = ?
|
||||
GROUP BY dept
|
||||
ORDER BY cnt DESC
|
||||
LIMIT 10`
|
||||
args = []any{mcuID}
|
||||
} else {
|
||||
query = `
|
||||
SELECT
|
||||
COALESCE(
|
||||
NULLIF(TRIM(M_PatientDepartement), ''),
|
||||
NULLIF(TRIM(M_PatientDivisi), ''),
|
||||
NULLIF(TRIM(M_PatientPosisi), ''),
|
||||
'-'
|
||||
) AS dept,
|
||||
COUNT(DISTINCT M_PatientID) AS cnt
|
||||
FROM kelainan_details
|
||||
WHERE Mgm_McuID = ?
|
||||
AND Mcu_KelainanGroupSummaryName = ?
|
||||
GROUP BY dept
|
||||
ORDER BY cnt DESC
|
||||
LIMIT 10`
|
||||
args = []any{mcuID, group}
|
||||
}
|
||||
|
||||
rows, err := db.DB.Query(query, args...)
|
||||
if err != nil {
|
||||
return DeptChartData{}, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var data DeptChartData
|
||||
for rows.Next() {
|
||||
var label string
|
||||
var cnt int
|
||||
if err := rows.Scan(&label, &cnt); err != nil {
|
||||
continue
|
||||
}
|
||||
data.Labels = append(data.Labels, label)
|
||||
data.Abnormal = append(data.Abnormal, cnt)
|
||||
}
|
||||
return data, rows.Err()
|
||||
}
|
||||
7
cpone-dashboard/menu/abnormal/route.go
Normal file
7
cpone-dashboard/menu/abnormal/route.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package abnormal
|
||||
|
||||
import "github.com/go-chi/chi/v5"
|
||||
|
||||
func Routes(r chi.Router) {
|
||||
r.Get("/", Index)
|
||||
}
|
||||
Reference in New Issue
Block a user