No VOD data to display.
`;
}
renderViewsChart(metrics.viewsByDate);
renderEngagementChart(metrics.engagementByVideo);
};
const renderViewsChart = (viewsByDate) => {
const ctx = elements.viewsChartCanvas?.getContext('2d');
if (!ctx) return;
const sortedDates = Object.keys(viewsByDate).sort((a, b) => new Date(a) - new Date(b));
const chartData = sortedDates.map(date => viewsByDate[date]);
if (viewsChart) viewsChart.destroy();
viewsChart = new Chart(ctx, {
type: 'line',
data: { labels: sortedDates, datasets: [{ label: 'Total Views', data: chartData, borderColor: '#7c3aed', backgroundColor: 'rgba(124, 58, 237, 0.1)', fill: true, tension: 0.3 }] },
options: { responsive: true, maintainAspectRatio: true }
});
};
const renderEngagementChart = (engagementByVideo) => {
const ctx = elements.engagementChartCanvas?.getContext('2d');
if (!ctx) return;
const labels = Object.keys(engagementByVideo);
const data = Object.values(engagementByVideo);
if (engagementChart) engagementChart.destroy();
engagementChart = new Chart(ctx, {
type: 'bar',
data: { labels, datasets: [{ label: 'Avg. Minutes Watched', data, backgroundColor: '#3b82f6', borderColor: '#2563eb', borderWidth: 1 }] },
options: { responsive: true, maintainAspectRatio: true, indexAxis: 'y' }
});
};
const renderConfigPanel = () => {
const container = elements.editableVodList;
if (!container) return;
container.innerHTML = '';
vodData.forEach(v => {
const card = document.createElement('div');
card.className = 'grid grid-cols-1 md:grid-cols-4 gap-4 items-center bg-white p-3 rounded-lg border';
card.innerHTML = `
`;
container.appendChild(card);
});
};
const handleAddVod = (e) => {
e.preventDefault();
try {
const getNumericValue = (id) => parseFloat(document.getElementById(id).value) || 0;
const newVod = {
id: vodData.length > 0 ? Math.max(...vodData.map(v => v.id)) + 1 : 1,
title: document.getElementById('videoTitle').value,
views: getNumericValue('videoViews'),
watchTime: getNumericValue('totalWatchTime'),
duration: getNumericValue('videoDuration'),
uniqueViewers: getNumericValue('uniqueViewers'),
date: document.getElementById('publishDate').value
};
vodData.push(newVod);
elements.addVodForm.reset();
renderAll();
showToast('VOD record added!');
} catch (error) {
console.error("Error adding VOD record:", error);
showToast("Error: Could not add record.");
}
};
const handleConfigListClick = (e) => {
const target = e.target.closest('button');
if (!target) return;
const action = target.dataset.action;
const id = parseInt(target.dataset.id, 10);
if (action === 'update') {
try {
const index = vodData.findIndex(v => v.id === id);
if (index === -1) return;
const inputs = target.closest('.grid').querySelectorAll('input');
inputs.forEach(input => {
const field = input.dataset.field;
let value = input.value;
if (input.type === 'number') value = parseFloat(value) || 0;
vodData[index][field] = value;
});
renderAll();
showToast('Record updated!');
} catch (error) {
console.error("Error updating record:", error);
showToast("Error: Could not save data.");
}
} else if (action === 'delete') {
vodData = vodData.filter(v => v.id !== id);
renderAll();
showToast('Record deleted.');
}
};
const handleGeneratePDF = () => {
try {
const { jsPDF } = window.jspdf;
const doc = new jsPDF();
const metrics = calculateMetrics();
if (metrics.totalViews === 0) {
showToast("Cannot generate PDF with no data.");
return;
}
doc.setFontSize(20);
doc.text("VOD Views & Engagement Report", 105, 20, null, null, 'center');
doc.setFontSize(10);
doc.text(`Generated on: ${new Date().toLocaleDateString()}`, 105, 26, null, null, 'center');
doc.autoTable({ startY: 35, head: [['Metric', 'Value']], body: [ ['Total Views', metrics.totalViews.toLocaleString()], ['Avg. View Duration (min)', metrics.avgDuration.toFixed(1)], ['Avg. Completion Rate', `${metrics.completionRate.toFixed(1)}%`], ['Total Unique Viewers', metrics.uniqueViewers.toLocaleString()] ], theme: 'grid' });
let finalY = doc.lastAutoTable.finalY || 60;
doc.setFontSize(14);
doc.text("Views Over Time", 14, finalY + 15);
doc.addImage(elements.viewsChartCanvas.toDataURL('image/png', 1.0), 'PNG', 14, finalY + 20, 90, 50);
doc.text("Engagement by Video", 115, finalY + 15);
doc.addImage(elements.engagementChartCanvas.toDataURL('image/png', 1.0), 'PNG', 115, finalY + 20, 80, 60);
finalY += 75;
doc.setFontSize(14);
doc.text("VOD Asset Performance", 14, finalY);
const tableBody = vodData.map(v => {
const avgDur = v.views > 0 ? v.watchTime / v.views : 0;
const compRate = v.duration > 0 ? (avgDur / v.duration) * 100 : 0;
return [v.title, v.views.toLocaleString(), `${avgDur.toFixed(1)}m`, `${compRate.toFixed(1)}%`];
});
doc.autoTable({ head: [['Title', 'Views', 'Avg. Duration', 'Completion %']], body: tableBody, startY: finalY + 5, theme: 'striped', headStyles: { fillColor: [124, 58, 237] } });
doc.save('VOD-Engagement-Report.pdf');
} catch (error) {
console.error("Failed to generate PDF:", error);
showToast("Error: Could not generate PDF.");
}
};
const switchTab = (tabName) => {
currentTab = tabName;
Object.values(elements.tabs).forEach(tab => tab.classList.add('hidden'));
Object.values(elements.tabButtons).forEach(btn => btn.classList.remove('active'));
elements.tabs[tabName].classList.remove('hidden');
elements.tabButtons[tabName].classList.add('active');
updateNavButtons();
};
const navigateTabs = (direction) => {
const currentIndex = tabOrder.indexOf(currentTab);
const newIndex = direction === 'next' ? Math.min(currentIndex + 1, tabOrder.length - 1) : Math.max(currentIndex - 1, 0);
if (newIndex !== currentIndex) switchTab(tabOrder[newIndex]);
};
const updateNavButtons = () => {
const currentIndex = tabOrder.indexOf(currentTab);
elements.navButtons.prev.disabled = currentIndex === 0;
elements.navButtons.next.disabled = currentIndex === tabOrder.length - 1;
};
const showToast = (message) => {
if (!elements.toast) return;
elements.toast.textContent = message;
elements.toast.classList.add('show');
setTimeout(() => { elements.toast.classList.remove('show'); }, 3000);
}
elements.addVodForm.addEventListener('submit', handleAddVod);
elements.editableVodList.addEventListener('click', handleConfigListClick);
elements.pdfButton.addEventListener('click', handleGeneratePDF);
elements.tabButtons.dashboard.addEventListener('click', () => switchTab('dashboard'));
elements.tabButtons.config.addEventListener('click', () => switchTab('config'));
elements.navButtons.prev.addEventListener('click', () => navigateTabs('prev'));
elements.navButtons.next.addEventListener('click', () => navigateTabs('next'));
renderAll();
updateNavButtons();
});