Files
rikkei_simple_care/management/src/components/SeatingTemplatesSection.tsx
PhuocNTB 39dcb37d51
Some checks failed
Deploy on Master Change / deploy (push) Failing after 39s
test so do lop
2026-07-03 22:07:31 +07:00

356 lines
14 KiB
TypeScript

import React, { useEffect, useState } from 'react';
export interface SeatingTemplate {
id: string;
name: string;
rows: number;
cols: number;
boardPosition: 'top' | 'bottom' | 'left' | 'right';
lockedSeats: string[]; // e.g. ["0,0", "1,2"]
}
const DEFAULT_TEMPLATES: SeatingTemplate[] = [
{
id: 'tpl-standard-30',
name: 'Phòng Lab Standard (30 máy)',
rows: 5,
cols: 6,
boardPosition: 'top',
lockedSeats: [],
},
{
id: 'tpl-aisle-large',
name: 'Phòng Máy A1 (Có lối đi giữa)',
rows: 6,
cols: 8,
boardPosition: 'top',
lockedSeats: ['0,3', '1,3', '2,3', '3,3', '4,3', '5,3'],
},
];
export const SeatingTemplatesSection: React.FC = () => {
const [templates, setTemplates] = useState<SeatingTemplate[]>([]);
const [editing, setEditing] = useState<SeatingTemplate | null>(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<string[]>([]);
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(
<div
key={key}
onClick={() => 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}`}
</div>
);
}
}
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem', margin: '1rem 0' }}>
<div style={{ display: 'flex', justifyContent: 'center' }}>
{boardPosition === 'top' && (
<div className="seating-board-label" style={{ width: '60%', textAlign: 'center', background: '#374151', color: '#fff', padding: '0.25rem 0.5rem', borderRadius: '4px', fontSize: '0.8rem', fontWeight: 700 }}>
📢 BẢNG / GIẢNG ĐƯNG (TOP)
</div>
)}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', justifyContent: 'center' }}>
{boardPosition === 'left' && (
<div className="seating-board-label" style={{ writingMode: 'vertical-lr', textOrientation: 'mixed', textAlign: 'center', background: '#374151', color: '#fff', padding: '0.5rem 0.25rem', borderRadius: '4px', fontSize: '0.8rem', fontWeight: 700, minHeight: '100px' }}>
📢 BẢNG (LEFT)
</div>
)}
<div
style={{
display: 'grid',
gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))`,
gap: '6px',
flex: 1,
maxHeight: '320px',
overflowY: 'auto',
padding: '4px',
}}
>
{gridItems}
</div>
{boardPosition === 'right' && (
<div className="seating-board-label" style={{ writingMode: 'vertical-lr', textOrientation: 'mixed', textAlign: 'center', background: '#374151', color: '#fff', padding: '0.5rem 0.25rem', borderRadius: '4px', fontSize: '0.8rem', fontWeight: 700, minHeight: '100px' }}>
📢 BẢNG (RIGHT)
</div>
)}
</div>
<div style={{ display: 'flex', justifyContent: 'center' }}>
{boardPosition === 'bottom' && (
<div className="seating-board-label" style={{ width: '60%', textAlign: 'center', background: '#374151', color: '#fff', padding: '0.25rem 0.5rem', borderRadius: '4px', fontSize: '0.8rem', fontWeight: 700 }}>
📢 BẢNG / GIẢNG ĐƯNG (BOTTOM)
</div>
)}
</div>
</div>
);
};
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<h3 style={{ margin: 0, fontSize: '1.1rem', fontWeight: 700 }}>Danh sách mẫu đ chỗ ngồi</h3>
<p style={{ margin: '4px 0 0 0', color: 'var(--text-secondary)', fontSize: '0.82rem' }}>
Thiết lập sẵn cấu trúc hàng, cột, bảng 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.
</p>
</div>
{!creating && !editing && (
<button className="btn btn-primary" onClick={handleOpenCreate}>
+ Thêm đ mẫu
</button>
)}
</div>
{(creating || editing) ? (
<div className="content-card" style={{ padding: '1.5rem', display: 'grid', gridTemplateColumns: '1fr 1.5fr', gap: '2rem' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', borderRight: '1px solid var(--border-color)', paddingRight: '2rem' }}>
<h4 style={{ margin: 0, fontSize: '0.95rem' }}>{creating ? 'Thêm sơ đồ mẫu mới' : 'Chỉnh sửa sơ đồ mẫu'}</h4>
{error && <div style={{ color: 'var(--danger)', fontSize: '0.82rem', fontWeight: 600 }}>{error}</div>}
<label className="checkbox-label" style={{ display: 'flex', flexDirection: 'column', gap: '4px', alignItems: 'flex-start' }}>
<span style={{ fontSize: '0.82rem', fontWeight: 600 }}>Tên đ</span>
<input
type="text"
className="search-input"
style={{ width: '100%' }}
placeholder="Ví dụ: Phòng Lab 302, Phòng A2"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</label>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' }}>
<label className="checkbox-label" style={{ display: 'flex', flexDirection: 'column', gap: '4px', alignItems: 'flex-start' }}>
<span style={{ fontSize: '0.82rem', fontWeight: 600 }}>Số hàng (dọc)</span>
<input
type="number"
min={1}
max={10}
className="search-input"
style={{ width: '100%' }}
value={rows}
onChange={(e) => setRows(Math.max(1, Math.min(10, Number(e.target.value))))}
/>
</label>
<label className="checkbox-label" style={{ display: 'flex', flexDirection: 'column', gap: '4px', alignItems: 'flex-start' }}>
<span style={{ fontSize: '0.82rem', fontWeight: 600 }}>Số cột (ngang)</span>
<input
type="number"
min={1}
max={15}
className="search-input"
style={{ width: '100%' }}
value={cols}
onChange={(e) => setCols(Math.max(1, Math.min(15, Number(e.target.value))))}
/>
</label>
</div>
<label className="checkbox-label" style={{ display: 'flex', flexDirection: 'column', gap: '4px', alignItems: 'flex-start' }}>
<span style={{ fontSize: '0.82rem', fontWeight: 600 }}>Vị trí bảng viết</span>
<select
className="select-filter"
style={{ width: '100%', padding: '0.5rem' }}
value={boardPosition}
onChange={(e) => setBoardPosition(e.target.value as any)}
>
<option value="top">Phía trước (Top)</option>
<option value="bottom">Phía sau (Bottom)</option>
<option value="left">Bên trái (Left)</option>
<option value="right">Bên phải (Right)</option>
</select>
</label>
<div style={{ display: 'flex', gap: '10px', marginTop: '1rem' }}>
<button className="btn btn-primary" style={{ flex: 1 }} onClick={handleSave}>
Lưu lại
</button>
<button className="btn btn-secondary" style={{ flex: 1 }} onClick={handleCloseForm}>
Hủy bỏ
</button>
</div>
</div>
<div>
<h4 style={{ margin: '0 0 4px 0', fontSize: '0.95rem' }}>Thiết kế vị trí khóa</h4>
<p style={{ margin: '0 0 1rem 0', color: 'var(--text-secondary)', fontSize: '0.78rem' }}>
Click chuột vào các ô bên dưới đ chuyển đi trạng thái giữa <strong>Chỗ trống khả dụng</strong> <strong>Vị trí khóa (Không thể xếp sinh viên)</strong>.
</p>
{renderDesignerGrid()}
</div>
</div>
) : (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: '1rem' }}>
{templates.map((tpl) => (
<div key={tpl.id} className="content-card" style={{ padding: '1.25rem', display: 'flex', flexDirection: 'column', gap: '0.75rem', position: 'relative' }}>
<div>
<h4 style={{ margin: 0, fontSize: '0.95rem', fontWeight: 700 }}>{tpl.name}</h4>
<div style={{ fontSize: '0.8rem', color: 'var(--text-secondary)', marginTop: '4px' }}>
Kích thước: <strong>{tpl.rows} hàng &times; {tpl.cols} cột</strong> ({tpl.rows * tpl.cols} vị trí)
</div>
<div style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>
Bảng : <strong>{tpl.boardPosition.toUpperCase()}</strong>
</div>
<div style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>
Vị trí khóa: <strong>{tpl.lockedSeats ? tpl.lockedSeats.length : 0} ô</strong>
</div>
</div>
<div style={{ display: 'flex', gap: '8px', marginTop: 'auto' }}>
<button className="btn btn-secondary btn-sm" style={{ flex: 1 }} onClick={() => handleOpenEdit(tpl)}>
Chỉnh sửa
</button>
<button
className="btn btn-secondary btn-sm text-danger"
style={{ flex: 1, borderColor: '#fca5a5', color: 'var(--danger)' }}
onClick={() => handleDelete(tpl)}
>
Xóa
</button>
</div>
</div>
))}
</div>
)}
</div>
);
};