${overallRiskPercentage.toFixed(1)}%
Risk Factor Summary:
`;
for (const categoryKey in riskCategories) {
const categoryData = riskCategories[categoryKey];
const categoryName = categoryKey.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase());
const categoryScorePercentage = categoryData.maxScore > 0 ? (categoryData.currentScore / categoryData.maxScore) * 100 : 0;
const categoryRiskLevel = getRiskLevel(categoryScorePercentage);
html += `
${categoryName} (Weight: ${categoryData.weight*100}%) - Risk: ${categoryRiskLevel.label}
`;
categoryData.factors.forEach(factor => {
html += `- ${factor.label}: ${factor.value} (Risk Points: ${factor.risk})
`;
});
html += `
`;
}
resultsOutputElem.innerHTML = html;
navigateToTab('tabResults');
}
function generatePdf() {
const pdf = new jsPDF();
const today = new Date().toLocaleDateString();
const projectName = getElementValue('projectName', false) || "N/A";
pdf.setFontSize(18);
pdf.setTextColor(59, 130, 246); // Tailwind blue-600
pdf.text("Land Development Investment Risk Report", 105, 20, null, null, "center");
pdf.setFontSize(10);
pdf.setTextColor(100);
pdf.text(`Project: ${projectName}`, 14, 30);
pdf.text(`Generated on: ${today}`, 105, 30, null, null, "center");
let yPos = 40;
const overallRiskPercentage = calculateRiskScore(); // Recalculate to ensure data is fresh
const riskLevel = getRiskLevel(overallRiskPercentage);
pdf.setFontSize(12);
// Removed the problematic setFillColor line that was causing the error.
// The text color for the summary block.
pdf.setTextColor(riskLevel.color === "#facc15" ? 0 : 255); // Black for yellow, white for others
// A simple colored block for summary
const rectColor = riskLevel.color; // Use hex color directly
// Parse the hex color string into R, G, B components
const parts = rectColor.substring(1).match(/.{1,2}/g).map(hex => parseInt(hex, 16));
pdf.setFillColor(parts[0], parts[1], parts[2]); // Set fill color using RGB components
pdf.rect(14, yPos, 182, 20, 'F'); // Draw the filled rectangle
pdf.setFontSize(14);
// Ensure text is visible on the colored rectangle
if (riskLevel.color === "#facc15") { // If color is yellow, use black text
pdf.setTextColor(0,0,0);
} else { // For other colors (greens, oranges, reds), use white text
pdf.setTextColor(255,255,255);
}
pdf.text(`Overall Risk: ${overallRiskPercentage.toFixed(1)}% - ${riskLevel.label}`, 105, yPos + 8, null, null, "center");
yPos += 25;
pdf.setTextColor(0); // Reset text color to black for subsequent text
for (const categoryKey in riskCategories) {
const categoryData = riskCategories[categoryKey];
const categoryName = categoryKey.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase());
const categoryScorePercentage = categoryData.maxScore > 0 ? (categoryData.currentScore / categoryData.maxScore) * 100 : 0;
const categoryRiskInfo = getRiskLevel(categoryScorePercentage);
pdf.setFontSize(12);
pdf.setTextColor(59, 130, 246);
pdf.text(`${categoryName} (Weight: ${categoryData.weight*100}%) - Risk: ${categoryRiskInfo.label}`, 14, yPos);
yPos += 7;
const tableData = categoryData.factors.map(f => [f.label, f.value, f.risk.toString()]);
pdf.autoTable({
startY: yPos,
head: [['Factor', 'Selected Value', 'Risk Points']],
body: tableData,
theme: 'grid',
headStyles: { fillColor: [229, 231, 235], textColor: [55, 65, 81] }, // gray-200, gray-700
styles: { fontSize: 9, cellPadding: 2 },
margin: { left: 14, right: 14 }
});
yPos = pdf.lastAutoTable.finalY + 8;
}
// Add informational inputs to PDF
yPos += 5;
pdf.setFontSize(12);
pdf.setTextColor(59, 130, 246);
pdf.text("Additional Project Information", 14, yPos);
yPos += 7;
const additionalInfo = [
["Land Cost ($)", getElementValue('landCost').toLocaleString()],
["Land Size (Acres)", getElementValue('landSize').toLocaleString()],
["Est. Dev Costs (Excl. Land) ($)", getElementValue('devCosts').toLocaleString()]
];
pdf.autoTable({
startY: yPos,
head: [['Item', 'Value']],
body: additionalInfo,
theme: 'grid',
headStyles: { fillColor: [229, 231, 235], textColor: [55, 65, 81] },
styles: { fontSize: 9, cellPadding: 2 },
margin: { left: 14, right: 14 }
});
pdf.save(`Land_Dev_Risk_Report_${projectName.replace(/[^a-zA-Z0-9]/g, '_')}.pdf`);
}
// Event Listeners & Initialization
document.addEventListener('DOMContentLoaded', () => {
// Assign elements
projectNameElem = document.getElementById('projectName');
landCostElem = document.getElementById('landCost');
landSizeElem = document.getElementById('landSize');
zoningStatusElem = document.getElementById('zoningStatus');
environmentalConcernsElem = document.getElementById('environmentalConcerns');
siteConditionsElem = document.getElementById('siteConditions');
marketDemandElem = document.getElementById('marketDemand');
economicOutlookElem = document.getElementById('economicOutlook');
infraAvailabilityElem = document.getElementById('infraAvailability');
roadAccessElem = document.getElementById('roadAccess');
proximityAmenitiesElem = document.getElementById('proximityAmenities');
devCostsElem = document.getElementById('devCosts');
fundingSourceElem = document.getElementById('fundingSource');
contingencyBudgetElem = document.getElementById('contingencyBudget');
devTimelineElem = document.getElementById('devTimeline');
teamExperienceElem = document.getElementById('teamExperience');
profitMarginElem = document.getElementById('profitMargin');
resultsOutputElem = document.getElementById('resultsOutput');
calculateRiskButtonElem = document.getElementById('calculateRiskButton');
downloadPdfButtonElem = document.getElementById('downloadPdfButton');
// Null checks for critical elements
if (!calculateRiskButtonElem) {
console.error("Calculate Risk button not found!");
} else {
calculateRiskButtonElem.addEventListener('click', displayResults);
}
if (!downloadPdfButtonElem) {
console.error("Download PDF button not found!");
} else {
downloadPdfButtonElem.addEventListener('click', generatePdf);
}
// Initialize with the first tab active
const firstTabButton = document.querySelector('.tab-button');
if (firstTabButton) {
changeTab({ currentTarget: firstTabButton }, 'tabProjectLand');
}
});