Files
CGW-Quote-Builder/app/parse_csv_to_json.py
T
2026-04-11 00:04:09 -05:00

653 lines
28 KiB
Python

"""
CSV to JSON Parser - Generate Navigation, Products, and Accessories JSON Files
Based on AI_PARSING_INSTRUCTIONS.md
This script reads data/products.csv and generates:
1. data/navigation.json - Question flow with conditional logic
2. data/products.json - Product catalog
3. data/accessories.json - Accessory options with compatibility rules
Run: python parse_csv_to_json.py
"""
import csv
import json
from collections import defaultdict
from typing import Dict, List, Set, Tuple
def parse_bool(value):
"""Parse TRUE/FALSE string to boolean"""
if isinstance(value, str):
return value.strip().upper() == 'TRUE'
return bool(value)
def clean_string(value):
"""Clean and strip string values"""
if isinstance(value, str):
return value.strip()
return value or ""
class CSVParser:
def __init__(self, csv_path: str):
self.csv_path = csv_path
self.products = []
self.accessories = []
self.base_types = set()
self.door_subtypes = defaultdict(list) # subtype -> [products]
self.window_subtypes = defaultdict(list)
self.material_counts = defaultdict(lambda: {'aluminum': 0, 'vinyl': 0})
self.color_counts = defaultdict(lambda: defaultdict(int)) # subtype -> {color: count}
self.color_by_material = defaultdict(lambda: defaultdict(set)) # subtype -> {material: {colors}}
self.detailed_products = {} # Load from questions.json
def load_detailed_product_info(self):
"""Load detailed product info from questions.json"""
try:
# Load the code mapping
try:
with open('data/product_code_mapping.json', 'r', encoding='utf-8') as f:
code_mapping = json.load(f)
except FileNotFoundError:
code_mapping = {}
with open('data/questions.json', 'r', encoding='utf-8') as f:
questions_data = json.load(f)
# Extract all product nodes
for key, value in questions_data.items():
if key.startswith('prod-') and value.get('type') == 'product':
code = value.get('code')
if code:
detailed_info = {
'title': value.get('title'),
'detailedDescription': value.get('description'),
'features': value.get('features', []),
'image': value.get('image'),
'url': value.get('url'),
'brochure': value.get('brochure'),
'measureGuide': value.get('measureGuide')
}
# Store under the simplified code
self.detailed_products[code] = detailed_info
# Also store under all mapped codes
for csv_code, simple_code in code_mapping.items():
if simple_code == code:
self.detailed_products[csv_code] = detailed_info
print(f"✓ Loaded {len(self.detailed_products)} detailed product descriptions")
except FileNotFoundError:
print("⚠ questions.json not found - skipping detailed product info")
except Exception as e:
print(f"⚠ Error loading product details: {e}")
def parse_csv(self):
"""Parse the CSV file and separate products from accessories"""
print("📖 Reading CSV file...")
with open(self.csv_path, 'r', encoding='utf-8', errors='ignore') as f:
reader = csv.reader(f)
# Skip first two header rows
next(reader) # Row 1: Category headers
header_row = next(reader) # Row 2: Column names
row_count = 0
for row in reader:
if len(row) < 11: # Need at least up to column K
continue
row_count += 1
# Skip discontinued items
if parse_bool(row[0]): # DISCONT column
continue
# Extract basic info
prod_code = clean_string(row[2]) # PROD_CODE
category = clean_string(row[3]) # CATEGORY
description = clean_string(row[5]) # DESCRIPTION
location = clean_string(row[1]) # LOC_CODE
base_type = clean_string(row[6]) if len(row) > 6 else "" # Base Type (G)
door_subtype = clean_string(row[7]) if len(row) > 7 else "" # Sub-type Door (H)
window_subtype = clean_string(row[8]) if len(row) > 8 else "" # Sub-type Window (I)
is_accessory = parse_bool(row[9]) if len(row) > 9 else False # Accessory Yes (J)
specific_item = parse_bool(row[10]) if len(row) > 10 else False # This Item (K)
# Parse materials (L, M)
materials = []
if len(row) > 11 and parse_bool(row[11]):
materials.append('Aluminum')
if len(row) > 12 and parse_bool(row[12]):
materials.append('Vinyl')
# Parse colors (N through S+)
colors = []
color_columns = [
(13, 'Black'), (14, 'White'), (15, 'Bronze'),
(16, 'Tan'), (17, 'Mill'), (18, 'Sandstone')
]
for col_idx, color_name in color_columns:
if len(row) > col_idx and parse_bool(row[col_idx]):
colors.append(color_name)
# Build item data
item_data = {
'id': prod_code,
'productCode': prod_code,
'category': category,
'description': description,
'discontinued': False,
'location': location,
'baseType': base_type,
'subType': {
'door': door_subtype if door_subtype else None,
'window': window_subtype if window_subtype else None
},
'materials': materials,
'colors': colors
}
# Separate accessories from products
if is_accessory:
# Build accessory-specific data
accessory = {
**item_data,
'accessoryCode': prod_code,
'compatibilityRules': self._build_compatibility_rules(
door_subtype, window_subtype, category, specific_item
),
'optionType': self._infer_option_type(description),
'metadata': {
'specificItemLink': specific_item
}
}
self.accessories.append(accessory)
else:
# Build product-specific data
product = {
**item_data,
'isAccessory': False,
'compatibleAccessories': [] # Will be populated later
}
# Merge detailed info if available
if prod_code in self.detailed_products:
detailed = self.detailed_products[prod_code]
product.update({
'title': detailed['title'],
'detailedDescription': detailed['detailedDescription'],
'features': detailed['features'],
'image': detailed['image'],
'url': detailed['url'],
'brochure': detailed.get('brochure'),
'measureGuide': detailed.get('measureGuide')
})
self.products.append(product)
# Track for navigation building
if base_type:
self.base_types.add(base_type)
# Track subtypes per type
if door_subtype:
self.door_subtypes[door_subtype].append(product)
if window_subtype:
self.window_subtypes[window_subtype].append(product)
# Count materials per subtype
subtype_key = door_subtype or window_subtype or "none"
if 'Aluminum' in materials:
self.material_counts[subtype_key]['aluminum'] += 1
# Track colors per material
for color in colors:
self.color_by_material[subtype_key]['Aluminum'].add(color)
if 'Vinyl' in materials:
self.material_counts[subtype_key]['vinyl'] += 1
# Track colors per material
for color in colors:
self.color_by_material[subtype_key]['Vinyl'].add(color)
# Count colors per subtype (overall)
for color in colors:
self.color_counts[subtype_key][color] += 1
print(f"✓ Parsed {row_count} rows")
print(f"✓ Found {len(self.products)} products")
print(f"✓ Found {len(self.accessories)} accessories")
def _build_compatibility_rules(self, door_subtype, window_subtype, category, specific_item):
"""Build compatibility rules for accessories"""
rules = {
'type': 'category', # Default
'subTypeDoor': door_subtype if door_subtype else None,
'subTypeWindow': window_subtype if window_subtype else None,
'categories': [category] if category else [],
'specificProducts': [],
'requiresMatch': {
'material': True, # Usually accessories need matching material
'color': False # Colors usually don't need to match
}
}
# Determine rule type
if specific_item:
rules['type'] = 'specific'
elif door_subtype or window_subtype:
rules['type'] = 'subType'
return rules
def _infer_option_type(self, description):
"""Infer option type from description"""
desc_upper = description.upper()
if 'HANDLE' in desc_upper:
return 'handle'
elif 'INSERT' in desc_upper or 'GLASS' in desc_upper:
return 'insert'
elif 'HARDWARE' in desc_upper:
return 'hardware'
elif 'SCREEN' in desc_upper:
return 'screen'
else:
return 'option'
def link_accessories_to_products(self):
"""Link compatible accessories to products"""
print("🔗 Linking accessories to products...")
linked_count = 0
for product in self.products:
compatible = []
for accessory in self.accessories:
rules = accessory['compatibilityRules']
# Check by subtype
if rules['type'] == 'subType':
prod_door = product['subType']['door']
prod_window = product['subType']['window']
acc_door = rules['subTypeDoor']
acc_window = rules['subTypeWindow']
if (prod_door and prod_door == acc_door) or \
(prod_window and prod_window == acc_window):
# Check material compatibility if required
if rules['requiresMatch']['material']:
# Check if they share any materials
if any(m in product['materials'] for m in accessory['materials']):
compatible.append(accessory['id'])
linked_count += 1
else:
compatible.append(accessory['id'])
linked_count += 1
# Check by category
elif rules['type'] == 'category':
if product['category'] in rules['categories']:
compatible.append(accessory['id'])
linked_count += 1
product['compatibleAccessories'] = compatible
print(f"✓ Created {linked_count} product-accessory links")
def build_navigation(self):
"""Build navigation JSON with conditional logic"""
print("🗺️ Building navigation flow...")
navigation = {}
# Question 1: Base Type Selection
navigation['start'] = {
'type': 'question',
'inputType': 'button',
'title': 'What are you looking for?',
'subtitle': 'Select the product category',
'answers': []
}
# Add base type options
sorted_types = sorted(self.base_types)
for base_type in sorted_types:
emoji = '🚪' if 'door' in base_type.lower() else '🪟'
next_key = f'q-{base_type.lower()}-type'
navigation['start']['answers'].append({
'caption': base_type,
'image': emoji,
'next': next_key,
'filter': {
'baseType': base_type
}
})
# Question 2: Sub-type Selection for Doors
if self.door_subtypes:
door_subtypes = sorted(self.door_subtypes.keys())
navigation['q-door-type'] = {
'type': 'question',
'inputType': 'button',
'title': 'What type of door?',
'subtitle': 'Select the door category',
'answers': []
}
for subtype in door_subtypes:
products = self.door_subtypes[subtype]
# Check if material question is needed
needs_material = self._needs_material_question(subtype)
# Determine next question
if needs_material:
next_key = f'q-material-{subtype.lower().replace(" ", "-")}'
else:
# No material choice needed - determine single material and check colors
single_material = 'Aluminum' if self.material_counts[subtype]['aluminum'] > 0 else 'Vinyl'
needs_color = self._needs_color_question(subtype, single_material)
next_key = f'q-color-{subtype.lower().replace(" ", "-")}-{single_material.lower()}' if needs_color else 'q-dimensions'
# Auto-apply material if only one
filter_obj = {
'baseType': 'Door',
'subType': subtype
}
if not needs_material:
# Auto-apply the single material
if self.material_counts[subtype]['aluminum'] > 0:
filter_obj['material'] = 'Aluminum'
elif self.material_counts[subtype]['vinyl'] > 0:
filter_obj['material'] = 'Vinyl'
# If no color question either, auto-apply single color
single_material = filter_obj['material']
if not self._needs_color_question(subtype, single_material):
colors = list(self.color_by_material[subtype][single_material])
if len(colors) == 1:
filter_obj['color'] = colors[0]
navigation['q-door-type']['answers'].append({
'caption': subtype,
'image': '🚪',
'next': next_key,
'filter': filter_obj
})
# Create material question if needed
if needs_material:
self._create_material_question(navigation, subtype, 'Door')
# Create color question if material skipped but color needed
elif next_key.startswith('q-color-'):
single_material = 'Aluminum' if self.material_counts[subtype]['aluminum'] > 0 else 'Vinyl'
self._create_color_question(navigation, subtype, single_material, 'Door')
# Question 2: Sub-type Selection for Windows
if self.window_subtypes:
window_subtypes = sorted(self.window_subtypes.keys())
navigation['q-window-type'] = {
'type': 'question',
'inputType': 'button',
'title': 'What type of window?',
'subtitle': 'Select the window category',
'answers': []
}
for subtype in window_subtypes:
needs_material = self._needs_material_question(subtype)
# Determine next question
if needs_material:
next_key = f'q-material-{subtype.lower().replace(" ", "-")}'
else:
# No material choice needed - determine single material and check colors
single_material = 'Aluminum' if self.material_counts[subtype]['aluminum'] > 0 else 'Vinyl'
needs_color = self._needs_color_question(subtype, single_material)
next_key = f'q-color-{subtype.lower().replace(" ", "-")}-{single_material.lower()}' if needs_color else 'q-dimensions'
filter_obj = {
'baseType': 'Window',
'subType': subtype
}
if not needs_material:
if self.material_counts[subtype]['aluminum'] > 0:
filter_obj['material'] = 'Aluminum'
elif self.material_counts[subtype]['vinyl'] > 0:
filter_obj['material'] = 'Vinyl'
# If no color question either, auto-apply single color
single_material = filter_obj['material']
if not self._needs_color_question(subtype, single_material):
colors = list(self.color_by_material[subtype][single_material])
if len(colors) == 1:
filter_obj['color'] = colors[0]
navigation['q-window-type']['answers'].append({
'caption': subtype,
'image': '🪟',
'next': next_key,
'filter': filter_obj
})
if needs_material:
self._create_material_question(navigation, subtype, 'Window')
# Create color question if material skipped but color needed
elif next_key.startswith('q-color-'):
single_material = 'Aluminum' if self.material_counts[subtype]['aluminum'] > 0 else 'Vinyl'
self._create_color_question(navigation, subtype, single_material, 'Window')
# Final Question: Dimensions
navigation['q-dimensions'] = {
'type': 'question',
'inputType': 'form',
'title': 'Enter Product Dimensions',
'subtitle': 'Please provide the measurements',
'fields': [
{
'name': 'width',
'label': 'Width (inches)',
'type': 'number',
'required': True,
'placeholder': 'e.g., 36'
},
{
'name': 'height',
'label': 'Height (inches)',
'type': 'number',
'required': True,
'placeholder': 'e.g., 80'
}
],
'next': 'results'
}
print(f"✓ Created {len(navigation)} navigation nodes")
return navigation
def _needs_material_question(self, subtype):
"""Check if a subtype needs a material selection question"""
aluminum = self.material_counts[subtype]['aluminum']
vinyl = self.material_counts[subtype]['vinyl']
# Show material question if both materials exist
return aluminum > 0 and vinyl > 0
def _create_material_question(self, navigation, subtype, base_type):
"""Create a material selection question"""
question_key = f'q-material-{subtype.lower().replace(" ", "-")}'
navigation[question_key] = {
'type': 'question',
'inputType': 'button',
'title': 'Select Material',
'subtitle': f'Choose your preferred material for {subtype}',
'conditional': {
'type': 'material',
'fallbackNext': 'q-dimensions'
},
'answers': []
}
# Add material options
if self.material_counts[subtype]['aluminum'] > 0:
# Check if color question needed after aluminum selection
needs_color = self._needs_color_question(subtype, 'Aluminum')
next_key_al = f'q-color-{subtype.lower().replace(" ", "-")}-aluminum' if needs_color else 'q-dimensions'
navigation[question_key]['answers'].append({
'caption': 'Aluminum',
'image': '🔩',
'next': next_key_al,
'filter': {
'baseType': base_type,
'subType': subtype,
'material': 'Aluminum'
}
})
if self.material_counts[subtype]['vinyl'] > 0:
# Check if color question needed after vinyl selection
needs_color = self._needs_color_question(subtype, 'Vinyl')
next_key_vin = f'q-color-{subtype.lower().replace(" ", "-")}-vinyl' if needs_color else 'q-dimensions'
navigation[question_key]['answers'].append({
'caption': 'Vinyl',
'image': '🪟',
'next': next_key_vin,
'filter': {
'baseType': base_type,
'subType': subtype,
'material': 'Vinyl'
}
})
# Create color questions for each material option
if self.material_counts[subtype]['aluminum'] > 0 and self._needs_color_question(subtype, 'Aluminum'):
self._create_color_question(navigation, subtype, 'Aluminum', base_type)
if self.material_counts[subtype]['vinyl'] > 0 and self._needs_color_question(subtype, 'Vinyl'):
self._create_color_question(navigation, subtype, 'Vinyl', base_type)
def _needs_color_question(self, subtype, material):
"""Check if a subtype+material combination needs a color selection question"""
colors = self.color_by_material[subtype][material]
# Show color question if multiple colors exist
return len(colors) > 1
def _create_color_question(self, navigation, subtype, material, base_type):
"""Create a color selection question"""
question_key = f'q-color-{subtype.lower().replace(" ", "-")}-{material.lower()}'
navigation[question_key] = {
'type': 'question',
'inputType': 'button',
'title': 'Select Color',
'subtitle': f'Choose your preferred color for {material} {subtype}',
'conditional': {
'type': 'color',
'fallbackNext': 'q-dimensions'
},
'answers': []
}
# Get available colors for this subtype+material
colors = sorted(self.color_by_material[subtype][material])
# Color emoji mapping
color_emojis = {
'Black': '',
'White': '',
'Bronze': '🟫',
'Tan': '🟤',
'Mill': '',
'Sandstone': '🟨'
}
for color in colors:
navigation[question_key]['answers'].append({
'caption': color,
'image': color_emojis.get(color, '🎨'),
'next': 'q-dimensions',
'filter': {
'baseType': base_type,
'subType': subtype,
'material': material,
'color': color
}
})
def save_json_files(self, navigation):
"""Save all three JSON files"""
print("💾 Saving JSON files...")
# Save products.json
with open('data/products.json', 'w', encoding='utf-8') as f:
json.dump(self.products, f, indent=2)
print(f"✓ Saved data/products.json ({len(self.products)} products)")
# Save accessories.json
with open('data/accessories.json', 'w', encoding='utf-8') as f:
json.dump(self.accessories, f, indent=2)
print(f"✓ Saved data/accessories.json ({len(self.accessories)} accessories)")
# Save navigation.json
with open('data/navigation.json', 'w', encoding='utf-8') as f:
json.dump(navigation, f, indent=2)
print(f"✓ Saved data/navigation.json ({len(navigation)} nodes)")
def print_statistics(self):
"""Print parsing statistics"""
print("\n" + "="*60)
print("PARSING STATISTICS")
print("="*60)
print(f"Total Products: {len(self.products)}")
print(f"Total Accessories: {len(self.accessories)}")
print(f"Base Types: {', '.join(sorted(self.base_types))}")
print(f"Door Subtypes: {len(self.door_subtypes)}")
print(f"Window Subtypes: {len(self.window_subtypes)}")
print()
print("Material Distribution:")
for subtype, counts in sorted(self.material_counts.items()):
if counts['aluminum'] > 0 or counts['vinyl'] > 0:
needs_q = "✓ Material Q" if (counts['aluminum'] > 0 and counts['vinyl'] > 0) else "✗ Skip"
print(f" {subtype:20} - Al:{counts['aluminum']:3} Vinyl:{counts['vinyl']:3} [{needs_q}]")
print("="*60)
def main():
csv_path = 'data/products.csv'
print("🚀 CSV to JSON Parser")
print("="*60)
try:
parser = CSVParser(csv_path)
parser.load_detailed_product_info()
parser.parse_csv()
parser.link_accessories_to_products()
navigation = parser.build_navigation()
parser.save_json_files(navigation)
parser.print_statistics()
print("\n✅ All JSON files generated successfully!")
print("\nNext steps:")
print("1. Review data/navigation.json for question flow")
print("2. Review data/products.json for product data")
print("3. Review data/accessories.json for accessory compatibility")
print("4. Test the application in your browser")
except FileNotFoundError:
print(f"❌ Error: Could not find {csv_path}")
print(" Make sure products.csv exists in the data/ folder")
except Exception as e:
print(f"❌ Error: {e}")
import traceback
traceback.print_exc()
if __name__ == '__main__':
main()