/* AAU CRM — Ghi danh (enrollment) · tạo ghi danh + ghi nhận thanh toán theo đợt (chạy thật, lưu trong phiên) */ const ENR_ISO = (d) => d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0'); const ENR_ADD_DAYS = (iso, n) => { const [y, m, d] = iso.split('-').map(Number); return ENR_ISO(new Date(y, m - 1, d + n)); }; function EnrollmentPage() { const [enr, setEnr] = useState(() => AAU.enrollments.map(e => ({ ...e }))); const [plans, setPlans] = useState(() => JSON.parse(JSON.stringify(AAU.paymentPlans))); const [planId, setPlanId] = useState(null); // enrollment đang mở drawer const [add, setAdd] = useState(false); const recompute = (e, plan) => { const paid = plan ? plan.filter(p => p.status === 'paid').reduce((s, p) => s + p.amount, 0) : e.paid; const hasOverdue = plan && plan.some(p => p.status === 'overdue'); const payStatus = paid >= e.fee ? 'paid' : hasOverdue ? 'overdue' : 'partial'; return { ...e, paid, payStatus }; }; function recordPay(enrId, idx) { setPlans(prev => { const np = { ...prev }; const plan = np[enrId].map((p, i) => i === idx ? { ...p, status: 'paid' } : p); np[enrId] = plan; setEnr(es => es.map(e => e.id === enrId ? recompute(e, plan) : e)); return np; }); } function addEnrollment(data) { const id = 'en' + Date.now(); const deposit = Math.min(data.deposit, data.fee); const today = ENR_ISO(AAU.TODAY); const plan = [ { name: 'Cọc giữ chỗ', amount: deposit, due: today, status: deposit > 0 ? 'paid' : 'due' }, { name: 'Học phí còn lại', amount: data.fee - deposit, due: ENR_ADD_DAYS(today, 14), status: 'due' }, ].filter(p => p.amount > 0); const e = recompute({ id, name: data.name, company: data.company, course: data.course, classId: data.classId, date: today, fee: data.fee, paid: 0, payStatus: 'partial' }, plan); setPlans(p => ({ ...p, [id]: plan })); setEnr(es => [e, ...es]); if (window.API && window.API.enabled) window.API.post('/enrollments', e).catch(() => {}); setAdd(false); } const rows = enr.map(e => { const k = AAU.classById(e.classId); return { ...e, who: e.leadId ? AAU.leadById(e.leadId)?.name : e.name, courseName: AAU.courseById(e.course)?.name, cls: k, debt: e.fee - e.paid }; }); const totalPaid = rows.reduce((s, e) => s + e.paid, 0); const totalDebt = rows.reduce((s, e) => s + e.debt, 0); const overdue = rows.filter(e => e.payStatus === 'overdue').length; const payTone = { paid: 'success', partial: 'warning', overdue: 'critical' }; const payLabel = { paid: 'Đã đủ', partial: 'Trả 1 phần', overdue: 'Quá hạn' }; const cols = [ { key: 'who', label: 'Học viên', render: e =>
{e.who}
{e.leadId ? { ev.stopPropagation(); navigate('/leads/' + e.leadId); }}>Hồ sơ CRM ↗ : e.company || '—'}
, csv: e => e.who }, { key: 'courseName', label: 'Khóa học', render: e => {e.courseName} }, { key: 'cls', label: 'Lớp · Khai giảng', render: e => e.cls ?
{e.cls.batch}
{AAU.fmtDate(e.cls.start)} · {e.cls.patLabel}
: '—', csv: e => e.cls?.batch }, { key: 'date', label: 'Ngày ghi danh', render: e => AAU.fmtDate(e.date) }, { key: 'fee', label: 'Học phí', num: true, render: e => AAU.fmtVND(e.fee) }, { key: 'paid', label: 'Đã thu', num: true, render: e => (
{AAU.fmtVNDm(e.paid)}{e.debt > 0 && nợ {AAU.fmtVNDm(e.debt)}}
), csv: e => e.paid }, { key: 'payStatus', label: 'Thanh toán', render: e => {payLabel[e.payStatus]} }, { key: 'act', label: '', sortable: false, render: e => plans[e.id] ? : Xong }, ]; const cur = planId && rows.find(e => e.id === planId); return (
} />
s + c.enrolled, 0)} sub={AAU.classes.length + ' lớp đang mở'} icon="graduation" />
{overdue > 0 &&
{overdue} đợt thanh toán quá hạn — tự động nhắc kế toán qua Email/ZNS. Mở “Đợt TT” để ghi nhận thu.
} e.id} searchKeys={['who', 'courseName']} onRowClick={e => plans[e.id] && setPlanId(e.id)} exportName="enrollments" pageSize={10} /> {cur && ( setPlanId(null)} footer={<>{cur.debt > 0 ? : Đã thu đủ}}>
{cur.who}
{cur.courseName} · {cur.cls?.batch} · {AAU.fmtVND(cur.fee)}
CÁC ĐỢT
{plans[cur.id].map((p, i) => (
{p.name}
Hạn {AAU.fmtDate(p.due)}
{AAU.fmtVND(p.amount)}
{p.status === 'paid' ? 'Đã thu' : p.status === 'overdue' ? 'Quá hạn' : 'Đến hạn'}
{p.status !== 'paid' && }
))}
)} {add && setAdd(false)} onSave={addEnrollment} />}
); } function EnrollNewModal({ onClose, onSave }) { const courses = AAU.courses.filter(c => c.price); const [name, setName] = useState(''); const [company, setCompany] = useState(''); const [course, setCourse] = useState(courses[0].id); const classesOf = AAU.classes.filter(c => c.course === course); const [classId, setClassId] = useState(classesOf[0]?.id || ''); const [fee, setFee] = useState(AAU.courseById(course).price); const [deposit, setDeposit] = useState(''); const num = (v) => Number(String(v).replace(/\D/g, '')) || 0; function pickCourse(cid) { setCourse(cid); const cls = AAU.classes.filter(c => c.course === cid); setClassId(cls[0]?.id || ''); setFee(AAU.courseById(cid).price); } return ( }>
({ value: c.id, label: c.batch + ' · KG ' + AAU.fmtDate(c.start) }))} /> :
Khóa này chưa có lớp mở — tạo lớp ở "Lịch khai giảng".
}
Hệ thống tạo sẵn 2 đợt: cọc giữ chỗ (thu ngay) + học phí còn lại (hạn sau 14 ngày). Doanh thu này tự chảy vào báo cáo Tài chính.
); } Object.assign(window, { EnrollmentPage });