package arrival import ( "cpone-dashboard/db" "fmt" "sort" "strings" "time" ) type StationBadge struct { Name string Tone string // "success" | "warning" | "danger" | "neutral" } type PaketStat struct { Name string Total int Completed int } type StationStat struct { Name string Count int } type ArrivalRow struct { PreregisterID int 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 { Name string CheckedIn int Pending int Total int } type ArrivalSummary struct { CheckedIn int Pending int Total int } func GetArrivalDates(mcuID int) ([]string, error) { rows, err := db.DB.Query(` SELECT DATE_FORMAT(Mcu_PatientScheduleDate, '%Y-%m-%d') AS schedule_date FROM mcu_patient_schedule WHERE Mcu_PatientSchedulePreregisterID IN ( SELECT Mcu_PatientPreregisterID FROM mcu_patient WHERE Mcu_PatientMcuID = ? AND Mcu_PatientIsActive = 'Y' ) AND Mcu_PatientScheduleIsActive = 'Y' GROUP BY Mcu_PatientScheduleDate ORDER BY Mcu_PatientScheduleDate DESC `, mcuID) if err != nil { return nil, err } defer rows.Close() var dates []string for rows.Next() { var d string if rows.Scan(&d) == nil && d != "" { dates = append(dates, d) } } return dates, nil } func GetArrivalRows(mcuID int, date string) ([]ArrivalRow, error) { rows, err := db.DB.Query(` SELECT mp.Mcu_PatientPreregisterID, COALESCE(NULLIF(TRIM(mp.Mcu_PatientNIP), ''), '') AS nip, COALESCE(NULLIF(TRIM(mp.Mcu_PatientName), ''), '') AS patient_name, COALESCE( NULLIF(TRIM(mp.Mcu_PatientDepartment), ''), NULLIF(TRIM(mp.Mcu_PatientDivision), ''), 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' WHEN mc.Mcu_CheckinoutInTime IS NOT NULL THEN 'In Progress' ELSE 'Not Check-in Yet' END AS status_text, CASE WHEN mc.Mcu_CheckinoutOutTime IS NOT NULL THEN 'success' WHEN mc.Mcu_CheckinoutInTime IS NOT NULL THEN 'warning' ELSE 'neutral' END AS status_tone 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 = mp.Mcu_PatientPreregisterID AND mc.Mcu_CheckinoutMcuID = ? AND mc.Mcu_CheckinoutDate = s.Mcu_PatientScheduleDate AND mc.Mcu_CheckinoutIsActive = 'Y' WHERE s.Mcu_PatientScheduleIsActive = 'Y' AND s.Mcu_PatientScheduleDate = ? ORDER BY CASE WHEN mc.Mcu_CheckinoutInTime IS NULL THEN 1 ELSE 0 END, mc.Mcu_CheckinoutInTime DESC, mp.Mcu_PatientName ASC `, mcuID, mcuID, date) if err != nil { return nil, err } defer rows.Close() var result []ArrivalRow for rows.Next() { var r ArrivalRow 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) == "" { r.NIP = "-" } if strings.TrimSpace(r.Name) == "" { r.Name = "-" } if strings.TrimSpace(r.Department) == "" { r.Department = "-" } result = append(result, r) } return result, nil } func GetStationProgress(mcuID int, date string) (map[int][]StationBadge, error) { rows, err := db.DB.Query(` SELECT rs.preregister_id, rs.station_name, CASE 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_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 } defer rows.Close() result := map[int][]StationBadge{} for rows.Next() { var preregID int var name, tone string if err := rows.Scan(&preregID, &name, &tone); err != nil { continue } result[preregID] = append(result[preregID], StationBadge{Name: name, Tone: tone}) } return result, nil } func BuildArrivalStats(rows []ArrivalRow) (ArrivalSummary, []DepartmentStat) { summary := ArrivalSummary{Total: len(rows)} deptMap := map[string]*DepartmentStat{} for _, row := range rows { if row.InTime != "" { summary.CheckedIn++ } dept := row.Department if dept == "" { dept = "-" } stat, ok := deptMap[dept] if !ok { stat = &DepartmentStat{Name: dept} deptMap[dept] = stat } stat.Total++ if row.InTime != "" { stat.CheckedIn++ } } summary.Pending = summary.Total - summary.CheckedIn if summary.Pending < 0 { summary.Pending = 0 } stats := make([]DepartmentStat, 0, len(deptMap)) for _, stat := range deptMap { stat.Pending = stat.Total - stat.CheckedIn if stat.Pending < 0 { stat.Pending = 0 } stats = append(stats, *stat) } sort.Slice(stats, func(i, j int) bool { if stats[i].CheckedIn != stats[j].CheckedIn { return stats[i].CheckedIn > stats[j].CheckedIn } if stats[i].Total != stats[j].Total { return stats[i].Total > stats[j].Total } return stats[i].Name < stats[j].Name }) return summary, stats } func BuildStationChart(stationMap map[int][]StationBadge) []StationStat { countMap := map[string]int{} for _, badges := range stationMap { for _, b := range badges { countMap[b.Name]++ } } stats := make([]StationStat, 0, len(countMap)) for name, count := range countMap { stats = append(stats, StationStat{Name: name, Count: count}) } sort.Slice(stats, func(i, j int) bool { return stats[i].Count > stats[j].Count }) return stats } func FilterArrivalRows(rows []ArrivalRow, search, dept, paket string) []ArrivalRow { search = strings.ToLower(strings.TrimSpace(search)) dept = strings.TrimSpace(dept) paket = strings.TrimSpace(paket) if search == "" && dept == "" && paket == "" { return rows } out := make([]ArrivalRow, 0, len(rows)) for _, row := range rows { if dept != "" && row.Department != dept { continue } if paket != "" && row.PaketCode != paket { continue } if search != "" { hay := strings.ToLower(row.Name + " " + row.NIP + " " + row.Department) if !strings.Contains(hay, search) { continue } } out = append(out, row) } 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 } if pageSize < 1 { pageSize = 30 } start := (page - 1) * pageSize if start >= len(rows) { return []ArrivalRow{}, false } end := start + pageSize if end > len(rows) { end = len(rows) } return rows[start:end], end < len(rows) } func UniqueDepartments(rows []ArrivalRow) []string { seen := map[string]struct{}{} var out []string for _, row := range rows { name := strings.TrimSpace(row.Department) if name == "" { name = "-" } if _, ok := seen[name]; ok { continue } seen[name] = struct{}{} out = append(out, name) } sort.Strings(out) 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 != "" { for _, d := range dates { if d == selected { return selected } } } today := time.Now().Format("2006-01-02") for _, d := range dates { if d == today { return today } } if len(dates) > 0 { return dates[0] } return fallback } func mustDayLabel(date string) string { if len(date) >= 10 { return fmt.Sprintf("%s/%s/%s", date[8:10], date[5:7], date[0:4]) } return date }