diff --git a/management/src/App.tsx b/management/src/App.tsx index c698328..c5899f0 100644 --- a/management/src/App.tsx +++ b/management/src/App.tsx @@ -95,6 +95,7 @@ const SYSTEM_NAV: { section: SystemSection }[] = [ { section: 'organization' }, { section: 'network' }, { section: 'templates' }, + { section: 'seating' }, ]; function App() { diff --git a/management/src/components/ClassWorkspace.tsx b/management/src/components/ClassWorkspace.tsx index 3129651..388d9bd 100644 --- a/management/src/components/ClassWorkspace.tsx +++ b/management/src/components/ClassWorkspace.tsx @@ -25,6 +25,7 @@ import { ExamGridProctor } from './ExamGridProctor'; import { AppPoolModal } from './AppPoolModal'; import { AppTemplatePickerModal } from './AppTemplatePickerModal'; import { mergeKeywordCSV } from '../utils/appKeywords'; +import { WorkspaceSeatingChart } from './WorkspaceSeatingChart'; interface ClassWorkspaceProps { classId: number; @@ -433,7 +434,7 @@ export const ClassWorkspace: React.FC = ({ classId, sourceT
-
+
{activeSubTab === 'attendance' ? ( ) : activeSubTab === 'grid' ? ( @@ -456,46 +457,16 @@ export const ClassWorkspace: React.FC = ({ classId, sourceT }} /> ) : activeSubTab === 'roster' ? ( - /* Student Roster Grid */ - filteredStudents.length === 0 ? ( -
-
👤
-

Không tìm thấy học viên nào

-

Hãy thử tìm kiếm với từ khóa khác hoặc kiểm tra lại danh sách lớp.

-
- ) : ( -
- {filteredStudents.map(st => { - const isOnline = onlineIds.includes(st.rkId); - return ( -
setSelectedStudent(st)} - role="button" - tabIndex={0} - onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') setSelectedStudent(st); }} - > - -
- - {st.fullName} - - - {st.studentCode} - -
- - - {isOnline ? 'ONLINE' : 'OFFLINE'} - -
-
-
- ); - })} -
- ) + ({ + rkId: st.rkId, + fullName: st.fullName, + studentCode: st.studentCode, + avatar: st.avatar, + }))} + onlineIds={onlineIds} + /> ) : (
diff --git a/management/src/components/ExamRoomWorkspace.tsx b/management/src/components/ExamRoomWorkspace.tsx index 317c0b6..fd08728 100644 --- a/management/src/components/ExamRoomWorkspace.tsx +++ b/management/src/components/ExamRoomWorkspace.tsx @@ -20,6 +20,7 @@ import { StudentAvatar } from './StudentAvatar'; import { StudentDetailModal } from './StudentDetailModal'; import { fmtTime, localInputToISO, toLocalInput } from './ExamsTab'; import { ExamGridProctor } from './ExamGridProctor'; +import { WorkspaceSeatingChart } from './WorkspaceSeatingChart'; const EXAM_APP_SUGGESTIONS = [...new Set([...BASE_APP_SUGGESTIONS, 'msedge', 'edge', 'acrobat', 'foxit'])]; @@ -915,68 +916,18 @@ export const ExamRoomWorkspace: React.FC = ({ examId, onBack }) => {
-
+
{activeSubTab === 'roster' ? ( - students.length === 0 ? ( -
-
👤
-

Chưa có sinh viên

-

Thêm sinh viên vào phòng thi để bắt đầu giám sát.

- {prepEditable && ( - - )} -
- ) : filteredStudents.length === 0 ? ( -
-
🔍
-

Không tìm thấy sinh viên

-

Hãy thử tìm kiếm với từ khóa khác.

-
- ) : ( -
- {filteredStudents.map((s) => { - const isOnline = onlineIds.includes(s.studentRkId); - return ( -
setSelectedStudent(toStudentItem(s))} - role="button" - tabIndex={0} - onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') setSelectedStudent(toStudentItem(s)); }} - > - -
- - {s.fullName} - - - {s.studentCode} - -
- - - {isOnline ? 'ONLINE' : 'OFFLINE'} - -
- {(s.paperTitle || s.submitted) && ( -
- {s.paperTitle && {s.paperTitle}} - {s.submitted && · Đã nộp} -
- )} -
-
- ); - })} -
- ) + ({ + rkId: s.studentRkId, + fullName: s.fullName, + studentCode: s.studentCode, + avatar: s.avatar, + }))} + onlineIds={onlineIds} + /> ) : activeSubTab === 'grid' ? ( { + const [templates, setTemplates] = useState([]); + const [editing, setEditing] = useState(null); + const [creating, setCreating] = useState(false); + + // Form states + const [name, setName] = useState(''); + const [rows, setRows] = useState(5); + const [cols, setCols] = useState(6); + const [boardPosition, setBoardPosition] = useState<'top' | 'bottom' | 'left' | 'right'>('top'); + const [lockedSeats, setLockedSeats] = useState([]); + const [error, setError] = useState(''); + + useEffect(() => { + const stored = localStorage.getItem('sc_seating_templates'); + if (stored) { + try { + setTemplates(JSON.parse(stored)); + } catch { + setTemplates(DEFAULT_TEMPLATES); + } + } else { + localStorage.setItem('sc_seating_templates', JSON.stringify(DEFAULT_TEMPLATES)); + setTemplates(DEFAULT_TEMPLATES); + } + }, []); + + const saveTemplates = (newTemplates: SeatingTemplate[]) => { + localStorage.setItem('sc_seating_templates', JSON.stringify(newTemplates)); + setTemplates(newTemplates); + }; + + const handleOpenCreate = () => { + setCreating(true); + setEditing(null); + setName(''); + setRows(5); + setCols(6); + setBoardPosition('top'); + setLockedSeats([]); + setError(''); + }; + + const handleOpenEdit = (tpl: SeatingTemplate) => { + setEditing(tpl); + setCreating(false); + setName(tpl.name); + setRows(tpl.rows); + setCols(tpl.cols); + setBoardPosition(tpl.boardPosition); + setLockedSeats(tpl.lockedSeats || []); + setError(''); + }; + + const handleCloseForm = () => { + setCreating(false); + setEditing(null); + setError(''); + }; + + const handleToggleLock = (r: number, c: number) => { + const key = `${r},${c}`; + if (lockedSeats.includes(key)) { + setLockedSeats(lockedSeats.filter((k) => k !== key)); + } else { + setLockedSeats([...lockedSeats, key]); + } + }; + + const handleSave = () => { + if (!name.trim()) { + setError('Vui lòng nhập tên template.'); + return; + } + + // Clean up locked seats that fall outside the current rows/cols boundary + const validLockedSeats = lockedSeats.filter((key) => { + const [r, c] = key.split(',').map(Number); + return r < rows && c < cols; + }); + + const newTemplate: SeatingTemplate = { + id: editing ? editing.id : `tpl-${Date.now()}`, + name: name.trim(), + rows, + cols, + boardPosition, + lockedSeats: validLockedSeats, + }; + + let updatedList: SeatingTemplate[]; + if (editing) { + updatedList = templates.map((t) => (t.id === editing.id ? newTemplate : t)); + } else { + updatedList = [...templates, newTemplate]; + } + + saveTemplates(updatedList); + handleCloseForm(); + }; + + const handleDelete = (tpl: SeatingTemplate) => { + if (confirm(`Bạn có chắc chắn muốn xóa template "${tpl.name}"?`)) { + const updatedList = templates.filter((t) => t.id !== tpl.id); + saveTemplates(updatedList); + if (editing?.id === tpl.id) { + handleCloseForm(); + } + } + }; + + // Helper to render preview grid in edit/create form + const renderDesignerGrid = () => { + const gridItems = []; + for (let r = 0; r < rows; r++) { + for (let c = 0; c < cols; c++) { + const key = `${r},${c}`; + const isLocked = lockedSeats.includes(key); + gridItems.push( +
handleToggleLock(r, c)} + className={`seating-designer-cell ${isLocked ? 'locked' : 'available'}`} + style={{ + padding: '0.5rem', + border: '1px solid var(--border-color)', + borderRadius: '6px', + textAlign: 'center', + cursor: 'pointer', + fontSize: '0.78rem', + fontWeight: 600, + userSelect: 'none', + backgroundColor: isLocked ? '#e5e7eb' : '#eff6ff', + color: isLocked ? '#9ca3af' : '#1e40af', + borderColor: isLocked ? '#d1d5db' : '#bfdbfe', + transition: 'var(--transition)', + }} + > + {isLocked ? '🔒 Khóa' : `H${r + 1}-C${c + 1}`} +
+ ); + } + } + + return ( +
+
+ {boardPosition === 'top' && ( +
+ 📢 BẢNG / GIẢNG ĐƯỜNG (TOP) +
+ )} +
+ +
+ {boardPosition === 'left' && ( +
+ 📢 BẢNG (LEFT) +
+ )} + +
+ {gridItems} +
+ + {boardPosition === 'right' && ( +
+ 📢 BẢNG (RIGHT) +
+ )} +
+ +
+ {boardPosition === 'bottom' && ( +
+ 📢 BẢNG / GIẢNG ĐƯỜNG (BOTTOM) +
+ )} +
+
+ ); + }; + + return ( +
+
+
+

Danh sách mẫu sơ đồ chỗ ngồi

+

+ Thiết lập sẵn cấu trúc hàng, cột, bảng và các vị trí máy hỏng hoặc lối đi để áp dụng nhanh cho lớp hoặc phòng thi. +

+
+ {!creating && !editing && ( + + )} +
+ + {(creating || editing) ? ( +
+
+

{creating ? 'Thêm sơ đồ mẫu mới' : 'Chỉnh sửa sơ đồ mẫu'}

+ + {error &&
{error}
} + + + +
+ + + +
+ + + +
+ + +
+
+ +
+

Thiết kế vị trí khóa

+

+ Click chuột vào các ô bên dưới để chuyển đổi trạng thái giữa Chỗ trống khả dụngVị trí khóa (Không thể xếp sinh viên). +

+ {renderDesignerGrid()} +
+
+ ) : ( +
+ {templates.map((tpl) => ( +
+
+

{tpl.name}

+
+ Kích thước: {tpl.rows} hàng × {tpl.cols} cột ({tpl.rows * tpl.cols} vị trí) +
+
+ Bảng ở: {tpl.boardPosition.toUpperCase()} +
+
+ Vị trí khóa: {tpl.lockedSeats ? tpl.lockedSeats.length : 0} ô +
+
+ +
+ + +
+
+ ))} +
+ )} +
+ ); +}; diff --git a/management/src/components/SystemTab.tsx b/management/src/components/SystemTab.tsx index 1b852c5..209d865 100644 --- a/management/src/components/SystemTab.tsx +++ b/management/src/components/SystemTab.tsx @@ -8,6 +8,7 @@ import { import { OrganizationSection } from './OrganizationSection'; import { NetworkSection } from './NetworkSection'; import { AppTemplatesSection } from './AppTemplatesSection'; +import { SeatingTemplatesSection } from './SeatingTemplatesSection'; const SECTIONS: { id: SystemSection; icon: React.ReactNode; hint: string }[] = [ { @@ -40,6 +41,19 @@ const SECTIONS: { id: SystemSection; icon: React.ReactNode; hint: string }[] = [ ), }, + { + id: 'seating', + hint: 'Sơ đồ chỗ ngồi', + icon: ( + + + + + + + + ), + }, ]; export const SystemTab: React.FC = () => { @@ -86,6 +100,7 @@ export const SystemTab: React.FC = () => { {section === 'organization' && } {section === 'network' && } {section === 'templates' && } + {section === 'seating' && }
); diff --git a/management/src/components/WorkspaceSeatingChart.tsx b/management/src/components/WorkspaceSeatingChart.tsx new file mode 100644 index 0000000..d853435 --- /dev/null +++ b/management/src/components/WorkspaceSeatingChart.tsx @@ -0,0 +1,638 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import type { SeatingTemplate } from './SeatingTemplatesSection'; +import { StudentAvatar } from './StudentAvatar'; + +interface WorkspaceSeatingChartProps { + workspaceId: string; // 'class-' or 'exam-' + students: { + rkId: number; + fullName: string; + studentCode: string; + avatar?: string; + }[]; + onlineIds: number[]; +} + +interface SeatingLayout { + rows: number; + cols: number; + boardPosition: 'top' | 'bottom' | 'left' | 'right'; + lockedSeats: string[]; // "row,col" + seats: Record; // "row,col" -> studentRkId +} + +export const WorkspaceSeatingChart: React.FC = ({ + workspaceId, + students, + onlineIds, +}) => { + const [layout, setLayout] = useState(null); + const [templates, setTemplates] = useState([]); + const [searchQuery, setSearchQuery] = useState(''); + + // Custom setup states + const [setupMode, setSetupMode] = useState<'template' | 'custom'>('template'); + const [customRows, setCustomRows] = useState(5); + const [customCols, setCustomCols] = useState(6); + const [customBoard, setCustomBoard] = useState<'top' | 'bottom' | 'left' | 'right'>('top'); + const [selectedTemplateId, setSelectedTemplateId] = useState(''); + + // Edit mode for structural changes + const [isEditingStructure, setIsEditingStructure] = useState(false); + const [dragOverCell, setDragOverCell] = useState(null); + + // Load templates and layout + useEffect(() => { + // Load templates + const storedTemplates = localStorage.getItem('sc_seating_templates'); + if (storedTemplates) { + try { + setTemplates(JSON.parse(storedTemplates)); + } catch { + setTemplates([]); + } + } + + // Load layout for this room + const storedLayout = localStorage.getItem(`sc_seating_layout_${workspaceId}`); + if (storedLayout) { + try { + setLayout(JSON.parse(storedLayout)); + } catch { + setLayout(null); + } + } + }, [workspaceId]); + + // Save layout + const saveLayout = (newLayout: SeatingLayout | null) => { + if (newLayout) { + localStorage.setItem(`sc_seating_layout_${workspaceId}`, JSON.stringify(newLayout)); + } else { + localStorage.removeItem(`sc_seating_layout_${workspaceId}`); + } + setLayout(newLayout); + }; + + // Get active seated list + const seatedStudentIds = useMemo(() => { + if (!layout) return new Set(); + return new Set(Object.values(layout.seats)); + }, [layout]); + + // Filter unseated students list + const unseatedStudents = useMemo(() => { + return students.filter((s) => !seatedStudentIds.has(s.rkId)); + }, [students, seatedStudentIds]); + + // Search unseated students + const filteredUnseatedStudents = useMemo(() => { + return unseatedStudents.filter( + (s) => + s.fullName.toLowerCase().includes(searchQuery.toLowerCase()) || + s.studentCode.toLowerCase().includes(searchQuery.toLowerCase()) + ); + }, [unseatedStudents, searchQuery]); + + // Initialize with Template + const handleApplyTemplate = () => { + const tpl = templates.find((t) => t.id === selectedTemplateId); + if (!tpl) { + alert('Vui lòng chọn sơ đồ mẫu.'); + return; + } + const newLayout: SeatingLayout = { + rows: tpl.rows, + cols: tpl.cols, + boardPosition: tpl.boardPosition, + lockedSeats: [...tpl.lockedSeats], + seats: {}, + }; + saveLayout(newLayout); + }; + + // Initialize with Custom values + const handleApplyCustom = () => { + const newLayout: SeatingLayout = { + rows: customRows, + cols: customCols, + boardPosition: customBoard, + lockedSeats: [], + seats: {}, + }; + saveLayout(newLayout); + }; + + // Drag and drop handlers + const handleDragStart = (e: React.DragEvent, source: string) => { + e.dataTransfer.setData('text/plain', source); + }; + + const handleDragOver = (e: React.DragEvent, cellKey: string) => { + e.preventDefault(); + if (dragOverCell !== cellKey) { + setDragOverCell(cellKey); + } + }; + + const handleDragLeave = () => { + setDragOverCell(null); + }; + + const handleDrop = (e: React.DragEvent, targetRow: number, targetCol: number) => { + e.preventDefault(); + setDragOverCell(null); + if (!layout) return; + + const source = e.dataTransfer.getData('text/plain'); + if (!source) return; + + const targetKey = `${targetRow},${targetCol}`; + if (layout.lockedSeats.includes(targetKey)) return; + + const newSeats = { ...layout.seats }; + const targetStudentId = newSeats[targetKey]; + + if (source.startsWith('unseated:')) { + const studentId = Number(source.replace('unseated:', '')); + + // Place in target cell + newSeats[targetKey] = studentId; + + // If target had a student, B is kicked to unseated automatically (since they are no longer in newSeats) + // Clean up B from any other slots (shouldn't exist, but safety) + Object.keys(newSeats).forEach((key) => { + if (key !== targetKey && newSeats[key] === studentId) { + delete newSeats[key]; + } + }); + + } else if (source.startsWith('cell:')) { + const sourceKey = source.replace('cell:', ''); + const studentId = newSeats[sourceKey]; + if (!studentId) return; + + if (targetStudentId) { + // Swap students + newSeats[targetKey] = studentId; + newSeats[sourceKey] = targetStudentId; + } else { + // Move to empty cell + newSeats[targetKey] = studentId; + delete newSeats[sourceKey]; + } + } + + saveLayout({ ...layout, seats: newSeats }); + }; + + // Unseat student + const handleUnseat = (cellKey: string) => { + if (!layout) return; + const newSeats = { ...layout.seats }; + delete newSeats[cellKey]; + saveLayout({ ...layout, seats: newSeats }); + }; + + // Auto assign random + const handleAutoAssign = () => { + if (!layout) return; + + // Find all empty, unlocked cells + const emptyCells: string[] = []; + for (let r = 0; r < layout.rows; r++) { + for (let c = 0; c < layout.cols; c++) { + const key = `${r},${c}`; + if (!layout.lockedSeats.includes(key) && !layout.seats[key]) { + emptyCells.push(key); + } + } + } + + if (unseatedStudents.length > emptyCells.length) { + alert(`Không đủ chỗ ngồi cho tất cả học viên! Số học viên chưa có chỗ: ${unseatedStudents.length}, Số chỗ trống khả dụng: ${emptyCells.length}. Thiếu ${unseatedStudents.length - emptyCells.length} chỗ.`); + } + + // Shuffle empty cells + const shuffledCells = [...emptyCells].sort(() => Math.random() - 0.5); + + const newSeats = { ...layout.seats }; + const assignCount = Math.min(unseatedStudents.length, shuffledCells.length); + + for (let i = 0; i < assignCount; i++) { + newSeats[shuffledCells[i]] = unseatedStudents[i].rkId; + } + + saveLayout({ ...layout, seats: newSeats }); + }; + + // Clear all seats + const handleClearAllSeats = () => { + if (!layout) return; + if (confirm('Bạn có chắc chắn muốn giải tán tất cả chỗ ngồi?')) { + saveLayout({ ...layout, seats: {} }); + } + }; + + // Reset entire layout configuration + const handleResetLayout = () => { + if (confirm('Lưu ý: Xóa sơ đồ sẽ xóa toàn bộ vị trí chỗ ngồi hiện tại. Bạn có chắc chắn muốn thực hiện?')) { + saveLayout(null); + setIsEditingStructure(false); + } + }; + + // Toggle cell lock dynamically + const handleToggleCellLock = (r: number, c: number) => { + if (!layout) return; + const key = `${r},${c}`; + let newLocked = [...layout.lockedSeats]; + let newSeats = { ...layout.seats }; + + if (newLocked.includes(key)) { + newLocked = newLocked.filter((k) => k !== key); + } else { + newLocked.push(key); + delete newSeats[key]; // remove student if locked + } + + saveLayout({ + ...layout, + lockedSeats: newLocked, + seats: newSeats, + }); + }; + + // Render Seating Grid + const renderSeatingGrid = () => { + if (!layout) return null; + + const gridItems = []; + for (let r = 0; r < layout.rows; r++) { + for (let c = 0; c < layout.cols; c++) { + const key = `${r},${c}`; + const isLocked = layout.lockedSeats.includes(key); + const studentId = layout.seats[key]; + const student = studentId ? students.find((s) => s.rkId === studentId) : null; + const isOnline = student ? onlineIds.includes(student.rkId) : false; + const isHovered = dragOverCell === key; + + gridItems.push( +
!isLocked && handleDragOver(e, key)} + onDragLeave={handleDragLeave} + onDrop={(e) => !isLocked && handleDrop(e, r, c)} + onClick={() => isEditingStructure && handleToggleCellLock(r, c)} + className={`seating-grid-cell ${isLocked ? 'locked' : ''} ${isHovered ? 'dragover' : ''}`} + style={{ + minHeight: '80px', + border: isEditingStructure ? '1.5px dashed var(--border-color)' : '1px solid var(--border-color)', + borderRadius: '8px', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + padding: '0.4rem', + position: 'relative', + backgroundColor: isLocked + ? '#e5e7eb' + : isHovered + ? '#dbeafe' + : student + ? '#ffffff' + : '#f9fafb', + color: isLocked ? '#9ca3af' : 'inherit', + cursor: isEditingStructure ? 'pointer' : student ? 'grab' : 'default', + boxShadow: student ? 'var(--shadow-sm)' : 'none', + transition: 'var(--transition)', + borderColor: isHovered ? 'var(--accent)' : isLocked ? '#e5e7eb' : 'var(--border-color)', + }} + draggable={!!student && !isEditingStructure} + onDragStart={(e) => student && handleDragStart(e, `cell:${key}`)} + > + {isLocked ? ( +
+ 🔒 + Khóa +
+ ) : student ? ( +
+ {!isEditingStructure && ( + + )} + + + {student.fullName.split(' ').pop()} + + + {student.studentCode} + +
+ ) : ( +
+ + H{r + 1}-C{c + 1} + + Trống +
+ )} +
+ ); + } + } + + return ( +
+ {/* Board Top */} +
+ {layout.boardPosition === 'top' && ( +
+ 📢 BẢNG GIẢNG ĐƯỜNG +
+ )} +
+ +
+ {/* Board Left */} + {layout.boardPosition === 'left' && ( +
+ 📢 BẢNG GIẢNG ĐƯỜNG +
+ )} + + {/* Grid Container */} +
+ {gridItems} +
+ + {/* Board Right */} + {layout.boardPosition === 'right' && ( +
+ 📢 BẢNG GIẢNG ĐƯỜNG +
+ )} +
+ + {/* Board Bottom */} +
+ {layout.boardPosition === 'bottom' && ( +
+ 📢 BẢNG GIẢNG ĐƯỜNG +
+ )} +
+
+ ); + }; + + return ( +
+ + {!layout ? ( + // Empty state: select template or customize +
+
🪑
+

Chưa thiết lập sơ đồ chỗ ngồi

+

+ Thiết lập nhanh sơ đồ từ mẫu lớp học có sẵn hoặc tạo tự do kích thước mong muốn để xếp vị trí cho sinh viên. +

+ +
+
setSetupMode('template')}> +

+ + Chọn từ Sơ đồ mẫu +

+ + +
+ +
setSetupMode('custom')}> +

+ + Tạo tự do tùy ý +

+
+ setCustomRows(Math.max(1, Number(e.target.value)))} + disabled={setupMode !== 'custom'} + title="Số hàng" + /> + setCustomCols(Math.max(1, Number(e.target.value)))} + disabled={setupMode !== 'custom'} + title="Số cột" + /> +
+ + +
+
+
+ ) : ( + // Seating Workspace View +
+ + {/* Main Seating Panel */} +
+ {/* Toolbar */} +
+
+ + +
+ +
+ + +
+
+ + {isEditingStructure && ( +
+ 💡 Đang ở chế độ chỉnh sửa: Click chuột vào các ô trong sơ đồ để thay đổi khóa/mở các vị trí. +
+ )} + + {/* Render Grid */} + {renderSeatingGrid()} +
+ + {/* Right Panel: Unseated Students list */} +
+
+

+ Chưa vào chỗ + + {unseatedStudents.length} SV + +

+

+ Kéo thả sinh viên bên dưới vào vị trí trống trên sơ đồ. +

+
+ + setSearchQuery(e.target.value)} + /> + +
+ {filteredUnseatedStudents.length === 0 ? ( +
+ Không tìm thấy sinh viên nào. +
+ ) : ( + filteredUnseatedStudents.map((s) => { + const isOnline = onlineIds.includes(s.rkId); + return ( +
handleDragStart(e, `unseated:${s.rkId}`)} + style={{ + display: 'flex', + alignItems: 'center', + gap: '0.5rem', + padding: '0.5rem', + border: '1px solid var(--border-color)', + borderRadius: '8px', + backgroundColor: '#ffffff', + cursor: 'grab', + transition: 'var(--transition)', + userSelect: 'none', + boxShadow: 'var(--shadow-sm)', + }} + className="student-drag-item" + > + +
+ + {s.fullName} + + + {s.studentCode} + +
+ +
+ ); + }) + )} +
+
+ +
+ )} +
+ ); +}; diff --git a/management/src/navigation.ts b/management/src/navigation.ts index 9e22a5a..e8dae6e 100644 --- a/management/src/navigation.ts +++ b/management/src/navigation.ts @@ -1,4 +1,4 @@ -export type SystemSection = 'organization' | 'network' | 'templates'; +export type SystemSection = 'organization' | 'network' | 'templates' | 'seating'; export type TabId = 'dashboard' | 'classes' | 'students' | 'learning' | 'exams' | 'system' | 'profile' | 'student-affairs' | 'applications'; @@ -17,6 +17,7 @@ export const SYSTEM_SECTION_LABELS: Record = { organization: 'Tổ chức', network: 'Gói mạng', templates: 'Khung ứng dụng', + seating: 'Sơ đồ chỗ ngồi', }; export const TAB_LABELS: Record = { @@ -49,7 +50,7 @@ function isTabId(value: string | null): value is TabId { } function isSystemSection(value: string | null): value is SystemSection { - return value === 'organization' || value === 'network' || value === 'templates'; + return value === 'organization' || value === 'network' || value === 'templates' || value === 'seating'; } function resolveTabAndSection(tabParam: string | null): { tab: TabId; systemSection: SystemSection } {