Average Rating: ${seoData.reviews.avgRating} / 5.0
New Reviews: ${seoData.reviews.newReviews}
`;
// Citation Health
citationHealthEl.innerHTML = `
Consistency: ${seoData.citations.consistency}%
Total Citations: ${seoData.citations.total}
`;
// Keyword Rankings Table
keywordRankingsBodyEl.innerHTML = seoData.keywords.map(k => `
| ${k.keyword} |
${k.localRank} |
${k.organicRank} |
${k.volume.toLocaleString()} |
`).join('');
createOrUpdateChart();
}
/**
* Populates the configuration form with data from the seoData object.
*/
function renderConfigForm() {
// GBP
document.getElementById('config-search-views').value = seoData.gbp.searchViews;
document.getElementById('config-map-views').value = seoData.gbp.mapViews;
document.getElementById('config-website-clicks').value = seoData.gbp.websiteClicks;
document.getElementById('config-phone-calls').value = seoData.gbp.phoneCalls;
document.getElementById('config-direction-requests').value = seoData.gbp.directionRequests;
// Reviews & Citations
document.getElementById('config-avg-rating').value = seoData.reviews.avgRating;
document.getElementById('config-new-reviews').value = seoData.reviews.newReviews;
document.getElementById('config-citation-consistency').value = seoData.citations.consistency;
document.getElementById('config-total-citations').value = seoData.citations.total;
// Chart Data
configChartDataEl.innerHTML = ''; // Clear previous
seoData.chartData.labels.forEach((label, index) => {
configChartDataEl.innerHTML += `
`;
});
// Keywords
renderKeywordConfigRows();
}
/**
* Renders the editable keyword rows in the config tab.
*/
function renderKeywordConfigRows() {
configKeywordListEl.innerHTML = ''; // Clear existing rows
seoData.keywords.forEach((kw, index) => {
const row = document.createElement('div');
row.className = 'grid grid-cols-1 md:grid-cols-5 gap-2 items-center';
row.innerHTML = `
`;
configKeywordListEl.appendChild(row);
});
// Re-attach event listeners for remove buttons
document.querySelectorAll('.remove-keyword-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const indexToRemove = parseInt(e.target.dataset.index);
seoData.keywords.splice(indexToRemove, 1);
renderKeywordConfigRows(); // Re-render the list
});
});
}
/**
* Creates or updates the Chart.js instance.
*/
function createOrUpdateChart() {
if (gbpChartInstance) {
gbpChartInstance.destroy();
}
const ctx = chartCanvas.getContext('2d');
gbpChartInstance = new Chart(ctx, {
type: 'bar',
data: {
labels: seoData.chartData.labels,
datasets: [{
label: 'Search Views',
data: seoData.chartData.searchViews,
backgroundColor: 'rgba(79, 70, 229, 0.8)', // Indigo
borderColor: 'rgba(79, 70, 229, 1)',
borderWidth: 1
}, {
label: 'Map Views',
data: seoData.chartData.mapViews,
backgroundColor: 'rgba(34, 197, 94, 0.8)', // Green
borderColor: 'rgba(34, 197, 94, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
ticks: { color: '#4b5563' }
},
x: {
ticks: { color: '#4b5563' }
}
},
plugins: {
legend: {
labels: {
color: '#1f2937'
}
}
}
}
});
}
// --- EVENT HANDLERS ---
/**
* Handles the "Update Dashboard" button click.
* Reads all data from the config form, updates the seoData object,
* then re-renders the dashboard and switches to it.
*/
function handleUpdate() {
// Helper to safely parse numbers
const getNum = (id) => parseFloat(document.getElementById(id).value) || 0;
const getInt = (id) => parseInt(document.getElementById(id).value) || 0;
// Update GBP
seoData.gbp.searchViews = getInt('config-search-views');
seoData.gbp.mapViews = getInt('config-map-views');
seoData.gbp.websiteClicks = getInt('config-website-clicks');
seoData.gbp.phoneCalls = getInt('config-phone-calls');
seoData.gbp.directionRequests = getInt('config-direction-requests');
// Update Reviews & Citations
seoData.reviews.avgRating = getNum('config-avg-rating');
seoData.reviews.newReviews = getInt('config-new-reviews');
seoData.citations.consistency = getInt('config-citation-consistency');
seoData.citations.total = getInt('config-total-citations');
// Update Chart Data
document.querySelectorAll('.chart-label-input').forEach(input => {
seoData.chartData.labels[input.dataset.index] = input.value;
});
document.querySelectorAll('.chart-search-input').forEach(input => {
seoData.chartData.searchViews[input.dataset.index] = parseInt(input.value) || 0;
});
document.querySelectorAll('.chart-map-input').forEach(input => {
seoData.chartData.mapViews[input.dataset.index] = parseInt(input.value) || 0;
});
// Update Keywords
const newKeywords = [];
document.querySelectorAll('#config-keyword-list .grid').forEach(row => {
newKeywords.push({
keyword: row.querySelector('.keyword-input').value,
localRank: parseInt(row.querySelector('.local-rank-input').value) || 0,
organicRank: parseInt(row.querySelector('.organic-rank-input').value) || 0,
volume: parseInt(row.querySelector('.volume-input').value) || 0,
});
});
seoData.keywords = newKeywords;
renderDashboard();
showTab('dashboard');
}
/**
* Handles PDF download, ensuring proper rendering without truncation.
*/
async function handlePdfDownload() {
const { jsPDF } = window.jspdf;
const content = document.getElementById('pdf-content');
const pdfTitle = document.getElementById('pdf-title');
const downloadBtn = document.getElementById('download-pdf-btn');
// Temporarily show the title for the PDF capture
pdfTitle.style.display = 'block';
downloadBtn.textContent = 'Generating PDF...';
downloadBtn.disabled = true;
try {
// Use html2canvas to capture the specified content area
const canvas = await html2canvas(content, {
scale: 2, // Increase scale for better resolution
useCORS: true,
logging: false,
width: content.scrollWidth,
height: content.scrollHeight,
windowWidth: content.scrollWidth,
windowHeight: content.scrollHeight
});
const imgData = canvas.toDataURL('image/png');
// Create a new PDF document (A4 size, portrait)
const pdf = new jsPDF({
orientation: 'portrait',
unit: 'pt',
format: 'a4'
});
const pdfWidth = pdf.internal.pageSize.getWidth();
const pdfHeight = pdf.internal.pageSize.getHeight();
const canvasWidth = canvas.width;
const canvasHeight = canvas.height;
const ratio = canvasHeight / canvasWidth;
const imgWidth = pdfWidth - 40; // With some margin
const imgHeight = imgWidth * ratio;
let heightLeft = imgHeight;
let position = 20; // Top margin
// Add the image to the PDF, handling pagination if necessary
pdf.addImage(imgData, 'PNG', 20, position, imgWidth, imgHeight);
heightLeft -= (pdfHeight - 40);
while (heightLeft > 0) {
position = -heightLeft - 20;
pdf.addPage();
pdf.addImage(imgData, 'PNG', 20, position, imgWidth, imgHeight);
heightLeft -= (pdfHeight - 40);
}
// Save the PDF
pdf.save('Local-SEO-Performance-Report.pdf');
} catch (error) {
console.error("Error generating PDF:", error);
alert("Could not generate PDF. Please try again.");
} finally {
// Clean up: hide the PDF title and restore the button state
pdfTitle.style.display = 'none';
downloadBtn.textContent = 'Download as PDF';
downloadBtn.disabled = false;
}
}
// --- TABBING LOGIC ---
const tabs = ['dashboard', 'config'];
let currentTabIndex = 0;
window.showTab = (tabName) => {
currentTabIndex = tabs.indexOf(tabName);
tabs.forEach(t => {
document.getElementById(`${t}Tab`).classList.toggle('active', t === tabName);
document.getElementById(`btn-${t}`).classList.toggle('active', t === tabName);
document.getElementById(`btn-${t}`).classList.toggle('inactive', t !== tabName);
});
updateNavButtons();
};
function updateNavButtons() {
prevBtn.style.visibility = (currentTabIndex === 0) ? 'hidden' : 'visible';
nextBtn.style.visibility = (currentTabIndex === tabs.length - 1) ? 'hidden' : 'visible';
}
// --- EVENT LISTENER ATTACHMENT ---
updateDashboardBtn.addEventListener('click', handleUpdate);
downloadPdfBtn.addEventListener('click', handlePdfDownload);
addKeywordBtn.addEventListener('click', () => {
seoData.keywords.push({ keyword: "", localRank: 0, organicRank: 0, volume: 0 });
renderKeywordConfigRows();
});
prevBtn.addEventListener('click', () => {
if (currentTabIndex > 0) {
showTab(tabs[currentTabIndex - 1]);
}
});
nextBtn.addEventListener('click', () => {
if (currentTabIndex < tabs.length - 1) {
showTab(tabs[currentTabIndex + 1]);
}
});
// --- INITIALIZATION ---
renderDashboard();
renderConfigForm();
showTab('dashboard'); // Start on the dashboard tab
});