Return on Marketing (ROMI)
${romi.toFixed(1)}%
Spend: $${totalSpend.toLocaleString()}
Customer Acquisition Cost (CAC)
$${cac.toLocaleString(undefined, {maximumFractionDigits:0})}
${s.outcomes.newCustomers} New Customers
Total Leads Generated
${s.funnel.leads.toLocaleString()}
${s.funnel.mqls} MQLs
`;
// Funnel
const rates = {
l2m: s.funnel.leads > 0 ? (s.funnel.mqls / s.funnel.leads) * 100 : 0,
m2s: s.funnel.mqls > 0 ? (s.funnel.sqls / s.funnel.mqls) * 100 : 0,
};
this.dom.funnelContainer.innerHTML = `
${s.funnel.visitors.toLocaleString()}
Visitors
${s.funnel.leads.toLocaleString()}
Leads
${s.funnel.mqls.toLocaleString()}
MQLs
${rates.l2m.toFixed(1)}%
${s.funnel.sqls.toLocaleString()}
SQLs
${rates.m2s.toFixed(1)}%
`;
// Charts
this.renderPieChart();
this.renderBarCharts();
}
renderPieChart() {
const totalLeads = this.state.channels.reduce((sum, c) => sum + c.leads, 0);
if (totalLeads === 0) {
this.dom.leadChannelChart.style.background = '#eee';
this.dom.leadChannelLegend.innerHTML = '
No lead data.'
return;
};
const colors = ['#0d47a1', '#1976d2', '#42a5f5', '#90caf9', '#e3f2fd'];
let gradientString = 'conic-gradient(';
let currentPercentage = 0;
this.dom.leadChannelLegend.innerHTML = '';
this.state.channels.forEach((c, i) => {
const percentage = (c.leads / totalLeads) * 100;
const color = colors[i % colors.length];
if(percentage > 0) {
gradientString += `${color} ${currentPercentage}% ${currentPercentage + percentage}%, `;
currentPercentage += percentage;
}
this.dom.leadChannelLegend.innerHTML += `
${c.name}`;
});
gradientString = gradientString.slice(0, -2) + ')';
this.dom.leadChannelChart.style.background = gradientString;
}
renderBarCharts() {
// CPL
const cplData = this.state.channels.map(c => ({ ...c, cpl: c.leads > 0 ? c.spend / c.leads : 0 }));
const maxCPL = Math.max(...cplData.map(c => c.cpl));
this.dom.cplChannelChart.innerHTML = cplData.map(c => `
${c.name}
$${c.cpl.toFixed(0)}
`).join('');
// Campaign ROI
const roiData = this.state.campaigns.map(c => ({ ...c, roi: c.budget > 0 ? ((c.revenue-c.budget)/c.budget)*100 : 0 })).sort((a,b) => b.roi-a.roi);
const maxROI = Math.max(...roiData.map(c => c.roi));
this.dom.campaignRoiChart.innerHTML = roiData.map(c => `
${c.name}
${c.roi.toFixed(0)}%
`).join('');
}
// --- PDF & Navigation ---
openTab(event, tabName) { this.state.activeTab = tabName; this.updateUI(); }
navigateTabs(dir) {
const i = this.TABS_ORDER.indexOf(this.state.activeTab);
let n = i;
if (dir === 'next' && i < this.TABS_ORDER.length - 1) n++;
if (dir === 'prev' && i > 0) n--;
this.openTab(null, this.TABS_ORDER[n]);
}
async generatePdf() {
if(this.state.activeTab !== 'dashboard') {
this.openTab(null, 'dashboard');
await new Promise(res => setTimeout(res, 50));
}
this.dom.downloadPdfBtn.disabled = true; this.dom.downloadPdfBtn.textContent = 'Generating...';
// This line was causing the error. It needs window.jspdf
const { jsPDF } = window.jspdf;
try {
const canvas = await html2canvas(this.dom.pdfOutput, { scale: 2, useCORS: true, backgroundColor: '#ffffff' });
const imgData = canvas.toDataURL('image/png');
const pdf = new jsPDF({ orientation: 'landscape', unit: 'pt', format: 'a4' });
const margin = 40;
const pdfWidth = pdf.internal.pageSize.getWidth();
const imgWidth = pdfWidth - margin * 2;
const imgHeight = canvas.height * imgWidth / canvas.width;
pdf.addImage(imgData, 'PNG', margin, margin, imgWidth, imgHeight);
pdf.save(`CMO_Marketing_Dashboard.pdf`);
} catch(e) {
console.error(e); alert('Error generating PDF.');
} finally {
this.dom.downloadPdfBtn.disabled = false; this.dom.downloadPdfBtn.textContent = 'Download Dashboard as PDF';
}
}
}
const app = new CMODashboard();