package handlers import ( "strconv" "strings" "time" "server/internal/middleware" "server/internal/models" internalWs "server/internal/websocket" "github.com/gofiber/fiber/v2" "gorm.io/gorm" ) func chatMessageDTO(m models.ChatMessage, staffName string) map[string]any { return map[string]any{ "id": m.ID, "staffId": m.StaffID, "studentRkId": m.StudentRkID, "senderRole": m.SenderRole, "body": m.Body, "createdAt": m.CreatedAt, "readAt": m.ReadAt, "staffName": staffName, } } // GET /api/chat/students?q= func SearchChatStudentsHandler(db *gorm.DB) fiber.Handler { return func(c *fiber.Ctx) error { q := strings.TrimSpace(c.Query("q", "")) limit := 30 query := db.Model(&models.Student{}) if q != "" { like := "%" + q + "%" query = query.Where("full_name LIKE ? OR student_code LIKE ? OR email LIKE ?", like, like, like) } var students []models.Student query.Order("full_name asc").Limit(limit).Find(&students) out := make([]fiber.Map, 0, len(students)) for _, s := range students { online := internalWs.Hub.IsStudentOnline(s.RkID) out = append(out, fiber.Map{ "studentRkId": s.RkID, "fullName": s.FullName, "studentCode": s.StudentCode, "email": s.Email, "online": online, }) } return c.JSON(fiber.Map{"data": out}) } } // GET /api/chat/conversations func ListChatConversationsHandler(db *gorm.DB) fiber.Handler { return func(c *fiber.Ctx) error { staffID := middleware.StaffIDFromCtx(c) type row struct { StudentRkID int64 LastAt time.Time Unread int64 } var rows []row db.Raw(` SELECT student_rk_id AS student_rk_id, MAX(created_at) AS last_at, SUM(CASE WHEN sender_role = 'student' AND read_at IS NULL THEN 1 ELSE 0 END) AS unread FROM chat_messages WHERE staff_id = ? GROUP BY student_rk_id ORDER BY last_at DESC LIMIT 50 `, staffID).Scan(&rows) out := make([]fiber.Map, 0, len(rows)) for _, r := range rows { var st models.Student db.Where("rk_id = ?", r.StudentRkID).First(&st) var last models.ChatMessage db.Where("staff_id = ? AND student_rk_id = ?", staffID, r.StudentRkID).Order("id desc").First(&last) out = append(out, fiber.Map{ "studentRkId": r.StudentRkID, "fullName": st.FullName, "studentCode": st.StudentCode, "lastMessage": last.Body, "lastAt": r.LastAt, "unread": r.Unread, }) } return c.JSON(fiber.Map{"data": out}) } } // GET /api/chat/messages/:studentRkId func ListChatMessagesHandler(db *gorm.DB) fiber.Handler { return func(c *fiber.Ctx) error { staffID := middleware.StaffIDFromCtx(c) studentRkID, _ := strconv.ParseInt(c.Params("studentRkId"), 10, 64) if studentRkID <= 0 { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid student id"}) } var staff models.StaffAccount _ = db.First(&staff, staffID).Error var msgs []models.ChatMessage db.Where("staff_id = ? AND student_rk_id = ?", staffID, studentRkID).Order("id asc").Limit(200).Find(&msgs) now := time.Now() db.Model(&models.ChatMessage{}). Where("staff_id = ? AND student_rk_id = ? AND sender_role = 'student' AND read_at IS NULL", staffID, studentRkID). Update("read_at", now) out := make([]fiber.Map, 0, len(msgs)) for _, m := range msgs { out = append(out, chatMessageDTO(m, staff.FullName)) } return c.JSON(fiber.Map{"data": out}) } } // POST /api/chat/messages func SendChatMessageHandler(db *gorm.DB) fiber.Handler { return func(c *fiber.Ctx) error { staffID := middleware.StaffIDFromCtx(c) var req struct { StudentRkID int64 `json:"studentRkId"` Body string `json:"body"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid payload"}) } body := strings.TrimSpace(req.Body) if body == "" || req.StudentRkID <= 0 { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "studentRkId and body required"}) } var staff models.StaffAccount if err := db.First(&staff, staffID).Error; err != nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "staff not found"}) } msg := models.ChatMessage{ StaffID: staffID, StudentRkID: req.StudentRkID, SenderRole: "staff", Body: body, } if err := db.Create(&msg).Error; err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } dto := chatMessageDTO(msg, staff.FullName) internalWs.Hub.PushChatToStudent(req.StudentRkID, dto) return c.JSON(fiber.Map{"ok": true, "data": dto}) } } // GET /api/student/chat/conversations?studentRkId= func StudentListChatConversationsHandler(db *gorm.DB) fiber.Handler { return func(c *fiber.Ctx) error { studentRkID, _ := strconv.ParseInt(c.Query("studentRkId"), 10, 64) if studentRkID <= 0 { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "studentRkId required"}) } type row struct { StaffID uint LastAt time.Time Unread int64 } var rows []row db.Raw(` SELECT staff_id AS staff_id, MAX(created_at) AS last_at, SUM(CASE WHEN sender_role = 'staff' AND read_at IS NULL THEN 1 ELSE 0 END) AS unread FROM chat_messages WHERE student_rk_id = ? GROUP BY staff_id ORDER BY last_at DESC LIMIT 50 `, studentRkID).Scan(&rows) out := make([]fiber.Map, 0, len(rows)) for _, r := range rows { var staff models.StaffAccount name, email := "", "" if db.First(&staff, r.StaffID).Error == nil { name = staff.FullName email = staff.Email if name == "" { name = email } } var last models.ChatMessage db.Where("staff_id = ? AND student_rk_id = ?", r.StaffID, studentRkID).Order("id desc").First(&last) out = append(out, fiber.Map{ "staffId": r.StaffID, "staffName": name, "staffEmail": email, "lastMessage": last.Body, "lastAt": r.LastAt, "unread": r.Unread, }) } return c.JSON(fiber.Map{"data": out}) } } // GET /api/student/chat/messages?studentRkId=&staffId= func StudentListChatHandler(db *gorm.DB) fiber.Handler { return func(c *fiber.Ctx) error { studentRkID, _ := strconv.ParseInt(c.Query("studentRkId"), 10, 64) staffID, _ := strconv.ParseUint(c.Query("staffId", "0"), 10, 64) if studentRkID <= 0 { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "studentRkId required"}) } query := db.Where("student_rk_id = ?", studentRkID) if staffID > 0 { query = query.Where("staff_id = ?", staffID) } var msgs []models.ChatMessage query.Order("id asc").Limit(200).Find(&msgs) now := time.Now() readQ := db.Model(&models.ChatMessage{}). Where("student_rk_id = ? AND sender_role = 'staff' AND read_at IS NULL", studentRkID) if staffID > 0 { readQ = readQ.Where("staff_id = ?", staffID) } readQ.Update("read_at", now) staffNames := map[uint]string{} out := make([]fiber.Map, 0, len(msgs)) for _, m := range msgs { name := staffNames[m.StaffID] if name == "" { var st models.StaffAccount if db.First(&st, m.StaffID).Error == nil { name = st.FullName if name == "" { name = st.Email } } staffNames[m.StaffID] = name } out = append(out, chatMessageDTO(m, name)) } var lastStaffID uint for i := len(msgs) - 1; i >= 0; i-- { if msgs[i].SenderRole == "staff" { lastStaffID = msgs[i].StaffID break } } return c.JSON(fiber.Map{"data": out, "replyStaffId": lastStaffID}) } } // POST /api/student/chat/messages func StudentSendChatHandler(db *gorm.DB) fiber.Handler { return func(c *fiber.Ctx) error { var req struct { StudentRkID int64 `json:"studentRkId"` StaffID uint `json:"staffId"` Body string `json:"body"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid payload"}) } body := strings.TrimSpace(req.Body) if body == "" || req.StudentRkID <= 0 { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid payload"}) } staffID := req.StaffID if staffID == 0 { var last models.ChatMessage if err := db.Where("student_rk_id = ? AND sender_role = 'staff'", req.StudentRkID). Order("id desc").First(&last).Error; err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "chưa có giáo viên nhắn tin"}) } staffID = last.StaffID } msg := models.ChatMessage{ StaffID: staffID, StudentRkID: req.StudentRkID, SenderRole: "student", Body: body, } if err := db.Create(&msg).Error; err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } var staff models.StaffAccount _ = db.First(&staff, staffID).Error dto := chatMessageDTO(msg, staff.FullName) internalWs.Hub.PushChatToStaff(staffID, dto) return c.JSON(fiber.Map{"ok": true, "data": dto}) } }