export function roofRepairCalculator(form: FormTs) {
// Roof type base costs per square foot
const roofTypeCosts: Record<string, number> = {
'asphalt': 4.50,
'metal': 9.00,
'tile': 12.00,
'slate': 18.00,
'wood-shake': 10.00,
'flat': 6.00
};
// Service type multipliers
const serviceTypeMultipliers: Record<string, number> = {
'repair': 0.15,
'partial': 0.50,
'full': 1.0,
'overlay': 0.75
};
// Pitch difficulty multipliers
const pitchMultipliers: Record<string, number> = {
'low': 1.0,
'medium': 1.15,
'steep': 1.35,
'very-steep': 1.55
};
form.addRow(row => {
row.addTextPanel('header', {
computedValue: () => 'Get Your Roof Estimate',
customStyles: { 'font-size': '1.5rem', 'font-weight': '600', 'color': '#1e293b' }
});
});
form.addSpacer({ height: 20 });
// Service Location Section
const locationSection = form.addSubform('serviceLocation', { title: '๐ Property 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 <= 30) return '๐ Within service area - Standard pricing';
if (miles <= 60) return '๐ Extended area - $125 travel fee';
if (miles <= 90) return '๐ Remote area - $225 travel fee';
return '๐ Long distance - Custom quote required';
},
customStyles: { 'font-size': '0.9rem', 'color': '#b45309', 'background': '#fef3c7', 'padding': '10px', 'border-radius': '6px' }
});
});
// Roof Details Section
const roofSection = form.addSubform('roofDetails', { title: '๐ Roof Details' });
roofSection.addRow(row => {
row.addDropdown('roofType', {
label: 'Roofing Material',
options: [
{ id: 'asphalt', name: 'Asphalt Shingles' },
{ id: 'metal', name: 'Metal Roofing' },
{ id: 'tile', name: 'Clay/Concrete Tile' },
{ id: 'slate', name: 'Slate' },
{ id: 'wood-shake', name: 'Wood Shake' },
{ id: 'flat', name: 'Flat/TPO/EPDM' }
],
defaultValue: 'asphalt',
isRequired: true
}, '1fr');
row.addInteger('roofSize', {
label: 'Roof Size (sq ft)',
min: 500,
max: 10000,
defaultValue: 2000,
placeholder: 'e.g. 2000',
isRequired: true
}, '1fr');
});
roofSection.addRow(row => {
row.addDropdown('roofPitch', {
label: 'Roof Pitch',
options: [
{ id: 'low', name: 'Low (0-4/12)' },
{ id: 'medium', name: 'Medium (5-8/12)' },
{ id: 'steep', name: 'Steep (9-12/12)' },
{ id: 'very-steep', name: 'Very Steep (12+/12)' }
],
defaultValue: 'medium',
isRequired: true
}, '1fr');
row.addInteger('stories', {
label: 'Number of Stories',
min: 1,
max: 4,
defaultValue: 2,
isRequired: true
}, '1fr');
});
// Service Type Section
const serviceSection = form.addSubform('serviceType', { title: '๐ง Service Type' });
serviceSection.addRow(row => {
row.addRadioButton('serviceType', {
label: 'What do you need?',
options: [
{ id: 'repair', name: 'Repair (patch damaged areas)' },
{ id: 'partial', name: 'Partial Replacement (50% of roof)' },
{ id: 'overlay', name: 'Overlay (new layer over existing)' },
{ id: 'full', name: 'Full Replacement (tear-off & replace)' }
],
defaultValue: 'repair',
orientation: 'vertical',
isRequired: true
});
});
// Damage & Condition Section
const conditionSection = form.addSubform('condition', { title: '๐ Current Condition' });
conditionSection.addRow(row => {
row.addDropdown('damageLevel', {
label: 'Damage Level',
options: [
{ id: 'minor', name: 'Minor (few missing shingles)' },
{ id: 'moderate', name: 'Moderate (visible wear, some leaks)' },
{ id: 'severe', name: 'Severe (multiple leaks, structural concern)' },
{ id: 'emergency', name: 'Emergency (active leak, storm damage)' }
],
defaultValue: 'moderate',
isRequired: true
}, '1fr');
row.addInteger('roofAge', {
label: 'Roof Age (years)',
min: 0,
max: 50,
defaultValue: 15,
isRequired: true
}, '1fr');
});
conditionSection.addRow(row => {
row.addInteger('existingLayers', {
label: 'Existing Shingle Layers',
min: 1,
max: 3,
defaultValue: 1,
tooltip: 'Multiple layers may require full tear-off'
}, '1fr');
row.addCheckbox('hasChimney', {
label: 'Has Chimney',
defaultValue: true
}, '1fr');
});
// Additional Work Section
const addonsSection = form.addSubform('addons', { title: 'โจ Additional Work' });
addonsSection.addRow(row => {
row.addCheckbox('gutterReplacement', {
label: 'Gutter Replacement (+$8/linear ft)',
defaultValue: false
}, '1fr');
row.addCheckbox('skylightWork', {
label: 'Skylight Repair/Flash (+$350/skylight)',
defaultValue: false
}, '1fr');
});
addonsSection.addRow(row => {
row.addCheckbox('ventilation', {
label: 'Add/Upgrade Ventilation (+$400)',
defaultValue: false
}, '1fr');
row.addCheckbox('deckRepair', {
label: 'Deck/Sheathing Repair (+$75/sheet)',
defaultValue: false
}, '1fr');
});
addonsSection.addRow(row => {
row.addCheckbox('fasciaSoffit', {
label: 'Fascia & Soffit Repair (+$12/linear ft)',
defaultValue: false
}, '1fr');
row.addCheckbox('iceDam', {
label: 'Ice Dam Protection (+$15/linear ft)',
defaultValue: false
}, '1fr');
});
addonsSection.addRow(row => {
row.addInteger('gutterLength', {
label: 'Gutter Length (linear ft)',
min: 50,
max: 500,
defaultValue: 150,
isVisible: () => addonsSection.checkbox('gutterReplacement')?.value() === true
}, '1fr');
row.addInteger('skylightCount', {
label: 'Number of Skylights',
min: 1,
max: 6,
defaultValue: 1,
isVisible: () => addonsSection.checkbox('skylightWork')?.value() === true
}, '1fr');
});
addonsSection.addRow(row => {
row.addInteger('deckSheets', {
label: 'Sheets Needed (4x8)',
min: 1,
max: 50,
defaultValue: 5,
isVisible: () => addonsSection.checkbox('deckRepair')?.value() === true
}, '1fr');
row.addInteger('fasciaLength', {
label: 'Fascia/Soffit Length (linear ft)',
min: 20,
max: 300,
defaultValue: 100,
isVisible: () => addonsSection.checkbox('fasciaSoffit')?.value() === true
}, '1fr');
});
addonsSection.addRow(row => {
row.addInteger('edgeLength', {
label: 'Roof Edge Length (linear ft)',
min: 50,
max: 400,
defaultValue: 150,
isVisible: () => addonsSection.checkbox('iceDam')?.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 <= 30) return 0;
if (miles <= 60) return 125;
if (miles <= 90) return 225;
return 300 + Math.floor((miles - 90) / 30) * 75;
};
// Price Summary Section
const summarySection = form.addSubform('summary', { title: '๐ฐ Your Estimate', isCollapsible: false });
summarySection.addRow(row => {
row.addPriceDisplay('materialCost', {
label: 'Material Cost',
computedValue: () => {
const roofType = roofSection.dropdown('roofType')?.value() || 'asphalt';
const roofSize = roofSection.integer('roofSize')?.value() || 2000;
const serviceType = serviceSection.radioButton('serviceType')?.value() || 'repair';
const baseCost = roofTypeCosts[roofType] || 4.50;
const serviceMultiplier = serviceTypeMultipliers[serviceType] || 1;
return Math.round(roofSize * baseCost * serviceMultiplier);
},
variant: 'default'
}, '1fr');
row.addPriceDisplay('laborCost', {
label: 'Labor Cost',
computedValue: () => {
const roofSize = roofSection.integer('roofSize')?.value() || 2000;
const pitch = roofSection.dropdown('roofPitch')?.value() || 'medium';
const stories = roofSection.integer('stories')?.value() || 2;
const serviceType = serviceSection.radioButton('serviceType')?.value() || 'repair';
const existingLayers = conditionSection.integer('existingLayers')?.value() || 1;
const baseLaborRate = 3.50;
const pitchMultiplier = pitchMultipliers[pitch] || 1;
const storyMultiplier = 1 + (stories - 1) * 0.15;
const serviceMultiplier = serviceTypeMultipliers[serviceType] || 1;
const layerMultiplier = serviceType === 'full' ? 1 + (existingLayers - 1) * 0.2 : 1;
return Math.round(roofSize * baseLaborRate * pitchMultiplier * storyMultiplier * serviceMultiplier * layerMultiplier);
},
variant: 'default'
}, '1fr');
});
summarySection.addRow(row => {
row.addPriceDisplay('addonsTotal', {
label: 'Additional Work',
computedValue: () => {
let total = 0;
if (addonsSection.checkbox('gutterReplacement')?.value()) {
const length = addonsSection.integer('gutterLength')?.value() || 150;
total += length * 8;
}
if (addonsSection.checkbox('skylightWork')?.value()) {
const count = addonsSection.integer('skylightCount')?.value() || 1;
total += count * 350;
}
if (addonsSection.checkbox('ventilation')?.value()) total += 400;
if (addonsSection.checkbox('deckRepair')?.value()) {
const sheets = addonsSection.integer('deckSheets')?.value() || 5;
total += sheets * 75;
}
if (addonsSection.checkbox('fasciaSoffit')?.value()) {
const length = addonsSection.integer('fasciaLength')?.value() || 100;
total += length * 12;
}
if (addonsSection.checkbox('iceDam')?.value()) {
const length = addonsSection.integer('edgeLength')?.value() || 150;
total += length * 15;
}
return total;
},
variant: 'default',
prefix: '+'
}, '1fr');
row.addPriceDisplay('emergencyFee', {
label: 'Urgency Adjustment',
computedValue: () => {
const damageLevel = conditionSection.dropdown('damageLevel')?.value() || 'moderate';
const roofType = roofSection.dropdown('roofType')?.value() || 'asphalt';
const roofSize = roofSection.integer('roofSize')?.value() || 2000;
const serviceType = serviceSection.radioButton('serviceType')?.value() || 'repair';
const baseCost = roofTypeCosts[roofType] || 4.50;
const serviceMultiplier = serviceTypeMultipliers[serviceType] || 1;
const materialCost = roofSize * baseCost * serviceMultiplier;
if (damageLevel === 'emergency') return Math.round(materialCost * 0.25);
if (damageLevel === 'severe') return Math.round(materialCost * 0.1);
return 0;
},
variant: 'default',
prefix: '+'
}, '1fr');
});
summarySection.addRow(row => {
row.addPriceDisplay('travelFee', {
label: 'Travel Fee',
computedValue: () => getTravelFee(),
variant: 'default',
prefix: '+'
});
});
const finalSection = form.addSubform('final', {
title: '๐งพ Summary',
isCollapsible: false,
sticky: 'bottom'
});
finalSection.addRow(row => {
row.addPriceDisplay('totalEstimate', {
label: 'Total Estimated Cost',
computedValue: () => {
const roofType = roofSection.dropdown('roofType')?.value() || 'asphalt';
const roofSize = roofSection.integer('roofSize')?.value() || 2000;
const pitch = roofSection.dropdown('roofPitch')?.value() || 'medium';
const stories = roofSection.integer('stories')?.value() || 2;
const serviceType = serviceSection.radioButton('serviceType')?.value() || 'repair';
const damageLevel = conditionSection.dropdown('damageLevel')?.value() || 'moderate';
const existingLayers = conditionSection.integer('existingLayers')?.value() || 1;
// Material cost
const baseCost = roofTypeCosts[roofType] || 4.50;
const serviceMultiplier = serviceTypeMultipliers[serviceType] || 1;
const materialCost = roofSize * baseCost * serviceMultiplier;
// Labor cost
const baseLaborRate = 3.50;
const pitchMultiplier = pitchMultipliers[pitch] || 1;
const storyMultiplier = 1 + (stories - 1) * 0.15;
const layerMultiplier = serviceType === 'full' ? 1 + (existingLayers - 1) * 0.2 : 1;
const laborCost = roofSize * baseLaborRate * pitchMultiplier * storyMultiplier * serviceMultiplier * layerMultiplier;
// Add-ons
let addonsTotal = 0;
if (addonsSection.checkbox('gutterReplacement')?.value()) {
addonsTotal += (addonsSection.integer('gutterLength')?.value() || 150) * 8;
}
if (addonsSection.checkbox('skylightWork')?.value()) {
addonsTotal += (addonsSection.integer('skylightCount')?.value() || 1) * 350;
}
if (addonsSection.checkbox('ventilation')?.value()) addonsTotal += 400;
if (addonsSection.checkbox('deckRepair')?.value()) {
addonsTotal += (addonsSection.integer('deckSheets')?.value() || 5) * 75;
}
if (addonsSection.checkbox('fasciaSoffit')?.value()) {
addonsTotal += (addonsSection.integer('fasciaLength')?.value() || 100) * 12;
}
if (addonsSection.checkbox('iceDam')?.value()) {
addonsTotal += (addonsSection.integer('edgeLength')?.value() || 150) * 15;
}
// Urgency fee
let urgencyFee = 0;
if (damageLevel === 'emergency') urgencyFee = materialCost * 0.25;
else if (damageLevel === 'severe') urgencyFee = materialCost * 0.1;
// Travel fee
const travelFee = getTravelFee();
return Math.round(materialCost + laborCost + addonsTotal + urgencyFee + travelFee);
},
variant: 'large'
});
});
finalSection.addRow(row => {
row.addTextPanel('priceRange', {
computedValue: () => {
const roofType = roofSection.dropdown('roofType')?.value() || 'asphalt';
const roofSize = roofSection.integer('roofSize')?.value() || 2000;
const serviceType = serviceSection.radioButton('serviceType')?.value() || 'repair';
const baseCost = roofTypeCosts[roofType] || 4.50;
const serviceMultiplier = serviceTypeMultipliers[serviceType] || 1;
const baseEstimate = roofSize * baseCost * serviceMultiplier * 1.8;
const lowEstimate = Math.round(baseEstimate * 0.85);
const highEstimate = Math.round(baseEstimate * 1.15);
return `Typical range: $${lowEstimate.toLocaleString()} - $${highEstimate.toLocaleString()}`;
},
customStyles: { 'font-size': '0.9rem', 'color': '#64748b', 'text-align': 'center' }
});
});
finalSection.addRow(row => {
row.addTextPanel('disclaimer', {
computedValue: () => 'This estimate is for budgeting purposes. Final pricing requires on-site inspection. Permit fees not included.',
customStyles: { 'font-size': '0.85rem', 'color': '#64748b', 'font-style': 'italic' }
});
});
form.configureSubmitButton({
label: 'Request Free Inspection'
});
}