export function treeServiceCalculator(form: FormTs) {
// Tree removal base costs by height
const removalByHeight: Record<string, number> = {
'small': 300,
'medium': 700,
'large': 1200,
'xlarge': 2000,
'giant': 3500
};
// Trimming costs by tree size
const trimmingBySize: Record<string, number> = {
'small': 150,
'medium': 350,
'large': 600,
'xlarge': 900
};
// Access difficulty multipliers
const accessMultipliers: Record<string, number> = {
'easy': 1.0,
'moderate': 1.3,
'difficult': 1.6,
'crane-needed': 2.5
};
form.addRow(row => {
row.addTextPanel('header', {
computedValue: () => 'Tree Service Estimate',
customStyles: { 'font-size': '1.5rem', 'font-weight': '600', 'color': '#1e293b' }
});
});
form.addSpacer({ height: 20 });
// Service Type Section
const serviceSection = form.addSubform('serviceType', { title: '🌳 Service Type' });
serviceSection.addRow(row => {
row.addRadioButton('primaryService', {
label: 'Primary Service Needed',
options: [
{ id: 'removal', name: 'Tree Removal' },
{ id: 'trimming', name: 'Tree Trimming/Pruning' },
{ id: 'stump', name: 'Stump Grinding Only' },
{ id: 'emergency', name: 'Emergency Service (Storm Damage)' }
],
defaultValue: 'trimming',
orientation: 'vertical',
isRequired: true
});
});
// Tree Details Section
const treeSection = form.addSubform('treeDetails', { title: '🌲 Tree Details' });
treeSection.addRow(row => {
row.addDropdown('treeSize', {
label: 'Tree Size',
options: [
{ id: 'small', name: 'Small (under 25 ft)' },
{ id: 'medium', name: 'Medium (25-50 ft)' },
{ id: 'large', name: 'Large (50-75 ft)' },
{ id: 'xlarge', name: 'Extra Large (75-100 ft)' },
{ id: 'giant', name: 'Giant (over 100 ft)' }
],
defaultValue: 'medium',
isRequired: true
}, '1fr');
row.addDropdown('treeType', {
label: 'Tree Type',
options: [
{ id: 'deciduous', name: 'Deciduous (Oak, Maple, etc.)' },
{ id: 'conifer', name: 'Conifer (Pine, Spruce, etc.)' },
{ id: 'palm', name: 'Palm Tree' },
{ id: 'fruit', name: 'Fruit Tree' },
{ id: 'dead', name: 'Dead/Dying Tree' }
],
defaultValue: 'deciduous',
isRequired: true
}, '1fr');
});
treeSection.addRow(row => {
row.addInteger('numberOfTrees', {
label: 'Number of Trees',
min: 1,
max: 20,
defaultValue: 1,
placeholder: 'e.g. 2',
isRequired: true
}, '1fr');
row.addDropdown('trunkDiameter', {
label: 'Trunk Diameter (inches)',
options: [
{ id: 'thin', name: 'Under 12 inches' },
{ id: 'medium', name: '12-24 inches' },
{ id: 'thick', name: '24-36 inches' },
{ id: 'massive', name: 'Over 36 inches' }
],
defaultValue: 'medium',
isRequired: true,
isVisible: () => {
const service = serviceSection.radioButton('primaryService')?.value();
return service === 'removal' || service === 'emergency';
}
}, '1fr');
});
// Location & Access Section
const locationSection = form.addSubform('location', { title: '📍 Location & Access' });
locationSection.addRow(row => {
row.addAddress('serviceAddress', {
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('travelZoneInfo', {
computedValue: () => {
const addressField = locationSection.address('serviceAddress');
const miles = addressField?.distance();
if (miles == null) return '📍 Enter address to check service area';
if (miles <= 15) return '📍 Within service area - No travel fee';
if (miles <= 30) return '📍 Extended area - $75 travel fee (equipment transport)';
if (miles <= 50) return '📍 Remote area - $150 travel fee (equipment transport)';
return '📍 Long distance - $200+ travel fee';
},
customStyles: { 'font-size': '0.9rem', 'color': '#854d0e', 'background': '#fef3c7', 'padding': '10px', 'border-radius': '6px' }
});
});
locationSection.addRow(row => {
row.addDropdown('accessDifficulty', {
label: 'Access Difficulty',
options: [
{ id: 'easy', name: 'Easy - Open area, good truck access' },
{ id: 'moderate', name: 'Moderate - Backyard, some obstacles' },
{ id: 'difficult', name: 'Difficult - Tight spaces, fences, slopes' },
{ id: 'crane-needed', name: 'Crane Required - Near structures, power lines' }
],
defaultValue: 'moderate',
isRequired: true
});
});
locationSection.addRow(row => {
row.addCheckbox('nearStructure', {
label: 'Tree near house or structure',
defaultValue: false
}, '1fr');
row.addCheckbox('nearPowerLines', {
label: 'Tree near power lines',
defaultValue: false
}, '1fr');
});
// Additional Services Section
const addonsSection = form.addSubform('addons', { title: '✨ Additional Services' });
addonsSection.addRow(row => {
row.addCheckbox('stumpGrinding', {
label: 'Stump Grinding (+$150-400 per stump)',
defaultValue: true,
isVisible: () => {
const service = serviceSection.radioButton('primaryService')?.value();
return service === 'removal' || service === 'emergency';
}
}, '1fr');
row.addCheckbox('debrisRemoval', {
label: 'Debris Hauling (+$150)',
defaultValue: true
}, '1fr');
});
addonsSection.addRow(row => {
row.addCheckbox('woodChipping', {
label: 'Wood Chipping/Mulch (+$75)',
defaultValue: false
}, '1fr');
row.addCheckbox('firewood', {
label: 'Cut Firewood Lengths (+$100)',
defaultValue: false,
isVisible: () => {
const service = serviceSection.radioButton('primaryService')?.value();
return service === 'removal' || service === 'emergency';
}
}, '1fr');
});
addonsSection.addRow(row => {
row.addCheckbox('rootRemoval', {
label: 'Root Removal (+$300)',
defaultValue: false,
isVisible: () => {
const service = serviceSection.radioButton('primaryService')?.value();
return service === 'removal' || service === 'stump';
}
}, '1fr');
row.addCheckbox('cleanUp', {
label: 'Full Site Clean-up (+$200)',
defaultValue: true
}, '1fr');
});
form.addSpacer({ height: 20, showLine: true, lineStyle: 'dashed' });
// Helper to calculate travel fee (equipment transport)
const getTravelFee = () => {
const addressField = locationSection.address('serviceAddress');
const miles = addressField?.distance();
if (miles == null || miles <= 15) return 0;
if (miles <= 30) return 75;
if (miles <= 50) return 150;
return 200 + Math.floor((miles - 50) / 20) * 50;
};
// Price Summary Section
const summarySection = form.addSubform('summary', { title: '💰 Cost Breakdown', isCollapsible: false });
summarySection.addRow(row => {
row.addPriceDisplay('serviceCost', {
label: 'Primary Service',
computedValue: () => {
const service = serviceSection.radioButton('primaryService')?.value() || 'trimming';
const treeSize = treeSection.dropdown('treeSize')?.value() || 'medium';
const numberOfTrees = treeSection.integer('numberOfTrees')?.value() || 1;
const access = locationSection.dropdown('accessDifficulty')?.value() || 'moderate';
let baseCost = 0;
if (service === 'removal') {
baseCost = removalByHeight[treeSize] || 700;
const diameter = treeSection.dropdown('trunkDiameter')?.value() || 'medium';
const diameterMultiplier: Record<string, number> = {
'thin': 0.8, 'medium': 1.0, 'thick': 1.4, 'massive': 2.0
};
baseCost *= diameterMultiplier[diameter] || 1.0;
} else if (service === 'trimming') {
baseCost = trimmingBySize[treeSize] || 350;
} else if (service === 'stump') {
baseCost = 200;
const diameter = treeSection.dropdown('trunkDiameter')?.value() || 'medium';
if (diameter === 'thick') baseCost = 300;
if (diameter === 'massive') baseCost = 450;
} else if (service === 'emergency') {
baseCost = (removalByHeight[treeSize] || 700) * 1.5;
}
const accessMultiplier = accessMultipliers[access] || 1.3;
return Math.round(baseCost * numberOfTrees * accessMultiplier);
},
variant: 'default'
}, '1fr');
row.addPriceDisplay('hazardCost', {
label: 'Hazard Fees',
computedValue: () => {
let total = 0;
if (locationSection.checkbox('nearStructure')?.value()) total += 300;
if (locationSection.checkbox('nearPowerLines')?.value()) total += 500;
return total;
},
variant: 'default',
prefix: '+'
}, '1fr');
});
summarySection.addRow(row => {
row.addPriceDisplay('addonsCost', {
label: 'Additional Services',
computedValue: () => {
const treeSize = treeSection.dropdown('treeSize')?.value() || 'medium';
const numberOfTrees = treeSection.integer('numberOfTrees')?.value() || 1;
let total = 0;
if (addonsSection.checkbox('stumpGrinding')?.value()) {
const stumpCost: Record<string, number> = {
'small': 150, 'medium': 200, 'large': 300, 'xlarge': 350, 'giant': 400
};
total += (stumpCost[treeSize] || 200) * numberOfTrees;
}
if (addonsSection.checkbox('debrisRemoval')?.value()) total += 150;
if (addonsSection.checkbox('woodChipping')?.value()) total += 75;
if (addonsSection.checkbox('firewood')?.value()) total += 100;
if (addonsSection.checkbox('rootRemoval')?.value()) total += 300 * numberOfTrees;
if (addonsSection.checkbox('cleanUp')?.value()) total += 200;
return total;
},
variant: 'default',
prefix: '+'
}, '1fr');
});
summarySection.addRow(row => {
row.addPriceDisplay('travelFee', {
label: 'Travel/Equipment Fee',
computedValue: () => getTravelFee(),
variant: 'default',
prefix: '+'
});
});
const finalSection = form.addSubform('final', {
title: '🪓 Your Estimate',
isCollapsible: false,
sticky: 'bottom'
});
finalSection.addRow(row => {
row.addPriceDisplay('totalCost', {
label: 'Total Estimated Cost',
computedValue: () => {
const service = serviceSection.radioButton('primaryService')?.value() || 'trimming';
const treeSize = treeSection.dropdown('treeSize')?.value() || 'medium';
const numberOfTrees = treeSection.integer('numberOfTrees')?.value() || 1;
const access = locationSection.dropdown('accessDifficulty')?.value() || 'moderate';
let baseCost = 0;
if (service === 'removal') {
baseCost = removalByHeight[treeSize] || 700;
const diameter = treeSection.dropdown('trunkDiameter')?.value() || 'medium';
const diameterMultiplier: Record<string, number> = {
'thin': 0.8, 'medium': 1.0, 'thick': 1.4, 'massive': 2.0
};
baseCost *= diameterMultiplier[diameter] || 1.0;
} else if (service === 'trimming') {
baseCost = trimmingBySize[treeSize] || 350;
} else if (service === 'stump') {
baseCost = 200;
const diameter = treeSection.dropdown('trunkDiameter')?.value() || 'medium';
if (diameter === 'thick') baseCost = 300;
if (diameter === 'massive') baseCost = 450;
} else if (service === 'emergency') {
baseCost = (removalByHeight[treeSize] || 700) * 1.5;
}
const accessMultiplier = accessMultipliers[access] || 1.3;
let serviceCost = Math.round(baseCost * numberOfTrees * accessMultiplier);
let hazardCost = 0;
if (locationSection.checkbox('nearStructure')?.value()) hazardCost += 300;
if (locationSection.checkbox('nearPowerLines')?.value()) hazardCost += 500;
let addonsCost = 0;
if (addonsSection.checkbox('stumpGrinding')?.value()) {
const stumpCost: Record<string, number> = {
'small': 150, 'medium': 200, 'large': 300, 'xlarge': 350, 'giant': 400
};
addonsCost += (stumpCost[treeSize] || 200) * numberOfTrees;
}
if (addonsSection.checkbox('debrisRemoval')?.value()) addonsCost += 150;
if (addonsSection.checkbox('woodChipping')?.value()) addonsCost += 75;
if (addonsSection.checkbox('firewood')?.value()) addonsCost += 100;
if (addonsSection.checkbox('rootRemoval')?.value()) addonsCost += 300 * numberOfTrees;
if (addonsSection.checkbox('cleanUp')?.value()) addonsCost += 200;
// Travel/equipment fee
const travelFee = getTravelFee();
return serviceCost + hazardCost + addonsCost + travelFee;
},
variant: 'large'
}, '1fr');
row.addPriceDisplay('perTree', {
label: 'Cost Per Tree',
computedValue: () => {
const service = serviceSection.radioButton('primaryService')?.value() || 'trimming';
const treeSize = treeSection.dropdown('treeSize')?.value() || 'medium';
const numberOfTrees = treeSection.integer('numberOfTrees')?.value() || 1;
const access = locationSection.dropdown('accessDifficulty')?.value() || 'moderate';
let baseCost = 0;
if (service === 'removal') {
baseCost = removalByHeight[treeSize] || 700;
const diameter = treeSection.dropdown('trunkDiameter')?.value() || 'medium';
const diameterMultiplier: Record<string, number> = {
'thin': 0.8, 'medium': 1.0, 'thick': 1.4, 'massive': 2.0
};
baseCost *= diameterMultiplier[diameter] || 1.0;
} else if (service === 'trimming') {
baseCost = trimmingBySize[treeSize] || 350;
} else if (service === 'stump') {
baseCost = 200;
} else if (service === 'emergency') {
baseCost = (removalByHeight[treeSize] || 700) * 1.5;
}
const accessMultiplier = accessMultipliers[access] || 1.3;
return Math.round(baseCost * accessMultiplier);
},
variant: 'default',
suffix: '/tree'
}, '1fr');
});
finalSection.addRow(row => {
row.addTextPanel('disclaimer', {
computedValue: () => 'Estimates based on average costs. Final price depends on tree condition, site access, and exact specifications. On-site assessment required for accurate quote.',
customStyles: { 'font-size': '0.85rem', 'color': '#64748b', 'font-style': 'italic' }
});
});
form.configureSubmitButton({
label: 'Get Free Quote'
});
}