fix view
All checks were successful
Deploy on Master Change / deploy (push) Successful in 1m6s

This commit is contained in:
2026-07-06 09:58:31 +07:00
parent d7a49d2b5c
commit 52d62dbc96

View File

@@ -44,6 +44,32 @@ export const WorkspaceSeatingChart: React.FC<WorkspaceSeatingChartProps> = ({
const [dragOverCell, setDragOverCell] = useState<string | null>(null); const [dragOverCell, setDragOverCell] = useState<string | null>(null);
const [saveNotice, setSaveNotice] = useState<string | null>(null); const [saveNotice, setSaveNotice] = useState<string | null>(null);
// Toggle layout view modes
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
const [listSearchQuery, setListSearchQuery] = useState('');
// Reverse map seat positions for list view
const studentSeats = useMemo(() => {
if (!layout) return {} as Record<number, string>;
const mapping: Record<number, string> = {};
Object.entries(layout.seats).forEach(([cellKey, rkId]) => {
const [rStr, cStr] = cellKey.split(',');
const r = Number(rStr) + 1;
const c = Number(cStr) + 1;
mapping[rkId] = `Hàng ${r}, Cột ${c}`;
});
return mapping;
}, [layout]);
// Filter student list
const filteredListStudents = useMemo(() => {
return students.filter(
(s) =>
s.fullName.toLowerCase().includes(listSearchQuery.toLowerCase()) ||
s.studentCode.toLowerCase().includes(listSearchQuery.toLowerCase())
);
}, [students, listSearchQuery]);
// Load templates and layout // Load templates and layout
useEffect(() => { useEffect(() => {
// Load templates // Load templates
@@ -531,127 +557,233 @@ export const WorkspaceSeatingChart: React.FC<WorkspaceSeatingChartProps> = ({
</div> </div>
) : ( ) : (
// Seating Workspace View // Seating Workspace View
<div className="seating-workspace-grid"> <div
className={viewMode === 'grid' ? "seating-workspace-grid" : "seating-workspace-main"}
style={viewMode === 'list' ? { display: 'flex', flexDirection: 'column', height: '100%' } : undefined}
>
{/* Main Seating Panel */} {/* Main Seating Panel */}
<div className="seating-workspace-main"> <div className="seating-workspace-main" style={{ height: '100%' }}>
{/* Toolbar */} {/* Toolbar */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '10px' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '10px' }}>
<div style={{ display: 'flex', gap: '8px' }}> <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<button className="btn btn-primary btn-sm" onClick={handleAutoAssign}> <div style={{ display: 'flex', gap: '2px', marginRight: '10px', borderRight: '1px solid var(--border-color)', paddingRight: '10px' }}>
Xếp ngẫu nhiên <button
</button> type="button"
<button className="btn btn-secondary btn-sm" onClick={handleClearAllSeats}> className={`btn btn-sm ${viewMode === 'grid' ? 'btn-primary' : 'btn-secondary'}`}
🗑 Giải tán chỗ onClick={() => setViewMode('grid')}
</button> style={{
backgroundColor: viewMode === 'grid' ? 'var(--accent)' : '',
borderColor: viewMode === 'grid' ? 'var(--accent)' : '',
color: viewMode === 'grid' ? '#fff' : '',
}}
>
🪑 đ
</button>
<button
type="button"
className={`btn btn-sm ${viewMode === 'list' ? 'btn-primary' : 'btn-secondary'}`}
onClick={() => setViewMode('list')}
style={{
backgroundColor: viewMode === 'list' ? 'var(--accent)' : '',
borderColor: viewMode === 'list' ? 'var(--accent)' : '',
color: viewMode === 'list' ? '#fff' : '',
}}
>
📋 Danh sách
</button>
</div>
{viewMode === 'grid' && (
<>
<button className="btn btn-primary btn-sm" onClick={handleAutoAssign}>
Xếp ngẫu nhiên
</button>
<button className="btn btn-secondary btn-sm" onClick={handleClearAllSeats}>
🗑 Giải tán chỗ
</button>
</>
)}
</div> </div>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}> {viewMode === 'grid' && (
<button <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
className={`btn btn-sm ${isEditingStructure ? 'btn-primary' : 'btn-secondary'}`} <button
onClick={() => setIsEditingStructure(!isEditingStructure)} className={`btn btn-sm ${isEditingStructure ? 'btn-primary' : 'btn-secondary'}`}
style={{ onClick={() => setIsEditingStructure(!isEditingStructure)}
backgroundColor: isEditingStructure ? 'var(--accent)' : '', style={{
borderColor: isEditingStructure ? 'var(--accent)' : '', backgroundColor: isEditingStructure ? 'var(--accent)' : '',
color: isEditingStructure ? '#fff' : '', borderColor: isEditingStructure ? 'var(--accent)' : '',
}} color: isEditingStructure ? '#fff' : '',
> }}
{isEditingStructure ? '✔️ Xong thiết lập' : '⚙️ Khóa/Mở ô chỗ'} >
</button> {isEditingStructure ? '✔️ Xong thiết lập' : '⚙️ Khóa/Mở ô chỗ'}
<button className="btn btn-secondary btn-sm text-danger" style={{ borderColor: '#fca5a5' }} onClick={handleResetLayout}> </button>
Reset đ <button className="btn btn-secondary btn-sm text-danger" style={{ borderColor: '#fca5a5' }} onClick={handleResetLayout}>
</button> Reset đ
</div> </button>
</div>
)}
</div> </div>
{isEditingStructure && ( {viewMode === 'grid' ? (
<div style={{ backgroundColor: '#fffbeb', border: '1px solid #fef3c7', padding: '0.65rem 1rem', borderRadius: '8px', color: '#b45309', fontSize: '0.8rem', fontWeight: 600 }}> <>
💡 Đang chế đ chỉnh sửa: Click chuột vào các ô trong đ đ thay đi khóa/mở các vị trí. {isEditingStructure && (
<div style={{ backgroundColor: '#fffbeb', border: '1px solid #fef3c7', padding: '0.65rem 1rem', borderRadius: '8px', color: '#b45309', fontSize: '0.8rem', fontWeight: 600 }}>
💡 Đang chế đ chỉnh sửa: Click chuột vào các ô trong đ đ thay đi khóa/mở các vị trí.
</div>
)}
{renderSeatingGrid()}
</>
) : (
// List View of Students
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0, gap: '10px' }}>
<div className="attendance-toolbar" style={{ display: 'flex', gap: '10px', margin: 0 }}>
<input
type="search"
placeholder="Tìm sinh viên..."
className="search-input"
style={{ maxWidth: '320px', padding: '0.4rem 0.6rem', fontSize: '0.85rem' }}
value={listSearchQuery}
onChange={(e) => setListSearchQuery(e.target.value)}
/>
</div>
<div className="attendance-table-scroll table-wrapper" style={{ border: 'none', flex: 1, overflowY: 'auto' }}>
{filteredListStudents.length === 0 ? (
<div style={{ textAlign: 'center', padding: '3rem 0', color: 'var(--text-muted)', fontSize: '0.9rem' }}>
Không tìm thấy sinh viên nào.
</div>
) : (
<table className="data-table">
<thead>
<tr>
<th>Sinh viên</th>
<th> SV</th>
<th>Vị trí chỗ ngồi</th>
<th>Trạng thái</th>
<th style={{ textAlign: 'right' }}>Thao tác</th>
</tr>
</thead>
<tbody>
{filteredListStudents.map((s) => {
const isOnline = onlineIds.includes(s.rkId);
const seatLabel = studentSeats[s.rkId] || 'Chưa xếp chỗ';
return (
<tr key={s.rkId}>
<td>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<StudentAvatar fullName={s.fullName} avatar={s.avatar} isOnline={isOnline} size={28} />
<div style={{ fontWeight: 600, color: 'var(--text-primary)' }}>{s.fullName}</div>
</div>
</td>
<td><code>{s.studentCode}</code></td>
<td>
<span className={`badge ${studentSeats[s.rkId] ? 'badge-info' : 'badge-muted'}`}>
{seatLabel}
</span>
</td>
<td style={{ color: isOnline ? 'var(--success)' : 'var(--text-muted)', fontWeight: 700 }}>
{isOnline ? 'Online' : 'Offline'}
</td>
<td style={{ textAlign: 'right' }}>
<button
type="button"
className="btn btn-primary btn-sm"
onClick={() => onStudentClick && onStudentClick(s.rkId)}
>
Giám sát
</button>
</td>
</tr>
);
})}
</tbody>
</table>
)}
</div>
</div> </div>
)} )}
{/* Render Grid */}
{renderSeatingGrid()}
</div> </div>
{/* Right Panel: Unseated Students list */} {/* Right Panel: Unseated Students list */}
<div className="seating-workspace-sidebar"> {viewMode === 'grid' && (
<div> <div className="seating-workspace-sidebar">
<h4 style={{ margin: 0, fontSize: '0.95rem', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <div>
<span>Chưa vào chỗ</span> <h4 style={{ margin: 0, fontSize: '0.95rem', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span className="badge" style={{ background: 'var(--accent-light)', color: 'var(--accent)', fontSize: '0.75rem', fontWeight: 700, padding: '0.15rem 0.4rem', borderRadius: '10px' }}> <span>Chưa vào chỗ</span>
{unseatedStudents.length} SV <span className="badge" style={{ background: 'var(--accent-light)', color: 'var(--accent)', fontSize: '0.75rem', fontWeight: 700, padding: '0.15rem 0.4rem', borderRadius: '10px' }}>
</span> {unseatedStudents.length} SV
</h4> </span>
<p style={{ margin: '4px 0 0 0', color: 'var(--text-secondary)', fontSize: '0.78rem' }}> </h4>
Kéo thả sinh viên bên dưới vào vị trí trống trên đ. <p style={{ margin: '4px 0 0 0', color: 'var(--text-secondary)', fontSize: '0.78rem' }}>
</p> Kéo thả sinh viên bên dưới vào vị trí trống trên đ.
</div> </p>
</div>
<input <input
type="search" type="search"
placeholder="Tìm tên, mã sinh viên..." placeholder="Tìm tên, mã sinh viên..."
className="search-input" className="search-input"
style={{ width: '100%', padding: '0.4rem 0.6rem', fontSize: '0.8rem' }} style={{ width: '100%', padding: '0.4rem 0.6rem', fontSize: '0.8rem' }}
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
/> />
<div <div
style={{ style={{
flex: 1, flex: 1,
overflowY: 'auto', overflowY: 'auto',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
gap: '8px', gap: '8px',
paddingRight: '4px', paddingRight: '4px',
minHeight: '300px', minHeight: '300px',
}} }}
> >
{filteredUnseatedStudents.length === 0 ? ( {filteredUnseatedStudents.length === 0 ? (
<div style={{ textAlign: 'center', padding: '2rem 0', color: 'var(--text-muted)', fontSize: '0.8rem' }}> <div style={{ textAlign: 'center', padding: '2rem 0', color: 'var(--text-muted)', fontSize: '0.8rem' }}>
Không tìm thấy sinh viên nào. Không tìm thấy sinh viên nào.
</div> </div>
) : ( ) : (
filteredUnseatedStudents.map((s) => { filteredUnseatedStudents.map((s) => {
const isOnline = onlineIds.includes(s.rkId); const isOnline = onlineIds.includes(s.rkId);
return ( return (
<div <div
key={s.rkId} key={s.rkId}
draggable draggable
onDragStart={(e) => handleDragStart(e, `unseated:${s.rkId}`)} onDragStart={(e) => handleDragStart(e, `unseated:${s.rkId}`)}
onClick={() => onStudentClick && onStudentClick(s.rkId)} onClick={() => onStudentClick && onStudentClick(s.rkId)}
style={{ style={{
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: '0.5rem', gap: '0.5rem',
padding: '0.5rem', padding: '0.5rem',
border: '1px solid var(--border-color)', border: '1px solid var(--border-color)',
borderRadius: '8px', borderRadius: '8px',
backgroundColor: '#ffffff', backgroundColor: '#ffffff',
cursor: onStudentClick ? 'pointer' : 'grab', cursor: onStudentClick ? 'pointer' : 'grab',
transition: 'var(--transition)', transition: 'var(--transition)',
userSelect: 'none', userSelect: 'none',
boxShadow: 'var(--shadow-sm)', boxShadow: 'var(--shadow-sm)',
}} }}
className="student-drag-item" className="student-drag-item"
> >
<StudentAvatar fullName={s.fullName} avatar={s.avatar} isOnline={isOnline} size={28} /> <StudentAvatar fullName={s.fullName} avatar={s.avatar} isOnline={isOnline} size={28} />
<div style={{ display: 'flex', flexDirection: 'column', overflow: 'hidden', flex: 1 }}> <div style={{ display: 'flex', flexDirection: 'column', overflow: 'hidden', flex: 1 }}>
<span style={{ fontSize: '0.8rem', fontWeight: 600, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}> <span style={{ fontSize: '0.8rem', fontWeight: 600, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{s.fullName} {s.fullName}
</span> </span>
<span style={{ fontSize: '0.7rem', color: 'var(--text-secondary)', fontFamily: 'monospace' }}> <span style={{ fontSize: '0.7rem', color: 'var(--text-secondary)', fontFamily: 'monospace' }}>
{s.studentCode} {s.studentCode}
</span> </span>
</div>
<span className={isOnline ? 'pulse-dot-online' : 'status-badge-offline'} style={{ width: '6px', height: '6px' }} />
</div> </div>
<span className={isOnline ? 'pulse-dot-online' : 'status-badge-offline'} style={{ width: '6px', height: '6px' }} /> );
</div> })
); )}
}) </div>
)}
</div> </div>
</div> )}
</div> </div>
)} )}