`;
campaignProgressChartEl.appendChild(barItem);
});
}
/**
* Renders the campaign data into the data configuration table with edit/delete actions.
*/
function renderManageCampaignsTable() {
if (!manageCampaignsTableBody) {
console.error('Manage campaigns table body not found.');
return;
}
manageCampaignsTableBody.innerHTML = ''; // Clear existing rows
if (campaigns.length === 0) {
manageCampaignsTableBody.innerHTML = `
No campaigns to manage.
`;
return;
}
campaigns.forEach(campaign => {
const row = manageCampaignsTableBody.insertRow();
row.setAttribute('data-campaign-id', campaign.id); // Set data attribute for easy lookup
row.classList.add('hover:bg-gray-50');
row.innerHTML = `
`;
});
}
/**
* Adds a new campaign from the input fields.
*/
function addCampaign() {
if (!newCampaignNameInput || !newCampaignGoalInput || !newCampaignRaisedInput || !newCampaignDonorsInput || !newStartDateInput || !newEndDateInput || !newCampaignStatusSelect) {
console.error('New campaign input elements not found.');
return;
}
const name = newCampaignNameInput.value.trim();
const goal = parseFloat(newCampaignGoalInput.value);
const raised = parseFloat(newCampaignRaisedInput.value);
const donors = parseInt(newCampaignDonorsInput.value);
const startDate = newStartDateInput.value;
const endDate = newEndDateInput.value;
const status = newCampaignStatusSelect.value;
if (!name || isNaN(goal) || goal < 0 || isNaN(raised) || raised < 0 || isNaN(donors) || donors < 0 || !startDate || !endDate || !status) {
alert('Please fill in all fields correctly: Campaign Name, non-negative Goal, non-negative Raised, non-negative Donors, Start Date, End Date, and Status.');
return;
}
if (raised > goal) {
alert('Raised amount cannot be greater than the Goal amount.');
return;
}
if (new Date(startDate) > new Date(endDate)) {
alert('Start Date cannot be after End Date.');
return;
}
campaigns.push({
id: generateUniqueId(),
name: name,
goal: goal,
raised: raised,
donors: donors,
startDate: startDate,
endDate: endDate,
status: status
});
// Clear input fields
newCampaignNameInput.value = '';
newCampaignGoalInput.value = '';
newCampaignRaisedInput.value = '';
newCampaignDonorsInput.value = '';
newStartDateInput.value = '';
newEndDateInput.value = '';
newCampaignStatusSelect.value = 'Planned'; // Reset to default
// Re-render tables and update metrics
updateAllViews();
}
/**
* Updates an existing campaign based on changes in the manage table.
* This function is exposed globally for `onclick` attributes.
* @param {string} campaignId - The ID of the campaign to update.
* @param {HTMLElement} buttonElement - The button element that triggered the update.
*/
window.updateCampaign = function(campaignId, buttonElement) {
const row = buttonElement.closest('tr');
if (!row) {
console.error('Could not find row for campaignId:', campaignId);
return;
}
const nameInput = row.querySelector('[data-field="name"]');
const goalInput = row.querySelector('[data-field="goal"]');
const raisedInput = row.querySelector('[data-field="raised"]');
const donorsInput = row.querySelector('[data-field="donors"]');
const startDateInput = row.querySelector('[data-field="startDate"]');
const endDateInput = row.querySelector('[data-field="endDate"]');
const statusSelect = row.querySelector('[data-field="status"]');
if (!nameInput || !goalInput || !raisedInput || !donorsInput || !startDateInput || !endDateInput || !statusSelect) {
console.error('Input fields not found in row for campaignId:', campaignId);
return;
}
const updatedName = nameInput.value.trim();
const updatedGoal = parseFloat(goalInput.value);
const updatedRaised = parseFloat(raisedInput.value);
const updatedDonors = parseInt(donorsInput.value);
const updatedStartDate = startDateInput.value;
const updatedEndDate = endDateInput.value;
const updatedStatus = statusSelect.value;
if (!updatedName || isNaN(updatedGoal) || updatedGoal < 0 || isNaN(updatedRaised) || updatedRaised < 0 || isNaN(updatedDonors) || updatedDonors < 0 || !updatedStartDate || !updatedEndDate || !updatedStatus) {
alert('Please ensure all fields are valid before saving.');
return;
}
if (updatedRaised > updatedGoal) {
alert('Raised amount cannot be greater than the Goal amount.');
return;
}
if (new Date(updatedStartDate) > new Date(updatedEndDate)) {
alert('Start Date cannot be after End Date.');
return;
}
const campaignIndex = campaigns.findIndex(c => c.id === campaignId);
if (campaignIndex !== -1) {
campaigns[campaignIndex] = {
...campaigns[campaignIndex],
name: updatedName,
goal: updatedGoal,
raised: updatedRaised,
donors: updatedDonors,
startDate: updatedStartDate,
endDate: updatedEndDate,
status: updatedStatus
};
updateAllViews();
} else {
console.warn('Campaign not found for update:', campaignId);
}
};
/**
* Deletes a campaign from the list.
* This function is exposed globally for `onclick` attributes.
* @param {string} campaignId - The ID of the campaign to delete.
*/
window.deleteCampaign = function(campaignId) {
const initialLength = campaigns.length;
campaigns = campaigns.filter(campaign => campaign.id !== campaignId);
if (campaigns.length < initialLength) {
updateAllViews();
} else {
console.warn('Campaign not found for deletion:', campaignId);
}
};
/**
* Updates all relevant views (metrics, dashboard table, manage table, bar chart).
*/
function updateAllViews() {
updateDashboardMetrics();
renderCampaignsTable();
renderCampaignProgressChart();
renderManageCampaignsTable();
}
/**
* Generates and downloads a PDF of the dashboard content.
*/
function downloadDashboardPdf() {
// Ensure jsPDF is available
if (typeof window.jspdf === 'undefined' || typeof window.jspdf.jsPDF === 'undefined') {
console.error("jsPDF library not loaded. Cannot generate PDF.");
alert("PDF generation library not loaded. Please try again later.");
return;
}
const { jsPDF } = window.jspdf;
const doc = new jsPDF('landscape'); // Use landscape for wider tables
// Set font and color for consistency
doc.setFont('helvetica');
doc.setTextColor('#1f2937'); // Dark gray
// Title
doc.setFontSize(24);
doc.text('Donation & Fundraising Campaign Performance Dashboard', doc.internal.pageSize.getWidth() / 2, 20, { align: 'center' });
// Add a small separator
doc.setDrawColor('#d1d5db');
doc.line(20, 25, doc.internal.pageSize.getWidth() - 20, 25);
// Dashboard Metrics
doc.setFontSize(14);
let yPos = 40;
const metrics = [
{ label: 'Total Donations Received:', value: totalDonationsReceivedEl ? totalDonationsReceivedEl.textContent : 'N/A' },
{ label: 'Number of Campaigns:', value: numCampaignsEl ? numCampaignsEl.textContent : 'N/A' },
{ label: 'Average Donation Per Campaign:', value: avgDonationPerCampaignEl ? avgDonationPerCampaignEl.textContent : 'N/A' },
{ label: 'Total Donors:', value: totalDonorsEl ? totalDonorsEl.textContent : 'N/A' }
];
metrics.forEach(metric => {
doc.text(`${metric.label} ${metric.value}`, 20, yPos);
yPos += 10;
});
yPos += 15; // Extra space before table
// Campaign Progress (as a table in PDF)
doc.setFontSize(16);
doc.text('Campaign Progress (Raised vs. Goal)', 20, yPos);
yPos += 10;
const progressTableColumn = ["Campaign Name", "Goal ($)", "Raised ($)", "% to Goal"];
const progressTableRows = [];
campaigns.forEach(campaign => {
const percentToGoal = campaign.goal > 0 ? ((campaign.raised / campaign.goal) * 100).toFixed(2) : 0;
progressTableRows.push([campaign.name, formatCurrency(campaign.goal), formatCurrency(campaign.raised), `${percentToGoal}%`]);
});
doc.autoTable({
head: [progressTableColumn],
body: progressTableRows,
startY: yPos,
theme: 'grid',
styles: {
fontSize: 10,
cellPadding: 3,
textColor: '#1f2937',
lineColor: '#e5e7eb',
lineWidth: 0.1
},
headStyles: {
fillColor: '#f3f4f6',
textColor: '#4b5563',
fontStyle: 'bold'
},
alternateRowStyles: {
fillColor: '#ffffff'
},
margin: { left: 20, right: 20 },
didDrawPage: function(data) {
let str = "Page " + doc.internal.getNumberOfPages();
doc.setFontSize(10);
doc.setTextColor('#6b7280');
doc.text(str, doc.internal.pageSize.getWidth() - 20, doc.internal.pageSize.getHeight() - 10, { align: 'right' });
}
});
yPos = doc.autoTable.previous.finalY + 15; // Continue from where the last table ended
// Campaign Overview Table
doc.setFontSize(16);
doc.text('Campaign Overview', 20, yPos);
yPos += 10;
const overviewTableColumn = ["Campaign Name", "Goal ($)", "Raised ($)", "Donors", "Start Date", "End Date", "Status", "% to Goal"];
const overviewTableRows = [];
campaigns.forEach(campaign => {
const percentToGoal = campaign.goal > 0 ? ((campaign.raised / campaign.goal) * 100).toFixed(2) : 0;
overviewTableRows.push([
campaign.name,
formatCurrency(campaign.goal),
formatCurrency(campaign.raised),
formatNumber(campaign.donors),
campaign.startDate,
campaign.endDate,
campaign.status,
`${percentToGoal}%`
]);
});
doc.autoTable({
head: [overviewTableColumn],
body: overviewTableRows,
startY: yPos,
theme: 'grid',
styles: {
fontSize: 6, // Further reduced font size for more columns in landscape
cellPadding: 1,
textColor: '#1f2937',
lineColor: '#e5e7eb',
lineWidth: 0.1
},
headStyles: {
fillColor: '#f3f4f6',
textColor: '#4b5563',
fontStyle: 'bold'
},
alternateRowStyles: {
fillColor: '#ffffff'
},
margin: { left: 10, right: 10 }, // Adjusted margins to fit more content
didDrawPage: function(data) {
let str = "Page " + doc.internal.getNumberOfPages();
doc.setFontSize(10);
doc.setTextColor('#6b7280');
doc.text(str, doc.internal.pageSize.getWidth() - 20, doc.internal.pageSize.getHeight() - 10, { align: 'right' });
}
});
// Save the PDF
doc.save('donation_fundraising_dashboard.pdf');
}
// --- Event Listeners ---
// Tab button clicks
if (dashboardTabButton) {
dashboardTabButton.addEventListener('click', function() {
switchTab(0);
});
}
if (dataConfigTabButton) {
dataConfigTabButton.addEventListener('click', function() {
switchTab(1);
});
}
// Navigation button clicks
if (prevTabButton) {
prevTabButton.addEventListener('click', function() {
if (currentTabIndex > 0) {
switchTab(currentTabIndex - 1);
}
});
}
if (nextTabButton) {
nextTabButton.addEventListener('click', function() {
if (currentTabIndex < tabs.length - 1) {
switchTab(currentTabIndex + 1);
}
});
}
// Add Campaign button click
if (addCampaignButton) {
addCampaignButton.addEventListener('click', addCampaign);
}
// PDF Download button click
if (downloadPdfButton) {
downloadPdfButton.addEventListener('click', downloadDashboardPdf);
}
// --- Initial Render ---
// Set initial tab to Dashboard
switchTab(0);
// Populate data and update views
updateAllViews();
});