package handlers import ( "strconv" "strings" "time" internalDb "server/internal/db" "server/internal/models" "server/internal/qldt" "server/internal/syncjobs" "github.com/gofiber/fiber/v2" "gorm.io/gorm" ) // Helper convert string to int func atoiDefault(s string, def int) int { if s == "" { return def } v, err := strconv.Atoi(s) if err != nil { return def } return v } // Handler bắt đầu đồng bộ lớp học func StartClassesSyncHandler(db *gorm.DB, client *qldt.Client, job *syncjobs.ClassesSyncJob) fiber.Handler { return func(c *fiber.Ctx) error { token := GetQldtToken(db) if token == "" { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Missing token QLDT_TOKEN"}) } started := job.Start(db, client, token) if !started { return c.Status(fiber.StatusConflict).JSON(fiber.Map{"error": "Sync classes is already running"}) } return c.JSON(fiber.Map{"ok": true, "message": "Classes sync job started"}) } } // Handler lấy status đồng bộ lớp học func GetClassesSyncStatusHandler(job *syncjobs.ClassesSyncJob) fiber.Handler { return func(c *fiber.Ctx) error { return c.JSON(job.Status()) } } // Handler bắt đầu đồng bộ toàn bộ sinh viên func StartStudentsSyncHandler(db *gorm.DB, client *qldt.Client, job *syncjobs.StudentsSyncJob) fiber.Handler { return func(c *fiber.Ctx) error { token := GetQldtToken(db) if token == "" { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Missing token QLDT_TOKEN"}) } started := job.Start(db, client, token) if !started { return c.Status(fiber.StatusConflict).JSON(fiber.Map{"error": "Sync students is already running"}) } return c.JSON(fiber.Map{"ok": true, "message": "Students sync job started"}) } } // Handler lấy status đồng bộ sinh viên func GetStudentsSyncStatusHandler(job *syncjobs.StudentsSyncJob) fiber.Handler { return func(c *fiber.Ctx) error { return c.JSON(job.Status()) } } type courseRefItem struct { CourseRkID int64 `json:"courseRkId"` CourseName string `json:"courseName"` } type classItem struct { RkID int64 `json:"rkId"` Name string `json:"name"` ClassCode string `json:"classCode"` Type string `json:"type"` StudentCount int `json:"studentCount"` SpecializeRkID *int64 `json:"specializeRkId"` SpecializeName *string `json:"specializeName"` SystemRkID *int64 `json:"systemRkId"` SystemCode *string `json:"systemCode"` SystemName *string `json:"systemName"` IsStudying bool `json:"isStudying"` Courses []courseRefItem `json:"courses"` } // Handler danh sách lớp học func ListClassesHandler(db *gorm.DB) fiber.Handler { return func(c *fiber.Ctx) error { page := atoiDefault(c.Query("page"), 1) pageSize := atoiDefault(c.Query("pageSize"), 20) if page < 1 { page = 1 } if pageSize < 1 { pageSize = 20 } queryDB := db.Model(&models.Class{}) if sys := c.Query("systemRkId"); sys != "" { if id, err := strconv.ParseInt(sys, 10, 64); err == nil { queryDB = queryDB.Where("system_rk_id = ?", id) } } if s := strings.TrimSpace(c.Query("q")); s != "" { pat := "%" + s + "%" queryDB = queryDB.Where("(name LIKE ? OR class_code LIKE ? OR type LIKE ? OR specialize_name LIKE ? OR system_name LIKE ?)", pat, pat, pat, pat, pat) } studyingOnly := c.Query("studyingOnly") if studyingOnly == "1" || studyingOnly == "true" { queryDB = queryDB.Where("is_studying = ?", true) } var total int64 if err := queryDB.Count(&total).Error; err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to count classes: " + err.Error()}) } var rows []models.Class if err := queryDB.Order("rk_id desc").Offset((page - 1) * pageSize).Limit(pageSize).Find(&rows).Error; err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to query classes: " + err.Error()}) } out := make([]classItem, 0, len(rows)) for _, r := range rows { out = append(out, classItem{ RkID: r.RkID, Name: r.Name, ClassCode: r.ClassCode, Type: r.Type, StudentCount: r.StudentCount, SpecializeRkID: r.SpecializeRkID, SpecializeName: r.SpecializeName, SystemRkID: r.SystemRkID, SystemCode: r.SystemCode, SystemName: r.SystemName, IsStudying: r.IsStudying, Courses: []courseRefItem{}, }) } return c.JSON(fiber.Map{ "data": out, "total": total, "page": page, "pageSize": pageSize, }) } } // GET /api/classes/:rkId - Lấy thông tin chi tiết một lớp học func GetClassHandler(db *gorm.DB) fiber.Handler { return func(c *fiber.Ctx) error { rkID, err := strconv.ParseInt(c.Params("rkId"), 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid class rkId"}) } var row models.Class if err := db.Where("rk_id = ?", rkID).First(&row).Error; err != nil { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Class not found"}) } coursesOut := []courseRefItem{} if cached, err := internalDb.ListClassCourses(db, rkID); err == nil { for _, co := range cached { coursesOut = append(coursesOut, courseRefItem{ CourseRkID: co.ID, CourseName: co.Name, }) } } out := classItem{ RkID: row.RkID, Name: row.Name, ClassCode: row.ClassCode, Type: row.Type, StudentCount: row.StudentCount, SpecializeRkID: row.SpecializeRkID, SpecializeName: row.SpecializeName, SystemRkID: row.SystemRkID, SystemCode: row.SystemCode, SystemName: row.SystemName, IsStudying: row.IsStudying, Courses: coursesOut, } return c.JSON(out) } } // Handler cập nhật trạng thái đang học (isStudying) của lớp học func PatchClassStudyingHandler(db *gorm.DB) fiber.Handler { return func(c *fiber.Ctx) error { rkIDStr := c.Params("rkId") rkID, err := strconv.ParseInt(rkIDStr, 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid class rkId"}) } var body struct { IsStudying bool `json:"isStudying"` } if err := c.BodyParser(&body); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid body payload"}) } var cl models.Class if err := db.Where("rk_id = ?", rkID).First(&cl).Error; err != nil { if gorm.ErrRecordNotFound == err { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Class not found"}) } return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } if body.IsStudying { // 1. Kiểm tra xem lớp đã cấu hình app được phép chưa var appConfig models.ClassAllowedApp errApp := db.Where("class_rk_id = ?", rkID).First(&appConfig).Error if errApp == gorm.ErrRecordNotFound || strings.TrimSpace(appConfig.Keywords) == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Không thể kích hoạt trạng thái đang học: Lớp chưa được cấu hình danh sách ứng dụng cho phép."}) } // 2. Kiểm tra xem có trong giờ học không var schedules []models.ClassSchedule errSched := db.Where("class_rk_id = ?", rkID).Find(&schedules).Error inSchedule := false if errSched == nil && len(schedules) > 0 { now := time.Now() goWeekday := now.Weekday() var scheduleDay int if goWeekday == time.Sunday { scheduleDay = 6 } else { scheduleDay = int(goWeekday) - 1 } currMin := now.Hour()*60 + now.Minute() for _, s := range schedules { if s.DayOfWeek == scheduleDay { startMin := parseTimeMinutes(s.StartTime) endMin := parseTimeMinutes(s.EndTime) if currMin >= startMin && currMin <= endMin { inSchedule = true break } } } } if !inSchedule { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Không thể kích hoạt trạng thái đang học: Hiện tại ngoài giờ học theo lịch đã cài đặt."}) } } cl.IsStudying = body.IsStudying if err := db.Save(&cl).Error; err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to update class: " + err.Error()}) } return c.JSON(fiber.Map{"ok": true, "data": cl}) } } // Handler lấy danh sách sinh viên của lớp học cụ thể func ListClassStudentsHandler(db *gorm.DB) fiber.Handler { return func(c *fiber.Ctx) error { rkIDStr := c.Params("rkId") classRkID, err := strconv.ParseInt(rkIDStr, 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid class rkId"}) } // Tìm danh sách student_rk_id thuộc lớp var links []models.ClassStudent if err := db.Where("class_rk_id = ?", classRkID).Find(&links).Error; err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to query class students: " + err.Error()}) } var studentRkIDs []int64 for _, link := range links { studentRkIDs = append(studentRkIDs, link.StudentRkID) } students := []models.Student{} if len(studentRkIDs) > 0 { if err := db.Where("rk_id IN ?", studentRkIDs).Order("full_name asc").Find(&students).Error; err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to load students info: " + err.Error()}) } } type studentRow struct { ID uint `json:"id"` RkID int64 `json:"rkId"` StudentCode string `json:"studentCode"` FullName string `json:"fullName"` Phone *string `json:"phone"` Email string `json:"email"` Avatar string `json:"avatar"` } out := make([]studentRow, 0, len(students)) for _, st := range students { row := studentRow{ ID: st.ID, RkID: st.RkID, StudentCode: st.StudentCode, FullName: st.FullName, Phone: st.Phone, Email: st.Email, } if st.Avatar != nil { row.Avatar = *st.Avatar } out = append(out, row) } return c.JSON(fiber.Map{"data": out, "total": len(out)}) } } // Handler danh sách toàn bộ sinh viên (đã sync) func ListAllStudentsHandler(db *gorm.DB) fiber.Handler { return func(c *fiber.Ctx) error { page := atoiDefault(c.Query("page"), 1) pageSize := atoiDefault(c.Query("pageSize"), 20) if page < 1 { page = 1 } if pageSize < 1 { pageSize = 20 } queryDB := db.Model(&models.Student{}) if s := strings.TrimSpace(c.Query("q")); s != "" { pat := "%" + s + "%" queryDB = queryDB.Where("(full_name LIKE ? OR student_code LIKE ? OR email LIKE ? OR phone LIKE ?)", pat, pat, pat, pat) } var total int64 if err := queryDB.Count(&total).Error; err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to count students: " + err.Error()}) } var students []models.Student if err := queryDB.Order("id desc").Offset((page - 1) * pageSize).Limit(pageSize).Find(&students).Error; err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to load students: " + err.Error()}) } return c.JSON(fiber.Map{ "data": students, "total": total, "page": page, "pageSize": pageSize, }) } } // Handler lấy stats nhanh hiển thị ở Dashboard func GetStatsHandler(db *gorm.DB) fiber.Handler { return func(c *fiber.Ctx) error { var totalClasses int64 var activeClasses int64 var totalStudents int64 db.Model(&models.Class{}).Count(&totalClasses) db.Model(&models.Class{}).Where("is_studying = ?", true).Count(&activeClasses) db.Model(&models.Student{}).Count(&totalStudents) return c.JSON(fiber.Map{ "totalClasses": totalClasses, "activeClasses": activeClasses, "totalStudents": totalStudents, }) } }