export function windowQuoteCalculator(form: FormTs) {
// Base price per white vinyl window, double-pane Low-E argon glass, half screen, no installation
const stylePrices: Record<string, number> = {
'double-hung': 385,
'single-hung': 335,
'slider': 365,
'casement': 495,
'awning': 455,
'picture': 295
};
// Size: united inches (width + height) included in the base price, then a rate per extra united inch
const unitedInchesIncluded = 72;
const unitedInchRate = 4.5;
const mullJoint = 60; // factory mulling of two lites into one frame
// Percentage surcharges on the window price
const triplePanePct = 20;
const energyStarPct = 8;
const tanFramesPct = 10;
const capstockPct = 22; // black or bronze exterior over a white interior
// Per-window options
const obscureGlass = 55;
const tintedGlass = 65;
const temperedGlass = 95;
const gridsBetweenGlass = 70;
const coloredHardware = 30; // per operating window
const fullScreen = 35; // instead of the half screen on hung windows and sliders
const brickmouldPerFoot = 6.5;
// Installation and delivery
const installPocketEach = 165; // replacement: pocket install into the existing frame
const installFinEach = 285; // new construction: nailing fin, flashing, J-channel
const haulAwayEach = 25;
const deliveryFlat = 149;
// Discounts and limits
const volumeDiscountFrom = 6; // windows
const volumeDiscountPct = 7;
const promotionPct = 0; // set above 0 to show the promotion banner
const promotionName = 'Spring window sale';
const minimumOrder = 750;
// Promises printed on the estimate
const measureDays = 3;
const leadWeeks = 4;
const estimateValidDays = 30;
const financingNote = 'Financing available: 0% APR for 12 months on approved credit.';
const climateZone = 'North-Central (U-factor 0.25 or lower)';
const MAX_KINDS = 3;
type Style = { name: string; lites: { style: string; share: number }[]; opens: boolean; cranks: boolean };
// Every style the plant makes and how it is priced: lite by lite
const styles: Record<string, Style> = {
'double-hung': { name: 'Double-hung', lites: [{ style: 'double-hung', share: 1 }], opens: true, cranks: false },
'single-hung': { name: 'Single-hung', lites: [{ style: 'single-hung', share: 1 }], opens: true, cranks: false },
'slider': { name: 'Horizontal slider', lites: [{ style: 'slider', share: 1 }], opens: true, cranks: false },
'casement': { name: 'Casement', lites: [{ style: 'casement', share: 1 }], opens: true, cranks: true },
'awning': { name: 'Awning', lites: [{ style: 'awning', share: 1 }], opens: true, cranks: true },
'picture': { name: 'Picture (fixed)', lites: [{ style: 'picture', share: 1 }], opens: false, cranks: false },
'twin-double-hung': { name: 'Twin double-hung', lites: [{ style: 'double-hung', share: 0.5 }, { style: 'double-hung', share: 0.5 }], opens: true, cranks: false },
'picture-flankers': { name: 'Picture with flankers', lites: [{ style: 'double-hung', share: 0.25 }, { style: 'picture', share: 0.5 }, { style: 'double-hung', share: 0.25 }], opens: true, cranks: false }
};
const colorNames: Record<string, string> = { 'white': 'White', 'tan': 'Tan', 'black': 'Black exterior', 'bronze': 'Bronze exterior' };
const glassNames: Record<string, string> = { 'clear': 'Clear Low-E', 'obscure': 'Obscure', 'tinted': 'Tinted' };
const gridNames: Record<string, string> = { 'none': 'none', 'colonial': 'colonial grids', 'prairie': 'prairie grids' };
const screenNames: Record<string, string> = { 'half': 'half screen', 'full': 'full screen', 'none': 'no screen' };
const hardwareNames: Record<string, string> = { 'white': 'White', 'nickel': 'Brushed nickel', 'bronze': 'Oil-rubbed bronze', 'black': 'Matte black' };
const money = (amount: number) => {
const rounded = Math.round(Math.abs(amount));
const digits = String(rounded).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return (amount < 0 ? '-$' : '$') + digits;
};
const defaultStyle: Style = { name: 'Double-hung', lites: [{ style: 'double-hung', share: 1 }], opens: true, cranks: false };
const styleOf = (id: string): Style => styles[id] ?? defaultStyle;
const priceOf = (style: string): number => stylePrices[style] ?? 0;
const nameOf = (names: Record<string, string>, id: string): string => names[id] ?? id;
form.addRow(row => {
row.addTextPanel('header', {
computedValue: () => 'Vinyl Window Estimate',
customStyles: { 'font-size': '1.5rem', 'font-weight': '600', 'color': '#1e293b' }
});
});
form.addRow(row => {
row.addTextPanel('intro', {
computedValue: () => 'Build your windows one kind at a time: size in inches, style, color and glass, with a per-window price. Within '
+ measureDays + ' business days we come out for a free in-home measure and confirm every size.',
customStyles: { 'font-size': '0.95rem', 'color': '#475569' }
});
});
form.addSpacer({ height: 16 });
const pages = form.addPages('steps');
// Step 1: the windows, every kind on its own card
const windowsPage = pages.addPage('windows');
windowsPage.addRow(row => {
row.addTextPanel('stepTitle', {
computedValue: () => 'Step 1 of 3: Your windows',
customStyles: { 'font-size': '1.1rem', 'font-weight': '600', 'color': '#1e293b' }
});
});
windowsPage.addRow(row => {
row.addInteger('kinds', {
label: 'How many different windows do you need?',
tooltip: 'Windows of the same size, style and options are one kind: set the quantity on the card',
min: 1,
max: MAX_KINDS,
defaultValue: 1,
isRequired: true
}, '1fr');
row.addEmpty('1fr');
});
const kindCount = () => windowsPage.integer('kinds')?.value() || 1;
type WindowLine = {
number: number; style: string; width: number; height: number; quantity: number; color: string;
triplePane: boolean; glass: string; grids: string; screen: string; tempered: boolean; brickmould: boolean;
};
// Starting width x height of each card, inches
const defaultSizes: [number, number][] = [[36, 60], [32, 54], [48, 48]];
const cards = defaultSizes.map(([defaultWidth, defaultHeight], index) => {
const i = index + 1;
const card = windowsPage.addSubform('window' + i, {
title: 'Window ' + i,
isCollapsible: true,
isVisible: () => kindCount() >= i
});
const defaults: [number, number] = [defaultWidth, defaultHeight];
const cardStyle = () => card.dropdown('style')?.value() || 'double-hung';
card.addRow(row => {
row.addDropdown('style', {
label: 'Style',
options: [
{ id: 'double-hung', name: 'Double-hung - from ' + money(priceOf('double-hung')) },
{ id: 'single-hung', name: 'Single-hung - from ' + money(priceOf('single-hung')) },
{ id: 'slider', name: 'Horizontal slider - from ' + money(priceOf('slider')) },
{ id: 'casement', name: 'Casement (crank-out) - from ' + money(priceOf('casement')) },
{ id: 'awning', name: 'Awning (hinged at top) - from ' + money(priceOf('awning')) },
{ id: 'picture', name: 'Picture (fixed) - from ' + money(priceOf('picture')) },
{ id: 'twin-double-hung', name: 'Twin double-hung - from ' + money(2 * priceOf('double-hung') + mullJoint) },
{ id: 'picture-flankers', name: 'Picture with flankers - from ' + money(priceOf('picture') + 2 * priceOf('double-hung') + 2 * mullJoint) }
],
defaultValue: 'double-hung',
isRequired: true
}, '2fr');
row.addInteger('quantity', {
label: 'Quantity',
min: 1,
max: 20,
defaultValue: 1,
isRequired: true
}, '1fr');
});
card.addRow(row => {
row.addInteger('width', {
label: 'Width (inches)',
min: 18,
max: 144,
defaultValue: defaults[0],
isRequired: true
}, '1fr');
row.addInteger('height', {
label: 'Height (inches)',
min: 18,
max: 78,
defaultValue: defaults[1],
isRequired: true
}, '1fr');
row.addTextPanel('unitedInches', {
computedValue: () => {
const ui = (card.integer('width')?.value() || defaults[0]) + (card.integer('height')?.value() || defaults[1]);
return 'United inches: ' + ui + (ui > unitedInchesIncluded ? ' (+' + money(unitedInchRate) + ' per inch above ' + unitedInchesIncluded + ')' : ' (within base size)');
},
customStyles: { 'font-size': '0.85rem', 'color': '#64748b', 'padding-top': '28px' }
}, '1fr');
});
card.addRow(row => {
row.addDropdown('color', {
label: 'Frame color',
options: [
{ id: 'white', name: 'White, inside and out (included)' },
{ id: 'tan', name: 'Tan, inside and out (+' + tanFramesPct + '%)' },
{ id: 'black', name: 'Black exterior, white inside (+' + capstockPct + '%)' },
{ id: 'bronze', name: 'Bronze exterior, white inside (+' + capstockPct + '%)' }
],
defaultValue: 'white',
isRequired: true
}, '1fr');
row.addDropdown('glazing', {
label: 'Glass package',
options: [
{ id: 'double', name: 'Double-pane Low-E, argon (included)' },
{ id: 'triple', name: 'Triple-pane Low-E, two argon spaces (+' + triplePanePct + '%)' }
],
defaultValue: 'double',
isRequired: true
}, '1fr');
});
card.addRow(row => {
row.addDropdown('glass', {
label: 'Glass',
options: [
{ id: 'clear', name: 'Clear Low-E (included)' },
{ id: 'obscure', name: 'Obscure privacy glass (+' + money(obscureGlass) + ')' },
{ id: 'tinted', name: 'Gray solar tint (+' + money(tintedGlass) + ')' }
],
defaultValue: 'clear',
isRequired: true
}, '1fr');
row.addDropdown('grids', {
label: 'Grids between the glass',
options: [
{ id: 'none', name: 'No grids (included)' },
{ id: 'colonial', name: 'Colonial (+' + money(gridsBetweenGlass) + ')' },
{ id: 'prairie', name: 'Prairie (+' + money(gridsBetweenGlass) + ')' }
],
defaultValue: 'none',
isRequired: true
}, '1fr');
});
card.addRow(row => {
row.addRadioButton('screen', {
label: 'Insect screen',
tooltip: 'Casements and awnings always come with a full screen at no charge',
options: [
{ id: 'half', name: 'Half screen (included)' },
{ id: 'full', name: 'Full screen (+' + money(fullScreen) + ' on hung windows and sliders)' },
{ id: 'none', name: 'No screen' }
],
defaultValue: 'half',
orientation: 'horizontal',
isVisible: () => cardStyle() !== 'picture'
});
});
card.addRow(row => {
row.addCheckbox('tempered', {
label: 'Tempered safety glass (+' + money(temperedGlass) + ') - required near tubs, showers, doors and glass close to the floor',
defaultValue: false
}, '1fr');
row.addCheckbox('brickmould', {
label: 'Exterior brickmould casing (+' + money(brickmouldPerFoot) + ' per ft of perimeter)',
defaultValue: false
}, '1fr');
});
// Every field read through its default, so the estimate never waits for input
const line = (): WindowLine => {
const style = cardStyle();
let screen = style === 'picture' ? 'none' : (card.radioButton('screen')?.value() || 'half');
if (styleOf(style).cranks && screen === 'half') screen = 'full';
return {
number: i,
style,
width: card.integer('width')?.value() || defaultWidth,
height: card.integer('height')?.value() || defaultHeight,
quantity: card.integer('quantity')?.value() || 1,
color: card.dropdown('color')?.value() || 'white',
triplePane: card.dropdown('glazing')?.value() === 'triple',
glass: card.dropdown('glass')?.value() || 'clear',
grids: card.dropdown('grids')?.value() || 'none',
screen,
tempered: card.checkbox('tempered')?.value() === true,
brickmould: card.checkbox('brickmould')?.value() === true
};
};
return { number: i, line };
});
const windowList = (): WindowLine[] => cards.filter(c => c.number <= kindCount()).map(c => c.line());
const windowCount = () => windowList().reduce((sum, w) => sum + w.quantity, 0);
// Step 2: hardware, frame type and installation for the whole order
const installPage = pages.addPage('install');
installPage.addRow(row => {
row.addTextPanel('stepTitle', {
computedValue: () => 'Step 2 of 3: Hardware and installation',
customStyles: { 'font-size': '1.1rem', 'font-weight': '600', 'color': '#1e293b' }
});
});
installPage.addRow(row => {
row.addRadioButton('hardware', {
label: 'Hardware finish (cam locks, tilt latches and cranks on every operating window)',
options: [
{ id: 'white', name: 'White - matches the frame (included)' },
{ id: 'nickel', name: 'Brushed nickel (+' + money(coloredHardware) + '/window)' },
{ id: 'bronze', name: 'Oil-rubbed bronze (+' + money(coloredHardware) + '/window)' },
{ id: 'black', name: 'Matte black (+' + money(coloredHardware) + '/window)' }
],
defaultValue: 'white',
orientation: 'vertical',
isRequired: true
}, '1fr');
row.addRadioButton('frameType', {
label: 'Frame type',
tooltip: 'Not sure? Leave replacement; the measure confirms the frames',
options: [
{ id: 'replacement', name: 'Replacement - pocket frame into the existing opening (install ' + money(installPocketEach) + '/window)' },
{ id: 'new-construction', name: 'New construction - nailing fin, flashed to the sheathing (install ' + money(installFinEach) + '/window)' }
],
defaultValue: 'replacement',
orientation: 'vertical',
isRequired: true
}, '1fr');
});
installPage.addRow(row => {
row.addCheckbox('energyStar', {
label: 'ENERGY STAR certified glass for the ' + climateZone + ' climate zone (+' + energyStarPct + '%) - may qualify for utility rebates and the federal tax credit',
defaultValue: true
});
});
const frameType = () => installPage.radioButton('frameType')?.value() || 'replacement';
const withInstall = () => installPage.checkbox('install')?.value() === true;
installPage.addRow(row => {
row.addCheckbox('install', {
label: 'Installation by our own crews',
defaultValue: true
}, '1fr');
row.addCheckbox('haulAway', {
label: 'Haul away the old windows (+' + money(haulAwayEach) + '/window)',
defaultValue: true,
isVisible: () => withInstall() && frameType() === 'replacement'
}, '1fr');
row.addCheckbox('delivery', {
label: 'Delivery (' + money(deliveryFlat) + ' for the whole order)',
defaultValue: true
}, '1fr');
});
installPage.addRow(row => {
row.addTextPanel('volumeInfo', {
computedValue: () => {
const count = windowCount();
if (count >= volumeDiscountFrom) return 'Volume discount: ' + volumeDiscountPct + '% off the windows and the installation is already in your price (' + count + ' windows).';
return 'Volume discount: from ' + volumeDiscountFrom + ' windows, ' + volumeDiscountPct + '% off the windows and the installation. You have ' + count + ' so far.';
},
customStyles: { 'font-size': '0.9rem', 'color': '#0369a1', 'background': '#e0f2fe', 'padding': '10px', 'border-radius': '6px' }
});
});
const hardware = () => installPage.radioButton('hardware')?.value() || 'white';
const energyStar = () => installPage.checkbox('energyStar')?.value() === true;
const withHaul = () => withInstall() && frameType() === 'replacement' && installPage.checkbox('haulAway')?.value() === true;
const withDelivery = () => installPage.checkbox('delivery')?.value() === true;
// Pricing
// One lite: the base price of its style, plus every united inch above the included size
const litePrice = (style: string, width: number, height: number): number => {
const united = Math.round(width) + Math.round(height);
return (stylePrices[style] || 0) + Math.max(0, united - unitedInchesIncluded) * unitedInchRate;
};
// Price of one window of the kind: the lites and the mulls, then the percentage surcharges
const windowEach = (w: WindowLine): number => {
const s = styleOf(w.style);
let price = s.lites.reduce((sum, lite) => sum + litePrice(lite.style, w.width * lite.share, w.height), 0) + (s.lites.length - 1) * mullJoint;
if (w.triplePane) price *= 1 + triplePanePct / 100;
if (energyStar()) price *= 1 + energyStarPct / 100;
if (w.color === 'tan') price *= 1 + tanFramesPct / 100;
if (w.color === 'black' || w.color === 'bronze') price *= 1 + capstockPct / 100;
return Math.round(price);
};
// Options of one window of the kind: glass, grids, hardware, screen, tempered glass and brickmould
const optionsEach = (w: WindowLine): number => {
const s = styleOf(w.style);
let sum = 0;
if (w.glass === 'obscure') sum += obscureGlass;
if (w.glass === 'tinted') sum += tintedGlass;
if (w.grids !== 'none') sum += gridsBetweenGlass;
if (s.opens && hardware() !== 'white') sum += coloredHardware;
if (w.screen === 'full' && !s.cranks) sum += fullScreen;
if (w.tempered) sum += temperedGlass;
if (w.brickmould) sum += brickmouldPerFoot * (2 * (w.width + w.height)) / 12;
return Math.round(sum);
};
const windowsCost = () => windowList().reduce((sum, w) => sum + windowEach(w) * w.quantity, 0);
const optionsCost = () => windowList().reduce((sum, w) => sum + optionsEach(w) * w.quantity, 0);
const installEach = () => (frameType() === 'replacement' ? installPocketEach : installFinEach) + (withHaul() ? haulAwayEach : 0);
const installCost = () => withInstall() ? installEach() * windowCount() : 0;
const deliveryCost = () => withDelivery() ? deliveryFlat : 0;
const volumeDiscount = () => windowCount() >= volumeDiscountFrom ? Math.round((windowsCost() + installCost()) * volumeDiscountPct / 100) : 0;
const beforePromotion = () => windowsCost() + optionsCost() + installCost() + deliveryCost() - volumeDiscount();
const promotion = () => Math.round(beforePromotion() * promotionPct / 100);
// The minimum holds last: a promotion may never take an order below the minimum
const minimumApplied = () => beforePromotion() - promotion() < minimumOrder;
const total = () => Math.max(beforePromotion() - promotion(), minimumOrder);
const describeOptions = (w: WindowLine): string => {
const parts = [nameOf(colorNames, w.color).toLowerCase(), w.triplePane ? 'triple-pane' : 'double-pane', nameOf(glassNames, w.glass).toLowerCase()];
if (w.grids !== 'none') parts.push(nameOf(gridNames, w.grids));
if (w.style !== 'picture') parts.push(nameOf(screenNames, w.screen));
if (w.tempered) parts.push('tempered');
if (w.brickmould) parts.push('brickmould');
return parts.join(', ');
};
const describeInstall = (): string => {
if (!withInstall()) return 'windows only, customer installs';
return (frameType() === 'replacement' ? 'replacement (pocket) install' : 'new-construction (nailing fin) install') + (withHaul() ? ', haul-away' : '');
};
// 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 in the price below, ' + money(promotion()) + ' less on this order.',
isVisible: () => promotion() > 0,
customStyles: { 'font-size': '0.95rem', 'font-weight': '600', 'color': '#166534', 'background': '#dcfce7', 'padding': '10px', 'border-radius': '6px' }
});
});
quotePage.addRow(row => {
row.addTextPanel('timing', {
computedValue: () => 'We come out for a free in-home measure within ' + measureDays + ' business days, and your windows are made to order in ' + leadWeeks + ' weeks from the signed order.',
customStyles: { 'font-size': '0.9rem', 'color': '#0369a1', 'background': '#e0f2fe', 'padding': '10px', 'border-radius': '6px' }
});
});
const summarySection = quotePage.addSubform('summary', { title: 'What makes up the price', isCollapsible: false });
cards.forEach(c => {
summarySection.addRow(row => {
row.addTextPanel('line' + c.number, {
computedValue: () => {
if (kindCount() < c.number) return '';
const w = c.line();
return 'Window ' + c.number + ': ' + w.quantity + ' x ' + styleOf(w.style).name.toLowerCase() + ' ' + w.width + ' x ' + w.height + ' in, ' + describeOptions(w)
+ ' - ' + money(windowEach(w)) + ' each' + (optionsEach(w) > 0 ? ' + ' + money(optionsEach(w)) + ' options' : '');
},
isVisible: () => kindCount() >= c.number,
customStyles: { 'font-size': '0.9rem', 'color': '#334155' }
});
});
});
summarySection.addRow(row => {
row.addPriceDisplay('windowsCost', {
label: 'Windows',
computedValue: () => windowsCost(),
decimals: 0,
variant: 'default'
}, '1fr');
row.addPriceDisplay('optionsCost', {
label: 'Options',
computedValue: () => optionsCost(),
decimals: 0,
variant: 'default',
prefix: '+'
}, '1fr');
});
summarySection.addRow(row => {
row.addPriceDisplay('installCost', {
label: () => 'Installation (' + describeInstall() + ')',
computedValue: () => installCost(),
decimals: 0,
variant: 'default',
prefix: '+'
}, '1fr');
row.addPriceDisplay('deliveryCost', {
label: () => withDelivery() ? 'Delivery' : 'Delivery (customer pickup)',
computedValue: () => deliveryCost(),
decimals: 0,
variant: 'default',
prefix: '+'
}, '1fr');
});
summarySection.addRow(row => {
row.addPriceDisplay('volumeDiscount', {
label: 'Volume discount (' + volumeDiscountPct + '% from ' + volumeDiscountFrom + ' windows)',
computedValue: () => -volumeDiscount(),
decimals: 0,
variant: 'success',
isVisible: () => volumeDiscount() > 0
}, '1fr');
row.addPriceDisplay('promotion', {
label: promotionName,
computedValue: () => -promotion(),
decimals: 0,
variant: 'success',
isVisible: () => promotion() > 0
}, '1fr');
row.addPriceDisplay('minimum', {
label: 'Minimum order (' + money(minimumOrder) + ')',
computedValue: () => total() - (beforePromotion() - promotion()),
decimals: 0,
variant: 'default',
prefix: '+',
isVisible: () => minimumApplied()
}, '1fr');
});
summarySection.addSpacer({ showLine: true, lineStyle: 'solid', lineColor: '#e2e8f0' });
summarySection.addRow(row => {
row.addPriceDisplay('total', {
label: 'Estimated price',
computedValue: () => total(),
decimals: 0,
variant: 'large',
suffix: () => ' ' + (withInstall() ? 'installed' : 'windows only') + ', before sales tax'
});
});
summarySection.addRow(row => {
row.addTextPanel('financing', {
computedValue: () => financingNote,
customStyles: { 'font-size': '0.9rem', 'color': '#334155', 'background': '#f1f5f9', '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: '(555) 123-4567',
isRequired: true
}, '1fr');
});
contactSection.addRow(row => {
row.addEmail('email', {
label: 'E-mail',
tooltip: 'We send the PDF estimate to this address',
placeholder: 'john@example.com',
isRequired: true
}, '1fr');
row.addTextbox('cityZip', {
label: 'City, state and ZIP code',
placeholder: 'Columbus, OH 43228',
tooltip: 'Where we come out to measure',
isRequired: true
}, '1fr');
});
contactSection.addRow(row => {
row.addTextarea('notes', {
label: 'Anything else?',
placeholder: 'E.g. an arched window, HOA color rules, a second-floor window, days that work for the measure',
rows: 3,
maxLength: 1500
});
});
// Sticky total under every step
const finalSection = form.addSubform('final', {
title: 'Summary',
isCollapsible: false,
sticky: 'bottom'
});
finalSection.addRow(row => {
row.addPriceDisplay('totalEstimate', {
label: () => windowCount() + (windowCount() === 1 ? ' window' : ' windows') + ', estimated price',
computedValue: () => total(),
decimals: 0,
variant: 'large',
suffix: () => ' ' + (withInstall() ? 'installed' : 'windows only')
});
});
finalSection.addRow(row => {
row.addTextPanel('disclaimer', {
computedValue: () => 'Estimate before sales tax. 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 list = windowList();
const count = windowCount();
pdf.configure({
filename: 'window-estimate',
pageSize: 'Letter',
allowUserDownload: true,
downloadButtonLabel: 'Download the PDF estimate',
header: {
title: 'Vinyl window estimate',
subtitle: count + (count === 1 ? ' window' : ' windows') + ' in ' + list.length + (list.length === 1 ? ' kind' : ' kinds')
},
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() || ''));
});
pdf.addSection('Summary', section => {
const rows: string[][] = list.map(w => [
'Window ' + w.number + ': ' + w.width + ' x ' + w.height + ' in, ' + styleOf(w.style).name.toLowerCase() + ', ' + nameOf(colorNames, w.color).toLowerCase(),
String(w.quantity),
money(windowEach(w)),
money(windowEach(w) * w.quantity)
]);
rows.push(['Options: glass, grids, hardware, screens, tempered glass, brickmould', '', '', optionsCost() > 0 ? money(optionsCost()) : 'none']);
rows.push(['Installation, ' + describeInstall(), withInstall() ? String(count) : '', withInstall() ? money(installEach()) : '', withInstall() ? money(installCost()) : 'windows only']);
rows.push(['Delivery', '', '', withDelivery() ? money(deliveryCost()) : 'customer pickup']);
if (volumeDiscount() > 0) rows.push(['Volume discount ' + volumeDiscountPct + '% from ' + volumeDiscountFrom + ' windows', '', '', '-' + money(volumeDiscount())]);
if (promotion() > 0) rows.push([promotionName + ' ' + promotionPct + '%', '', '', '-' + money(promotion())]);
if (minimumApplied()) rows.push(['Minimum order', '', '', money(total() - (beforePromotion() - promotion()))]);
rows.push(['Total before sales tax', '', '', money(total())]);
section.addTable(['Item', 'Qty', 'Each', 'Amount'], rows);
section.addText('Estimated price: ' + money(total()) + ', ' + (withInstall() ? 'installed' : 'windows only') + ', before sales tax. ' + financingNote);
});
pdf.addSection('Window specifications', section => {
list.forEach((w, index) => {
const s = styleOf(w.style);
section.addText('Window ' + w.number + ' - ' + w.quantity + (w.quantity === 1 ? ' pc' : ' pcs'));
section.addRow(row => row
.addField('Size', w.width + ' x ' + w.height + ' in')
.addField('United inches', String(Math.round(w.width) + Math.round(w.height)))
.addField('Style', s.name)
.addField('Frame color', nameOf(colorNames, w.color)));
section.addRow(row => row
.addField('Glass package', w.triplePane ? 'triple-pane Low-E, two argon spaces' : 'double-pane Low-E, argon')
.addField('Glass', nameOf(glassNames, w.glass) + (w.tempered ? ', tempered' : ''))
.addField('Grids', w.grids === 'none' ? 'none' : nameOf(gridNames, w.grids) + ', between the glass')
.addField('Screen', w.style === 'picture' ? '-' : nameOf(screenNames, w.screen)));
section.addRow(row => row
.addField('Brickmould', w.brickmould ? 'yes, ' + (2 * (w.width + w.height) / 12).toFixed(1) + ' ft' : 'no')
.addField('Hardware', s.opens ? nameOf(hardwareNames, hardware()) : 'none, fixed window')
.addField('Price per window', money(windowEach(w)) + (optionsEach(w) > 0 ? ' + ' + money(optionsEach(w)) + ' options' : '')));
if (index < list.length - 1) section.addDivider();
});
});
pdf.addSection('Shared by the whole order', section => {
section.addRow(row => row
.addField('Frame type', frameType() === 'replacement' ? 'Replacement (pocket frame)' : 'New construction (nailing fin)')
.addField('ENERGY STAR', energyStar() ? 'yes, ' + climateZone : 'no')
.addField('Installation', withInstall() ? describeInstall() + ', ' + money(installEach()) + ' per window' : 'windows only')
.addField('Delivery', withDelivery() ? 'yes, ' + money(deliveryFlat) : 'customer pickup'));
});
pdf.addSection('What happens next', section => {
section.addText('1. A consultant calls within one business day to go over the configuration.');
section.addText('2. We book your free in-home measure within ' + measureDays + ' business days; after it you get a firm quote.');
section.addText('3. Your windows are made to order in ' + leadWeeks + ' weeks from the signed order; installation takes one day for most homes.');
});
const notes = contactSection.textarea('notes')?.value();
if (notes) {
pdf.addSection('Notes', section => section.addText(notes));
}
pdf.addSection('Terms', section => {
section.addText('Prices are before sales tax. This estimate is valid for ' + estimateValidDays + ' days from its date. It is not a binding offer: the final price depends on the in-home measure, which confirms sizes, frame condition and installation type. A firm quote in writing follows the measure.');
});
});
form.configureSubmitButton({ label: 'Send and get my estimate' });
form.configureCompletionScreen({
type: 'text',
title: 'Thank you!',
message: 'Your estimate is ready to download. A consultant calls within one business day, and we come out for your free in-home measure within ' + measureDays + ' business days.'
});
}