export function carportMetalBuildingCalculator(form: FormTs) {
// Price per square foot by building, delivered and installed (doors and windows on top)
const buildingRates: Record<string, number> = {
'carport': 7.5, // steel carport and RV cover: per sq ft of roof, 14-gauge frame, 29-gauge panels
'rv': 7.5,
'garage': 18, // enclosed steel garage: frame, roof and walls with base trim
'wood': 58, // wood-framed garage: 2x4 walls, trusses, lap siding, shingles, on the customer's slab
'barn': 30 // pole barn: posts in the ground, girts and purlins, vertical panels
};
const buildingNames: Record<string, string> = {
'carport': 'Steel carport',
'rv': 'RV cover',
'garage': 'Enclosed steel garage',
'wood': 'Wood-framed garage',
'barn': 'Pole barn'
};
// The height the rates are quoted at; every foot above carries a surcharge
const standardHeights: Record<string, number> = { 'carport': 6, 'rv': 12, 'garage': 8, 'wood': 8, 'barn': 10 };
// Typical order each building starts from (width x length x height in feet)
const typicalSizes: Record<string, number[]> = {
'carport': [18, 21, 6],
'rv': [12, 36, 12],
'garage': [24, 26, 9],
'wood': [24, 24, 9],
'barn': [30, 40, 12]
};
// Percent surcharges on the structure
const surcharges = {
boxedRoof: 12, // A-frame roof, horizontal panels, boxed eave
verticalRoof: 25, // panels down the slope, ridge cap, hat channels
legFoot: 5, // each foot of leg or wall above the standard height
gauge12: 12, // 2.5 in 12-gauge tubing instead of 14-gauge
certified: 8 // engineer-stamped drawings, extra bracing and anchors
};
const certifiedWindMph = 140;
const certifiedSnowPsf = 30;
const structureExtras = {
closedSidePerFt: 9, // panels from the eave to the ground, per linear foot of side
gableEnd: 350, // one end closed above the legs; the form counts two
insulationPerSqFt: 1.4, // double-bubble foil under the roof, and on the walls of an enclosed building
wainscotPerFt: 6 // 36 in band in the roof color along the bottom of the walls, per foot of perimeter
};
const doorPrices = {
rollupSmall: 750, // up to 10 x 8 ft
rollupLarge: 1250, // 10 x 10 ft and larger
sectional: 1600, // single insulated sectional door
sectionalDouble: 2700, // 16 x 7, 16 x 8, 18 x 8 ft
sliding: 1100, // sliding barn door on a track
opener: 550, // belt-drive opener, per sectional door
walkDoor: 450, // 36 x 80 in steel walk-in door
window: 325 // 30 x 36 in single-hung with a screen
};
const doorSizes: Record<string, string[]> = {
'rollup': ['8x7', '9x7', '9x8', '10x8', '10x10', '12x12', '12x14'],
'sectional': ['8x7', '9x7', '9x8', '10x8', '16x7', '16x8', '18x8'],
'sliding': ['10x10', '12x12', '14x14', '16x14']
};
const doorTypeNames: Record<string, string> = {
'rollup': 'Roll-up door',
'sectional': 'Sectional overhead door',
'sliding': 'Sliding barn door'
};
const sitePrices = {
slabPerSqFt: 8.5, // 4 in reinforced slab with a turned-down footer
gravelPerSqFt: 3.5, // compacted crushed-stone pad about 4 in deep
anchors: 180, // auger anchors for a steel building on gravel or bare ground
permit: 350, // drawings, application and inspections; county fee at cost
mileRate: 3.5, // per mile beyond the free delivery radius
freeMiles: 75
};
const padNames: Record<string, string> = {
'existing': 'My slab is ready',
'slab': 'Pour a concrete slab',
'gravel': 'Spread a gravel pad',
'ground': 'Set it on the ground with anchors'
};
const colorNames: Record<string, string> = {
'white': 'White', 'pewter': 'Pewter gray', 'charcoal': 'Charcoal', 'black': 'Black', 'red': 'Barn red', 'burgundy': 'Burgundy',
'green': 'Evergreen', 'tan': 'Tan', 'clay': 'Clay', 'brown': 'Brown', 'blue': 'Gallery blue'
};
const depositPercent = 10;
const prepayPercent = 3;
const minimumOrder = 1495;
// Promotion applied last on the whole order; 0 hides the banner
const promoPercent = 0;
const promoText = 'Free walk-in door with any garage';
const leadWeeks = 4;
// Steel comes in 5-ft bays: a 21 ft building has five bows
const FOOT_BAY = 5;
const money = (amount: number): string => {
const whole = String(Math.round(Math.abs(amount)));
let out = '';
for (let i = 0; i < whole.length; i++) {
out += whole.charAt(i);
const remaining = whole.length - i - 1;
if (remaining > 0 && remaining % 3 === 0) out += ',';
}
return (amount < 0 ? '-$' : '$') + out;
};
const perSqFt = (rate: number) => '$' + rate.toFixed(2) + '/sq ft';
const sizeText = (size: string) => size.replace('x', ' x ') + ' ft';
const colorOptions = (): ControlOption[] => Object.keys(colorNames).map(id => ({ id, name: colorNames[id] || id }));
form.addRow(row => {
row.addTextPanel('header', {
computedValue: () => 'Carport & Metal Building Quote',
customStyles: { 'font-size': '1.5rem', 'font-weight': '600', 'color': '#1e293b' }
});
});
form.addRow(row => {
row.addTextPanel('intro', {
computedValue: () => 'Pick the building, set the size in feet, choose the roof, colors and doors. Delivery and installation are included within ' + sitePrices.freeMiles + ' miles.',
customStyles: { 'font-size': '0.95rem', 'color': '#475569' }
});
});
form.addSpacer({ height: 20 });
// Pages are created up front so the building tile can preset the sliders of the next step
const steps = form.addPages('steps', { heightMode: 'current-page' });
const buildingPage = steps.addPage('building');
const configPage = steps.addPage('configure');
const sitePage = steps.addPage('site');
const quotePage = steps.addPage('quote');
// ---- Step 1: the building ------------------------------------------------------
const buildingSection = buildingPage.addSubform('building', { title: '🏠 What Are We Building?', isCollapsible: false });
// The sections of the next step, filled in below; the tile presets their fields once they exist
let configSection: SubformTs | undefined;
let doorsSection: SubformTs | undefined;
// The tile sets the size and doors a typical order of that building has
const applyBuilding = (id: string) => {
const size = typicalSizes[id];
if (!size) return;
configSection?.slider('width')?.setValue(size[0]);
configSection?.slider('length')?.setValue(size[1]);
configSection?.slider('height')?.setValue(size[2]);
configSection?.radioButton('roof')?.setValue(id === 'carport' ? 'regular' : id === 'garage' ? 'boxed' : 'vertical');
doorsSection?.integer('doors')?.setValue(id === 'garage' ? 2 : id === 'wood' || id === 'barn' ? 1 : 0);
doorsSection?.radioButton('doorType')?.setValue(id === 'wood' ? 'sectional' : id === 'barn' ? 'sliding' : 'rollup');
doorsSection?.dropdown('doorSize')?.setValue(id === 'wood' ? '16x7' : id === 'barn' ? '12x12' : '9x7');
};
buildingSection.addRow(row => {
row.addRadioButton('kind', {
label: 'Building type (each starts from a typical size; you set every dimension on the next step)',
options: [
{ id: 'carport', name: 'Steel carport - legs and a roof, open or closed sides (' + perSqFt(buildingRates['carport'] || 0) + ')' },
{ id: 'rv', name: 'RV cover - tall legs, a vertical roof for the length (' + perSqFt(buildingRates['rv'] || 0) + ')' },
{ id: 'garage', name: 'Enclosed steel garage - walls, roll-up doors, lockable (' + perSqFt(buildingRates['garage'] || 0) + ')' },
{ id: 'wood', name: 'Wood-framed garage - siding and shingles to match the house (' + perSqFt(buildingRates['wood'] || 0) + ')' },
{ id: 'barn', name: 'Pole barn - post-frame, the biggest for the money (' + perSqFt(buildingRates['barn'] || 0) + ')' }
],
defaultValue: 'carport',
orientation: 'vertical',
isRequired: true,
onValueChange: value => {
if (typeof value === 'string') applyBuilding(value);
}
});
});
buildingSection.addRow(row => {
row.addTextPanel('included', {
computedValue: () => 'Delivery and installation are included: our crew delivers and installs within ' + sitePrices.freeMiles + ' miles at no charge. You prepare a level site (a slab, a gravel pad or level ground within 3 inches), or we do it and the site step prices it.',
customStyles: { 'font-size': '0.9rem', 'color': '#0369a1', 'background': '#e0f2fe', 'padding': '10px', 'border-radius': '6px' }
});
});
const kind = form.computedValue(() => {
const raw = buildingSection.radioButton('kind')?.value() || 'carport';
return buildingRates[raw] !== undefined ? raw : 'carport';
});
const isCarport = form.computedValue(() => kind() === 'carport' || kind() === 'rv');
const isSteel = form.computedValue(() => kind() !== 'wood' && kind() !== 'barn');
const hasWalls = form.computedValue(() => !isCarport());
const hasRoofChoice = form.computedValue(() => isSteel());
const standardHeight = form.computedValue(() => standardHeights[kind()] || 6);
// ---- Step 2: size, roof, colors, structure ------------------------------------
configSection = configPage.addSubform('size', { title: '📐 Size, Roof & Colors', isCollapsible: false });
configSection.addRow(row => {
row.addSlider('width', {
label: 'Width (one car needs 12 ft, two cars 18 to 24 ft)',
min: 10,
max: 60,
step: 1,
unit: 'ft',
defaultValue: 18
}, '1fr');
row.addSlider('length', {
label: 'Length (a car needs 20 ft, a truck 26 ft)',
min: 10,
max: 80,
step: 1,
unit: 'ft',
defaultValue: 21
}, '1fr');
row.addSlider('height', {
label: () => (isCarport() ? 'Leg height' : 'Wall height') + ' to the eave (standard ' + standardHeight() + ' ft, each foot above +' + surcharges.legFoot + '%)',
min: 6,
max: 16,
step: 1,
unit: 'ft',
defaultValue: 6
}, '1fr');
});
const widthFt = form.computedValue(() => configSection.slider('width')?.value() || 18);
const lengthFt = form.computedValue(() => configSection.slider('length')?.value() || 21);
const heightFt = form.computedValue(() => configSection.slider('height')?.value() || 6);
// Steel is built to the nearest 5-ft bay; the order is written for the built length
const lengthBuilt = form.computedValue(() => isSteel() ? Math.max(11, Math.round((lengthFt() - 1) / FOOT_BAY) * FOOT_BAY + 1) : lengthFt());
const bays = form.computedValue(() => Math.round((lengthBuilt() - 1) / FOOT_BAY));
const area = form.computedValue(() => widthFt() * lengthBuilt());
configSection.addRow(row => {
row.addTextPanel('sizeInfo', {
computedValue: () => widthFt() + ' x ' + lengthBuilt() + ' ft, ' + area() + ' sq ft'
+ (isSteel() ? ' - steel comes in 5-ft bays: built as ' + lengthBuilt() + ' ft, ' + bays() + ' bays' : ''),
customStyles: { 'font-size': '0.9rem', 'color': '#059669', 'font-weight': '500' }
});
});
configSection.addRow(row => {
row.addRadioButton('roof', {
label: 'Roof style',
options: [
{ id: 'regular', name: 'Regular - rounded eaves, horizontal panels (included)' },
{ id: 'boxed', name: 'Boxed eave - A-frame look, horizontal panels (+' + surcharges.boxedRoof + '%)' },
{ id: 'vertical', name: 'Vertical - panels down the slope, sheds snow (+' + surcharges.verticalRoof + '%)' }
],
defaultValue: 'regular',
orientation: 'vertical',
isVisible: () => hasRoofChoice()
}, '1fr');
row.addTextPanel('roofNote', {
computedValue: () => kind() === 'wood' ? 'A wood garage always has architectural shingles.' : 'A pole barn has a vertical panel roof.',
customStyles: { 'font-size': '0.9rem', 'color': '#64748b' },
isVisible: () => !hasRoofChoice()
}, '1fr');
});
const roof = form.computedValue(() => kind() === 'wood' ? 'shingle' : kind() === 'barn' ? 'vertical' : (configSection.radioButton('roof')?.value() || 'regular'));
const roofName = form.computedValue(() => roof() === 'shingle' ? 'Architectural shingles' : roof() === 'boxed' ? 'Boxed eave' : roof() === 'vertical' ? 'Vertical' : 'Regular');
configSection.addRow(row => {
row.addDropdown('roofColor', {
label: () => roof() === 'shingle' ? 'Shingle color' : 'Roof color',
options: colorOptions(),
defaultValue: 'charcoal'
}, '1fr');
row.addDropdown('wallColor', {
label: () => kind() === 'wood' ? 'Siding color' : isCarport() ? 'Side and trim base color' : 'Side color',
options: colorOptions(),
defaultValue: 'pewter'
}, '1fr');
row.addDropdown('trimColor', {
label: 'Trim',
options: [
{ id: 'white', name: 'White' },
{ id: 'black', name: 'Black' },
{ id: 'match', name: 'Match the sides' }
],
defaultValue: 'white'
}, '1fr');
});
const roofColor = form.computedValue(() => configSection.dropdown('roofColor')?.value() || 'charcoal');
const wallColor = form.computedValue(() => configSection.dropdown('wallColor')?.value() || 'pewter');
const trimColor = form.computedValue(() => configSection.dropdown('trimColor')?.value() || 'white');
const structureSection = configPage.addSubform('structure', {
title: '🛡️ Structure',
isCollapsible: false,
isVisible: () => isSteel()
});
structureSection.addRow(row => {
row.addRadioButton('gauge', {
label: 'Frame (12-gauge tubing is thicker and carries a longer frame warranty)',
options: [
{ id: '14', name: '14 gauge (standard)' },
{ id: '12', name: '12 gauge (+' + surcharges.gauge12 + '%)' }
],
defaultValue: '14',
orientation: 'vertical'
}, '1fr');
row.addRadioButton('certification', {
label: 'Certification (most counties ask for it with the permit)',
options: [
{ id: 'standard', name: 'Standard' },
{ id: 'certified', name: 'Certified: engineer-stamped drawings for ' + certifiedWindMph + ' mph wind and ' + certifiedSnowPsf + ' psf snow (+' + surcharges.certified + '%)' }
],
defaultValue: 'standard',
orientation: 'vertical'
}, '1fr');
});
structureSection.addRow(row => {
row.addRadioButton('sides', {
label: 'Sides (closed sides keep the rain and the sun off what stands under the roof)',
options: [
{ id: 'open', name: 'Open' },
{ id: 'closed', name: 'Closed with panels (+' + money(structureExtras.closedSidePerFt) + '/ft, both sides)' }
],
defaultValue: 'open',
orientation: 'vertical',
isVisible: () => isCarport()
}, '1fr');
row.addCheckbox('gableEnds', {
label: 'Gable ends - both ends closed above the legs (+' + money(2 * structureExtras.gableEnd) + ')',
defaultValue: false,
isVisible: () => isCarport() && roof() !== 'regular'
}, '1fr');
});
const gauge12 = form.computedValue(() => isSteel() && structureSection.radioButton('gauge')?.value() === '12');
const certified = form.computedValue(() => isSteel() && structureSection.radioButton('certification')?.value() === 'certified');
const closedSides = form.computedValue(() => isCarport() && structureSection.radioButton('sides')?.value() === 'closed');
const gableEnds = form.computedValue(() => isCarport() && roof() !== 'regular' && structureSection.checkbox('gableEnds')?.value() === true);
// ---- Doors, windows and extras ---------------------------------------------------
doorsSection = configPage.addSubform('doors', {
title: '🚪 Doors & Windows',
isCollapsible: false,
isVisible: () => hasWalls()
});
doorsSection.addRow(row => {
row.addRadioButton('doorType', {
label: 'Garage doors',
options: [
{ id: 'rollup', name: 'Roll-up door - rolls into a drum over the opening (from ' + money(doorPrices.rollupSmall) + ')' },
{ id: 'sectional', name: 'Sectional overhead - insulated panels on tracks, takes an opener (from ' + money(doorPrices.sectional) + ')' },
{ id: 'sliding', name: 'Sliding barn door - on a track over the wall, no headroom needed (' + money(doorPrices.sliding) + ')' }
],
defaultValue: 'rollup',
orientation: 'vertical'
}, '1fr');
row.addInteger('doors', {
label: 'How many garage doors (0 = storage building with a walk-in door only)',
min: 0,
max: 2,
defaultValue: 0
}, '1fr');
});
const doorCount = form.computedValue(() => hasWalls() ? (doorsSection.integer('doors')?.value() || 0) : 0);
const doorType = form.computedValue(() => {
const raw = doorsSection.radioButton('doorType')?.value() || 'rollup';
return doorSizes[raw] ? raw : 'rollup';
});
// Which price a door of this type at this size takes
const doorPrice = (type: string, size: string): number => {
const w = Number(size.split('x')[0] || 0);
const h = Number(size.split('x')[1] || 0);
if (type === 'sliding') return doorPrices.sliding;
if (type === 'sectional') return w >= 14 ? doorPrices.sectionalDouble : doorPrices.sectional;
return w >= 10 && h >= 10 ? doorPrices.rollupLarge : doorPrices.rollupSmall;
};
doorsSection.addRow(row => {
row.addDropdown('doorSize', {
label: 'Door size, width x height',
options: () => (doorSizes[doorType()] || []).map(size => ({ id: size, name: sizeText(size) + ' - ' + money(doorPrice(doorType(), size)) })),
defaultValue: '9x7',
isVisible: () => doorCount() > 0
}, '1fr');
row.addCheckbox('opener', {
label: () => 'Door opener - belt drive, two remotes and safety sensors (+' + money(doorPrices.opener) + ' per door)',
defaultValue: false,
isVisible: () => doorCount() > 0 && doorType() === 'sectional'
}, '1fr');
});
// The chosen size if the door type comes in it, else the first size the type is made in
const doorSize = form.computedValue(() => {
const sizes = doorSizes[doorType()] || ['9x7'];
const raw = doorsSection.dropdown('doorSize')?.value() || '';
return sizes.includes(raw) ? raw : (sizes[0] || '9x7');
});
const doorW = form.computedValue(() => Number(doorSize().split('x')[0] || 0));
const doorH = form.computedValue(() => Number(doorSize().split('x')[1] || 0));
const opener = form.computedValue(() => doorCount() > 0 && doorType() === 'sectional' && doorsSection.checkbox('opener')?.value() === true);
doorsSection.addRow(row => {
row.addTextPanel('doorHint', {
computedValue: () => {
if (doorCount() > 0 && doorCount() * doorW() + 2 * doorCount() > widthFt()) return 'Too wide for the front: pick a narrower door or a wider building.';
if (doorCount() > 0 && heightFt() < doorH() + 1) return 'A ' + doorH() + ' ft door needs walls at least ' + (doorH() + 1) + ' ft tall.';
return 'Doors are framed into the front wall and installed.';
},
customStyles: { 'font-size': '0.9rem', 'color': '#64748b' }
});
});
doorsSection.addRow(row => {
row.addCheckbox('walkDoor', {
label: 'Walk-in door - 36 x 80 in steel door with a lockset (+' + money(doorPrices.walkDoor) + ')',
defaultValue: false
}, '1fr');
row.addInteger('windows', {
label: 'Windows - 30 x 36 in single-hung with a screen (' + money(doorPrices.window) + ' each)',
min: 0,
max: 4,
defaultValue: 0
}, '1fr');
});
const walkDoor = form.computedValue(() => hasWalls() && doorsSection.checkbox('walkDoor')?.value() === true);
const windows = form.computedValue(() => hasWalls() ? (doorsSection.integer('windows')?.value() || 0) : 0);
const extrasSection = configPage.addSubform('extras', { title: '✨ Extras', isCollapsible: false });
extrasSection.addRow(row => {
row.addCheckbox('insulation', {
label: () => (isCarport() ? 'Insulation - foil bubble under the roof, stops the drip' : 'Insulation - foil bubble under the roof and on the walls') + ' (' + perSqFt(structureExtras.insulationPerSqFt) + ')',
defaultValue: false
}, '1fr');
row.addCheckbox('wainscot', {
label: 'Wainscot - a 36 in band in the roof color along the bottom of the walls (+' + money(structureExtras.wainscotPerFt) + '/ft)',
defaultValue: false,
isVisible: () => kind() === 'garage' || kind() === 'barn'
}, '1fr');
});
const insulation = form.computedValue(() => extrasSection.checkbox('insulation')?.value() === true);
const wainscot = form.computedValue(() => (kind() === 'garage' || kind() === 'barn') && extrasSection.checkbox('wainscot')?.value() === true);
// ---- Step 3: the site ------------------------------------------------------------
const siteSection = sitePage.addSubform('site', { title: '🏗️ Site, Delivery & Payment', isCollapsible: false });
siteSection.addRow(row => {
row.addRadioButton('pad', {
label: 'What does the building stand on?',
options: () => {
const options: ControlOption[] = [
{ id: 'existing', name: 'My slab is ready - level concrete, at least the size of the building' },
{ id: 'slab', name: 'Pour a concrete slab - 4 in reinforced with a turned-down footer (+' + perSqFt(sitePrices.slabPerSqFt) + ')' }
];
if (kind() !== 'wood') options.push({ id: 'gravel', name: 'Spread a gravel pad - compacted crushed stone, leveled (+' + perSqFt(sitePrices.gravelPerSqFt) + (isSteel() ? ' plus ' + money(sitePrices.anchors) + ' anchors' : '') + ')' });
if (isSteel()) options.push({ id: 'ground', name: 'Set it on the ground - level grass or dirt, auger anchors (+' + money(sitePrices.anchors) + ')' });
return options;
},
defaultValue: 'existing',
orientation: 'vertical',
isRequired: true
});
});
// The chosen pad if the building can stand on it, else the customer's slab
const pad = form.computedValue(() => {
const raw = siteSection.radioButton('pad')?.value() || 'existing';
if (raw === 'gravel' && kind() === 'wood') return 'existing';
if (raw === 'ground' && !isSteel()) return 'existing';
return padNames[raw] ? raw : 'existing';
});
siteSection.addRow(row => {
row.addSlider('distance', {
label: 'Distance from our yard (free within ' + sitePrices.freeMiles + ' miles, then $' + sitePrices.mileRate.toFixed(2) + '/mile)',
min: 0,
max: 300,
step: 5,
unit: 'mi',
defaultValue: 30
}, '1fr');
row.addDropdown('timing', {
label: 'When do you need it?',
options: [
{ id: 'asap', name: 'As soon as possible' },
{ id: 'quarter', name: 'Within 3 months' },
{ id: 'year', name: 'This year' },
{ id: 'comparing', name: 'Just comparing' }
],
defaultValue: 'quarter'
}, '1fr');
});
siteSection.addRow(row => {
row.addCheckbox('permit', {
label: 'Pull the permit for me - drawings, application and inspections; the county fee at cost (+' + money(sitePrices.permit) + ')',
defaultValue: false
}, '1fr');
row.addCheckbox('prepay', {
label: 'Pay in full at the order instead of a ' + depositPercent + '% deposit (' + prepayPercent + '% off)',
defaultValue: false
}, '1fr');
});
const distance = form.computedValue(() => siteSection.slider('distance')?.value() || 0);
const permit = form.computedValue(() => siteSection.checkbox('permit')?.value() === true);
const prepay = form.computedValue(() => siteSection.checkbox('prepay')?.value() === true);
const timingNames: Record<string, string> = { asap: 'As soon as possible', quarter: 'Within 3 months', year: 'This year', comparing: 'Just comparing' };
const timing = form.computedValue(() => siteSection.dropdown('timing')?.value() || 'quarter');
// ---- Price logic -----------------------------------------------------------------
const legSteps = form.computedValue(() => Math.max(0, Math.ceil(heightFt() - standardHeight())));
const roofPercent = form.computedValue(() => {
if (!hasRoofChoice()) return 0;
return roof() === 'boxed' ? surcharges.boxedRoof : roof() === 'vertical' ? surcharges.verticalRoof : 0;
});
// Structure: the area times the rate, then the roof, the legs, the gauge and the certification
const structure = form.computedValue(() => {
const base = area() * (buildingRates[kind()] || 0);
const forRoof = 1 + roofPercent() / 100;
const forLegs = 1 + legSteps() * surcharges.legFoot / 100;
const forGauge = gauge12() ? 1 + surcharges.gauge12 / 100 : 1;
const forCert = certified() ? 1 + surcharges.certified / 100 : 1;
return base * forRoof * forLegs * forGauge * forCert;
});
const sidesCost = form.computedValue(() => (closedSides() ? 2 * lengthBuilt() * structureExtras.closedSidePerFt : 0) + (gableEnds() ? 2 * structureExtras.gableEnd : 0));
const roofArea = form.computedValue(() => area() * 1.05);
const wallArea = form.computedValue(() => 2 * (widthFt() + lengthBuilt()) * heightFt());
const insulatedArea = form.computedValue(() => hasWalls() ? roofArea() + wallArea() : roofArea());
const insulationCost = form.computedValue(() => insulation() ? insulatedArea() * structureExtras.insulationPerSqFt : 0);
const perimeter = form.computedValue(() => 2 * (widthFt() + lengthBuilt()));
const wainscotCost = form.computedValue(() => wainscot() ? perimeter() * structureExtras.wainscotPerFt : 0);
const doorsCost = form.computedValue(() => {
if (!hasWalls()) return 0;
let sum = doorCount() * doorPrice(doorType(), doorSize());
if (opener()) sum += doorCount() * doorPrices.opener;
if (walkDoor()) sum += doorPrices.walkDoor;
sum += windows() * doorPrices.window;
return sum;
});
const padCost = form.computedValue(() => {
if (pad() === 'slab') return area() * sitePrices.slabPerSqFt;
if (pad() === 'gravel') return area() * sitePrices.gravelPerSqFt + (isSteel() ? sitePrices.anchors : 0);
if (pad() === 'ground') return isSteel() ? sitePrices.anchors : 0;
return 0;
});
const permitCost = form.computedValue(() => permit() ? sitePrices.permit : 0);
const delivery = form.computedValue(() => Math.max(0, distance() - sitePrices.freeMiles) * sitePrices.mileRate);
const subtotal = form.computedValue(() => structure() + sidesCost() + insulationCost() + wainscotCost() + doorsCost() + padCost() + permitCost() + delivery());
const prepayDiscount = form.computedValue(() => prepay() ? subtotal() * prepayPercent / 100 : 0);
const beforePromo = form.computedValue(() => subtotal() - prepayDiscount());
const promo = form.computedValue(() => beforePromo() * promoPercent / 100);
// The minimum holds last: a promotion never takes an order below what the steel costs
const total = form.computedValue(() => Math.max(Math.round(beforePromo() - promo()), minimumOrder));
const deposit = form.computedValue(() => prepay() ? total() : Math.round(total() * depositPercent / 100));
const describeSize = (): string => widthFt() + ' x ' + lengthBuilt() + ' ft, ' + heightFt() + ' ft ' + (isCarport() ? 'legs' : 'walls');
const describeColors = (): string => 'roof ' + (colorNames[roofColor()] || '').toLowerCase() + ', sides ' + (colorNames[wallColor()] || '').toLowerCase() + ', trim ' + (trimColor() === 'match' ? 'to match the sides' : trimColor());
const describeFrame = (): string => isSteel()
? (gauge12() ? '12-gauge' : '14-gauge') + ' frame, ' + (certified() ? 'certified ' + certifiedWindMph + ' mph / ' + certifiedSnowPsf + ' psf' : 'standard, not certified')
: kind() === 'wood' ? 'stick-built 2x4 walls, trusses' : 'pressure-treated posts, girts and purlins';
const describeDoors = (): string => {
if (!hasWalls()) return 'none';
if (doorCount() === 0) return 'no garage door';
return doorCount() + ' x ' + (doorTypeNames[doorType()] || '').toLowerCase() + ' ' + sizeText(doorSize()) + (opener() ? ', with opener' : '');
};
const describeExtras = (): string => {
const parts: string[] = [];
if (closedSides()) parts.push('closed sides');
if (gableEnds()) parts.push('gable ends');
if (walkDoor()) parts.push('walk-in door');
if (windows() > 0) parts.push(windows() + ' window' + (windows() > 1 ? 's' : ''));
if (insulation()) parts.push('insulation');
if (wainscot()) parts.push('wainscot');
return parts.join(', ') || 'none';
};
// ---- Step 4: the quote and contact details ----------------------------------------
quotePage.addRow(row => {
row.addTextPanel('promoBanner', {
computedValue: () => promoText + ': the ' + promoPercent + '% discount is already in the price below.',
customStyles: { 'font-size': '0.95rem', 'font-weight': '600', 'color': '#166534', 'background': '#dcfce7', 'padding': '10px', 'border-radius': '6px' },
isVisible: () => promoPercent > 0
});
});
const summarySection = quotePage.addSubform('summary', { title: '💰 Your Quote', isCollapsible: false });
summarySection.addRow(row => {
row.addPriceDisplay('structureCost', {
label: () => (buildingNames[kind()] || '') + ', ' + area() + ' sq ft (' + describeSize() + ', ' + roofName().toLowerCase() + ')',
computedValue: () => Math.round(structure()),
decimals: 0,
variant: 'default'
}, '1fr');
row.addPriceDisplay('sidesCost', {
label: 'Closed sides & gable ends',
computedValue: () => Math.round(sidesCost()),
decimals: 0,
variant: 'default',
prefix: '+',
isVisible: () => sidesCost() > 0
}, '1fr');
});
summarySection.addRow(row => {
row.addPriceDisplay('doorsCost', {
label: () => 'Doors & windows (' + describeDoors() + ')',
computedValue: () => Math.round(doorsCost()),
decimals: 0,
variant: 'default',
prefix: '+',
isVisible: () => hasWalls()
}, '1fr');
row.addPriceDisplay('extrasCost', {
label: () => 'Insulation & wainscot' + (insulation() ? ' (' + Math.round(insulatedArea()) + ' sq ft insulated)' : ''),
computedValue: () => Math.round(insulationCost() + wainscotCost()),
decimals: 0,
variant: 'default',
prefix: '+',
isVisible: () => insulationCost() + wainscotCost() > 0
}, '1fr');
});
summarySection.addRow(row => {
row.addPriceDisplay('siteWorkCost', {
label: () => 'Site work (' + (padNames[pad()] || '').toLowerCase() + (permit() ? ', permit handled by us' : ', permit by the owner') + ')',
computedValue: () => Math.round(padCost() + permitCost()),
decimals: 0,
variant: 'default',
prefix: '+'
}, '1fr');
row.addPriceDisplay('deliveryCost', {
label: () => 'Delivery & installation (' + distance() + ' mi from our yard' + (delivery() === 0 ? ', included' : '') + ')',
computedValue: () => Math.round(delivery()),
decimals: 0,
variant: 'default',
prefix: '+'
}, '1fr');
});
summarySection.addRow(row => {
row.addPriceDisplay('prepayDiscount', {
label: 'Paid in full at the order (' + prepayPercent + '% off)',
computedValue: () => -Math.round(prepayDiscount()),
decimals: 0,
variant: 'success',
isVisible: () => prepayDiscount() > 0
}, '1fr');
row.addPriceDisplay('promoDiscount', {
label: () => promoText + ' (-' + promoPercent + '%)',
computedValue: () => -Math.round(promo()),
decimals: 0,
variant: 'success',
isVisible: () => promo() > 0
}, '1fr');
});
summarySection.addSpacer({ showLine: true, lineStyle: 'solid', lineColor: '#e2e8f0' });
summarySection.addRow(row => {
row.addPriceDisplay('total', {
label: () => (buildingNames[kind()] || '') + ', delivered and installed',
computedValue: () => total(),
decimals: 0,
variant: 'large',
suffix: 'plus sales tax'
});
});
summarySection.addRow(row => {
row.addTextPanel('schedule', {
computedValue: () => 'From the order to the keys: '
+ (prepay() ? money(deposit()) + ' paid in full at the order' : money(deposit()) + ' deposit at the order, ' + money(total() - deposit()) + ' due when the crew is done')
+ '. ' + (permit() ? 'We pull the permit' + (certified() ? ' with the engineer-stamped drawings' : '') + '.' : 'Where your county requires a permit, you pull it' + (certified() ? '; we supply the engineer-stamped drawings' : '') + '.')
+ ' ' + (pad() === 'existing' ? 'You have the site level within 3 inches before the crew arrives.' : 'Our crew does the site work a few days before the building.')
+ ' Delivery and installation about ' + leadWeeks + ' weeks from the order; we call first to confirm the color, the site and the access for the truck.',
customStyles: { 'font-size': '0.9rem', 'color': '#0369a1', 'background': '#e0f2fe', 'padding': '10px', 'border-radius': '6px' }
});
});
summarySection.addRow(row => {
row.addTextPanel('minimumNote', {
computedValue: () => 'Orders below ' + money(minimumOrder) + ' are priced at the minimum, because a delivery has to pay for itself.',
customStyles: { 'font-size': '0.85rem', 'color': '#64748b', 'font-style': 'italic' },
isVisible: () => Math.round(beforePromo() - promo()) < minimumOrder
});
});
const contactSection = quotePage.addSubform('contact', { title: '📋 Your Contact Details', isCollapsible: false });
contactSection.addRow(row => {
row.addTextbox('name', {
label: 'Full name',
placeholder: 'John Smith',
isRequired: true
}, '1fr');
row.addTextbox('phone', {
label: 'Phone',
placeholder: '(336) 555-0100',
isRequired: true
}, '1fr');
});
contactSection.addRow(row => {
row.addEmail('email', {
label: 'Email (where we send the PDF quote)',
placeholder: 'john@example.com',
isRequired: true
}, '1fr');
row.addTextbox('address', {
label: 'Installation address',
placeholder: '1204 Pine Ridge Rd',
isRequired: true
}, '1fr');
});
contactSection.addRow(row => {
row.addTextbox('cityState', {
label: 'City, state',
placeholder: 'Mount Airy, NC',
isRequired: true
}, '1fr');
row.addTextbox('zip', {
label: 'ZIP code',
placeholder: '27030',
isRequired: true
}, '1fr');
});
contactSection.addRow(row => {
row.addTextarea('notes', {
label: 'Anything else we should know?',
placeholder: 'E.g. the site is behind the house, the gate is 10 ft wide, the HOA wants the same color as the roof of the house',
rows: 3,
isRequired: false
});
});
// Sticky total, visible on every step
const finalSection = form.addSubform('final', {
title: '🧾 Summary',
isCollapsible: false,
sticky: 'bottom'
});
finalSection.addRow(row => {
row.addPriceDisplay('totalPrice', {
label: () => (buildingNames[kind()] || '') + ', ' + describeSize(),
computedValue: () => total(),
decimals: 0,
variant: 'large',
suffix: 'delivered and installed, plus sales tax'
});
});
finalSection.addRow(row => {
row.addTextPanel('disclaimer', {
computedValue: () => 'Site preparation and permit as chosen on the site step. The order is confirmed by phone.',
customStyles: { 'font-size': '0.85rem', 'color': '#94a3b8', 'font-style': 'italic' }
});
});
const fullAddress = () => [contactSection.textbox('address')?.value(), contactSection.textbox('cityState')?.value(), contactSection.textbox('zip')?.value()].filter(Boolean).join(', ');
// ---- PDF quote ---------------------------------------------------------------------
form.configurePdf('quote', pdf => {
pdf.configure({
filename: 'building-quote',
pageSize: 'Letter',
allowUserDownload: true,
downloadButtonLabel: 'Download the quote (PDF)',
header: {
title: 'Quote: ' + (buildingNames[kind()] || '').toLowerCase(),
subtitle: describeSize() + ' - ' + roofName().toLowerCase()
},
footer: {
text: 'Delivery and installation included. Site preparation, permit and sales tax as noted.',
showPageNumbers: true
}
});
pdf.addSection('Customer', section => {
section.addRow(row => row
.addField('Name', contactSection.textbox('name')?.value() || '')
.addField('Phone', contactSection.textbox('phone')?.value() || '')
.addField('Email', contactSection.email('email')?.value() || '')
.addField('Installation address', fullAddress()));
});
pdf.addSection('Building', section => {
section.addRow(row => row
.addField('Type', buildingNames[kind()] || '')
.addField('Size', describeSize() + (isSteel() ? ' (' + bays() + ' bays)' : ''))
.addField('Floor area', area() + ' sq ft')
.addField('Roof', roofName()));
section.addRow(row => row
.addField('Colors', describeColors())
.addField('Frame', describeFrame())
.addField('Doors', describeDoors())
.addField('Extras', describeExtras()));
});
pdf.addSection('Site', section => {
section.addRow(row => row
.addField('Pad', padNames[pad()] || '')
.addField('Permit', permit() ? 'pulled by us' : 'by the owner, where required')
.addField('Distance', distance() + ' mi from our yard')
.addField('Timing', timingNames[timing()] || ''));
});
pdf.addSection('Quote', section => {
const rows: string[][] = [
[(buildingNames[kind()] || '') + ', ' + area() + ' sq ft', describeSize() + ', ' + roofName().toLowerCase(), money(structure())]
];
if (sidesCost() > 0) rows.push(['Closed sides and gable ends', [closedSides() ? 'both sides' : '', gableEnds() ? 'both ends' : ''].filter(Boolean).join(', '), money(sidesCost())]);
if (insulationCost() > 0) rows.push(['Insulation', Math.round(insulatedArea()) + ' sq ft', money(insulationCost())]);
if (wainscotCost() > 0) rows.push(['Wainscot', perimeter() + ' ft', money(wainscotCost())]);
if (hasWalls()) rows.push(['Doors and windows', describeDoors() + (walkDoor() ? ', walk-in door' : '') + (windows() > 0 ? ', ' + windows() + ' window' + (windows() > 1 ? 's' : '') : ''), money(doorsCost())]);
rows.push(['Site work', (padNames[pad()] || '') + (permit() ? ', permit' : ''), money(padCost() + permitCost())]);
rows.push(['Delivery and installation', delivery() === 0 ? 'included' : distance() + ' mi', money(delivery())]);
rows.push(['Subtotal', '', money(subtotal())]);
if (prepayDiscount() > 0) rows.push(['Paid in full at the order', prepayPercent + '%', '-' + money(prepayDiscount())]);
if (promo() > 0) rows.push([promoText, promoPercent + '%', '-' + money(promo())]);
rows.push(['Total', 'plus sales tax', money(total())]);
section.addTable(['Item', 'Details', 'Amount'], rows);
section.addSpacer(6);
section.addText(prepay()
? 'Paid in full at the order: ' + money(total()) + '.'
: money(deposit()) + ' deposit at the order, ' + money(total() - deposit()) + ' at installation.');
});
pdf.addSection('What happens next', section => {
section.addText('We call to confirm the color, the site and the access for the delivery truck. '
+ (permit() ? 'We pull the building permit' + (certified() ? ' with the engineer-stamped drawings' : '') + '; the county fee is billed at cost. ' : 'Where your county requires a permit, you pull it' + (certified() ? '; we supply the engineer-stamped drawings' : '') + '. ')
+ (pad() === 'existing' ? 'You have the site level within 3 inches and clear before the crew arrives. ' : 'Our crew does the site work a few days before the building. ')
+ 'Delivery and installation about ' + leadWeeks + ' weeks from the order' + (prepay() ? '.' : '; the balance is due when the crew is done.'));
});
const notes = contactSection.textarea('notes')?.value();
if (notes) {
pdf.addSection('Notes', section => section.addText(notes));
}
pdf.addSection('Terms', section => {
section.addText('Prices include the building, delivery and installation on a prepared site; sales tax not included. Site preparation and permit as noted above. This quote is not an offer to contract; the order is confirmed by phone.');
});
});
form.configureSubmitButton({
label: 'Send My Configuration for a Quote'
});
form.configureCompletionScreen({
type: 'text',
title: 'Thank you!',
message: 'The quote is on its way to your email. We will call to confirm the site and the access for the truck; delivery and installation about ' + leadWeeks + ' weeks from the order.'
});
}