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[]; onStudentClick?: (studentRkId: number) => void; } 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, onStudentClick, }) => { 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); const [saveNotice, setSaveNotice] = 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)); setSaveNotice('Đã lưu thay đổi sơ đồ chỗ ngồi!'); } else { localStorage.removeItem(`sc_seating_layout_${workspaceId}`); setSaveNotice('Đã xóa sơ đồ chỗ ngồi!'); } setLayout(newLayout); }; useEffect(() => { if (saveNotice) { const timer = setTimeout(() => { setSaveNotice(null); }, 2000); return () => clearTimeout(timer); } }, [saveNotice]); // 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 ? (
{ if (!isEditingStructure && onStudentClick) { onStudentClick(student.rkId); } }} > {!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}`)} onClick={() => onStudentClick && onStudentClick(s.rkId)} style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', padding: '0.5rem', border: '1px solid var(--border-color)', borderRadius: '8px', backgroundColor: '#ffffff', cursor: onStudentClick ? 'pointer' : 'grab', transition: 'var(--transition)', userSelect: 'none', boxShadow: 'var(--shadow-sm)', }} className="student-drag-item" >
{s.fullName} {s.studentCode}
); }) )}
)} {saveNotice && (
{saveNotice}
)}
); };