package handlers import ( "fmt" "strconv" "strings" "time" internalDb "server/internal/db" "server/internal/models" "github.com/gofiber/fiber/v2" "gorm.io/gorm" ) type studentShiftItem struct { Period int `json:"period"` CourseName string `json:"courseName"` StartTime string `json:"startTime"` EndTime string `json:"endTime"` IsActiveNow bool `json:"isActiveNow"` OnlineSeconds int `json:"onlineSeconds"` OfflineSeconds int `json:"offlineSeconds"` AttendanceStatus int `json:"attendanceStatus"` AttendanceLabel string `json:"attendanceLabel"` } // GET /api/student/status — trạng thái giám sát, lớp/ca hiện tại, tích lũy & điểm danh hôm nay func GetStudentStatusHandler(db *gorm.DB) fiber.Handler { return func(c *fiber.Ctx) error { studentID, err := strconv.ParseInt(c.Query("studentRkId", "0"), 10, 64) if err != nil || studentID <= 0 { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "studentRkId is required"}) } now := time.Now() today := now.Format("2006-01-02") dayIdx := weekdayIndex(now) currMin := now.Hour()*60 + now.Minute() classID, activePeriod := internalDb.FindActiveClassAndPeriodForStudent(db, studentID) if classID <= 0 { var link models.ClassStudent if err := db.Where("student_rk_id = ?", studentID).First(&link).Error; err == nil { classID = link.ClassRkID } } className, classCode := "", "" if classID > 0 { var cl models.Class if err := db.Where("rk_id = ?", classID).First(&cl).Error; err == nil { className = cl.Name classCode = cl.ClassCode } } var todaySchedules []models.ClassSchedule if classID > 0 { _ = db.Where("class_rk_id = ? AND day_of_week = ? AND is_active = ?", classID, dayIdx, true). Order("period asc, start_time asc").Find(&todaySchedules).Error } inScheduleNow := false currentPeriod := 0 currentCourse, currentStart, currentEnd := "", "", "" for _, s := range todaySchedules { startMin := parseTimeMinutes(s.StartTime) endMin := parseTimeMinutes(s.EndTime) if currMin >= startMin && currMin <= endMin { inScheduleNow = true currentPeriod = s.Period if currentPeriod < 1 { currentPeriod = 1 } currentCourse = s.CourseName currentStart = s.StartTime currentEnd = s.EndTime break } } if !inScheduleNow && activePeriod > 0 { currentPeriod = activePeriod } hasApps := false keywords := "" if classID > 0 { var appConfig models.ClassAllowedApp if err := db.Where("class_rk_id = ?", classID).First(&appConfig).Error; err == nil { keywords = strings.TrimSpace(appConfig.Keywords) hasApps = keywords != "" if hasApps { keywords = keywords + "," + systemAllowedApps } } } monitorMode := "outside_schedule" monitorLabel := "Ngoài giờ học" if classID <= 0 { monitorMode = "not_configured" monitorLabel = "Chưa xác định được lớp học" } else if !hasApps { monitorMode = "not_configured" monitorLabel = "Lớp chưa cấu hình ứng dụng được phép" } else if inScheduleNow { monitorMode = "learning" if currentCourse != "" { monitorLabel = fmt.Sprintf("Ca %d — %s (%s–%s)", currentPeriod, currentCourse, currentStart, currentEnd) } else { monitorLabel = fmt.Sprintf("Đang giám sát — Ca %d", currentPeriod) } } // monitorMode = "exam" — xử lý ở nhánh FindActiveExamForStudent phía trên sessionMap := map[int]models.StudentSession{} attMap := map[int]models.AttendanceResult{} if classID > 0 { var sessions []models.StudentSession _ = db.Where("student_rk_id = ? AND class_rk_id = ? AND session_date = ?", studentID, classID, today).Find(&sessions).Error for _, s := range sessions { p := s.Period if p < 1 { p = 1 } sessionMap[p] = s } var attendances []models.AttendanceResult _ = db.Where("student_rk_id = ? AND class_rk_id = ? AND session_date = ?", studentID, classID, today).Find(&attendances).Error for _, a := range attendances { p := a.Period if p < 1 { p = 1 } attMap[p] = a } } shifts := make([]studentShiftItem, 0, len(todaySchedules)) for _, sch := range todaySchedules { period := sch.Period if period < 1 { period = 1 } sess := sessionMap[period] att := attMap[period] startMin := parseTimeMinutes(sch.StartTime) endMin := parseTimeMinutes(sch.EndTime) isActiveNow := currMin >= startMin && currMin <= endMin attStatus := -1 attLabel := "Chưa tính" if att.ID > 0 { attStatus = att.Status attLabel = att.StatusLabel if attLabel == "" { attLabel = attendanceLabelFallback(att.Status) } } shifts = append(shifts, studentShiftItem{ Period: period, CourseName: sch.CourseName, StartTime: sch.StartTime, EndTime: sch.EndTime, IsActiveNow: isActiveNow, OnlineSeconds: sess.OnlineSeconds, OfflineSeconds: sess.OfflineSeconds, AttendanceStatus: attStatus, AttendanceLabel: attLabel, }) } // Ưu tiên phòng thi nếu sinh viên đang trong khung giờ thi if examInfo := internalDb.FindActiveExamForStudent(db, studentID); examInfo != nil { examKeywords := strings.TrimSpace(examInfo.Room.AllowedApps) if examKeywords == "" { examKeywords = systemAllowedApps } else { examKeywords = examKeywords + "," + systemAllowedApps } paperSent := examInfo.Enrollment.PaperSentAt != nil var subCount int64 _ = db.Model(&models.ExamSubmission{}).Where("exam_room_id = ? AND student_rk_id = ?", examInfo.Room.ID, studentID).Count(&subCount).Error examPayload := fiber.Map{ "examRoomId": examInfo.Room.ID, "examName": examInfo.Room.Name, "startTime": examInfo.Room.StartTime, "endTime": examInfo.Room.EndTime, "quizUrl": examInfo.Room.QuizURL, "paperSent": paperSent, "submitted": subCount > 0, } if paperSent && examInfo.Paper != nil { examPayload["paperId"] = examInfo.Paper.ID examPayload["paperTitle"] = examInfo.Paper.Title } return c.JSON(fiber.Map{ "sessionDate": today, "monitorMode": "exam", "monitorLabel": fmt.Sprintf("Phòng thi: %s", examInfo.Room.Name), "classRkId": classID, "className": className, "classCode": classCode, "currentPeriod": 0, "currentCourseName": "", "currentShiftStart": "", "currentShiftEnd": "", "inScheduleNow": false, "blockerActive": examKeywords != "", "allowedKeywords": examKeywords, "shifts": shifts, "exam": examPayload, }) } return c.JSON(fiber.Map{ "sessionDate": today, "monitorMode": monitorMode, "monitorLabel": monitorLabel, "classRkId": classID, "className": className, "classCode": classCode, "currentPeriod": currentPeriod, "currentCourseName": currentCourse, "currentShiftStart": currentStart, "currentShiftEnd": currentEnd, "inScheduleNow": inScheduleNow, "blockerActive": hasApps && inScheduleNow, "allowedKeywords": keywords, "shifts": shifts, }) } } func weekdayIndex(t time.Time) int { wd := t.Weekday() if wd == time.Sunday { return 6 } return int(wd) - 1 } func attendanceLabelFallback(status int) string { switch status { case 1: return "Nghỉ có phép" case 2: return "Nghỉ nửa buổi" case 3: return "Đi học muộn" case 4: return "Đi học đầy đủ" default: return "Nghỉ không phép" } }