/* AAU CRM — Marketing Hub part 3: Channel Detail, Content Calendar, Marketing Settings, Attribution models */ // channel field on leads/conversations uses legacy short ids const LEGACY_CHAN = { facebook: 'fb', instagram: 'instagram', tiktok: 'tiktok', zalo: 'zalo', email: 'email' }; function ChannelDetail({ channelId }) { const c = AAU.mktChannelById(channelId); const t = useTimeRange('30d'); if (!c) return
navigate('/marketing/sources')}>Về Source Performance} />
; const fs = AAU.mktFacts.filter(f => f.channel === channelId); const spend = fs.reduce((s, f) => s + f.spend, 0); const org = fs.reduce((s, f) => s + f.leadsOrg, 0); const ads = fs.reduce((s, f) => s + f.leadsAds, 0); const won = fs.reduce((s, f) => s + f.won, 0); const msgs = fs.reduce((s, f) => s + f.messages, 0); const leads = org + ads; const fn = AAU.channelFunnel.find(x => x.channel === channelId); const posts = AAU.contentPostsX.filter(p => p.channel === channelId); const cps = AAU.campaigns.filter(cp => cp.platform === channelId); const chLeads = AAU.leads.filter(l => l.channel === (LEGACY_CHAN[channelId] || '___')); const kpis = [ { l: 'Σ Lead', v: leads, sub: org + ' organic · ' + ads + ' ads' }, { l: 'Spend', v: spend ? AAU.fmtVNDm(spend) : '—' }, { l: 'CPL (ads)', v: ads ? AAU.fmtVND(spend / ads) : '—', crm: true }, { l: 'Won (CRM)', v: won, crm: true }, { l: 'CAC', v: won && spend ? AAU.fmtVNDm(spend / won) : '—', crm: true }, { l: 'Bài đăng', v: posts.length }, ]; return (
navigate('/marketing/sources') }, { label: c.label }]} title={{c.label}} subtitle={'Chi tiết hiệu quả kênh ' + c.label + ' — organic, ads, nội dung và lead'} actions={<>} />
{c.label}
{c.kind === 'social' ? 'Mạng xã hội' : c.kind === 'messaging' ? 'Nhắn tin' : c.kind === 'search' ? 'Tìm kiếm' : c.kind === 'web' ? 'Website' : 'CRM'} · {c.hasAds ? 'Có chạy ads' : 'Chỉ organic'}
{kpis.map((k, i) => (
{k.crm && }{k.l}
{k.v}
{k.sub &&
{k.sub}
}
))}
{fn && } {cps.length ? {cps.map(cp => navigate('/marketing/ads/' + cp.id)}>)}
CampaignSpendMsgLeadCPLROAS
{cp.name}{AAU.fmtVNDm(cp.spend)}{cp.messages}{cp.leads}{AAU.fmtVND(cp.spend / cp.leads)}{(cp.won * 14900000 / cp.spend).toFixed(1)}×
: }
{posts.length ? {posts.map(p => )}
Bài đăngFormatĐăng lúcReachEng%MsgLead
{p.title}{p.format}{AAU.fmtDate(p.date)} {p.time}{AAU.fmtNum(p.reach)}{p.engagement}%{p.messages}{p.leads}
: }
{chLeads.length ? {chLeads.map(l => navigate('/leads/' + l.id)}>)}
LeadCông tyGiai đoạnPhụ trách
{l.name}{l.company}{AAU.users.find(u => u.id === l.assignedTo)?.name || '—'}
: }
); } function ContentCalendar() { const [chan, setChan] = useState('all'); const [mo, setMo] = useState(0); // months offset from June 2026 const cur = new Date(2026, 5 + mo, 1); const year = cur.getFullYear(), month = cur.getMonth(); const isThisMonth = mo === 0; const entries = isThisMonth ? AAU.contentCalendar.filter(e => chan === 'all' || e.channel === chan) : []; const firstDow = (new Date(year, month, 1).getDay() + 6) % 7; // Monday-start const daysInMonth = new Date(year, month + 1, 0).getDate(); const monthLabel = 'Tháng ' + (month + 1) + ', ' + year; const cells = []; for (let i = 0; i < firstDow; i++) cells.push(null); for (let d = 1; d <= daysInMonth; d++) cells.push(d); const evByDay = {}; entries.forEach(e => { (evByDay[e.day] = evByDay[e.day] || []).push(e); }); const counts = { posted: 0, scheduled: 0, draft: 0 }; AAU.contentCalendar.forEach(e => counts[e.status]++); const statusLabel = { posted: 'Đã đăng', scheduled: 'Đã lên lịch', draft: 'Bản nháp' }; return (
set(k, e.target.value)} style={{ width: 130, textAlign: 'right' }} />{unit}
); const [mAlerts, setMAlerts] = useState([ { id: 'cpl', name: 'CPL vượt ngưỡng cảnh báo', note: 'Tô đỏ kênh/khóa + đẩy cảnh báo lên Overview', on: true }, { id: 'pace', name: 'Pacing ngân sách > 100%', note: 'Báo khi chi vượt nhịp ngân sách tháng', on: true }, { id: 'roas', name: 'ROAS dưới mục tiêu', note: 'Cảnh báo khi hiệu quả tụt dưới ngưỡng', on: true }, { id: 'fill', name: 'Lớp sắp ế — cần đẩy ads', note: 'Fill thấp gần ngày khai giảng', on: true }, { id: 'msg', name: 'Cost/Message vượt ngưỡng', note: 'Chi phí mỗi tin nhắn quá cao', on: false }, ]); return (
{saved && Đã lưu}} />
{numRow('monthlyBudget', 'Ngân sách quảng cáo / tháng', 'triệu ₫', 'Dùng cho pacing ở Overview')} {numRow('roasTarget', 'Mục tiêu ROAS', '×', 'Ngưỡng hiệu quả tối thiểu')} {numRow('cplTarget', 'CPL mục tiêu', 'nghìn ₫')} {numRow('cplAlert', 'CPL cảnh báo', 'nghìn ₫', 'CPL vượt mức này sẽ tô đỏ + sinh cảnh báo')} {numRow('costMsgTarget', 'Cost/Msg mục tiêu', 'nghìn ₫')} {numRow('costMsgAlert', 'Cost/Msg cảnh báo', 'nghìn ₫')}
CPL hiện tại (blended){AAU.fmtVND(AAU.mktSummary('channel').total.cplBlended)}
Ngưỡng cảnh báo{AAU.fmtVND(num(f.cplAlert) * 1e3)}
Pacing ngân sách{Math.round(AAU.mktBudget.spentMTD / (num(f.monthlyBudget) * 1e6) * 100)}%
{mAlerts.map(r => (
{r.name}
{r.note}
setMAlerts(a => a.map(x => x.id === r.id ? { ...x, on: v } : x))} />
))}
Cảnh báo hiển ở Marketing Overview và gửi in-app cho phụ trách khi bật.
); } function AttributionModelsBody() { const [model, setModel] = useState('linear'); const rows = AAU.attributionByModel(model); const totRev = rows.reduce((s, r) => s + r.revenue, 0); const totLeads = rows.reduce((s, r) => s + r.leads, 0); const donut = rows.map(r => ({ l: AAU.mktChannelById(r.channel).label, v: Math.round(r.revenue / 1e6), color: AAU.mktChannelById(r.channel).color })); const modelDesc = { first: 'Lần chạm ĐẦU — toàn bộ công ghi cho kênh khách tiếp xúc đầu tiên (đo kênh khám phá / top-funnel).', last: 'Lần chạm CUỐI — toàn bộ công ghi cho kênh ngay trước khi chốt (đo kênh chốt đơn / bottom-funnel).', linear: 'Tuyến tính — chia đều công cho mọi điểm chạm trong hành trình (cân bằng cả phễu).', }; return ( <>
Mô hình ghi công chuyển đổi
{({ first: 'Lần chạm đầu', last: 'Lần chạm cuối', linear: 'Tuyến tính' })[model]}: {modelDesc[model]}
({ l: d.l, color: d.color }))} />
{rows.map(r => ( navigate('/marketing/channel/' + r.channel)}> ))}
KênhLeadWonDoanh thu% doanh thu
{r.leads}{r.won} {AAU.fmtVNDm(r.revenue)}
{r.pct.toFixed(0)}%
{AAU.touchPaths.map((p, i) => (
{p.path.map((ch, j) => ( {j > 0 && } ))}
{p.leads} lead{p.won} won{AAU.fmtVNDm(p.revenue)}
))}
); } Object.assign(window, { ChannelDetail, ContentCalendar, MarketingSettings, AttributionModelsBody });