/* 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 */}
Đầ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
;
}
// ---------- 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) => (
))}
{/* ⑤ 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 (
);
}
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.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 });