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

772 lines
24 KiB
Markdown

# 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
```javascript
// 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
```javascript
// 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
```javascript
// 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
```javascript
// 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:
```json
{
"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:
```json
{
"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:
```css
.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:
```python
# 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:
```python
# 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