384 lines
12 KiB
Go
384 lines
12 KiB
Go
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, token string) fiber.Handler {
|
|
return func(c *fiber.Ctx) error {
|
|
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, token string) fiber.Handler {
|
|
return func(c *fiber.Ctx) error {
|
|
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,
|
|
})
|
|
}
|
|
}
|