Prepared by: ${escapeHTML(data.salesRep)} | Period: ${escapeHTML(data.period)}
1. Forecast Summary
Total Pipeline Value:
${formatCurrency(totals.totalValue)}
TOTAL EXPECTED REVENUE:
${formatCurrency(totals.totalExpected)}
2. Key Assumptions
${escapeHTML(data.assumptions).replace(/\n/g, '
')}
3. Pipeline Details
| Client / Opportunity |
Deal Value ($) |
Probability (%) |
Expected Value ($) |
Target Close |
${itemRowsHTML.length > 0 ? itemRowsHTML : '| No items logged. |
'}
`;
};
const downloadTxt = () => {
const data = getReportData();
const totals = data.totals;
let content = `SALES FORECAST REPORT: ${data.title.toUpperCase()}\n`;
content += "========================================================\n";
content += `Period: ${data.period}\n`;
content += `Sales Rep: ${data.salesRep}\n\n`;
content += "SUMMARY\n";
content += "--------------------------------------------------------\n";
content += `Total Pipeline Value: ${formatCurrency(totals.totalValue)}\n`;
content += `TOTAL EXPECTED REVENUE: ${formatCurrency(totals.totalExpected)}\n\n`;
content += "KEY ASSUMPTIONS\n";
content += "--------------------------------------------------------\n";
content += `${data.assumptions}\n\n`;
content += "PIPELINE DETAILS\n";
content += "--------------------------------------------------------\n";
content += "Client / Opportunity | Value ($) | Prob. (%) | Expected ($) | Close Date\n";
content += "------------------------------------------------------------------------------------------------------------------------\n";
data.items.forEach(item => {
const expectedValue = item.value * (item.probability / 100);
content += `${item.description.padEnd(25).substring(0, 25)} | ${item.value.toFixed(2).padEnd(10)} | ${item.probability.toString().padEnd(9)} | ${expectedValue.toFixed(2).padEnd(12)} | ${item.close}\n`;
});
content += "------------------------------------------------------------------------------------------------------------------------\n";
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `forecast_${data.title.replace(/ /g, '_')}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(a.href);
};
const downloadPDF = () => {
if (typeof window.jspdf === 'undefined' || typeof window.jspdf.jsPDF === 'undefined') {
alert('Error: jsPDF library not loaded.');
return;
}
const { jsPDF } = window.jspdf;
const doc = new jsPDF('l', 'mm', 'a4'); // Landscape for better table fit
const data = getReportData();
const totals = data.totals;
const margin = 15;
let yPos = 20;
const pageWidth = doc.internal.pageSize.getWidth();
const usableWidth = pageWidth - margin * 2;
const addTitle = (text, size, color, style = 'bold') => {
if (yPos > 190) { doc.addPage('l'); yPos = 20; }
doc.setFontSize(size);
doc.setFont(undefined, style);
doc.setTextColor(color[0], color[1], color[2]);
doc.text(text, margin, yPos);
yPos += size / 2 + 3;
};
const addText = (text, size = 10, style = 'normal', indent = 0) => {
doc.setFontSize(size);
doc.setFont(undefined, style);
doc.setTextColor(52, 73, 94);
const lines = doc.splitTextToSize(text, usableWidth - indent);
if (lines.length * 5 + yPos > 190) { doc.addPage('l'); yPos = 20; }
doc.text(lines, margin + indent, yPos);
yPos += (lines.length * 5) + 3;
};
// 1. Header
doc.setFontSize(20);
doc.setFont(undefined, 'bold');
doc.setTextColor(44, 62, 80);
doc.text(`Sales Forecast Report`, pageWidth / 2, yPos, { align: 'center' });
yPos += 8;
doc.setFontSize(14);
doc.setTextColor(46, 204, 113);
doc.text(data.title, pageWidth / 2, yPos, { align: 'center' });
yPos += 10;
doc.setFontSize(10);
doc.setTextColor(107, 114, 128);
doc.text(`Prepared by: ${data.salesRep} | Period: ${data.period}`, pageWidth / 2, yPos, { align: 'center' });
yPos += 10;
// 2. Forecast Summary
addTitle("1. Forecast Summary", 12, [46, 204, 113]);
const totalsX = pageWidth - margin - 60; // Start X for totals column (wider)
const addTotalLine = (label, value, isGrand = false) => {
doc.setFontSize(isGrand ? 12 : 10);
doc.setFont(undefined, isGrand ? 'bold' : 'normal');
doc.setTextColor(isGrand ? 46 : 52, isGrand ? 204 : 73, isGrand ? 113 : 94);
if (isGrand) {
doc.setDrawColor(44, 62, 80);
doc.setLineWidth(0.5);
doc.line(totalsX - 10, yPos - 1, pageWidth - margin, yPos - 1);
yPos += 2;
}
doc.text(label, totalsX, yPos, { align: 'left' });
doc.text(formatCurrency(value), pageWidth - margin, yPos, { align: 'right' });
yPos += 5;
};
addTotalLine("Total Pipeline Value:", totals.totalValue);
addTotalLine("TOTAL EXPECTED REVENUE:", totals.totalExpected, true);
yPos += 5;
// 3. Key Assumptions
addTitle("2. Key Assumptions", 12, [46, 204, 113]);
addText(data.assumptions, 10, 'normal', 5);
yPos += 3;
// 4. Pipeline Details Table
addTitle("3. Pipeline Details", 12, [46, 204, 113]);
const itemHead = [['Client / Opportunity', 'Deal Value ($)', 'Probability (%)', 'Expected Value ($)', 'Target Close']];
const itemBody = data.items.map(item => [
item.description,
formatCurrency(item.value),
`${item.probability}%`,
formatCurrency(item.value * (item.probability / 100)),
item.close
]);
doc.autoTable({
startY: yPos,
head: itemHead,
body: itemBody,
theme: 'grid',
styles: { fontSize: 9, cellPadding: 2, textColor: [52, 73, 94] },
headStyles: { fillColor: [46, 204, 113], textColor: [255, 255, 255] },
columnStyles: {
0: { cellWidth: 70, fontStyle: 'normal' },
1: { halign: 'right' },
2: { halign: 'center', cellWidth: 20 },
3: { halign: 'right', fontStyle: 'bold' },
4: { halign: 'center', cellWidth: 25 }
},
margin: { left: margin, right: margin }
});
// yPos is updated automatically by autoTable
doc.save(`sales_forecast_${data.title.replace(/ /g, '_')}.pdf`);
};
// --- Event Listeners ---
// Tab Buttons
tabButtons.forEach((btn, index) => {
btn.addEventListener('click', () => showTab(index + 1));
});
// Next/Prev Navigation
nextBtn.addEventListener('click', () => showTab(currentTab + 1));
prevBtn.addEventListener('click', () => showTab(currentTab - 1));
// Tab 2 Actions (Add, Remove, Edit)
itemInputs.addBtn.addEventListener('click', addItem);
itemInputs.tbody.addEventListener('click', (e) => {
if (e.target.dataset.removeId) {
removeItem(parseInt(e.target.dataset.removeId));
}
});
itemInputs.tbody.addEventListener('blur', (e) => {
if (e.target.tagName === 'TD' && e.target.isContentEditable) {
const id = parseInt(e.target.dataset.id);
const field = e.target.dataset.field;
updateItem(id, field, e.target.textContent);
}
}, true);
// Ensure inputs that affect calculations trigger a render
Object.values(itemInputs).forEach(input => {
if (input.tagName === 'INPUT' || input.tagName === 'SELECT') {
input.addEventListener('change', renderItems);
}
});
// Tab 3 Actions
downloadPdfBtn.addEventListener('click', downloadPDF);
downloadTxtBtn.addEventListener('click', downloadTxt);
// --- Initialization ---
loadSampleData();
showTab(1);
});