export function windowReplacementCalculator(form: FormTs) {
// Window prices: white vinyl, smallest size tier, double-pane Low-E, no installation
const windowPrices: Record<string, number> = {
'double-hung': 420,
'single-hung': 360,
'slider': 400,
'casement': 520,
'awning': 480,
'picture': 340,
'bay-bow': 2900 // one unit with seat board and roof; size tiers do not apply
};
// Size tiers by united inches (width + height)
const tier1MaxUnitedInches = 72;
const tier2MaxUnitedInches = 101;
const tier2Pct = 35;
const tier3Pct = 75;
// Frame material surcharges on the window price
const materialPct: Record<string, number> = { 'vinyl': 0, 'fiberglass': 45, 'wood-clad': 85, 'aluminum': 30 };
// Glass and options
const triplePanePct = 22;
const energyStarPct = 8;
const temperedEach = 90;
const obscureEach = 60;
const gridsEach = 75;
const colorExteriorEach = 120; // tan or black exterior; white is included
// Window installation
const installEach = 175; // insert (pocket) installation per opening
const fullFrameEach = 225; // added per opening when the old frame comes out to the studs
const disposalEach = 25;
const trimEach = 85; // interior casing and exterior aluminum capping
const secondFloorEach = 40;
const minimumJob = 1000;
// Entry doors: prehung 36 x 80 in, primed, black lever handleset, no installation
const doorPrices: Record<string, number> = { 'steel': 1450, 'fiberglass': 2400, 'wood': 3900 };
const doorStylePct: Record<string, number> = { 'flush': 0, 'vertical-lite': 12, 'grooved-lite': 12, 'panel': 15 };
const doorFinishEach = 180; // factory paint or stain; primed is included
const sidelightEach = 650;
const transomEach = 550;
const handlePrices: Record<string, number> = { 'lever-black': 0, 'lever-brass': 140, 'pull-steel': 260, 'pull-black': 260 };
const smartLockEach = 320;
const stormDoorEach = 480;
const doorInstallEach = 650;
// Promotion, estimate range and promises
const promotionPct = 0; // set above 0 to show the promotion banner
const promotionName = 'Fall window sale';
const rangePct = 12; // the estimate is shown as a range of +/- this much
const measureDays = 3;
const leadWeeks = 6;
const financingNote = 'Financing available: 0% APR for 18 months on approved credit.';
const MAX_WINDOW_KINDS = 3;
const MAX_DOORS = 2;
type WindowStyle = { name: string; width: number; height: number; tiered: boolean };
// Typical sizes stand in for a window the visitor has not measured
const windowStyles: Record<string, WindowStyle> = {
'double-hung': { name: 'Double-hung', width: 36, height: 60, tiered: true },
'single-hung': { name: 'Single-hung', width: 36, height: 60, tiered: true },
'slider': { name: 'Slider', width: 60, height: 36, tiered: true },
'casement': { name: 'Casement', width: 30, height: 48, tiered: true },
'awning': { name: 'Awning', width: 36, height: 24, tiered: true },
'picture': { name: 'Picture', width: 60, height: 48, tiered: true },
'bay-bow': { name: 'Bay or bow', width: 96, height: 60, tiered: false }
};
const defaultWindowStyle: WindowStyle = { name: 'Double-hung', width: 36, height: 60, tiered: true };
const windowStyleOf = (id: string): WindowStyle => windowStyles[id] ?? defaultWindowStyle;
// Common sizes offered in the dropdown; the id carries the size
const commonSizes: { id: string; width: number; height: number }[] = [
{ id: '24x36', width: 24, height: 36 },
{ id: '28x54', width: 28, height: 54 },
{ id: '32x54', width: 32, height: 54 },
{ id: '36x48', width: 36, height: 48 },
{ id: '36x60', width: 36, height: 60 },
{ id: '48x48', width: 48, height: 48 },
{ id: '60x48', width: 60, height: 48 },
{ id: '72x60', width: 72, height: 60 }
];
const materialNames: Record<string, string> = { 'vinyl': 'Vinyl', 'fiberglass': 'Fiberglass', 'wood-clad': 'Wood-clad', 'aluminum': 'Aluminum' };
const colorNames: Record<string, string> = { 'white': 'White', 'tan': 'Tan', 'black': 'Black' };
const doorStyleNames: Record<string, string> = { 'flush': 'Flush', 'vertical-lite': 'Vertical lite', 'grooved-lite': 'Grooved with lite', 'panel': 'Traditional panel' };
const doorMaterialNames: Record<string, string> = { 'steel': 'Steel', 'fiberglass': 'Fiberglass', 'wood': 'Wood' };
const doorColorNames: Record<string, string> = { 'primed': 'Primed', 'white': 'White', 'black': 'Black', 'bronze': 'Bronze', 'red': 'Red', 'stain': 'Woodgrain stain' };
const handleNames: Record<string, string> = { 'lever-black': 'Black handleset', 'lever-brass': 'Brass handleset', 'pull-steel': 'Stainless pull bar', 'pull-black': 'Black pull bar' };
const sidelightCounts: Record<string, number> = { 'none': 0, 'one': 1, 'two': 2 };
const nameOf = (names: Record<string, string>, id: string): string => names[id] ?? id;
const numberOf = (values: Record<string, number>, id: string): number => values[id] ?? 0;
const money = (amount: number) => {
const digits = String(Math.round(Math.abs(amount))).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return (amount < 0 ? '-$' : '$') + digits;
};
const roundTo50 = (amount: number) => Math.round(amount / 50) * 50;
form.addRow(row => {
row.addTextPanel('header', {
computedValue: () => 'Replacement Windows and Entry Doors Quote',
customStyles: { 'font-size': '1.5rem', 'font-weight': '600', 'color': '#1e293b' }
});
});
form.addRow(row => {
row.addTextPanel('intro', {
computedValue: () => 'Tell us about your windows and doors and get an instant estimate. Within ' + measureDays
+ ' business days we come out for a free in-home measure and go over the details with you.',
customStyles: { 'font-size': '0.95rem', 'color': '#475569' }
});
});
form.addSpacer({ height: 16 });
const pages = form.addPages('steps');
// Step 1: the project and the windows
const windowsPage = pages.addPage('windows');
windowsPage.addRow(row => {
row.addTextPanel('stepTitle', {
computedValue: () => 'Step 1 of 3: Your project and windows',
customStyles: { 'font-size': '1.1rem', 'font-weight': '600', 'color': '#1e293b' }
});
});
const projectSection = windowsPage.addSubform('project', { title: 'Project', isCollapsible: false });
projectSection.addRow(row => {
row.addRadioButton('scope', {
label: 'What do you need a quote for?',
options: [
{ id: 'windows', name: 'Replacement windows' },
{ id: 'doors', name: 'Entry doors' },
{ id: 'both', name: 'Windows and doors' }
],
defaultValue: 'windows',
orientation: 'horizontal',
isRequired: true
});
});
const scope = () => projectSection.radioButton('scope')?.value() || 'windows';
const withWindows = () => scope() !== 'doors';
const withDoors = () => scope() !== 'windows';
projectSection.addRow(row => {
row.addRadioButton('projectType', {
label: 'Replacement or new construction?',
options: [
{ id: 'replacement', name: 'Replacement in my home' },
{ id: 'new-build', name: 'New construction' }
],
defaultValue: 'replacement',
isRequired: true
}, '1fr');
row.addRadioButton('stories', {
label: 'How many stories?',
tooltip: 'Windows above the ground floor add ' + money(secondFloorEach) + ' each for ladder work',
options: [
{ id: 'one', name: 'One story' },
{ id: 'two', name: 'Two or more' }
],
defaultValue: 'one',
isVisible: () => withWindows()
}, '1fr');
});
const replacement = () => projectSection.radioButton('projectType')?.value() !== 'new-build';
const twoStory = () => withWindows() && projectSection.radioButton('stories')?.value() === 'two';
// A card for every kind of window
const windowsSection = windowsPage.addSubform('windows', { title: 'Windows', isCollapsible: false, isVisible: () => withWindows() });
windowsSection.addRow(row => {
row.addInteger('kinds', {
label: 'How many different windows are you replacing?',
tooltip: 'Windows of the same style, size and options are one card: set the quantity on it',
min: 1,
max: MAX_WINDOW_KINDS,
defaultValue: 1,
isRequired: true
}, '1fr');
row.addEmpty('1fr');
});
const kindCount = () => withWindows() ? (windowsSection.integer('kinds')?.value() || 1) : 0;
type WindowLine = { number: number; style: string; sized: boolean; width: number; height: number; quantity: number; floor: string; tempered: boolean; options: string[] };
const windowCards = [1, 2, 3].map(i => {
const card = windowsSection.addSubform('window' + i, {
title: 'Window ' + i,
isCollapsible: true,
isVisible: () => kindCount() >= i
});
const cardStyle = () => card.dropdown('style')?.value() || 'double-hung';
const sizeMode = () => card.dropdown('size')?.value() || 'measure';
card.addRow(row => {
row.addDropdown('style', {
label: 'Style',
options: [
{ id: 'double-hung', name: 'Double-hung, both sashes slide - from ' + money(numberOf(windowPrices, 'double-hung')) },
{ id: 'single-hung', name: 'Single-hung, bottom sash slides - from ' + money(numberOf(windowPrices, 'single-hung')) },
{ id: 'slider', name: 'Slider, sashes slide sideways - from ' + money(numberOf(windowPrices, 'slider')) },
{ id: 'casement', name: 'Casement, cranks open - from ' + money(numberOf(windowPrices, 'casement')) },
{ id: 'awning', name: 'Awning, hinged at the top - from ' + money(numberOf(windowPrices, 'awning')) },
{ id: 'picture', name: 'Picture, fixed glass - from ' + money(numberOf(windowPrices, 'picture')) },
{ id: 'bay-bow', name: 'Bay or bow, projects out with a seat - from ' + money(numberOf(windowPrices, 'bay-bow')) }
],
defaultValue: 'double-hung',
isRequired: true
}, '2fr');
row.addInteger('quantity', {
label: 'Quantity',
min: 1,
max: 40,
defaultValue: 1,
isRequired: true
}, '1fr');
});
card.addRow(row => {
row.addDropdown('size', {
label: 'Size (width x height)',
tooltip: 'Measure the old frame outside to outside, in inches. We confirm every size at the measure',
options: [
{ id: 'measure', name: 'Measure for me' },
{ id: 'custom', name: 'I know the size' },
...commonSizes.map(s => ({ id: s.id, name: s.width + ' x ' + s.height + ' in' }))
],
defaultValue: 'measure',
isRequired: true
}, '1fr');
row.addInteger('width', {
label: 'Width (inches)',
min: 12,
max: 144,
placeholder: 'e.g. 36',
isRequired: true,
isVisible: () => sizeMode() === 'custom'
}, '1fr');
row.addInteger('height', {
label: 'Height (inches)',
min: 12,
max: 120,
placeholder: 'e.g. 60',
isRequired: true,
isVisible: () => sizeMode() === 'custom'
}, '1fr');
});
card.addRow(row => {
row.addCheckboxList('options', {
label: 'Options for this window',
options: [
{ id: 'grids', name: 'Grids between the panes, colonial or prairie (+' + money(gridsEach) + ')' },
{ id: 'obscure', name: 'Obscure privacy glass for bathrooms (+' + money(obscureEach) + ')' },
{ id: 'tempered', name: 'Tempered glass, required near tubs, showers, doors and floor-level glass (+' + money(temperedEach) + ')' }
],
orientation: 'vertical'
}, '2fr');
row.addRadioButton('floor', {
label: 'Floor',
options: [
{ id: 'first', name: 'Ground' },
{ id: 'second', name: 'Upstairs' }
],
defaultValue: 'first',
isVisible: () => twoStory()
}, '1fr');
});
// Every field read through its default, so the estimate never waits for input
const line = (): WindowLine => {
const style = cardStyle();
const typical = windowStyleOf(style);
const size = sizeMode();
const common = commonSizes.find(s => s.id === size);
const width = card.integer('width')?.value();
const height = card.integer('height')?.value();
const custom = size === 'custom' && !!width && !!height;
const selected = card.checkboxList('options')?.value() || [];
return {
number: i,
style,
sized: !!common || custom,
width: common ? common.width : (custom && width ? width : typical.width),
height: common ? common.height : (custom && height ? height : typical.height),
quantity: card.integer('quantity')?.value() || 1,
floor: twoStory() ? (card.radioButton('floor')?.value() || 'first') : 'first',
tempered: selected.includes('tempered'),
options: selected.filter(id => id !== 'tempered')
};
};
return { number: i, line };
});
const windowList = (): WindowLine[] => windowCards.filter(c => c.number <= kindCount()).map(c => c.line());
const windowCount = () => windowList().reduce((sum, w) => sum + w.quantity, 0);
const upstairsCount = () => windowList().filter(w => w.floor === 'second').reduce((sum, w) => sum + w.quantity, 0);
const windowSize = (w: WindowLine) => w.sized ? w.width + ' x ' + w.height + ' in' : 'to be measured';
const windowOptionNames = (w: WindowLine): string[] => [
...w.options.map(id => id === 'grids' ? 'grids' : 'obscure glass'),
...(w.tempered ? ['tempered glass'] : [])
];
const describeWindow = (w: WindowLine): string => {
const parts = [windowStyleOf(w.style).name, windowSize(w), w.quantity + (w.quantity === 1 ? ' pc' : ' pcs')];
if (w.floor === 'second') parts.push('upstairs');
if (windowOptionNames(w).length > 0) parts.push(windowOptionNames(w).join(', '));
return parts.join(', ');
};
// Frame material, glass package and color for the whole house
const lookSection = windowsPage.addSubform('look', { title: 'Frames, glass and color for the whole house', isCollapsible: false, isVisible: () => withWindows() });
lookSection.addRow(row => {
row.addRadioButton('material', {
label: 'Frame material',
options: [
{ id: 'vinyl', name: 'Vinyl - most popular, no painting (included)' },
{ id: 'fiberglass', name: 'Fiberglass - stronger, slimmer frames (+' + numberOf(materialPct, 'fiberglass') + '%)' },
{ id: 'wood-clad', name: 'Wood-clad - wood inside, aluminum outside (+' + numberOf(materialPct, 'wood-clad') + '%)' },
{ id: 'aluminum', name: 'Aluminum - thermally broken, large glass (+' + numberOf(materialPct, 'aluminum') + '%)' }
],
defaultValue: 'vinyl',
orientation: 'vertical',
isRequired: true
}, '1fr');
row.addRadioButton('glass', {
label: 'Glass package',
options: [
{ id: 'double', name: 'Double-pane Low-E, argon filled (included)' },
{ id: 'triple', name: 'Triple-pane Low-E, quieter and warmer (+' + triplePanePct + '%)' }
],
defaultValue: 'double',
orientation: 'vertical',
isRequired: true
}, '1fr');
});
lookSection.addRow(row => {
row.addCheckbox('energyStar', {
label: 'ENERGY STAR certified glass for our climate zone, may qualify for utility rebates (+' + energyStarPct + '%)',
defaultValue: true
}, '1fr');
row.addDropdown('color', {
label: 'Exterior color',
tooltip: 'The interior stays white on vinyl and fiberglass; wood-clad frames come unfinished inside',
options: [
{ id: 'white', name: 'White (included)' },
{ id: 'tan', name: 'Tan (+' + money(colorExteriorEach) + '/window)' },
{ id: 'black', name: 'Black (+' + money(colorExteriorEach) + '/window)' }
],
defaultValue: 'white',
isRequired: true
}, '1fr');
});
const material = () => lookSection.radioButton('material')?.value() || 'vinyl';
const triplePane = () => lookSection.radioButton('glass')?.value() === 'triple';
const energyStar = () => lookSection.checkbox('energyStar')?.value() === true;
const color = () => lookSection.dropdown('color')?.value() || 'white';
const describeLook = (): string => nameOf(materialNames, material()) + ' frames, ' + nameOf(colorNames, color()).toLowerCase() + ' exterior, '
+ (triplePane() ? 'triple-pane' : 'double-pane') + ' Low-E' + (energyStar() ? ', ENERGY STAR' : '');
// Step 2: entry doors and installation
const installPage = pages.addPage('install');
installPage.addRow(row => {
row.addTextPanel('stepTitle', {
computedValue: () => 'Step 2 of 3: Entry doors and installation',
customStyles: { 'font-size': '1.1rem', 'font-weight': '600', 'color': '#1e293b' }
});
});
const doorsSection = installPage.addSubform('doors', { title: 'Entry doors', isCollapsible: false, isVisible: () => withDoors() });
doorsSection.addRow(row => {
row.addInteger('doorKinds', {
label: 'How many doors?',
tooltip: 'Front, back, side or garage entry: each door gets its own card',
min: 1,
max: MAX_DOORS,
defaultValue: 1,
isRequired: true
}, '1fr');
row.addEmpty('1fr');
});
const doorCount = () => withDoors() ? (doorsSection.integer('doorKinds')?.value() || 1) : 0;
type DoorLine = { number: number; style: string; material: string; color: string; sidelights: string; transom: boolean; handle: string; smartLock: boolean; stormDoor: boolean };
const doorCards = [1, 2].map(i => {
const card = doorsSection.addSubform('door' + i, {
title: 'Door ' + i,
isCollapsible: true,
isVisible: () => doorCount() >= i
});
card.addRow(row => {
row.addDropdown('style', {
label: 'Style',
options: [
{ id: 'flush', name: 'Flush - smooth slab, no glass (included)' },
{ id: 'vertical-lite', name: 'Vertical lite - a narrow glass strip (+' + numberOf(doorStylePct, 'vertical-lite') + '%)' },
{ id: 'grooved-lite', name: 'Grooved with lite - horizontal grooves and glass (+' + numberOf(doorStylePct, 'grooved-lite') + '%)' },
{ id: 'panel', name: 'Traditional panel - raised panels with a fanlight (+' + numberOf(doorStylePct, 'panel') + '%)' }
],
defaultValue: 'flush',
isRequired: true
}, '1fr');
row.addDropdown('material', {
label: 'Material',
options: [
{ id: 'steel', name: 'Steel - best value, primed - from ' + money(numberOf(doorPrices, 'steel')) },
{ id: 'fiberglass', name: 'Fiberglass - woodgrain look, no rust - from ' + money(numberOf(doorPrices, 'fiberglass')) },
{ id: 'wood', name: 'Wood - solid, stain grade - from ' + money(numberOf(doorPrices, 'wood')) }
],
defaultValue: 'steel',
isRequired: true
}, '1fr');
});
card.addRow(row => {
row.addDropdown('color', {
label: 'Color',
options: [
{ id: 'primed', name: 'Primed, paint it yourself (included)' },
{ id: 'white', name: 'White, factory paint (+' + money(doorFinishEach) + ')' },
{ id: 'black', name: 'Black, factory paint (+' + money(doorFinishEach) + ')' },
{ id: 'bronze', name: 'Bronze, factory paint (+' + money(doorFinishEach) + ')' },
{ id: 'red', name: 'Red, factory paint (+' + money(doorFinishEach) + ')' },
{ id: 'stain', name: 'Woodgrain stain, fiberglass and wood (+' + money(doorFinishEach) + ')' }
],
defaultValue: 'primed',
isRequired: true
}, '1fr');
row.addDropdown('handle', {
label: 'Hardware',
options: [
{ id: 'lever-black', name: 'Black lever handleset and deadbolt (included)' },
{ id: 'lever-brass', name: 'Brass lever handleset (+' + money(numberOf(handlePrices, 'lever-brass')) + ')' },
{ id: 'pull-steel', name: 'Stainless pull bar outside, lever inside (+' + money(numberOf(handlePrices, 'pull-steel')) + ')' },
{ id: 'pull-black', name: 'Matte black pull bar (+' + money(numberOf(handlePrices, 'pull-black')) + ')' }
],
defaultValue: 'lever-black',
isRequired: true
}, '1fr');
});
card.addRow(row => {
row.addRadioButton('sidelights', {
label: 'Sidelites (' + money(sidelightEach) + ' each)',
tooltip: 'Fixed glass panels beside the door in the same frame',
options: [
{ id: 'none', name: 'None' },
{ id: 'one', name: 'One side' },
{ id: 'two', name: 'Both sides' }
],
defaultValue: 'none',
orientation: 'horizontal'
}, '1fr');
row.addCheckboxList('extras', {
label: 'Extras',
options: [
{ id: 'transom', name: 'Transom above the door (+' + money(transomEach) + ')' },
{ id: 'smart-lock', name: 'Smart lock, keypad and app deadbolt (+' + money(smartLockEach) + ')' },
{ id: 'storm-door', name: 'Aluminum full-view storm door with a screen (+' + money(stormDoorEach) + ')' }
],
orientation: 'vertical'
}, '1fr');
});
const line = (): DoorLine => {
const extras = card.checkboxList('extras')?.value() || [];
return {
number: i,
style: card.dropdown('style')?.value() || 'flush',
material: card.dropdown('material')?.value() || 'steel',
color: card.dropdown('color')?.value() || 'primed',
sidelights: card.radioButton('sidelights')?.value() || 'none',
transom: extras.includes('transom'),
handle: card.dropdown('handle')?.value() || 'lever-black',
smartLock: extras.includes('smart-lock'),
stormDoor: extras.includes('storm-door')
};
};
return { number: i, line };
});
const doorList = (): DoorLine[] => doorCards.filter(c => c.number <= doorCount()).map(c => c.line());
const doorExtras = (d: DoorLine): string[] => {
const count = numberOf(sidelightCounts, d.sidelights);
const parts: string[] = [];
if (count > 0) parts.push(count + (count === 1 ? ' sidelite' : ' sidelites'));
if (d.transom) parts.push('transom');
if (d.smartLock) parts.push('smart lock');
if (d.stormDoor) parts.push('storm door');
return parts;
};
const describeDoor = (d: DoorLine): string => [
nameOf(doorStyleNames, d.style) + ', ' + nameOf(doorMaterialNames, d.material).toLowerCase(),
nameOf(doorColorNames, d.color).toLowerCase(),
nameOf(handleNames, d.handle).toLowerCase(),
...doorExtras(d)
].join(', ');
// Installation
const workSection = installPage.addSubform('installation', { title: 'Installation', isCollapsible: false });
const withInstall = () => workSection.radioButton('work')?.value() !== 'supply';
const fullFrame = () => withInstall() && withWindows() && replacement() && workSection.radioButton('installType')?.value() === 'full-frame';
workSection.addRow(row => {
row.addRadioButton('work', {
label: 'Who installs?',
options: [
{ id: 'install', name: 'Measure and install (doors ' + money(doorInstallEach) + ' each)' },
{ id: 'supply', name: 'Supply only' }
],
defaultValue: 'install',
orientation: 'horizontal',
isRequired: true
}, '1fr');
row.addRadioButton('installType', {
label: 'Window installation type',
tooltip: 'Not sure? Leave insert; your consultant checks the frames at the measure',
options: [
{ id: 'insert', name: 'Insert (pocket): the new window goes into the existing frame (' + money(installEach) + '/opening)' },
{ id: 'full-frame', name: 'Full-frame: the old frame comes out to the studs, new flashing and trim (+' + money(fullFrameEach) + '/opening)' }
],
defaultValue: 'insert',
orientation: 'vertical',
isRequired: true,
isVisible: () => withInstall() && withWindows() && replacement()
}, '1fr');
});
workSection.addRow(row => {
row.addCheckbox('disposal', {
label: 'Haul away the old windows (+' + money(disposalEach) + '/window)',
defaultValue: true,
isVisible: () => withInstall() && withWindows() && replacement()
}, '1fr');
row.addCheckbox('trim', {
label: 'Interior trim and exterior aluminum capping, no more painting (+' + money(trimEach) + '/window)',
defaultValue: false,
isVisible: () => withInstall() && withWindows()
}, '1fr');
});
const withDisposal = () => withInstall() && withWindows() && replacement() && workSection.checkbox('disposal')?.value() === true;
const withTrim = () => withInstall() && withWindows() && workSection.checkbox('trim')?.value() === true;
const describeInstall = (): string => {
if (!withInstall()) return 'supply only, customer installs';
const parts = ['measure and install'];
if (withWindows() && replacement()) parts.push(fullFrame() ? 'full-frame window install' : 'insert window install');
if (withWindows() && !replacement()) parts.push('new construction windows');
if (withDisposal()) parts.push('haul-away');
if (withTrim()) parts.push('interior trim and capping');
if (upstairsCount() > 0) parts.push(upstairsCount() + ' upstairs');
if (withDoors()) parts.push(doorCount() + (doorCount() === 1 ? ' door installed' : ' doors installed'));
return parts.join(', ');
};
// Pricing
const sizeTierPct = (w: WindowLine): number => {
if (!windowStyleOf(w.style).tiered) return 0;
const ui = w.width + w.height;
if (ui <= tier1MaxUnitedInches) return 0;
if (ui <= tier2MaxUnitedInches) return tier2Pct;
return tier3Pct;
};
// One window: the style price with the size tier, material, glass package and ENERGY STAR on top, then the per-window options
const windowEach = (w: WindowLine): number => {
const base = numberOf(windowPrices, w.style);
const glassPct = (triplePane() ? triplePanePct : 0) + (energyStar() ? energyStarPct : 0);
const frame = base * (1 + sizeTierPct(w) / 100) * (1 + numberOf(materialPct, material()) / 100) * (1 + glassPct / 100);
let options = 0;
if (w.options.includes('grids')) options += gridsEach;
if (w.options.includes('obscure')) options += obscureEach;
if (w.tempered) options += temperedEach;
if (color() !== 'white') options += colorExteriorEach;
return Math.round(frame + options);
};
const windowInstallEach = (w: WindowLine): number => {
if (!withInstall()) return 0;
let sum = installEach;
if (fullFrame()) sum += fullFrameEach;
if (withDisposal()) sum += disposalEach;
if (withTrim()) sum += trimEach;
if (w.floor === 'second') sum += secondFloorEach;
return sum;
};
// One door: the material price with the style surcharge, then finish, sidelites, transom, hardware and locks
const doorEach = (d: DoorLine): number => Math.round(
numberOf(doorPrices, d.material) * (1 + numberOf(doorStylePct, d.style) / 100)
+ (d.color !== 'primed' ? doorFinishEach : 0)
+ numberOf(sidelightCounts, d.sidelights) * sidelightEach
+ (d.transom ? transomEach : 0)
+ numberOf(handlePrices, d.handle)
+ (d.smartLock ? smartLockEach : 0)
+ (d.stormDoor ? stormDoorEach : 0)
);
const windowsCost = () => windowList().reduce((sum, w) => sum + windowEach(w) * w.quantity, 0);
const windowsInstallCost = () => windowList().reduce((sum, w) => sum + windowInstallEach(w) * w.quantity, 0);
const doorsCost = () => doorList().reduce((sum, d) => sum + doorEach(d), 0);
const doorsInstallCost = () => withInstall() ? doorCount() * doorInstallEach : 0;
const beforePromotion = () => windowsCost() + windowsInstallCost() + doorsCost() + doorsInstallCost();
const promotion = () => Math.round(beforePromotion() * promotionPct / 100);
const afterPromotion = () => beforePromotion() - promotion();
// The minimum job applies last, after every discount
const minimumApplied = () => afterPromotion() < minimumJob;
const total = () => Math.round(Math.max(afterPromotion(), minimumJob));
const totalLow = () => roundTo50(total() * (1 - rangePct / 100));
const totalHigh = () => roundTo50(total() * (1 + rangePct / 100));
const estimateText = () => money(totalLow()) + ' - ' + money(totalHigh());
// Step 3: the estimate and the contact details
const quotePage = pages.addPage('quote');
quotePage.addRow(row => {
row.addTextPanel('stepTitle', {
computedValue: () => 'Step 3 of 3: Your estimate',
customStyles: { 'font-size': '1.1rem', 'font-weight': '600', 'color': '#1e293b' }
});
});
quotePage.addRow(row => {
row.addTextPanel('promotionBanner', {
computedValue: () => promotionName + ': ' + promotionPct + '% off is already included in the estimate below (' + money(promotion()) + ' less).',
isVisible: () => promotion() > 0,
customStyles: { 'font-size': '0.95rem', 'font-weight': '600', 'color': '#166534', 'background': '#dcfce7', 'padding': '10px', 'border-radius': '6px' }
});
});
const summarySection = quotePage.addSubform('summary', { title: 'What makes up the estimate', isCollapsible: false });
windowCards.forEach(c => {
summarySection.addRow(row => {
row.addTextPanel('windowLine' + c.number, {
computedValue: () => {
if (kindCount() < c.number) return '';
const w = c.line();
return 'Window ' + c.number + ': ' + describeWindow(w) + ' - ' + money(windowEach(w)) + ' each, ' + money(windowEach(w) * w.quantity);
},
isVisible: () => kindCount() >= c.number,
customStyles: { 'font-size': '0.9rem', 'color': '#334155' }
});
});
});
summarySection.addRow(row => {
row.addTextPanel('lookLine', {
computedValue: () => 'Frames and glass: ' + describeLook(),
isVisible: () => withWindows(),
customStyles: { 'font-size': '0.9rem', 'color': '#334155' }
});
});
doorCards.forEach(c => {
summarySection.addRow(row => {
row.addTextPanel('doorLine' + c.number, {
computedValue: () => {
if (doorCount() < c.number) return '';
const d = c.line();
return 'Door ' + c.number + ': ' + describeDoor(d) + ' - ' + money(doorEach(d));
},
isVisible: () => doorCount() >= c.number,
customStyles: { 'font-size': '0.9rem', 'color': '#334155' }
});
});
});
summarySection.addRow(row => {
row.addPriceDisplay('windowsCost', {
label: 'Windows',
computedValue: () => windowsCost(),
decimals: 0,
variant: 'default',
isVisible: () => withWindows()
}, '1fr');
row.addPriceDisplay('doorsCost', {
label: 'Entry doors',
computedValue: () => doorsCost(),
decimals: 0,
variant: 'default',
isVisible: () => withDoors()
}, '1fr');
row.addPriceDisplay('installCost', {
label: () => 'Installation (' + describeInstall() + ')',
computedValue: () => windowsInstallCost() + doorsInstallCost(),
decimals: 0,
variant: 'default',
prefix: '+'
}, '1fr');
});
summarySection.addRow(row => {
row.addPriceDisplay('promotion', {
label: promotionName,
computedValue: () => -promotion(),
decimals: 0,
variant: 'success',
isVisible: () => promotion() > 0
}, '1fr');
row.addPriceDisplay('minimum', {
label: 'Minimum job (' + money(minimumJob) + ')',
computedValue: () => total() - afterPromotion(),
decimals: 0,
variant: 'default',
prefix: '+',
isVisible: () => minimumApplied()
}, '1fr');
});
summarySection.addSpacer({ showLine: true, lineStyle: 'solid', lineColor: '#e2e8f0' });
summarySection.addRow(row => {
row.addPriceDisplay('estimateLow', {
label: 'Estimated cost, from',
computedValue: () => totalLow(),
decimals: 0,
variant: 'large'
}, '1fr');
row.addPriceDisplay('estimateHigh', {
label: 'to',
computedValue: () => totalHigh(),
decimals: 0,
variant: 'large',
suffix: () => ' ' + (withInstall() ? 'installed' : 'supply only') + ', before sales tax'
}, '1fr');
});
summarySection.addRow(row => {
row.addTextPanel('financing', {
computedValue: () => financingNote + ' The range of +/-' + rangePct + '% covers what a measure usually finds.',
customStyles: { 'font-size': '0.9rem', 'color': '#334155', 'background': '#f1f5f9', 'padding': '10px', 'border-radius': '6px' }
});
});
summarySection.addRow(row => {
row.addTextPanel('nextSteps', {
computedValue: () => 'A consultant calls to go over your request and books a free in-home measure within ' + measureDays
+ ' business days. After the measure you get a firm quote, and installation takes about ' + leadWeeks + ' weeks from your order.',
customStyles: { 'font-size': '0.9rem', 'color': '#0369a1', 'background': '#e0f2fe', 'padding': '10px', 'border-radius': '6px' }
});
});
const contactSection = quotePage.addSubform('contact', { title: 'Contact details', isCollapsible: false });
contactSection.addRow(row => {
row.addTextbox('name', { label: 'Name', placeholder: 'John Smith', isRequired: true }, '1fr');
row.addTextbox('phone', { label: 'Phone', placeholder: '(817) 555-0100', isRequired: true }, '1fr');
});
contactSection.addRow(row => {
row.addTextbox('cityZip', { label: 'City and ZIP code', placeholder: 'Fort Worth, 76116', tooltip: 'Where we come out to measure', isRequired: true }, '1fr');
row.addEmail('email', { label: 'E-mail', placeholder: 'john@example.com', tooltip: 'For the PDF estimate and the written quote', isRequired: true }, '1fr');
});
contactSection.addRow(row => {
row.addTextarea('notes', {
label: 'Anything else?',
placeholder: 'E.g. an arched window, HOA color rules, gate code, days that work for the measure',
rows: 3,
maxLength: 1500
});
});
// Sticky estimate under every step
const finalSection = form.addSubform('final', { title: 'Summary', isCollapsible: false, sticky: 'bottom' });
finalSection.addRow(row => {
row.addTextPanel('stickyEstimate', {
computedValue: () => {
const parts: string[] = [];
if (withWindows()) parts.push(windowCount() + (windowCount() === 1 ? ' window' : ' windows'));
if (withDoors()) parts.push(doorCount() + (doorCount() === 1 ? ' entry door' : ' entry doors'));
return parts.join(' and ') + ': estimated ' + estimateText() + ', ' + (withInstall() ? 'installed' : 'supply only') + ', before sales tax';
},
customStyles: { 'font-size': '1.15rem', 'font-weight': '600', 'color': '#1e293b' }
});
});
finalSection.addRow(row => {
row.addTextPanel('disclaimer', {
computedValue: () => 'A firm quote follows the free in-home measure.',
customStyles: { 'font-size': '0.85rem', 'color': '#94a3b8', 'font-style': 'italic' }
});
});
// PDF estimate
form.configurePdf('estimate', pdf => {
const yesNo = (value: boolean) => value ? 'yes' : 'no';
const windows = windowList();
const doors = doorList();
const subtitle = [
withWindows() ? windowCount() + (windowCount() === 1 ? ' window' : ' windows') : '',
withDoors() ? doorCount() + (doorCount() === 1 ? ' entry door' : ' entry doors') : '',
withWindows() ? describeLook() : ''
].filter(part => part !== '').join(', ');
pdf.configure({
filename: 'window-door-estimate',
pageSize: 'Letter',
allowUserDownload: true,
downloadButtonLabel: 'Download the PDF estimate',
header: { title: 'Window and door estimate', subtitle },
footer: { text: 'Estimate only. A firm quote follows the free in-home measure.', showPageNumbers: true }
});
pdf.addSection('Prepared for', section => {
section.addRow(row => row
.addField('Name', contactSection.textbox('name')?.value() || '')
.addField('Phone', contactSection.textbox('phone')?.value() || '')
.addField('E-mail', contactSection.email('email')?.value() || '')
.addField('Location', contactSection.textbox('cityZip')?.value() || ''));
});
if (withWindows()) {
pdf.addSection('Windows', section => {
section.addTable(
['Style', 'Size', 'Qty', 'Floor', 'Options'],
windows.map(w => [windowStyleOf(w.style).name, windowSize(w), String(w.quantity), w.floor === 'second' ? 'upstairs' : 'ground', windowOptionNames(w).join(', ') || '-'])
);
section.addRow(row => row
.addField('Frame material', nameOf(materialNames, material()))
.addField('Exterior color', nameOf(colorNames, color()))
.addField('Glass', triplePane() ? 'triple-pane Low-E' : 'double-pane Low-E')
.addField('ENERGY STAR', yesNo(energyStar())));
});
}
if (withDoors()) {
pdf.addSection('Entry doors', section => {
section.addTable(
['Style', 'Material', 'Color', 'Hardware', 'Extras'],
doors.map(d => [nameOf(doorStyleNames, d.style), nameOf(doorMaterialNames, d.material), nameOf(doorColorNames, d.color), nameOf(handleNames, d.handle), doorExtras(d).join(', ') || '-'])
);
});
}
pdf.addSection('Installation', section => {
section.addRow(row => row
.addField('Project', (scope() === 'both' ? 'windows and entry doors' : scope() === 'doors' ? 'entry doors' : 'windows') + ', ' + (replacement() ? 'replacement in an existing home' : 'new construction'))
.addField('Scope', withInstall() ? 'measure and install' : 'supply only'));
if (withInstall() && withWindows()) {
section.addRow(row => row
.addField('Install type', replacement() ? (fullFrame() ? 'full-frame' : 'insert (pocket)') : 'new construction')
.addField('Haul-away', replacement() ? yesNo(withDisposal()) : '-')
.addField('Trim and capping', yesNo(withTrim()))
.addField('Upstairs windows', twoStory() ? String(upstairsCount()) : '-'));
}
});
pdf.addSection('Estimated cost', section => {
const rows: string[][] = windows.map(w => ['Window ' + w.number + ': ' + describeWindow(w), String(w.quantity), money(windowEach(w) * w.quantity)]);
doors.forEach(d => rows.push(['Door ' + d.number + ': ' + describeDoor(d), '1', money(doorEach(d))]));
rows.push(['Installation, ' + describeInstall(), withInstall() ? String(windowCount() + doorCount()) : '', withInstall() ? money(windowsInstallCost() + doorsInstallCost()) : 'supply only']);
if (promotion() > 0) rows.push([promotionName + ' (-' + promotionPct + '%)', '', '-' + money(promotion())]);
if (minimumApplied()) rows.push(['Minimum job', '', money(total() - afterPromotion())]);
rows.push(['Estimated, ' + (withInstall() ? 'installed' : 'supply only') + ', before sales tax', '', estimateText()]);
section.addTable(['Item', 'Qty', 'Amount'], rows);
section.addText('The range of +/-' + rangePct + '% covers what a measure usually finds; a firm quote follows the measure. ' + financingNote);
});
pdf.addSection('What happens next', section => {
section.addText('1. We call you shortly to go over your request.');
section.addText('2. We book your free in-home measure within ' + measureDays + ' business days.');
section.addText('3. After the measure you get a firm quote, and installation takes about ' + leadWeeks + ' weeks from your order.');
});
const notes = contactSection.textarea('notes')?.value();
if (notes) {
pdf.addSection('Notes', section => section.addText(notes));
}
pdf.addSection('Terms', section => {
section.addText('This is an estimate, not a binding offer. Prices are before sales tax and may change after the in-home measure, which confirms sizes, frame condition and installation type. A firm quote in writing follows the measure.');
});
});
form.configureSubmitButton({ label: 'Request my quote' });
form.configureCompletionScreen({
type: 'text',
title: 'Your request is with a consultant',
message: () => 'We will call you shortly to go over the details and book your free in-home measure within ' + measureDays + ' business days.'
});
}