Choose YOUR EQUIPMENT

Then click REQUEST A QUOTE

RENTAL DAYS 0
RATE CARD $0
Clear Equipment
Clear Dates
Clear Search
Clear ALL

INSTRUCTIONS

• Choose Pickup and Return Dates

• Choose Equipment from Scrolling List

• Use Search for Categories and Equipment

• Click REQUEST A QUOTE button when ready!

.category-header { font-weight: bold; color: var(--eqs-accent-color); text-transform: uppercase; } #customDropdownMenu .sub-category > .category-header { padding-left: 20px; font-weight: normal; color: var(--eqs-accent-color); } #customDropdownMenu .third-level > .category-header { padding-left: 30px; font-style: normal; font-size: 13px; color: var(--eqs-accent-color); } #customDropdownMenu .submenu { margin: 0; padding: 0; } /* Add to your CSS for even less spacing */ /* Consolidated selector - appears only once now */ #customDropdownMenu ul { padding: 0; margin: 0; list-style: none; /* Added from another instance */ } /* This definition was consolidated above */ /* Merged into main close-btn-container definition */ #customDropdownMenu.visible { display: block !important; } /* ============================== Enhanced Search Popup Styling ============================== */ #directSearchResults .search-results-list { margin: 0; padding: 0; list-style: none; } /* Menu control buttons row with search options */ #directSearchResults .close-btn-container { display: flex; align-items: center; justify-content: flex-start; /* Changed from space-between to align like equipment categories */ padding: 4px 8px; background: #fff; position: sticky; top: 0; z-index: 1; border-bottom: 1px solid #eee; } /* Search toggle button to match menu toggle button */ #searchMenuToggleButton { width: 24px; height: 24px; border-radius: 50%; background: #f0f0f0; border: 1px solid #999; cursor: pointer; font-size: 16px; line-height: 16px; text-align: center; margin-right: 6px; display: flex; align-items: center; justify-content: center; transition: transform 0.2s ease; } /* Search options styling - moved to right side */ #directSearchResults .search-options { display: flex; align-items: center; font-size: 14px; /* Increased from 12px */ margin-left: auto; /* Push to right side */ font-weight: normal; } #directSearchResults .search-options label { display: flex; align-items: center; cursor: pointer; } #directSearchResults .search-options input[type="checkbox"] { width: 20px; /* Increased size */ height: 20px; /* Increased size */ margin-right: 6px; cursor: pointer; } /* Remove old menu control buttons styles */ #directSearchResults .menu-controls { display: none; /* Hide the old controls */ } /* Style for the search results item categories */ #directSearchResults .menu-item { position: relative; padding: 0; margin: 0; border-bottom: 1px solid #eee; } #directSearchResults .menu-item:last-child { border-bottom: none; } #directSearchResults .submenu { margin: 0; padding: 0; } #directSearchResults .category-header { display: flex; align-items: center; padding: 4px 0; /* Changed horizontal padding to 0 */ cursor: pointer; line-height: 1.2; } #directSearchResults .category-header:hover { background-color: #f0f8ff; } /* Item name styling */ #directSearchResults .item-name { font-size: 14px; font-weight: normal; } /* Toggle icon styling */ #directSearchResults .toggle-icon { display: flex; width: 30px; height: 30px; text-align: center; margin-right: 4px; font-size: 18px; user-select: none; cursor: pointer; align-items: center; justify-content: center; } #directSearchResults .toggle-icon:not(.has-children) { visibility: hidden; } /* Category level styling */ #directSearchResults .main-category > .category-header { font-weight: bold; color: var(--eqs-accent-color); text-transform: uppercase; } #directSearchResults .sub-category > .category-header { padding-left: 20px; font-weight: normal; color: var(--eqs-accent-color); } #directSearchResults .third-level > .category-header { padding-left: 30px; font-style: normal; font-size: 13px; color: var(--eqs-accent-color); } #directSearchResults .item-result > .category-header { padding-left: 40px; font-style: normal; font-size: 14px; } /* No results message */ #directSearchResults .no-results { padding: 12px; color: #666; font-style: italic; text-align: center; } /* ======================================== 5. EQUIPMENT LIST STYLES ======================================== */ /* Placeholder category (main cat) */ .equipment-category.placeholderCat { background: #fff !important; color: #000 !important; text-align: center !important; padding: 12px; border-top: 1px solid #444; } /* Normal category heading in red with darker gray background - BALANCED COMPRESSION */ .equipment-category { position: sticky; top: var(--sticky-header-height); z-index: 200; font-size: calc(var(--eqs-base-font-size) * 1.4); font-weight: bold; color: var(--eqs-accent-color); text-transform: uppercase; margin: 0; text-align: left; background: #c0c0c0; /* Darker gray background */ padding: 1px 6px; /* Added 1px top/bottom padding */ border-bottom: none; height: var(--equipment-category-height); /* Restored original height */ box-sizing: border-box; line-height: 1.0; margin-bottom: 0; /* Remove extra space */ } /* Subcategory styling - BALANCED COMPRESSION */ .equipment-subcategory { position: sticky; top: calc(var(--sticky-header-height) + var(--equipment-category-height)); z-index: 199; font-size: 14px !important; font-weight: normal; font-style: italic; color: var(--eqs-accent-color); margin: 0; text-align: left; padding: 4px 12px; /* Equal padding top/bottom */ background: lightgray; border-bottom: none; height: var(--equipment-subcategory-height); box-sizing: border-box; line-height: 1.0; margin-top: -1px; /* Pull it closer to mainCat */ display: flex; align-items: center; /* Center text vertically */ } /* Third level styling for equipment list - BALANCED COMPRESSION */ .equipment-thirdlevel { position: sticky; top: calc(var(--sticky-header-height) + var(--equipment-category-height) + var(--equipment-subcategory-height)); z-index: 198; font-size: 14px !important; font-weight: normal; font-style: italic; color: var(--eqs-accent-color); margin: 0; text-align: left; padding: 4px 24px; /* Updated padding to match subcategory style */ background: lightgray; border-bottom: none; box-sizing: border-box; line-height: 1.0; display: flex; align-items: center; /* Center text vertically */ } /* Items */ .equipment-item { margin: 0; padding: 8px; text-align: left; font-size: 14px; transition: background-color 0.2s ease-in-out; border-bottom: 1px solid #eee; } .equipment-item:last-child { border-bottom: none; } .equipment-item.selected { background-color: var(--eqs-selected-bg); } @media (hover: hover) and (pointer: fine) { .equipment-item:hover { background-color: #f9f9f9; } .equipment-item.selected:hover { background-color: var(--eqs-selected-bg); } } /* eqs-line1: Item name + Rate */ .eqs-line1 { display: flex; justify-content: space-between; align-items: center; margin: 0; padding: 0; line-height: 1.2; } .eqs-line1 .item-name, .eqs-line1 .item-dayrate { font-size: 18px !important; font-weight: bold; color: #000; } .item-subtotal { font-weight: bold; } /* eqs-line2: subrent, inhouse info, and quantity controls */ .eqs-line2 { display: flex; justify-content: flex-end; align-items: center; margin: 2px 0; padding: 0; line-height: 1.2; color: #555; } .subrent-text { margin-right: 8px; color: var(--eqs-accent-color); font-style: italic; } .inhouse-info { font-weight: normal; color: #000; font-style: italic; margin-right: 8px; } .quantity-controls { display: flex; align-items: center; margin-left: 8px; } /* Add space between the quantity and buttons */ .eqs-line2 .item-qty { margin: 0 5px; min-width: 20px; text-align: center; } /* Style for plus/minus buttons */ .eqs-line2 .minus-button, .eqs-line2 .plus-button { min-width: 24px; height: 24px; text-align: center; cursor: pointer; border: 1px solid #ddd; background: #f0f0f0; font-weight: bold; border-radius: 3px; } .eqs-line2 .plus-button:hover, .eqs-line2 .minus-button:not(:disabled):hover { background: #e0e0e0; } .eqs-line2 .minus-button:disabled { opacity: 0.5; cursor: default; } /* eqs-line3: Subtotal line styling */ .eqs-line3 { font-style: italic; margin: 2px 0; padding: 0; line-height: 1.2; text-align: right; color: #333; display: none; /* Hide the entire line */ } .eqs-line3 .item-subtotal { color: var(--eqs-accent-color); } /* eqs-line4 styles for backwards compatibility */ .eqs-line4 { display: none; /* Hide since we've moved the controls to eqs-line2 */ } /* Keep these styles for older code that might reference them */ .eqs-line4 .item-qty { margin: 0 5px; min-width: 20px; text-align: center; } .eqs-line4 .minus-button, .eqs-line4 .plus-button { min-width: 24px; height: 24px; text-align: center; cursor: pointer; border: 1px solid #ddd; background: #f0f0f0; font-weight: bold; border-radius: 3px; } .eqs-line4 .plus-button:hover, .eqs-line4 .minus-button:not(:disabled):hover { background: #e0e0e0; } .eqs-line4 .minus-button:disabled { opacity: 0.5; cursor: default; } /* Discount message styling - New Row 5 content */ .discount-message-container { display: flex; justify-content: flex-end; width: 100%; margin: 0; padding: 0; } .discount-message { font-size: 15px; color: var(--eqs-accent-color); background: none; border: none; padding: 0; margin: 0 2px; line-height: 1; font-weight: bold; } /* eqs-line5: Additional item description styling */ .eqs-line5 { margin: 2px 0 1px 0; padding: 0; line-height: 1.2; font-style: italic; color: #333; } /* Discount message styling - New Row 5 content */ .discount-message-container { display: flex; justify-content: flex-end; width: 100%; margin: 0; padding: 0; } .discount-message { font-size: 15px; color: var(--eqs-accent-color); background: none; border: none; padding: 0; margin: 0 2px; line-height: 1; font-weight: bold; } /* Specific styling for eqs-header-row.line5 */ .eqs-header-row.line5 { margin-top: 0; padding-top: 0; grid-template-columns: 1fr; } .option-label { font-weight: bold; color: #000; } .item-description { font-style: italic; } .item-description a { font-weight: bold; color: blue; text-decoration: underline; } .eqs-line5 .item-description div { margin-bottom: 2px; } /* Reduce spacing between multiple descriptions */ .item-description br { line-height: 0.8; margin: 0; content: ""; display: block; margin-bottom: 2px; } /* ======================================== 6. FORM & DIALOG STYLES ======================================== */ /* Start Over Confirmation Dialog */ #startOverDialog { display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #fff; border: 2px solid #000; padding: 20px; border-radius: 6px; box-shadow: 0 4px 8px rgba(0,0,0,0.3); z-index: 3000; text-align: left; width: 400px; max-width: 80%; } .dialog-line { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; font-size: 16px; font-weight: bold; } .dialog-line button { background: var(--eqs-primary-color); border: 1px solid var(--eqs-primary-border); border-radius: 2px; cursor: pointer; font-weight: bold; padding: 6px 12px; } .dialog-line button:hover { background: #fdd; } /* QuoteHelpPage Modal */ #QuoteHelpPage { display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #fff; border: 2px solid #000; padding: 20px; border-radius: 6px; box-shadow: 0 4px 8px rgba(0,0,0,0.3); z-index: 3000; text-align: left; width: 400px; max-width: 80%; } #QuoteHelpPage p { margin: 5px 0; font-size: 16px; font-weight: bold; line-height: 1.4; } #QuoteHelpPage button { display: block; margin: 10px auto 0; padding: 6px 12px; font-size: 14px; font-weight: bold; border: 1px solid var(--eqs-primary-border); border-radius: 2px; background: var(--eqs-primary-color); cursor: pointer; } #QuoteHelpPage button:hover { background: #fdd; } /* Quote form container */ #quoteFormContainer { display: none; position: fixed; top: 5%; left: 50%; transform: translateX(-50%); width: 90%; max-width: 800px; max-height: 90vh; overflow-y: auto; background: #fff; padding: 10px; border-radius: 4px; box-shadow: 0 4px 12px rgba(0,0,0,0.2); z-index: 4000; } .quote-form button { width: 100%; font-size: var(--eqs-header-font-size); padding: 4px 6px; text-align: center; border: 1px solid transparent; border-radius: 6px; cursor: pointer; text-transform: uppercase; color: #000; margin: 8px 0; box-shadow: 0 1px 2px rgba(0,0,0,0.1); height: 36px; box-sizing: border-box; transition: transform 0.15s ease-in-out; background: var(--eqs-highlight-color); } .quote-form button[type="submit"] { background: var(--eqs-primary-color); border: 1px solid transparent; } .quote-form button:hover { transform: scale(1.02); } .quote-form h1 { color: var(--eqs-accent-color); font-size: 20px; margin: 0 0 8px; font-weight: bold; text-align: center; } .quote-form h2 { color: #000; font-size: 14px; margin: 0 0 8px; font-weight: bold; text-align: center; } .quote-form h3 { margin: 0 0 8px; font-size: calc(var(--eqs-base-font-size) * 1.5); font-weight: bold; color: var(--eqs-accent-color); } .quote-form div { margin-bottom: 6px; } .quote-form label { display: block; margin-bottom: 2px; font-size: calc(var(--eqs-base-font-size) * 1.1); color: #000; font-weight: bold; } .quote-form input, .quote-form textarea, .quote-form button { width: 100%; padding: 6px; margin: 2px 0 8px; border: 1px solid #ddd; border-radius: 2px; font-size: calc(var(--eqs-base-font-size) * 1.0); box-sizing: border-box; font-weight: bold; color: #000; } .quote-form input[required]::placeholder { color: var(--eqs-accent-color); } .quote-form textarea { max-height: 200px; overflow-y: auto; resize: none; } /* Form top buttons styling */ .form-top-buttons { display: flex; justify-content: space-between; margin-bottom: 15px; } .form-top-buttons button { flex: 1; margin: 0 5px; background: var(--eqs-highlight-color); border: 1px solid transparent; border-radius: 6px; height: 36px; } .form-top-buttons button:first-child { margin-left: 0; } .form-top-buttons button:last-child { margin-right: 0; background: var(--eqs-primary-color); border: 1px solid transparent; } /* Form bottom buttons styling - mirror of top buttons */ .form-bottom-buttons { display: flex; justify-content: space-between; margin-top: 15px; } .form-bottom-buttons button { flex: 1; margin: 0 5px; background: var(--eqs-highlight-color); border: 1px solid transparent; border-radius: 6px; height: 36px; } .form-bottom-buttons button:first-child { margin-left: 0; } .form-bottom-buttons button:last-child { margin-right: 0; background: var(--eqs-primary-color); border: 1px solid transparent; } /* Thank you modal overlay styling */ .thank-you-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 4001; display: flex; justify-content: center; align-items: center; } .thank-you-message { background: #fff; border-radius: 6px; padding: 20px; max-width: 500px; width: 90%; box-sizing: border-box; text-align: center; } .thank-you-message h3 { margin: 0 0 8px; font-size: calc(var(--eqs-base-font-size) * 1.5); font-weight: bold; color: var(--eqs-accent-color); } .thank-you-message p { margin: 0 0 8px; } .thank-you-message .modal-buttons { display: flex; justify-content: center; gap: 20px; margin-top: 15px; } .thank-you-message .modal-buttons button { width: 100px; padding: 6px 12px; font-size: 14px; font-weight: bold; border: 1px solid transparent; border-radius: 6px; background: var(--eqs-highlight-color); /* Blue background */ color: #000; cursor: pointer; text-transform: uppercase; } .thank-you-message .modal-buttons button:hover { transform: scale(1.05); transition: transform 0.15s ease-in-out; } /* ======================================== 7. MEDIA QUERIES - DESKTOP (min-width: 501px) ======================================== */ /* ======================================== DESKTOP STYLES - Override mobile & base styles These styles apply ONLY to screens 501px and wider ======================================== */ @media only screen and (min-width: 501px) { /* Global font settings for desktop buttons */ .eqs-date-input, #instructionsButton, #customDropdownButton, #yourEquipmentButton, #requestQuote, #startOver { font-size: 12px; font-weight: normal; text-transform: uppercase; } /* Make sure date title and values have consistent font size */ .eqs-date-input .date-title, .eqs-date-input .date-value { font-size: 12px; font-weight: normal; } /* Special styling for display fields (Rate Card and Rental Days) */ #estimateButton .date-title, #estimateValue, #rentalDayButton .date-title, #rentalDayValue { font-size: 12px; font-weight: bold; } .eqs-header-row.line3 { grid-template-columns: 1fr; } #customDropdownButton { width: 140px; } .eqs-header-row.line2 { grid-template-columns: 1fr 1fr 120px; align-items: center; /* Center-align all items in the row */ } /* Ensure consistent heights and centered text for all elements in row 2 */ #pickupDateButton, #returnDateButton, #rentalDayButton { height: 36px; display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center; } /* Ensure date title and value are centered */ .eqs-date-input .date-title, .eqs-date-input .date-value { text-align: center; width: 100%; } .eqs-header-row.line4 { grid-template-columns: 1fr 1fr 120px; } .eqs-header-row.line5 { grid-template-columns: 1fr; margin-top: 0; padding-top: 0; } } /* ======================================== 8. MEDIA QUERIES - MOBILE & TABLET ======================================== */ /* Flatpickr positioning fixes for mobile - using specificity instead of !important */ @media screen and (max-width: 768px) { /* Using more specific selectors to increase specificity */ html body #stickyBlock .flatpickr-calendar, html body .flatpickr-calendar, body .flatpickr-calendar.open { width: 280px; left: 50%; right: auto; transform: translateX(-50%); position: fixed; top: 50%; margin-top: -140px; /* Half the height to center vertically */ animation: none; transition: none; } /* Ensure calendar days are properly sized */ html body .flatpickr-days { width: 280px; } html body .dayContainer { width: 280px; min-width: 280px; max-width: 280px; } #quoteHeader h1 { font-size: 36px; } #quoteHeader h2 { font-size: 14px; } .eqs-header-row { flex-direction: row; flex-wrap: nowrap; max-width: 400px; gap: 2px; } .date-col { min-width: 80px; margin-bottom: 4px; } .eqs-date-input { width: 100px; font-size: 10px; } #estimateButton, #rentalDayButton { width: 90px; } #equipmentSearchInput { width: 90%; font-size: 12px; height: 24px; } #equipmentSearchPopup .search-result { font-size: 12px; padding: 6px; } .equipment-item { padding: 6px; } .eqs-line1 .item-name, .eqs-line1 .item-dayrate { font-size: 16px !important; } /* Mobile-specific improvements for touch targets These make buttons and interactive elements larger for easier tapping on mobile */ #customDropdownMenu .toggle-icon { width: 44px; /* Larger for better touch targets on mobile */ height: 44px; /* Larger for better touch targets on mobile */ font-size: 22px; } #customDropdownMenu .category-header { padding: 8px; /* More padding for better touch targets */ } .menu-control-btn { width: 36px; /* Larger for better touch targets on mobile */ height: 36px; /* Larger for better touch targets on mobile */ } } /* ======================================== MOBILE STYLES These styles apply to screens 500px and narrower ======================================== */ @media only screen and (max-width: 500px) { .eqs-header-row { max-width: 98%; /* Increased from 95% to 98% to extend closer to edge */ } .eqs-header-row.line1 { max-width: 98%; /* Increased from 95% to 98% */ grid-template-columns: 1fr 1fr; /* Two buttons only */ width: calc(100% - 46px); /* Account for the third column space */ margin-right: auto; } .eqs-header-row.line1 button { width: 100%; } /* Make all buttons consistent width */ .eqs-date-input, #instructionsButton, #customDropdownButton, #yourEquipmentButton, #requestQuote, #startOver { font-size: var(--eqs-header-font-size); font-weight: normal; text-transform: uppercase; width: 100%; box-sizing: border-box; } /* Ensure text inside ALL buttons is consistent */ #instructionsButton .date-title, #instructionsButton .date-value, #startOver .date-title, #startOver .date-value, #yourEquipmentButton .date-title, #yourEquipmentButton .date-value, #requestQuote .date-title, #requestQuote .date-value, .eqs-date-input .date-title, .eqs-date-input .date-value { font-size: var(--eqs-header-font-size); /* Uses root variable for consistency */ font-weight: normal; text-align: center; } /* The following button date styles should override the above for specific cases */ /* Make pickup and return date buttons match other buttons */ #pickupDateButton, #returnDateButton { font-size: 9px; box-sizing: border-box; padding-left: 2px; padding-right: 2px; } #pickupDateButton .date-title, #returnDateButton .date-title { font-size: 9px; } #pickupDateButton .date-value, #returnDateButton .date-value { font-size: 9px; } /* Make search container full width */ .eqs-header-row.line3 { max-width: 100%; grid-template-columns: 1fr; padding: 0; margin-left: auto; margin-right: auto; width: 100%; } #equipmentSearchContainer { width: 100%; padding: 0; margin: 0; } #equipmentSearchInput { width: 100%; max-width: 100%; box-sizing: border-box; padding-left: 8px; padding-right: 8px; margin: 0; } /* Make all buttons in row 2 and 4 the same width */ .eqs-header-row.line2, .eqs-header-row.line4 { grid-template-columns: 1fr 1fr 0.8fr; } /* Row 5 layout for mobile */ .eqs-header-row.line5 { grid-template-columns: 1fr; margin-top: 0; padding-top: 0; } /* Make discount message text smaller on mobile */ .discount-message { font-size: 14px; } /* Ensure consistent button sizing between rows */ #pickupDateButton, #returnDateButton, #yourEquipmentButton, #requestQuote { box-sizing: border-box; width: 100%; padding-left: 2px; padding-right: 2px; } #customDropdownButton { width: 90px; font-size: 10px; padding: 2px; } #equipmentCategoriesContainer { width: 90px; } #equipmentSearchInput { height: 36px; max-width: 100%; } .eqs-date-input .date-title { font-size: 10px; font-weight: bold; } .eqs-date-input .date-value { font-size: 11px; font-weight: bold; } /* Special styling for Rental Day display field */ #rentalDayButton { display: flex; flex-direction: column; justify-content: center; align-items: center; padding: 4px 0; height: 36px; /* Match the height of date buttons */ box-sizing: border-box; } #rentalDayButton .date-title, #rentalDayValue { font-size: 11px; font-weight: 800; /* Extra bold */ margin: 0; line-height: 1.3; } /* Special styling for Rate Card display field */ #estimateButton { display: flex; flex-direction: column; justify-content: center; align-items: center; padding: 4px 0; } #estimateButton .date-title, #estimateValue { font-size: 11px; font-weight: 800; /* Extra bold */ margin: 0; line-height: 1.3; } } /** * equipment-quote.js * Version 4.2.0 * * This file handles all the functionality for the equipment quote system, * including data loading, event handlers, and calculations. */ jQuery(document).ready(function ($) { /** * ================================================== * CONSTANTS AND GLOBAL VARIABLES * ================================================== */ const SHEET_URL = 'https://docs.google.com/spreadsheets/d/e/2PACX-1vRz3pANX48l9yT7SItc7wjyk003dOWXFGiS7w_qBFON_VJT_Mj-HQ1qYFamQKyv5rNzLw4zdcBL3ubv/pub?output=csv'; let equipmentData = []; // Stores all equipment data from the Google sheet let itemCounts = {}; // Stores the count of each selected item let submissionCount = 0; // Tracks form submissions (max 3) let searchInDescriptions = true; // New variable to track whether to search in descriptions let searchMenuExpanded = false; // Track whether search menu is expanded let showOnlySelected = false; // Tracks whether to show only selected items // Function to close all popups function closeAllPopups() { $('#equipmentSearchPopup').hide(); $('#directSearchResults').hide(); $('#customDropdownMenu').hide(); $('#QuoteHelpPage').hide(); $('#startOverDialog').hide(); } // Helper functions for modal interaction window.modalRevise = function() { var modal = document.getElementById('thankYouModal'); if (modal) modal.remove(); }; window.modalEmail = function(link) { var modal = document.getElementById('thankYouModal'); var form = document.getElementById('quoteFormContainer'); if (modal) modal.remove(); if (form) form.style.display = 'none'; setTimeout(function() { window.location.href = link; }, 500); }; /** * ================================================== * UTILITY FUNCTIONS * ================================================== */ /** * Sanitize a string for safe use in HTML attributes * @param {string} str - String to sanitize * @return {string} Sanitized string */ function sanitizeAttribute(str) { if (typeof str !== 'string') return ''; return str.replace(/[&<>"'`=\/]/g, function(s) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', '`': '`', '=': '=', '/': '/' }[s]; }); } /** * Create a safe ID string by removing special characters and replacing spaces with hyphens * @param {string} str - String to sanitize for use as an ID * @return {string} Safe ID string */ function createSafeId(str) { if (typeof str !== 'string') return ''; // Remove all special characters and replace spaces with hyphens return str.replace(/[&<>"'`=\/]/g, '').replace(/\s+/g, '-').toLowerCase(); } /** /** /** /** /** * Enhanced scroll function that correctly positions item content after scrolling * @param {jQuery} $target - jQuery element to scroll to * @param {number} additionalOffset - Additional offset beyond sticky header (default: 0) * @param {number} animationTime - Duration of scroll animation in ms (default: 550) */ function scrollToElement($target, additionalOffset = 0, animationTime = 550) { if ($target && $target.length) { // Get sticky header height - this is the main toolbar area const stickyHeight = $('#stickyBlock').outerHeight() || 150; // Get heights from CSS variables for consistent calculations const mainCatHeight = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--equipment-category-height')) || 35; const subCatHeight = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--equipment-subcategory-height')) || 25; const thirdLevelHeight = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--equipment-subcategory-height')) || 25; // Determine what type of element we're scrolling to const isMainCategory = $target.hasClass('equipment-category'); const isSubCategory = $target.hasClass('equipment-subcategory'); const isThirdLevel = $target.hasClass('equipment-thirdlevel'); const isItem = $target.hasClass('equipment-item'); // Get the target's position let targetPosition = $target.offset().top; // Temporarily unstick the target element if it's a sticky header if (isMainCategory || isSubCategory || isThirdLevel) { const originalPosition = $target.css('position'); $target.css('position', 'static'); targetPosition = $target.offset().top; $target.css('position', originalPosition); } // Calculate the offset based on element type let totalHeaderOffset = stickyHeight; // Start with main sticky header // SIMPLER APPROACH: Directly use the data-level-type for items // or element class for headings, without the DOM traversal if (isItem) { const levelType = $target.data('level-type'); if (levelType === "mainCat") { // Item is directly under main category totalHeaderOffset += mainCatHeight; } else if (levelType === "subCat") { // Item is under subcategory totalHeaderOffset += mainCatHeight + subCatHeight; } else if (levelType === "subCat3") { // Item is under third level totalHeaderOffset += mainCatHeight + subCatHeight + thirdLevelHeight; } // For items, we want to see a bit LESS of the header above // Reduce additional offset to show less of the heading additionalOffset = -5; // Negative value moves the target UP in the viewport } else if (isMainCategory) { // For main categories, we want them perfectly at the top additionalOffset = 0; } else if (isSubCategory) { // Main category header above subcategory totalHeaderOffset += mainCatHeight; additionalOffset = 0; } else if (isThirdLevel) { // Main category and subcategory headers above third level totalHeaderOffset += mainCatHeight + subCatHeight; additionalOffset = 0; } // Calculate final scroll position const scrollTarget = targetPosition - totalHeaderOffset - additionalOffset; console.log("Scrolling to:", $target.prop('tagName'), "Type:", isMainCategory ? "MainCat" : isSubCategory ? "SubCat" : isThirdLevel ? "ThirdLevel" : "Item", "Level Type:", $target.data('level-type'), "Total Header Offset:", totalHeaderOffset, "Additional Offset:", additionalOffset, "Target Position:", targetPosition, "Scroll Target:", scrollTarget); // Perform the scroll with the adjusted position window.scrollTo({ top: scrollTarget, behavior: 'smooth' }); // Fallback for older browsers setTimeout(function() { if (Math.abs($(window).scrollTop() - scrollTarget) > 5) { $('html, body').animate({ scrollTop: scrollTarget }, animationTime); } }, 100); } else { console.warn("scrollToElement: Target element not found"); } } // Handle scrolling on body - modified to not lock scrolling function lockBodyScroll() { // Function kept for compatibility but no longer adds the no-scroll class console.log("Body scroll lock disabled"); } function unlockBodyScroll() { // Function kept for compatibility but no longer does anything console.log("Body scroll unlock disabled"); } // Format numbers with commas function formatWithCommas(num) { return num.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 0 }); } // Weekly rate quote link $(document).on('click', '#weeklyRateLink', function (e) { e.stopPropagation(); e.preventDefault(); handleRequestQuoteClick(); }); /** /** * Function to update the sticky header positions dynamically * This ensures they stack correctly when scrolling in any direction */ function updateStickyHeaderOffset() { // Get the main sticky header height const stickyHeight = $('#stickyBlock').outerHeight() || 150; // Update the CSS variable for sticky header height document.documentElement.style.setProperty('--sticky-header-height', stickyHeight + 'px'); // Get a sample of each header type to measure actual heights const $sampleMainCat = $('.equipment-category').first(); const $sampleSubCat = $('.equipment-subcategory').first(); if ($sampleMainCat.length) { const mainCatHeight = $sampleMainCat.outerHeight() || 35; document.documentElement.style.setProperty('--equipment-category-height', mainCatHeight + 'px'); } if ($sampleSubCat.length) { const subCatHeight = $sampleSubCat.outerHeight() || 25; document.documentElement.style.setProperty('--equipment-subcategory-height', subCatHeight + 'px'); } console.log("Sticky headers updated - Main height:", stickyHeight, "px"); } // Auto-resize textarea fields window.autoResizeTextarea = function (el) { el.style.height = 'auto'; const maxHeight = 200; const newHeight = Math.min(el.scrollHeight, maxHeight); el.style.height = newHeight + 'px'; }; // Format phone numbers function formatInternational(number) { let clean = '+' + number.slice(1).replace(/\D/g, ''); if (clean.startsWith('+44') && clean.length === 13) { return ( clean.slice(0, 3) + ' ' + clean.slice(3, 5) + ' ' + clean.slice(5, 9) + ' ' + clean.slice(9) ); } else { let countryMatch = clean.match(/^\+\d{1,3}/); if (countryMatch) { let country = countryMatch[0]; let rest = clean.slice(country.length); let groups = rest.match(/\d{1,3}/g); return country + ' ' + groups.join(' '); } return clean; } } // Debounce function for search function debounce(func, delay) { let timer; return function(...args) { clearTimeout(timer); timer = setTimeout(() => func.apply(this, args), delay); }; } /** * Combines items with identical names, summing their quantities * and merging their descriptions if they differ * @param {Array} items - Array of item objects to process * @returns {Array} - New array with combined items */ function combineIdenticalItems(items) { if (!items || !items.length) return []; const combinedItems = {}; items.forEach(item => { const itemName = item.item; // Convert qtyOwned to a number first (it might be a string from CSV) const numQtyOwned = parseInt(item.qtyOwned, 10) || 0; if (!combinedItems[itemName]) { // First occurrence of this item combinedItems[itemName] = { ...item, qtyOwned: numQtyOwned, // Ensure qtyOwned is a number originalDescriptions: [item.description], uniqueDescriptions: new Set([item.description]) }; } else { // Found a duplicate - combine it const existingItem = combinedItems[itemName]; // Sum the quantities (ensuring both are numbers) existingItem.qtyOwned = (existingItem.qtyOwned || 0) + numQtyOwned; // Add description if it's different if (item.description && !existingItem.uniqueDescriptions.has(item.description)) { existingItem.originalDescriptions.push(item.description); existingItem.uniqueDescriptions.add(item.description); } } }); // Process all combined items to create final descriptions Object.values(combinedItems).forEach(item => { // Remove empty descriptions const validDescriptions = item.originalDescriptions.filter(d => d && d.trim() !== ''); // Join unique descriptions with line breaks if there are multiple if (validDescriptions.length > 1) { item.description = validDescriptions.join('
'); } // Clean up temporary properties delete item.originalDescriptions; delete item.uniqueDescriptions; }); return Object.values(combinedItems); } /** * ================================================== * DATE FUNCTIONS * ================================================== */ // Get days until pickup date function daysUntilPickup() { const pickupDate = pickupPicker.selectedDates[0]; if (!pickupDate) return 0; const today = new Date(); today.setHours(0, 0, 0, 0); pickupDate.setHours(0, 0, 0, 0); return Math.ceil((pickupDate - today) / (1000 * 60 * 60 * 24)); } // Format date for email subject function pickupSubjectPrefix(daysUntil) { if (daysUntil === 0) return 'PU TODAY'; if (daysUntil === 1) return 'PU TOMORROW'; return 'PU in ' + daysUntil + ' days'; } /** * Format date with various patterns * @param {Date} dateObj - Date object to format * @param {string} format - Format pattern: 'subject', 'mmddyyyy', 'withDay', etc. * @return {string} Formatted date string */ function formatDate(dateObj, format) { if (!dateObj || !(dateObj instanceof Date) || isNaN(dateObj.getTime())) { return 'Not selected'; } const month = dateObj.getMonth() + 1; const day = dateObj.getDate(); const year = dateObj.getFullYear(); switch (format) { case 'subject': const weekdayShort = dateObj .toLocaleDateString(undefined, { weekday: 'short' }) .replace(/,/g, '') .toUpperCase(); return `${weekdayShort} ${month}/${day}/${year}`; case 'mmddyyyy': return `${month}/${day}/${year}`; case 'withDay': const opts = { weekday: 'long', month: 'numeric', day: 'numeric', year: 'numeric' }; return dateObj.toLocaleDateString(undefined, opts).replace(/,/g, ''); default: return dateObj.toLocaleDateString(); } } // Format dates for display on buttons function formatPickupDate(date) { const day = date.getDate(); const month = date.toLocaleDateString(undefined, { month: 'short' }).toUpperCase(); const weekday = date.toLocaleDateString(undefined, { weekday: 'short' }).toUpperCase(); const year = date.getFullYear(); return { line1: 'PICKUP', line2: `${weekday} ${month} ${day} ${year}` }; } function formatReturnDate(date) { const day = date.getDate(); const month = date.toLocaleDateString(undefined, { month: 'short' }).toUpperCase(); const weekday = date.toLocaleDateString(undefined, { weekday: 'short' }).toUpperCase(); const year = date.getFullYear(); return { line1: 'RETURN', line2: `${weekday} ${month} ${day} ${year}` }; } // Calculate rental days function calculateRentalDays() { const pd = pickupPicker.selectedDates[0]; const rd = returnPicker.selectedDates[0]; // Debug info console.log(`Calculating rental days: pickup=${pd}, return=${rd}`); // Both dates must be properly set if (!pd || !rd || !(pd instanceof Date) || !(rd instanceof Date)) { console.log("Returning 0 days: invalid date objects"); return 0; } // Additional validation to ensure dates are valid if (isNaN(pd.getTime()) || isNaN(rd.getTime())) { console.log("Returning 0 days: invalid date timestamps"); return 0; } const msPerDay = 1000 * 60 * 60 * 24; // Calculate days between dates (subtract 1 as requested) let diff = Math.ceil((rd - pd) / msPerDay) - 1; const result = diff < 1 ? 1 : diff; console.log(`Rental day calculation: ${pd.toDateString()} to ${rd.toDateString()} = ${result} days`); return result; } function updateDates() { // Check if both dates are actually set const pd = pickupPicker.selectedDates[0]; const rd = returnPicker.selectedDates[0]; // Reset pickup display if not set if (!pd) { $('#pickupDateButton .date-title').text('CHOOSE'); $('#pickupDateButton .date-value').text('PICKUP DATE'); } // Reset return display if not set if (!rd) { $('#returnDateButton .date-title').text('CHOOSE'); $('#returnDateButton .date-value').text('RETURN DATE'); } const d = calculateRentalDays(); $('#rentalDayTitle').text(d === 1 ? 'RENTAL DAY' : 'RENTAL DAYS'); $('#rentalDayValue').text(d); updateAllItemDisplays(); // Add this line to update all items' subtotals when dates change updateCalculations(); updateButtonColors(); // Update date button colors based on selection } /** * ================================================== * DATA LOADING AND RENDERING * ================================================== */ /** * Filter equipment list to show all items or only selected items * Based on the showOnlySelected toggle state */ function filterEquipmentList() { // Clear any existing message $('#noSelectedItemsMessage').remove(); if (showOnlySelected) { // Filtering mode: hide unselected items and empty categories let hasSelectedItems = false; // Keep track of which categories have at least one selected item // We'll need to track subcategory to main category relationships let visibleCategories = new Set(); let visibleSubcategories = new Set(); let visibleThirdLevels = new Set(); // Create maps to track hierarchical relationships let subcategoryToMainCategory = {}; let thirdLevelToSubcategory = {}; // Build relationship maps first $('.equipment-subcategory').each(function() { const $subcategory = $(this); const subcategoryId = $subcategory.attr('id'); // Find the nearest preceding main category let $mainCategory = $subcategory.prevAll('.equipment-category').first(); if ($mainCategory.length) { subcategoryToMainCategory[subcategoryId] = $mainCategory.attr('id'); } }); $('.equipment-thirdlevel').each(function() { const $thirdLevel = $(this); const thirdLevelId = $thirdLevel.attr('id'); // Find the nearest preceding subcategory let $subcategory = $thirdLevel.prevAll('.equipment-subcategory').first(); if ($subcategory.length) { thirdLevelToSubcategory[thirdLevelId] = $subcategory.attr('id'); } }); // First pass: mark items as visible/hidden and collect parent IDs $('.equipment-item').each(function() { const $item = $(this); const itemId = $item.data('item-id'); const count = itemCounts[itemId] || 0; const parentId = $item.data('parent-id'); const levelType = $item.data('level-type'); if (count > 0) { hasSelectedItems = true; $item.show(); // Mark all parents in the hierarchy as visible if (levelType === "mainCat") { // Main category item - just add the direct parent visibleCategories.add(parentId); } else if (levelType === "subCat") { // Subcategory item - add subcategory and its parent main category visibleSubcategories.add(parentId); // Add the parent main category if we know it if (subcategoryToMainCategory[parentId]) { visibleCategories.add(subcategoryToMainCategory[parentId]); } } else if (levelType === "subCat3") { // Third level item - need to handle the full hierarchy visibleThirdLevels.add(parentId); // Add the parent subcategory if we know it if (thirdLevelToSubcategory[parentId]) { const subCatId = thirdLevelToSubcategory[parentId]; visibleSubcategories.add(subCatId); // Add the grandparent main category if we know it if (subcategoryToMainCategory[subCatId]) { visibleCategories.add(subcategoryToMainCategory[subCatId]); } } } } else { $item.hide(); } }); // Handle case with no selected items if (!hasSelectedItems) { // Reset button text and toggle state $('#yourEquipmentButton .date-title').text('SEE ONLY'); $('#yourEquipmentButton .date-value').text('YOUR EQUIPMENT'); $('#yourEquipmentButton').removeClass('toggled'); // Switch view mode flag showOnlySelected = false; // Update button colors updateButtonColors(); // Show all equipment instead of just a message $('.equipment-category, .equipment-subcategory, .equipment-thirdlevel, .equipment-item').show(); // Remove any existing message $('#noSelectedItemsMessage').remove(); return; } // Show/hide based on our sets of visible elements $('.equipment-category').each(function() { const $category = $(this); const categoryId = $category.attr('id'); if (visibleCategories.has(categoryId)) { $category.show(); } else { $category.hide(); } }); $('.equipment-subcategory').each(function() { const $subcategory = $(this); const subcategoryId = $subcategory.attr('id'); if (visibleSubcategories.has(subcategoryId)) { $subcategory.show(); } else { $subcategory.hide(); } }); $('.equipment-thirdlevel').each(function() { const $thirdlevel = $(this); const thirdlevelId = $thirdlevel.attr('id'); if (visibleThirdLevels.has(thirdlevelId)) { $thirdlevel.show(); } else { $thirdlevel.hide(); } }); } else { // Show all mode: show everything $('.equipment-category, .equipment-subcategory, .equipment-thirdlevel, .equipment-item').show(); // Update button colors after filtering updateButtonColors(); } } // Load data from Google Sheets async function loadEquipmentData() { try { // Restore search menu expansion state try { searchMenuExpanded = localStorage.getItem('searchMenuExpanded') === 'true'; } catch (err) { searchMenuExpanded = false; } const response = await fetch(SHEET_URL); const csvText = await response.text(); Papa.parse(csvText, { header: true, dynamicTyping: false, complete: (results) => { // Log first few rows to inspect format console.log("Sample data rows:", results.data.slice(0, 5)); // Add debugging to check what's happening with showQuote values console.log("Checking showQuote values..."); const allShowQuoteValues = new Set(); results.data.forEach(r => { if (r.showQuote) { allShowQuoteValues.add(r.showQuote); } }); console.log("All showQuote values found:", Array.from(allShowQuoteValues)); // Filter rows that should be shown in the quote system const rows = results.data.filter(r => { // Skip rows without a main category if (!r.mainCat) return false; // Get showQuote value and trim whitespace if it's a string const showQuoteValue = typeof r.showQuote === 'string' ? r.showQuote.trim() : r.showQuote; // Log rows that might be problematic if (r.item && showQuoteValue && showQuoteValue !== "✓" && showQuoteValue.toLowerCase() !== "true" && showQuoteValue !== true && showQuoteValue !== "1" && showQuoteValue !== "✔" && showQuoteValue !== "☑") { console.log("Potentially problematic row:", r.item, "showQuote:", showQuoteValue); } // Return true if showQuote indicates this row should be included const isIncluded = ( // Check for checkmarks showQuoteValue === "✓" || showQuoteValue === "✔" || showQuoteValue === "☑" || // Check for text versions of true (case insensitive) (typeof showQuoteValue === 'string' && showQuoteValue.toLowerCase() === "true") || // Check for boolean true showQuoteValue === true || // Check for "1" showQuoteValue === "1" || showQuoteValue === 1 ); // Log items that are being excluded or included if (r.item && !isIncluded) { console.log("Excluding item:", r.item, "showQuote:", showQuoteValue); } else if (r.item && isIncluded) { console.log("Including item:", r.item, "showQuote:", showQuoteValue); } return isIncluded; }); // Create hierarchical data structure let categoryHierarchy = {}; // First pass: organize data into hierarchy rows.forEach(r => { const mainCat = (r.mainCat || '').trim(); const subCat = (r.subCat || '').trim(); const subCat3 = (r.subCat3 || '').trim(); const itemVal = (r.item || '').trim(); const dayRateVal = (r.dayRate || '').trim(); const qtyVal = (r.qtyOwned || '').trim(); const descVal = (r.description2 || '').trim(); // Skip empty rows if (!mainCat) return; // Create main category if it doesn't exist if (!categoryHierarchy[mainCat]) { categoryHierarchy[mainCat] = { id: createSafeId(mainCat), name: mainCat, subCategories: {}, items: [] }; } // Handle subcategory if present if (subCat) { if (!categoryHierarchy[mainCat].subCategories[subCat]) { categoryHierarchy[mainCat].subCategories[subCat] = { id: `${createSafeId(mainCat)}-${createSafeId(subCat)}`, name: subCat, thirdLevels: {}, items: [] }; } // Handle third level if present if (subCat3) { if (!categoryHierarchy[mainCat].subCategories[subCat].thirdLevels[subCat3]) { categoryHierarchy[mainCat].subCategories[subCat].thirdLevels[subCat3] = { id: `${createSafeId(mainCat)}-${createSafeId(subCat)}-${createSafeId(subCat3)}`, name: subCat3, items: [] }; } } } // Add the item if it exists if (itemVal) { const item = { item: itemVal, dayRateRaw: dayRateVal, dayRateNum: parseFloat(dayRateVal) || 0, qtyOwned: parseInt(qtyVal, 10) || 0, description: descVal }; // Add to the appropriate level if (subCat3 && categoryHierarchy[mainCat].subCategories[subCat] && categoryHierarchy[mainCat].subCategories[subCat].thirdLevels[subCat3]) { categoryHierarchy[mainCat].subCategories[subCat].thirdLevels[subCat3].items.push(item); } else if (subCat && categoryHierarchy[mainCat].subCategories[subCat]) { categoryHierarchy[mainCat].subCategories[subCat].items.push(item); } else { categoryHierarchy[mainCat].items.push(item); } } }); // Process each level to combine duplicate items before conversion // First handle main category level Object.values(categoryHierarchy).forEach(mainCat => { mainCat.items = combineIdenticalItems(mainCat.items); // Process subcategory level Object.values(mainCat.subCategories).forEach(subCat => { subCat.items = combineIdenticalItems(subCat.items); // Process third level Object.values(subCat.thirdLevels).forEach(thirdLevel => { thirdLevel.items = combineIdenticalItems(thirdLevel.items); }); }); }); // Convert hierarchy object to array format for easier iteration const categoriesArray = Object.values(categoryHierarchy).map(mainCat => { return { ...mainCat, subCategories: Object.values(mainCat.subCategories).map(subCat => { return { ...subCat, thirdLevels: Object.values(subCat.thirdLevels) }; }) }; }); equipmentData = categoriesArray; renderEquipmentList(); updateStickyHeaderOffset(); // Add this line populateCustomDropdownMenu(); initializeSearch(); restoreItemCounts(); // Add bottom buttons if ($('#bottomButtons').length === 0) { $('#equipmentList').after(`
`); // Event handlers for bottom buttons $('#returnToTop').on('click', function (e) { e.stopPropagation(); e.preventDefault(); $('html, body').animate({ scrollTop: 0 }, 500); }); $('#requestQuoteBottom').on('click', function (e) { e.stopPropagation(); e.preventDefault(); handleRequestQuoteClick(); }); } }, error: (err) => { console.error('Error parsing CSV:', err); } }); } catch (error) { console.error('Error fetching CSV:', error); } } // Render the equipment list function renderEquipmentList() { const $list = $('#equipmentList'); $list.empty(); // Iterate through main categories equipmentData.forEach(mainCat => { // Create a container block for this main category const $mainCatBlock = $('
'); // Add main category heading inside this block $mainCatBlock.append(`

${mainCat.name}

`); // Add items directly under the main category (if any) mainCat.items.forEach(item => { appendItemToList($mainCatBlock, item, mainCat.id, "mainCat"); }); // Process subcategories mainCat.subCategories.forEach(subCat => { // Add subcategory heading inside the same block $mainCatBlock.append(`

${subCat.name}

`); // Add items under this subcategory subCat.items.forEach(item => { appendItemToList($mainCatBlock, item, subCat.id, "subCat"); }); // Process third-level subcategories subCat.thirdLevels.forEach(thirdLevel => { if (thirdLevel.items.length > 0) { $mainCatBlock.append(`

${thirdLevel.name}

`); // Add items under this third level thirdLevel.items.forEach(item => { appendItemToList($mainCatBlock, item, thirdLevel.id, "subCat3"); }); } }); }); // Finally, append this entire mainCat block to #equipmentList $list.append($mainCatBlock); }); // Attach event handlers for each item - now inside renderEquipmentList function attachItemEventHandlers(); } // End of renderEquipmentList function // Create a separate function to attach event handlers function attachItemEventHandlers() { // Clear any existing event handlers to prevent duplicates $('.equipment-item').off('click'); $('.equipment-item .minus-button').off('click'); $('.equipment-item .plus-button').off('click'); // Attach event handlers for each item $('.equipment-item').each(function() { const $item = $(this); // Minus button click handler $item.find('.minus-button').on('click', function(e) { e.stopPropagation(); e.preventDefault(); changeItemCount($item, -1); }); // Plus button click handler $item.find('.plus-button').on('click', function(e) { e.stopPropagation(); e.preventDefault(); changeItemCount($item, +1); }); // Item click (entire area) $item.on('click', function(e) { if ( $(e.target).is('.minus-button, .plus-button') || $(e.target).closest('a').length > 0 ) { return; } e.stopPropagation(); e.preventDefault(); incrementItem($item); }); }); } // Helper function to append an item to the list function appendItemToList($list, item, parentId, levelType) { if (!item.item) return; // Skip if no item name // Ensure dayRateNum is a valid number const dayRateNum = parseFloat(item.dayRateNum) || 0; let dayRateLabel = dayRateNum === 0 ? '$0/day' : `$${item.dayRateRaw}/day`; let finalDescription = item.description || ''; // Ensure qtyOwned is a number const numQtyOwned = parseInt(item.qtyOwned, 10) || 0; // Create a new element with sanitized attributes const $newItem = $(`
${sanitizeAttribute(item.item)} ${dayRateLabel}
In-house: ${numQtyOwned}
0
${finalDescription}
`); // Append the new item to the list $list.append($newItem); // Make links in descriptions clickable - FIXED approach $newItem.find(".item-description a").on("click", function(e) { e.stopPropagation(); // Keep default link behavior }); } // Increment item count function incrementItem($item) { changeItemCount($item, +1); } /** * Toggle expansion state for a category * @param {jQuery} $icon - The toggle icon element */ function toggleCategoryExpansion($icon) { const $submenu = $icon.closest('.menu-item').find('> ul.submenu'); if ($icon.hasClass('collapsed')) { // Expand this submenu $icon.removeClass('collapsed').addClass('expanded'); $icon.text('▼'); // Downward arrow when expanded $submenu.slideDown(200); } else { // Collapse this submenu $icon.removeClass('expanded').addClass('collapsed'); $icon.text('▶'); // Right arrow when collapsed $submenu.slideUp(200); } } /** * Expand all categories within a specified container * @param {string} containerSelector - jQuery selector for the container */ function expandAllCategoriesInContainer(containerSelector) { const $icons = $(containerSelector + ' .toggle-icon.has-children.collapsed'); $icons.each(function() { const $icon = $(this); $icon.removeClass('collapsed').addClass('expanded'); $icon.text('▼'); // Downward arrow $icon.closest('.menu-item').find('> ul.submenu').slideDown(200); }); } /** * Collapse all categories within a specified container * @param {string} containerSelector - jQuery selector for the container */ function collapseAllCategoriesInContainer(containerSelector) { const $icons = $(containerSelector + ' .toggle-icon.has-children.expanded'); $icons.each(function() { const $icon = $(this); $icon.removeClass('expanded').addClass('collapsed'); $icon.text('▶'); // Right arrow $icon.closest('.menu-item').find('> ul.submenu').slideUp(200); }); } /** * Wrapper functions for backward compatibility */ function expandAllCategories() { expandAllCategoriesInContainer('#customDropdownMenu'); } function collapseAllCategories() { collapseAllCategoriesInContainer('#customDropdownMenu'); } function expandAllSearchResults() { expandAllCategoriesInContainer('#directSearchResults'); } function collapseAllSearchResults() { collapseAllCategoriesInContainer('#directSearchResults'); } // Populate the categories dropdown menu - FIXED IMPLEMENTATION function populateCustomDropdownMenu() { const $menuContainer = $('#customDropdownMenu'); const $menuUl = $menuContainer.find('ul'); $menuUl.empty(); // Update the close button container $menuContainer.find('.close-btn-container').html(` `); // Add menu items recursively for each main category equipmentData.forEach(mainCat => { // Create main category item with expand/collapse toggle $menuUl.append(` `); const $mainCatSubmenu = $menuUl.find(`li[data-cat-id="${mainCat.id}"] > ul.submenu`); // Add subcategories mainCat.subCategories.forEach(subCat => { $mainCatSubmenu.append(` `); const $subCatSubmenu = $mainCatSubmenu.find(`li[data-cat-id="${subCat.id}"] > ul.submenu`); // Add third level categories subCat.thirdLevels.forEach(thirdLevel => { $subCatSubmenu.append(` `); }); }); }); // Menu toggle button handler $('#menuToggleButton').off('click').on('click', function(e) { e.stopPropagation(); if ($(this).hasClass('expanded')) { $(this).removeClass('expanded').text('▶'); collapseAllCategories(); } else { $(this).addClass('expanded').text('▼'); expandAllCategories(); } }); // Toggle icon click handler $('#customDropdownMenu').off('click.toggleMenu').on('click.toggleMenu', '.toggle-icon.has-children', function(e) { e.stopPropagation(); toggleCategoryExpansion($(this)); }); // Handle clicks on category header (except toggle icons) $('#customDropdownMenu').off('click.selectCategory').on('click.selectCategory', '.category-header', function(e) { // Skip if clicking directly on the toggle icon if ($(e.target).hasClass('toggle-icon') || $(e.target).closest('.toggle-icon.has-children').length > 0) { return; } e.stopPropagation(); const $menuItem = $(this).closest('.menu-item'); const catId = $menuItem.data('cat-id'); const catName = $(this).find('.category-name').text().trim(); const parentCategory = $menuItem.closest('.submenu').closest('.menu-item').find('.category-name').first().text().trim(); console.log("Clicking on category:", catName); console.log("Under parent category:", parentCategory); console.log("Category ID:", catId); $('#customDropdownMenu').hide(); // Special handling for Camera, Lighting & Grip under VEHICLE if (catName === "Camera, Lighting & Grip" && parentCategory === "VEHICLE") { // First try to find the subcategory directly let $cameraTarget = $('.equipment-subcategory').filter(function() { return $(this).text().trim() === "Camera, Lighting & Grip"; }); // If not found, try to find any element containing this text under VEHICLE if (!$cameraTarget.length) { const $vehicleSection = $('.equipment-category:contains("VEHICLE")'); if ($vehicleSection.length) { $cameraTarget = $vehicleSection.nextUntil('.equipment-category').filter(':contains("Camera, Lighting & Grip")').first(); } } if ($cameraTarget.length) { console.log("Found Camera, Lighting & Grip section by text search"); scrollToElement($cameraTarget); return; } } // Special handling for Sprinter else if (catName === "Sprinter") { const $sprintTarget = $('.equipment-thirdlevel:contains("Sprinter")').first(); if ($sprintTarget.length) { console.log("Found Sprinter by text search"); scrollToElement($sprintTarget); return; } } // Default ID-based lookup const $target = $('#' + catId); console.log("Found target element by ID:", $target.length > 0); if ($target.length) { scrollToElement($target); } else if (catName.includes("Camera")) { // Keep existing fallback for Camera items const $cameraTarget = $('.equipment-subcategory:contains("Camera")'); if ($cameraTarget.length) { console.log("Found Camera section by text search"); scrollToElement($cameraTarget); } } else { console.log("No matching element found for", catId); } }); } /** * ================================================== * ITEM MANAGEMENT * ================================================== */ // Change item count (+/-) function changeItemCount($item, inc) { const itemId = $item.data('item-id'); const oldCount = itemCounts[itemId] || 0; let newCount = oldCount + inc; if (newCount < 0) newCount = 0; itemCounts[itemId] = newCount; saveItemCounts(); updateItemDisplay($item); updateCalculations(); // Check if this was the last selected item and it's now unselected let hasAnyItemsLeft = false; if (newCount === 0 && showOnlySelected) { // Check if there are any items still selected for (const id in itemCounts) { if (itemCounts[id] > 0) { hasAnyItemsLeft = true; break; } } // If no items left and we were in filtered view, switch to full view if (!hasAnyItemsLeft) { showOnlySelected = false; $('#yourEquipmentButton .date-title').text('SEE ONLY'); $('#yourEquipmentButton .date-value').text('YOUR EQUIPMENT'); $('#yourEquipmentButton').removeClass('toggled'); // Show all items $('.equipment-category, .equipment-subcategory, .equipment-thirdlevel, .equipment-item').show(); } } updateButtonColors(); // Apply filter if we're still in filtered view if (showOnlySelected) { filterEquipmentList(); } } // Update single item display function updateItemDisplay($item) { const itemId = $item.data('item-id'); // Always get the day rate from the data attribute and force it to be a number const dayRateNum = parseFloat($item.data('day-rate-num')); const actualDayRate = isNaN(dayRateNum) ? 0 : dayRateNum; // Get in-house quantity from data attribute const qtyOwned = parseInt($item.data('qty-owned'), 10) || 0; const count = itemCounts[itemId] || 0; const $qtySpan = $item.find('.item-qty'); $qtySpan.text(count); if (count > 0) { $qtySpan.addClass('count-active'); $item.addClass('selected'); } else { $qtySpan.removeClass('count-active'); $item.removeClass('selected'); } $item.find('.minus-button').prop('disabled', count <= 0); const rentalDays = calculateRentalDays(); // Calculate subtotal - ensure all values are proper numbers const subtotal = count * actualDayRate * rentalDays; const $subtotalSpan = $item.find('.item-subtotal'); // Debug logging console.log(`Item subtotal: ${itemId}, count=${count}, dayRate=${actualDayRate}, days=${rentalDays}, subtotal=${subtotal}`); if (count > 0) { $subtotalSpan.html(` Request a Quote: ${count} of these for ${rentalDays} day${rentalDays > 1 ? 's' : ''} $${formatWithCommas(subtotal)} `); $item.find('.eqs-line3').show(); } else { $subtotalSpan.text(''); $item.find('.eqs-line3').hide(); } // Only show sub-rent message if we need more than what's in-house const subRent = count - qtyOwned; const $subrentInfo = $item.find('.subrent-text'); if (subRent > 0) { $subrentInfo.text(`Will research sub-rent: ${subRent}`); $subrentInfo.css('color', 'red'); } else { $subrentInfo.text(''); } } // Update all items display function updateAllItemDisplays() { $('.equipment-item').each(function () { updateItemDisplay($(this)); }); } // Update totals function updateCalculations() { const rentalDays = calculateRentalDays(); let totalEstimate = 0; let itemsInTotal = 0; // Debug the rental days calculation console.log(`Calculating totals with ${rentalDays} rental days`); // Process each selected item Object.keys(itemCounts).forEach((id) => { const c = itemCounts[id] || 0; if (c > 0) { itemsInTotal++; const $i = $(`.equipment-item[data-item-id="${id}"]`); if ($i.length) { // Force the day rate to be a proper number const rawRate = $i.data('day-rate-num'); const rate = (typeof rawRate === 'number' && !isNaN(rawRate)) ? rawRate : 0; // Calculate item total const itemTotal = c * rate * rentalDays; totalEstimate += itemTotal; // Debug logging for each item console.log(`Total calculation: item=${id}, count=${c}, rate=${rate}, days=${rentalDays}, itemTotal=${itemTotal}`); } else { console.warn(`Item with ID ${id} not found in DOM but has count ${c}`); } } }); // Final debug log console.log(`Final total: ${totalEstimate} from ${itemsInTotal} items for ${rentalDays} days`); // Update the display with proper formatting $('#estimateValue').text(`$${formatWithCommas(totalEstimate)}`); // Update weekly rate value (currently same as total estimate) $('#weeklyRateAmount').text(formatWithCommas(totalEstimate)); } // Save/restore item selections function saveItemCounts() { localStorage.setItem('itemCounts', JSON.stringify(itemCounts)); } function restoreItemCounts() { try { const saved = JSON.parse(localStorage.getItem('itemCounts')); if (saved && typeof saved === 'object') { itemCounts = saved; } } catch (err) { console.error('Error restoring item counts:', err); } updateAllItemDisplays(); updateCalculations(); updateButtonColors(); // Add this line } function clearOnlySelections() { itemCounts = {}; saveItemCounts(); updateAllItemDisplays(); updateCalculations(); updateButtonColors(); // Add this line console.log("Equipment selections cleared"); } function clearDates() { pickupPicker.clear(); returnPicker.clear(); localStorage.removeItem('pickupDate'); localStorage.removeItem('returnDate'); $('#pickupDateButton .date-title').text('CHOOSE'); $('#pickupDateButton .date-value').text('PICKUP DATE'); $('#returnDateButton .date-title').text('CHOOSE'); $('#returnDateButton .date-value').text('RETURN DATE'); updateDates(); // Reset button colors $('#pickupDateButton').css('background', 'var(--eqs-primary-color)'); $('#returnDateButton').css('background', 'var(--eqs-primary-color)'); console.log("Dates cleared"); } function clearSearch() { $('#equipmentSearchInput').val(''); localStorage.removeItem('lastSearchQuery'); $('#equipmentSearchPopup').hide(); $('#directSearchResults').hide(); console.log("Search menu cleared"); } function clearAllSelections() { itemCounts = {}; saveItemCounts(); pickupPicker.clear(); returnPicker.clear(); localStorage.removeItem('pickupDate'); localStorage.removeItem('returnDate'); $('#pickupDateButton .date-title').text('CHOOSE'); $('#pickupDateButton .date-value').text('PICKUP DATE'); $('#returnDateButton .date-title').text('CHOOSE'); $('#returnDateButton .date-value').text('RETURN DATE'); clearSearch(); updateAllItemDisplays(); updateDates(); updateButtonColors(); // Add this line console.log("All selections cleared"); } /** * ================================================== * FLATPICKR DATE PICKER INITIALIZATION * ================================================== */ // Initialize pickup date picker const pickupPicker = flatpickr('#pickupDateButton', { dateFormat: 'Y-m-d', minDate: 'today', disableMobile: true, appendTo: document.body, // Append to body for better positioning positionElement: null, // Don't use the input for positioning position: "auto", // Auto positioning instead of center static: true, // Prevent repositioning after display onChange: function (sel) { if (sel.length) { localStorage.setItem('pickupDate', sel[0].toISOString()); const formatted = formatPickupDate(sel[0]); $('#pickupDateButton .date-title').text(formatted.line1); $('#pickupDateButton .date-value').text(formatted.line2); returnPicker.set('minDate', sel[0]); } else { localStorage.removeItem('pickupDate'); $('#pickupDateButton .date-title').text('CHOOSE'); $('#pickupDateButton .date-value').text('PICKUP DATE'); } updateDates(); } }); // Initialize return date picker const returnPicker = flatpickr('#returnDateButton', { dateFormat: 'Y-m-d', minDate: 'today', disableMobile: true, appendTo: document.body, // Append to body for better positioning positionElement: null, // Don't use the input for positioning position: "auto", // Auto positioning instead of center static: true, // Prevent repositioning after display onChange: function (sel) { if (sel.length) { localStorage.setItem('returnDate', sel[0].toISOString()); const formatted = formatReturnDate(sel[0]); $('#returnDateButton .date-title').text(formatted.line1); $('#returnDateButton .date-value').text(formatted.line2); } else { localStorage.removeItem('returnDate'); $('#returnDateButton .date-title').text('CHOOSE'); $('#returnDateButton .date-value').text('RETURN DATE'); } updateDates(); } }); // Restore stored pickup date const storedPickup = localStorage.getItem('pickupDate'); if (storedPickup) { try { const pd = new Date(storedPickup); // Validate date is actually valid if (!isNaN(pd.getTime())) { pickupPicker.setDate(pd, false); const formatted = formatPickupDate(pd); $('#pickupDateButton .date-title').text(formatted.line1); $('#pickupDateButton .date-value').text(formatted.line2); returnPicker.set('minDate', pd); } else { localStorage.removeItem('pickupDate'); $('#pickupDateButton .date-title').text('CHOOSE'); $('#pickupDateButton .date-value').text('PICKUP DATE'); } } catch (e) { localStorage.removeItem('pickupDate'); $('#pickupDateButton .date-title').text('CHOOSE'); $('#pickupDateButton .date-value').text('PICKUP DATE'); } } // Restore stored return date const storedReturn = localStorage.getItem('returnDate'); if (storedReturn) { try { const rd = new Date(storedReturn); // Validate date is actually valid if (!isNaN(rd.getTime())) { returnPicker.setDate(rd, false); const formatted2 = formatReturnDate(rd); $('#returnDateButton .date-title').text(formatted2.line1); $('#returnDateButton .date-value').text(formatted2.line2); } else { localStorage.removeItem('returnDate'); $('#returnDateButton .date-title').text('CHOOSE'); $('#returnDateButton .date-value').text('RETURN DATE'); } } catch (e) { localStorage.removeItem('returnDate'); $('#returnDateButton .date-title').text('CHOOSE'); $('#returnDateButton .date-value').text('RETURN DATE'); } } // Call updateDates to ensure everything is consistent updateDates(); // Set initial button colors on page load updateButtonColors(); /** * ================================================== * SEARCH FUNCTIONALITY * ================================================== */ /** /** /** * Enhanced search function with hierarchical display matching Equipment Categories popup * @param {string} query - The search query */ function performEnhancedSearch(query) { const searchTerm = query.trim().toLowerCase(); const $popup = $('#directSearchResults'); // Store current expansion states before clearing const expandedStates = {}; if (!$popup.data('initialized')) { // First time initialization $popup.data('initialized', true); } else { // Save current expansion states $popup.find('.menu-item').each(function() { const $item = $(this); const categoryId = $item.data('cat-id') || $item.data('item'); if (categoryId) { const $icon = $item.find('> .category-header > .toggle-icon.has-children'); if ($icon.length && $icon.hasClass('expanded')) { expandedStates[categoryId] = true; } } }); } // Clear old results $popup.empty(); // Hide the legacy popup $('#equipmentSearchPopup').hide(); // Add the header with controls - UPDATED to match equipment categories style $popup.append(`
    `); const $ul = $popup.find('ul.search-results-list'); // Set up close button click handler $('#closeSearchResults').on('click', function(e) { e.stopPropagation(); e.preventDefault(); $popup.hide(); }); // Search menu toggle button handler - NEW $('#searchMenuToggleButton').off('click').on('click', function(e) { e.stopPropagation(); searchMenuExpanded = !searchMenuExpanded; if (searchMenuExpanded) { $(this).text('▼'); expandAllCategoriesInContainer('#directSearchResults'); } else { $(this).text('▶'); collapseAllCategoriesInContainer('#directSearchResults'); } // Store the expanded state localStorage.setItem('searchMenuExpanded', searchMenuExpanded); }); // Set up description toggle handler $('#searchDescriptionsToggle').on('change', function() { searchInDescriptions = $(this).is(':checked'); // Re-perform search with current settings performEnhancedSearch($('#equipmentSearchInput').val()); }); // If we have data if (equipmentData && equipmentData.length > 0) { // Check if there's a search term if (searchTerm) { // Split search into words for multi-word matching const words = searchTerm.split(/\s+/); // Track all matching items across all hierarchy levels let hasAnyResults = false; // Process each main category equipmentData.forEach((mainCat, mainIndex) => { let mainCatMatches = false; let mainCatItems = []; // Check if main category name matches const mainCatMatches_name = words.every(word => mainCat.name.toLowerCase().includes(word)); // Create main category list item, but don't add yet (wait to see if it or children match) const $mainCatLi = $(` `); const $mainSubmenu = $mainCatLi.find('> ul.submenu'); // Check items in main category mainCat.items.forEach(item => { const itemMatches_name = words.every(word => item.item.toLowerCase().includes(word)); const itemMatches_desc = searchInDescriptions && item.description && words.every(word => item.description.toLowerCase().includes(word)); if (itemMatches_name || itemMatches_desc) { mainCatMatches = true; mainCatItems.push(item); } }); // Process subcategories mainCat.subCategories.forEach((subCat, subIndex) => { let subCatMatches = false; let subCatItems = []; // Check if subcategory name matches const subCatMatches_name = words.every(word => subCat.name.toLowerCase().includes(word)); // Create subcategory list item const $subCatLi = $(` `); const $subSubmenu = $subCatLi.find('> ul.submenu'); // Check items in subcategory subCat.items.forEach(item => { const itemMatches_name = words.every(word => item.item.toLowerCase().includes(word)); const itemMatches_desc = searchInDescriptions && item.description && words.every(word => item.description.toLowerCase().includes(word)); if (itemMatches_name || itemMatches_desc) { subCatMatches = true; subCatItems.push(item); } }); // Process third level categories subCat.thirdLevels.forEach((thirdLevel, thirdIndex) => { let thirdLevelMatches = false; let thirdLevelItems = []; // Check if third level name matches const thirdLevelMatches_name = words.every(word => thirdLevel.name.toLowerCase().includes(word)); // Create third level list item const $thirdLevelLi = $(` `); const $thirdSubmenu = $thirdLevelLi.find('> ul.submenu'); // Check items in third level thirdLevel.items.forEach(item => { const itemMatches_name = words.every(word => item.item.toLowerCase().includes(word)); const itemMatches_desc = searchInDescriptions && item.description && words.every(word => item.description.toLowerCase().includes(word)); if (itemMatches_name || itemMatches_desc) { thirdLevelMatches = true; thirdLevelItems.push(item); // Add matching item to third level submenu $thirdSubmenu.append(` `); } }); // If this third level or its name matches, add it to subcategory submenu if (thirdLevelMatches || thirdLevelMatches_name) { subCatMatches = true; $subSubmenu.append($thirdLevelLi); } }); // Add matching subcategory items to submenu subCatItems.forEach(item => { $subSubmenu.append(` `); }); // If this subcategory or its name matches, add it to main category submenu if (subCatMatches || subCatMatches_name) { mainCatMatches = true; $mainSubmenu.append($subCatLi); } }); // Add matching main category items to submenu mainCatItems.forEach(item => { $mainSubmenu.append(` `); }); // If this main category or its name matches, add it to the results if (mainCatMatches || mainCatMatches_name) { hasAnyResults = true; $ul.append($mainCatLi); } }); // If no results were found if (!hasAnyResults) { $ul.append('
  • No results found
  • '); } } else { // No search term - build complete hierarchical menu equipmentData.forEach((mainCat) => { // Create main category with collapsed submenu const $mainCatLi = $(` `); const $mainSubmenu = $mainCatLi.find('> ul.submenu'); // Add items directly under main category mainCat.items.forEach(item => { $mainSubmenu.append(` `); }); // Add subcategories mainCat.subCategories.forEach(subCat => { // Create subcategory with collapsed submenu const $subCatLi = $(` `); const $subSubmenu = $subCatLi.find('> ul.submenu'); // Add items directly under subcategory subCat.items.forEach(item => { $subSubmenu.append(` `); }); // Add third level categories subCat.thirdLevels.forEach(thirdLevel => { // Create third level with submenu const $thirdLevelLi = $(` `); const $thirdSubmenu = $thirdLevelLi.find('> ul.submenu'); // Add items under third level thirdLevel.items.forEach(item => { $thirdSubmenu.append(` `); }); // Only add third level if it has items if (thirdLevel.items.length > 0) { $subSubmenu.append($thirdLevelLi); } }); // Only add subcategory if it has items or third levels if (subCat.items.length > 0 || subCat.thirdLevels.length > 0) { $mainSubmenu.append($subCatLi); } }); // Add main category to results $ul.append($mainCatLi); }); } } else { // No equipment data loaded yet $ul.append('
  • Loading categories...
  • '); } // Set up click handlers for category headers and toggle icons // Toggle icon click handler - expand/collapse submenu $popup.off('click.toggleSearch').on('click.toggleSearch', '.toggle-icon.has-children', function(e) { e.stopPropagation(); toggleCategoryExpansion($(this)); }); // Category header click handler - navigate to category $popup.off('click.selectSearchCategory').on('click.selectSearchCategory', '.category-header', function(e) { // Skip if clicking directly on the toggle icon if ($(e.target).hasClass('toggle-icon') || $(e.target).closest('.toggle-icon.has-children').length > 0) { return; } e.stopPropagation(); const $menuItem = $(this).closest('.menu-item'); // Handle item results differently if ($menuItem.hasClass('item-result')) { const itemName = $menuItem.data('item'); const $target = $(`.equipment-item[data-item-id="${itemName}"]`).first(); if ($target.length) { scrollToElement($target); $popup.hide(); // Do not clear the search input - keep the term } return; } // Handle category navigation const catId = $menuItem.data('cat-id'); if (catId) { const $target = $('#' + catId); if ($target.length) { scrollToElement($target); $popup.hide(); // Do not clear the search input - keep the term } } }); // Apply saved expansion states $popup.find('.menu-item').each(function() { const $item = $(this); const categoryId = $item.data('cat-id') || $item.data('item'); if (categoryId && expandedStates[categoryId]) { const $icon = $item.find('> .category-header > .toggle-icon.has-children'); if ($icon.length && $icon.hasClass('collapsed')) { // Expand without animation $icon.removeClass('collapsed').addClass('expanded'); $icon.text('▼'); $item.find('> ul.submenu').show(); } } }); // Apply the current expansion state only if search term exists and we have no saved states if (searchTerm && searchMenuExpanded && Object.keys(expandedStates).length === 0) { expandAllCategoriesInContainer('#directSearchResults'); } // Show the popup $popup.show(); } /** * Toggle expansion state for a category or search result * Used by both regular category menus and search results */ // Initialize search functionality function initializeSearch() { // Input handler - debounced for better performance const debouncedSearch = debounce(function(query) { performEnhancedSearch(query); }, 300); $('#equipmentSearchInput').off('input').on('input', function() { // Check if dates are set const pd = pickupPicker.selectedDates[0]; const rd = returnPicker.selectedDates[0]; if (!pd || !rd) { // Clear the input field $(this).val(''); $(this).blur(); showQuoteHelpPage(); return; } const query = $(this).val(); // Close all other popups first $('#customDropdownMenu').hide(); $('#QuoteHelpPage').hide(); $('#startOverDialog').hide(); debouncedSearch(query); // Save the search query to localStorage localStorage.setItem('lastSearchQuery', query); }); // Focus handler for search input $('#equipmentSearchInput').off('focus').on('focus', function(e) { // Check if dates are set at the beginning of the focus handler const pd = pickupPicker.selectedDates[0]; const rd = returnPicker.selectedDates[0]; if (!pd || !rd) { // Remove focus immediately without showing search results e.preventDefault(); $(this).blur(); showQuoteHelpPage(); return; } $(this).attr('placeholder', 'Type to search...'); // Close all other popups first, especially categories menu $('#customDropdownMenu').hide(); $('#QuoteHelpPage').hide(); $('#startOverDialog').hide(); // Retrieve last search query from localStorage const lastQuery = localStorage.getItem('lastSearchQuery') || ''; // Only set the value if it's different to avoid triggering unnecessary events if ($(this).val() !== lastQuery) { $(this).val(lastQuery); } // Show results based on the last query performEnhancedSearch(lastQuery); // Place cursor at the end of the text instead of selecting all const textLength = $(this).val().length; this.setSelectionRange(textLength, textLength); }); // Blur handler - restore placeholder $('#equipmentSearchInput').off('blur').on('blur', function() { $(this).attr('placeholder', 'Search Equipment...'); }); // Hide search results when clicking outside - now fully consistent with global handler $(document).off('click.search').on('click.search', function(e) { if (!$(e.target).closest('#equipmentSearchContainer, #equipmentSearchInput, #directSearchResults, #closeSearchPopup').length) { $('#directSearchResults').hide(); $('#equipmentSearchPopup').hide(); } }); // Legacy click handler override to use new search $('#closeSearchPopup').off('click').on('click', function(e) { e.stopPropagation(); e.preventDefault(); $('#equipmentSearchPopup').hide(); $('#directSearchResults').hide(); }); // Touch events for mobile $('#equipmentSearchInput').off('touchstart click').on('touchstart click', function(e) { const pd = pickupPicker.selectedDates[0]; const rd = returnPicker.selectedDates[0]; if (!pd || !rd) { e.preventDefault(); e.stopPropagation(); $(this).blur(); showQuoteHelpPage(); return false; } }); } /** * ================================================== * QUOTE FORM * ================================================== */ // Show quote form function showQuoteForm() { $('#quoteFormContainer').show(); if (!$('#quoteFormContainer').find('form').length) { $('#quoteFormContainer').html(`

    CineKits Quote Request

    Please complete this form and submit for a

    Amazing Bid for your Project

    `); // Prevent Enter from auto-submitting $('#quoteFormContainer') .find('input') .on('keydown', function (e) { if (e.key === 'Enter') e.preventDefault(); }); // Phone number auto-format $('#mobile, #landline').on('input', function () { let input = $(this).val(); if (input.charAt(0) === '+') { $(this).val(formatInternational(input)); } else { let digits = input.replace(/\D/g, ''); if (digits.length > 10) { digits = digits.slice(0, 10); } let formatted = ''; if (digits.length > 6) { formatted = digits.slice(0, 3) + '-' + digits.slice(3, 6) + '-' + digits.slice(6); } else if (digits.length > 3) { formatted = digits.slice(0, 3) + '-' + digits.slice(3); } else { formatted = digits; } $(this).val(formatted); } }); // Cancel button $('#cancelQuoteForm').on('click', function (e) { e.stopPropagation(); e.preventDefault(); $('#quoteFormContainer').hide(); }); // Bottom Cancel button $('#cancelQuoteFormBottom').on('click', function (e) { e.stopPropagation(); e.preventDefault(); $('#quoteFormContainer').hide(); }); // Submit $('#quoteFormContainer') .find('form') .on('submit', handleQuoteSubmission); } } // Handle quote form submission function handleQuoteSubmission(e) { console.log("DEBUG: handleQuoteSubmission called"); e.preventDefault(); // Removed submission limit temporarily console.log("DEBUG: Proceeding with form submission"); const pickupDate = pickupPicker.selectedDates[0]; const returnDate = returnPicker.selectedDates[0]; const daysUntil = pickupDate ? daysUntilPickup() : 0; const formData = { firstname: $('#firstname').val(), lastname: $('#lastname').val(), company: $('#company').val(), email: $('#email').val(), mobile: $('#mobile').val(), landline: $('#landline').val(), title: $('#title').val(), project: $('#project').val(), extra: $('#extra').val(), comments: $('#comments').val() }; const rentalDays = calculateRentalDays(); let totalEstimate = 0; Object.keys(itemCounts).forEach((itemId) => { const count = itemCounts[itemId] || 0; if (count > 0) { const $item = $(`.equipment-item[data-item-id="${itemId}"]`); if ($item.length) { const dayRateNum = parseFloat($item.data('day-rate-num')) || 0; totalEstimate += count * dayRateNum * rentalDays; } } }); const selectedEquipment = Object.keys(itemCounts) .filter((itemId) => itemCounts[itemId] > 0) .map((itemId) => { const count = itemCounts[itemId]; const $item = $(`.equipment-item[data-item-id="${itemId}"]`); const name = $item.find('.item-name').text(); const dayRateNum = parseFloat($item.data('day-rate-num')) || 0; const dayRateRaw = $item.data('day-rate-raw') || dayRateNum; const dayRateLabel = dayRateNum === 0 ? '$0/day' : `$${dayRateRaw}/day`; const qtyOwned = parseInt($item.data('qty-owned'), 10) || 0; const subtotalForItem = count * dayRateNum * rentalDays; let line = `${count} @ ${rentalDays} day Rate Card: $${formatWithCommas(subtotalForItem)} | ${name} - ${dayRateLabel}`; const subRent = count - qtyOwned; if (subRent > 0) { line += `\n will locate ${subRent} more\n`; } return line; }) .join('\n'); const formattedPickup = pickupDate ? formatDate(pickupDate, 'mmddyyyy') : 'Not selected'; const totalEstimateFormatted = formatWithCommas(totalEstimate); let prefix = pickupSubjectPrefix(daysUntil); let subjectPickupStr = pickupDate ? formatDate(pickupDate, 'subject') : 'Not selected'; let subjectLine = `Quote Needed: ${prefix}: ${subjectPickupStr}`; let emailBody = `Hi CineKits,\nLooking forward to getting a bid for my upcoming project.\n\n`; emailBody += `Quote Request in ${daysUntil} days: Pickup on ${formattedPickup}\n`; emailBody += `-------------------------------\n`; emailBody += `Name: ${formData.firstname} ${formData.lastname}\n`; emailBody += `Company: ${formData.company}\n`; emailBody += `Email: ${formData.email}\n`; emailBody += `Mobile: ${formData.mobile}\n`; emailBody += `Work Phone: ${formData.landline}\n`; emailBody += `Title: ${formData.title}\n`; emailBody += `Project: ${formData.project}\n\n`; emailBody += `Rental Period:\n-------------\n`; emailBody += `Pickup Date: ${pickupDate ? formatDate(pickupDate, 'withDay') : 'Not selected'}\n`; emailBody += `Return Date: ${returnDate ? formatDate(returnDate, 'withDay') : 'Not selected'}\n`; emailBody += `Total Rental Days: ${rentalDays}\n\n`; emailBody += `Selected Equipment:\n-----------------\n${selectedEquipment}\n\n`; emailBody += `Total Rate Card Estimate: $${totalEstimateFormatted}\n\n`; emailBody += `Extra Equipment Needed:\n---------------------\n${formData.extra}\n\n`; emailBody += `Additional Comments:\n---------------------\n${formData.comments}\n`; const mailtoLink = `mailto:cinekits@gmail.com?cc=${formData.email}&subject=${encodeURIComponent(subjectLine)}&body=${encodeURIComponent(emailBody)}`; // First, add these helper functions at the beginning of the file (after variable declarations) // Around line 20, after global variables and before functions window.modalRevise = function() { var modal = document.getElementById('thankYouModal'); if (modal) modal.remove(); }; window.modalEmail = function(link) { var modal = document.getElementById('thankYouModal'); var form = document.getElementById('quoteFormContainer'); if (modal) modal.remove(); if (form) form.style.display = 'none'; setTimeout(function() { window.location.href = link; }, 500); }; // Log debug information // Log debug information console.log("DEBUG: About to create modal and redirect to email"); console.log("DEBUG: mailtoLink =", mailtoLink); // First, hide the quoteFormContainer to prevent event interference $('#quoteFormContainer').hide(); console.log("DEBUG: Temporarily hid form container"); // Remove any existing modal if ($('#thankYouModal').length > 0) { $('#thankYouModal').remove(); } // Create a simpler modal similar to your other working modals $('body').append(`

    Ready to email

    CineKits for a Quote?

    `); // Delay adding event handlers to ensure DOM is ready setTimeout(function() { // Attach click event to the "REVISE" button $('#reviseModalButton').on('click', function() { $('#thankYouModal').remove(); $('#quoteFormContainer').show(); // Show the form again console.log("DEBUG: REVISE clicked"); }); // Attach click event to the "EMAIL" button $('#emailModalButton').on('click', function() { $('#thankYouModal').remove(); console.log("DEBUG: EMAIL clicked"); setTimeout(function() { console.log("DEBUG: Redirecting to email client"); window.location.href = mailtoLink; }, 300); }); console.log("DEBUG: Modal event handlers attached with delay"); }, 100); console.log("DEBUG: Modal created after hiding form"); // Function close } // Handle Request Quote button click function handleRequestQuoteClick() { let hasAnyItem = false; for (const id in itemCounts) { if (itemCounts[id] > 0) { hasAnyItem = true; break; } } if (!hasAnyItem) { showQuoteHelpPage(); return; } showQuoteForm(); } /** * ================================================== * EVENT HANDLERS * ================================================== */ // Global click handler (capture phase) document.addEventListener('click', function(e) { // 1) If any search popup is visible (either legacy or enhanced) if ($('#equipmentSearchPopup').is(':visible') || $('#directSearchResults').is(':visible')) { // If click is outside the search container and related elements if (!$(e.target).closest('#equipmentSearchContainer, #equipmentSearchInput, #directSearchResults, #closeSearchPopup').length) { e.preventDefault(); e.stopPropagation(); $('#equipmentSearchPopup').hide(); $('#directSearchResults').hide(); return; } } // 2) If the custom dropdown is visible if ($('#customDropdownMenu').is(':visible')) { // If click is outside the button or the menu if (!$(e.target).closest('#customDropdownButton, #customDropdownMenu, .close-popup-btn').length) { e.preventDefault(); e.stopPropagation(); $('#customDropdownMenu').hide(); return; } } // Check for clicks on elements that need dates set if ($(e.target).closest('.equipment-item, #requestQuote, #yourEquipmentButton, #customDropdownButton, #estimateButton, #rentalDayButton, #requestQuoteLink, #requestQuoteBottom, #equipmentSearchInput').length) { // Don't interrupt clicks on the date buttons themselves if (!$(e.target).closest('#pickupDateButton, #returnDateButton').length) { // Check if dates are set const pd = pickupPicker.selectedDates[0]; const rd = returnPicker.selectedDates[0]; if (!pd || !rd) { e.preventDefault(); e.stopPropagation(); showQuoteHelpPage(); return; } } } // 3) If the instructions popup is visible if ($('#QuoteHelpPage').is(':visible')) { // If click is outside the instructions popup and not on the search field if (!$(e.target).closest('#QuoteHelpPage, #instructionsButton, #closeQuoteHelpPage, #equipmentSearchInput, #equipmentCategoriesContainer').length) { e.preventDefault(); e.stopPropagation(); $('#QuoteHelpPage').hide(); return; } } // 4) If the quote form is visible if ($('#quoteFormContainer').is(':visible')) { // If click is outside the form container if (!$(e.target).closest('#quoteFormContainer, #cancelQuoteForm').length) { e.preventDefault(); e.stopPropagation(); return; } } // 5) If the start over dialog is visible if ($('#startOverDialog').is(':visible')) { // If click is outside the dialog if (!$(e.target).closest('#startOverDialog, #startOver').length) { e.preventDefault(); e.stopPropagation(); $('#startOverDialog').hide(); return; } } }, true); // true means capture phase // Close search popup $(document).on('click', '#closeSearchPopup', function(e) { e.stopPropagation(); e.preventDefault(); $('#equipmentSearchPopup').hide(); }); // Close dropdown menu $(document).on('click', '#closeDropdownMenu', function(e) { e.stopPropagation(); e.preventDefault(); $('#customDropdownMenu').hide(); }); // Close instructions $(document).on('click', '#closeQuoteHelpPage', function(e) { e.preventDefault(); $('#QuoteHelpPage').hide(); }); // Start Over button $('#startOver').on('click', function (e) { e.stopPropagation(); e.preventDefault(); const hasPickup = pickupPicker.selectedDates[0]; const hasReturn = returnPicker.selectedDates[0]; let hasAnyItem = false; for (const id in itemCounts) { if (itemCounts[id] > 0) { hasAnyItem = true; break; } } if (hasPickup || hasReturn || hasAnyItem) { $('#startOverDialog') .css({ top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }) .show(); } else { showQuoteHelpPage(); } }); // Start Over Dialog buttons $('#confirmClearEquipment').click(function () { clearOnlySelections(); $('#startOverDialog').hide(); }); $('#confirmClearDates').click(function () { clearDates(); $('#startOverDialog').hide(); }); $('#confirmClearSearch').click(function () { clearSearch(); $('#startOverDialog').hide(); }); $('#confirmClearAll').click(function () { clearAllSelections(); $('#startOverDialog').hide(); }); $('#confirmStartOverNo').click(function () { $('#startOverDialog').hide(); }); // Request Quote buttons $('#requestQuote').on('click', function (e) { e.stopPropagation(); e.preventDefault(); handleRequestQuoteClick(); }); $(document).on('click', '#requestQuoteLink', function (e) { e.stopPropagation(); e.preventDefault(); handleRequestQuoteClick(); }); // Equipment Categories dropdown $('#customDropdownButton').on('click', function (e) { e.stopPropagation(); e.preventDefault(); // Close all popups closeAllPopups(); const $menu = $('#customDropdownMenu'); // Simple toggle if ($menu.is(':visible')) { $menu.hide(); } else { $menu.show(); } }); // Instructions button $('#instructionsButton').on('click', function (e) { e.stopPropagation(); e.preventDefault(); showQuoteHelpPage(); }); // Show Instructions / Help Page function showQuoteHelpPage() { // First hide the specific popups that should be closed (but not QuoteHelpPage) $('#equipmentSearchPopup').hide(); $('#directSearchResults').hide(); $('#customDropdownMenu').hide(); $('#startOverDialog').hide(); // Then position and show the help page $('#QuoteHelpPage') .css({ top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }) .show(); } // Other header buttons $('#estimateButton').on('click', function (e) { e.stopPropagation(); e.preventDefault(); showQuoteHelpPage(); }); $('#rentalDayButton').on('click', function (e) { e.stopPropagation(); e.preventDefault(); showQuoteHelpPage(); }); // YOUR EQUIPMENT button - toggle between all items and selected items $('#yourEquipmentButton').on('click', function (e) { e.stopPropagation(); e.preventDefault(); // Check if dates are set const pd = pickupPicker.selectedDates[0]; const rd = returnPicker.selectedDates[0]; if (!pd || !rd) { showQuoteHelpPage(); return; } // Check if any items are selected let hasAnyItem = false; for (const id in itemCounts) { if (itemCounts[id] > 0) { hasAnyItem = true; break; } } // If no items selected, show help page if (!hasAnyItem) { showQuoteHelpPage(); return; } // Toggle the view state showOnlySelected = !showOnlySelected; // Update button text and style if (showOnlySelected) { $('#yourEquipmentButton .date-title').text('SHOW ALL'); $('#yourEquipmentButton .date-value').text('EQUIPMENT'); $('#yourEquipmentButton').addClass('toggled'); } else { $('#yourEquipmentButton .date-title').text('SHOW YOUR'); $('#yourEquipmentButton .date-value').text('EQUIPMENT'); $('#yourEquipmentButton').removeClass('toggled'); } // Filter the equipment list filterEquipmentList(); // Scroll to top of equipment list when toggling between views window.scrollTo({ top: $('#equipmentList').offset().top - $('#stickyBlock').outerHeight(), behavior: 'smooth' }); }); $('#quoteHeader').on('click', function (e) { if (!$(e.target).is('#requestQuoteLink')) { e.stopPropagation(); e.preventDefault(); showQuoteHelpPage(); } }); /** * ================================================== * INITIALIZATION * ================================================== */ // Initialize button text for YOUR EQUIPMENT $('#yourEquipmentButton .date-title').text('SHOW YOUR'); $('#yourEquipmentButton .date-value').text('EQUIPMENT'); // Load equipment data loadEquipmentData(); // Initialize dates updateDates(); // Force a refresh of all displays after full load $(window).on('load', function() { updateDates(); updateAllItemDisplays(); updateCalculations(); updateStickyHeaderOffset(); // Add this line updateButtonColors(); // Make sure this is called }); // Function to update button colors based on selected state function updateButtonColors() { // Update pickup date button color if (pickupPicker.selectedDates.length > 0) { $('#pickupDateButton').css('background', 'var(--eqs-highlight-color)'); } else { $('#pickupDateButton').css('background', 'var(--eqs-primary-color)'); } // Update return date button color if (returnPicker.selectedDates.length > 0) { $('#returnDateButton').css('background', 'var(--eqs-highlight-color)'); } else { $('#returnDateButton').css('background', 'var(--eqs-primary-color)'); } // Check if any items are selected let hasSelectedItems = false; for (const id in itemCounts) { if (itemCounts[id] > 0) { hasSelectedItems = true; break; } } // Update Your Equipment button color based on item selection AND toggle state if (hasSelectedItems) { // If items are selected, use pink for normal state, keep blue for toggled if (showOnlySelected) { // In "See All Equipment" mode (toggled) $('#yourEquipmentButton').css('background', 'var(--eqs-primary-color)'); // Pink } else { // In "See Only Your Equipment" mode (normal) $('#yourEquipmentButton').css('background', 'var(--eqs-primary-color)'); // Pink } } else { // No items selected - both states should be blue $('#yourEquipmentButton').css('background', 'var(--eqs-highlight-color)'); // Blue $('#yourEquipmentButton').removeClass('toggled'); // Also remove toggled class // Reset button text since we can't show "selected" when there are none $('#yourEquipmentButton .date-title').text('SHOW YOUR'); $('#yourEquipmentButton .date-value').text('EQUIPMENT'); // Reset the view state showOnlySelected = false; } // Update Request Quote button color based on item selection if (hasSelectedItems) { $('#requestQuote').css('background', 'var(--eqs-primary-color)'); // Pink $('#requestQuoteBottom').css('background', 'var(--eqs-primary-color)'); // Pink for bottom button too } else { $('#requestQuote').css('background', 'var(--eqs-highlight-color)'); // Blue $('#requestQuoteBottom').css('background', 'var(--eqs-highlight-color)'); // Blue for bottom button too } } // Call these functions on page load and when window resizes $(window).on('load resize', function() { updateStickyHeaderOffset(); }); });