Files
CGW-Quote-Builder/information/REQUIRED_CODE_CHANGES.md
T

24 KiB

Conditional Navigation and Filtering Status

Purpose

This document records what was originally proposed for conditional question flow, what is already implemented, and what still remains. // Load all data files Promise.all([ fetch('data/navigation.json').then(r => r.json()), fetch('data/products.json').then(r => r.json()), fetch('data/accessories.json').then(r => r.json()), fetch('data/product_bitwise.json').then(r => r.json()) ]) .then(([questions, products, accessories, bitwise]) => { questionData = questions; productData = products; accessoryData = accessories;

    // Index bitwise data by product code
    bitwiseData = {};
    bitwise.forEach(item => {
        bitwiseData[item.PROD_CODE] = item.BIT_VALUE;
    });
    
    // Check if there's state in URL
    initFromURL();
    
    // If no URL state, start normally
    if (!window.location.search) {
        loadContent('start');
    }
})
.catch(error => {
    console.error('Error loading data:', error);
    // Show error message
});

}


#### B. Add Dynamic Next Resolution Function
```javascript
// Add this new function to handle conditional navigation
function resolveNextQuestion(currentKey, selectedAnswer, answerObject) {
    // Get the current accumulated filters
    const currentFilters = buildFilterFromAnswers();
    
    // Add the new filter from this answer
    if (answerObject.filter) {
        Object.assign(currentFilters, answerObject.filter);
    }
    
    // Check if next question in chain should be shown
    const nextKey = answerObject.next;
    const nextQuestion = questionData[nextKey];
    
    // If next question has conditional flag, evaluate it
    if (nextQuestion && nextQuestion.conditional) {
        return evaluateConditional(nextQuestion, currentFilters);
    }
    
    return nextKey;
}

// Evaluate if a conditional question should be shown
function evaluateConditional(question, filters) {
    if (question.conditional.type === 'material') {
        // Check if products matching current filters have multiple materials
        const matchingProducts = filterProducts(filters);
        const materials = getAvailableMaterials(matchingProducts);
        
        if (materials.length <= 1) {
            // Skip this question, auto-apply material if exists
            if (materials.length === 1) {
                userAnswers[question.id + '.material'] = materials[0];
            }
            // Return the fallback next
            return question.conditional.fallbackNext || question.next;
        }
    }
    
    if (question.conditional.type === 'color') {
        // Similar logic for colors
        const matchingProducts = filterProducts(filters);
        const colors = getAvailableColors(matchingProducts);
        
        if (colors.length <= 1) {
            if (colors.length === 1) {
                userAnswers[question.id + '.color'] = colors[0];
            }
            return question.conditional.fallbackNext || question.next;
        }
    }
    
    // Show the question
    return question.id;
}

// Build current filter object from user answers
function buildFilterFromAnswers() {
    const filters = {};
    
    // Extract filter info from answers
    for (const [key, value] of Object.entries(userAnswers)) {
        if (key === 'start') {
            filters.baseType = value;
        } else if (key.includes('type')) {
            filters.subType = value;
        } else if (key.includes('material')) {
            filters.material = value;
        } else if (key.includes('color')) {
            filters.color = value;
        }
    }
    
    return filters;
}

// Filter products based on criteria
function filterProducts(filters) {
    return productData.filter(product => {
        if (filters.baseType && product.baseType !== filters.baseType) {
            return false;
        }
        if (filters.subType) {
            const subType = product.subType.door || product.subType.window;
            if (subType !== filters.subType) {
                return false;
            }
        }
        if (filters.material && !product.materials.includes(filters.material)) {
            return false;
        }
        if (filters.color && !product.colors.includes(filters.color)) {
            return false;
        }
        return true;
    });
}

// Get available materials from product set
function getAvailableMaterials(products) {
    const materials = new Set();
    products.forEach(p => {
        p.materials.forEach(m => materials.add(m));
    });
    return Array.from(materials);
}

// Get available colors from product set
function getAvailableColors(products) {
    const colors = new Set();
    products.forEach(p => {
        p.colors.forEach(c => colors.add(c));
    });
    return Array.from(colors);
}

C. Update handleAnswer Function

// Replace the existing handleAnswer function
function handleAnswer(currentKey, answerValue, answerIndex) {
    const currentQuestion = questionData[currentKey];
    const answerObject = currentQuestion.answers[answerIndex];
    
    // Store the answer
    userAnswers[currentKey] = answerValue;
    
    // Update bit value based on selection
    updateBitValue(answerObject);
    
    // Resolve the next question (handles conditionals)
    const nextKey = resolveNextQuestion(currentKey, answerValue, answerObject);
    
    history.push(nextKey);
    loadContent(nextKey);
}

D. Update renderQuestion to Pass Answer Index

// In renderQuestion function, change the button onclick
// FROM:
// onclick="handleAnswer('${currentKey}', '${answer.caption}', '${answer.next}')"

// TO:
data.answers.forEach((answer, index) => {
    html += `
        <button class="answer-button" onclick="handleAnswer('${currentKey}', '${answer.caption}', ${index})">
            <div class="answer-image">${answer.image}</div>
            <div class="answer-caption">${answer.caption}</div>
        </button>
    `;
});

D2. Add URL State Management

// Track accumulated bit value from user selections
let accumulatedBitValue = 0;

// Load bitwise helper data
let bitwiseData = {};

function loadBitwiseData() {
    return fetch('data/product_bitwise.json')
        .then(r => r.json())
        .then(data => {
            bitwiseData = {};
            data.forEach(item => {
                bitwiseData[item.PROD_CODE] = item.BIT_VALUE;
            });
            return bitwiseData;
        });
}

// Initialize from URL on page load
function initFromURL() {
    const params = new URLSearchParams(window.location.search);
    
    // Check for bit value in URL
    const bitValue = params.get('b');
    if (bitValue) {
        accumulatedBitValue = parseInt(bitValue, 10);
        // Restore state based on bit value
        restoreStateFromBitValue(accumulatedBitValue);
    }
    
    // Check for product code in URL
    const productCode = params.get('p');
    if (productCode) {
        showProductByCode(productCode);
    }
}

// Update URL with current state
function updateURL() {
    const params = new URLSearchParams();
    
    if (accumulatedBitValue > 0) {
        params.set('b', accumulatedBitValue);
    }
    
    // Get current question
    if (history.length > 0) {
        const currentKey = history[history.length - 1];
        params.set('q', currentKey);
    }
    
    // Update URL without reloading page
    const newURL = window.location.pathname + '?' + params.toString();
    window.history.replaceState({ bitValue: accumulatedBitValue }, '', newURL);
}

// Update bit value based on user selection
function updateBitValue(answerObject) {
    if (answerObject.filter) {
        // Add bits based on filter
        if (answerObject.filter.baseType === 'Door') {
            accumulatedBitValue |= 1;  // Bit 0
        } else if (answerObject.filter.baseType === 'Window') {
            accumulatedBitValue |= 2;  // Bit 1
        }
        
        if (answerObject.filter.material === 'Aluminum') {
            accumulatedBitValue |= 16;  // Bit 4
        } else if (answerObject.filter.material === 'Vinyl') {
            accumulatedBitValue |= 32;  // Bit 5
        }
        
        // Add color bits
        const colorBits = {
            'Black': 64,
            'White': 128,
            'Bronze': 256,
            'Tan': 512,
            'Mill': 1024,
            'Sandstone': 2048
        };
        
        if (answerObject.filter.color && colorBits[answerObject.filter.color]) {
            accumulatedBitValue |= colorBits[answerObject.filter.color];
        }
        
        // Add subtype bits (based on bitwise_legend.json)
        const subtypeBits = {
            'Patio Door': 4096,
            'Primary Window': 8192,
            'Storm Door': 16384,
            'Storm Window': 32768
        };
        
        if (answerObject.filter.subType && subtypeBits[answerObject.filter.subType]) {
            accumulatedBitValue |= subtypeBits[answerObject.filter.subType];
        }
    }
    
    updateURL();
}

// Restore state from bit value
function restoreStateFromBitValue(bitValue) {
    // Decode bit value back to user selections
    const selections = {};
    
    if (bitValue & 1) selections.baseType = 'Door';
    if (bitValue & 2) selections.baseType = 'Window';
    
    if (bitValue & 16) selections.material = 'Aluminum';
    if (bitValue & 32) selections.material = 'Vinyl';
    
    const colors = ['Black', 'White', 'Bronze', 'Tan', 'Mill', 'Sandstone'];
    const colorBits = [64, 128, 256, 512, 1024, 2048];
    
    colors.forEach((color, idx) => {
        if (bitValue & colorBits[idx]) {
            selections.color = color;
        }
    });
    
    // Store in userAnswers
    if (selections.baseType) userAnswers['start'] = selections.baseType;
    if (selections.material) userAnswers['q-material.material'] = selections.material;
    if (selections.color) userAnswers['q-color.color'] = selections.color;
    
    // Navigate to appropriate question from URL param
    const params = new URLSearchParams(window.location.search);
    const questionKey = params.get('q') || 'start';
    loadContent(questionKey);
}

// Share current state
function shareCurrentState() {
    const url = window.location.href;
    
    // Copy to clipboard
    if (navigator.clipboard) {
        navigator.clipboard.writeText(url).then(() => {
            alert('Link copied to clipboard! Share this link to return to this exact state.');
        });
    } else {
        // Fallback: show URL
        prompt('Copy this URL to share:', url);
    }
}

// Add share button to breadcrumb
function updateBreadcrumbWithShare() {
    const breadcrumbDiv = document.getElementById('breadcrumb');
    const shareButton = `
        <button onclick="shareCurrentState()" 
                style="float: right; padding: 5px 10px; cursor: pointer;">
            🔗 Share
        </button>
    `;
    breadcrumbDiv.innerHTML += shareButton;
}

E. Add Product Results Page

// Add new function to show filtered products
function showProductResults(filters) {
    const matchingProducts = filterProducts(filters);
    
    const contentDiv = document.getElementById('content');
    
    if (matchingProducts.length === 0) {
        contentDiv.innerHTML = `
            <div class="result-container">
                <div class="result-title">No Products Found</div>
                <div class="result-details">
                    No products match your specifications. Please try different options.
                </div>
                <button class="back-button" onclick="goBack()">← Go Back</button>
            </div>
        `;
        return;
    }
    
    let html = `
        <div class="result-container">
            <div class="result-title">Found ${matchingProducts.length} Product(s)</div>
            <div class="products-grid">
    `;
    
    matchingProducts.forEach(product => {
        // Get compatible accessories
        const compatibleAccessories = accessoryData.filter(acc => 
            product.compatibleAccessories.includes(acc.id)
        );
        
        html += `
            <div class="product-card" onclick="showProductDetail('${product.productCode}')">
                <h3>${product.description}</h3>
                <p><strong>Code:</strong> ${product.productCode}</p>
                <p><strong>Materials:</strong> ${product.materials.join(', ')}</p>
                <p><strong>Colors:</strong> ${product.colors.join(', ')}</p>
                ${compatibleAccessories.length > 0 ? `
                    <p><strong>Available Options:</strong></p>
                    <ul>
                        ${compatibleAccessories.map(acc => 
                            `<li>${acc.description}</li>`
                        ).join('')}
                    </ul>
                ` : ''}
            </div>
        `;
    });
    
    html += `
            </div>
            <div class="result-actions">
                <button class="back-button" onclick="goBack()">← Go Back</button>
                <button class="back-button" onclick="startOver()">Start Over</button>
                <button class="back-button" onclick="shareCurrentState()">🔗 Share Results</button>
            </div>
        </div>
    `;
    
    contentDiv.innerHTML = html;
}

// Show individual product detail with URL update
function showProductDetail(productCode) {
    const product = productData.find(p => p.productCode === productCode);
    if (!product) return;
    
    // Update URL with product code
    const params = new URLSearchParams();
    params.set('p', productCode);
    if (accumulatedBitValue > 0) {
        params.set('b', accumulatedBitValue);
    }
    window.history.pushState({ productCode }, '', '?' + params.toString());
    
    // Get compatible accessories
    const compatibleAccessories = accessoryData.filter(acc => 
        product.compatibleAccessories.includes(acc.id)
    );
    
    const contentDiv = document.getElementById('content');
    let html = `
        <div class="result-container">
            <div class="result-title">${product.description}</div>
            <div class="result-content">
                <div class="result-details">
                    <p><strong>Product Code:</strong> ${product.productCode}</p>
                    <p><strong>Category:</strong> ${product.category}</p>
                    <p><strong>Base Type:</strong> ${product.baseType || 'N/A'}</p>
                    <p><strong>Materials:</strong> ${product.materials.join(', ') || 'N/A'}</p>
                    <p><strong>Colors:</strong> ${product.colors.join(', ') || 'N/A'}</p>
                    
                    ${compatibleAccessories.length > 0 ? `
                        <h3 style="margin-top: 20px;">Compatible Accessories</h3>
                        <div class="accessories-list">
                            ${compatibleAccessories.map(acc => `
                                <div class="accessory-item">
                                    <strong>${acc.description}</strong><br>
                                    <small>Code: ${acc.accessoryCode}</small><br>
                                    <small>Materials: ${acc.materials.join(', ')}</small>
                                </div>
                            `).join('')}
                        </div>
                    ` : ''}
                </div>
            </div>
            <div class="result-actions">
                <button class="back-button" onclick="goBack()">← Back to Results</button>
                <button class="back-button" onclick="startOver()">Start Over</button>
                <button class="back-button" onclick="shareCurrentState()">🔗 Share Product</button>
            </div>
        </div>
    `;
    
    contentDiv.innerHTML = html;
}

// Show product by code (from URL)
function showProductByCode(productCode) {
    const product = productData.find(p => p.productCode === productCode);
    if (product) {
        showProductDetail(productCode);
    }
}

2. JSON Structure Changes (data/navigation.json)

Add Conditional Metadata to Questions

Questions that might be skipped need a conditional property:

{
  "q-material-storm-door": {
    "type": "question",
    "inputType": "button",
    "title": "Select Material",
    "subtitle": "Choose your preferred material",
    "conditional": {
      "type": "material",
      "fallbackNext": "q-dimensions",
      "autoApply": true
    },
    "answers": [
      {
        "caption": "Aluminum",
        "next": "q-color",
        "filter": { "material": "Aluminum" }
      },
      {
        "caption": "Vinyl",
        "next": "q-color",
        "filter": { "material": "Vinyl" }
      }
    ]
  }
}

Add Filter Properties to Answers

Each answer should include filter criteria:

{
  "caption": "Storm Door",
  "image": "🚪",
  "next": "q-material-storm-door",
  "filter": {
    "baseType": "Door",
    "subType": "Storm Door"
  }
}

3. CSS Changes (css/styles.css)

Add styles for product grid:

.products-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
    gap: 20px;
    margin: 20px 0;
    padding: 20px;
}

.product-card {
    background: white;
    border: 1px solid #ddd;
    border-radius: 8px;
    padding: 20px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

.product-card h3 {
    margin-top: 0;
    color: #333;
    font-size: 1.1em;
}

.product-card p {
    margin: 10px 0;
    line-height: 1.6;
}

.product-card ul {
    list-style-position: inside;
    padding-left: 0;
}

4. Python Changes (OPTIONAL)

If you want server-side filtering:

# Add to app.py
import json
import csv

@app.route('/api/filter-products', methods=['POST'])
def filter_products():
    """Filter products based on criteria"""
    filters = request.json
    
    # Load products
    with open('data/products.json', 'r') as f:
        products = json.load(f)
    
    # Apply filters
    filtered = []
    for product in products:
        if filters.get('baseType') and product['baseType'] != filters['baseType']:
            continue
        if filters.get('subType'):
            sub = product['subType'].get('door') or product['subType'].get('window')
            if sub != filters['subType']:
                continue
        if filters.get('material') and filters['material'] not in product['materials']:
            continue
        if filters.get('color') and filters['color'] not in product['colors']:
            continue
        
        filtered.append(product)
    
    return jsonify({
        'status': 'success',
        'count': len(filtered),
        'products': filtered
    })

5. CSV Parsing Script Changes

Create a Python script to generate the JSON files:

# create_json_files.py
import csv
import json
from collections import defaultdict

def parse_products_csv(csv_path):
    """Parse products.csv and generate three JSON files"""
    
    products = []
    accessories = []
    nav_data = {
        "start": {
            "type": "question",
            "inputType": "button",
            "title": "What are you looking for?",
            "subtitle": "Select the product category",
            "answers": []
        }
    }
    
    # Track unique values for navigation
    base_types = set()
    door_subtypes = defaultdict(int)  # Track material diversity per subtype
    window_subtypes = defaultdict(int)
    
    with open(csv_path, 'r', encoding='utf-8') as f:
        reader = csv.DictReader(f, skipinitialspace=True)
        
        for row in reader:
            # Skip discontinued
            if row['DISCONT'].upper() == 'TRUE':
                continue
            
            # Parse materials
            materials = []
            if row['Aluminum'].upper() == 'TRUE':
                materials.append('Aluminum')
            if row['Vinyl'].upper() == 'TRUE':
                materials.append('Vinyl')
            
            # Parse colors
            colors = []
            for color in ['Black', 'White', 'Bronze', 'Tan', 'Mill', 'Sandstone']:
                if row.get(color, '').upper() == 'TRUE':
                    colors.append(color)
            
            # Determine if accessory
            is_accessory = row['Accessory\\nYes'].upper() == 'TRUE'
            
            # Build sub-type object
            sub_type = {
                "door": row['Sub-type\\nDoor'].strip() if row['Sub-type\\nDoor'] else None,
                "window": row['Sub-type\\nWindow'].strip() if row['Sub-type\\nWindow'] else None
            }
            
            # Common data
            item_data = {
                "id": row['PROD_CODE'],
                "productCode": row['PROD_CODE'],
                "category": row['CATEGORY'],
                "description": row['DESCRIPTION'],
                "discontinued": False,
                "location": row['LOC_CODE'],
                "baseType": row['Base\\nType'].strip() if row['Base\\nType'] else None,
                "subType": sub_type,
                "materials": materials,
                "colors": colors
            }
            
            if is_accessory:
                # Add to accessories
                accessories.append({
                    **item_data,
                    "compatibilityRules": {
                        "type": "subType",
                        "subTypeDoor": sub_type['door'],
                        "subTypeWindow": sub_type['window'],
                        # ... more rules
                    }
                })
            else:
                # Add to products
                products.append({
                    **item_data,
                    "isAccessory": False,
                    "compatibleAccessories": []
                })
                
                # Track for navigation
                if item_data['baseType']:
                    base_types.add(item_data['baseType'])
    
    # Build navigation structure
    # ... (logic to create navigation.json based on analysis)
    
    # Write JSON files
    with open('data/products.json', 'w') as f:
        json.dump(products, f, indent=2)
    
    with open('data/accessories.json', 'w') as f:
        json.dump(accessories, f, indent=2)
    
    with open('data/navigation.json', 'w') as f:
        json.dump(nav_data, f, indent=2)

if __name__ == '__main__':
    parse_products_csv('data/products.csv')
    print("JSON files generated successfully!")

Implementation Checklist

  • Update js/script.js with conditional navigation logic
  • Create parsing script to generate JSON files from CSV
  • Run parsing script to create navigation.json, products.json, accessories.json
  • Update CSS for product display
  • Test navigation flow with different product types
  • Verify material questions are skipped when appropriate
  • Verify color questions are skipped when appropriate
  • Test product filtering and results display
  • Add accessory display on product pages

Testing Scenarios

  1. Storm Door Selection: Should skip material question (all Aluminum)
  2. Window Selection: Should show material question (mixed materials)
  3. Single Color Products: Should skip color question
  4. Multi-Color Products: Should show color question
  5. Product Results: Should show filtered products with compatible accessories

Summary

CSV: No changes needed (unless adding bit values) JavaScript: Major refactoring for conditional logic JSON: New structure with filter objects and conditional flags Python: Optional parsing script to generate JSON from CSV