|
|
|
|
@@ -14,6 +14,7 @@ import (
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
internalDb "server/internal/db"
|
|
|
|
|
"server/internal/middleware"
|
|
|
|
|
"server/internal/models"
|
|
|
|
|
internalWs "server/internal/websocket"
|
|
|
|
|
|
|
|
|
|
@@ -120,15 +121,23 @@ func deliverExamPaper(db *gorm.DB, enrollmentID uint) {
|
|
|
|
|
// GET /api/exam-rooms
|
|
|
|
|
func ListExamRoomsHandler(db *gorm.DB) fiber.Handler {
|
|
|
|
|
return func(c *fiber.Ctx) error {
|
|
|
|
|
staffID := middleware.StaffIDFromCtx(c)
|
|
|
|
|
mineOnly := c.Query("mine") == "1" || c.Query("mine") == "true"
|
|
|
|
|
|
|
|
|
|
q := db.Order("start_time desc")
|
|
|
|
|
if mineOnly && staffID > 0 {
|
|
|
|
|
q = q.Where("created_by_staff_id = ?", staffID)
|
|
|
|
|
}
|
|
|
|
|
var rooms []models.ExamRoom
|
|
|
|
|
if err := db.Order("start_time desc").Find(&rooms).Error; err != nil {
|
|
|
|
|
if err := q.Find(&rooms).Error; err != nil {
|
|
|
|
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
|
|
|
|
}
|
|
|
|
|
type row struct {
|
|
|
|
|
models.ExamRoom
|
|
|
|
|
StudentCount int `json:"studentCount"`
|
|
|
|
|
PaperCount int `json:"paperCount"`
|
|
|
|
|
DisplayStatus string `json:"displayStatus"`
|
|
|
|
|
StudentCount int `json:"studentCount"`
|
|
|
|
|
PaperCount int `json:"paperCount"`
|
|
|
|
|
DisplayStatus string `json:"displayStatus"`
|
|
|
|
|
CreatedByStaffID uint `json:"createdByStaffId"`
|
|
|
|
|
}
|
|
|
|
|
out := make([]row, 0, len(rooms))
|
|
|
|
|
now := time.Now()
|
|
|
|
|
@@ -140,10 +149,11 @@ func ListExamRoomsHandler(db *gorm.DB) fiber.Handler {
|
|
|
|
|
_ = db.Model(&models.ExamRoomStudent{}).Where("exam_room_id = ?", r.ID).Count(&sc).Error
|
|
|
|
|
_ = db.Model(&models.ExamPaper{}).Where("exam_room_id = ?", r.ID).Count(&pc).Error
|
|
|
|
|
out = append(out, row{
|
|
|
|
|
ExamRoom: r,
|
|
|
|
|
StudentCount: int(sc),
|
|
|
|
|
PaperCount: int(pc),
|
|
|
|
|
DisplayStatus: internalDb.ExamDisplayStatus(r, now),
|
|
|
|
|
ExamRoom: r,
|
|
|
|
|
StudentCount: int(sc),
|
|
|
|
|
PaperCount: int(pc),
|
|
|
|
|
DisplayStatus: internalDb.ExamDisplayStatus(r, now),
|
|
|
|
|
CreatedByStaffID: r.CreatedByStaffID,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
return c.JSON(fiber.Map{"data": out})
|
|
|
|
|
@@ -177,12 +187,13 @@ func CreateExamRoomHandler(db *gorm.DB) fiber.Handler {
|
|
|
|
|
}
|
|
|
|
|
apps := strings.TrimSpace(req.AllowedApps)
|
|
|
|
|
room := models.ExamRoom{
|
|
|
|
|
Name: req.Name,
|
|
|
|
|
StartTime: start,
|
|
|
|
|
EndTime: end,
|
|
|
|
|
AllowedApps: apps,
|
|
|
|
|
QuizURL: strings.TrimSpace(req.QuizURL),
|
|
|
|
|
Status: models.ExamStatusDraft,
|
|
|
|
|
Name: req.Name,
|
|
|
|
|
StartTime: start,
|
|
|
|
|
EndTime: end,
|
|
|
|
|
AllowedApps: apps,
|
|
|
|
|
QuizURL: strings.TrimSpace(req.QuizURL),
|
|
|
|
|
Status: models.ExamStatusDraft,
|
|
|
|
|
CreatedByStaffID: middleware.StaffIDFromCtx(c),
|
|
|
|
|
}
|
|
|
|
|
if err := db.Create(&room).Error; err != nil {
|
|
|
|
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
|
|
|
|
@@ -259,8 +270,9 @@ func GetExamRoomHandler(db *gorm.DB) fiber.Handler {
|
|
|
|
|
"editable": internalDb.ExamRoomEditable(room),
|
|
|
|
|
"prepEditable": internalDb.ExamRoomPrepEditable(room, now),
|
|
|
|
|
"canPublish": room.Status == models.ExamStatusDraft,
|
|
|
|
|
"canUnpublish": internalDb.ExamRoomCanUnpublish(room, time.Now()),
|
|
|
|
|
"canCancel": internalDb.ExamRoomCanCancel(room, time.Now()),
|
|
|
|
|
"canUnpublish": internalDb.ExamRoomCanUnpublish(room, now),
|
|
|
|
|
"canCancel": internalDb.ExamRoomCanCancel(room, now),
|
|
|
|
|
"canExtend": internalDb.ExamRoomCanExtend(room, now),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@@ -307,8 +319,15 @@ func UpdateExamRoomHandler(db *gorm.DB) fiber.Handler {
|
|
|
|
|
if err := c.BodyParser(&req); err != nil {
|
|
|
|
|
return c.Status(400).JSON(fiber.Map{"error": "Invalid body"})
|
|
|
|
|
}
|
|
|
|
|
now := time.Now()
|
|
|
|
|
ds := internalDb.ExamDisplayStatus(room, now)
|
|
|
|
|
if !internalDb.ExamRoomEditable(room) {
|
|
|
|
|
if room.Status == models.ExamStatusReady {
|
|
|
|
|
if ds == "active" {
|
|
|
|
|
// Đang thi: chỉ cho sửa app được phép + giờ kết thúc (gia hạn)
|
|
|
|
|
if req.Name != nil || req.StartTime != nil || req.QuizURL != nil {
|
|
|
|
|
return c.Status(403).JSON(fiber.Map{"error": "Khi đang thi chỉ được sửa ứng dụng được phép hoặc gia hạn giờ kết thúc"})
|
|
|
|
|
}
|
|
|
|
|
} else if room.Status == models.ExamStatusReady {
|
|
|
|
|
if req.Name != nil || req.StartTime != nil || req.EndTime != nil || req.QuizURL != nil {
|
|
|
|
|
return c.Status(403).JSON(fiber.Map{"error": "Chỉ sửa được cấu hình ứng dụng được phép khi phòng thi đã đẩy hoặc đang thi"})
|
|
|
|
|
}
|
|
|
|
|
@@ -331,6 +350,14 @@ func UpdateExamRoomHandler(db *gorm.DB) fiber.Handler {
|
|
|
|
|
if err != nil {
|
|
|
|
|
return c.Status(400).JSON(fiber.Map{"error": "endTime không hợp lệ"})
|
|
|
|
|
}
|
|
|
|
|
if ds == "active" {
|
|
|
|
|
if !t.After(now) {
|
|
|
|
|
return c.Status(400).JSON(fiber.Map{"error": "Giờ kết thúc mới phải sau thời điểm hiện tại"})
|
|
|
|
|
}
|
|
|
|
|
if !t.After(room.EndTime) {
|
|
|
|
|
return c.Status(400).JSON(fiber.Map{"error": "Gia hạn phải chọn giờ kết thúc muộn hơn giờ hiện tại của phòng"})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
room.EndTime = t
|
|
|
|
|
}
|
|
|
|
|
if !room.EndTime.After(room.StartTime) {
|
|
|
|
|
@@ -420,13 +447,54 @@ func CancelExamRoomHandler(db *gorm.DB) fiber.Handler {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// POST /api/exam-rooms/:id/extend — gia hạn thêm phút cho phòng đang thi
|
|
|
|
|
func ExtendExamRoomHandler(db *gorm.DB) fiber.Handler {
|
|
|
|
|
return func(c *fiber.Ctx) error {
|
|
|
|
|
id, err := parseUintParam(c, "id")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return c.Status(400).JSON(fiber.Map{"error": "Invalid id"})
|
|
|
|
|
}
|
|
|
|
|
var req struct {
|
|
|
|
|
Minutes int `json:"minutes"`
|
|
|
|
|
}
|
|
|
|
|
if err := c.BodyParser(&req); err != nil {
|
|
|
|
|
return c.Status(400).JSON(fiber.Map{"error": "Invalid body"})
|
|
|
|
|
}
|
|
|
|
|
if req.Minutes < 1 || req.Minutes > 480 {
|
|
|
|
|
return c.Status(400).JSON(fiber.Map{"error": "Số phút gia hạn phải từ 1 đến 480"})
|
|
|
|
|
}
|
|
|
|
|
var room models.ExamRoom
|
|
|
|
|
if err := db.First(&room, id).Error; err != nil {
|
|
|
|
|
return c.Status(404).JSON(fiber.Map{"error": "Không tìm thấy"})
|
|
|
|
|
}
|
|
|
|
|
now := time.Now()
|
|
|
|
|
if !internalDb.ExamRoomCanExtend(room, now) {
|
|
|
|
|
return c.Status(403).JSON(fiber.Map{"error": "Chỉ gia hạn được phòng thi đang diễn ra"})
|
|
|
|
|
}
|
|
|
|
|
base := room.EndTime
|
|
|
|
|
if !base.After(now) {
|
|
|
|
|
base = now
|
|
|
|
|
}
|
|
|
|
|
room.EndTime = base.Add(time.Duration(req.Minutes) * time.Minute)
|
|
|
|
|
if err := db.Save(&room).Error; err != nil {
|
|
|
|
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
|
|
|
|
}
|
|
|
|
|
return c.JSON(fiber.Map{
|
|
|
|
|
"ok": true,
|
|
|
|
|
"room": room,
|
|
|
|
|
"displayStatus": internalDb.ExamDisplayStatus(room, time.Now()),
|
|
|
|
|
"addedMinutes": req.Minutes,
|
|
|
|
|
"endTime": room.EndTime,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DELETE /api/exam-rooms/:id
|
|
|
|
|
func DeleteExamRoomHandler(db *gorm.DB) fiber.Handler {
|
|
|
|
|
return func(c *fiber.Ctx) error {
|
|
|
|
|
staffID := middleware.StaffIDFromCtx(c)
|
|
|
|
|
email, _ := c.Locals("staffEmail").(string)
|
|
|
|
|
if email != "phuocntb@rikkeiacademy.com" {
|
|
|
|
|
return c.Status(403).JSON(fiber.Map{"error": "Bạn không có quyền thực hiện chức năng này"})
|
|
|
|
|
}
|
|
|
|
|
isSuperAdmin := email == "phuocntb@rikkeiacademy.com"
|
|
|
|
|
|
|
|
|
|
id, err := parseUintParam(c, "id")
|
|
|
|
|
if err != nil {
|
|
|
|
|
@@ -436,6 +504,9 @@ func DeleteExamRoomHandler(db *gorm.DB) fiber.Handler {
|
|
|
|
|
if err := db.First(&room, id).Error; err != nil {
|
|
|
|
|
return c.Status(404).JSON(fiber.Map{"error": "Không tìm thấy"})
|
|
|
|
|
}
|
|
|
|
|
if !isSuperAdmin && (room.CreatedByStaffID == 0 || room.CreatedByStaffID != staffID) {
|
|
|
|
|
return c.Status(403).JSON(fiber.Map{"error": "Chỉ người tạo phòng hoặc admin mới được xóa"})
|
|
|
|
|
}
|
|
|
|
|
if internalDb.ExamRoomIsLive(room, time.Now()) {
|
|
|
|
|
return c.Status(403).JSON(fiber.Map{"error": "Không thể xóa phòng thi đang diễn ra — hãy hủy phòng trước"})
|
|
|
|
|
}
|
|
|
|
|
@@ -451,6 +522,7 @@ func DeleteExamRoomHandler(db *gorm.DB) fiber.Handler {
|
|
|
|
|
_ = db.Where("exam_paper_id = ?", p.ID).Delete(&models.ExamPaperResource{}).Error
|
|
|
|
|
}
|
|
|
|
|
_ = db.Where("exam_room_id = ?", id).Delete(&models.ExamPaper{}).Error
|
|
|
|
|
_ = db.Where("exam_room_id = ?", id).Delete(&models.ExamSeatingLayout{}).Error
|
|
|
|
|
_ = db.Delete(&room).Error
|
|
|
|
|
return c.JSON(fiber.Map{"ok": true})
|
|
|
|
|
}
|
|
|
|
|
@@ -593,8 +665,9 @@ func RemoveExamRoomStudentHandler(db *gorm.DB) fiber.Handler {
|
|
|
|
|
if err := db.First(&room, roomID).Error; err != nil {
|
|
|
|
|
return c.Status(404).JSON(fiber.Map{"error": "Không tìm thấy"})
|
|
|
|
|
}
|
|
|
|
|
if !internalDb.ExamRoomPrepEditable(room, time.Now()) {
|
|
|
|
|
return c.Status(403).JSON(fiber.Map{"error": "Phòng thi đã bắt đầu — không thể xóa sinh viên"})
|
|
|
|
|
ds := internalDb.ExamDisplayStatus(room, time.Now())
|
|
|
|
|
if ds == models.ExamStatusEnded {
|
|
|
|
|
return c.Status(403).JSON(fiber.Map{"error": "Phòng thi đã kết thúc — không thể xóa sinh viên"})
|
|
|
|
|
}
|
|
|
|
|
_ = db.Where("exam_room_id = ? AND student_rk_id = ?", roomID, studentRkID).Delete(&models.ExamRoomStudent{}).Error
|
|
|
|
|
return c.JSON(fiber.Map{"ok": true})
|
|
|
|
|
|