/* AAU CRM — Executive Dashboard + Flow Map Rebuilt to fit the full system: Planning, Conversation Intel / Enablement and the QC Engine are now first-class pillars, not just the old Marketing→Inbox→Sales→Delivery→Finance line. */ // ---------- shared computed signals (single source for both screens) ---------- function useSystemPulse() { const A = AAU, PA = A.planning.periodActual, T = A.totals, P = A.pnl; // money / pace (live month-to-date vs target) const target = T.revenueTarget; // 600tr const actual = PA.revenue; // 318tr MTD const dE = PA.daysElapsed, dT = PA.daysTotal; // 18 / 30 const onPace = target * dE / dT; // nhịp cần đạt hôm nay const projected = actual / dE * dT; // dự phóng cuối kỳ const pace = { target, actual, dE, dT, onPace, projected, pacePct: Math.round(actual / onPace * 100), projPct: Math.round(projected / target * 100), remaining: Math.max(0, target - actual), daysLeft: dT - dE, perDay: Math.max(0, target - actual) / Math.max(1, dT - dE), vsLast: Math.round((projected / A.planning.lastMonth - 1) * 100), behind: actual < onPace, }; // finance const cogs = P.cogs.reduce((s, c) => s + c.amount, 0); const opex = P.opex.reduce((s, c) => s + c.amount, 0); const gross = P.revenue - cogs, ebit = gross - opex, net = ebit * (1 - P.taxRate); const margin = Math.round(net / P.revenue * 100); // delivery / fill const enrolling = A.classes.filter(c => c.status === 'enrolling'); const fillRate = enrolling.length ? Math.round(enrolling.reduce((s, c) => s + c.enrolled / c.cap, 0) / enrolling.length * 100) : 0; const atRisk = enrolling.filter(c => c.enrolled / c.cap < 0.55).length; // sales const winRate = Math.round(PA.won / PA.deals * 100); // deals → won const cac = T.spend / PA.won; // QC (rulesets aggregate) const qc = A.qcRulesets.reduce((a, r) => { a.analyzed += r.analyzed; a.pass += r.pass; a.fail += r.fail; a.w += r.avg * r.analyzed; return a; }, { analyzed: 0, pass: 0, fail: 0, w: 0 }); qc.avg = qc.w / qc.analyzed; qc.passRate = Math.round(qc.pass / qc.analyzed * 100); qc.pending = 6; // intel / enablement const intel = { coverage: 83, patterns: 27, gaps: (window.INTEL ? INTEL.patterns.filter(p => p.coverage === 'gap').length : 2), autoLinked: '19/27', }; return { A, PA, T, P, pace, net, margin, fillRate, atRisk, winRate, cac, qc, intel }; } // ---------- pace-to-target band (Planning pillar, forward looking) ---------- function PaceBand({ s }) { const p = s.pace, f = AAU.fmtVNDm; const actualPct = Math.min(100, p.actual / p.target * 100); const paceMarkPct = Math.min(100, p.onPace / p.target * 100); const fill = p.behind ? '#b06f00' : '#0e7c4a'; return (
Nhịp doanh thu · {s.PA.monthLabel}
{f(p.actual)} / mục tiêu {f(p.target)} {p.behind ? 'Chậm nhịp ' + (100 - p.pacePct) + '%' : 'Vượt nhịp ' + (p.pacePct - 100) + '%'}
{/* full-width pace bar */}
{Math.round(actualPct)}%
Đầu kỳ ▕ Nhịp cần đạt hôm nay · {f(p.onPace)} ({Math.round(paceMarkPct)}%) {f(p.target)}
{/* stats */}
= 100 ? 'ok' : 'warn'} /> = 0 ? '+' : '') + p.vsLast + '%'} tone={p.vsLast >= 0 ? 'ok' : 'warn'} />
); } function PaceStat({ k, v, tone }) { const c = tone === 'ok' ? '#0e7c4a' : tone === 'warn' ? '#b06f00' : 'var(--p-text)'; return
{k}
{v}
; } // ---------- per-pillar health card ---------- function PillarCard({ icon, color, name, metric, sub, tone, path }) { return (
navigate(path)} className="pillar-card" style={{ cursor: 'pointer', background: '#fff', border: '1px solid var(--p-border)', borderRadius: 12, padding: 14, boxShadow: 'var(--p-shadow-card)' }}>
{name} {tone && }
{metric}
{sub}
); } function ExecDashboard() { const per = usePeriod('month'); const sc = per.scale, cmp = per.compareOn; const s = useSystemPulse(); const T = s.T, P = s.P; const sv = v => Math.round(v * sc); const kind = per.cur.kind.toLowerCase(); const cogs = P.cogs.reduce((a, c) => a + c.amount, 0); const opex = P.opex.reduce((a, c) => a + c.amount, 0); const gross = P.revenue - cogs, ebit = gross - opex, net = ebit * (1 - P.taxRate); const revTrend = [ { l: 'T12', v: 410 }, { l: 'T1', v: 388 }, { l: 'T2', v: 432 }, { l: 'T3', v: 498 }, { l: 'T4', v: 558 }, { l: 'T5', v: 642 }, ].map(m => ({ ...m, v: Math.round(m.v * sc) })); // alerts now span pillars: SLA (sales), QC (quality), delivery, intel (enablement), finance const alerts = [ { tone: 'critical', icon: 'fire', t: '2 lead hot quá hạn SLA', d: 'Anh Khoa (Khoa BBQ) còn 3h ở Đàm phán · Anh Phong quá hạn TVCS', act: () => navigate('/leads/kanban') }, { tone: 'critical', icon: 'shield', t: '1 hội thoại vi phạm nghiêm trọng', d: 'QC bắt cam kết sai chính sách "đảm bảo tăng x2 doanh thu" — cần manager duyệt', act: () => navigate('/qc/review') }, { tone: 'warning', icon: 'alert', t: 'Lớp Nhượng quyền B04 nguy cơ ế', d: 'Mới 6/15 chỗ, khai giảng 22/06 — cần đẩy marketing', act: () => navigate('/classes') }, { tone: 'warning', icon: 'sparkles', t: s.intel.gaps + ' pattern chưa có kịch bản', d: '"Chưa cần ngay" & "Đòi hoàn cọc" — win-rate thấp, Intel đề xuất tạo script', act: () => navigate('/enablement/intelligence') }, { tone: 'warning', icon: 'dollar', t: 'Công nợ quá hạn 6tr', d: 'Mai Tú (Tú Tea) — đợt cuối quá hạn 30/05', act: () => navigate('/enrollments') }, ]; return (
} /> {/* ① Forward pace vs target (Planning) */} {/* ② Money & throughput KPIs */}
{/* ③ Pillar health — one signal from every part of the system */}
{/* ④ Trend + cross-pillar alerts */}
+15% MoM}> Math.round(v)} height={240} />
{alerts.map((a, i) => (
{a.t}
{a.d}
))}
{/* ⑤ Funnel + P&L + sources */}
{[['Doanh thu', P.revenue, '#0e7c4a'], ['− COGS', -cogs, '#616161'], ['= Lãi gộp', gross, '#303030'], ['− Opex', -opex, '#616161'], ['− Thuế', -ebit * P.taxRate, '#616161'], ['= Lãi ròng', net, '#0e7c4a']].map((r, i) => (
{r[0]}
))}
{(() => { const by = {}; AAU.leads.forEach(l => { const c = AAU.sourceMeta[l.source]; const k = c ? c.label : l.source; by[k] = (by[k] || 0) + 1; }); const colors = ['#0084ff', '#111111', '#ea4335', '#0a9e6e', '#7c3aed', '#f59e0b']; const data = Object.entries(by).map(([l, v], i) => ({ l, v: v + 20, color: colors[i % colors.length] })); return
{data.map((d, i) => {d.l})}
; })()}
); } // ===================== FLOW MAP — layered operating-system map ===================== function StepLabel({ n, title, sub }) { return (
{n}
{title}
{sub &&
{sub}
}
); } function FlowNode({ n }) { return (
navigate(n.path)} className="flow-node" style={{ flex: 1, minWidth: 168, cursor: 'pointer', background: '#fff', border: '1px solid var(--p-border)', borderRadius: 14, padding: 16, boxShadow: 'var(--p-shadow-card)' }}>
{n.label}
{n.metric}
{n.sub}
Mở module
); } function ConvArrow({ pct, label }) { return (
{pct} {label}
); } function EngineCard({ icon, color, name, role, metric, serves, path }) { return (
navigate(path)} className="f1" style={{ minWidth: 230, cursor: 'pointer', background: '#fff', border: '1px dashed var(--p-border-hover)', borderRadius: 12, padding: '14px 16px' }}>
{name}
{role}
{metric} ↑ {serves}
); } function FlowMap() { const s = useSystemPulse(); const T = s.T, PA = s.PA, f = AAU.fmtVNDm, fn = AAU.fmtNum; const msgToLead = (PA.leads / T.messages * 100).toFixed(1).replace('.', ',') + '%'; const leadToQual = Math.round(PA.qualified / PA.leads * 100) + '%'; const qualToWon = (PA.won / PA.qualified * 100).toFixed(1).replace('.', ',') + '%'; const nodes = [ { id: 'mkt', label: 'Marketing', icon: 'megaphone', color: '#0084ff', path: '/marketing/overview', metric: f(T.spend) + ' chi', sub: PA.posts + ' bài · ' + fn(PA.leads) + ' lead' }, { id: 'inbox', label: 'Inbox', icon: 'inbox', color: '#7c3aed', path: '/inbox', metric: fn(T.messages), sub: 'hội thoại · 92% phản hồi' }, { id: 'sales', label: 'Sales CRM', icon: 'kanban', color: '#f59e0b', path: '/leads/kanban', metric: PA.won + ' won', sub: PA.deals + ' deal · ' + fn(PA.qualified) + ' qualified' }, { id: 'delivery', label: 'Delivery', icon: 'graduation', color: '#0a9e6e', path: '/classes', metric: PA.students + ' ghi danh', sub: PA.cohorts + ' khóa khai giảng' }, { id: 'finance', label: 'Finance', icon: 'dollar', color: '#008060', path: '/finance', metric: f(PA.revenue), sub: 'biên ròng ' + s.margin + '%' }, ]; const convs = [ { pct: msgToLead, label: 'msg → lead' }, { pct: leadToQual, label: 'lead → qualified' }, { pct: qualToWon, label: 'qualified → won' }, { pct: f(PA.revenue / PA.students), label: 'doanh thu / HV' }, ]; return (
{/* ① PLAN sets the targets */}
navigate('/planning')} style={{ cursor: 'pointer' }}>
Mục tiêu kỳ {PA.monthLabel}
Doanh thu {f(s.pace.target)} · {PA.cohorts}+ khóa khai giảng · quota {Math.round(s.pace.target / 11000000)} ghi danh
Đã đạt
{f(PA.revenue)} · {s.pace.pacePct}% nhịp
{s.pace.behind ? 'Chậm nhịp' : 'Đúng nhịp'}
{/* ② CORE CONVEYOR */}
{nodes.map((n, i) => ( {i < nodes.length - 1 && } ))}
{/* ③ SUPPORT ENGINES */}
{/* ④ conversion scoreboard */}
); } Object.assign(window, { ExecDashboard, FlowMap });