From 11f0d0436ea2cac728063b845793b3be0187b649 Mon Sep 17 00:00:00 2001
From: "sas.fajri"
Date: Thu, 7 May 2026 12:04:09 +0700
Subject: [PATCH] Update dashboard filters and packet views
---
cpone-dashboard/menu/arrival/handler.go | 42 +-
cpone-dashboard/menu/arrival/query.go | 192 +++++++--
cpone-dashboard/menu/dashboard/handler.go | 90 ++++-
cpone-dashboard/menu/dashboard/query.go | 107 ++++--
cpone-dashboard/menu/progress/handler.go | 8 +-
cpone-dashboard/menu/progress/query.go | 60 ++-
cpone-dashboard/menu/result/handler.go | 8 +-
cpone-dashboard/menu/result/query.go | 59 ++-
cpone-dashboard/templates/abnormal/index.html | 2 +-
cpone-dashboard/templates/arrival/index.html | 363 +++++++++++-------
.../templates/dashboard/index.html | 42 +-
.../dashboard/partials/arrivals.html | 4 +-
.../templates/dashboard/partials/kpi.html | 8 +-
.../dashboard/partials/patients.html | 260 ++++++++++---
.../dashboard/partials/stations.html | 6 +-
cpone-dashboard/templates/progress/index.html | 62 ++-
cpone-dashboard/templates/result/index.html | 37 +-
17 files changed, 1008 insertions(+), 342 deletions(-)
diff --git a/cpone-dashboard/menu/arrival/handler.go b/cpone-dashboard/menu/arrival/handler.go
index 2e76b99..59f13bc 100644
--- a/cpone-dashboard/menu/arrival/handler.go
+++ b/cpone-dashboard/menu/arrival/handler.go
@@ -24,13 +24,16 @@ type pageData struct {
AvailableDates []string
Search string
Department string
+ Paket string
Rows []ArrivalRow
FilteredRows []ArrivalRow
Summary ArrivalSummary
Departments []DepartmentStat
DepartmentOptions []string
+ PaketOptions []PaketOption
OverviewJSON template.JS
DepartmentJSON template.JS
+ PaketChartJSON template.JS
Page int
PageSize int
HasMore bool
@@ -76,15 +79,27 @@ func buildArrivalData(r *http.Request) (pageData, int, error) {
return pageData{}, http.StatusInternalServerError, err
}
summary, deptStats := BuildArrivalStats(rows)
- filteredRows := FilterArrivalRows(rows, r.URL.Query().Get("search"), r.URL.Query().Get("dept"))
+ filteredRows := FilterArrivalRows(rows, r.URL.Query().Get("search"), r.URL.Query().Get("dept"), r.URL.Query().Get("paket"))
deptOptions := UniqueDepartments(rows)
+ paketOptions := UniquePakets(rows)
stationMap, err := GetStationProgress(mcuID, selectedDate)
if err != nil {
return pageData{}, http.StatusInternalServerError, err
}
for i := range filteredRows {
- filteredRows[i].Stations = stationMap[filteredRows[i].PreregisterID]
+ stations := stationMap[filteredRows[i].PreregisterID]
+ filteredRows[i].Stations = stations
+ for _, s := range stations {
+ switch s.Tone {
+ case "success":
+ filteredRows[i].DoneCount++
+ case "warning":
+ filteredRows[i].ProgressCount++
+ default:
+ filteredRows[i].PendingCount++
+ }
+ }
}
outerDepts := []map[string]any{}
@@ -111,6 +126,26 @@ func buildArrivalData(r *http.Request) (pageData, int, error) {
"counts": stationCounts,
}
+ paketStats, err := GetPaketStats(mcuID, selectedDate)
+ if err != nil {
+ return pageData{}, http.StatusInternalServerError, err
+ }
+ paketLabels := make([]string, len(paketStats))
+ paketTotal := make([]int, len(paketStats))
+ paketDone := make([]int, len(paketStats))
+ paketRemaining := make([]int, len(paketStats))
+ for i, p := range paketStats {
+ paketLabels[i] = p.Name
+ paketTotal[i] = p.Total
+ paketDone[i] = p.Completed
+ paketRemaining[i] = p.Total - p.Completed
+ }
+ paketChart := map[string]any{
+ "labels": paketLabels,
+ "done": paketDone,
+ "remaining": paketRemaining,
+ }
+
page := parsePage(r.URL.Query().Get("page"))
pagedRows, hasMore := PaginateArrivalRows(filteredRows, page, defaultPageSize)
visibleCount := page * defaultPageSize
@@ -125,13 +160,16 @@ func buildArrivalData(r *http.Request) (pageData, int, error) {
AvailableDates: dates,
Search: r.URL.Query().Get("search"),
Department: r.URL.Query().Get("dept"),
+ Paket: r.URL.Query().Get("paket"),
Rows: rows,
FilteredRows: pagedRows,
Summary: summary,
Departments: deptStats,
DepartmentOptions: deptOptions,
+ PaketOptions: paketOptions,
OverviewJSON: toJS(overview),
DepartmentJSON: toJS(stationChart),
+ PaketChartJSON: toJS(paketChart),
Page: page,
PageSize: defaultPageSize,
HasMore: hasMore,
diff --git a/cpone-dashboard/menu/arrival/query.go b/cpone-dashboard/menu/arrival/query.go
index 7520238..476a6a9 100644
--- a/cpone-dashboard/menu/arrival/query.go
+++ b/cpone-dashboard/menu/arrival/query.go
@@ -13,6 +13,12 @@ type StationBadge struct {
Tone string // "success" | "warning" | "danger" | "neutral"
}
+type PaketStat struct {
+ Name string
+ Total int
+ Completed int
+}
+
type StationStat struct {
Name string
Count int
@@ -23,10 +29,16 @@ type ArrivalRow struct {
NIP string
Name string
Department string
+ Pemeriksaan string
+ PaketCode string
+ PaketName string
InTime string
Status string
StatusTone string
Stations []StationBadge
+ DoneCount int
+ ProgressCount int
+ PendingCount int
}
type DepartmentStat struct {
@@ -83,6 +95,17 @@ func GetArrivalRows(mcuID int, date string) ([]ArrivalRow, error) {
NULLIF(TRIM(mp.Mcu_PatientPosisi), ''),
'-'
) AS department_name,
+ COALESCE(NULLIF(TRIM(mp.Mcu_PatientPemeriksaan), ''), '') AS pemeriksaan,
+ COALESCE((
+ SELECT GROUP_CONCAT(DISTINCT COALESCE(NULLIF(TRIM(pp.Mcu_PatientPacketCode), ''), pp.Mcu_PatientPacketName) ORDER BY COALESCE(NULLIF(TRIM(pp.Mcu_PatientPacketCode), ''), pp.Mcu_PatientPacketName) SEPARATOR ',')
+ FROM mcu_patient_packet pp
+ WHERE pp.Mcu_PatientPacketPreregisterID = mp.Mcu_PatientPreregisterID
+ ), '') AS paket_code,
+ COALESCE((
+ SELECT GROUP_CONCAT(DISTINCT pp.Mcu_PatientPacketName ORDER BY pp.Mcu_PatientPacketName SEPARATOR ', ')
+ FROM mcu_patient_packet pp
+ WHERE pp.Mcu_PatientPacketPreregisterID = mp.Mcu_PatientPreregisterID
+ ), '') AS paket_name,
COALESCE(DATE_FORMAT(mc.Mcu_CheckinoutInTime, '%H:%i'), '') AS in_time,
CASE
WHEN mc.Mcu_CheckinoutOutTime IS NOT NULL THEN 'Performed'
@@ -119,7 +142,7 @@ func GetArrivalRows(mcuID int, date string) ([]ArrivalRow, error) {
var result []ArrivalRow
for rows.Next() {
var r ArrivalRow
- if err := rows.Scan(&r.PreregisterID, &r.NIP, &r.Name, &r.Department, &r.InTime, &r.Status, &r.StatusTone); err != nil {
+ if err := rows.Scan(&r.PreregisterID, &r.NIP, &r.Name, &r.Department, &r.Pemeriksaan, &r.PaketCode, &r.PaketName, &r.InTime, &r.Status, &r.StatusTone); err != nil {
continue
}
if strings.TrimSpace(r.NIP) == "" {
@@ -139,31 +162,59 @@ func GetArrivalRows(mcuID int, date string) ([]ArrivalRow, error) {
func GetStationProgress(mcuID int, date string) (map[int][]StationBadge, error) {
rows, err := db.DB.Query(`
SELECT
- sp.Mcu_StationProgressPreregisterID,
- sp.Mcu_StationProgressStationName,
+ rs.preregister_id,
+ rs.station_name,
CASE
- WHEN sp.Mcu_StationProgressSource = 'lab'
- AND sp.Mcu_StationProgressReceiveAt IS NOT NULL THEN 'success'
- WHEN sp.Mcu_StationProgressSource = 'nonlab'
- AND sp.Mcu_StationProgressDoneAt IS NOT NULL THEN 'success'
- WHEN sp.Mcu_StationProgressProcessAt IS NOT NULL
- OR sp.Mcu_StationProgressReceiveAt IS NOT NULL
- OR sp.Mcu_StationProgressSamplingAt IS NOT NULL THEN 'warning'
+ WHEN MAX(CASE
+ WHEN sp.Mcu_StationProgressSource = 'lab'
+ AND sp.Mcu_StationProgressReceiveAt IS NOT NULL
+ AND DATE(sp.Mcu_StationProgressReceiveAt) <= mc.Mcu_CheckinoutDate THEN 1
+ WHEN sp.Mcu_StationProgressSource = 'nonlab'
+ AND sp.Mcu_StationProgressProcessAt IS NOT NULL
+ AND DATE(sp.Mcu_StationProgressProcessAt) <= mc.Mcu_CheckinoutDate THEN 1
+ WHEN sp.Mcu_StationProgressSource = 'nonlab'
+ AND sp.Mcu_StationProgressDoneAt IS NOT NULL
+ AND DATE(sp.Mcu_StationProgressDoneAt) <= mc.Mcu_CheckinoutDate THEN 1
+ ELSE 0
+ END) = 1 THEN 'success'
+ WHEN MAX(CASE
+ WHEN sp.Mcu_StationProgressProcessAt IS NOT NULL
+ AND DATE(sp.Mcu_StationProgressProcessAt) <= mc.Mcu_CheckinoutDate THEN 1
+ WHEN sp.Mcu_StationProgressReceiveAt IS NOT NULL
+ AND DATE(sp.Mcu_StationProgressReceiveAt) <= mc.Mcu_CheckinoutDate THEN 1
+ WHEN sp.Mcu_StationProgressSamplingAt IS NOT NULL
+ AND DATE(sp.Mcu_StationProgressSamplingAt) <= mc.Mcu_CheckinoutDate THEN 1
+ ELSE 0
+ END) = 1 THEN 'warning'
ELSE 'neutral'
END AS tone
- FROM mcu_station_progress sp
- WHERE sp.Mcu_StationProgressMcuID = ?
- AND sp.Mcu_StationProgressPreregisterID IN (
- SELECT mp.Mcu_PatientPreregisterID
- FROM mcu_patient_schedule s
- JOIN mcu_patient mp ON mp.Mcu_PatientPreregisterID = s.Mcu_PatientSchedulePreregisterID
- WHERE mp.Mcu_PatientMcuID = ?
- AND mp.Mcu_PatientIsActive = 'Y'
- AND s.Mcu_PatientScheduleDate = ?
- AND s.Mcu_PatientScheduleIsActive = 'Y'
- )
- ORDER BY sp.Mcu_StationProgressPreregisterID, sp.Mcu_StationProgressStationName
- `, mcuID, mcuID, date)
+ FROM mcu_patient_required_station rs
+ JOIN mcu_checkinout mc
+ ON mc.Mcu_CheckinoutPreregisterID = rs.preregister_id
+ AND mc.Mcu_CheckinoutMcuID = rs.mcu_id
+ AND mc.Mcu_CheckinoutDate = ?
+ AND mc.Mcu_CheckinoutIsActive = 'Y'
+ LEFT JOIN mcu_station_progress sp
+ ON sp.Mcu_StationProgressPreregisterID = rs.preregister_id
+ AND sp.Mcu_StationProgressStationID = rs.sample_station_id
+ AND sp.Mcu_StationProgressMcuID = rs.mcu_id
+ AND (
+ (sp.Mcu_StationProgressCheckinDate IS NOT NULL
+ AND sp.Mcu_StationProgressCheckinDate <= mc.Mcu_CheckinoutDate)
+ OR
+ (sp.Mcu_StationProgressCheckinDate IS NULL
+ AND (
+ (sp.Mcu_StationProgressProcessAt IS NOT NULL AND DATE(sp.Mcu_StationProgressProcessAt) <= mc.Mcu_CheckinoutDate)
+ OR (sp.Mcu_StationProgressDoneAt IS NOT NULL AND DATE(sp.Mcu_StationProgressDoneAt) <= mc.Mcu_CheckinoutDate)
+ OR (sp.Mcu_StationProgressReceiveAt IS NOT NULL AND DATE(sp.Mcu_StationProgressReceiveAt) <= mc.Mcu_CheckinoutDate)
+ OR (sp.Mcu_StationProgressSamplingAt IS NOT NULL AND DATE(sp.Mcu_StationProgressSamplingAt) <= mc.Mcu_CheckinoutDate)
+ )
+ )
+ )
+ WHERE rs.mcu_id = ?
+ GROUP BY rs.preregister_id, rs.station_name
+ ORDER BY rs.preregister_id, rs.station_name
+ `, date, mcuID)
if err != nil {
return nil, err
}
@@ -247,16 +298,20 @@ func BuildStationChart(stationMap map[int][]StationBadge) []StationStat {
return stats
}
-func FilterArrivalRows(rows []ArrivalRow, search, dept string) []ArrivalRow {
+func FilterArrivalRows(rows []ArrivalRow, search, dept, paket string) []ArrivalRow {
search = strings.ToLower(strings.TrimSpace(search))
dept = strings.TrimSpace(dept)
- if search == "" && dept == "" {
+ paket = strings.TrimSpace(paket)
+ if search == "" && dept == "" && paket == "" {
return rows
}
out := make([]ArrivalRow, 0, len(rows))
for _, row := range rows {
- if dept != "" && dept != "All Departments" && row.Department != dept {
+ if dept != "" && row.Department != dept {
+ continue
+ }
+ if paket != "" && row.PaketCode != paket {
continue
}
if search != "" {
@@ -270,6 +325,33 @@ func FilterArrivalRows(rows []ArrivalRow, search, dept string) []ArrivalRow {
return out
}
+type PaketOption struct {
+ Code string
+ Name string
+}
+
+func UniquePakets(rows []ArrivalRow) []PaketOption {
+ seen := map[string]struct{}{}
+ var out []PaketOption
+ for _, row := range rows {
+ code := strings.TrimSpace(row.PaketCode)
+ if code == "" {
+ continue
+ }
+ if _, ok := seen[code]; ok {
+ continue
+ }
+ seen[code] = struct{}{}
+ name := strings.TrimSpace(row.PaketName)
+ if name == "" {
+ name = code
+ }
+ out = append(out, PaketOption{Code: code, Name: name})
+ }
+ sort.Slice(out, func(i, j int) bool { return out[i].Code < out[j].Code })
+ return out
+}
+
func PaginateArrivalRows(rows []ArrivalRow, page, pageSize int) ([]ArrivalRow, bool) {
if page < 1 {
page = 1
@@ -306,6 +388,64 @@ func UniqueDepartments(rows []ArrivalRow) []string {
return out
}
+func GetPaketStats(mcuID int, date string) ([]PaketStat, error) {
+ rows, err := db.DB.Query(`
+ SELECT
+ pp.Mcu_PatientPacketName,
+ COUNT(DISTINCT s.Mcu_PatientSchedulePreregisterID) AS total,
+ COUNT(DISTINCT CASE
+ WHEN st.total_required > 0 AND st.total_required = st.total_done
+ THEN s.Mcu_PatientSchedulePreregisterID
+ END) AS completed
+ FROM mcu_patient_packet pp
+ JOIN mcu_patient_schedule s
+ ON s.Mcu_PatientSchedulePreregisterID = pp.Mcu_PatientPacketPreregisterID
+ AND s.Mcu_PatientScheduleDate = ?
+ AND s.Mcu_PatientScheduleIsActive = 'Y'
+ JOIN mcu_patient mp
+ ON mp.Mcu_PatientPreregisterID = s.Mcu_PatientSchedulePreregisterID
+ AND mp.Mcu_PatientMcuID = ?
+ AND mp.Mcu_PatientIsActive = 'Y'
+ LEFT JOIN (
+ SELECT
+ rs.preregister_id,
+ COUNT(rs.sample_station_id) AS total_required,
+ SUM(CASE
+ WHEN sp.Mcu_StationProgressSource = 'lab'
+ AND sp.Mcu_StationProgressReceiveAt IS NOT NULL THEN 1
+ WHEN sp.Mcu_StationProgressSource = 'nonlab'
+ AND sp.Mcu_StationProgressProcessAt IS NOT NULL THEN 1
+ WHEN sp.Mcu_StationProgressSource = 'nonlab'
+ AND sp.Mcu_StationProgressDoneAt IS NOT NULL THEN 1
+ ELSE 0
+ END) AS total_done
+ FROM mcu_patient_required_station rs
+ LEFT JOIN mcu_station_progress sp
+ ON sp.Mcu_StationProgressPreregisterID = rs.preregister_id
+ AND sp.Mcu_StationProgressStationID = rs.sample_station_id
+ AND sp.Mcu_StationProgressMcuID = rs.mcu_id
+ WHERE rs.mcu_id = ?
+ GROUP BY rs.preregister_id
+ ) st ON st.preregister_id = s.Mcu_PatientSchedulePreregisterID
+ GROUP BY pp.Mcu_PatientPacketName
+ ORDER BY total DESC
+ `, date, mcuID, mcuID)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var result []PaketStat
+ for rows.Next() {
+ var p PaketStat
+ if err := rows.Scan(&p.Name, &p.Total, &p.Completed); err != nil {
+ continue
+ }
+ result = append(result, p)
+ }
+ return result, nil
+}
+
func activeDateOrLatest(dates []string, selected string, fallback string) string {
selected = strings.TrimSpace(selected)
if selected != "" {
diff --git a/cpone-dashboard/menu/dashboard/handler.go b/cpone-dashboard/menu/dashboard/handler.go
index c1a3696..09f236f 100644
--- a/cpone-dashboard/menu/dashboard/handler.go
+++ b/cpone-dashboard/menu/dashboard/handler.go
@@ -9,6 +9,7 @@ import (
"log"
"net/http"
"strconv"
+ "strings"
"time"
)
@@ -26,8 +27,15 @@ type ArrivalsPartial struct {
}
type PatientsPartial struct {
- Patients []PatientDetail
- IsRange bool
+ Patients []PatientDetail
+ IsRange bool
+ Search string
+ ProgressStatus string
+ KpiFilter string
+ Page int
+ PageSize int
+ HasMore bool
+ NextPage int
}
type PageData struct {
@@ -237,12 +245,84 @@ func Patients(w http.ResponseWriter, r *http.Request) {
http.Error(w, "query error", http.StatusInternalServerError)
return
}
+
+ search := strings.TrimSpace(r.URL.Query().Get("search"))
+ progressStatus := strings.TrimSpace(r.URL.Query().Get("progress_status"))
+ kpiFilter := strings.TrimSpace(r.URL.Query().Get("kpi_filter"))
+
+ if kpiFilter != "" {
+ filteredByKPI := make([]PatientDetail, 0, len(patients))
+ for _, p := range patients {
+ switch kpiFilter {
+ case "invited":
+ filteredByKPI = append(filteredByKPI, p)
+ case "total_staff":
+ if p.InTime != "" {
+ filteredByKPI = append(filteredByKPI, p)
+ }
+ case "inprogress":
+ if p.InTime != "" && !p.HasOut {
+ filteredByKPI = append(filteredByKPI, p)
+ }
+ case "checkedout":
+ if p.HasOut {
+ filteredByKPI = append(filteredByKPI, p)
+ }
+ default:
+ filteredByKPI = append(filteredByKPI, p)
+ }
+ }
+ patients = filteredByKPI
+ }
+
+ if search != "" || progressStatus != "" {
+ lowerSearch := strings.ToLower(search)
+ filtered := make([]PatientDetail, 0, len(patients))
+ for _, p := range patients {
+ if progressStatus != "" && p.ProgressStatus != progressStatus {
+ continue
+ }
+ if lowerSearch != "" && !strings.Contains(strings.ToLower(p.Name), lowerSearch) && !strings.Contains(strconv.Itoa(p.ID), lowerSearch) {
+ continue
+ }
+ filtered = append(filtered, p)
+ }
+ patients = filtered
+ }
+
+ page := 1
+ if p, err := strconv.Atoi(strings.TrimSpace(r.URL.Query().Get("page"))); err == nil && p > 0 {
+ page = p
+ }
+ pageSize := 30
+ start := (page - 1) * pageSize
+ if start > len(patients) {
+ start = len(patients)
+ }
+ end := start + pageSize
+ if end > len(patients) {
+ end = len(patients)
+ }
+ pagedPatients := patients[start:end]
+ hasMore := end < len(patients)
+
data := PatientsPartial{
- Patients: patients,
- IsRange: mode != "daily" || dateFrom != dateTo,
+ Patients: pagedPatients,
+ IsRange: mode != "daily" || dateFrom != dateTo,
+ Search: search,
+ ProgressStatus: progressStatus,
+ KpiFilter: kpiFilter,
+ Page: page,
+ PageSize: pageSize,
+ HasMore: hasMore,
+ NextPage: page + 1,
}
t := parse("templates/dashboard/partials/patients.html")
- if err := t.ExecuteTemplate(w, "patients", data); err != nil {
+ templateName := "patients"
+ if r.URL.Query().Get("chunk") == "1" {
+ templateName = "patients-rows"
+ }
+ if err := t.ExecuteTemplate(w, templateName, data); err != nil {
log.Printf("[dashboard] patients template error: %v", err)
}
}
diff --git a/cpone-dashboard/menu/dashboard/query.go b/cpone-dashboard/menu/dashboard/query.go
index 053a8c2..c4636b2 100644
--- a/cpone-dashboard/menu/dashboard/query.go
+++ b/cpone-dashboard/menu/dashboard/query.go
@@ -391,6 +391,7 @@ func GetPeriodTrend(mcuID int, dateFrom, dateTo string, isRange bool) ([]TrendPo
type PatientStationStatus struct {
Station string
Done bool
+ Progress bool
ProcessAt string
DoneAt string
}
@@ -404,6 +405,8 @@ type PatientDetail struct {
HasOut bool
Stations []PatientStationStatus
DoneCount int
+ ProgressCount int
+ ProgressStatus string
}
type patientKey struct {
@@ -414,16 +417,22 @@ type patientKey struct {
func GetAllPatients(mcuID int, dateFrom, dateTo string) ([]PatientDetail, error) {
rows, err := db.DB.Query(`
SELECT
- mc.Mcu_CheckinoutPreregisterID,
+ s.Mcu_PatientSchedulePreregisterID,
mp.Mcu_PatientName,
- DATE_FORMAT(mc.Mcu_CheckinoutDate, '%Y-%m-%d'),
- DATE_FORMAT(mc.Mcu_CheckinoutInTime, '%H:%i'),
+ DATE_FORMAT(s.Mcu_PatientScheduleDate, '%Y-%m-%d'),
+ COALESCE(DATE_FORMAT(mc.Mcu_CheckinoutInTime, '%H:%i'), ''),
COALESCE(DATE_FORMAT(mc.Mcu_CheckinoutOutTime, '%H:%i'), ''),
IF(mc.Mcu_CheckinoutOutTime IS NOT NULL, 1, 0),
- rs.station_name,
+ COALESCE(rs.station_name, ''),
IF(
- (sp.Mcu_StationProgressSource = 'lab' AND sp.Mcu_StationProgressReceiveAt IS NOT NULL AND DATE(sp.Mcu_StationProgressReceiveAt) <= mc.Mcu_CheckinoutDate)
- OR (sp.Mcu_StationProgressSource = 'nonlab' AND sp.Mcu_StationProgressDoneAt IS NOT NULL AND DATE(sp.Mcu_StationProgressDoneAt) <= mc.Mcu_CheckinoutDate),
+ (sp.Mcu_StationProgressSource = 'lab' AND sp.Mcu_StationProgressReceiveAt IS NOT NULL AND DATE(sp.Mcu_StationProgressReceiveAt) <= s.Mcu_PatientScheduleDate)
+ OR (sp.Mcu_StationProgressSource = 'nonlab' AND sp.Mcu_StationProgressProcessAt IS NOT NULL AND DATE(sp.Mcu_StationProgressProcessAt) <= s.Mcu_PatientScheduleDate)
+ OR (sp.Mcu_StationProgressSource = 'nonlab' AND sp.Mcu_StationProgressDoneAt IS NOT NULL AND DATE(sp.Mcu_StationProgressDoneAt) <= s.Mcu_PatientScheduleDate),
+ 1, 0),
+ IF(
+ (sp.Mcu_StationProgressProcessAt IS NOT NULL AND DATE(sp.Mcu_StationProgressProcessAt) <= s.Mcu_PatientScheduleDate)
+ OR (sp.Mcu_StationProgressReceiveAt IS NOT NULL AND DATE(sp.Mcu_StationProgressReceiveAt) <= s.Mcu_PatientScheduleDate)
+ OR (sp.Mcu_StationProgressSamplingAt IS NOT NULL AND DATE(sp.Mcu_StationProgressSamplingAt) <= s.Mcu_PatientScheduleDate),
1, 0),
COALESCE(DATE_FORMAT(
CASE
@@ -435,29 +444,41 @@ func GetAllPatients(mcuID int, dateFrom, dateTo string) ([]PatientDetail, error)
END,
'%d/%m/%Y %H:%i'), ''),
COALESCE(DATE_FORMAT(
- IF(sp.Mcu_StationProgressSource = 'lab', sp.Mcu_StationProgressReceiveAt, sp.Mcu_StationProgressDoneAt),
+ CASE
+ WHEN sp.Mcu_StationProgressSource = 'lab' THEN sp.Mcu_StationProgressReceiveAt
+ WHEN sp.Mcu_StationProgressSource = 'nonlab' THEN COALESCE(sp.Mcu_StationProgressDoneAt, sp.Mcu_StationProgressProcessAt)
+ ELSE NULL
+ END,
'%d/%m/%Y %H:%i'), '')
- FROM mcu_checkinout mc
- JOIN mcu_patient mp ON mp.Mcu_PatientPreregisterID = mc.Mcu_CheckinoutPreregisterID
- JOIN mcu_patient_required_station rs ON
- rs.preregister_id = mc.Mcu_CheckinoutPreregisterID
+ FROM mcu_patient_schedule s
+ JOIN mcu_patient mp ON mp.Mcu_PatientPreregisterID = s.Mcu_PatientSchedulePreregisterID
+ AND mp.Mcu_PatientMcuID = ?
+ AND mp.Mcu_PatientIsActive = 'Y'
+ LEFT JOIN mcu_checkinout mc ON
+ mc.Mcu_CheckinoutPreregisterID = s.Mcu_PatientSchedulePreregisterID
+ AND mc.Mcu_CheckinoutMcuID = ?
+ AND mc.Mcu_CheckinoutDate = s.Mcu_PatientScheduleDate
+ AND mc.Mcu_CheckinoutIsActive = 'Y'
+ LEFT JOIN mcu_patient_required_station rs ON
+ rs.preregister_id = s.Mcu_PatientSchedulePreregisterID
AND rs.mcu_id = ?
LEFT JOIN mcu_station_progress sp ON
- sp.Mcu_StationProgressPreregisterID = mc.Mcu_CheckinoutPreregisterID
+ sp.Mcu_StationProgressPreregisterID = s.Mcu_PatientSchedulePreregisterID
AND sp.Mcu_StationProgressStationID = rs.sample_station_id
AND sp.Mcu_StationProgressMcuID = ?
- AND sp.Mcu_StationProgressCheckinDate = (
- SELECT MAX(sp2.Mcu_StationProgressCheckinDate)
- FROM mcu_station_progress sp2
- WHERE sp2.Mcu_StationProgressPreregisterID = mc.Mcu_CheckinoutPreregisterID
- AND sp2.Mcu_StationProgressStationID = rs.sample_station_id
- AND sp2.Mcu_StationProgressMcuID = ?
- AND sp2.Mcu_StationProgressCheckinDate <= mc.Mcu_CheckinoutDate
+ AND (
+ (sp.Mcu_StationProgressCheckinDate IS NOT NULL AND sp.Mcu_StationProgressCheckinDate <= s.Mcu_PatientScheduleDate)
+ OR
+ (sp.Mcu_StationProgressCheckinDate IS NULL AND (
+ (sp.Mcu_StationProgressDoneAt IS NOT NULL AND DATE(sp.Mcu_StationProgressDoneAt) <= s.Mcu_PatientScheduleDate)
+ OR (sp.Mcu_StationProgressProcessAt IS NOT NULL AND DATE(sp.Mcu_StationProgressProcessAt) <= s.Mcu_PatientScheduleDate)
+ OR (sp.Mcu_StationProgressReceiveAt IS NOT NULL AND DATE(sp.Mcu_StationProgressReceiveAt) <= s.Mcu_PatientScheduleDate)
+ OR (sp.Mcu_StationProgressSamplingAt IS NOT NULL AND DATE(sp.Mcu_StationProgressSamplingAt) <= s.Mcu_PatientScheduleDate)
+ ))
)
- WHERE mc.Mcu_CheckinoutMcuID = ?
- AND mc.Mcu_CheckinoutDate BETWEEN ? AND ?
- AND mc.Mcu_CheckinoutIsActive = 'Y'
- ORDER BY mc.Mcu_CheckinoutDate DESC, mc.Mcu_CheckinoutInTime DESC, rs.station_name
+ WHERE s.Mcu_PatientScheduleDate BETWEEN ? AND ?
+ AND s.Mcu_PatientScheduleIsActive = 'Y'
+ ORDER BY s.Mcu_PatientScheduleDate DESC, mc.Mcu_CheckinoutInTime DESC, mp.Mcu_PatientName ASC, rs.station_name
`, mcuID, mcuID, mcuID, mcuID, dateFrom, dateTo)
if err != nil {
return nil, err
@@ -470,8 +491,8 @@ func GetAllPatients(mcuID int, dateFrom, dateTo string) ([]PatientDetail, error)
for rows.Next() {
var pid int
var name, date, inTime, outTime, stationName, processAt, doneAt string
- var hasOutInt, doneInt int
- if err := rows.Scan(&pid, &name, &date, &inTime, &outTime, &hasOutInt, &stationName, &doneInt, &processAt, &doneAt); err != nil {
+ var hasOutInt, doneInt, progressInt int
+ if err := rows.Scan(&pid, &name, &date, &inTime, &outTime, &hasOutInt, &stationName, &doneInt, &progressInt, &processAt, &doneAt); err != nil {
continue
}
k := patientKey{ID: pid, Date: date}
@@ -489,15 +510,33 @@ func GetAllPatients(mcuID int, dateFrom, dateTo string) ([]PatientDetail, error)
patients = append(patients, p)
idx = len(patients) - 1
}
- done := doneInt == 1
- patients[idx].Stations = append(patients[idx].Stations, PatientStationStatus{
- Station: stationName,
- Done: done,
- ProcessAt: processAt,
- DoneAt: doneAt,
- })
- if done {
- patients[idx].DoneCount++
+ if stationName != "" {
+ done := doneInt == 1
+ patients[idx].Stations = append(patients[idx].Stations, PatientStationStatus{
+ Station: stationName,
+ Done: done,
+ Progress: progressInt == 1,
+ ProcessAt: processAt,
+ DoneAt: doneAt,
+ })
+ if done {
+ patients[idx].DoneCount++
+ }
+ if progressInt == 1 {
+ patients[idx].ProgressCount++
+ }
+ }
+ }
+
+ for i := range patients {
+ totalStations := len(patients[i].Stations)
+ switch {
+ case totalStations > 0 && patients[i].DoneCount == totalStations:
+ patients[i].ProgressStatus = "done"
+ case patients[i].DoneCount > 0 || patients[i].ProgressCount > 0:
+ patients[i].ProgressStatus = "partial"
+ default:
+ patients[i].ProgressStatus = "not_yet"
}
}
diff --git a/cpone-dashboard/menu/progress/handler.go b/cpone-dashboard/menu/progress/handler.go
index f23cca8..a2df4a7 100644
--- a/cpone-dashboard/menu/progress/handler.go
+++ b/cpone-dashboard/menu/progress/handler.go
@@ -21,6 +21,8 @@ type pageData struct {
CurrentProject projects.ProjectItem
Search string
Status string
+ Packet string
+ PacketOptions []PacketOption
Rows []ProgressRow
FilteredRows []ProgressRow
Summary ProgressSummary
@@ -64,7 +66,9 @@ func buildProgressData(r *http.Request) (pageData, int, error) {
summary := BuildProgressSummary(rows)
search := r.URL.Query().Get("search")
status := r.URL.Query().Get("status")
- filteredRows := FilterProgressRows(rows, search, status)
+ packet := r.URL.Query().Get("packet")
+ packetOptions := UniquePackets(rows)
+ filteredRows := FilterProgressRows(rows, search, status, packet)
page := parsePage(r.URL.Query().Get("page"))
pagedRows, hasMore := PaginateProgressRows(filteredRows, page, defaultPageSize)
visibleCount := page * defaultPageSize
@@ -77,6 +81,8 @@ func buildProgressData(r *http.Request) (pageData, int, error) {
CurrentProject: project,
Search: search,
Status: status,
+ Packet: packet,
+ PacketOptions: packetOptions,
Rows: rows,
FilteredRows: pagedRows,
Summary: summary,
diff --git a/cpone-dashboard/menu/progress/query.go b/cpone-dashboard/menu/progress/query.go
index 015a370..aa6f096 100644
--- a/cpone-dashboard/menu/progress/query.go
+++ b/cpone-dashboard/menu/progress/query.go
@@ -10,6 +10,9 @@ type ProgressRow struct {
NIP string
Name string
Posisi string
+ PacketCode string
+ PacketName string
+ Tests string
ResumeStatus string
Validated string
Published string
@@ -33,6 +36,17 @@ func GetProgressRows(mcuID int) ([]ProgressRow, error) {
NULLIF(TRIM(mp.Mcu_PatientPosisi), ''),
'-'
) AS posisi,
+ COALESCE((
+ SELECT GROUP_CONCAT(DISTINCT COALESCE(NULLIF(TRIM(pp.Mcu_PatientPacketCode), ''), pp.Mcu_PatientPacketName) ORDER BY COALESCE(NULLIF(TRIM(pp.Mcu_PatientPacketCode), ''), pp.Mcu_PatientPacketName) SEPARATOR ',')
+ FROM mcu_patient_packet pp
+ WHERE pp.Mcu_PatientPacketPreregisterID = mp.Mcu_PatientPreregisterID
+ ), '') AS packet_code,
+ COALESCE((
+ SELECT GROUP_CONCAT(DISTINCT pp.Mcu_PatientPacketName ORDER BY pp.Mcu_PatientPacketName SEPARATOR ', ')
+ FROM mcu_patient_packet pp
+ WHERE pp.Mcu_PatientPacketPreregisterID = mp.Mcu_PatientPreregisterID
+ ), '') AS packet_name,
+ COALESCE(NULLIF(TRIM(mp.Mcu_PatientPemeriksaan), ''), '') AS tests,
COALESCE(rs.Mcu_PatientResumeStatusStatus, '') AS resume_status,
COALESCE(rs.Mcu_PatientResumeStatusValidated, 'N') AS validated,
COALESCE(rs.Mcu_PatientResumeStatusPublished, 'N') AS published
@@ -54,7 +68,7 @@ func GetProgressRows(mcuID int) ([]ProgressRow, error) {
var result []ProgressRow
for rows.Next() {
var r ProgressRow
- if err := rows.Scan(&r.PreregisterID, &r.NIP, &r.Name, &r.Posisi, &r.ResumeStatus, &r.Validated, &r.Published); err != nil {
+ if err := rows.Scan(&r.PreregisterID, &r.NIP, &r.Name, &r.Posisi, &r.PacketCode, &r.PacketName, &r.Tests, &r.ResumeStatus, &r.Validated, &r.Published); err != nil {
continue
}
result = append(result, r)
@@ -75,14 +89,18 @@ func BuildProgressSummary(rows []ProgressRow) ProgressSummary {
return s
}
-func FilterProgressRows(rows []ProgressRow, search, status string) []ProgressRow {
+func FilterProgressRows(rows []ProgressRow, search, status, packetCode string) []ProgressRow {
search = strings.ToLower(strings.TrimSpace(search))
status = strings.TrimSpace(status)
- if search == "" && status == "" {
+ packetCode = strings.TrimSpace(packetCode)
+ if search == "" && status == "" && packetCode == "" {
return rows
}
out := make([]ProgressRow, 0, len(rows))
for _, r := range rows {
+ if packetCode != "" && r.PacketCode != packetCode {
+ continue
+ }
switch status {
case "validated":
if r.Validated != "Y" {
@@ -102,7 +120,7 @@ func FilterProgressRows(rows []ProgressRow, search, status string) []ProgressRow
}
}
if search != "" {
- hay := strings.ToLower(r.Name + " " + r.NIP + " " + r.Posisi)
+ hay := strings.ToLower(r.Name + " " + r.NIP + " " + r.Posisi + " " + r.PacketCode + " " + r.PacketName + " " + r.Tests)
if !strings.Contains(hay, search) {
continue
}
@@ -112,6 +130,40 @@ func FilterProgressRows(rows []ProgressRow, search, status string) []ProgressRow
return out
}
+type PacketOption struct {
+ Code string
+ Name string
+}
+
+func UniquePackets(rows []ProgressRow) []PacketOption {
+ seen := map[string]struct{}{}
+ out := []PacketOption{}
+ for _, r := range rows {
+ code := strings.TrimSpace(r.PacketCode)
+ if code == "" {
+ continue
+ }
+ if _, ok := seen[code]; ok {
+ continue
+ }
+ seen[code] = struct{}{}
+ name := strings.TrimSpace(r.PacketName)
+ if name == "" {
+ name = code
+ }
+ out = append(out, PacketOption{Code: code, Name: name})
+ }
+ // Keep deterministic order without new imports.
+ for i := 0; i < len(out); i++ {
+ for j := i + 1; j < len(out); j++ {
+ if out[j].Code < out[i].Code {
+ out[i], out[j] = out[j], out[i]
+ }
+ }
+ }
+ return out
+}
+
func Pct(num, total int) int {
if total == 0 {
return 0
diff --git a/cpone-dashboard/menu/result/handler.go b/cpone-dashboard/menu/result/handler.go
index 7c25f0d..df7c82c 100644
--- a/cpone-dashboard/menu/result/handler.go
+++ b/cpone-dashboard/menu/result/handler.go
@@ -23,6 +23,8 @@ type pageData struct {
CurrentProject projects.ProjectItem
Search string
Filter string
+ Packet string
+ PacketOptions []PacketOption
Rows []ResultRow
FilteredRows []ResultRow
Summary ResultSummary
@@ -65,7 +67,9 @@ func buildResultData(r *http.Request) (pageData, int, error) {
summary := BuildResultSummary(rows)
search := r.URL.Query().Get("search")
filter := r.URL.Query().Get("filter")
- filteredRows := FilterResultRows(rows, search, filter)
+ packet := r.URL.Query().Get("packet")
+ packetOptions := UniquePackets(rows)
+ filteredRows := FilterResultRows(rows, search, filter, packet)
page := parsePage(r.URL.Query().Get("page"))
pagedRows, hasMore := PaginateResultRows(filteredRows, page, defaultPageSize)
visibleCount := page * defaultPageSize
@@ -78,6 +82,8 @@ func buildResultData(r *http.Request) (pageData, int, error) {
CurrentProject: project,
Search: search,
Filter: filter,
+ Packet: packet,
+ PacketOptions: packetOptions,
Rows: rows,
FilteredRows: pagedRows,
Summary: summary,
diff --git a/cpone-dashboard/menu/result/query.go b/cpone-dashboard/menu/result/query.go
index c54ac5a..1e0d37c 100644
--- a/cpone-dashboard/menu/result/query.go
+++ b/cpone-dashboard/menu/result/query.go
@@ -9,6 +9,9 @@ type ResultRow struct {
NIP string
Name string
Posisi string
+ PacketCode string
+ PacketName string
+ Tests string
FileUrl string
ReportDate string
}
@@ -29,6 +32,17 @@ func GetResultRows(mcuID int) ([]ResultRow, error) {
NULLIF(TRIM(mp.Mcu_PatientPosisi), ''),
'-'
) AS posisi,
+ COALESCE((
+ SELECT GROUP_CONCAT(DISTINCT COALESCE(NULLIF(TRIM(pp.Mcu_PatientPacketCode), ''), pp.Mcu_PatientPacketName) ORDER BY COALESCE(NULLIF(TRIM(pp.Mcu_PatientPacketCode), ''), pp.Mcu_PatientPacketName) SEPARATOR ',')
+ FROM mcu_patient_packet pp
+ WHERE pp.Mcu_PatientPacketPreregisterID = mp.Mcu_PatientPreregisterID
+ ), '') AS packet_code,
+ COALESCE((
+ SELECT GROUP_CONCAT(DISTINCT pp.Mcu_PatientPacketName ORDER BY pp.Mcu_PatientPacketName SEPARATOR ', ')
+ FROM mcu_patient_packet pp
+ WHERE pp.Mcu_PatientPacketPreregisterID = mp.Mcu_PatientPreregisterID
+ ), '') AS packet_name,
+ COALESCE(NULLIF(TRIM(mp.Mcu_PatientPemeriksaan), ''), '') AS tests,
COALESCE(p.Published_McuDasboardFileUrl, '') AS file_url,
CASE
WHEN p.Published_McuDasboardFileUrl IS NOT NULL
@@ -53,7 +67,7 @@ func GetResultRows(mcuID int) ([]ResultRow, error) {
var result []ResultRow
for rows.Next() {
var r ResultRow
- if err := rows.Scan(&r.NIP, &r.Name, &r.Posisi, &r.FileUrl, &r.ReportDate); err != nil {
+ if err := rows.Scan(&r.NIP, &r.Name, &r.Posisi, &r.PacketCode, &r.PacketName, &r.Tests, &r.FileUrl, &r.ReportDate); err != nil {
continue
}
result = append(result, r)
@@ -71,14 +85,18 @@ func BuildResultSummary(rows []ResultRow) ResultSummary {
return s
}
-func FilterResultRows(rows []ResultRow, search, filter string) []ResultRow {
+func FilterResultRows(rows []ResultRow, search, filter, packetCode string) []ResultRow {
search = strings.ToLower(strings.TrimSpace(search))
filter = strings.TrimSpace(filter)
- if search == "" && filter == "" {
+ packetCode = strings.TrimSpace(packetCode)
+ if search == "" && filter == "" && packetCode == "" {
return rows
}
out := make([]ResultRow, 0, len(rows))
for _, r := range rows {
+ if packetCode != "" && r.PacketCode != packetCode {
+ continue
+ }
switch filter {
case "has_pdf":
if r.FileUrl == "" {
@@ -90,7 +108,7 @@ func FilterResultRows(rows []ResultRow, search, filter string) []ResultRow {
}
}
if search != "" {
- hay := strings.ToLower(r.Name + " " + r.NIP + " " + r.Posisi)
+ hay := strings.ToLower(r.Name + " " + r.NIP + " " + r.Posisi + " " + r.PacketCode + " " + r.PacketName + " " + r.Tests)
if !strings.Contains(hay, search) {
continue
}
@@ -100,6 +118,39 @@ func FilterResultRows(rows []ResultRow, search, filter string) []ResultRow {
return out
}
+type PacketOption struct {
+ Code string
+ Name string
+}
+
+func UniquePackets(rows []ResultRow) []PacketOption {
+ seen := map[string]struct{}{}
+ out := []PacketOption{}
+ for _, r := range rows {
+ code := strings.TrimSpace(r.PacketCode)
+ if code == "" {
+ continue
+ }
+ if _, ok := seen[code]; ok {
+ continue
+ }
+ seen[code] = struct{}{}
+ name := strings.TrimSpace(r.PacketName)
+ if name == "" {
+ name = code
+ }
+ out = append(out, PacketOption{Code: code, Name: name})
+ }
+ for i := 0; i < len(out); i++ {
+ for j := i + 1; j < len(out); j++ {
+ if out[j].Code < out[i].Code {
+ out[i], out[j] = out[j], out[i]
+ }
+ }
+ }
+ return out
+}
+
func PaginateResultRows(rows []ResultRow, page, pageSize int) ([]ResultRow, bool) {
if page < 1 {
page = 1
diff --git a/cpone-dashboard/templates/abnormal/index.html b/cpone-dashboard/templates/abnormal/index.html
index 8629fad..2b64ece 100644
--- a/cpone-dashboard/templates/abnormal/index.html
+++ b/cpone-dashboard/templates/abnormal/index.html
@@ -18,7 +18,7 @@
- Ganti project
+ Change Project
diff --git a/cpone-dashboard/templates/arrival/index.html b/cpone-dashboard/templates/arrival/index.html
index 7b5aa2f..f8a19ea 100644
--- a/cpone-dashboard/templates/arrival/index.html
+++ b/cpone-dashboard/templates/arrival/index.html
@@ -18,20 +18,21 @@
@@ -74,26 +75,44 @@
+
+
+
Per Paket Completion
+
Total pasien per paket vs sudah menyelesaikan semua station
+
+
+
+