export function exteriorPaintingCalculator(form: FormTs) {
// Base rates per sq ft by siding type
const sidingRates: Record<string, number> = {
'wood': 3.50,
'vinyl': 2.50,
'stucco': 3.00,
'brick': 4.00,
'aluminum': 2.75,
'fiber-cement': 3.25,
'cedar-shake': 4.50
};
// Stories multiplier (labor difficulty)
const storyMultipliers: Record<string, number> = {
'1': 1.0,
'2': 1.20,
'3': 1.45,
'4+': 1.75
};
// Paint quality multipliers
const paintQualityMultipliers: Record<string, number> = {
'standard': 1.0,
'premium': 1.25,
'luxury': 1.50
};
// Condition multipliers
const conditionMultipliers: Record<string, number> = {
'good': 0.9,
'fair': 1.0,
'poor': 1.25,
'severe': 1.50
};
form.addRow(row => {
row.addTextPanel('header', {
computedValue: () => 'Exterior Painting Estimate',
customStyles: { 'font-size': '1.5rem', 'font-weight': '600', 'color': '#1e293b' }
});
});
form.addSpacer({ height: 20 });
// Service Location Section
const locationSection = form.addSubform('serviceLocation', { title: '📍 Project Location' });
locationSection.addRow(row => {
row.addAddress('propertyAddress', {
label: 'Property Address',
placeholder: 'Enter the property address...',
showMap: true,
showDistance: true,
referenceAddress: {
formattedAddress: 'Service Center, Denver, CO',
coordinates: { lat: 39.7392, lng: -104.9903 }
},
restrictToCountries: ['US', 'CA'],
distanceUnit: 'miles',
isRequired: true
});
});
locationSection.addRow(row => {
row.addTextPanel('serviceAreaInfo', {
computedValue: () => {
const addressField = locationSection.address('propertyAddress');
const miles = addressField?.distance();
if (miles == null) return '📍 Enter address to check service area';
if (miles <= 25) return '📍 Within service area - Standard pricing';
if (miles <= 50) return '📍 Extended area - $100 travel fee';
if (miles <= 75) return '📍 Remote area - $175 travel fee';
return '📍 Long distance - Custom quote required';
},
customStyles: { 'font-size': '0.9rem', 'color': '#c2410c', 'background': '#fff7ed', 'padding': '10px', 'border-radius': '6px' }
});
});
// Property Details Section
const propertySection = form.addSubform('propertyDetails', { title: '🏠 Property Details' });
propertySection.addRow(row => {
row.addDropdown('propertyType', {
label: 'Property Type',
options: [
{ id: 'single-family', name: 'Single Family Home' },
{ id: 'townhouse', name: 'Townhouse' },
{ id: 'duplex', name: 'Duplex' },
{ id: 'multi-unit', name: 'Multi-Unit Building' },
{ id: 'commercial', name: 'Commercial Building' }
],
defaultValue: 'single-family',
isRequired: true
}, '1fr');
row.addDropdown('stories', {
label: 'Number of Stories',
options: [
{ id: '1', name: '1 Story' },
{ id: '2', name: '2 Stories (+20%)' },
{ id: '3', name: '3 Stories (+45%)' },
{ id: '4+', name: '4+ Stories (+75%)' }
],
defaultValue: '2',
isRequired: true
}, '1fr');
});
propertySection.addRow(row => {
row.addSlider('totalSqFt', {
label: 'Total Exterior Surface Area',
min: 500,
max: 10000,
step: 100,
defaultValue: 2500,
showValue: true,
unit: 'sq ft'
}, '1fr');
});
propertySection.addRow(row => {
row.addTextPanel('sqftHelp', {
computedValue: () => 'Tip: Average home has 1,500-3,000 sq ft of exterior paintable surface. Multiply home sq ft by ~1.5 for estimate.',
customStyles: { 'font-size': '0.85rem', 'color': '#64748b', 'font-style': 'italic' }
});
});
// Siding Details Section
const sidingSection = form.addSubform('sidingDetails', { title: '🧱 Siding Material' });
sidingSection.addRow(row => {
row.addDropdown('primarySiding', {
label: 'Primary Siding Type',
options: [
{ id: 'wood', name: 'Wood Siding ($3.50/sq ft)' },
{ id: 'vinyl', name: 'Vinyl Siding ($2.50/sq ft)' },
{ id: 'stucco', name: 'Stucco ($3.00/sq ft)' },
{ id: 'brick', name: 'Brick ($4.00/sq ft)' },
{ id: 'aluminum', name: 'Aluminum ($2.75/sq ft)' },
{ id: 'fiber-cement', name: 'Fiber Cement/Hardie ($3.25/sq ft)' },
{ id: 'cedar-shake', name: 'Cedar Shake ($4.50/sq ft)' }
],
defaultValue: 'wood',
isRequired: true
}, '1fr');
});
sidingSection.addRow(row => {
row.addRadioButton('currentCondition', {
label: 'Current Exterior Condition',
options: [
{ id: 'good', name: 'Good - Minor prep needed, paint in decent shape (-10%)' },
{ id: 'fair', name: 'Fair - Standard prep, some peeling/fading' },
{ id: 'poor', name: 'Poor - Significant prep, peeling, mildew (+25%)' },
{ id: 'severe', name: 'Severe - Major repairs needed, extensive prep (+50%)' }
],
defaultValue: 'fair',
orientation: 'vertical'
});
});
// Paint Options Section
const paintSection = form.addSubform('paintOptions', { title: '🎨 Paint Selection' });
paintSection.addRow(row => {
row.addRadioButton('paintQuality', {
label: 'Paint Quality',
options: [
{ id: 'standard', name: 'Standard - Good quality, 10-year warranty' },
{ id: 'premium', name: 'Premium - Better durability, 15-year warranty (+25%)' },
{ id: 'luxury', name: 'Luxury - Best protection, 25-year warranty (+50%)' }
],
defaultValue: 'standard',
orientation: 'vertical'
});
});
paintSection.addRow(row => {
row.addDropdown('coats', {
label: 'Number of Coats',
options: [
{ id: '1', name: '1 Coat (touch-up only)' },
{ id: '2', name: '2 Coats (standard)' },
{ id: '3', name: '3 Coats (color change/dark colors)' }
],
defaultValue: '2'
}, '1fr');
row.addInteger('colors', {
label: 'Number of Colors',
min: 1,
max: 10,
defaultValue: 2,
placeholder: 'Body + trim'
}, '1fr');
});
// Additional Areas Section
const areasSection = form.addSubform('additionalAreas', { title: '📍 Additional Areas' });
areasSection.addRow(row => {
row.addCheckbox('trim', {
label: 'Trim & Fascia (included in estimate)',
defaultValue: true,
isReadOnly: true
}, '1fr');
row.addCheckbox('shutters', {
label: 'Shutters (+$25/pair)',
defaultValue: false
}, '1fr');
});
areasSection.addRow(row => {
row.addCheckbox('doors', {
label: 'Doors (+$75/door)',
defaultValue: true
}, '1fr');
row.addCheckbox('garage', {
label: 'Garage Door(s) (+$150/door)',
defaultValue: true
}, '1fr');
});
areasSection.addRow(row => {
row.addCheckbox('deck', {
label: 'Deck/Porch Staining (+$4/sq ft)',
defaultValue: false
}, '1fr');
row.addCheckbox('fence', {
label: 'Fence Painting (+$3/linear ft)',
defaultValue: false
}, '1fr');
});
areasSection.addRow(row => {
row.addInteger('shutterPairs', {
label: 'Number of Shutter Pairs',
min: 1,
max: 20,
defaultValue: 6,
isVisible: () => areasSection.checkbox('shutters')?.value() === true
}, '1fr');
row.addInteger('doorCount', {
label: 'Number of Entry Doors',
min: 1,
max: 10,
defaultValue: 2,
isVisible: () => areasSection.checkbox('doors')?.value() === true
}, '1fr');
});
areasSection.addRow(row => {
row.addInteger('garageDoors', {
label: 'Number of Garage Doors',
min: 1,
max: 5,
defaultValue: 2,
isVisible: () => areasSection.checkbox('garage')?.value() === true
}, '1fr');
});
areasSection.addRow(row => {
row.addSlider('deckSqFt', {
label: 'Deck/Porch Area',
min: 50,
max: 2000,
step: 50,
defaultValue: 300,
showValue: true,
unit: 'sq ft',
isVisible: () => areasSection.checkbox('deck')?.value() === true
}, '1fr');
row.addSlider('fenceLength', {
label: 'Fence Length',
min: 20,
max: 500,
step: 10,
defaultValue: 100,
showValue: true,
unit: 'linear ft',
isVisible: () => areasSection.checkbox('fence')?.value() === true
}, '1fr');
});
// Prep Work Section
const prepSection = form.addSubform('prepWork', { title: '🔧 Preparation Services' });
prepSection.addRow(row => {
row.addCheckbox('powerWashing', {
label: 'Power Washing (+$0.15/sq ft)',
defaultValue: true
}, '1fr');
row.addCheckbox('scraping', {
label: 'Heavy Scraping/Sanding (+$0.40/sq ft)',
defaultValue: false
}, '1fr');
});
prepSection.addRow(row => {
row.addCheckbox('caulking', {
label: 'Caulking Windows/Doors (+$8/opening)',
defaultValue: true
}, '1fr');
row.addCheckbox('priming', {
label: 'Full Primer Coat (+$0.50/sq ft)',
defaultValue: false
}, '1fr');
});
prepSection.addRow(row => {
row.addCheckbox('repairs', {
label: 'Minor Wood Repairs (+$300 flat)',
defaultValue: false
}, '1fr');
row.addCheckbox('leadPaint', {
label: 'Lead Paint Testing/Mitigation (+$500)',
defaultValue: false
}, '1fr');
});
prepSection.addRow(row => {
row.addInteger('openings', {
label: 'Windows & Doors to Caulk',
min: 1,
max: 50,
defaultValue: 15,
isVisible: () => prepSection.checkbox('caulking')?.value() === true
}, '1fr');
});
form.addSpacer({ height: 20, showLine: true, lineStyle: 'dashed' });
// Helper to calculate travel fee
const getTravelFee = () => {
const addressField = locationSection.address('propertyAddress');
const miles = addressField?.distance();
if (miles == null || miles <= 25) return 0;
if (miles <= 50) return 100;
if (miles <= 75) return 175;
return 250 + Math.floor((miles - 75) / 25) * 50;
};
// Price Summary Section
const summarySection = form.addSubform('summary', { title: '💰 Your Estimate', isCollapsible: false });
const calculateBasePrice = () => {
const sqft = propertySection.slider('totalSqFt')?.value() || 2500;
const siding = sidingSection.dropdown('primarySiding')?.value() || 'wood';
const rate = sidingRates[siding] || 3.50;
return sqft * rate;
};
summarySection.addRow(row => {
row.addPriceDisplay('basePrice', {
label: 'Base Painting Cost',
computedValue: () => Math.round(calculateBasePrice()),
variant: 'default'
}, '1fr');
row.addPriceDisplay('heightAdjustment', {
label: 'Height/Access Adjustment',
computedValue: () => {
const base = calculateBasePrice();
const stories = propertySection.dropdown('stories')?.value() || '2';
const multiplier = storyMultipliers[stories] || 1;
return Math.round(base * (multiplier - 1));
},
variant: 'default',
prefix: '+',
isVisible: () => {
const stories = propertySection.dropdown('stories')?.value();
return stories !== '1';
}
}, '1fr');
});
summarySection.addRow(row => {
row.addPriceDisplay('conditionAdjustment', {
label: 'Surface Prep Adjustment',
computedValue: () => {
const base = calculateBasePrice();
const stories = propertySection.dropdown('stories')?.value() || '2';
const storyMult = storyMultipliers[stories] || 1;
const condition = sidingSection.radioButton('currentCondition')?.value() || 'fair';
const condMult = conditionMultipliers[condition] || 1;
return Math.round(base * storyMult * (condMult - 1));
},
variant: () => {
const condition = sidingSection.radioButton('currentCondition')?.value();
return condition === 'good' ? 'success' : 'default';
},
prefix: () => {
const condition = sidingSection.radioButton('currentCondition')?.value();
return condition === 'good' ? '' : '+';
}
}, '1fr');
row.addPriceDisplay('paintQualityAdjustment', {
label: 'Paint Quality Upgrade',
computedValue: () => {
const base = calculateBasePrice();
const quality = paintSection.radioButton('paintQuality')?.value() || 'standard';
const multiplier = paintQualityMultipliers[quality] || 1;
return Math.round(base * (multiplier - 1));
},
variant: 'default',
prefix: '+',
isVisible: () => {
const quality = paintSection.radioButton('paintQuality')?.value();
return quality !== 'standard';
}
}, '1fr');
});
summarySection.addRow(row => {
row.addPriceDisplay('additionalAreas', {
label: 'Additional Areas',
computedValue: () => {
let total = 0;
if (areasSection.checkbox('shutters')?.value()) {
const pairs = areasSection.integer('shutterPairs')?.value() || 0;
total += pairs * 25;
}
if (areasSection.checkbox('doors')?.value()) {
const doors = areasSection.integer('doorCount')?.value() || 0;
total += doors * 75;
}
if (areasSection.checkbox('garage')?.value()) {
const gDoors = areasSection.integer('garageDoors')?.value() || 0;
total += gDoors * 150;
}
if (areasSection.checkbox('deck')?.value()) {
const deckSqft = areasSection.slider('deckSqFt')?.value() || 0;
total += deckSqft * 4;
}
if (areasSection.checkbox('fence')?.value()) {
const fenceLen = areasSection.slider('fenceLength')?.value() || 0;
total += fenceLen * 3;
}
// Extra coat
const coats = paintSection.dropdown('coats')?.value() || '2';
if (coats === '3') {
const sqft = propertySection.slider('totalSqFt')?.value() || 2500;
total += sqft * 0.75;
}
// Extra colors
const colors = paintSection.integer('colors')?.value() || 2;
if (colors > 2) total += (colors - 2) * 150;
return Math.round(total);
},
variant: 'default',
prefix: '+'
}, '1fr');
row.addPriceDisplay('prepServices', {
label: 'Prep Services',
computedValue: () => {
let total = 0;
const sqft = propertySection.slider('totalSqFt')?.value() || 2500;
if (prepSection.checkbox('powerWashing')?.value()) total += sqft * 0.15;
if (prepSection.checkbox('scraping')?.value()) total += sqft * 0.40;
if (prepSection.checkbox('caulking')?.value()) {
const openings = prepSection.integer('openings')?.value() || 0;
total += openings * 8;
}
if (prepSection.checkbox('priming')?.value()) total += sqft * 0.50;
if (prepSection.checkbox('repairs')?.value()) total += 300;
if (prepSection.checkbox('leadPaint')?.value()) total += 500;
return Math.round(total);
},
variant: 'default',
prefix: '+'
}, '1fr');
});
summarySection.addRow(row => {
row.addPriceDisplay('travelFee', {
label: 'Travel Fee',
computedValue: () => getTravelFee(),
variant: 'default',
prefix: '+'
});
});
summarySection.addSpacer({ showLine: true, lineStyle: 'solid', lineColor: '#e2e8f0' });
summarySection.addRow(row => {
row.addPriceDisplay('totalEstimate', {
label: 'Total Estimated Cost',
computedValue: () => {
const sqft = propertySection.slider('totalSqFt')?.value() || 2500;
const siding = sidingSection.dropdown('primarySiding')?.value() || 'wood';
const rate = sidingRates[siding] || 3.50;
let total = sqft * rate;
// Stories
const stories = propertySection.dropdown('stories')?.value() || '2';
total *= storyMultipliers[stories] || 1;
// Condition
const condition = sidingSection.radioButton('currentCondition')?.value() || 'fair';
total *= conditionMultipliers[condition] || 1;
// Paint quality
const quality = paintSection.radioButton('paintQuality')?.value() || 'standard';
total *= paintQualityMultipliers[quality] || 1;
// Additional areas
if (areasSection.checkbox('shutters')?.value()) {
const pairs = areasSection.integer('shutterPairs')?.value() || 0;
total += pairs * 25;
}
if (areasSection.checkbox('doors')?.value()) {
const doors = areasSection.integer('doorCount')?.value() || 0;
total += doors * 75;
}
if (areasSection.checkbox('garage')?.value()) {
const gDoors = areasSection.integer('garageDoors')?.value() || 0;
total += gDoors * 150;
}
if (areasSection.checkbox('deck')?.value()) {
const deckSqft = areasSection.slider('deckSqFt')?.value() || 0;
total += deckSqft * 4;
}
if (areasSection.checkbox('fence')?.value()) {
const fenceLen = areasSection.slider('fenceLength')?.value() || 0;
total += fenceLen * 3;
}
const coats = paintSection.dropdown('coats')?.value() || '2';
if (coats === '3') total += sqft * 0.75;
const colors = paintSection.integer('colors')?.value() || 2;
if (colors > 2) total += (colors - 2) * 150;
// Prep services
if (prepSection.checkbox('powerWashing')?.value()) total += sqft * 0.15;
if (prepSection.checkbox('scraping')?.value()) total += sqft * 0.40;
if (prepSection.checkbox('caulking')?.value()) {
const openings = prepSection.integer('openings')?.value() || 0;
total += openings * 8;
}
if (prepSection.checkbox('priming')?.value()) total += sqft * 0.50;
if (prepSection.checkbox('repairs')?.value()) total += 300;
if (prepSection.checkbox('leadPaint')?.value()) total += 500;
// Travel fee
total += getTravelFee();
return Math.round(total);
},
variant: 'large'
});
});
summarySection.addRow(row => {
row.addTextPanel('timeEstimate', {
computedValue: () => {
const sqft = propertySection.slider('totalSqFt')?.value() || 2500;
const minDays = Math.ceil(sqft / 1500);
const maxDays = Math.ceil(sqft / 800);
return `Estimated project duration: ${minDays}-${maxDays} days (weather permitting)`;
},
customStyles: { 'font-size': '0.9rem', 'color': '#059669' }
});
});
const finalSection = form.addSubform('final', {
title: '🧾 Summary',
isCollapsible: false,
sticky: 'bottom'
});
finalSection.addRow(row => {
row.addPriceDisplay('totalEstimate', {
label: 'Total Estimated Cost',
computedValue: () => {
const sqft = propertySection.slider('totalSqFt')?.value() || 2500;
const siding = sidingSection.dropdown('primarySiding')?.value() || 'wood';
const rate = sidingRates[siding] || 3.50;
let total = sqft * rate;
const stories = propertySection.dropdown('stories')?.value() || '2';
total *= storyMultipliers[stories] || 1;
const condition = sidingSection.radioButton('currentCondition')?.value() || 'fair';
total *= conditionMultipliers[condition] || 1;
const quality = paintSection.radioButton('paintQuality')?.value() || 'standard';
total *= paintQualityMultipliers[quality] || 1;
if (areasSection.checkbox('shutters')?.value()) {
const pairs = areasSection.integer('shutterPairs')?.value() || 0;
total += pairs * 25;
}
if (areasSection.checkbox('doors')?.value()) {
const doors = areasSection.integer('doorCount')?.value() || 0;
total += doors * 75;
}
if (areasSection.checkbox('garage')?.value()) {
const gDoors = areasSection.integer('garageDoors')?.value() || 0;
total += gDoors * 150;
}
if (areasSection.checkbox('deck')?.value()) {
const deckSqft = areasSection.slider('deckSqFt')?.value() || 0;
total += deckSqft * 4;
}
if (areasSection.checkbox('fence')?.value()) {
const fenceLen = areasSection.slider('fenceLength')?.value() || 0;
total += fenceLen * 3;
}
const coats = paintSection.dropdown('coats')?.value() || '2';
if (coats === '3') total += sqft * 0.75;
const colors = paintSection.integer('colors')?.value() || 2;
if (colors > 2) total += (colors - 2) * 150;
if (prepSection.checkbox('powerWashing')?.value()) total += sqft * 0.15;
if (prepSection.checkbox('scraping')?.value()) total += sqft * 0.40;
if (prepSection.checkbox('caulking')?.value()) {
const openings = prepSection.integer('openings')?.value() || 0;
total += openings * 8;
}
if (prepSection.checkbox('priming')?.value()) total += sqft * 0.50;
if (prepSection.checkbox('repairs')?.value()) total += 300;
if (prepSection.checkbox('leadPaint')?.value()) total += 500;
// Travel fee
total += getTravelFee();
return Math.round(total);
},
variant: 'large'
});
});
finalSection.addRow(row => {
row.addTextPanel('disclaimer', {
computedValue: () => 'Final price determined after on-site inspection.',
customStyles: { 'font-size': '0.85rem', 'color': '#64748b', 'font-style': 'italic' }
});
});
form.configureSubmitButton({
label: 'Schedule Free Estimate'
});
}