/* AAU CRM — Operations: Khóa học · Lịch khai giảng (lớp + lịch tháng) · chỉnh sửa + đồng bộ aau.vn */ const GROUP_C = id => (AAU.courseGroupById(id) || {}).color || '#616161'; const fillColor = t => (t === 'ok' ? '#0e7c4a' : t === 'warn' ? '#d97706' : '#c4320a'); const DOW_VN = ['CN', 'T2', 'T3', 'T4', 'T5', 'T6', 'T7']; const PATTERNS = [ { pat: ['2', '4', '6'], label: 'Thứ 2 · 4 · 6' }, { pat: ['3', '5', '7'], label: 'Thứ 3 · 5 · 7' }, { pat: ['6', '7', 'CN'], label: 'Thứ 6 · 7 · CN' }, { pat: ['7', 'CN'], label: 'Thứ 7 · CN' }, { pat: ['2', '4'], label: 'Thứ 2 · 4' }, { pat: ['3', '5'], label: 'Thứ 3 · 5' }, { pat: ['2', '3', '4', '5', '6'], label: 'Thứ 2 → 6' }, ]; function useOps() { const [, force] = React.useReducer(x => x + 1, 0); React.useEffect(() => OpsStore.subscribe(force), []); return OpsStore; } function classFill(c) { const fill = Math.round(c.enrolled / c.cap * 100); const slots = Math.max(0, c.cap - c.enrolled); const days = AAU.daysToStart(c); const tone = fill >= 80 ? 'ok' : fill >= 50 ? 'warn' : 'bad'; const almostFull = slots <= 2; const lowFill = c.status === 'enrolling' && fill < 50 && days <= 22; return { fill, slots, days, tone, almostFull, lowFill }; } function fmtFieldVal(k, v) { if (v == null || v === '') return '—'; if (k === 'price') return AAU.fmtVND(v); if (k === 'start') return AAU.fmtDate(v); if (k === 'status') return v === 'active' ? 'Đang mở' : 'Sắp ra mắt'; if (k === 'group') return (AAU.courseGroupById(v) || {}).label || v; if (k === 'brochure') return 'link Drive'; if (k === 'desc') return String(v).slice(0, 40) + '…'; return String(v); } function GroupTag({ id }) { const g = AAU.courseGroupById(id); if (!g) return null; return {g.label}; } function ClassStatusBadge({ c }) { if (c.status === 'in_progress') return Đang học · buổi {c.doneCount}/{c.sessions.length}; return Đang tuyển; } function FillBar({ c, h = 6 }) { const { fill, tone } = classFill(c); return
; } function WebChip() { return Đã đăng web; } function DiffChip() { return Lệch web; } function InsAvatar({ id, size = 24 }) { const i = AAU.instructorById(id); if (!i) return null; return ; } /* ============================= KHÓA HỌC ============================= */ function CourseCatalog() { useOps(); const [grp, setGrp] = useState('all'); const [view, setView] = useState(() => localStorage.getItem('aau_course_view') || 'grid'); const [course, setCourse] = useState(null); const [sync, setSync] = useState(false); const setV = v => { setView(v); localStorage.setItem('aau_course_view', v); }; const courses = AAU.courses.filter(c => grp === 'all' || c.group === grp); const diffIds = new Set(OpsStore.courseDiffs().map(d => d.id)); const courseObj = course && AAU.courseById(course.id); const totalStudents = AAU.classes.reduce((s, c) => s + c.enrolled, 0); const openClasses = AAU.classes.filter(c => c.status === 'enrolling').length; const activeCourses = AAU.courses.filter(c => c.status === 'active').length; const totalCourses = AAU.courses.length; const soonCourses = AAU.courses.filter(c => c.status === 'coming_soon').length; return (
} />
s + Math.max(0, c.cap - c.enrolled), 0)} sub="tất cả lớp đang mở" icon="target" />
({ value: g.id, label: g.label + ' (' + AAU.courses.filter(c => c.group === g.id).length + ')' }))]} />
{view === 'grid' ?
{courses.map(c => setCourse(c)} />)}
: } {courseObj && setCourse(null)} />} {sync && setSync(false)} />}
); } function CourseCard({ c, diff, onOpen }) { const cls = AAU.classesByCourse(c.id); const soon = c.status === 'coming_soon'; return (
{diff && }{soon ? Sắp ra mắt : Đang mở}
{c.name}
{c.en}
{c.desc}
{c.price ? AAU.fmtVNDm(c.price) : 'Đang cập nhật'} {c.sessions} buổi · {c.hours}h
{soon ?
Mở đăng ký nhận tin khi ra mắt
: cls.length ?
{cls.length} LỚP ĐANG KHAI GIẢNG
{cls.slice(0, 2).map(k => { const f = classFill(k); return (
{k.batch}KG {AAU.fmtDate(k.start)}
{k.enrolled}/{k.cap} HVcòn {f.slots} chỗ
); })}
:
Chưa có lịch khai giảng
} ); } function CourseList({ courses, diffIds, onOpen }) { return ( {courses.map(c => { const cls = AAU.classesByCourse(c.id); const soon = c.status === 'coming_soon'; const hv = cls.reduce((s, k) => s + k.enrolled, 0), cap = cls.reduce((s, k) => s + k.cap, 0); return ( onOpen(c)} style={{ borderTop: '1px solid var(--p-border-secondary)', fontSize: 13 }}> ); })}
Khóa họcNhómBuổiHọc phíLớp đang mởHV / Chỗ
{c.name}{diffIds.has(c.id) && }
{c.code} · {c.en}
{c.sessions} buổi · {c.hours}h {c.price ? AAU.fmtVND(c.price) : '—'} {soon ? Sắp ra mắt : cls.length ? cls.map(k => k.batch).join(', ') : } {cap ? {hv} : '—'}{cap ? / {cap} : ''}
); } function CourseEditForm({ c, onDone }) { const [f, setF] = useState({ name: c.name, en: c.en || '', code: c.code, group: c.group, sessions: c.sessions, hours: c.hours, price: c.price || 0, status: c.status, targetStudents: c.targetStudents, desc: c.desc }); const set = (k, v) => setF(s => ({ ...s, [k]: v })); const save = () => { OpsStore.updateCourse(c.id, { name: f.name, en: f.en, code: f.code, group: f.group, sessions: +f.sessions, hours: +f.hours, price: +f.price, status: f.status, targetStudents: +f.targetStudents, desc: f.desc }); onDone(); }; return (
set('name', v)} /> set('en', v)} />
set('code', v)} /> set('sessions', v)} /> set('hours', v)} /> set('price', v)} /> set('targetStudents', v)} />