This commit is contained in:
@@ -1216,21 +1216,36 @@ export const apiMyClasses = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const apiSeatingLayout = {
|
export const apiSeatingLayout = {
|
||||||
get: async (classRkId: number): Promise<string | null> => {
|
getClass: async (classRkId: number): Promise<string | null> => {
|
||||||
const res = await staffFetch(`/classes/${classRkId}/seating-layout`);
|
const res = await staffFetch(`/classes/${classRkId}/seating-layout`);
|
||||||
if (!res.ok) return null;
|
if (!res.ok) return null;
|
||||||
const json = await res.json() as { layoutJson: string | null };
|
const json = await res.json() as { layoutJson: string | null };
|
||||||
return json.layoutJson ?? null;
|
return json.layoutJson ?? null;
|
||||||
},
|
},
|
||||||
save: async (classRkId: number, layoutJson: string): Promise<void> => {
|
saveClass: async (classRkId: number, layoutJson: string): Promise<void> => {
|
||||||
await staffFetch(`/classes/${classRkId}/seating-layout`, {
|
await staffFetch(`/classes/${classRkId}/seating-layout`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({ layoutJson }),
|
body: JSON.stringify({ layoutJson }),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
delete: async (classRkId: number): Promise<void> => {
|
deleteClass: async (classRkId: number): Promise<void> => {
|
||||||
await staffFetch(`/classes/${classRkId}/seating-layout`, { method: 'DELETE' });
|
await staffFetch(`/classes/${classRkId}/seating-layout`, { method: 'DELETE' });
|
||||||
},
|
},
|
||||||
|
getExam: async (examRoomId: number): Promise<string | null> => {
|
||||||
|
const res = await staffFetch(`/exam-rooms/${examRoomId}/seating-layout`);
|
||||||
|
if (!res.ok) return null;
|
||||||
|
const json = await res.json() as { layoutJson: string | null };
|
||||||
|
return json.layoutJson ?? null;
|
||||||
|
},
|
||||||
|
saveExam: async (examRoomId: number, layoutJson: string): Promise<void> => {
|
||||||
|
await staffFetch(`/exam-rooms/${examRoomId}/seating-layout`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({ layoutJson }),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
deleteExam: async (examRoomId: number): Promise<void> => {
|
||||||
|
await staffFetch(`/exam-rooms/${examRoomId}/seating-layout`, { method: 'DELETE' });
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const apiQldt = {
|
export const apiQldt = {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
apiExam,
|
apiExam,
|
||||||
|
apiSeatingLayout,
|
||||||
staffFetch,
|
staffFetch,
|
||||||
type ExamPaper,
|
type ExamPaper,
|
||||||
type ExamRoomStudent,
|
type ExamRoomStudent,
|
||||||
@@ -327,12 +328,12 @@ export const ExamRoomWorkspace: React.FC<Props> = ({ examId, onBack }) => {
|
|||||||
const assignRandomPackages = async () => {
|
const assignRandomPackages = async () => {
|
||||||
if (papers.length === 0) return;
|
if (papers.length === 0) return;
|
||||||
|
|
||||||
// Đọc sơ đồ chỗ ngồi từ localStorage (exam rooms vẫn lưu local)
|
// Đọc sơ đồ chỗ ngồi từ server
|
||||||
let seatingSeats: Record<string, number> | null = null;
|
let seatingSeats: Record<string, number> | null = null;
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(`sc_seating_layout_exam-${examId}`);
|
const json = await apiSeatingLayout.getExam(examId);
|
||||||
if (raw) {
|
if (json) {
|
||||||
const parsed = JSON.parse(raw) as { seats?: Record<string, number> };
|
const parsed = JSON.parse(json) as { seats?: Record<string, number> };
|
||||||
if (parsed.seats && Object.keys(parsed.seats).length > 0) {
|
if (parsed.seats && Object.keys(parsed.seats).length > 0) {
|
||||||
seatingSeats = parsed.seats;
|
seatingSeats = parsed.seats;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,11 +53,15 @@ export const WorkspaceSeatingChart: React.FC<WorkspaceSeatingChartProps> = ({
|
|||||||
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
||||||
const [listSearchQuery, setListSearchQuery] = useState('');
|
const [listSearchQuery, setListSearchQuery] = useState('');
|
||||||
|
|
||||||
// Detect if this is a class workspace (vs exam) to use API storage
|
// Parse workspace type and ID for API storage
|
||||||
const classRkId = useMemo(() => {
|
const workspaceInfo = useMemo(() => {
|
||||||
if (workspaceId.startsWith('class-')) {
|
if (workspaceId.startsWith('class-')) {
|
||||||
const n = Number(workspaceId.replace('class-', ''));
|
const n = Number(workspaceId.replace('class-', ''));
|
||||||
return isNaN(n) ? null : n;
|
return isNaN(n) ? null : { kind: 'class' as const, id: n };
|
||||||
|
}
|
||||||
|
if (workspaceId.startsWith('exam-')) {
|
||||||
|
const n = Number(workspaceId.replace('exam-', ''));
|
||||||
|
return isNaN(n) ? null : { kind: 'exam' as const, id: n };
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, [workspaceId]);
|
}, [workspaceId]);
|
||||||
@@ -103,58 +107,44 @@ export const WorkspaceSeatingChart: React.FC<WorkspaceSeatingChartProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (classRkId !== null) {
|
if (workspaceInfo !== null) {
|
||||||
// Load layout từ server theo lớp
|
const getter = workspaceInfo.kind === 'class'
|
||||||
apiSeatingLayout.get(classRkId).then((json) => {
|
? apiSeatingLayout.getClass(workspaceInfo.id)
|
||||||
|
: apiSeatingLayout.getExam(workspaceInfo.id);
|
||||||
|
getter.then((json) => {
|
||||||
if (json) {
|
if (json) {
|
||||||
try {
|
try { setLayout(JSON.parse(json)); } catch { setLayout(null); }
|
||||||
setLayout(JSON.parse(json));
|
|
||||||
} catch {
|
|
||||||
setLayout(null);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
setLayout(null);
|
setLayout(null);
|
||||||
}
|
}
|
||||||
}).catch(() => setLayout(null));
|
}).catch(() => setLayout(null));
|
||||||
} else {
|
} else {
|
||||||
// Exam rooms — vẫn dùng localStorage
|
setLayout(null);
|
||||||
const storedLayout = localStorage.getItem(`sc_seating_layout_${workspaceId}`);
|
|
||||||
if (storedLayout) {
|
|
||||||
try {
|
|
||||||
setLayout(JSON.parse(storedLayout));
|
|
||||||
} catch {
|
|
||||||
setLayout(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [workspaceId, classRkId]);
|
}, [workspaceId, workspaceInfo]);
|
||||||
|
|
||||||
// Save layout — debounce API calls, fallback to localStorage for exam rooms
|
// Save layout — debounce để tránh gọi API quá nhiều khi kéo thả nhanh
|
||||||
const saveLayout = (newLayout: SeatingLayout | null) => {
|
const saveLayout = (newLayout: SeatingLayout | null) => {
|
||||||
setLayout(newLayout);
|
setLayout(newLayout);
|
||||||
|
if (!workspaceInfo) return;
|
||||||
|
|
||||||
if (classRkId !== null) {
|
if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
|
||||||
// Debounce để tránh gọi API quá nhiều khi kéo thả nhanh
|
saveTimerRef.current = setTimeout(() => {
|
||||||
if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
|
|
||||||
saveTimerRef.current = setTimeout(() => {
|
|
||||||
if (newLayout) {
|
|
||||||
apiSeatingLayout.save(classRkId, JSON.stringify(newLayout)).catch(() => {});
|
|
||||||
setSaveNotice('Đã lưu sơ đồ chỗ ngồi!');
|
|
||||||
} else {
|
|
||||||
apiSeatingLayout.delete(classRkId).catch(() => {});
|
|
||||||
setSaveNotice('Đã xóa sơ đồ chỗ ngồi!');
|
|
||||||
}
|
|
||||||
}, 600);
|
|
||||||
} else {
|
|
||||||
// Exam rooms — localStorage
|
|
||||||
if (newLayout) {
|
if (newLayout) {
|
||||||
localStorage.setItem(`sc_seating_layout_${workspaceId}`, JSON.stringify(newLayout));
|
const json = JSON.stringify(newLayout);
|
||||||
setSaveNotice('Đã lưu thay đổi sơ đồ chỗ ngồi!');
|
const saver = workspaceInfo.kind === 'class'
|
||||||
|
? apiSeatingLayout.saveClass(workspaceInfo.id, json)
|
||||||
|
: apiSeatingLayout.saveExam(workspaceInfo.id, json);
|
||||||
|
saver.catch(() => {});
|
||||||
|
setSaveNotice('Đã lưu sơ đồ chỗ ngồi!');
|
||||||
} else {
|
} else {
|
||||||
localStorage.removeItem(`sc_seating_layout_${workspaceId}`);
|
const deleter = workspaceInfo.kind === 'class'
|
||||||
|
? apiSeatingLayout.deleteClass(workspaceInfo.id)
|
||||||
|
: apiSeatingLayout.deleteExam(workspaceInfo.id);
|
||||||
|
deleter.catch(() => {});
|
||||||
setSaveNotice('Đã xóa sơ đồ chỗ ngồi!');
|
setSaveNotice('Đã xóa sơ đồ chỗ ngồi!');
|
||||||
}
|
}
|
||||||
}
|
}, 600);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ func AutoMigrate(db *gorm.DB) error {
|
|||||||
&models.ClassCourse{},
|
&models.ClassCourse{},
|
||||||
&models.ClassAllowedApp{},
|
&models.ClassAllowedApp{},
|
||||||
&models.ClassSeatingLayout{},
|
&models.ClassSeatingLayout{},
|
||||||
|
&models.ExamSeatingLayout{},
|
||||||
&models.StaffMyClass{},
|
&models.StaffMyClass{},
|
||||||
&models.AppPoolEntry{},
|
&models.AppPoolEntry{},
|
||||||
&models.AppTemplate{},
|
&models.AppTemplate{},
|
||||||
|
|||||||
@@ -75,6 +75,65 @@ func DeleteClassSeatingLayoutHandler(db *gorm.DB) fiber.Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GET /exam-rooms/:id/seating-layout
|
||||||
|
func GetExamSeatingLayoutHandler(db *gorm.DB) fiber.Handler {
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
examID, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid exam room ID"})
|
||||||
|
}
|
||||||
|
var layout models.ExamSeatingLayout
|
||||||
|
if err := db.Where("exam_room_id = ?", examID).First(&layout).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return c.JSON(fiber.Map{"layoutJson": nil})
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||||
|
}
|
||||||
|
return c.JSON(fiber.Map{"layoutJson": layout.LayoutJSON})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT /exam-rooms/:id/seating-layout
|
||||||
|
func PutExamSeatingLayoutHandler(db *gorm.DB) fiber.Handler {
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
examID, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid exam room ID"})
|
||||||
|
}
|
||||||
|
var body struct {
|
||||||
|
LayoutJSON string `json:"layoutJson"`
|
||||||
|
}
|
||||||
|
if err := c.BodyParser(&body); err != nil || body.LayoutJSON == "" {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "layoutJson is required"})
|
||||||
|
}
|
||||||
|
layout := models.ExamSeatingLayout{
|
||||||
|
ExamRoomID: uint(examID),
|
||||||
|
LayoutJSON: body.LayoutJSON,
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
result := db.Clauses(clause.OnConflict{
|
||||||
|
Columns: []clause.Column{{Name: "exam_room_id"}},
|
||||||
|
DoUpdates: clause.AssignmentColumns([]string{"layout_json", "updated_at"}),
|
||||||
|
}).Create(&layout)
|
||||||
|
if result.Error != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": result.Error.Error()})
|
||||||
|
}
|
||||||
|
return c.JSON(fiber.Map{"ok": true})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE /exam-rooms/:id/seating-layout
|
||||||
|
func DeleteExamSeatingLayoutHandler(db *gorm.DB) fiber.Handler {
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
examID, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid exam room ID"})
|
||||||
|
}
|
||||||
|
db.Where("exam_room_id = ?", examID).Delete(&models.ExamSeatingLayout{})
|
||||||
|
return c.JSON(fiber.Map{"ok": true})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GET /staff/my-classes
|
// GET /staff/my-classes
|
||||||
func GetMyClassesHandler(db *gorm.DB) fiber.Handler {
|
func GetMyClassesHandler(db *gorm.DB) fiber.Handler {
|
||||||
return func(c *fiber.Ctx) error {
|
return func(c *fiber.Ctx) error {
|
||||||
|
|||||||
@@ -106,6 +106,15 @@ type ClassSeatingLayout struct {
|
|||||||
|
|
||||||
func (ClassSeatingLayout) TableName() string { return "class_seating_layouts" }
|
func (ClassSeatingLayout) TableName() string { return "class_seating_layouts" }
|
||||||
|
|
||||||
|
// ExamSeatingLayout lưu sơ đồ chỗ ngồi theo phòng thi
|
||||||
|
type ExamSeatingLayout struct {
|
||||||
|
ExamRoomID uint `gorm:"column:exam_room_id;primaryKey;not null" json:"examRoomId"`
|
||||||
|
LayoutJSON string `gorm:"column:layout_json;type:longtext;not null" json:"layoutJson"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ExamSeatingLayout) TableName() string { return "exam_seating_layouts" }
|
||||||
|
|
||||||
// ClassAllowedApp lưu từ khóa app được mở của lớp
|
// ClassAllowedApp lưu từ khóa app được mở của lớp
|
||||||
type ClassAllowedApp struct {
|
type ClassAllowedApp struct {
|
||||||
ClassRkID int64 `gorm:"column:class_rk_id;primaryKey;not null" json:"classRkId"`
|
ClassRkID int64 `gorm:"column:class_rk_id;primaryKey;not null" json:"classRkId"`
|
||||||
|
|||||||
@@ -213,6 +213,9 @@ func main() {
|
|||||||
staff.Post("/exam-rooms/:id/cancel", handlers.CancelExamRoomHandler(gormDB))
|
staff.Post("/exam-rooms/:id/cancel", handlers.CancelExamRoomHandler(gormDB))
|
||||||
staff.Post("/exam-rooms/:id/assign-random", handlers.RandomAssignExamPapersHandler(gormDB))
|
staff.Post("/exam-rooms/:id/assign-random", handlers.RandomAssignExamPapersHandler(gormDB))
|
||||||
staff.Post("/exam-rooms/:id/assign-papers-batch", handlers.AssignExamPapersBatchHandler(gormDB))
|
staff.Post("/exam-rooms/:id/assign-papers-batch", handlers.AssignExamPapersBatchHandler(gormDB))
|
||||||
|
staff.Get("/exam-rooms/:id/seating-layout", handlers.GetExamSeatingLayoutHandler(gormDB))
|
||||||
|
staff.Put("/exam-rooms/:id/seating-layout", handlers.PutExamSeatingLayoutHandler(gormDB))
|
||||||
|
staff.Delete("/exam-rooms/:id/seating-layout", handlers.DeleteExamSeatingLayoutHandler(gormDB))
|
||||||
staff.Post("/exam-rooms/:id/send-papers", handlers.SendExamPapersHandler(gormDB))
|
staff.Post("/exam-rooms/:id/send-papers", handlers.SendExamPapersHandler(gormDB))
|
||||||
staff.Get("/exam-rooms/:id/submissions", handlers.ListExamSubmissionsHandler(gormDB))
|
staff.Get("/exam-rooms/:id/submissions", handlers.ListExamSubmissionsHandler(gormDB))
|
||||||
staff.Get("/exam-rooms/:id/submissions/download-all", handlers.DownloadAllExamSubmissionsHandler(gormDB))
|
staff.Get("/exam-rooms/:id/submissions/download-all", handlers.DownloadAllExamSubmissionsHandler(gormDB))
|
||||||
|
|||||||
Reference in New Issue
Block a user