Client: ${data.brandClient || 'N/A'} | Report Date: ${new Date().toLocaleDateString()}
Status: ${data.overallStatus}
Target Audience: ${data.targetAudience.substring(0, 70)}...
Primary Channel: ${data.primaryChannel}
Budget: ${totalBudget} | Spend: ${actualSpend}
Remaining: ${remaining}
Primary KPIs: ${data.kpis.join(', ').substring(0, 70)}...
Core Message
${data.coreMessage || 'N/A'}
Execution Snapshot (${data.executionPlan.filter(i => i.status === 'Complete').length}/${data.executionPlan.length} Complete)
${data.executionPlan.slice(0, 3).map(item => `- [${item.date}] ${item.activity} (${item.status})
`).join('')}
${data.executionPlan.length > 3 ? `- ... and ${data.executionPlan.length - 3} more activities.
` : ''}
`;
previewContent.innerHTML = html;
}
// --- Data Aggregation ---
function collectAllData() {
const data = {
// Tab 0
campaignName: campaignNameInput.value.trim(),
brandClient: brandClientInput.value.trim(),
startDate: startDateInput.value,
targetAudience: targetAudienceTextarea.value.trim(),
primaryObjectives: primaryObjectivesTextarea.value.trim(),
// Tab 1
coreMessage: coreMessageTextarea.value.trim(),
keyThemes: keyThemesInput.value.split(',').map(s => s.trim()).filter(s => s),
tone: toneSelect.value,
primaryChannel: channelSelect.value,
// Tab 2 (already in state: executionPlan)
executionPlan: executionPlan,
// Tab 3
totalBudget: parseFloat(totalBudgetInput.value) || 0,
actualSpend: parseFloat(actualSpendInput.value) || 0,
kpis: kpisTextarea.value.split(',').map(s => s.trim()).filter(s => s)
};
// Set status for PDF only
data.overallStatus = container.querySelector('#prcpo-tab-0 #prcpo-overall-status').value;
return data;
}
// --- PDF Generation (Spec II.C) ---
function downloadPDF() {
if (typeof window.jspdf === 'undefined' || typeof window.jspdf.jsPDF === 'undefined') {
alert('Error: jsPDF library not loaded.'); return;
}
const data = collectAllData();
const { jsPDF } = window.jspdf;
const doc = new jsPDF();
const margin = 40;
const pageWidth = doc.internal.pageSize.getWidth();
let currentY = margin;
// --- Helper function to add space and break pages ---
function checkY(needed) {
if (currentY + needed > doc.internal.pageSize.getHeight() - margin) {
doc.addPage();
currentY = margin;
}
}
// --- Section 1: Header ---
doc.setFontSize(22);
doc.setFont('helvetica', 'bold');
doc.setTextColor(124, 58, 237); // violet-600
doc.text(data.campaignName || 'PR CAMPAIGN PLAN', pageWidth / 2, currentY, { align: 'center' });
currentY += 15;
doc.setFontSize(12);
doc.setFont('helvetica', 'normal');
doc.setTextColor(51, 65, 85);
doc.text(`Client: ${data.brandClient || 'N/A'} | Report Date: ${new Date().toLocaleDateString()}`, pageWidth / 2, currentY, { align: 'center' });
currentY += 25;
// --- Section 2: Overview ---
doc.setFontSize(14);
doc.setFont('helvetica', 'bold');
doc.text("1. Overview & Goals", margin, currentY);
currentY += 15;
const overviewData = [
["Campaign Start Date:", data.startDate || 'N/A', "Overall Status:", data.overallStatus],
["Tone / Voice:", data.tone, "Primary Channel:", data.primaryChannel],
["Target Audience:", data.targetAudience],
["Primary Objectives:", data.primaryObjectives]
];
doc.autoTable({
startY: currentY,
body: overviewData,
theme: 'striped',
styles: { fontSize: 9, cellPadding: 3 },
columnStyles: { 0: { fontStyle: 'bold', fillColor: [243, 232, 255] }, 2: { fontStyle: 'bold', fillColor: [243, 232, 255] } }
});
currentY = doc.autoTable.previous.finalY + 15;
// --- Section 3: Strategy ---
checkY(40);
doc.setFontSize(14);
doc.setFont('helvetica', 'bold');
doc.text("2. Strategy & Messaging", margin, currentY);
currentY += 15;
doc.setFontSize(10);
doc.setFont('helvetica', 'bold');
doc.text("Core Message:", margin, currentY);
currentY += 12;
doc.setFont('helvetica', 'normal');
let messageLines = doc.splitTextToSize(data.coreMessage || 'N/A', pageWidth - margin * 2);
doc.text(messageLines, margin, currentY);
currentY += messageLines.length * 12 + 8;
doc.setFont('helvetica', 'bold');
doc.text("Key Themes:", margin, currentY);
currentY += 12;
doc.setFont('helvetica', 'normal');
doc.text(data.keyThemes.join(' • ') || 'N/A', margin, currentY);
currentY += 15;
// --- Section 4: Execution Plan ---
checkY(40);
doc.setFontSize(14);
doc.setFont('helvetica', 'bold');
doc.text("3. Execution Plan & Timeline", margin, currentY);
currentY += 15;
if (data.executionPlan.length > 0) {
const executionTableData = data.executionPlan.map(item => [
item.activity,
item.target || 'N/A',
item.date,
item.status
]);
doc.autoTable({
startY: currentY,
head: [['Activity', 'Target/Channel', 'Date Due', 'Status']],
body: executionTableData,
theme: 'striped',
headStyles: { fillColor: [124, 58, 237] }, // violet-600
styles: { fontSize: 8 }
});
currentY = doc.autoTable.previous.finalY + 15;
} else {
doc.setFontSize(10);
doc.setFont('helvetica', 'italic');
doc.text("No execution activities are currently planned.", margin, currentY);
currentY += 15;
}
// --- Section 5: Budget & Evaluation ---
checkY(80);
doc.setFontSize(14);
doc.setFont('helvetica', 'bold');
doc.text("4. Evaluation & Budget", margin, currentY);
currentY += 15;
const financeData = [
["Budget Metrics", "Value"],
["Total Allocated Budget:", formatCurrency(data.totalBudget)],
["Actual Spend to Date:", formatCurrency(data.actualSpend)],
["Remaining Budget:", formatCurrency(data.totalBudget - data.actualSpend)],
];
doc.autoTable({
startY: currentY,
body: financeData,
theme: 'plain',
styles: { fontSize: 10, cellPadding: 3, textColor: [33, 41, 53] },
columnStyles: { 0: { fontStyle: 'bold', cellWidth: 150 } }
});
currentY = doc.autoTable.previous.finalY + 15;
doc.setFontSize(10);
doc.setFont('helvetica', 'bold');
doc.text("Key Performance Indicators (KPIs):", margin, currentY);
currentY += 12;
doc.setFont('helvetica', 'normal');
let kpiLines = doc.splitTextToSize(data.kpis.join('; ') || 'N/A', pageWidth - margin * 2);
doc.text(kpiLines, margin, currentY);
currentY += kpiLines.length * 12 + 10;
doc.save('pr-campaign-plan.pdf');
}
// --- Initial Load and Event Listeners ---
addActivityBtn.addEventListener('click', addActivity);
pdfBtn.addEventListener('click', downloadPDF);
// Set default date
startDateInput.value = new Date().toISOString().split('T')[0];
// Initial render
renderExecutionPlan();
prcpoShowTab(0);
});