${act.member} ${act.action}
${act.time}
`;
elements.recentActivityList.appendChild(div);
});
}
function renderMemberTable() {
if (!elements.membersTableBody) return;
elements.membersTableBody.innerHTML = '';
dashboardState.members.forEach(member => {
const tr = document.createElement('tr');
tr.innerHTML = `
${member.name} |
${member.points.toLocaleString('en-US')} |
${member.since} |
|
`;
elements.membersTableBody.appendChild(tr);
});
}
window.addMember = () => {
const name = elements.memberNameInput.value.trim();
const points = parseInt(elements.memberPointsInput.value, 10);
if (!name || isNaN(points)) {
alert('Please enter a valid name and points value.');
return;
}
const newMember = {
id: Date.now(),
name,
points,
since: new Date().toISOString().split('T')[0]
};
dashboardState.members.unshift(newMember);
renderMemberTable();
elements.memberNameInput.value = '';
elements.memberPointsInput.value = '100';
};
window.deleteMember = (id) => {
dashboardState.members = dashboardState.members.filter(m => m.id !== id);
renderMemberTable();
};
function populateConfigForm() {
const { kpis } = dashboardState;
if (elements.configTotalMembers) elements.configTotalMembers.value = kpis.totalMembers;
if (elements.configActiveMembers) elements.configActiveMembers.value = kpis.activeMembers;
if (elements.configRedemptionRate) elements.configRedemptionRate.value = kpis.redemptionRate;
if (elements.configPointsRedeemed) elements.configPointsRedeemed.value = kpis.pointsRedeemed;
renderRewardsConfigFields();
}
function renderRewardsConfigFields() {
if (!elements.rewardsConfigList) return;
elements.rewardsConfigList.innerHTML = '';
dashboardState.rewards.forEach(reward => {
addRewardField(reward);
});
}
window.addRewardField = (reward = { id: Date.now(), name: '', redeemed: 0 }) => {
if (!elements.rewardsConfigList) return;
const div = document.createElement('div');
div.className = 'grid grid-cols-1 md:grid-cols-7 gap-4 items-center reward-field';
div.dataset.id = reward.id;
div.innerHTML = `
`;
elements.rewardsConfigList.appendChild(div);
};
window.updateDashboardData = () => {
dashboardState.kpis.totalMembers = parseInt(elements.configTotalMembers.value, 10) || 0;
dashboardState.kpis.activeMembers = parseInt(elements.configActiveMembers.value, 10) || 0;
dashboardState.kpis.redemptionRate = parseInt(elements.configRedemptionRate.value, 10) || 0;
dashboardState.kpis.pointsRedeemed = elements.configPointsRedeemed.value || "0";
const newRewards = [];
document.querySelectorAll('.reward-field').forEach(field => {
const nameInput = field.querySelector('.reward-name-input');
const redeemedInput = field.querySelector('.reward-redeemed-input');
if (nameInput.value.trim()) {
newRewards.push({
id: parseInt(field.dataset.id, 10),
name: nameInput.value.trim(),
redeemed: parseInt(redeemedInput.value, 10) || 0
});
}
});
dashboardState.rewards = newRewards;
renderDashboard();
alert('Dashboard has been updated!');
changeTab('dashboard');
};
/**
* Generates and downloads a beautiful, well-structured PDF of the dashboard content.
*/
window.downloadPDF = async () => {
const { jsPDF } = window.jspdf;
const pdf = new jsPDF({ orientation: 'p', unit: 'mm', format: 'a4' });
// --- Document Settings ---
const docWidth = pdf.internal.pageSize.getWidth();
const margin = 15;
let cursorY = margin;
// --- Title ---
pdf.setFont('helvetica', 'bold');
pdf.setFontSize(22);
pdf.setTextColor('#374151'); // gray-700
pdf.text('Loyalty Program Dashboard Report', docWidth / 2, cursorY, { align: 'center' });
cursorY += 15;
// --- KPIs Section ---
pdf.setFont('helvetica', 'bold');
pdf.setFontSize(16);
pdf.setTextColor('#4f46e5'); // indigo-600
pdf.text('Key Performance Indicators', margin, cursorY);
cursorY += 8;
const kpiData = [
{ title: 'Total Members', value: dashboardState.kpis.totalMembers.toLocaleString('en-US') },
{ title: 'Active Members (30d)', value: dashboardState.kpis.activeMembers.toLocaleString('en-US') },
{ title: 'Redemption Rate', value: `${dashboardState.kpis.redemptionRate}%` },
{ title: 'Total Points Redeemed', value: dashboardState.kpis.pointsRedeemed }
];
pdf.setFont('helvetica', 'normal');
pdf.setFontSize(11);
pdf.setTextColor('#1f2937'); // gray-800
let kpiX = margin;
const kpiBoxWidth = (docWidth - margin * 2) / 4;
kpiData.forEach(kpi => {
pdf.setFont('helvetica', 'bold');
pdf.text(kpi.value, kpiX, cursorY);
pdf.setFont('helvetica', 'normal');
pdf.setTextColor('#6b7280'); // gray-500
pdf.text(kpi.title, kpiX, cursorY + 5);
pdf.setTextColor('#1f2937'); // gray-800
kpiX += kpiBoxWidth;
});
cursorY += 15;
// --- Charts Section ---
pdf.setFont('helvetica', 'bold');
pdf.setFontSize(16);
pdf.setTextColor('#4f46e5');
pdf.text('Visual Analytics', margin, cursorY);
cursorY += 8;
const chart1Element = document.getElementById('memberGrowthChart');
const chart2Element = document.getElementById('topRewardsChart');
try {
const chart1Canvas = await html2canvas(chart1Element, { scale: 3, backgroundColor: null });
const chart2Canvas = await html2canvas(chart2Element, { scale: 3, backgroundColor: null });
const chart1Data = chart1Canvas.toDataURL('image/png');
const chart2Data = chart2Canvas.toDataURL('image/png');
const chart1Width = 110;
const chart1Height = (chart1Canvas.height * chart1Width) / chart1Canvas.width;
const chart2Width = docWidth - chart1Width - margin * 3;
const chart2Height = (chart2Canvas.height * chart2Width) / chart2Canvas.width;
const maxChartHeight = Math.max(chart1Height, chart2Height);
pdf.setFontSize(12);
pdf.setTextColor('#374151');
pdf.text('Member Growth (Last 6 Months)', margin, cursorY);
pdf.addImage(chart1Data, 'PNG', margin, cursorY + 2, chart1Width, chart1Height);
pdf.text('Top Redeemed Rewards', margin + chart1Width + 10, cursorY);
pdf.addImage(chart2Data, 'PNG', margin + chart1Width + 10, cursorY + 2, chart2Width, chart2Height);
cursorY += maxChartHeight + 15;
} catch (error) {
console.error("Error capturing charts with html2canvas:", error);
pdf.setTextColor('#ef4444'); // red-500
pdf.text('Could not render charts.', margin, cursorY);
cursorY += 10;
}
// --- Recent Activity Section ---
pdf.addPage();
cursorY = margin;
pdf.setFont('helvetica', 'bold');
pdf.setFontSize(16);
pdf.setTextColor('#4f46e5');
pdf.text('Recent Member Activity', margin, cursorY);
cursorY += 10;
pdf.setFont('helvetica', 'normal');
pdf.setFontSize(10);
pdf.setTextColor('#374151');
dashboardState.activity.forEach(act => {
if (cursorY > 270) { // Check for page break
pdf.addPage();
cursorY = margin;
}
const activityText = `${act.member} ${act.action}`;
const timeText = act.time;
pdf.text(activityText, margin, cursorY);
pdf.text(timeText, docWidth - margin, cursorY, { align: 'right' });
cursorY += 7;
pdf.setDrawColor('#e5e7eb'); // gray-200
pdf.line(margin, cursorY - 3, docWidth - margin, cursorY - 3);
});
// --- Footer ---
const pageCount = pdf.internal.getNumberOfPages();
for (let i = 1; i <= pageCount; i++) {
pdf.setPage(i);
pdf.setFontSize(8);
pdf.setTextColor('#6b7280');
const footerText = `Page ${i} of ${pageCount} | Generated on: ${new Date().toLocaleDateString()}`;
pdf.text(footerText, docWidth / 2, 287, { align: 'center' });
}
// --- Save PDF ---
pdf.save('Loyalty_Program_Dashboard_Report.pdf');
};
// --- INITIALIZATION --- //
populateConfigForm();
renderDashboard();
renderMemberTable();
updateNavButtons();
});