`;
document.body.appendChild(modal);
return;
}
compCount++;
const compId = `comp-${compCount}`;
const compElement = document.createElement('div');
compElement.id = compId;
compElement.className = 'comp-item border border-gray-200 rounded-lg p-4 relative';
const sampleData = [
{ address: '456 Oak Ave, Anytown, CA', price: '750000', sqft: '1900', beds: '4', baths: '2.5', year: '2005' },
{ address: '789 Pine Ln, Anytown, CA', price: '680000', sqft: '1750', beds: '3', baths: '2', year: '1990' },
{ address: '101 Maple Dr, Anytown, CA', price: '715000', sqft: '1850', beds: '3', baths: '2.5', year: '2000' },
{ address: '212 Birch Rd, Anytown, CA', price: '780000', sqft: '2000', beds: '4', baths: '3', year: '2010' },
{ address: '333 Cedar Ct, Anytown, CA', price: '650000', sqft: '1600', beds: '3', baths: '2', year: '1985' }
];
const data = useSampleData && compCount <= sampleData.length ? sampleData[compCount - 1] : { address: '', price: '', sqft: '', beds: '', baths: '', year: '' };
compElement.innerHTML = `
Comparable Property #${compCount}
`;
if (compsContainer) compsContainer.appendChild(compElement);
compElement.querySelector('.remove-comp-btn').addEventListener('click', removeComparableProperty);
compElement.querySelectorAll('.comp-input').forEach(input => {
input.addEventListener('input', calculateValuation);
});
if (addCompBtn) addCompBtn.disabled = (compCount >= MAX_COMPS);
calculateValuation();
};
const removeComparableProperty = (e) => {
const targetId = e.currentTarget.getAttribute('data-target');
const elementToRemove = document.getElementById(targetId);
if (elementToRemove) {
elementToRemove.remove();
compCount--;
if (addCompBtn) addCompBtn.disabled = (compCount >= MAX_COMPS);
calculateValuation();
}
};
// --- VALUATION CALCULATION LOGIC ---
const calculateValuation = () => {
const subject = {
sqft: parseFloat(document.getElementById('prop-sqft').value) || 0,
bedrooms: parseFloat(document.getElementById('prop-bedrooms').value) || 0,
bathrooms: parseFloat(document.getElementById('prop-bathrooms').value) || 0,
yearBuilt: parseInt(document.getElementById('prop-year-built').value) || new Date().getFullYear(),
condition: parseInt(document.getElementById('prop-condition').value) || 3,
};
const comps = [];
document.querySelectorAll('.comp-item').forEach(item => {
const price = parseFloat(item.querySelector('[data-field="price"]').value) || 0;
const sqft = parseFloat(item.querySelector('[data-field="sqft"]').value) || 0;
if (price > 0 && sqft > 0) {
comps.push({
price,
sqft,
pricePerSqft: price / sqft,
bedrooms: parseFloat(item.querySelector('[data-field="beds"]').value) || 0,
bathrooms: parseFloat(item.querySelector('[data-field="baths"]').value) || 0,
yearBuilt: parseInt(item.querySelector('[data-field="year"]').value) || new Date().getFullYear(),
});
}
});
if (comps.length === 0) {
updateUI(0, { base: 0, condition: 0, age: 0, features: 0 });
return;
}
const avg = {
pricePerSqft: comps.reduce((sum, c) => sum + c.pricePerSqft, 0) / comps.length,
bedrooms: comps.reduce((sum, c) => sum + c.bedrooms, 0) / comps.length,
bathrooms: comps.reduce((sum, c) => sum + c.bathrooms, 0) / comps.length,
yearBuilt: comps.reduce((sum, c) => sum + c.yearBuilt, 0) / comps.length,
};
const baseValue = subject.sqft * avg.pricePerSqft;
const conditionAdjustmentValue = baseValue * (subject.condition - 3) * 0.05;
const ageDifference = subject.yearBuilt - avg.yearBuilt;
const ageAdjustmentValue = baseValue * (ageDifference * -0.003);
const bedValue = 15000;
const bathValue = 10000;
const bedDifference = subject.bedrooms - avg.bedrooms;
const bathDifference = subject.bathrooms - avg.bathrooms;
const featureAdjustmentValue = (bedDifference * bedValue) + (bathDifference * bathValue);
const finalValue = baseValue + conditionAdjustmentValue + ageAdjustmentValue + featureAdjustmentValue;
updateUI(finalValue, {
base: baseValue,
condition: conditionAdjustmentValue,
age: ageAdjustmentValue,
features: featureAdjustmentValue
});
};
// --- UI UPDATE & FORMATTING ---
const formatCurrency = (value) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(value);
};
const updateUI = (finalValue, breakdown) => {
document.getElementById('estimated-value').textContent = formatCurrency(finalValue);
document.getElementById('breakdown-base').textContent = formatCurrency(breakdown.base);
document.getElementById('breakdown-condition').textContent = formatCurrency(breakdown.condition);
document.getElementById('breakdown-age').textContent = formatCurrency(breakdown.age);
document.getElementById('breakdown-features').textContent = formatCurrency(breakdown.features);
};
// --- PDF GENERATION (REVISED AND IMPROVED) ---
const generatePDF = async () => {
if (typeof window.jspdf === 'undefined' || typeof window.html2canvas === 'undefined') {
console.error('PDF generation libraries not loaded.');
return;
}
const { jsPDF } = window.jspdf;
const pdfReportEl = document.createElement('div');
// Style the report container for off-screen rendering
Object.assign(pdfReportEl.style, {
position: 'absolute',
left: '-9999px',
top: '0',
width: '800px',
backgroundColor: '#ffffff',
padding: '40px',
fontFamily: 'Arial, sans-serif',
color: '#333',
});
// --- GATHER DATA FOR PDF ---
const subject = {
address: document.getElementById('prop-address').value,
type: document.getElementById('prop-type').value,
sqft: document.getElementById('prop-sqft').value,
lotSize: document.getElementById('prop-lot-size').value,
bedrooms: document.getElementById('prop-bedrooms').value,
bathrooms: document.getElementById('prop-bathrooms').value,
yearBuilt: document.getElementById('prop-year-built').value,
condition: conditionMap[document.getElementById('prop-condition').value],
};
const valuation = {
final: document.getElementById('estimated-value').textContent,
base: document.getElementById('breakdown-base').textContent,
condition: document.getElementById('breakdown-condition').textContent,
age: document.getElementById('breakdown-age').textContent,
features: document.getElementById('breakdown-features').textContent,
};
const comps = Array.from(document.querySelectorAll('.comp-item')).map(item => ({
address: item.querySelector('[data-field="address"]').value,
price: formatCurrency(parseFloat(item.querySelector('[data-field="price"]').value) || 0),
sqft: item.querySelector('[data-field="sqft"]').value,
beds: item.querySelector('[data-field="beds"]').value,
baths: item.querySelector('[data-field="baths"]').value,
year: item.querySelector('[data-field="year"]').value,
}));
// --- BUILD HTML FOR PDF ---
let compsHtml = '';
if (comps.length > 0) {
compsHtml = `
Comparable Properties Summary
Address
Sale Price
Sq.Ft.
Beds
Baths
Year
${comps.map(c => `
${c.address}
${c.price}
${c.sqft}
${c.beds}
${c.baths}
${c.year}
`).join('')}
`;
}
pdfReportEl.innerHTML = `
Property Valuation Report
Generated on: ${new Date().toLocaleDateString()}
Subject Property
${subject.address}
Type: ${subject.type}
Sq. Footage: ${subject.sqft} sq. ft.
Lot Size: ${subject.lotSize} acres
Bedrooms: ${subject.bedrooms}
Bathrooms: ${subject.bathrooms}
Year Built: ${subject.yearBuilt}
Condition: ${subject.condition}
Valuation Summary
${valuation.final}
Base Value: ${valuation.base}
Condition Adj: ${valuation.condition}
Age Adj: ${valuation.age}
Features Adj: ${valuation.features}
${compsHtml}
`;
document.body.appendChild(pdfReportEl);
try {
const canvas = await html2canvas(pdfReportEl, { scale: 2, useCORS: true });
const imgData = canvas.toDataURL('image/png');
const pdf = new jsPDF({
orientation: 'portrait',
unit: 'px',
format: [canvas.width, canvas.height]
});
pdf.addImage(imgData, 'PNG', 0, 0, canvas.width, canvas.height);
pdf.save(`Property-Valuation-${subject.address.replace(/[^a-zA-Z0-9]/g, '_')}.pdf`);
} catch (error) {
console.error("Error generating PDF:", error);
} finally {
document.body.removeChild(pdfReportEl);
}
};
// --- START THE APP ---
initialize();
});