This commit is contained in:
@@ -148,26 +148,48 @@ export const ExamsTab: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{showCreate && (
|
||||
<div className="system-form-card exams-create-card">
|
||||
<h2 className="system-form-title">Phòng thi mới</h2>
|
||||
<div className="system-form-grid">
|
||||
<label className="login-field system-form-full">
|
||||
<span>Tên phòng thi</span>
|
||||
<input value={name} onChange={(e) => setName(e.target.value)} placeholder="VD: Thi cuối kỳ Java" />
|
||||
<div className="modal-overlay" onClick={() => setShowCreate(false)}>
|
||||
<div className="modal-container" style={{ maxWidth: '500px' }} onClick={e => e.stopPropagation()}>
|
||||
<div className="modal-header">
|
||||
<h2 className="modal-title">Tạo phòng thi mới</h2>
|
||||
<button className="modal-close-btn" onClick={() => setShowCreate(false)} aria-label="Đóng">×</button>
|
||||
</div>
|
||||
<div className="modal-body" style={{ padding: '1.5rem', display: 'flex', flexDirection: 'column', gap: '1.25rem' }}>
|
||||
<label className="login-field" style={{ display: 'flex', flexDirection: 'column', gap: '0.35rem' }}>
|
||||
<span style={{ fontWeight: 600, fontSize: '0.85rem', color: 'var(--text-secondary)' }}>Tên phòng thi</span>
|
||||
<input
|
||||
className="search-input"
|
||||
style={{ width: '100%', boxSizing: 'border-box' }}
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="VD: Thi cuối kỳ Java"
|
||||
/>
|
||||
</label>
|
||||
<label className="login-field">
|
||||
<span>Bắt đầu</span>
|
||||
<input type="datetime-local" value={start} onChange={(e) => setStart(e.target.value)} />
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
|
||||
<label className="login-field" style={{ display: 'flex', flexDirection: 'column', gap: '0.35rem' }}>
|
||||
<span style={{ fontWeight: 600, fontSize: '0.85rem', color: 'var(--text-secondary)' }}>Bắt đầu</span>
|
||||
<input
|
||||
type="datetime-local"
|
||||
className="search-input"
|
||||
style={{ width: '100%', boxSizing: 'border-box', padding: '0.5rem 0.75rem' }}
|
||||
value={start}
|
||||
onChange={(e) => setStart(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label className="login-field">
|
||||
<span>Kết thúc</span>
|
||||
<input type="datetime-local" value={end} onChange={(e) => setEnd(e.target.value)} />
|
||||
<label className="login-field" style={{ display: 'flex', flexDirection: 'column', gap: '0.35rem' }}>
|
||||
<span style={{ fontWeight: 600, fontSize: '0.85rem', color: 'var(--text-secondary)' }}>Kết thúc</span>
|
||||
<input
|
||||
type="datetime-local"
|
||||
className="search-input"
|
||||
style={{ width: '100%', boxSizing: 'border-box', padding: '0.5rem 0.75rem' }}
|
||||
value={end}
|
||||
onChange={(e) => setEnd(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="system-form-actions">
|
||||
<div />
|
||||
<div className="system-form-actions-right">
|
||||
<button type="button" className="btn btn-ghost" onClick={() => setShowCreate(false)}>
|
||||
</div>
|
||||
<div className="modal-footer" style={{ gap: '0.65rem' }}>
|
||||
<button type="button" className="btn btn-secondary" onClick={() => setShowCreate(false)}>
|
||||
Hủy
|
||||
</button>
|
||||
<button type="button" className="btn btn-primary" disabled={busy} onClick={create}>
|
||||
|
||||
@@ -17,6 +17,9 @@ import type {
|
||||
ScheduleConflictItem,
|
||||
} from '../api';
|
||||
import { LiveProctorModal } from './LiveProctorModal';
|
||||
import { AppPoolModal } from './AppPoolModal';
|
||||
import { AppTemplatePickerModal } from './AppTemplatePickerModal';
|
||||
import { mergeKeywordCSV } from '../utils/appKeywords';
|
||||
import { openClass } from './NavHistoryBar';
|
||||
import { ScheduleEditor } from './ScheduleEditor';
|
||||
import { LearningTimetable } from './LearningTimetable';
|
||||
@@ -53,6 +56,23 @@ export const LearningTab: React.FC = () => {
|
||||
const [proctorStudent, setProctorStudent] = useState<{ id: number; name: string; code: string } | null>(null);
|
||||
const [conflictWarning, setConflictWarning] = useState<{ cls: ClassItem; conflicts: ScheduleConflictItem[] } | null>(null);
|
||||
|
||||
const [appPoolOpen, setAppPoolOpen] = useState(false);
|
||||
const [templatePickerOpen, setTemplatePickerOpen] = useState(false);
|
||||
|
||||
const addAppKeyword = (kw: string) => {
|
||||
const trimmed = kw.trim().toLowerCase();
|
||||
if (!trimmed) return;
|
||||
setAllowedApps(prev => {
|
||||
const parts = prev.split(',').map(s => s.trim().toLowerCase()).filter(Boolean);
|
||||
if (parts.includes(trimmed)) return prev;
|
||||
return prev ? `${prev}, ${trimmed}` : trimmed;
|
||||
});
|
||||
};
|
||||
|
||||
const applyTemplate = (keywords: string) => {
|
||||
setAllowedApps((prev) => mergeKeywordCSV(prev, keywords));
|
||||
};
|
||||
|
||||
const loadActiveClasses = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -451,11 +471,30 @@ export const LearningTab: React.FC = () => {
|
||||
backgroundColor: '#fff',
|
||||
outline: 'none',
|
||||
transition: 'var(--transition)',
|
||||
resize: 'vertical'
|
||||
resize: 'vertical',
|
||||
boxSizing: 'border-box'
|
||||
}}
|
||||
onFocus={(e) => e.target.style.borderColor = 'var(--accent)'}
|
||||
onBlur={(e) => e.target.style.borderColor = 'var(--border-color)'}
|
||||
/>
|
||||
<div style={{ display: 'flex', gap: '8px', marginTop: '10px' }}>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-sm"
|
||||
style={{ flex: 1 }}
|
||||
onClick={() => setTemplatePickerOpen(true)}
|
||||
>
|
||||
Chọn khung
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-sm"
|
||||
style={{ flex: 1 }}
|
||||
onClick={() => setAppPoolOpen(true)}
|
||||
>
|
||||
Mở kho
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button className="btn btn-primary" onClick={handleSaveApps} style={{ width: '100%', marginTop: '15px' }}>
|
||||
Lưu whitelist app
|
||||
@@ -674,6 +713,19 @@ export const LearningTab: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<AppPoolModal
|
||||
open={appPoolOpen}
|
||||
onClose={() => setAppPoolOpen(false)}
|
||||
onSelect={addAppKeyword}
|
||||
allowedApps={allowedApps}
|
||||
/>
|
||||
<AppTemplatePickerModal
|
||||
open={templatePickerOpen}
|
||||
onClose={() => setTemplatePickerOpen(false)}
|
||||
onSelect={applyTemplate}
|
||||
allowedApps={allowedApps}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -111,6 +111,40 @@ export const ScheduleEditor: React.FC<ScheduleEditorProps> = ({ classId, schedul
|
||||
}
|
||||
};
|
||||
|
||||
const handleQuickCreateLocal = () => {
|
||||
const nextLocal = [...local];
|
||||
const standardTimes = [
|
||||
{ period: 1, start: '07:00', end: '09:00' },
|
||||
{ period: 2, start: '09:10', end: '11:10' },
|
||||
{ period: 3, start: '12:10', end: '14:10' },
|
||||
{ period: 4, start: '14:20', end: '16:20' },
|
||||
];
|
||||
let added = 0;
|
||||
for (let day = 0; day <= 4; day++) {
|
||||
for (const slot of standardTimes) {
|
||||
const exists = nextLocal.some(s => s.dayOfWeek === day && s.period === slot.period);
|
||||
if (!exists) {
|
||||
nextLocal.push({
|
||||
dayOfWeek: day,
|
||||
period: slot.period,
|
||||
startTime: slot.start,
|
||||
endTime: slot.end,
|
||||
courseId: 0,
|
||||
courseName: '',
|
||||
isActive: false, // status is tắt (false)
|
||||
});
|
||||
added++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (added === 0) {
|
||||
alert('Tất cả các ca học từ T2 đến T6 đã tồn tại.');
|
||||
return;
|
||||
}
|
||||
setLocal(nextLocal);
|
||||
alert(`Đã tạo nhanh ${added} ca học nháp (trạng thái Tắt) cho Thứ 2–6. Vui lòng chọn môn học và bấm "Lưu lịch học".`);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
const payload = local.filter(s => s.startTime?.trim() && s.endTime?.trim());
|
||||
try {
|
||||
@@ -127,7 +161,10 @@ export const ScheduleEditor: React.FC<ScheduleEditorProps> = ({ classId, schedul
|
||||
|
||||
return (
|
||||
<div className="schedule-editor">
|
||||
<div className="schedule-editor-toolbar">
|
||||
<div className="schedule-editor-toolbar" style={{ display: 'flex', gap: '8px', marginBottom: '10px' }}>
|
||||
<button type="button" className="btn btn-secondary" onClick={handleQuickCreateLocal}>
|
||||
Tạo nhanh 4 ca T2–T6 (Tắt)
|
||||
</button>
|
||||
<button type="button" className="btn btn-secondary" onClick={handleApplyTemplate} disabled={applying}>
|
||||
{applying ? 'Đang áp dụng...' : 'Template 4 ca (T2–T6)'}
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user