.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(`
`);
});
});
});
// 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 = $(`
▶${mainCat.name}
`);
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 = $(`
▶${subCat.name}
`);
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 = $(`
▶${thirdLevel.name}
`);
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(`
${item.item}
`);
}
});
// 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(`
${item.item}
`);
});
// 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(`
${item.item}
`);
});
// 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 = $(`
▶${mainCat.name}
`);
const $mainSubmenu = $mainCatLi.find('> ul.submenu');
// Add items directly under main category
mainCat.items.forEach(item => {
$mainSubmenu.append(`
`);
});
// Add third level categories
subCat.thirdLevels.forEach(thirdLevel => {
// Create third level with submenu
const $thirdLevelLi = $(`
▶${thirdLevel.name}
`);
const $thirdSubmenu = $thirdLevelLi.find('> ul.submenu');
// Add items under third level
thirdLevel.items.forEach(item => {
$thirdSubmenu.append(`
${item.item}
`);
});
// 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(`
`);
// 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();
});
});