package arrival import ( "cpone-dashboard/db" "fmt" "sort" "strings" "time" ) type StationBadge struct { Name string Tone string // "success" | "warning" | "danger" | "neutral" } type StationStat struct { Name string Count int } type ArrivalRow struct { PreregisterID int NIP string Name string Department string InTime string Status string StatusTone string Stations []StationBadge } 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(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.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 sp.Mcu_StationProgressPreregisterID, sp.Mcu_StationProgressStationName, 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' 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) 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 string) []ArrivalRow { search = strings.ToLower(strings.TrimSpace(search)) dept = strings.TrimSpace(dept) if search == "" && dept == "" { return rows } out := make([]ArrivalRow, 0, len(rows)) for _, row := range rows { if dept != "" && dept != "All Departments" && row.Department != dept { continue } if search != "" { hay := strings.ToLower(row.Name + " " + row.NIP + " " + row.Department) if !strings.Contains(hay, search) { continue } } out = append(out, row) } 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 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 }