/* AAU CRM — Giảng viên · hồ sơ + chỉnh sửa phí/buổi + LỊCH THANH TOÁN giảng viên Thù lao tính từ số buổi giảng viên thực dạy × phí/buổi (lấy từ lịch lớp). */ const INS_GROUP_C = id => (AAU.courseGroupById ? (AAU.courseGroupById(id) || {}).color : '#616161') || '#616161'; // các đợt thanh toán cho 1 lớp: tạm ứng 30% (khai giảng) · giữa khóa 40% · kết thúc 30% function insClassPayments(insId) { const fee = (AAU.instructorById(insId) || {}).feePerSession || 0; return AAU.classes .filter(c => c.sessions.some(s => s.instructor === insId)) .map(c => { const taught = c.sessions.filter(s => s.instructor === insId).length; const total = fee * taught; const mid = c.sessions[Math.floor((c.sessions.length - 1) / 2)] || c.sessions[0]; const last = c.sessions[c.sessions.length - 1]; const a1 = Math.round(total * 0.3), a2 = Math.round(total * 0.4), a3 = total - a1 - a2; const milestones = [ { key: c.id + '-0', name: 'Tạm ứng 30%', due: c.start, amount: a1 }, { key: c.id + '-1', name: 'Giữa khóa 40%', due: (mid || {}).date || c.start, amount: a2 }, { key: c.id + '-2', name: 'Kết thúc 30%', due: (last || {}).date || c.start, amount: a3 }, ]; return { cls: c, fee, taught, total, milestones }; }); } function insDueState(due) { const [y, m, d] = due.split('-').map(Number); const days = Math.round((new Date(y, m - 1, d) - AAU.TODAY) / 86400000); if (days < 0) return 'paid'; // quá khứ → coi như đã chi (seed) if (days <= 10) return 'due'; return 'upcoming'; } function InstructorsPage() { const [tick, setTick] = useState(0); const [sel, setSel] = useState(null); // trạng thái thanh toán đã ghi nhận thủ công (ngoài seed theo ngày) const [paidSet, setPaidSet] = useState(() => new Set()); const vndM = AAU.fmtVNDm, vnd = AAU.fmtVND; const isPaid = (key, due) => paidSet.has(key) || (!paidSet.has('!' + key) && insDueState(due) === 'paid'); const markPaid = (key) => setPaidSet(s => { const n = new Set(s); n.add(key); n.delete('!' + key); return n; }); const markUnpaid = (key) => setPaidSet(s => { const n = new Set(s); n.delete(key); n.add('!' + key); return n; }); const data = AAU.instructors.map(ins => { const pays = insClassPayments(ins.id); const total = pays.reduce((s, p) => s + p.total, 0); const paid = pays.reduce((s, p) => s + p.milestones.filter(m => isPaid(m.key, m.due)).reduce((a, m) => a + m.amount, 0), 0); return { ins, pays, total, paid, due: total - paid, classes: pays.length }; }); const sumTotal = data.reduce((s, d) => s + d.total, 0); const sumPaid = data.reduce((s, d) => s + d.paid, 0); const sumDue = sumTotal - sumPaid; const selData = sel && data.find(d => d.ins.id === sel); function saveFee(insId, fee) { const ins = AAU.instructors.find(i => i.id === insId); if (ins) ins.feePerSession = Number(String(fee).replace(/\D/g, '')) || 0; setTick(t => t + 1); } return (
Thêm giảng viên} />
{data.map(({ ins, total, due, classes }) => (
setSel(ins.id)}>
{ins.name}
{ins.title}
{ins.rating}
{ins.groups.map(g => {(AAU.courseGroupById(g) || {}).label})}
Phí / buổi{vnd(ins.feePerSession)}
Lớp phụ trách{classes} lớp
Còn phải trả 0 ? 'sla-warn' : 'sla-ok')}>{due > 0 ? vndM(due) : 'Đã đủ'}
))}
{selData && ( setSel(null)} onSaveFee={saveFee} isPaid={isPaid} markPaid={markPaid} markUnpaid={markUnpaid} key={tick} /> )}
); function fmtRatio(a, b) { return b ? Math.round(a / b * 100) + '% tổng' : '—'; } } function InstructorDrawer({ d, onClose, onSaveFee, isPaid, markPaid, markUnpaid }) { const ins = d.ins; const [edit, setEdit] = useState(false); const [fee, setFee] = useState(ins.feePerSession); const vnd = AAU.fmtVND, vndM = AAU.fmtVNDm; const stMeta = { paid: { t: 'success', l: 'Đã chi' }, due: { t: 'warning', l: 'Đến hạn' }, upcoming: { t: 'neutral', l: 'Sắp tới' } }; return ( : <>}>
{ins.name}
{ins.title} · {ins.expertise}
{ins.rating}
{edit ? (
Phí/buổi là nguồn của dòng "Chi phí giảng viên" trong P&L từng khóa — đổi ở đây sẽ phản ánh vào báo cáo.
) : ( <>
Phí/buổi
{vndM(ins.feePerSession)}
Tổng thù lao
{vndM(d.total)}
Còn phải trả
0 ? '#b06f00' : '#0e7c4a' }}>{vndM(d.due)}
LỊCH THANH TOÁN THEO LỚP
{d.pays.map(p => (
{p.cls.courseName}
{p.cls.batch} · {p.taught} buổi × {vndM(p.fee)} · KG {AAU.fmtDate(p.cls.start)}
{vndM(p.total)}
{p.milestones.map(m => { const paid = isPaid(m.key, m.due); const st = paid ? 'paid' : insDueState(m.due); const meta = stMeta[st]; return (
{m.name}
Hạn {AAU.fmtDate(m.due)}
{vnd(m.amount)} {meta.l} {paid ? : }
); })}
))} {d.pays.length === 0 && }
)}
); } window.InstructorsPage = InstructorsPage;