export function movingCostCalculator(form: FormTs) {
// Base rates
const hourlyRates: Record<string, number> = {
'2movers': 120,
'3movers': 160,
'4movers': 200,
'5movers': 240
};
// Home size to hours estimate
const homeSizeHours: Record<string, number> = {
'studio': 2,
'1bed': 3,
'2bed': 5,
'3bed': 7,
'4bed': 9,
'5bed': 12
};
// Long distance rates per mile
const perMileRate = 0.75;
// Floor fees
const floorFees: Record<string, number> = {
'ground': 0,
'elevator': 0,
'2nd': 75,
'3rd': 150,
'4th+': 225
};
form.addRow(row => {
row.addTextPanel('header', {
computedValue: () => 'Moving Cost Calculator',
customStyles: { 'font-size': '1.5rem', 'font-weight': '600', 'color': '#1e293b' }
});
});
form.addSpacer({ height: 20 });
// Move Type Section
const moveTypeSection = form.addSubform('moveType', { title: '📍 Pickup & Delivery Locations' });
moveTypeSection.addRow(row => {
row.addAddress('originAddress', {
label: 'Pickup Address',
placeholder: 'Enter pickup location...',
restrictToCountries: ['US'],
distanceUnit: 'miles',
isRequired: true
});
});
moveTypeSection.addRow(row => {
row.addAddress('destAddress', {
label: 'Delivery Address',
placeholder: 'Enter delivery location...',
showMap: true,
showDistance: true,
referenceAddress: () => moveTypeSection.address('originAddress')?.value() ?? null,
restrictToCountries: ['US'],
distanceUnit: 'miles',
isRequired: true
});
});
moveTypeSection.addRow(row => {
row.addTextbox('calculatedDistance', {
label: 'Calculated Distance',
computedValue: () => {
const destField = moveTypeSection.address('destAddress');
const miles = destField?.distance();
if (miles == null) return 'Enter both addresses to calculate';
return `${miles.toFixed(1)} miles`;
}
}, '1fr');
row.addDropdown('moveDate', {
label: 'Moving Time',
options: [
{ id: 'offpeak', name: 'Weekday, Mid-month (Best rates)' },
{ id: 'standard', name: 'Weekend or Start/End of month' },
{ id: 'peak', name: 'Summer months (June-Aug) +20%' },
{ id: 'holiday', name: 'Holiday period +30%' }
],
defaultValue: 'standard'
}, '1fr');
});
moveTypeSection.addRow(row => {
row.addTextPanel('moveTypeInfo', {
computedValue: () => {
const destField = moveTypeSection.address('destAddress');
const miles = destField?.distance();
if (miles == null) return '';
if (miles < 50) return '🏠 Local Move - Hourly rates apply';
if (miles < 500) return '🚛 Long Distance Move - Flat rate + mileage';
return '✈️ Cross Country Move - Premium rates apply';
},
customStyles: { 'font-size': '0.9rem', 'color': '#0369a1', 'background': '#e0f2fe', 'padding': '10px', 'border-radius': '6px' }
});
});
// Home Details Section
const homeSection = form.addSubform('home', { title: '🏠 Home Details' });
homeSection.addRow(row => {
row.addDropdown('homeSize', {
label: 'Current Home Size',
options: [
{ id: 'studio', name: 'Studio / Small apartment' },
{ id: '1bed', name: '1 Bedroom' },
{ id: '2bed', name: '2 Bedrooms' },
{ id: '3bed', name: '3 Bedrooms' },
{ id: '4bed', name: '4 Bedrooms' },
{ id: '5bed', name: '5+ Bedrooms / Large home' }
],
defaultValue: '2bed',
isRequired: true
}, '1fr');
row.addDropdown('furnishing', {
label: 'Furnishing Level',
options: [
{ id: 'minimal', name: 'Minimal - Few items (-20%)' },
{ id: 'average', name: 'Average - Standard furnishing' },
{ id: 'full', name: 'Fully furnished (+20%)' },
{ id: 'packed', name: 'Heavily packed (+40%)' }
],
defaultValue: 'average'
}, '1fr');
});
homeSection.addRow(row => {
row.addDropdown('originFloor', {
label: 'Origin Floor Level',
options: [
{ id: 'ground', name: 'Ground floor / House' },
{ id: 'elevator', name: 'Apartment with elevator' },
{ id: '2nd', name: '2nd floor (no elevator) +$75' },
{ id: '3rd', name: '3rd floor (no elevator) +$150' },
{ id: '4th+', name: '4th+ floor (no elevator) +$225' }
],
defaultValue: 'ground'
}, '1fr');
row.addDropdown('destFloor', {
label: 'Destination Floor Level',
options: [
{ id: 'ground', name: 'Ground floor / House' },
{ id: 'elevator', name: 'Apartment with elevator' },
{ id: '2nd', name: '2nd floor (no elevator) +$75' },
{ id: '3rd', name: '3rd floor (no elevator) +$150' },
{ id: '4th+', name: '4th+ floor (no elevator) +$225' }
],
defaultValue: 'ground'
}, '1fr');
});
// Services Section
const servicesSection = form.addSubform('services', { title: '📦 Services Needed' });
servicesSection.addRow(row => {
row.addRadioButton('packingService', {
label: 'Packing Service',
options: [
{ id: 'none', name: 'No packing (I will pack myself)' },
{ id: 'partial', name: 'Partial packing - Fragiles only (+$200-400)' },
{ id: 'full', name: 'Full packing service (+$400-800)' }
],
defaultValue: 'none',
orientation: 'vertical'
});
});
servicesSection.addRow(row => {
row.addCheckbox('packingMaterials', {
label: 'Packing Materials (boxes, tape, wrap) +$100-300',
defaultValue: false
}, '1fr');
row.addCheckbox('furnitureDisassembly', {
label: 'Furniture Disassembly/Assembly +$150',
defaultValue: true
}, '1fr');
});
servicesSection.addRow(row => {
row.addCheckbox('storageNeeded', {
label: 'Temporary Storage Needed',
defaultValue: false
}, '1fr');
row.addInteger('storageDays', {
label: 'Storage Duration (days)',
min: 1,
max: 90,
defaultValue: 7,
isVisible: () => servicesSection.checkbox('storageNeeded')?.value() === true
}, '1fr');
});
// Special Items Section
const specialSection = form.addSubform('special', { title: '🎹 Special Items' });
specialSection.addRow(row => {
row.addCheckbox('piano', {
label: 'Piano (+$200-500)',
defaultValue: false
}, '1fr');
row.addCheckbox('hotTub', {
label: 'Hot Tub (+$300-600)',
defaultValue: false
}, '1fr');
});
specialSection.addRow(row => {
row.addCheckbox('poolTable', {
label: 'Pool Table (+$300-450)',
defaultValue: false
}, '1fr');
row.addCheckbox('safeLarge', {
label: 'Large Safe (+$150-300)',
defaultValue: false
}, '1fr');
});
specialSection.addRow(row => {
row.addCheckbox('artwork', {
label: 'Valuable Artwork/Antiques (+$100-200)',
defaultValue: false
}, '1fr');
row.addCheckbox('gym', {
label: 'Home Gym Equipment (+$100-200)',
defaultValue: false
}, '1fr');
});
// Insurance Section
const insuranceSection = form.addSubform('insurance', { title: '🛡️ Insurance & Protection' });
insuranceSection.addRow(row => {
row.addRadioButton('coverage', {
label: 'Valuation Coverage',
options: [
{ id: 'basic', name: 'Basic Coverage (60c per lb) - Included' },
{ id: 'declared', name: 'Declared Value Protection - ~1% of declared value' },
{ id: 'full', name: 'Full Value Protection - ~2% of total value' }
],
defaultValue: 'basic',
orientation: 'vertical'
});
});
insuranceSection.addRow(row => {
row.addMoney('declaredValue', {
label: 'Declared Total Value of Items',
currency: '$',
min: 1000,
max: 500000,
defaultValue: 15000,
isVisible: () => {
const coverage = insuranceSection.radioButton('coverage')?.value();
return coverage === 'declared' || coverage === 'full';
}
});
});
form.addSpacer({ height: 20, showLine: true, lineStyle: 'dashed' });
// Price Summary
const summarySection = form.addSubform('summary', { title: '💰 Cost Estimate', isCollapsible: false });
// Furnishing multipliers
const furnishingMultipliers: Record<string, number> = {
'minimal': 0.8,
'average': 1,
'full': 1.2,
'packed': 1.4
};
// Packing costs by home size
const packingCosts: Record<string, Record<string, number>> = {
'none': { 'studio': 0, '1bed': 0, '2bed': 0, '3bed': 0, '4bed': 0, '5bed': 0 },
'partial': { 'studio': 150, '1bed': 200, '2bed': 300, '3bed': 350, '4bed': 400, '5bed': 450 },
'full': { 'studio': 300, '1bed': 400, '2bed': 550, '3bed': 700, '4bed': 850, '5bed': 1000 }
};
// Material costs by home size
const materialCosts: Record<string, number> = {
'studio': 75,
'1bed': 100,
'2bed': 175,
'3bed': 225,
'4bed': 275,
'5bed': 350
};
// Base fees for long distance
const baseFees: Record<string, number> = {
'studio': 400,
'1bed': 600,
'2bed': 900,
'3bed': 1200,
'4bed': 1600,
'5bed': 2000
};
// Helper to get miles from addresses (distanceUnit is set to 'miles')
const getMiles = () => {
const destField = moveTypeSection.address('destAddress');
const miles = destField?.distance();
if (miles == null) return 15; // default
return miles;
};
// Helper to get move type from distance
const getMoveType = () => {
const miles = getMiles();
if (miles < 50) return 'local';
if (miles < 500) return 'longDistance';
return 'crossCountry';
};
summarySection.addRow(row => {
row.addPriceDisplay('laborCost', {
label: 'Labor Cost',
computedValue: () => {
const distance = getMoveType();
const homeSize = homeSection.dropdown('homeSize')?.value() || '2bed';
const furnishing = homeSection.dropdown('furnishing')?.value() || 'average';
if (distance !== 'local') {
let fee = baseFees[homeSize] || 900;
fee *= furnishingMultipliers[furnishing] || 1;
return Math.round(fee);
}
const hours = homeSizeHours[homeSize] || 5;
const adjustedHours = hours * (furnishingMultipliers[furnishing] || 1);
let crewSize = '2movers';
if (homeSize === '3bed' || homeSize === '4bed') crewSize = '3movers';
if (homeSize === '5bed') crewSize = '4movers';
const rate = hourlyRates[crewSize] || 160;
return Math.round(adjustedHours * rate);
},
variant: 'default'
}, '1fr');
row.addPriceDisplay('travelFee', {
label: 'Travel / Mileage Fee',
computedValue: () => {
const distance = getMoveType();
const miles = getMiles();
if (distance === 'local') {
return 50 + Math.max(0, miles - 10) * 2;
}
return Math.round(miles * perMileRate);
},
variant: 'default',
prefix: '+'
}, '1fr');
});
summarySection.addRow(row => {
row.addPriceDisplay('floorFees', {
label: 'Floor/Stairs Fees',
computedValue: () => {
const originFloor = homeSection.dropdown('originFloor')?.value() || 'ground';
const destFloor = homeSection.dropdown('destFloor')?.value() || 'ground';
return (floorFees[originFloor] || 0) + (floorFees[destFloor] || 0);
},
variant: 'default',
prefix: '+'
}, '1fr');
row.addPriceDisplay('servicesFee', {
label: 'Additional Services',
computedValue: () => {
let total = 0;
const homeSize = homeSection.dropdown('homeSize')?.value() || '2bed';
const packingService = servicesSection.radioButton('packingService')?.value() || 'none';
total += packingCosts[packingService]?.[homeSize] || 0;
if (servicesSection.checkbox('packingMaterials')?.value()) {
total += materialCosts[homeSize] || 175;
}
if (servicesSection.checkbox('furnitureDisassembly')?.value()) {
total += 150;
}
if (servicesSection.checkbox('storageNeeded')?.value()) {
const days = servicesSection.integer('storageDays')?.value() || 7;
const dailyRate = homeSize === 'studio' || homeSize === '1bed' ? 10 :
homeSize === '2bed' || homeSize === '3bed' ? 15 : 20;
total += days * dailyRate + 100;
}
return total;
},
variant: 'default',
prefix: '+'
}, '1fr');
});
summarySection.addRow(row => {
row.addPriceDisplay('specialItems', {
label: 'Special Items',
computedValue: () => {
let total = 0;
if (specialSection.checkbox('piano')?.value()) total += 350;
if (specialSection.checkbox('hotTub')?.value()) total += 450;
if (specialSection.checkbox('poolTable')?.value()) total += 375;
if (specialSection.checkbox('safeLarge')?.value()) total += 225;
if (specialSection.checkbox('artwork')?.value()) total += 150;
if (specialSection.checkbox('gym')?.value()) total += 150;
return total;
},
variant: 'default',
prefix: '+'
}, '1fr');
row.addPriceDisplay('insuranceCost', {
label: 'Insurance',
computedValue: () => {
const coverage = insuranceSection.radioButton('coverage')?.value() || 'basic';
if (coverage === 'basic') return 0;
const declaredValue = insuranceSection.money('declaredValue')?.value() || 15000;
const rate = coverage === 'declared' ? 0.01 : 0.02;
return Math.round(declaredValue * rate);
},
variant: 'default',
prefix: '+'
}, '1fr');
});
summarySection.addRow(row => {
row.addTextPanel('tip', {
computedValue: () => 'Tip: Moving mid-week and mid-month typically saves 10-20%.',
customStyles: { 'font-size': '0.85rem', 'color': '#059669', 'background': '#ecfdf5', 'padding': '12px', 'border-radius': '6px' }
});
});
const finalSection = form.addSubform('final', {
title: '🧾 Summary',
isCollapsible: false,
sticky: 'bottom'
});
finalSection.addRow(row => {
row.addPriceDisplay('totalEstimate', {
label: 'Total Estimated Cost',
computedValue: () => {
const distance = getMoveType();
const miles = getMiles();
const homeSize = homeSection.dropdown('homeSize')?.value() || '2bed';
const furnishing = homeSection.dropdown('furnishing')?.value() || 'average';
let total = 0;
// Labor
if (distance === 'local') {
const hours = homeSizeHours[homeSize] || 5;
const adjustedHours = hours * (furnishingMultipliers[furnishing] || 1);
let crewSize = '2movers';
if (homeSize === '3bed' || homeSize === '4bed') crewSize = '3movers';
if (homeSize === '5bed') crewSize = '4movers';
total += adjustedHours * (hourlyRates[crewSize] || 160);
total += 50 + Math.max(0, miles - 10) * 2;
} else {
total += (baseFees[homeSize] || 900) * (furnishingMultipliers[furnishing] || 1);
total += miles * perMileRate;
}
// Floor fees
const originFloor = homeSection.dropdown('originFloor')?.value() || 'ground';
const destFloor = homeSection.dropdown('destFloor')?.value() || 'ground';
total += (floorFees[originFloor] || 0) + (floorFees[destFloor] || 0);
// Services
const packingService = servicesSection.radioButton('packingService')?.value() || 'none';
total += packingCosts[packingService]?.[homeSize] || 0;
if (servicesSection.checkbox('packingMaterials')?.value()) {
total += materialCosts[homeSize] || 175;
}
if (servicesSection.checkbox('furnitureDisassembly')?.value()) total += 150;
if (servicesSection.checkbox('storageNeeded')?.value()) {
const days = servicesSection.integer('storageDays')?.value() || 7;
const dailyRate = homeSize === 'studio' || homeSize === '1bed' ? 10 :
homeSize === '2bed' || homeSize === '3bed' ? 15 : 20;
total += days * dailyRate + 100;
}
// Special items
if (specialSection.checkbox('piano')?.value()) total += 350;
if (specialSection.checkbox('hotTub')?.value()) total += 450;
if (specialSection.checkbox('poolTable')?.value()) total += 375;
if (specialSection.checkbox('safeLarge')?.value()) total += 225;
if (specialSection.checkbox('artwork')?.value()) total += 150;
if (specialSection.checkbox('gym')?.value()) total += 150;
// Insurance
const coverage = insuranceSection.radioButton('coverage')?.value() || 'basic';
if (coverage !== 'basic') {
const declaredValue = insuranceSection.money('declaredValue')?.value() || 15000;
const rate = coverage === 'declared' ? 0.01 : 0.02;
total += declaredValue * rate;
}
// Seasonal adjustment
const moveDateVal = moveTypeSection.dropdown('moveDate')?.value() || 'standard';
if (moveDateVal === 'peak') total *= 1.2;
if (moveDateVal === 'holiday') total *= 1.3;
if (moveDateVal === 'offpeak') total *= 0.95;
return Math.round(total);
},
variant: 'large'
});
});
finalSection.addRow(row => {
row.addTextPanel('disclaimer', {
computedValue: () => 'Get quotes from at least 3 movers for best rates.',
customStyles: { 'font-size': '0.85rem', 'color': '#64748b', 'font-style': 'italic' }
});
});
form.configureSubmitButton({
label: 'Get Moving Quotes'
});
}