PageSourceSearch

https://book.ductcare.com/themes/ductcare/js/bookings-request.js?v=69

js ductcare.com collected 2026-09-30 21:26:50 UTC 22,789 bytes, 500 lines download raw bytes

1/*global $, BookingsCommon */
2/**
3 * bookings-request.js
4 * Main request/cart page: path /request but not schedule, confirmation, or /requests/.
5 * Handles: service cards, system/furnace quantity modal, add-to-cart (per product index), checkbox cart,
6 * positive/negative buttons, loadCart. Commercial users are redirected to confirmation.
7 */
8console.warn("[BookingsRequest] script loaded"); 
9
10var BookingsRequest = {
11	init: function () {
12		"use strict";
13		var path = window.location.pathname;
14		var href = window.location.href;
15		console.warn("book test");
16		if ((!path.includes("/request") && !path.includes("/hvac-duct-cleaning-services")) || href.includes("/request/schedule") || href.includes("/request/confirmation") || path.includes("/requests/")) return;
17
18
19		try{
20			$("#ductcare-mobile-cta-bar").hide()
21		}
22		catch(ex){
23			console.warn("Error "+ex);
24		}
25
26		BookingsCommon.fetchPromotions();
27		localStorage.setItem("squareFootage", 0);
28		localStorage.setItem("systemRequests", 1);
29		BookingsCommon.checkBlankCustomerInfo();
30		$("#page-header-image, #block-olivero-pageheader").css("max-height", "20vh");
31
32		$('.grid-full').css({
33			'display': 'grid',
34			'grid-template-rows': 'unset',
35			'grid-template-columns': 'unset',
36			'grid-column-gap': 'unset'
37		});
38
39		$("#wb_bookingStep1").addClass("booking_progress_current");
40		$("#wb_bookingStep2").addClass("booking_progress_next_prev");
41		$("#wb_bookingStep3").addClass("booking_progress_next_prev");
42		$("#wb_bookingStep4").addClass("booking_progress_next_prev");
43
44		var customerInfo = JSON.parse(sessionStorage.getItem('customerInfo') || '{}');
45		if (customerInfo.service_type === "Commercial") {
46			window.location.href = '/request/confirmation';
47			return;
48		}
49
50		// Build package cards from grid items (icons, features, hidden add-to-cart button)
51		try {
52			$(".request-service-grid .views-view-responsive-grid__item").each(function (index,item) {
53				var $item = $(this);
54				var item_id = $item.find(".service-request-type-id")[0].innerText;
55				var item_name = $item.find(".services-title")[0].innerText.trim();
56				var item_price = $item.find(".booking-price")[0].innerHTML;
57				var modelEl = $(item).find(".pricing-model")[0];
58				var model = modelEl ? modelEl.innerText.trim() : "";
59				var additionalFurnacePrice = BookingsCommon.getAdditionalFurnacePriceFromGridItem($item);
60				$item.find(".additional-furnace-price").hide();
61				console.warn("Pricing model 7: ", model);
62				var pricingModel = BookingsCommon.getPricingModelFromGridItem(model);
63				console.warn("Pricing model 6: ", pricingModel);
64				var unitLabel = BookingsCommon.pricingModelUnitLabel(pricingModel);
65				var descEl = $(".services-description")[index];
66				var desc_first_line = descEl && descEl.children[0] ? descEl.children[0].innerText.trim() : '';
67				var featureLines = [];
68				$(descEl).find('p, li').each(function () {
69					var t = $(this).text().trim();
70					if (t) featureLines.push(t);
71				});
72				if (featureLines.length === 0 && desc_first_line) featureLines = [desc_first_line];
73
74				var iconClasses = ['request-service-card__icon--blue', 'request-service-card__icon--green', 'request-service-card__icon--purple'];
75				var iconFa = ['fa-fan', 'fa-shield-alt', 'fa-bolt'];
76				var iconClass = iconClasses[index % iconClasses.length];
77				var iconFaClass = iconFa[index % iconFa.length];
78				var iconHtml =""// '<div class="request-service-card__icon ' + iconClass + '"><i class="fas ' + iconFaClass + '"></i></div>';
79				var featuresHtml = featureLines.map(function (line) {
80					return '<div class="request-service-card__feature"><span class="request-service-card__check">✓</span> ' + line + '</div>';
81				}).join('');
82
83				var $midHl = $item.find(".service-middle-highlight").first();
84				var $footHl = $item.find(".service-footer-highlight").first();
85				var middleHlHtml = $midHl.length ? $.trim($midHl.html()) : "";
86				var footerHlHtml = $footHl.length ? $.trim($footHl.html()) : "";
87				var middleHlBlock = middleHlHtml
88					? '<div class="request-service-card__middle-highlight">' + middleHlHtml + "</div>"
89					: "";
90				var footerHlBlock = footerHlHtml
91					? '<div class="request-service-card__footer-highlight">' + footerHlHtml + "</div>"
92					: "";
93
94				var packageCardHtml =
95					'<div class="request-service-card">' + iconHtml +
96					'<h3 class="request-service-card__title">' + item_name + '</h3>' +
97					'<div class="request-service-card__price-row"><span class="request-service-card__price">' + item_price + '</span><span class="request-service-card__starting"> ' + unitLabel + '</span></div>' +
98					middleHlBlock +
99					'<p class="request-service-card__desc">' + desc_first_line + '</p>' +
100					footerHlBlock +
101					'<div class="request-service-card__features">' + featuresHtml + '</div>' +
102					'<div class="request-service-card__cta">SELECT PACKAGE <i class="fas fa-chevron-right"></i></div></div>';
103
104				$item.find(".services-title").html(packageCardHtml);
105				$item.addClass("request-service-grid__item--card");
106				$item.append('<div class="services-description-expand">' + ($(".services-description")[index] ? $(".services-description")[index].innerHTML : '') + '</div>');
107				$item.append('<button type="button" item="' + item_name + '" price="' + item_price + '" data-pricing-model="' + pricingModel + '" data-additional-furnace-price="' + additionalFurnacePrice + '" data-drupal-selector="edit-field-service-requested-' + item_id + '" id="edit-field-service-requested-' + item_id + '" name="field_service_requested' + item_id + '" value="' + item_id + '" class="button-blue add-to-cart add-to-cart--hidden" aria-hidden="true" tabindex="-1">Add To Cart</button>');
108			});
109
110			if ($(".request-service-grid").length && !$(".request-service-features").length) {
111				var featuresHtml = '<div class="request-service-features">' +
112					'<div class="request-service-features__item"><div class="request-service-features__icon request-service-features__icon--green"><i class="fas fa-shield-alt"></i></div><div class="request-service-features__text"><strong>Certified Techs</strong><span>NADCA certified professionals with 10+ years experience.</span></div></div>' +
113					'<div class="request-service-features__item"><div class="request-service-features__icon request-service-features__icon--blue"><i class="fas fa-fan"></i></div><div class="request-service-features__text"><strong>HEPA Filtration</strong><span>We use medical-grade HEPA filters to capture 99.97% of particles.</span></div></div>' +
114					'<div class="request-service-features__item"><div class="request-service-features__icon request-service-features__icon--purple"><i class="fas fa-bolt"></i></div><div class="request-service-features__text"><strong>Quick Service</strong><span>Most cleanings completed in 2-4 hours with zero mess left behind.</span></div></div></div>';
115				$(".request-service-grid").append(featuresHtml);
116			}
117		} catch (ex) {}
118
119		// Hide raw fields; card UI is shown instead
120		$(".services-description").hide();
121		$(".booking-measurements").hide();
122		$(".booking-price").hide();
123		$(".service-footer-highlight").hide();
124		$(".service-middle-highlight").hide();
125		$(".services-description-expand").hide();
126		$(".pricing-model, .additional-furnace-price").hide();
127
128		// Quantity modal: per-system (systems + furnaces) or per-vent (vent quantity only)
129		var $ductModal = $(
130			'<div id="duct-count-overlay" class="duct-modal-overlay" style="display:none;">' +
131			'<div id="duct-count-modal" class="duct-modal-box">' +
132			'<div class="duct-modal-vent-group" style="display:none;">' +
133			'<h3 class="duct-modal-title">How many vents do you have?</h3>' +
134			'<p class="duct-modal-subtitle">Pricing is per vent. Enter the quantity of vents to calculate your total.</p>' +
135			'<label class="duct-modal-label" for="vent-count-input">Quantity of vents</label>' +
136			'<input type="number" id="vent-count-input" class="duct-modal-input" min="1" value="1" />' +
137			'</div>' +
138			'<div class="duct-modal-system-group">' +
139			'<h3 class="duct-modal-title">How many HVAC systems?</h3>' +
140			'<p class="duct-modal-subtitle">Pricing is per system. Enter systems and furnaces to calculate your total.</p>' +
141			'<label class="duct-modal-label" for="duct-count-input">Systems</label>' +
142			'<input type="number" id="duct-count-input" class="duct-modal-input duct-modal-input--systems" min="1" value="1" />' +
143			'<div class="duct-modal-furnace-group">' +
144			'<label class="duct-modal-label" for="furnace-count-input">Furnaces</label>' +
145			'<input type="number" id="furnace-count-input" class="duct-modal-input duct-modal-input--fur
145nace" min="1" value="1" />' +
146			'<p class="duct-modal-furnace-note"></p>' +
147			'</div>' +
148			'</div>' +
149			'<p class="duct-modal-total">Total: $<span id="duct-total-amount">0.00</span></p>' +
150			'<div class="duct-modal-actions">' +
151			'<button type="button" class="duct-modal-select-date button-orange">Select service date</button>' +
152			'<br><button type="button" class="duct-modal-submit button-blue">Add another product</button>' +
153			'<br><button type="button" class="duct-modal-cancel button-grey">Cancel</button></div></div></div>'
154		);
155		$("body").append($ductModal);
156		var pendingDuctAdd = null;
157
158		function furnaceExtraUsd() {
159			if (pendingDuctAdd && pendingDuctAdd.furnaceExtraUsd != null) {
160				return pendingDuctAdd.furnaceExtraUsd;
161			}
162			return (typeof BookingsCommon !== "undefined" && BookingsCommon.FURNACE_EXTRA_SURCHARGE_USD != null)
163				? BookingsCommon.FURNACE_EXTRA_SURCHARGE_USD
164				: 145;
165		}
166
167		function updateFurnaceModalNote(amount) {
168			$(".duct-modal-furnace-note").text(
169				"The first furnace is included with your quote. Each additional furnace adds $" + amount + " to the total."
170			);
171		}
172
173		function isPerVentPricing(model) {
174			return BookingsCommon.normalizePricingModel(model) === "per_vent";
175		}
176
177		function setModalPricingMode(pricingModel) {
178			var perVent = isPerVentPricing(pricingModel);
179			$(".duct-modal-vent-group").toggle(perVent);
180			$(".duct-modal-system-group").toggle(!perVent);
181		}
182
183		function qtyForTotal($input, minVal) {
184			var qty = parseInt($input.val(), 10);
185			// Only overwrite input when valid; leave empty/partial so mobile users can type digits
186			if (!isNaN(qty) && qty >= minVal) {
187				$input.val(qty);
188				return qty;
189			}
190			return minVal;
191		}
192
193		function updateDuctTotal() {
194			if (!pendingDuctAdd) return;
195
196			if (isPerVentPricing(pendingDuctAdd.pricingModel)) {
197				var vents = qtyForTotal($("#vent-count-input"), 1);
198				$("#duct-total-amount").text((pendingDuctAdd.unitPrice * vents).toFixed(2));
199				return;
200			}
201
202			var systems = qtyForTotal($("#duct-count-input"), 1);
203			var furnaces = qtyForTotal($("#furnace-count-input"), 1);
204			var base = pendingDuctAdd.unitPrice * systems;
205			var addon = Math.max(0, furnaces - 1) * furnaceExtraUsd();
206			$("#duct-total-amount").text((base + addon).toFixed(2));
207		}
208
209		function openDuctModal(unitPrice, value, name, pricingModel, furnaceExtraUsdAmount) {
210			pricingModel = BookingsCommon.normalizePricingModel(pricingModel || "per_system");
211			var furnaceExtra = BookingsCommon.getAdditionalFurnacePriceFromGridItem(furnaceExtraUsdAmount);
212			pendingDuctAdd = {
213				unitPrice: unitPrice,
214				value: value,
215				name: name,
216				pricingModel: pricingModel,
217				furnaceExtraUsd: furnaceExtra
218			};
219			updateFurnaceModalNote(furnaceExtra);
220			setModalPricingMode(pricingModel);
221			$("#vent-count-input").val(1);
222			$("#duct-count-input").val(1);
223			$("#furnace-count-input").val(1);
224			$("#duct-total-amount").text(unitPrice.toFixed(2));
225			$("#duct-count-overlay").fadeIn(200);
226			if (isPerVentPricing(pricingModel)) {
227				$("#vent-count-input").focus();
228			} else {
229				$("#duct-count-input").focus();
230			}
231		}
232
233		function getPricingModelForItem($item) {
234			var $btn = $item.find(".add-to-cart");
235			if ($btn.attr("data-pricing-model")) {
236				return BookingsCommon.normalizePricingModel($btn.attr("data-pricing-model"));
237			}
238			var modelEl = $item.find(".pricing-model")[0];
239			var model = modelEl ? modelEl.innerText.trim() : "";
240			return BookingsCommon.getPricingModelFromGridItem(model);
241		}
242
243		function getAdditionalFurnacePriceForItem($item) {
244			var $btn = $item.find(".add-to-cart");
245			var fromAttr = parseFloat($btn.attr("data-additional-furnace-price"));
246			if (!isNaN(fromAttr) && fromAttr >= 0) {
247				return fromAttr;
248			}
249			return BookingsCommon.getAdditionalFurnacePriceFromGridItem($item);
250		}
251
252		function closeDuctModal() {
253			pendingDuctAdd = null;
254			$("#duct-count-overlay").fadeOut(200);
255		}
256
257		$("#duct-count-overlay").on("click", function (e) {
258			if (e.target === this) closeDuctModal();
259		});
260		$(".duct-modal-cancel").on("click", closeDuctModal);
261		function readModalQuantities(pricingModel) {
262			if (isPerVentPricing(pricingModel)) {
263				return { vents: parseInt($("#vent-count-input").val(), 10) };
264			}
265			return {
266				systems: parseInt($("#duct-count-input").val(), 10),
267				furnaces: parseInt($("#furnace-count-input").val(), 10)
268			};
269		}
270
271		function validateModalQuantities(q, pricingModel) {
272			if (isPerVentPricing(pricingModel)) {
273				if (isNaN(q.vents) || q.vents < 1) {
274					alert("Please enter a valid quantity of vents (1 or more).");
275					return false;
276				}
277				return true;
278			}
279			if (isNaN(q.systems) || q.systems < 1) {
280				alert("Please enter a valid number of systems (1 or more).");
281				return false;
282			}
283			if (isNaN(q.furnaces) || q.furnaces < 1) {
284				alert("Please enter a valid number of furnaces (1 or more).");
285				return false;
286			}
287			return true;
288		}
289
290		function addPendingModalToCartThenClose(goSchedule) {
291			if (!pendingDuctAdd) return;
292			var q = readModalQuantities(pendingDuctAdd.pricingModel);
293			if (!validateModalQuantities(q, pendingDuctAdd.pricingModel)) return;
294			var unitPrice = pendingDuctAdd.unitPrice;
295			var cartItemID = {
296				value: pendingDuctAdd.value,
297				name: pendingDuctAdd.name,
298				price: unitPrice,
299				sqft: 0,
300				pricingModel: pendingDuctAdd.pricingModel
301			};
302			var total;
303			if (isPerVentPricing(pendingDuctAdd.pricingModel)) {
304				cartItemID.qty = q.vents;
305				total = unitPrice * q.vents;
306			} else {
307				cartItemID.qty = q.systems;
308				cartItemID.furnaceQty = q.furnaces;
309				cartItemID.furnaceExtraUsd = furnaceExtraUsd();
310				total = unitPrice * q.systems + Math.max(0, q.furnaces - 1) * furnaceExtraUsd();
311			}
312			BookingsCommon.updateCart(cartItemID, true, { itemPrice: unitPrice, itemTotal: total });
313			closeDuctModal();
314			if (goSchedule) window.location.href = "/request/schedule";
315		}
316
317		$(".duct-modal-submit").on("click", function () {
318			addPendingModalToCartThenClose(false);
319		});
320		$(".duct-modal-select-date").on("click", function () {
321			addPendingModalToCartThenClose(true);
322		});
323		$("#vent-count-input, #duct-count-input, #furnace-count-input").on("input change keyup", updateDuctTotal);
324		$("#duct-count-overlay").on("keydown", "#vent-count-input, #duct-count-input, #furnace-count-input", function (e) {
325			if (e.which === 13) $(".duct-modal-submit").click();
326			if (e.which === 27) closeDuctModal();
327		});
328
329		// Card click opens quantity modal (per-system or per-vent)
330		$(".services-title").on("click", function () {
331			var $item = $(this).closest(".views-view-responsive-grid__item");
332			var $btn = $item.find(".add-to-cart");
333			var priceStr = $btn.attr("price") || "";
334			var unitPrice = parseFloat(priceStr.replace(/[^0-9.]/g, "")) || 0;
335			if (!unitPrice) return;
336			var pricingModel = getPricingModelForItem($item);
337			openDuctModal(unitPrice, $btn.val(), $btn.attr("item"), pricingModel, getAdditionalFurnacePriceForItem($item));
338		});
339
340		$('.views-field-name .field-content a').contents().unwrap();
341		$(".service-request-type-id").hide();
342
343		var cartItemID = {};
344		var itemPrice = 0, itemTotal = 0, itemSqft = 0;
345
346		/** Use common pricing then add item via BookingsCommon.updateCart. */
347		function submitAddToCart(squareFootage, system_requests, service) {
348			var results = BookingsCommon.totalCalculation(system_requests, squareFootage, service);
349			if (results && results.pass) {
350				cartItemID.sqft = squareFootage;
351				BookingsCommon.updateCart(cartItemID, true, { itemPrice: results.itemPrice, itemTotal: results.itemTotal, itemSqft: results.itemSqft });
352			}
353		}
354
355		$('.add-to-cart').each(function (index) {
356			var system_requests = $("#bookingSystems").length ? parseFloat($("#bookingSystems")[0].value) : 1;
357			if (index === 5) {
358				$(this).on("click", function (e) {
359					e.preventDefault();
360					cartItemID.value = $(this).val();
361					cartItemID.name = $(this).attr('item');
362					cartItemID.price = $(this).attr('price');
363					cartItemID.qty = system_requests;
364					submitAddToCart($("#bookingSqft")[0].value, system_requests, 0);
365				});
366			} else if (index === 0) {
367				$(this).on("click", function (e) {
368					e.preventDefault();
369					system_requests = $("#bookingSystems").length ? parseFloat($("#bookingSystems")[0].value) : 1;
370					cartItemID.value = $(this).val();
371					cartItemID.name = $(this).attr('item');
372					cartItemID.price = $(this).attr('price');
373					cartItemID.qty = system_requests;
374					submitAddToCart($("#bookingSqft")[0].value, system_requests, index + 1);
375				});
376			} else if (index === 1 || index === 2) {
377				$(this).on("click", function (e) {
378					e.preventDefault();
379					system_requests = $("#bookingSystems").length ? parseFloat($("#bookingSystems")[0].value) : 1;
380					cartItemID.value = $(this).val();
381					cartItemID.name = $(this).attr('item');
382					cartItemID.price = $(this).attr('price');
383					cartItemID.qty = system_requests;
384					submitAddToCart($("#bookingSqft")[0].value, system_requests, index + 1);
385				});
386			} else {
387				$(this).on("click", function (e) {
388					e.preventDefault();
389					system_requests = $("#bookingSystems").length ? parseFloat($("#bookingSystems")[0].value) : 1;
390					cartItemID.value = $(this).val();
391					cartItemID.name = $(this).attr('item');
392					cartItemID.price = parseFloat($(this).attr('price').replace("$", "")) || 0;
393					cartItemID.qty = system_requests;
394					cartItemID.sqft = 0;
395					var total = cartItemID.price * system_requests;
396					BookingsCommon.updateCart(cartItemID, true, { itemPrice: cartItemID.price, itemTotal: total });
397				});
398			}
399		});
400
401		// Grid layout; 3-col if more than 2 products
402		$('.views-view-responsive-grid').addClass('row');
403		$('.views-view-responsive-grid .views-view-responsive-grid__item').addClass('col-md-6');
404		$(".views-view-responsive-grid--horizontal").css("gap", "0px");
405		var requestGridCount = $(".request-service-grid .views-view-responsive-grid__item").length;
406		if (requestGridCount > 2) {
407			$(".left-content-holder").remove();
408			$(".main-content-holder").removeClass("col-md-6").addClass("col-md-9");
409			$(".request-service-grid .views-view-responsive-grid__item").removeClass("col-md-6").addClass("col-md-4");
410		}
411
412		// Preselect checkboxes from session when returning with clear_cart
413		var selectedItems = BookingsCommon.getSelectedItems();
414		if (sessionStorage.getItem('clear_cart')) {
415			selectedItems = BookingsCommon.getSelectedItems();
416			if (selectedItems.length > 0) {
417				$.each(selectedItems, function (i, item) {
418					var $checkbox = $('.service-requested-checkbox').filter(function () { return $(this).val().trim() === item.id; });
419					if ($checkbox.length) $checkbox.prop('checked', true);
420				});
421			}
422			$("#edit-field-service-requested-197").prop("checked", true);
423		}
424
425		$('.service-requested-checkbox').on('change', function () {
426			var val = $(this).val();
427			if (this.checked) {
428				var $card = $('.add-to-cart[value="' + val + '"]').closest('.views-view-responsive-grid__item');
429				var name = $card.length ? $card.find('.add-to-cart').attr('item') || 'Item' : 'Item';
430				var priceStr = $card.length ? $card.find('.add-to-cart').attr('price') || '0' : '0';
431				var price = parseFloat(priceStr.replace(/[^0-9.]/g, '')) || 0;
432				var qty = $("#bookingSystems").length ? parseInt($("#bookingSystems")[0].value, 10) || 1 : 1;
433				BookingsCommon.updateCart({ value: val, name: name, price: price, qty: qty, sqft: 0 }, true, { itemPrice: price, itemTotal: price * qty });
434			} else {
435				BookingsCommon.updateCart({ value: val }, false);
436			}
437		});
438
439		// Proceed to calendar or show "select at least one" modal
440		$(".button-positive").on("click", function () {
441			var items = BookingsCommon.getSelectedItems();
442			if (!items || items.length === 0) {
443				$("#popup-body").html('<p class="error-modal-message" style="margin:0 0 1rem 0; color: #333;">Please select at least one service before proceeding to the scheduler page.</p><button type="button" class="button-orange error-modal-ok">OK</button>');
444				$("#popup-overlay, #popup-container").fadeIn();
445				$("#popup-container .error-modal-ok").on("click", function () { $("#popup-overlay, #popup-container").fadeOut(); });
446			} else {
447				window.location.href = "/calendar-clear-cache";
448			}
449		});
450
451		// Clear cart and refresh display
452		$(".button-negative").on("click", function () {
453			sessionStorage.setItem('selectedItems', JSON.stringify([]));
454			BookingsCommon.loadCart();
455		});
456
457		// Recalculate and refresh cart
458		$(".calculate-rate").on("click", function () {
459			localStorage.setItem("squareFootage", 0);
460			localStorage.setItem("systemRequests", 0);
461			BookingsCommon.loadCart();
462		});
463
464		BookingsCommon.loadCart();
465		$("#bookingSqft, #bookingSystems").prop("readonly", false);
466
467		// Deep link: e.g. /request?itemIdEl=217 — open quantity modal for preselected package
468		(function openPackageFromItemIdElQuery() {
469			var params;
470			try {
471				params = new URLSearchParams(window.location.search);
472			} catch (e) {
473				return;
474			}
475			var rawId = params.get("itemIdEl");
476			if (rawId == null || String(rawId).trim() === "") {
477				return;
478			}
479			var itemId = String(rawId).trim();
480			var $btn = $(".request-service-grid .add-to-cart").filter(function () {
481				return String($(this).val()).trim() === itemId;
482			});
483			if (!$btn.length) {
484				return;
485			}
486			var $item = $btn.closest(".views-view-responsive-grid__item");
487			var $title = $item.find(".services-title").first();
488			if (!$title.length) {
489				return;
490			}
491			setTimeout(function () {
492				var el = $item.get(0);
493				if (el && typeof el.scrollIntoView === "function") {
494					el.scrollIntoView({ behavior: "smooth", block: "center" });
495				}
496				$title.trigger("click");
497			}, 0);
498		})();
499	}
500};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.