/* 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 (
>}>
);
}
Object.assign(window, { EnrollmentPage });