Rolling Shutter Quote Calculator

Give your rolling shutter business a quote form that recommends like a consultant. The customer does not need to know a recessed housing from a surface-mounted one: they say whether it is an existing home, new construction, a condo or a storefront, whether the walls get new stucco, and what the shutters are for - storm protection, security, sun or privacy - and the form recommends the housing, the slat profile and the storm rating. Every opening is a line with a typical size or exact inches, a quantity and an upstairs flag. Then the operation: manual crank, motor with switch or remote, or a solar motor when there is no power at the opening, with battery backup, a smart hub and locking pins. Walls of stucco, brick or siding, removal of old shutters, the permit for a storm-rated shutter and the HOA application round out the installed price, shown with a range, a minimum job and a financing note. Storefronts get solid, perforated or open-grille security shutters. The request collects the contact details and produces a downloadable PDF estimate.

Home Services

Try the Calculator

Rolling Shutter Quote
🏠 Project
 
 
🪟 Openings
 
Opening 1
 
 
🛡️ Shutter
 
 
 
 
⚙️ Operation
 
 
🔧 Installation
 
💰 Your Estimate
Opening 1: Window, to be measured, 1 pc - $1,090 Shutter: surface-mounted housing, 55 mm foam-filled slat, white, not storm-rated Operation: motor with remote
$ 1,090
+
$ 165

$ 1,255
installed, before sales tax
Expect $1,100 – $1,400 (±12%): the range covers what a measure usually finds. Financing available: 0% APR for 12 months on approved credit.
A consultant calls to go over your request and books a free on-site measure within 3 business days. After the measure you get a firm quote, and the shutters are installed about 5 weeks from your order.
📋 Contact Details
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
export function rollingShutterCalculator(form: FormTs) {
// Shutter price per square foot by slat profile (housing, rails and bottom bar included; no motor or installation)
const slatRates: Record<string, number> = {
'foam40': 32, // 40 mm foam-filled aluminum slat
'foam55': 38, // 55 mm foam-filled slat, wind-rated profile
'extruded': 58, // solid extruded security slat, impact-rated
'perforated': 64, // perforated storefront slat
'grille': 46 // open rod-and-link grille
};
const slatNames: Record<string, string> = {
'foam40': '40 mm foam-filled',
'foam55': '55 mm foam-filled',
'extruded': 'extruded security',
'perforated': 'perforated',
'grille': 'open grille'
};
// A small shutter takes almost as long to build as a medium one, so it is billed from this area
const minSqft = 12;
 
// Percent surcharges on the shutter
const housingPcts: Record<string, number> = { 'recessed': 18, 'surface': 0, 'inset': 6 };
const ratingPcts: Record<string, number> = { 'none': 0, 'wind': 25, 'hvhz': 45 };
const colorPcts: Record<string, number> = { 'white': 0, 'beige': 0, 'bronze': 0, 'woodgrain': 15 };
const permitFee = 350; // once per job for a storm-rated shutter
 
// Operation, per shutter
const drivePrices: Record<string, number> = { 'crank': 0, 'switch': 420, 'remote': 520, 'solar': 780 };
const hookupPerShutter = 185; // electrician runs the line when there is no power at the opening
const smartHubPrice = 350; // once per order
const batteryPerShutter = 240;
const locksPerShutter = 45;
 
// Installation, per shutter
const installPerShutter = 165; // on stucco over block
const wallSurcharges: Record<string, number> = { 'stucco': 0, 'brick': 40, 'siding': 75, 'not_sure': 0 };
const secondFloorPerShutter = 55;
const removalPerShutter = 60;
const minJob = 900;
 
// Promotion, range and lead times
const promoPct = 0;
const promoText = 'Pre-season storm sale';
const financingNote = 'Financing available: 0% APR for 12 months on approved credit.';
const rangePct = 12;
const measureDays = 3;
const leadWeeks = 5;
 
// Typical openings stand in for one the visitor has not measured (width × height in inches)
const openingKinds: Record<string, { name: string; width: number; height: number; storefront: boolean }> = {
'window': { name: 'Window', width: 36, height: 60, storefront: false },
'slider': { name: 'Sliding glass door', width: 96, height: 80, storefront: false },
'lanai': { name: 'Lanai or patio opening', width: 144, height: 96, storefront: false },
'entry': { name: 'Entry door', width: 36, height: 80, storefront: false },
'store_window': { name: 'Storefront window', width: 96, height: 96, storefront: true },
'store_door': { name: 'Entrance door', width: 72, height: 84, storefront: true },
'bay': { name: 'Service or bay opening', width: 120, height: 120, storefront: true }
};
const maxOpenings = 4;
 
const housingNames: Record<string, string> = { 'recessed': 'built-in (concealed)', 'surface': 'surface-mounted', 'inset': 'in the opening' };
const ratingNames: Record<string, string> = { 'none': 'not storm-rated', 'wind': 'wind-rated', 'hvhz': 'HVHZ impact-rated' };
const driveNames: Record<string, string> = { 'crank': 'manual crank', 'switch': 'motor with wall switch', 'remote': 'motor with remote', 'solar': 'solar motor' };
const wallNames: Record<string, string> = { 'stucco': 'stucco over block', 'brick': 'brick', 'siding': 'siding or wood frame', 'not_sure': 'not sure' };
 
const money = (amount: number) => '$' + Math.round(amount).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
 
form.addRow(row => {
row.addTextPanel('header', {
computedValue: () => 'Rolling Shutter Quote',
customStyles: { 'font-size': '1.5rem', 'font-weight': '600', 'color': '#1e293b' }
});
});
 
form.addSpacer({ height: 10 });
 
const steps = form.addPages('steps', { heightMode: 'current-page' });
 
// ---------- Step 1: the project ----------
const projectStep = steps.addPage('project');
const projectSection = projectStep.addSubform('project', { title: '🏠 Project', isCollapsible: false });
 
projectSection.addRow(row => {
row.addRadioButton('building', {
label: 'Where do the shutters go?',
options: [
{ id: 'home', name: 'Existing home' },
{ id: 'new_build', name: 'New construction or major remodel' },
{ id: 'condo', name: 'Condo or townhome (association rules apply)' },
{ id: 'storefront', name: 'Storefront or business (security shutters over the glass)' }
],
defaultValue: 'home',
orientation: 'vertical',
isRequired: true
});
});
 
const building = () => projectSection.radioButton('building')?.value() || 'home';
const isStorefront = () => building() === 'storefront';
 
projectSection.addRow(row => {
row.addDropdown('refinish', {
label: 'Re-stuccoing or re-siding soon?',
options: [
{ id: 'yes', name: 'Yes' },
{ id: 'no', name: 'No' },
{ id: 'not_sure', name: 'Not sure yet' }
],
defaultValue: 'not_sure',
isVisible: () => building() === 'home',
tooltip: 'Under new stucco or siding the housing can disappear into the wall'
}, '1fr');
row.addDropdown('stories', {
label: 'How many stories?',
options: [
{ id: 'one', name: 'One story' },
{ id: 'two', name: 'Two or more (+$55 per upstairs shutter)' }
],
defaultValue: 'one',
isVisible: () => !isStorefront()
}, '1fr');
});
 
const twoStory = () => !isStorefront() && projectSection.dropdown('stories')?.value() === 'two';
 
projectSection.addRow(row => {
row.addCheckboxList('purposes', {
label: 'What are the shutters for? (we recommend the slat, the storm rating and the extras from this)',
options: [
{ id: 'storm', name: 'Storm protection - hurricane season, insurance discount' },
{ id: 'security', name: 'Security - break-ins, months away' },
{ id: 'heat', name: 'Sun and heat - cooling bills, west-facing glass' },
{ id: 'privacy', name: 'Privacy - blackout, rooms facing the street' }
],
orientation: 'vertical'
}, '1fr');
row.addDropdown('timing', {
label: 'When would you like them installed?',
options: [
{ id: 'asap', name: 'As soon as possible' },
{ id: 'season', name: 'Before hurricane season' },
{ id: 'quarter', name: 'Within 3 months' },
{ id: 'comparing', name: 'Just comparing quotes' }
],
defaultValue: 'quarter',
isRequired: true
}, '1fr');
});
 
const purposes = () => projectSection.checkboxList('purposes')?.value() || [];
const wantsSecurity = () => purposes().includes('security');
const wantsStorm = () => purposes().includes('storm');
 
// ---------- Step 2: the openings ----------
const openingsStep = steps.addPage('openings');
const openingsSection = openingsStep.addSubform('openings', { title: '🪟 Openings', isCollapsible: false });
 
openingsSection.addRow(row => {
row.addInteger('kinds', {
label: 'How many different opening sizes?',
min: 1,
max: maxOpenings,
defaultValue: 1,
isRequired: true,
tooltip: 'Openings of the same kind and size are one line: set the quantity on it'
}, '1fr');
row.addEmpty('1fr');
});
 
const kindCount = () => Math.min(Math.max(openingsSection.integer('kinds')?.value() || 1, 1), maxOpenings);
 
const residentialKinds: ControlOption[] = Object.keys(openingKinds).filter(id => !openingKinds[id]?.storefront).map(id => ({ id, name: openingKinds[id]?.name || id }));
const storefrontKinds: ControlOption[] = Object.keys(openingKinds).filter(id => openingKinds[id]?.storefront).map(id => ({ id, name: openingKinds[id]?.name || id }));
 
type Opening = { number: number; kind: string; sized: boolean; width: number; height: number; quantity: number; upstairs: boolean };
 
const cards: SubformTs[] = [];
for (let i = 1; i <= maxOpenings; i++) {
const card = openingsSection.addSubform('opening' + i, {
title: 'Opening ' + i,
isCollapsible: false,
isVisible: () => kindCount() >= i
});
 
const measure = () => card.radioButton('size')?.value() !== 'known';
 
card.addRow(row => {
row.addDropdown('kind', {
label: 'What is the shutter for?',
options: () => isStorefront() ? storefrontKinds : residentialKinds,
defaultValue: 'window'
}, '1fr');
row.addInteger('quantity', {
label: 'Quantity',
min: 1,
max: 40,
defaultValue: 1,
isRequired: true
}, '1fr');
});
 
card.addRow(row => {
row.addRadioButton('size', {
label: 'Size',
options: [
{ id: 'measure', name: 'Measure for me (typical size assumed for the estimate)' },
{ id: 'known', name: 'I know the size (inches, outside of the frame)' }
],
defaultValue: 'measure',
orientation: 'vertical'
});
});
 
card.addRow(row => {
row.addInteger('width', {
label: 'Width (in)',
min: 12,
max: 240,
placeholder: 'e.g. 36',
isRequired: true,
isVisible: () => !measure()
}, '1fr');
row.addInteger('height', {
label: 'Height (in)',
min: 12,
max: 144,
placeholder: 'e.g. 60',
isRequired: true,
isVisible: () => !measure()
}, '1fr');
row.addDropdown('floor', {
label: 'Floor',
options: [
{ id: 'first', name: 'Ground' },
{ id: 'second', name: 'Upstairs' }
],
defaultValue: 'first',
isVisible: () => twoStory()
}, '1fr');
});
 
cards.push(card);
}
 
// One opening, every field read through its default, so the estimate never waits for input
const opening = (i: number): Opening => {
const card = cards[i - 1];
const chosen = card?.dropdown('kind')?.value() || '';
const allowed = isStorefront() ? storefrontKinds : residentialKinds;
const kind = allowed.some(o => o.id === chosen) ? chosen : (allowed[0]?.id || 'window');
const typical = openingKinds[kind] || { name: 'Window', width: 36, height: 60, storefront: false };
const known = card?.radioButton('size')?.value() === 'known';
const width = card?.integer('width')?.value() || 0;
const height = card?.integer('height')?.value() || 0;
return {
number: i,
kind,
sized: known && width > 0 && height > 0,
width: known && width > 0 ? width : typical.width,
height: known && height > 0 ? height : typical.height,
quantity: card?.integer('quantity')?.value() || 1,
upstairs: twoStory() && card?.dropdown('floor')?.value() === 'second'
};
};
 
const openingList = (): Opening[] => {
const out: Opening[] = [];
for (let i = 1; i <= kindCount(); i++) out.push(opening(i));
return out;
};
 
const openingSize = (o: Opening) => o.sized ? o.width + ' × ' + o.height + ' in' : 'to be measured';
const squareFeet = (o: Opening) => Math.max((o.width * o.height) / 144, minSqft);
const describeOpening = (o: Opening) => (openingKinds[o.kind]?.name || 'Window') + ', ' + openingSize(o) + ', ' + o.quantity + (o.quantity === 1 ? ' pc' : ' pcs') + (o.upstairs ? ', upstairs' : '');
const shutterCount = () => openingList().reduce((sum, o) => sum + o.quantity, 0);
const upstairsCount = () => openingList().filter(o => o.upstairs).reduce((sum, o) => sum + o.quantity, 0);
 
// ---------- Step 3: the shutter ----------
const shutterStep = steps.addPage('shutter');
const shutterSection = shutterStep.addSubform('shutter', { title: '🛡️ Shutter', isCollapsible: false });
 
// New construction or new stucco to come: concealed. A house that stays as it is, or a storefront: on the wall. A condo: in the opening.
const recommendedHousing = () => {
if (building() === 'new_build') return 'recessed';
if (building() === 'condo') return 'inset';
if (building() === 'storefront') return 'surface';
return projectSection.dropdown('refinish')?.value() === 'yes' ? 'recessed' : 'surface';
};
 
shutterSection.addRow(row => {
row.addRadioButton('housing', {
label: () => 'Housing (we recommend: ' + (housingNames[recommendedHousing()] || '') + ')',
options: [
{ id: 'recessed', name: 'Built-in (concealed) - hidden behind the stucco or soffit (+18%)' },
{ id: 'surface', name: 'Surface-mounted - housing on the wall above the opening (included)' },
{ id: 'inset', name: 'In the opening - tucked under the header inside the recess (+6%)' }
],
defaultValue: 'surface',
orientation: 'vertical',
isRequired: true
});
});
 
const housing = () => shutterSection.radioButton('housing')?.value() || 'surface';
 
shutterSection.addRow(row => {
row.addRadioButton('slat', {
label: () => {
const tip = wantsSecurity() ? 'extruded security' : wantsStorm() ? '55 mm foam-filled' : purposes().length > 0 ? '40 mm foam-filled' : '';
return 'Slat profile' + (tip ? ' (we recommend: ' + tip + ')' : '');
},
options: [
{ id: 'foam40', name: '40 mm foam-filled - slim housing; sun, privacy and everyday security ($32/sq ft)' },
{ id: 'foam55', name: '55 mm foam-filled - wider spans and wind-rated; sliding doors and lanais ($38/sq ft)' },
{ id: 'extruded', name: 'Extruded security slat - solid aluminum, impact-rated ($58/sq ft)' }
],
defaultValue: 'foam55',
orientation: 'vertical',
isRequired: true,
isVisible: () => !isStorefront()
});
});
 
shutterSection.addRow(row => {
row.addRadioButton('storeSlat', {
label: 'Storefront shutter',
options: [
{ id: 'extruded', name: 'Solid security shutter - impact-rated, nothing to see after hours ($58/sq ft)' },
{ id: 'perforated', name: 'Perforated shutter - the display stays on show ($64/sq ft)' },
{ id: 'grille', name: 'Open grille - air and light pass; covered walkways and malls ($46/sq ft)' }
],
defaultValue: 'extruded',
orientation: 'vertical',
isRequired: true,
isVisible: () => isStorefront()
});
});
 
const slat = () => isStorefront()
? (shutterSection.radioButton('storeSlat')?.value() || 'extruded')
: (shutterSection.radioButton('slat')?.value() || 'foam55');
 
shutterSection.addRow(row => {
row.addRadioButton('rating', {
label: () => 'Storm rating' + (wantsStorm() ? ' (we recommend: wind-rated)' : ''),
options: [
{ id: 'none', name: 'Not needed - inland, or sun and privacy only (included)' },
{ id: 'wind', name: 'Wind-rated - Florida Building Code approved outside the HVHZ (+25%)' },
{ id: 'hvhz', name: 'HVHZ impact-rated - Miami-Dade NOA, large-missile tested (+45%)' }
],
defaultValue: 'none',
orientation: 'vertical',
isRequired: true,
tooltip: 'A rated shutter can earn a wind mitigation discount on your property insurance; ask your agent'
}, '1fr');
row.addRadioButton('color', {
label: 'Color',
options: [
{ id: 'white', name: 'White (included)' },
{ id: 'beige', name: 'Beige (included)' },
{ id: 'bronze', name: 'Bronze (included)' },
{ id: 'woodgrain', name: 'Wood grain (+15%)' }
],
defaultValue: 'white',
orientation: 'vertical',
isRequired: true,
tooltip: 'Housing, rails and slats in the same color; custom powder-coat colors on request'
}, '1fr');
});
 
const rating = () => shutterSection.radioButton('rating')?.value() || 'none';
const stormRated = () => rating() !== 'none';
const color = () => shutterSection.radioButton('color')?.value() || 'white';
 
shutterSection.addRow(row => {
row.addTextPanel('shutterNotes', {
computedValue: () => {
const notes: string[] = [];
if (building() === 'condo') notes.push('🏢 Association approval: rolling shutters change the exterior, so most condo and homeowner associations must approve them. A housing in the opening changes the least, and many associations have a pre-approved color.');
if (!isStorefront() && stormRated() && slat() === 'foam40') notes.push('⚠️ The 40 mm slat is not storm-rated. For a rated shutter we quote the 55 mm or the extruded slat.');
if (isStorefront() && stormRated() && slat() === 'grille') notes.push('⚠️ An open grille stops people, not wind and debris. For a rated storefront shutter we quote the solid security shutter.');
if (stormRated()) notes.push('📋 A storm-rated shutter needs a building permit and a product approval (Florida FL# or Miami-Dade NOA). We pull the permit, install to the approval and schedule the final inspection (' + money(permitFee) + ' once per job).');
return notes.join('\n');
},
isVisible: () => building() === 'condo' || stormRated(),
customStyles: { 'font-size': '0.9rem', 'color': '#0369a1', 'background': '#e0f2fe', 'padding': '10px', 'border-radius': '6px', 'white-space': 'pre-line' }
});
});
 
// ---------- Step 4: operation ----------
const operationStep = steps.addPage('operation');
const operationSection = operationStep.addSubform('operation', { title: '⚙️ Operation', isCollapsible: false });
 
operationSection.addRow(row => {
row.addRadioButton('drive', {
label: 'How do the shutters move?',
options: [
{ id: 'crank', name: 'Manual crank - gear and crank handle inside, no power needed (included)' },
{ id: 'switch', name: 'Motor with wall switch (+$420/shutter)' },
{ id: 'remote', name: 'Motor with remote - every shutter from one remote, in groups (+$520/shutter)' },
{ id: 'solar', name: 'Solar motor - panel and battery, no wiring or electrician (+$780/shutter)' }
],
defaultValue: 'remote',
orientation: 'vertical',
isRequired: true
});
});
 
const drive = () => operationSection.radioButton('drive')?.value() || 'remote';
const motorized = () => drive() !== 'crank';
const wiredMotor = () => drive() === 'switch' || drive() === 'remote';
 
operationSection.addRow(row => {
row.addDropdown('power', {
label: 'Is there power at the openings?',
options: [
{ id: 'yes', name: 'Yes' },
{ id: 'no', name: 'No (+$185/shutter electrician hookup, or pick a solar motor)' },
{ id: 'not_sure', name: 'Not sure' }
],
defaultValue: 'not_sure',
isVisible: () => building() !== 'new_build' && wiredMotor(),
tooltip: 'A 110 V line or junction box by the header, where the motor connects. Every wired motor is hooked up by a licensed electrician; it is included when the line is there'
}, '1fr');
row.addEmpty('1fr');
});
 
const noPower = () => building() !== 'new_build' && wiredMotor() && operationSection.dropdown('power')?.value() === 'no';
 
operationSection.addRow(row => {
row.addTextPanel('noPowerNote', {
computedValue: () => '🔌 No power at the opening: we recommend a solar motor, with no wiring and no electrician. For a wired motor a licensed electrician runs the line and makes the connection (' + money(hookupPerShutter) + ' per shutter).',
isVisible: () => noPower(),
customStyles: { 'font-size': '0.9rem', 'color': '#0369a1', 'background': '#e0f2fe', 'padding': '10px', 'border-radius': '6px' }
});
});
 
operationSection.addRow(row => {
row.addCheckboxList('extras', {
label: 'Extras',
options: () => {
const options: CheckboxListOption[] = [];
if (motorized()) options.push({ id: 'smart', name: 'Smart home hub - app, schedules and voice control, once per order (+$350)' });
if (wiredMotor()) options.push({ id: 'battery', name: 'Battery backup - closes and opens when the grid is down before a storm (+$240/shutter)' + (wantsStorm() ? ' - recommended' : '') });
options.push({ id: 'locks', name: 'Interior locking pins - the curtain cannot be lifted from outside (+$45/shutter)' + (wantsSecurity() ? ' - recommended' : '') });
return options;
},
orientation: 'vertical'
});
});
 
const extras = () => operationSection.checkboxList('extras')?.value() || [];
const smartHub = () => motorized() && extras().includes('smart');
const battery = () => wiredMotor() && extras().includes('battery');
const locks = () => extras().includes('locks');
 
// ---------- Step 5: installation ----------
const installStep = steps.addPage('install');
const installSection = installStep.addSubform('installation', { title: '🔧 Installation', isCollapsible: false });
 
installSection.addRow(row => {
row.addRadioButton('wall', {
label: 'What are the walls around the openings?',
options: [
{ id: 'stucco', name: 'Stucco over block (included)' },
{ id: 'brick', name: 'Brick (+$40/shutter)' },
{ id: 'siding', name: 'Siding or wood frame - blocking behind the siding (+$75/shutter)' },
{ id: 'not_sure', name: 'Not sure' }
],
defaultValue: 'stucco',
orientation: 'vertical',
isRequired: true
});
});
 
const wall = () => installSection.radioButton('wall')?.value() || 'stucco';
 
installSection.addRow(row => {
row.addCheckbox('removal', {
label: 'Remove old accordion, panel or roll-down shutters and haul them away (+$60/shutter)',
defaultValue: false,
isVisible: () => building() !== 'new_build'
}, '1fr');
row.addCheckbox('hoa', {
label: 'My HOA or association must approve (we prepare the drawings, product approval and color sample)',
defaultValue: false,
isVisible: () => !isStorefront()
}, '1fr');
});
 
const removal = () => building() !== 'new_build' && !!installSection.checkbox('removal')?.value();
const hoa = () => !isStorefront() && !!installSection.checkbox('hoa')?.value();
 
installSection.addRow(row => {
row.addTextPanel('hoaNote', {
computedValue: () => '📅 Most associations answer within two to four weeks. We send the application package right after the measure, so the approval and the factory run side by side.',
isVisible: () => hoa(),
customStyles: { 'font-size': '0.9rem', 'color': '#0369a1', 'background': '#e0f2fe', 'padding': '10px', 'border-radius': '6px' }
});
});
 
// ---------- Pricing ----------
const surchargePct = () => (housingPcts[housing()] || 0) + (ratingPcts[rating()] || 0) + (colorPcts[color()] || 0);
const shutterEach = (o: Opening) => squareFeet(o) * (slatRates[slat()] || 38) * (1 + surchargePct() / 100);
const driveEach = () => (drivePrices[drive()] || 0) + (wiredMotor() && noPower() ? hookupPerShutter : 0);
const extrasEach = () => (battery() ? batteryPerShutter : 0) + (locks() ? locksPerShutter : 0);
const installEach = (o: Opening) => installPerShutter + (wallSurcharges[wall()] || 0) + (o.upstairs ? secondFloorPerShutter : 0) + (removal() ? removalPerShutter : 0);
 
const shuttersCost = () => openingList().reduce((sum, o) => sum + (shutterEach(o) + driveEach() + extrasEach()) * o.quantity, 0);
const installCost = () => openingList().reduce((sum, o) => sum + installEach(o) * o.quantity, 0);
const smartHubCost = () => smartHub() ? smartHubPrice : 0;
const permitCost = () => stormRated() ? permitFee : 0;
const beforePromo = () => shuttersCost() + installCost() + smartHubCost() + permitCost();
const promoDiscount = () => beforePromo() * promoPct / 100;
const afterPromo = () => beforePromo() - promoDiscount();
// The minimum job applies last, after every discount
const minimumApplied = () => afterPromo() < minJob;
const total = () => Math.round(Math.max(afterPromo(), minJob));
const roundTo50 = (amount: number) => Math.round(amount / 50) * 50;
const totalLow = () => roundTo50(total() * (1 - rangePct / 100));
const totalHigh = () => roundTo50(total() * (1 + rangePct / 100));
 
const describeShutter = () => (housingNames[housing()] || '') + ' housing, ' + (slatNames[slat()] || '') + ' slat, ' + color() + ', ' + (ratingNames[rating()] || '')
+ (housing() === recommendedHousing() ? '' : ' (we recommended ' + (housingNames[recommendedHousing()] || '') + ')');
const describeOperation = () => [
driveNames[drive()] || '',
noPower() ? 'no power at the openings' : '',
smartHub() ? 'smart home hub' : '',
battery() ? 'battery backup' : '',
locks() ? 'interior locking pins' : ''
].filter(part => part).join(', ');
const describeInstall = () => [
'measure and install on ' + (wallNames[wall()] || wall()),
removal() ? 'removal of old shutters' : '',
upstairsCount() > 0 ? upstairsCount() + ' upstairs' : '',
stormRated() ? 'permit and inspection' : '',
hoa() ? 'HOA application' : ''
].filter(part => part).join(', ');
 
// ---------- Step 6: estimate and contact ----------
const quoteStep = steps.addPage('quote');
const summarySection = quoteStep.addSubform('summary', { title: '💰 Your Estimate', isCollapsible: false });
 
summarySection.addRow(row => {
row.addTextPanel('promo', {
computedValue: () => '🎉 ' + promoText + ': ' + promoPct + '% off is already included in the estimate below.',
isVisible: () => promoPct > 0,
customStyles: { 'font-size': '0.95rem', 'color': '#065f46', 'background': '#d1fae5', 'padding': '10px', 'border-radius': '6px' }
});
});
 
summarySection.addRow(row => {
row.addTextPanel('openingsSummary', {
computedValue: () => openingList().map(o => 'Opening ' + o.number + ': ' + describeOpening(o) + ' - ' + money((shutterEach(o) + driveEach() + extrasEach()) * o.quantity)).join('\n')
+ '\nShutter: ' + describeShutter() + '\nOperation: ' + describeOperation(),
customStyles: { 'font-size': '0.9rem', 'color': '#334155', 'white-space': 'pre-line' }
});
});
 
summarySection.addRow(row => {
row.addPriceDisplay('shuttersCost', {
label: () => 'Shutters, motors and extras (' + shutterCount() + (shutterCount() === 1 ? ' shutter' : ' shutters') + ')',
computedValue: () => Math.round(shuttersCost()),
decimals: 0
}, '1fr');
row.addPriceDisplay('installCost', {
label: () => 'Installation - ' + describeInstall(),
computedValue: () => Math.round(installCost()),
decimals: 0,
prefix: '+'
}, '1fr');
});
 
summarySection.addRow(row => {
row.addPriceDisplay('smartHubCost', {
label: 'Smart home hub (once per order)',
computedValue: () => smartHubCost(),
decimals: 0,
prefix: '+',
isVisible: () => smartHub()
}, '1fr');
row.addPriceDisplay('permitCost', {
label: 'Permit and inspection (once per job)',
computedValue: () => permitCost(),
decimals: 0,
prefix: '+',
isVisible: () => stormRated()
}, '1fr');
});
 
summarySection.addRow(row => {
row.addPriceDisplay('promoDiscount', {
label: promoText + ' (-' + promoPct + '%)',
computedValue: () => -Math.round(promoDiscount()),
decimals: 0,
variant: 'success',
isVisible: () => promoDiscount() > 0
}, '1fr');
row.addPriceDisplay('minimumJob', {
label: 'Minimum job adjustment (small jobs are billed at our minimum of ' + money(minJob) + ')',
computedValue: () => Math.round(total() - afterPromo()),
decimals: 0,
prefix: '+',
isVisible: () => minimumApplied()
}, '1fr');
});
 
summarySection.addSpacer({ showLine: true, lineStyle: 'solid', lineColor: '#e2e8f0' });
 
summarySection.addRow(row => {
row.addPriceDisplay('totalEstimate', {
label: 'Estimated cost',
computedValue: () => total(),
decimals: 0,
variant: 'large',
suffix: 'installed, before sales tax'
});
});
 
summarySection.addRow(row => {
row.addTextPanel('rangeNote', {
computedValue: () => 'Expect ' + money(totalLow()) + ' – ' + money(totalHigh()) + ' (±' + rangePct + '%): the range covers what a measure usually finds.'
+ (financingNote ? ' ' + financingNote : ''),
customStyles: { 'font-size': '0.9rem', 'color': '#059669', 'font-weight': '500' }
});
});
 
summarySection.addRow(row => {
row.addTextPanel('nextSteps', {
computedValue: () => 'A consultant calls to go over your request and books a free on-site measure within ' + measureDays
+ ' business days. After the measure you get a firm quote, and the shutters are installed about ' + leadWeeks + ' weeks from your order.',
customStyles: { 'font-size': '0.85rem', 'color': '#64748b', 'font-style': 'italic' }
});
});
 
const contactSection = quoteStep.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.addTextbox('cityZip', {
label: 'City and ZIP code',
placeholder: 'Fort Myers, 33966',
isRequired: true,
tooltip: 'Where we come out to measure; the county sets the wind zone'
}, '1fr');
row.addEmail('email', {
label: 'Email',
placeholder: 'john@example.com',
tooltip: 'For the PDF estimate and the written quote'
}, '1fr');
});
 
contactSection.addRow(row => {
row.addDropdown('callTime', {
label: 'Best time to call',
options: [
{ id: 'any', name: 'Any time' },
{ id: 'morning', name: 'Before noon' },
{ id: 'afternoon', name: 'After noon' }
],
defaultValue: 'any'
}, '1fr');
row.addEmpty('1fr');
});
 
contactSection.addRow(row => {
row.addTextarea('notes', {
label: 'Anything else?',
placeholder: 'E.g. an arched opening, a gate code, HOA color rules, days that work for the measure',
rows: 3
});
});
 
const whenWeCall = () => {
const when = contactSection.dropdown('callTime')?.value();
return when === 'morning' ? 'before noon' : when === 'afternoon' ? 'after noon' : 'shortly';
};
 
form.configureSubmitButton({ label: 'Request My Quote' });
 
form.configureCompletionScreen({
type: 'text',
title: 'Your request is with a consultant',
message: () => 'We will call you ' + whenWeCall() + ' with a shutter recommendation and book your free on-site measure within ' + measureDays + ' business days.'
});
 
// ---------- PDF estimate ----------
form.configurePdf('estimate', pdf => {
const items = openingList();
const yesNo = (value: boolean) => value ? 'yes' : 'no';
 
pdf.configure({
filename: 'rolling-shutter-estimate',
pageSize: 'Letter',
allowUserDownload: true,
downloadButtonLabel: 'Download the PDF estimate',
header: {
title: 'Rolling shutter estimate',
subtitle: shutterCount() + (shutterCount() === 1 ? ' shutter' : ' shutters') + ' · ' + (housingNames[housing()] || '') + ' · ' + color() + ' · ' + (driveNames[drive()] || '')
},
footer: {
text: 'Estimate only. A firm quote follows the free on-site 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('Email', contactSection.email('email')?.value() || '')
.addField('City and ZIP', contactSection.textbox('cityZip')?.value() || ''));
});
 
pdf.addSection('Openings', section => {
section.addTable(
['No.', 'Opening', 'Size', 'Floor', 'Qty'],
items.map(o => [String(o.number), openingKinds[o.kind]?.name || 'Window', openingSize(o), o.upstairs ? 'upstairs' : 'ground', String(o.quantity)])
);
if (items.some(o => !o.sized)) {
section.addText('Openings without a size are measured on site; the estimate assumes typical sizes for them.');
}
});
 
pdf.addSection('Shutter', section => {
section.addRow(row => row
.addField('Housing', housingNames[housing()] || '')
.addField('Slat profile', slatNames[slat()] || '')
.addField('Storm rating', ratingNames[rating()] || '')
.addField('Color', color()));
if (housing() !== recommendedHousing()) {
section.addText('For this building we recommended a ' + (housingNames[recommendedHousing()] || '') + ' housing; your consultant goes over both.');
}
if (stormRated()) {
section.addText('A storm-rated shutter is installed to its product approval (Florida FL# or Miami-Dade NOA) under a building permit with a final inspection.');
}
});
 
pdf.addSection('Operation and extras', section => {
section.addRow(row => row
.addField('Operation', driveNames[drive()] || '')
.addField('Power at the openings', wiredMotor() && building() !== 'new_build' ? (operationSection.dropdown('power')?.value() || 'not sure').replace('_', ' ') : 'not applicable')
.addField('Smart home hub', yesNo(smartHub()))
.addField('Battery backup', yesNo(battery()))
.addField('Interior locking pins', yesNo(locks())));
});
 
pdf.addSection('Building, installation and timing', section => {
section.addRow(row => row
.addField('Building', building().replace('_', ' '))
.addField('Shutters for', purposes().join(', ') || 'not marked')
.addField('Walls', wallNames[wall()] || wall())
.addField('Remove old shutters', yesNo(removal()))
.addField('HOA approval', hoa() ? 'needed, we prepare the package' : 'not needed')
.addField('Timing', projectSection.dropdown('timing')?.value() || ''));
});
 
pdf.addSection('Estimated cost', section => {
const rows: string[][] = items.map(o => [
'Opening ' + o.number + ': ' + (openingKinds[o.kind]?.name || 'Window').toLowerCase() + ', ' + openingSize(o) + ', ' + (slatNames[slat()] || '') + ' slat, ' + (driveNames[drive()] || ''),
String(o.quantity),
money((shutterEach(o) + driveEach() + extrasEach()) * o.quantity)
]);
rows.push(['Installation: ' + describeInstall(), String(shutterCount()), money(installCost())]);
if (smartHub()) rows.push(['Smart home hub', '1', money(smartHubCost())]);
if (stormRated()) rows.push(['Permit and inspection', '1', money(permitCost())]);
if (promoDiscount() > 0) rows.push([promoText + ' (-' + promoPct + '%)', '', '-' + money(promoDiscount())]);
if (minimumApplied()) rows.push(['Minimum job', '', money(total() - afterPromo())]);
rows.push(['Estimated total (installed, before sales tax)', '', money(total())]);
section.addTable(['Item', 'Qty', 'Amount'], rows);
section.addText('Expect ' + money(totalLow()) + ' – ' + money(totalHigh()) + ' (range ±' + rangePct + '%); the firm quote follows the measure.');
});
 
const notes = contactSection.textarea('notes')?.value();
if (notes) {
pdf.addSection('Notes', section => section.addText(notes));
}
 
pdf.addSection('Terms', section => {
section.addText('We call you ' + whenWeCall() + ' with a shutter recommendation and book your free on-site measure within ' + measureDays + ' business days; the shutters are installed about ' + leadWeeks + ' weeks from your order.' + (stormRated() ? ' We pull the permit and schedule the final inspection.' : ''));
section.addText('This is an estimate, not a binding offer. Prices are before sales tax and may change after the on-site measure, which confirms sizes, wall construction, power at the openings and the wind zone. A firm quote in writing follows the measure.');
});
});
}
 

Frequently Asked Questions

How are rolling shutters priced in this calculator?

Per square foot of opening by slat profile (40 mm foam-filled, 55 mm foam-filled, extruded security, perforated or open grille), with percent surcharges for a concealed or in-the-opening housing, a wind-rated or HVHZ impact-rated version and a wood-grain finish. Motors, battery backup, locking pins and installation are added per shutter, the smart hub and the permit once per job. Every rate is a constant at the top of the code.

What if the customer does not know the size of the opening?

Each opening has a 'Measure for me' option. The estimate then assumes a typical size for that kind of opening - a 36 × 60 in window, a 96 × 80 in sliding glass door, a 144 × 96 in lanai - and the PDF says which openings still need to be measured. You can change the typical sizes to match your market.

How does the form recommend the housing and the slat?

From the answers on the first step. New construction or a planned re-stucco points to a concealed housing, a condo to a housing in the opening, an existing home or a storefront to a surface-mounted one. Security points to the extruded slat, storm protection to the 55 mm slat and a wind rating. The customer still makes the final choice and the lead notes when it differs from the recommendation.

Does the calculator handle storefront security shutters?

Yes. When the customer picks 'Storefront or business', the openings switch to storefront windows, entrance doors and bay openings, and the slat choice becomes solid, perforated or open-grille security shutters. A warning explains that an open grille is not a storm shutter when a storm rating is selected.

Can I enforce a minimum job and show financing?

Yes. The minimum job is a constant applied after every discount, and the calculator shows the adjustment as its own line so the customer understands it. The financing note is one sentence shown next to the estimate; leave it empty to hide it. The estimate range width and the promotion percentage are constants too.