854 lines
34 KiB
HTML
854 lines
34 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||
<meta http-equiv="Pragma" content="no-cache">
|
||
<meta http-equiv="Expires" content="0">
|
||
<title>Advanced Search - Product Finder</title>
|
||
<link rel="stylesheet" href="{{ url_for('products.serve_css', filename='styles.css') }}">
|
||
<style>
|
||
.search-container {
|
||
padding: 30px 40px;
|
||
}
|
||
|
||
.search-header {
|
||
text-align: center;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.search-input-container {
|
||
max-width: 800px;
|
||
margin: 0 auto;
|
||
text-align: center;
|
||
}
|
||
|
||
.products-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||
gap: 20px;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.product-card {
|
||
background: white;
|
||
border: 2px solid #e0e0e0;
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.product-card:hover {
|
||
border-color: #6a4c93;
|
||
box-shadow: 0 4px 12px rgba(106, 76, 147, 0.15);
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.product-card h3 {
|
||
color: #333;
|
||
font-size: 1.1em;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.no-results {
|
||
text-align: center;
|
||
padding: 40px;
|
||
color: #666;
|
||
}
|
||
|
||
.search-stats {
|
||
text-align: center;
|
||
padding: 15px;
|
||
background: #f5f5f5;
|
||
border-radius: 8px;
|
||
margin-bottom: 20px;
|
||
color: #666;
|
||
}
|
||
|
||
/* Filter Sections */
|
||
.filters-container {
|
||
max-width: 800px;
|
||
margin: 20px auto;
|
||
}
|
||
|
||
.filter-section {
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.filter-toggle {
|
||
width: 100%;
|
||
background-color: #f8f9fa;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 6px;
|
||
padding: 12px 16px;
|
||
font-size: 14px;
|
||
color: #495057;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
transition: all 0.2s ease;
|
||
text-align: left;
|
||
}
|
||
|
||
.filter-toggle:hover {
|
||
background-color: #e9ecef;
|
||
border-color: #ced4da;
|
||
}
|
||
|
||
.filter-icon {
|
||
font-size: 18px;
|
||
}
|
||
|
||
.filter-label {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 5px;
|
||
}
|
||
|
||
.filter-title {
|
||
font-weight: 600;
|
||
}
|
||
|
||
.filter-values {
|
||
color: #6a4c93;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.filter-arrow {
|
||
font-size: 12px;
|
||
transition: transform 0.2s ease;
|
||
}
|
||
|
||
.filter-content {
|
||
margin-top: 5px;
|
||
padding: 16px;
|
||
background-color: #fff;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.filter-options {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||
gap: 10px;
|
||
}
|
||
|
||
.filter-checkbox {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
cursor: pointer;
|
||
padding: 6px;
|
||
border-radius: 4px;
|
||
transition: background-color 0.2s ease;
|
||
}
|
||
|
||
.filter-checkbox:hover {
|
||
background-color: #f8f9fa;
|
||
}
|
||
|
||
.filter-checkbox input[type="checkbox"] {
|
||
width: 18px;
|
||
height: 18px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.filter-checkbox span {
|
||
user-select: none;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="header">
|
||
<h1>Advanced Search</h1>
|
||
</div>
|
||
|
||
<div class="user-info-bar" id="userInfoBar">
|
||
<span id="userLocationInfo"></span>
|
||
<a href="{{ url_for('auth.logout') }}" class="logout-btn" title="Logout"
|
||
onclick="return confirm('Are you sure you want to log out?');"
|
||
style="background: none; border: none; cursor: pointer; font-size: 16px; text-decoration: none; display: inline-block; padding: 8px 12px;">🚪</a>
|
||
</div>
|
||
|
||
<div class="breadcrumb">
|
||
Start › Advanced Search
|
||
</div>
|
||
|
||
<div id="content">
|
||
<div class="search-container">
|
||
<div class="search-header">
|
||
<h2 style="margin-bottom: 15px;">🔍 Search Products</h2>
|
||
<p style="color: #666; margin-bottom: 20px;">
|
||
Search by product code, description, color, material, or any combination.
|
||
Try: "c1800", "storm door white", "bronze aluminum"
|
||
</p>
|
||
</div>
|
||
|
||
<div class="search-input-container">
|
||
<input
|
||
type="text"
|
||
id="searchInput"
|
||
placeholder="Enter search terms..."
|
||
style="width: 100%; padding: 15px 20px; font-size: 16px; border: 2px solid #ddd; border-radius: 8px; box-sizing: border-box;"
|
||
onkeyup="handleSearch(event)"
|
||
>
|
||
<!-- Hidden field stores the cleaned search text after filter keywords are removed -->
|
||
<input type="hidden" id="remainingSearchText" value="">
|
||
|
||
<!-- Filter Sections (dynamically generated from filter_config.json) -->
|
||
<div id="filtersContainer" class="filters-container" style="margin-top: 20px;">
|
||
<!-- Filters will be generated here by JavaScript -->
|
||
</div>
|
||
|
||
<button
|
||
onclick="performSearch()"
|
||
style="margin-top: 15px; padding: 15px 30px; font-size: 16px; background-color: #6a4c93; color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: 600;"
|
||
>
|
||
Search
|
||
</button>
|
||
<button
|
||
onclick="clearSearch()"
|
||
style="margin-top: 15px; margin-left: 10px; padding: 15px 30px; font-size: 16px; background-color: #ccc; color: #333; border: none; border-radius: 8px; cursor: pointer; font-weight: 600;"
|
||
>
|
||
Clear
|
||
</button>
|
||
</div>
|
||
|
||
<div id="searchResults" style="margin-top: 30px;"></div>
|
||
|
||
<div class="result-actions" style="margin-top: 30px;">
|
||
<button class="back-button" onclick="goToFinder()">🏠 Back to Product Finder</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// Global data
|
||
let productData = [];
|
||
let filterConfig = null;
|
||
let searchFieldDirty = false; // Track if user has manually edited search after initial load
|
||
|
||
// Load filter configuration
|
||
async function loadFilterConfig() {
|
||
try {
|
||
const response = await fetch('/quiz/data/filter_config.json');
|
||
filterConfig = await response.json();
|
||
console.log('Loaded filter config:', filterConfig);
|
||
return filterConfig;
|
||
} catch (error) {
|
||
console.error('Error loading filter config:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// Generate filter UI from configuration
|
||
function generateFiltersUI() {
|
||
if (!filterConfig) return;
|
||
|
||
const container = document.getElementById('filtersContainer');
|
||
let html = '';
|
||
|
||
// Sort filter groups by order
|
||
const sortedGroups = [...filterConfig.filterGroups].sort((a, b) => a.order - b.order);
|
||
|
||
sortedGroups.forEach(group => {
|
||
const filterId = `${group.id}Filter`;
|
||
const valuesId = `${group.id}FilterValues`;
|
||
|
||
html += `
|
||
<div class="filter-section">
|
||
<button class="filter-toggle" onclick="toggleFilter('${filterId}')" type="button">
|
||
<span class="filter-icon">${group.icon}</span>
|
||
<span class="filter-label">
|
||
<span class="filter-title">${group.label}:</span>
|
||
<span class="filter-values" id="${valuesId}">None selected</span>
|
||
</span>
|
||
<span class="filter-arrow" id="${filterId}-arrow">▼</span>
|
||
</button>
|
||
<div class="filter-content" id="${filterId}" style="display: none;">
|
||
<div class="filter-options">
|
||
`;
|
||
|
||
group.options.forEach(option => {
|
||
html += `
|
||
<label class="filter-checkbox">
|
||
<input type="checkbox"
|
||
name="${group.id}"
|
||
value="${option.value}"
|
||
data-option-id="${option.id}">
|
||
<span>${option.label}</span>
|
||
</label>
|
||
`;
|
||
});
|
||
|
||
html += `
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
// Toggle filter visibility
|
||
function toggleFilter(filterId) {
|
||
const filterContent = document.getElementById(filterId);
|
||
const arrow = document.getElementById(filterId + '-arrow');
|
||
|
||
if (filterContent.style.display === 'none') {
|
||
filterContent.style.display = 'block';
|
||
arrow.textContent = '▲';
|
||
} else {
|
||
filterContent.style.display = 'none';
|
||
arrow.textContent = '▼';
|
||
}
|
||
}
|
||
|
||
// Get all checked filter values organized by group
|
||
function getCheckedFilters() {
|
||
const filters = {};
|
||
|
||
filterConfig.filterGroups.forEach(group => {
|
||
filters[group.id] = [];
|
||
document.querySelectorAll(`input[name="${group.id}"]:checked`).forEach(checkbox => {
|
||
const optionId = checkbox.dataset.optionId;
|
||
const option = group.options.find(opt => opt.id === optionId);
|
||
if (option) {
|
||
filters[group.id].push(option);
|
||
}
|
||
});
|
||
});
|
||
|
||
return filters;
|
||
}
|
||
|
||
// Update filter display to show selected values
|
||
function updateFilterDisplays() {
|
||
if (!filterConfig) return;
|
||
|
||
filterConfig.filterGroups.forEach(group => {
|
||
const valuesId = `${group.id}FilterValues`;
|
||
const display = document.getElementById(valuesId);
|
||
if (!display) return;
|
||
|
||
const checked = document.querySelectorAll(`input[name="${group.id}"]:checked`);
|
||
|
||
if (checked.length > 0) {
|
||
const labels = Array.from(checked).map(cb => cb.value);
|
||
display.textContent = labels.join(', ');
|
||
display.style.color = '#6a4c93';
|
||
} else {
|
||
display.textContent = 'None selected';
|
||
display.style.color = '#999';
|
||
}
|
||
});
|
||
}
|
||
|
||
// Add event listeners to all checkboxes
|
||
function initializeFilterListeners() {
|
||
document.querySelectorAll('.filter-checkbox input[type="checkbox"]').forEach(checkbox => {
|
||
checkbox.addEventListener('change', updateFilterDisplays);
|
||
});
|
||
}
|
||
|
||
// Check if keywords appear sequentially in search string
|
||
function checkSequentialMatch(searchString, keywords) {
|
||
if (keywords.length === 0) return false;
|
||
if (keywords.length === 1) return searchString.includes(keywords[0]);
|
||
|
||
let lastIndex = -1;
|
||
for (const keyword of keywords) {
|
||
const index = searchString.indexOf(keyword, lastIndex + 1);
|
||
if (index === -1 || index <= lastIndex) {
|
||
return false;
|
||
}
|
||
lastIndex = index;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// Remove only the specific keywords from search string, not text between them
|
||
function removeKeywordsFromSearch(searchString, keywords) {
|
||
if (keywords.length === 0) return searchString;
|
||
|
||
let result = searchString;
|
||
|
||
// Remove each keyword individually
|
||
keywords.forEach(keyword => {
|
||
// Use word boundary to remove only complete words
|
||
const regex = new RegExp(`\\b${keyword}\\b`, 'gi');
|
||
result = result.replace(regex, '').trim();
|
||
});
|
||
|
||
// Clean up multiple spaces
|
||
result = result.replace(/\s+/g, ' ').trim();
|
||
|
||
return result;
|
||
}
|
||
|
||
// Auto-check filters based on search query using sequential matching
|
||
function autoCheckFiltersFromQuery(searchQuery) {
|
||
if (!searchQuery || !filterConfig) return;
|
||
|
||
let remainingSearch = searchQuery.toLowerCase().trim();
|
||
|
||
// Remove ignored words
|
||
const ignoreWords = filterConfig.searchBehavior.ignoreWords || [];
|
||
ignoreWords.forEach(word => {
|
||
const regex = new RegExp(`\\b${word}\\b`, 'gi');
|
||
remainingSearch = remainingSearch.replace(regex, ' ').replace(/\s+/g, ' ').trim();
|
||
});
|
||
|
||
console.log('Processing search:', remainingSearch);
|
||
|
||
// Process filter groups in configured order
|
||
const processOrder = filterConfig.searchBehavior.processOrder || [];
|
||
|
||
processOrder.forEach(groupId => {
|
||
const group = filterConfig.filterGroups.find(g => g.id === groupId);
|
||
if (!group || !group.autoCheckBehavior.enabled) return;
|
||
|
||
group.options.forEach(option => {
|
||
// Normalize keywords
|
||
const normalizedKeywords = option.keywords.map(kw => kw.toLowerCase());
|
||
|
||
// Check if minimum specificity is met
|
||
const minSpec = group.autoCheckBehavior.minSpecificity || 1;
|
||
if (normalizedKeywords.length < minSpec) return;
|
||
|
||
// Check if all keywords are present
|
||
const allPresent = normalizedKeywords.every(kw =>
|
||
remainingSearch.includes(kw)
|
||
);
|
||
|
||
if (!allPresent) return;
|
||
|
||
// If sequential is required, check that
|
||
if (group.autoCheckBehavior.requireSequential && normalizedKeywords.length > 1) {
|
||
if (!checkSequentialMatch(remainingSearch, normalizedKeywords)) {
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Check the checkbox
|
||
const checkbox = document.querySelector(
|
||
`input[name="${group.id}"][data-option-id="${option.id}"]`
|
||
);
|
||
if (checkbox) {
|
||
checkbox.checked = true;
|
||
console.log(`Auto-checked: ${option.label}`);
|
||
|
||
// Remove keywords from search if configured
|
||
if (filterConfig.searchBehavior.removeMatchedFromSearch) {
|
||
remainingSearch = removeKeywordsFromSearch(remainingSearch, normalizedKeywords);
|
||
console.log('Remaining search:', remainingSearch);
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
// Update displays after auto-checking
|
||
updateFilterDisplays();
|
||
|
||
console.log('Final remaining search:', remainingSearch);
|
||
|
||
// Store the cleaned search text in hidden field
|
||
document.getElementById('remainingSearchText').value = remainingSearch;
|
||
}
|
||
|
||
// Load product data on page load
|
||
async function loadProductData() {
|
||
try {
|
||
const response = await fetch('/quiz/data/products.json');
|
||
productData = await response.json();
|
||
console.log('Loaded', productData.length, 'products');
|
||
} catch (error) {
|
||
console.error('Error loading product data:', error);
|
||
document.getElementById('searchResults').innerHTML = `
|
||
<div class="no-results">
|
||
<p style="color: #d32f2f;">⚠️ Error loading product data. Please refresh the page.</p>
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
// Normalize text for matching (remove spaces, hyphens, special chars, lowercase)
|
||
function normalizeText(text) {
|
||
if (!text) return '';
|
||
return text.toString().toLowerCase()
|
||
.replace(/[-\s_]/g, '') // Remove hyphens, spaces, underscores
|
||
.replace(/[^a-z0-9]/g, ''); // Remove other special characters
|
||
}
|
||
|
||
// Check if a value matches a search term (partial match)
|
||
function matchesTerm(value, term) {
|
||
if (!value) return false;
|
||
|
||
// For arrays (like colors, materials)
|
||
if (Array.isArray(value)) {
|
||
return value.some(item => normalizeText(item).includes(term));
|
||
}
|
||
|
||
// For objects (like subType)
|
||
if (typeof value === 'object' && value !== null) {
|
||
return Object.values(value).some(v =>
|
||
v && normalizeText(v).includes(term)
|
||
);
|
||
}
|
||
|
||
// For strings and numbers
|
||
return normalizeText(value).includes(term);
|
||
}
|
||
|
||
// Match product against filter option
|
||
function matchesFilterOption(product, option) {
|
||
const association = option.associations;
|
||
const field = association.productField;
|
||
const normalizedValue = association.normalizedValue;
|
||
const matchMethod = association.matchMethod;
|
||
|
||
// Get the product field value
|
||
let productValue;
|
||
if (field.includes('.')) {
|
||
const parts = field.split('.');
|
||
productValue = product[parts[0]]?.[parts[1]];
|
||
} else {
|
||
productValue = product[field];
|
||
}
|
||
|
||
if (!productValue) return false;
|
||
|
||
// Apply match method
|
||
switch (matchMethod) {
|
||
case 'exact':
|
||
return normalizeText(productValue) === normalizedValue;
|
||
case 'array_includes':
|
||
if (Array.isArray(productValue)) {
|
||
return productValue.some(v => normalizeText(v) === normalizedValue);
|
||
}
|
||
return normalizeText(productValue) === normalizedValue;
|
||
case 'contains':
|
||
return normalizeText(productValue).includes(normalizedValue);
|
||
default:
|
||
return normalizeText(productValue).includes(normalizedValue);
|
||
}
|
||
}
|
||
|
||
// Search products with new filter-based logic
|
||
function searchProducts(searchQuery, checkedFilters) {
|
||
if (!filterConfig) return [];
|
||
|
||
// Filter products
|
||
const results = productData.filter(product => {
|
||
// Step 1: Check filters (AND between groups, OR within group)
|
||
for (const groupId in checkedFilters) {
|
||
const selectedOptions = checkedFilters[groupId];
|
||
|
||
if (selectedOptions.length > 0) {
|
||
// Check if product matches ANY of the selected options (OR logic)
|
||
const matchesGroup = selectedOptions.some(option =>
|
||
matchesFilterOption(product, option)
|
||
);
|
||
|
||
if (!matchesGroup) return false; // AND logic between groups
|
||
}
|
||
}
|
||
|
||
// Step 2: Check search terms (AND logic)
|
||
if (searchQuery && searchQuery.trim()) {
|
||
const searchTerms = searchQuery.toLowerCase()
|
||
.split(/\s+/)
|
||
.filter(term => term.length > 0)
|
||
.map(term => normalizeText(term));
|
||
|
||
// Product must match ALL search terms
|
||
const matchesSearch = searchTerms.every(term => {
|
||
return (
|
||
matchesTerm(product.productCode, term) ||
|
||
matchesTerm(product.id, term) ||
|
||
matchesTerm(product.description, term) ||
|
||
matchesTerm(product.category, term) ||
|
||
matchesTerm(product.baseType, term) ||
|
||
matchesTerm(product.subType, term) ||
|
||
matchesTerm(product.materials, term) ||
|
||
matchesTerm(product.colors, term) ||
|
||
matchesTerm(product.location, term)
|
||
);
|
||
});
|
||
|
||
if (!matchesSearch) return false;
|
||
}
|
||
|
||
return true;
|
||
});
|
||
|
||
return results;
|
||
}
|
||
|
||
// Display search results
|
||
function displayResults(results, searchQuery) {
|
||
const resultsDiv = document.getElementById('searchResults');
|
||
|
||
if (results.length === 0) {
|
||
resultsDiv.innerHTML = `
|
||
<div class="no-results">
|
||
<h3 style="color: #333; margin-bottom: 10px;">No products found</h3>
|
||
<p>No products match "${searchQuery}"</p>
|
||
<p style="margin-top: 15px; font-size: 0.9em;">Try:</p>
|
||
<ul style="text-align: left; max-width: 400px; margin: 10px auto;">
|
||
<li>Fewer or more general terms</li>
|
||
<li>Different spellings (c1800, c-1800, c 1800)</li>
|
||
<li>Product categories (storm door, window, slider)</li>
|
||
<li>Materials (aluminum, vinyl) or colors (white, bronze)</li>
|
||
</ul>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
let html = `
|
||
<div class="search-stats">
|
||
Found <strong>${results.length}</strong> product${results.length !== 1 ? 's' : ''} matching "<strong>${searchQuery}</strong>"
|
||
</div>
|
||
<div class="products-grid">
|
||
`;
|
||
|
||
results.forEach(product => {
|
||
const prodCode = product.productCode || product.id;
|
||
const desc = product.description || 'No description';
|
||
const materials = product.materials && product.materials.length > 0
|
||
? product.materials.join(', ')
|
||
: 'N/A';
|
||
const colors = product.colors && product.colors.length > 0
|
||
? product.colors.join(', ')
|
||
: 'N/A';
|
||
const location = product.location || 'N/A';
|
||
const discontinued = product.discontinued ? ' <span style="color: #d32f2f;">(Discontinued)</span>' : '';
|
||
|
||
// Get subtype string
|
||
let subtypeStr = '';
|
||
if (product.subType) {
|
||
const doorType = product.subType.door;
|
||
const windowType = product.subType.window;
|
||
if (doorType) subtypeStr = doorType;
|
||
else if (windowType) subtypeStr = windowType;
|
||
}
|
||
|
||
html += `
|
||
<div class="product-card" onclick="viewProductDetail('${prodCode}')">
|
||
<h3>${desc}${discontinued}</h3>
|
||
<p style="color: #999; font-size: 0.95em; margin-top: 5px;"><strong>${prodCode}</strong></p>
|
||
<div style="font-size: 0.9em; color: #666; margin-top: 10px;">
|
||
${product.baseType ? `<strong>Type:</strong> ${product.baseType}<br>` : ''}
|
||
${subtypeStr ? `<strong>Subtype:</strong> ${subtypeStr}<br>` : ''}
|
||
<strong>Materials:</strong> ${materials}<br>
|
||
<strong>Colors:</strong> ${colors}<br>
|
||
<strong>Location:</strong> ${location}
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
html += '</div>';
|
||
resultsDiv.innerHTML = html;
|
||
}
|
||
|
||
// Perform search with new filter-based logic
|
||
function performSearch() {
|
||
const searchInput = document.getElementById('searchInput');
|
||
let searchQuery = searchInput.value.trim();
|
||
let remainingSearch;
|
||
|
||
// If search field has been manually modified, re-process everything
|
||
if (searchFieldDirty) {
|
||
console.log('=== Search field modified, re-processing ===');
|
||
|
||
// Clear all filters
|
||
document.querySelectorAll('.filter-option input[type="checkbox"]').forEach(cb => {
|
||
cb.checked = false;
|
||
});
|
||
|
||
// Re-run auto-check from the new search text
|
||
if (searchQuery) {
|
||
autoCheckFiltersFromQuery(searchQuery);
|
||
}
|
||
|
||
// Use the cleaned search from hidden field
|
||
remainingSearch = document.getElementById('remainingSearchText').value.trim();
|
||
|
||
// Reset the dirty flag
|
||
searchFieldDirty = false;
|
||
} else {
|
||
// Use the pre-calculated remaining search from hidden field
|
||
remainingSearch = document.getElementById('remainingSearchText').value.trim().toLowerCase();
|
||
console.log('=== Using stored remaining search ===');
|
||
}
|
||
|
||
// Get checked filters
|
||
const checkedFilters = getCheckedFilters();
|
||
|
||
// Check if we have any search criteria
|
||
const hasRemainingSearch = remainingSearch && remainingSearch.trim().length > 0;
|
||
const hasFilters = Object.values(checkedFilters).some(arr => arr.length > 0);
|
||
|
||
if (!hasRemainingSearch && !hasFilters) {
|
||
document.getElementById('searchResults').innerHTML = `
|
||
<div class="no-results">
|
||
<p>Please enter a search term or select filters</p>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
console.log('Remaining search terms:', remainingSearch);
|
||
console.log('Checked filters:', checkedFilters);
|
||
|
||
// Perform search
|
||
const results = searchProducts(remainingSearch, checkedFilters);
|
||
|
||
// Build display query string
|
||
const filterParts = [];
|
||
filterConfig.filterGroups.forEach(group => {
|
||
const checkedOptions = checkedFilters[group.id] || [];
|
||
if (checkedOptions.length > 0) {
|
||
const labels = checkedOptions.map(opt => opt.label).join(', ');
|
||
filterParts.push(`${group.label}: ${labels}`);
|
||
}
|
||
});
|
||
|
||
let displayQuery = remainingSearch || '';
|
||
if (filterParts.length > 0) {
|
||
if (displayQuery) {
|
||
displayQuery += ' | ' + filterParts.join(' | ');
|
||
} else {
|
||
displayQuery = filterParts.join(' | ');
|
||
}
|
||
}
|
||
|
||
displayResults(results, displayQuery || 'all filters');
|
||
}
|
||
|
||
// Handle Enter key in search input
|
||
function handleSearch(event) {
|
||
if (event.key === 'Enter') {
|
||
performSearch();
|
||
}
|
||
}
|
||
|
||
// Clear search
|
||
function clearSearch() {
|
||
document.getElementById('searchInput').value = '';
|
||
document.getElementById('searchResults').innerHTML = '';
|
||
|
||
// Uncheck all filters
|
||
document.querySelectorAll('.filter-checkbox input[type="checkbox"]').forEach(checkbox => {
|
||
checkbox.checked = false;
|
||
});
|
||
|
||
// Update filter displays
|
||
updateFilterDisplays();
|
||
|
||
document.getElementById('searchInput').focus();
|
||
}
|
||
|
||
// View product detail (redirect to product finder with product code)
|
||
function viewProductDetail(productCode) {
|
||
window.location.href = `/quiz/?p=${productCode}`;
|
||
}
|
||
|
||
// Load user session info
|
||
async function loadUserInfo() {
|
||
try {
|
||
const response = await fetch('/api/session');
|
||
const data = await response.json();
|
||
|
||
if (data.status === 'success') {
|
||
const locationNames = {
|
||
'LINDS': 'Lindsborg',
|
||
'IOLA': 'Iola',
|
||
'KC': 'Kansas City',
|
||
'BMD': 'BMD'
|
||
};
|
||
|
||
const currentLoc = locationNames[data.currentLocation] || data.currentLocation;
|
||
const canChangeLocation = data.accessibleLocations && data.accessibleLocations.length > 1;
|
||
|
||
let locationDisplay;
|
||
if (canChangeLocation) {
|
||
locationDisplay = `<a href="{{ url_for('auth.select_location_page') }}" style="color: #6a4c93; text-decoration: none; font-weight: 500; cursor: pointer;"
|
||
onmouseover="this.style.textDecoration='underline'"
|
||
onmouseout="this.style.textDecoration='none'"
|
||
title="Click to change location">📍 ${currentLoc}</a>`;
|
||
} else {
|
||
locationDisplay = `📍 ${currentLoc}`;
|
||
}
|
||
|
||
document.getElementById('userLocationInfo').innerHTML =
|
||
`👤 ${data.username} | ${locationDisplay}`;
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading user info:', error);
|
||
}
|
||
}
|
||
|
||
// Navigation functions
|
||
function goBack() {
|
||
window.history.back();
|
||
}
|
||
|
||
function goToFinder() {
|
||
window.location.href = '{{ url_for("products.quiz_index") }}';
|
||
}
|
||
|
||
// Initialize page
|
||
async function init() {
|
||
await loadUserInfo();
|
||
await loadProductData();
|
||
await loadFilterConfig();
|
||
|
||
// Generate filter UI from config
|
||
if (filterConfig) {
|
||
generateFiltersUI();
|
||
initializeFilterListeners();
|
||
}
|
||
|
||
// Mark search field as dirty when user manually types
|
||
document.getElementById('searchInput').addEventListener('input', () => {
|
||
searchFieldDirty = true;
|
||
console.log('Search field marked as dirty');
|
||
});
|
||
|
||
// Check if there's a search query in URL
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const queryParam = urlParams.get('q');
|
||
|
||
if (queryParam) {
|
||
// Populate search input with the query
|
||
const searchInput = document.getElementById('searchInput');
|
||
searchInput.value = queryParam;
|
||
|
||
// Auto-check filters that match the search query
|
||
autoCheckFiltersFromQuery(queryParam);
|
||
|
||
// Automatically perform the search
|
||
performSearch();
|
||
}
|
||
|
||
// Focus on search input
|
||
document.getElementById('searchInput').focus();
|
||
}
|
||
|
||
// Load when page loads
|
||
init();
|
||
</script>
|
||
</body>
|
||
</html>
|