Update dashboard filters and packet views

This commit is contained in:
sas.fajri
2026-05-07 12:04:09 +07:00
parent da419950ce
commit 11f0d0436e
17 changed files with 1008 additions and 342 deletions

View File

@@ -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,

View File

@@ -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 != "" {