${healthy}
Healthy Accounts
`;
// Render distribution chart
this.renderDistributionChart(processedAccounts);
// Render dashboard table
this.dom.dashboardTableHead.innerHTML = `
| Account | Health Score | Status | MRR | Renewal Date | Upcoming |
|---|
`;
this.dom.dashboardTableBody.innerHTML = processedAccounts
.sort((a,b) => a.healthScore - b.healthScore)
.map(acc => {
const { status, color } = this.getHealthStatus(acc.healthScore);
const renewalDate = new Date(acc.renewal);
const daysToRenewal = Math.ceil((renewalDate - new Date()) / (1000 * 60 * 60 * 24));
let upcoming = '-';
if (daysToRenewal <= 30 && daysToRenewal >= 0) upcoming = `
In ${daysToRenewal} days`;
else if (daysToRenewal <= 90 && daysToRenewal > 0) upcoming = `
In ${daysToRenewal} days`;
return `
| ${acc.name} |
${acc.healthScore.toFixed(0)}% |
${status} |
$${acc.mrr.toLocaleString()} |
${acc.renewal} |
${upcoming} |
`;
}).join('');
}
renderDistributionChart(accounts) {
const buckets = { poor: 0, ok: 0, good: 0 };
accounts.forEach(acc => {
if (acc.healthScore < 50) buckets.poor++;
else if (acc.healthScore < 80) buckets.ok++;
else buckets.good++;
});
const max = Math.max(...Object.values(buckets)) || 1;
this.dom.healthDistributionChart.innerHTML = `
${Object.keys(buckets).map(key => `
${key.charAt(0).toUpperCase() + key.slice(1)} (${buckets[key]})
`).join('')}
`;
}
// --- Calculation Logic ---
calculateHealthScore(account) {
if (Object.values(this.state.weights).reduce((s,w) => s+w, 0) !== 100) return 0;
const scores = {
usage: { 'High': 100, 'Medium': 60, 'Low': 10 }[account.usage],
satisfaction: Math.max(0, (account.satisfaction + 100) / 2),
tickets: Math.max(0, 100 - (account.tickets * 20)),
relationship: { 'Strong': 100, 'Neutral': 60, 'Weak': 20 }[account.relationship]
};
let totalScore = 0;
for (const key in this.METRICS) {
totalScore += scores[key] * (this.state.weights[key] / 100);
}
return totalScore;
}
getHealthStatus(score) {
if (score < 50) return { status: 'At Risk', color: 'var(--health-poor)' };
if (score < 80) return { status: 'Needs Attention', color: 'var(--health-ok)' };
return { status: 'Healthy', color: 'var(--health-good)' };
}
// --- Navigation & PDF ---
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));
}
const { jsPDF } = window.jspdf;
this.dom.downloadPdfBtn.disabled = true; this.dom.downloadPdfBtn.textContent = 'Generating...';
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 pdfHeight = pdf.internal.pageSize.getHeight();
const imgWidth = pdfWidth - margin * 2;
const imgHeight = canvas.height * imgWidth / canvas.width;
let heightLeft = imgHeight;
let position = margin;
pdf.addImage(imgData, 'PNG', margin, position, imgWidth, imgHeight);
heightLeft -= (pdfHeight - margin * 2);
while (heightLeft > 0) {
position = -heightLeft - margin;
pdf.addPage();
pdf.addImage(imgData, 'PNG', margin, position, imgWidth, imgHeight);
heightLeft -= pdfHeight;
}
pdf.save(`Account_Health_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 AccountHealthDashboard();