fix ui teacher
Some checks failed
Deploy on Master Change / deploy (push) Failing after 37s

This commit is contained in:
2026-07-02 16:05:59 +07:00
parent 1b424ab62d
commit 3fa8778084
4 changed files with 40 additions and 46 deletions

View File

@@ -61,7 +61,6 @@ export const ExamRoomWorkspace: React.FC<Props> = ({ examId, onBack }) => {
const [editable, setEditable] = useState(false); const [editable, setEditable] = useState(false);
const [prepEditable, setPrepEditable] = useState(false); const [prepEditable, setPrepEditable] = useState(false);
const [displayStatus, setDisplayStatus] = useState('draft'); const [displayStatus, setDisplayStatus] = useState('draft');
const canEditApps = displayStatus === 'draft' || displayStatus === 'ready' || displayStatus === 'active';
const [searchMode, setSearchMode] = useState<'single' | 'bulk'>('single'); const [searchMode, setSearchMode] = useState<'single' | 'bulk'>('single');
const [bulkInput, setBulkInput] = useState(''); const [bulkInput, setBulkInput] = useState('');
const [canPublish, setCanPublish] = useState(false); const [canPublish, setCanPublish] = useState(false);
@@ -96,6 +95,10 @@ export const ExamRoomWorkspace: React.FC<Props> = ({ examId, onBack }) => {
const [pdfLoading, setPdfLoading] = useState(false); const [pdfLoading, setPdfLoading] = useState(false);
const [inlineResource, setInlineResource] = useState<{ url: string; title: string; isImage: boolean } | null>(null); const [inlineResource, setInlineResource] = useState<{ url: string; title: string; isImage: boolean } | null>(null);
const canEditApps = displayStatus === 'draft' || displayStatus === 'ready' || displayStatus === 'active';
const anyPaperSent = students.some((s) => s.paperSentAt);
const canModifyPapers = prepEditable || (!anyPaperSent && (displayStatus === 'draft' || displayStatus === 'ready' || displayStatus === 'active'));
const load = useCallback(async () => { const load = useCallback(async () => {
setLoading(true); setLoading(true);
try { try {
@@ -727,12 +730,12 @@ export const ExamRoomWorkspace: React.FC<Props> = ({ examId, onBack }) => {
<p className="config-card-desc"> <p className="config-card-desc">
Mỗi gói đ gồm <strong>1 file PDF</strong> (đ chính) <strong>tài nguyên kèm</strong> (nh, zip, doc). Chia ngẫu nhiên sẽ gán cả gói cho từng sinh viên. Mỗi gói đ gồm <strong>1 file PDF</strong> (đ chính) <strong>tài nguyên kèm</strong> (nh, zip, doc). Chia ngẫu nhiên sẽ gán cả gói cho từng sinh viên.
</p> </p>
{!prepEditable && ( {!canModifyPapers && (
<p className="config-card-desc" style={{ color: 'var(--danger)' }}> <p className="config-card-desc" style={{ color: 'var(--danger)' }}>
Không thể thêm gói đ phòng đã khóa hoặc đã tới giờ thi. Không thể thay đi gói đ đ thi đã đưc gửi cho sinh viên hoặc phòng thi đã kết thúc.
</p> </p>
)} )}
{prepEditable && ( {canModifyPapers && (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.65rem', marginBottom: '0.75rem' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '0.65rem', marginBottom: '0.75rem' }}>
<input <input
className="search-input" className="search-input"
@@ -749,7 +752,7 @@ export const ExamRoomWorkspace: React.FC<Props> = ({ examId, onBack }) => {
/> />
</label> </label>
<label className="login-field"> <label className="login-field">
<span>Tài nguyên kèm (tùy chọn, nhiều file)</span> <span>Tài nguyên kèm (tùy chọn, many files)</span>
<input <input
type="file" type="file"
multiple multiple
@@ -800,7 +803,7 @@ export const ExamRoomWorkspace: React.FC<Props> = ({ examId, onBack }) => {
)) ))
)} )}
</ul> </ul>
{prepEditable && ( {canModifyPapers && (
<div style={{ display: 'flex', gap: '0.35rem', marginTop: '0.5rem', flexWrap: 'wrap' }}> <div style={{ display: 'flex', gap: '0.35rem', marginTop: '0.5rem', flexWrap: 'wrap' }}>
<label className="btn btn-ghost btn-sm" style={{ cursor: 'pointer' }}> <label className="btn btn-ghost btn-sm" style={{ cursor: 'pointer' }}>
+ Tài nguyên + Tài nguyên
@@ -815,7 +818,7 @@ export const ExamRoomWorkspace: React.FC<Props> = ({ examId, onBack }) => {
))} ))}
</div> </div>
)} )}
{papers.length > 0 && students.length > 0 && prepEditable && ( {papers.length > 0 && students.length > 0 && canModifyPapers && (
<button type="button" className="btn btn-secondary" style={{ width: '100%', marginTop: '0.65rem' }} onClick={assignRandomPackages}> <button type="button" className="btn btn-secondary" style={{ width: '100%', marginTop: '0.65rem' }} onClick={assignRandomPackages}>
Chia gói đ ngẫu nhiên Chia gói đ ngẫu nhiên
</button> </button>

View File

@@ -91,7 +91,6 @@ export const LearningTab: React.FC = () => {
const handleActivateClass = async (cls: ClassItem) => { const handleActivateClass = async (cls: ClassItem) => {
setShowAddClassModal(false); setShowAddClassModal(false);
try { try {
await apiApplyScheduleTemplate(cls.rkId);
await apiSaveAllowedApps(cls.rkId, DEFAULT_ALLOWED_APPS); await apiSaveAllowedApps(cls.rkId, DEFAULT_ALLOWED_APPS);
await loadActiveClasses(); await loadActiveClasses();
handleOpenConfig(cls as ActiveClassItem); handleOpenConfig(cls as ActiveClassItem);

View File

@@ -49,28 +49,23 @@ func validateExamRoomForPublish(db *gorm.DB, room *models.ExamRoom) []string {
if strings.TrimSpace(room.AllowedApps) == "" { if strings.TrimSpace(room.AllowedApps) == "" {
issues = append(issues, "Chưa cấu hình ứng dụng được phép khi thi") issues = append(issues, "Chưa cấu hình ứng dụng được phép khi thi")
} }
var paperCount int64
_ = db.Model(&models.ExamPaper{}).Where("exam_room_id = ?", room.ID).Count(&paperCount).Error
if paperCount == 0 {
issues = append(issues, "Chưa có gói đề (PDF + tài nguyên)")
}
var students []models.ExamRoomStudent var students []models.ExamRoomStudent
_ = db.Where("exam_room_id = ?", room.ID).Find(&students).Error _ = db.Where("exam_room_id = ?", room.ID).Find(&students).Error
if len(students) == 0 { if len(students) == 0 {
issues = append(issues, "Chưa có sinh viên trong phòng thi") issues = append(issues, "Chưa có sinh viên trong phòng thi")
} }
unassigned := 0
for _, s := range students {
if s.AssignedPaperID == nil {
unassigned++
}
}
if unassigned > 0 {
issues = append(issues, fmt.Sprintf("Còn %d sinh viên chưa được chia gói đề", unassigned))
}
return issues return issues
} }
func canModifyExamPapers(db *gorm.DB, room models.ExamRoom) bool {
if room.Status != models.ExamStatusDraft && room.Status != models.ExamStatusReady {
return false
}
var sentCount int64
_ = db.Model(&models.ExamRoomStudent{}).Where("exam_room_id = ? AND paper_sent_at IS NOT NULL", room.ID).Count(&sentCount).Error
return sentCount == 0
}
// ProcessScheduledExamSends gửi đề đã hẹn giờ — gọi định kỳ từ main. // ProcessScheduledExamSends gửi đề đã hẹn giờ — gọi định kỳ từ main.
func ProcessScheduledExamSends(db *gorm.DB) { func ProcessScheduledExamSends(db *gorm.DB) {
internalDb.ProcessExamRoomLifecycle(db) internalDb.ProcessExamRoomLifecycle(db)
@@ -617,9 +612,8 @@ func UploadExamPaperHandler(db *gorm.DB) fiber.Handler {
if err := db.First(&room, roomID).Error; err != nil { if err := db.First(&room, roomID).Error; err != nil {
return c.Status(404).JSON(fiber.Map{"error": "Không tìm thấy"}) return c.Status(404).JSON(fiber.Map{"error": "Không tìm thấy"})
} }
now := time.Now() if !canModifyExamPapers(db, room) {
if !internalDb.ExamRoomPrepEditable(room, now) { return c.Status(403).JSON(fiber.Map{"error": "Không thể thêm gói đề khi đề đã được gửi cho sinh viên hoặc phòng thi đã kết thúc"})
return c.Status(403).JSON(fiber.Map{"error": "Chỉ thêm gói đề khi phòng tạm thời hoặc sẵn sàng (trước giờ thi)"})
} }
form, err := c.MultipartForm() form, err := c.MultipartForm()
if err != nil { if err != nil {
@@ -724,8 +718,8 @@ func UploadExamPaperResourceHandler(db *gorm.DB) fiber.Handler {
if err := db.First(&room, roomID).Error; err != nil { if err := db.First(&room, roomID).Error; err != nil {
return c.Status(404).JSON(fiber.Map{"error": "Không tìm thấy phòng"}) return c.Status(404).JSON(fiber.Map{"error": "Không tìm thấy phòng"})
} }
if !internalDb.ExamRoomPrepEditable(room, time.Now()) { if !canModifyExamPapers(db, room) {
return c.Status(403).JSON(fiber.Map{"error": "Phòng thi đã bắt đầu — không thể thêm tài nguyên"}) return c.Status(403).JSON(fiber.Map{"error": "Không thể thêm tài nguyên khi đề đã được gửi cho sinh viên hoặc phòng thi đã kết thúc"})
} }
var paper models.ExamPaper var paper models.ExamPaper
if err := db.Where("id = ? AND exam_room_id = ?", paperID, roomID).First(&paper).Error; err != nil { if err := db.Where("id = ? AND exam_room_id = ?", paperID, roomID).First(&paper).Error; err != nil {
@@ -771,14 +765,12 @@ func DeleteExamPaperHandler(db *gorm.DB) fiber.Handler {
if err := db.First(&room, roomID).Error; err != nil { if err := db.First(&room, roomID).Error; err != nil {
return c.Status(404).JSON(fiber.Map{"error": "Không tìm thấy"}) return c.Status(404).JSON(fiber.Map{"error": "Không tìm thấy"})
} }
if !internalDb.ExamRoomPrepEditable(room, time.Now()) { if !canModifyExamPapers(db, room) {
return c.Status(403).JSON(fiber.Map{"error": "Phòng thi đã bắt đầu — không thể xóa gói đề"}) return c.Status(403).JSON(fiber.Map{"error": "Không thể xóa gói đề khi đề đã được gửi cho sinh viên hoặc phòng thi đã kết thúc"})
}
var assigned int64
_ = db.Model(&models.ExamRoomStudent{}).Where("exam_room_id = ? AND assigned_paper_id = ?", roomID, paperID).Count(&assigned).Error
if assigned > 0 {
return c.Status(403).JSON(fiber.Map{"error": "Gói đề đã được gán cho sinh viên — không thể xóa"})
} }
// Clear student assignments for this paper first
_ = db.Model(&models.ExamRoomStudent{}).Where("exam_room_id = ? AND assigned_paper_id = ?", roomID, paperID).Update("assigned_paper_id", nil).Error
_ = os.RemoveAll(filepath.Join(examRoomDir(roomID), "papers", fmt.Sprintf("%d", paperID))) _ = os.RemoveAll(filepath.Join(examRoomDir(roomID), "papers", fmt.Sprintf("%d", paperID)))
_ = db.Where("exam_paper_id = ?", paperID).Delete(&models.ExamPaperResource{}).Error _ = db.Where("exam_paper_id = ?", paperID).Delete(&models.ExamPaperResource{}).Error
_ = db.Where("id = ? AND exam_room_id = ?", paperID, roomID).Delete(&models.ExamPaper{}).Error _ = db.Where("id = ? AND exam_room_id = ?", paperID, roomID).Delete(&models.ExamPaper{}).Error
@@ -797,8 +789,8 @@ func RandomAssignExamPapersHandler(db *gorm.DB) fiber.Handler {
if err := db.First(&room, roomID).Error; err != nil { if err := db.First(&room, roomID).Error; err != nil {
return c.Status(404).JSON(fiber.Map{"error": "Không tìm thấy"}) return c.Status(404).JSON(fiber.Map{"error": "Không tìm thấy"})
} }
if !internalDb.ExamRoomPrepEditable(room, time.Now()) { if !canModifyExamPapers(db, room) {
return c.Status(403).JSON(fiber.Map{"error": "Chỉ chia gói đề khi phòng tạm thời hoặc sẵn sàng (trước giờ thi)"}) return c.Status(403).JSON(fiber.Map{"error": "Không thể chia gói đề khi đề đã được gửi cho sinh viên hoặc phòng thi đã kết thúc"})
} }
var papers []models.ExamPaper var papers []models.ExamPaper
if err := db.Where("exam_room_id = ?", roomID).Order("sort_order asc, id asc").Find(&papers).Error; err != nil || len(papers) == 0 { if err := db.Where("exam_room_id = ?", roomID).Order("sort_order asc, id asc").Find(&papers).Error; err != nil || len(papers) == 0 {

Binary file not shown.