Initial
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
# Apache configuration for Flask subdirectory deployment
|
||||
# Passenger configuration ONLY - minimal to avoid blocking issues
|
||||
|
||||
PassengerEnabled on
|
||||
PassengerAppRoot /home/bmdwtjuw/product-finder
|
||||
PassengerPython /home/bmdwtjuw/virtualenv/product-finder/3.13/bin/python3.13_bin
|
||||
SetEnv APPLICATION_ROOT /product-finder
|
||||
@@ -0,0 +1,4 @@
|
||||
# MINIMAL .htaccess for troubleshooting
|
||||
# If you're getting 403/404, try this simplified version first
|
||||
|
||||
PassengerEnabled on
|
||||
@@ -0,0 +1,98 @@
|
||||
# Admin Utilities
|
||||
|
||||
This folder contains administrative tools and utilities for maintaining the CGW Product Finder application.
|
||||
|
||||
## 🛠️ Available Tools
|
||||
|
||||
### fix_default_locations.py
|
||||
**Purpose**: Ensures all users have their default location marked as accessible.
|
||||
|
||||
**Usage**:
|
||||
```bash
|
||||
cd app/admin
|
||||
python fix_default_locations.py
|
||||
```
|
||||
|
||||
**What it does**:
|
||||
- Scans all users in `data/users.json`
|
||||
- Checks if each user's default location is marked as accessible
|
||||
- Automatically fixes any users where the default location is not accessible
|
||||
- Reports results for each user
|
||||
|
||||
**When to use**:
|
||||
- After manual edits to users.json
|
||||
- After importing users from backup
|
||||
- If users report they can't access their default location
|
||||
- As a maintenance check
|
||||
|
||||
### test_password_security.py
|
||||
**Purpose**: Demonstrates and tests the password hashing security implementation.
|
||||
|
||||
**Usage**:
|
||||
```bash
|
||||
cd app/admin
|
||||
python test_password_security.py
|
||||
```
|
||||
|
||||
**What it does**:
|
||||
- Demonstrates PBKDF2-SHA256 password hashing
|
||||
- Shows security features (iterations, salt, etc.)
|
||||
- Verifies password verification works correctly
|
||||
- Useful for understanding the security implementation
|
||||
|
||||
**When to use**:
|
||||
- To verify password hashing is working correctly
|
||||
- To demonstrate security to stakeholders
|
||||
- For educational purposes
|
||||
- When troubleshooting password-related issues
|
||||
|
||||
## 📋 Running Admin Tools
|
||||
|
||||
All admin tools should be run from within the `app/admin` directory to ensure correct file paths.
|
||||
|
||||
**Example**:
|
||||
```bash
|
||||
# Navigate to admin folder
|
||||
cd "C:\Users\Work\Desktop\CGW Product Finder\app\admin"
|
||||
|
||||
# Run a tool
|
||||
python fix_default_locations.py
|
||||
```
|
||||
|
||||
## ⚠️ Important Notes
|
||||
|
||||
- **Backup First**: Always backup `data/users.json` before running utilities that modify data
|
||||
- **Test Environment**: Test utilities in a development environment before running in production
|
||||
- **File Paths**: These tools assume they're being run from the `app/admin` directory
|
||||
- **Python Version**: Requires Python 3.7 or higher
|
||||
|
||||
## 🔒 Security Considerations
|
||||
|
||||
- These tools have direct access to user data
|
||||
- Do not expose these tools to web-facing directories
|
||||
- Keep this folder secure with appropriate file permissions
|
||||
- Never commit sensitive user data to version control
|
||||
|
||||
## 📝 Adding New Admin Tools
|
||||
|
||||
When adding new administrative tools to this folder:
|
||||
|
||||
1. Add the Python script file
|
||||
2. Update this README with documentation
|
||||
3. Include clear usage instructions
|
||||
4. Document any data modifications it makes
|
||||
5. Include error handling and user feedback
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
**"File not found" errors**:
|
||||
- Ensure you're running from the `app/admin` directory
|
||||
- Check that `../data/users.json` exists
|
||||
|
||||
**Permission denied**:
|
||||
- Ensure the application is not running
|
||||
- Check file permissions on `data/users.json`
|
||||
|
||||
**Import errors**:
|
||||
- Ensure all required dependencies are installed: `pip install -r ../requirements.txt`
|
||||
- Verify Python version: `python --version`
|
||||
@@ -0,0 +1,65 @@
|
||||
"""
|
||||
Utility script to fix existing users by ensuring their default location
|
||||
is marked as accessible in their locationSettings.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
# Get the directory where this script is located
|
||||
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||
USERS_FILE = os.path.join(basedir, '..', 'data', 'users.json')
|
||||
|
||||
def fix_default_locations():
|
||||
"""
|
||||
Ensure all users have their default location marked as accessible.
|
||||
"""
|
||||
if not os.path.exists(USERS_FILE):
|
||||
print("No users.json file found.")
|
||||
return
|
||||
|
||||
# Load users
|
||||
with open(USERS_FILE, 'r') as f:
|
||||
users = json.load(f)
|
||||
|
||||
print(f"Found {len(users)} users to check...")
|
||||
|
||||
changes_made = 0
|
||||
for user in users:
|
||||
username = user.get('username', 'Unknown')
|
||||
default_location = user.get('defaultLocation')
|
||||
|
||||
if not default_location:
|
||||
print(f"⚠️ User '{username}' has no default location - skipping")
|
||||
continue
|
||||
|
||||
location_settings = user.get('locationSettings', {})
|
||||
|
||||
# Check if default location is in settings and accessible
|
||||
if default_location not in location_settings:
|
||||
print(f"✓ Adding default location '{default_location}' for user '{username}'")
|
||||
location_settings[default_location] = {'accessible': True}
|
||||
user['locationSettings'] = location_settings
|
||||
changes_made += 1
|
||||
elif not location_settings[default_location].get('accessible', False):
|
||||
print(f"✓ Marking default location '{default_location}' as accessible for user '{username}'")
|
||||
location_settings[default_location]['accessible'] = True
|
||||
changes_made += 1
|
||||
else:
|
||||
print(f" User '{username}' - default location already accessible")
|
||||
|
||||
if changes_made > 0:
|
||||
# Save updated users
|
||||
with open(USERS_FILE, 'w') as f:
|
||||
json.dump(users, f, indent=2)
|
||||
print(f"\n✓ Fixed {changes_made} user(s) and saved to users.json")
|
||||
else:
|
||||
print("\n✓ All users already have correct default location settings")
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("=" * 60)
|
||||
print("DEFAULT LOCATION FIX UTILITY")
|
||||
print("=" * 60)
|
||||
print()
|
||||
fix_default_locations()
|
||||
print()
|
||||
@@ -0,0 +1,60 @@
|
||||
"""
|
||||
Test script to demonstrate password hashing and verification
|
||||
"""
|
||||
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
|
||||
# Example: Creating a hashed password
|
||||
plain_password = "mySecurePassword123"
|
||||
hashed_password = generate_password_hash(plain_password, method='pbkdf2:sha256')
|
||||
|
||||
print("=" * 60)
|
||||
print("PASSWORD HASHING DEMONSTRATION")
|
||||
print("=" * 60)
|
||||
print()
|
||||
print(f"Original Password: {plain_password}")
|
||||
print()
|
||||
print(f"Hashed Password: {hashed_password}")
|
||||
print()
|
||||
print(f"Hash Length: {len(hashed_password)} characters")
|
||||
print()
|
||||
print("=" * 60)
|
||||
print("SECURITY FEATURES")
|
||||
print("=" * 60)
|
||||
print("✓ Algorithm: PBKDF2-SHA256")
|
||||
print("✓ Iterations: 600,000+")
|
||||
print("✓ Unique salt per password")
|
||||
print("✓ Cannot be reversed to plain text")
|
||||
print("✓ Industry-standard security")
|
||||
print()
|
||||
print("=" * 60)
|
||||
print("PASSWORD VERIFICATION TEST")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
# Test correct password
|
||||
correct = check_password_hash(hashed_password, "mySecurePassword123")
|
||||
print(f"Testing correct password: {correct} ✓")
|
||||
|
||||
# Test incorrect password
|
||||
incorrect = check_password_hash(hashed_password, "wrongPassword")
|
||||
print(f"Testing incorrect password: {incorrect} ✗")
|
||||
print()
|
||||
|
||||
# Show that the same password produces different hashes (due to unique salts)
|
||||
print("=" * 60)
|
||||
print("UNIQUE SALT DEMONSTRATION")
|
||||
print("=" * 60)
|
||||
print()
|
||||
print("Hashing the same password twice produces different hashes:")
|
||||
print()
|
||||
hash1 = generate_password_hash("password123", method='pbkdf2:sha256')
|
||||
hash2 = generate_password_hash("password123", method='pbkdf2:sha256')
|
||||
print(f"Hash 1: {hash1}")
|
||||
print(f"Hash 2: {hash2}")
|
||||
print()
|
||||
print(f"Are they different? {hash1 != hash2}")
|
||||
print("Both hashes are valid and will verify correctly!")
|
||||
print(f"Hash 1 verifies: {check_password_hash(hash1, 'password123')}")
|
||||
print(f"Hash 2 verifies: {check_password_hash(hash2, 'password123')}")
|
||||
print()
|
||||
+995
@@ -0,0 +1,995 @@
|
||||
from flask import Flask, render_template, send_from_directory, jsonify, request, send_file, session, redirect, url_for
|
||||
import os
|
||||
from io import BytesIO
|
||||
import base64
|
||||
import json
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from functools import wraps
|
||||
import secrets
|
||||
|
||||
# Import image generator
|
||||
try:
|
||||
from image_generator import ProductImageGenerator, image_to_base64
|
||||
IMAGE_GENERATOR_AVAILABLE = True
|
||||
print("✓ Image generation available")
|
||||
except ImportError as e:
|
||||
IMAGE_GENERATOR_AVAILABLE = False
|
||||
print(f"Warning: Could not import image generator: {e}")
|
||||
print("Image generation endpoints will be disabled.")
|
||||
except Exception as e:
|
||||
IMAGE_GENERATOR_AVAILABLE = False
|
||||
print(f"Error loading image generator: {e}")
|
||||
print("Image generation endpoints will be disabled.")
|
||||
|
||||
# Get the directory where this script is located
|
||||
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
# Initialize Flask with explicit paths
|
||||
app = Flask(__name__,
|
||||
template_folder=os.path.join(basedir, 'templates'),
|
||||
static_folder=os.path.join(basedir, 'static'))
|
||||
|
||||
# Configure session - generate a random secret key
|
||||
app.secret_key = secrets.token_hex(32)
|
||||
app.config['SESSION_COOKIE_HTTPONLY'] = True
|
||||
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
|
||||
|
||||
# Configure static folders
|
||||
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0 # Disable caching during development
|
||||
|
||||
# Support for subdirectory deployments (e.g., /product-finder/)
|
||||
# Auto-detect production environment based on Python path
|
||||
import sys
|
||||
if 'virtualenv/product-finder' in sys.executable or '/home/bmdwtjuw' in sys.executable:
|
||||
# Running on production server
|
||||
app.config['APPLICATION_ROOT'] = '/product-finder'
|
||||
print("[OK] Production environment detected - using /product-finder")
|
||||
else:
|
||||
# Local development
|
||||
app.config['APPLICATION_ROOT'] = os.environ.get('APPLICATION_ROOT', '/')
|
||||
print(f"[OK] Development environment - using {app.config['APPLICATION_ROOT']}")
|
||||
|
||||
# ===== SUBDIRECTORY DEPLOYMENT MIDDLEWARE =====
|
||||
|
||||
class PrefixMiddleware:
|
||||
"""
|
||||
Middleware to handle subdirectory deployments.
|
||||
This ensures Flask knows about the /product-finder prefix when deployed.
|
||||
"""
|
||||
def __init__(self, app, prefix=''):
|
||||
self.app = app
|
||||
self.prefix = prefix.rstrip('/') # Remove trailing slash if present
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
# Only apply prefix if not already in SCRIPT_NAME and prefix is set
|
||||
if self.prefix and self.prefix != '/':
|
||||
# Check if the URL path starts with our prefix
|
||||
path = environ.get('PATH_INFO', '')
|
||||
script_name = environ.get('SCRIPT_NAME', '')
|
||||
|
||||
# If prefix not already in SCRIPT_NAME, add it
|
||||
if not script_name.startswith(self.prefix):
|
||||
environ['SCRIPT_NAME'] = self.prefix + script_name
|
||||
|
||||
# Remove prefix from PATH_INFO if it's there
|
||||
if path.startswith(self.prefix):
|
||||
environ['PATH_INFO'] = path[len(self.prefix):]
|
||||
|
||||
return self.app(environ, start_response)
|
||||
|
||||
# Apply middleware if APPLICATION_ROOT is set to a subdirectory
|
||||
application_root = app.config.get('APPLICATION_ROOT', '/')
|
||||
if application_root and application_root != '/':
|
||||
app.wsgi_app = PrefixMiddleware(app.wsgi_app, prefix=application_root)
|
||||
print(f"✓ PrefixMiddleware applied for subdirectory: {application_root}")
|
||||
|
||||
# Make base URL available to all templates
|
||||
@app.context_processor
|
||||
def inject_base_url():
|
||||
"""Inject base URL into all templates for relative paths"""
|
||||
return {
|
||||
'base_url': request.script_root or '',
|
||||
'url_for': url_for
|
||||
}
|
||||
|
||||
# ===== AUTHENTICATION HELPERS =====
|
||||
|
||||
def login_required(f):
|
||||
"""Decorator to require login for routes"""
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if 'user_id' not in session:
|
||||
return redirect(url_for('login_page'))
|
||||
|
||||
# Check if user is active
|
||||
users = load_users()
|
||||
if session['user_id'] >= len(users):
|
||||
session.clear()
|
||||
return redirect(url_for('login_page', message='User not found'))
|
||||
|
||||
user = users[session['user_id']]
|
||||
if not user.get('active', True):
|
||||
session.clear()
|
||||
return redirect(url_for('login_page', message='Account is inactive'))
|
||||
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
def get_current_user():
|
||||
"""Get the currently logged in user"""
|
||||
if 'user_id' not in session:
|
||||
return None
|
||||
|
||||
users = load_users()
|
||||
if session['user_id'] >= len(users):
|
||||
return None
|
||||
|
||||
return users[session['user_id']]
|
||||
|
||||
def can_user(permission, location=None):
|
||||
"""
|
||||
Check if current user has a specific permission.
|
||||
WordPress-style permission checking.
|
||||
|
||||
Args:
|
||||
permission (str): Permission name (e.g., 'manage_users', 'create_quotes')
|
||||
location (str, optional): Check permission for specific location.
|
||||
If None, uses current session location.
|
||||
If 'global', checks global permissions only.
|
||||
|
||||
Returns:
|
||||
bool: True if user has permission, False otherwise
|
||||
|
||||
Examples:
|
||||
can_user('manage_users') # Check global permission
|
||||
can_user('create_quotes') # Check at current location
|
||||
can_user('view_reports', 'LINDS') # Check at specific location
|
||||
"""
|
||||
user = get_current_user()
|
||||
if not user:
|
||||
return False
|
||||
|
||||
# Check if user is active
|
||||
if not user.get('active', True):
|
||||
return False
|
||||
|
||||
# Check global permissions first
|
||||
global_permissions = user.get('permissions', {})
|
||||
if permission in global_permissions:
|
||||
return global_permissions[permission] is True
|
||||
|
||||
# If location is 'global', only check global permissions
|
||||
if location == 'global':
|
||||
return False
|
||||
|
||||
# Determine which location to check
|
||||
check_location = location if location else session.get('currentLocation')
|
||||
|
||||
if not check_location:
|
||||
return False
|
||||
|
||||
# Check location-specific permissions
|
||||
location_settings = user.get('locationSettings', {})
|
||||
if check_location in location_settings:
|
||||
loc_permissions = location_settings[check_location].get('permissions', {})
|
||||
if permission in loc_permissions:
|
||||
return loc_permissions[permission] is True
|
||||
|
||||
return False
|
||||
|
||||
def user_has_any_permission(permissions, location=None):
|
||||
"""
|
||||
Check if user has ANY of the provided permissions.
|
||||
|
||||
Args:
|
||||
permissions (list): List of permission names
|
||||
location (str, optional): Location to check
|
||||
|
||||
Returns:
|
||||
bool: True if user has at least one permission
|
||||
"""
|
||||
return any(can_user(perm, location) for perm in permissions)
|
||||
|
||||
def user_has_all_permissions(permissions, location=None):
|
||||
"""
|
||||
Check if user has ALL of the provided permissions.
|
||||
|
||||
Args:
|
||||
permissions (list): List of permission names
|
||||
location (str, optional): Location to check
|
||||
|
||||
Returns:
|
||||
bool: True if user has all permissions
|
||||
"""
|
||||
return all(can_user(perm, location) for perm in permissions)
|
||||
|
||||
def permission_required(permission, location=None):
|
||||
"""
|
||||
Decorator to require specific permission for a route.
|
||||
Similar to @login_required but checks permissions.
|
||||
|
||||
Args:
|
||||
permission (str): Required permission name
|
||||
location (str, optional): Location to check permission for
|
||||
|
||||
Example:
|
||||
@app.route('/admin/users')
|
||||
@login_required
|
||||
@permission_required('manage_users')
|
||||
def manage_users_page():
|
||||
return render_template('user_manager.html')
|
||||
"""
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not can_user(permission, location):
|
||||
return render_template('access_denied.html',
|
||||
required_permission=permission), 403
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
return decorator
|
||||
|
||||
def get_user_permissions(location=None):
|
||||
"""
|
||||
Get all permissions for the current user.
|
||||
|
||||
Args:
|
||||
location (str, optional): Get permissions for specific location.
|
||||
If None, returns global + current location.
|
||||
|
||||
Returns:
|
||||
dict: Dictionary of permission_name: True/False
|
||||
"""
|
||||
user = get_current_user()
|
||||
if not user:
|
||||
return {}
|
||||
|
||||
permissions = {}
|
||||
|
||||
# Get global permissions
|
||||
global_perms = user.get('permissions', {})
|
||||
permissions.update(global_perms)
|
||||
|
||||
# Get location-specific permissions
|
||||
if location != 'global':
|
||||
check_location = location if location else session.get('currentLocation')
|
||||
if check_location:
|
||||
location_settings = user.get('locationSettings', {})
|
||||
if check_location in location_settings:
|
||||
loc_perms = location_settings[check_location].get('permissions', {})
|
||||
permissions.update(loc_perms)
|
||||
|
||||
return permissions
|
||||
|
||||
# ===== USER MANAGEMENT FUNCTIONS (moved here for use in auth) =====
|
||||
|
||||
USERS_FILE = os.path.join(basedir, 'data', 'users.json')
|
||||
|
||||
def load_users():
|
||||
"""Load users from JSON file"""
|
||||
if os.path.exists(USERS_FILE):
|
||||
try:
|
||||
with open(USERS_FILE, 'r') as f:
|
||||
return json.load(f)
|
||||
except:
|
||||
return []
|
||||
return []
|
||||
|
||||
def save_users(users):
|
||||
"""Save users to JSON file"""
|
||||
with open(USERS_FILE, 'w') as f:
|
||||
json.dump(users, f, indent=2)
|
||||
|
||||
# ===== AUTHENTICATION ROUTES =====
|
||||
|
||||
@app.route('/login')
|
||||
def login_page():
|
||||
"""Serve the login page"""
|
||||
# If already logged in, redirect to main app
|
||||
if 'user_id' in session:
|
||||
return redirect(url_for('index'))
|
||||
return render_template('login.html')
|
||||
|
||||
@app.route('/select-location')
|
||||
def select_location_page():
|
||||
"""Serve the location selection page"""
|
||||
if 'user_id' not in session:
|
||||
return redirect(url_for('login_page'))
|
||||
return render_template('select_location.html')
|
||||
|
||||
@app.route('/api/login', methods=['POST'])
|
||||
def login():
|
||||
"""Authenticate user and create session"""
|
||||
try:
|
||||
data = request.json
|
||||
username = data.get('username')
|
||||
password = data.get('password')
|
||||
|
||||
if not username or not password:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Username and password are required'
|
||||
}), 400
|
||||
|
||||
users = load_users()
|
||||
|
||||
# Find user by username
|
||||
user_index = None
|
||||
user = None
|
||||
for i, u in enumerate(users):
|
||||
if u['username'] == username:
|
||||
user_index = i
|
||||
user = u
|
||||
break
|
||||
|
||||
if not user:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Invalid username or password'
|
||||
}), 401
|
||||
|
||||
# Check if user is active
|
||||
if not user.get('active', True):
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Account is inactive. Please contact an administrator.'
|
||||
}), 403
|
||||
|
||||
# Verify password
|
||||
if not check_password_hash(user['password'], password):
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Invalid username or password'
|
||||
}), 401
|
||||
|
||||
# Create session
|
||||
session['user_id'] = user_index
|
||||
session['username'] = user['username']
|
||||
session['defaultLocation'] = user.get('defaultLocation')
|
||||
|
||||
# Get accessible locations
|
||||
accessible_locations = []
|
||||
if 'locationSettings' in user:
|
||||
for loc_code, settings in user['locationSettings'].items():
|
||||
if settings.get('accessible', False):
|
||||
accessible_locations.append(loc_code)
|
||||
|
||||
session['accessibleLocations'] = accessible_locations
|
||||
|
||||
# Check if user needs to select a location
|
||||
requiresLocationSelection = len(accessible_locations) > 1
|
||||
|
||||
# If only one location or no accessible locations, auto-select default
|
||||
if not requiresLocationSelection:
|
||||
session['currentLocation'] = user.get('defaultLocation')
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': 'Login successful',
|
||||
'requiresLocationSelection': requiresLocationSelection
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': str(e)
|
||||
}), 500
|
||||
|
||||
@app.route('/api/logout', methods=['POST'])
|
||||
def logout():
|
||||
"""Log out user and clear session"""
|
||||
session.clear()
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': 'Logged out successfully'
|
||||
})
|
||||
|
||||
@app.route('/api/session', methods=['GET'])
|
||||
def get_session():
|
||||
"""Get current session information"""
|
||||
if 'user_id' not in session:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Not logged in'
|
||||
}), 401
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'username': session.get('username'),
|
||||
'defaultLocation': session.get('defaultLocation'),
|
||||
'currentLocation': session.get('currentLocation'),
|
||||
'accessibleLocations': session.get('accessibleLocations', [])
|
||||
})
|
||||
|
||||
@app.route('/api/select-location', methods=['POST'])
|
||||
def select_location():
|
||||
"""Select a location for the current session"""
|
||||
if 'user_id' not in session:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Not logged in'
|
||||
}), 401
|
||||
|
||||
try:
|
||||
data = request.json
|
||||
location = data.get('location')
|
||||
|
||||
# Verify user has access to this location
|
||||
accessible = session.get('accessibleLocations', [])
|
||||
if location not in accessible:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'You do not have access to this location'
|
||||
}), 403
|
||||
|
||||
session['currentLocation'] = location
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': 'Location selected',
|
||||
'currentLocation': location
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': str(e)
|
||||
}), 500
|
||||
|
||||
# ===== PERMISSION CHECK ENDPOINT =====
|
||||
|
||||
@app.route('/api/check-permission', methods=['POST'])
|
||||
@login_required
|
||||
def check_permission():
|
||||
"""Check if current user has specific permission(s)"""
|
||||
try:
|
||||
data = request.json
|
||||
permission = data.get('permission')
|
||||
location = data.get('location') # Optional, defaults to current location
|
||||
|
||||
if not permission:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Permission name required'
|
||||
}), 400
|
||||
|
||||
has_permission = can_user(permission, location)
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'hasPermission': has_permission,
|
||||
'permission': permission,
|
||||
'location': location if location else session.get('currentLocation', 'global')
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': str(e)
|
||||
}), 500
|
||||
|
||||
@app.route('/api/user-permissions', methods=['GET'])
|
||||
@login_required
|
||||
def get_user_permissions_endpoint():
|
||||
"""Get all permissions for current user"""
|
||||
try:
|
||||
location = request.args.get('location') # Optional query parameter
|
||||
permissions = get_user_permissions(location)
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'permissions': permissions,
|
||||
'location': location if location else session.get('currentLocation', 'global')
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': str(e)
|
||||
}), 500
|
||||
|
||||
# ===== MAIN APPLICATION ROUTES =====
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""Serve the quiz page as the main page or redirect to login"""
|
||||
# Simple test to see if app is running
|
||||
if 'username' not in session:
|
||||
return redirect(url_for('login_page'))
|
||||
|
||||
# If user hasn't selected a location yet, redirect to selection
|
||||
if not session.get('currentLocation') and len(session.get('accessibleLocations', [])) > 1:
|
||||
return redirect(url_for('select_location_page'))
|
||||
return render_template('index2.html')
|
||||
|
||||
@app.route('/test')
|
||||
def test_route():
|
||||
"""Simple test route to verify app is running - NO AUTH REQUIRED"""
|
||||
import sys
|
||||
info = {
|
||||
'status': 'OK',
|
||||
'message': 'Flask app is running!',
|
||||
'python': sys.version,
|
||||
'application_root': app.config.get('APPLICATION_ROOT'),
|
||||
'middleware': type(app.wsgi_app).__name__,
|
||||
'routes': len(list(app.url_map.iter_rules()))
|
||||
}
|
||||
return f"""
|
||||
<html>
|
||||
<head><title>App Test</title></head>
|
||||
<body style="font-family: monospace; padding: 20px;">
|
||||
<h1>✓ Flask App is Running!</h1>
|
||||
<ul>
|
||||
<li><strong>Python:</strong> {info['python']}</li>
|
||||
<li><strong>APPLICATION_ROOT:</strong> {info['application_root']}</li>
|
||||
<li><strong>Middleware:</strong> {info['middleware']}</li>
|
||||
<li><strong>Routes:</strong> {info['routes']}</li>
|
||||
</ul>
|
||||
<p><a href="{url_for('login_page')}">Go to Login</a></p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
@app.route('/quiz')
|
||||
@login_required
|
||||
def quiz():
|
||||
"""Serve the quiz page"""
|
||||
return render_template('index2.html')
|
||||
|
||||
@app.route('/image-test')
|
||||
@login_required
|
||||
def image_test():
|
||||
"""Serve the image generation test page"""
|
||||
return render_template('image_test.html')
|
||||
|
||||
@app.route('/css/<path:filename>')
|
||||
def serve_css(filename):
|
||||
"""Serve CSS files"""
|
||||
return send_from_directory(os.path.join(basedir, 'css'), filename)
|
||||
|
||||
@app.route('/js/<path:filename>')
|
||||
def serve_js(filename):
|
||||
"""Serve JavaScript files"""
|
||||
return send_from_directory(os.path.join(basedir, 'js'), filename)
|
||||
|
||||
@app.route('/images/<path:filename>')
|
||||
def serve_images(filename):
|
||||
"""Serve image files"""
|
||||
return send_from_directory(os.path.join(basedir, 'images'), filename)
|
||||
|
||||
@app.route('/data/<path:filename>')
|
||||
def serve_data(filename):
|
||||
"""Serve data files (JSON)"""
|
||||
return send_from_directory(os.path.join(basedir, 'data'), filename)
|
||||
|
||||
# API endpoint for future expansion (e.g., saving user selections)
|
||||
@app.route('/api/save-selection', methods=['POST'])
|
||||
def save_selection():
|
||||
"""API endpoint to save user selections"""
|
||||
data = request.json
|
||||
# Here you could save to a database or file
|
||||
# For now, just return success
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': 'Selection saved',
|
||||
'data': data
|
||||
})
|
||||
|
||||
@app.route('/api/get-products', methods=['GET'])
|
||||
def get_products():
|
||||
"""API endpoint to get product data"""
|
||||
# This could fetch from a database in the future
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'products': []
|
||||
})
|
||||
|
||||
# ===== DYNAMIC IMAGE GENERATION ENDPOINTS =====
|
||||
|
||||
@app.route('/api/product-image/<product_code>')
|
||||
def generate_product_image(product_code):
|
||||
"""
|
||||
Generate a product image with specified configuration.
|
||||
|
||||
URL Parameters:
|
||||
color: Color name (default: 'white')
|
||||
hinge: Hinge side - 'left' or 'right' (default: 'right')
|
||||
material: Material type (default: 'aluminum')
|
||||
format: Return format - 'image' or 'json' (default: 'image')
|
||||
|
||||
Examples:
|
||||
/api/product-image/cobrai?color=black&hinge=left
|
||||
/api/product-image/cobrai?color=bronze&hinge=right&format=json
|
||||
"""
|
||||
if not IMAGE_GENERATOR_AVAILABLE:
|
||||
return jsonify({
|
||||
'error': 'Image generation not available. Please install Pillow: pip install Pillow'
|
||||
}), 500
|
||||
|
||||
try:
|
||||
# Get parameters
|
||||
color = request.args.get('color', 'white').lower()
|
||||
hinge = request.args.get('hinge', 'right').lower()
|
||||
material = request.args.get('material', 'aluminum').lower()
|
||||
return_format = request.args.get('format', 'image').lower()
|
||||
use_cache = request.args.get('cache', 'true').lower() == 'true'
|
||||
|
||||
# Initialize generator
|
||||
generator = ProductImageGenerator()
|
||||
|
||||
# Generate image
|
||||
image = generator.generate_product_image(
|
||||
product_code=product_code,
|
||||
color=color,
|
||||
hinge=hinge,
|
||||
material=material,
|
||||
use_cache=use_cache
|
||||
)
|
||||
|
||||
# Return based on format
|
||||
if return_format == 'json':
|
||||
# Return base64 encoded image in JSON
|
||||
img_base64 = image_to_base64(image, format='PNG')
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'productCode': product_code,
|
||||
'color': color,
|
||||
'hinge': hinge,
|
||||
'material': material,
|
||||
'image': img_base64,
|
||||
'format': 'base64'
|
||||
})
|
||||
else:
|
||||
# Return raw image file
|
||||
img_io = BytesIO()
|
||||
image.save(img_io, 'PNG', optimize=True)
|
||||
img_io.seek(0)
|
||||
return send_file(img_io, mimetype='image/png')
|
||||
|
||||
except ValueError as e:
|
||||
return jsonify({'error': str(e)}), 404
|
||||
except FileNotFoundError as e:
|
||||
return jsonify({'error': str(e)}), 404
|
||||
except Exception as e:
|
||||
return jsonify({'error': f'Error generating image: {str(e)}'}), 500
|
||||
|
||||
@app.route('/api/product-config/<product_code>')
|
||||
def get_product_image_config(product_code):
|
||||
"""
|
||||
Get the image configuration for a product.
|
||||
|
||||
Returns JSON with available colors, hardware positions, and other metadata.
|
||||
|
||||
Example:
|
||||
/api/product-config/cobrai
|
||||
"""
|
||||
if not IMAGE_GENERATOR_AVAILABLE:
|
||||
return jsonify({
|
||||
'error': 'Image generation not available. Please install Pillow: pip install Pillow'
|
||||
}), 500
|
||||
|
||||
try:
|
||||
generator = ProductImageGenerator()
|
||||
config = generator.get_product_config(product_code)
|
||||
|
||||
if not config:
|
||||
return jsonify({
|
||||
'error': f'Configuration for product {product_code} not found'
|
||||
}), 404
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'productCode': product_code,
|
||||
'config': config
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/clear-image-cache', methods=['POST'])
|
||||
def clear_image_cache():
|
||||
"""
|
||||
Clear the image cache.
|
||||
|
||||
Optional JSON body:
|
||||
{ "productCode": "cobrai" } - Clear cache for specific product
|
||||
|
||||
Example:
|
||||
POST /api/clear-image-cache
|
||||
POST /api/clear-image-cache with body: {"productCode": "cobrai"}
|
||||
"""
|
||||
if not IMAGE_GENERATOR_AVAILABLE:
|
||||
return jsonify({
|
||||
'error': 'Image generation not available.'
|
||||
}), 500
|
||||
|
||||
try:
|
||||
data = request.json or {}
|
||||
product_code = data.get('productCode')
|
||||
|
||||
generator = ProductImageGenerator()
|
||||
generator.clear_cache(product_code)
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': f'Cache cleared for {product_code}' if product_code else 'All cache cleared'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
# ===== END DYNAMIC IMAGE GENERATION ENDPOINTS =====
|
||||
|
||||
# ===== USER MANAGEMENT ENDPOINTS =====
|
||||
|
||||
@app.route('/users')
|
||||
@login_required
|
||||
@permission_required('manage_users')
|
||||
def user_manager():
|
||||
"""Serve the user management page"""
|
||||
return render_template('user_manager.html')
|
||||
|
||||
@app.route('/api/users', methods=['GET'])
|
||||
@login_required
|
||||
@permission_required('manage_users')
|
||||
def get_users():
|
||||
"""Get all users"""
|
||||
users = load_users()
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'users': users
|
||||
})
|
||||
|
||||
@app.route('/api/users', methods=['POST'])
|
||||
@login_required
|
||||
@permission_required('manage_users')
|
||||
def add_user():
|
||||
"""Add a new user with hashed password"""
|
||||
try:
|
||||
data = request.json
|
||||
|
||||
# Validate required fields
|
||||
if not data.get('username') or not data.get('password') or not data.get('defaultLocation'):
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Username, password, and default location are required'
|
||||
}), 400
|
||||
|
||||
users = load_users()
|
||||
|
||||
# Check if username already exists
|
||||
if any(user['username'] == data['username'] for user in users):
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Username already exists'
|
||||
}), 400
|
||||
|
||||
# Hash the password using pbkdf2:sha256 (secure)
|
||||
hashed_password = generate_password_hash(data['password'], method='pbkdf2:sha256')
|
||||
|
||||
# Get location settings and ensure default location is marked as accessible
|
||||
location_settings = data.get('locationSettings', {})
|
||||
default_location = data['defaultLocation']
|
||||
|
||||
# Ensure default location exists in settings and is marked as accessible
|
||||
if default_location not in location_settings:
|
||||
location_settings[default_location] = {}
|
||||
location_settings[default_location]['accessible'] = True
|
||||
|
||||
# Create new user
|
||||
new_user = {
|
||||
'username': data['username'],
|
||||
'password': hashed_password,
|
||||
'defaultLocation': default_location,
|
||||
'locationSettings': location_settings,
|
||||
'active': data.get('active', True) # Default to active
|
||||
}
|
||||
|
||||
users.append(new_user)
|
||||
save_users(users)
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': 'User added successfully',
|
||||
'users': users
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': str(e)
|
||||
}), 500
|
||||
|
||||
@app.route('/api/users/<int:user_index>/active', methods=['PATCH'])
|
||||
@login_required
|
||||
@permission_required('manage_users')
|
||||
def toggle_user_active(user_index):
|
||||
"""Toggle user active status"""
|
||||
try:
|
||||
data = request.json
|
||||
users = load_users()
|
||||
|
||||
if user_index < 0 or user_index >= len(users):
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Invalid user index'
|
||||
}), 400
|
||||
|
||||
# Update active status
|
||||
users[user_index]['active'] = data.get('active', True)
|
||||
save_users(users)
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': 'User status updated',
|
||||
'users': users
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': str(e)
|
||||
}), 500
|
||||
|
||||
@app.route('/api/users/<int:user_index>', methods=['DELETE'])
|
||||
@login_required
|
||||
@permission_required('manage_users')
|
||||
def delete_user(user_index):
|
||||
"""Delete a specific user by index"""
|
||||
try:
|
||||
users = load_users()
|
||||
|
||||
if user_index < 0 or user_index >= len(users):
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Invalid user index'
|
||||
}), 400
|
||||
|
||||
users.pop(user_index)
|
||||
save_users(users)
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': 'User deleted successfully',
|
||||
'users': users
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': str(e)
|
||||
}), 500
|
||||
|
||||
@app.route('/api/users/<int:user_index>/change-password', methods=['POST'])
|
||||
@login_required
|
||||
@permission_required('manage_users')
|
||||
def change_user_password(user_index):
|
||||
"""Change a user's password with current user verification"""
|
||||
try:
|
||||
data = request.json
|
||||
new_password = data.get('newPassword')
|
||||
current_user_password = data.get('currentUserPassword')
|
||||
|
||||
# Validate required fields
|
||||
if not new_password or not current_user_password:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'New password and current user password are required'
|
||||
}), 400
|
||||
|
||||
# Validate password length
|
||||
if len(new_password) < 6:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Password must be at least 6 characters long'
|
||||
}), 400
|
||||
|
||||
# Get current logged-in user
|
||||
current_user = get_current_user()
|
||||
if not current_user:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Not authenticated'
|
||||
}), 401
|
||||
|
||||
# Verify current user's password
|
||||
if not check_password_hash(current_user['password'], current_user_password):
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Your password is incorrect. Password change denied for security reasons.'
|
||||
}), 403
|
||||
|
||||
# Load users and validate index
|
||||
users = load_users()
|
||||
if user_index < 0 or user_index >= len(users):
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Invalid user index'
|
||||
}), 400
|
||||
|
||||
# Hash the new password
|
||||
hashed_password = generate_password_hash(new_password, method='pbkdf2:sha256')
|
||||
|
||||
# Update the target user's password
|
||||
target_username = users[user_index]['username']
|
||||
users[user_index]['password'] = hashed_password
|
||||
save_users(users)
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': f'Password changed successfully for user "{target_username}"'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': str(e)
|
||||
}), 500
|
||||
|
||||
@app.route('/api/users/clear', methods=['DELETE'])
|
||||
def clear_all_users():
|
||||
"""Clear all users"""
|
||||
try:
|
||||
save_users([])
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': 'All users cleared'
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': str(e)
|
||||
}), 500
|
||||
|
||||
@app.route('/api/users/download')
|
||||
def download_users():
|
||||
"""Download users as JSON file"""
|
||||
try:
|
||||
users = load_users()
|
||||
|
||||
if not users:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'No users to download'
|
||||
}), 400
|
||||
|
||||
# Create JSON file in memory
|
||||
json_data = json.dumps(users, indent=2)
|
||||
|
||||
# Send as downloadable file
|
||||
return send_file(
|
||||
BytesIO(json_data.encode()),
|
||||
mimetype='application/json',
|
||||
as_attachment=True,
|
||||
download_name='users.json'
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': str(e)
|
||||
}), 500
|
||||
|
||||
# ===== END USER MANAGEMENT ENDPOINTS =====
|
||||
|
||||
# Error handlers
|
||||
@app.errorhandler(404)
|
||||
def page_not_found(e):
|
||||
return render_template('404.html'), 404
|
||||
|
||||
@app.errorhandler(500)
|
||||
def internal_error(e):
|
||||
return "Internal Server Error", 500
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Create necessary directories
|
||||
os.makedirs(os.path.join(basedir, 'images'), exist_ok=True)
|
||||
os.makedirs(os.path.join(basedir, 'templates'), exist_ok=True)
|
||||
os.makedirs(os.path.join(basedir, 'css'), exist_ok=True)
|
||||
os.makedirs(os.path.join(basedir, 'js'), exist_ok=True)
|
||||
os.makedirs(os.path.join(basedir, 'data'), exist_ok=True)
|
||||
|
||||
# Run the application
|
||||
# Set debug=False for production
|
||||
# Port 8080 is used instead of 5000 (Windows reserves port 5000)
|
||||
app.run(debug=True, host='0.0.0.0', port=8080)
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 147 KiB |
@@ -0,0 +1,44 @@
|
||||
"""
|
||||
Configuration file for Flask application
|
||||
"""
|
||||
import os
|
||||
|
||||
class Config:
|
||||
"""Base configuration"""
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production'
|
||||
DEBUG = False
|
||||
TESTING = False
|
||||
|
||||
# Application root for subdirectory deployments
|
||||
# Defaults to '/' (root) for local development
|
||||
# Set to '/product-finder' for production deployment
|
||||
APPLICATION_ROOT = os.environ.get('APPLICATION_ROOT', '/')
|
||||
|
||||
class DevelopmentConfig(Config):
|
||||
"""Development configuration"""
|
||||
DEBUG = True
|
||||
ENV = 'development'
|
||||
# Development runs at root
|
||||
APPLICATION_ROOT = os.environ.get('APPLICATION_ROOT', '/')
|
||||
|
||||
class ProductionConfig(Config):
|
||||
"""Production configuration"""
|
||||
DEBUG = False
|
||||
ENV = 'production'
|
||||
# Production deployed to /product-finder subdirectory
|
||||
APPLICATION_ROOT = os.environ.get('APPLICATION_ROOT', '/product-finder')
|
||||
# Add production-specific settings here
|
||||
# DATABASE_URI = os.environ.get('DATABASE_URL')
|
||||
|
||||
class TestingConfig(Config):
|
||||
"""Testing configuration"""
|
||||
TESTING = True
|
||||
DEBUG = True
|
||||
|
||||
# Configuration dictionary
|
||||
config = {
|
||||
'development': DevelopmentConfig,
|
||||
'production': ProductionConfig,
|
||||
'testing': TestingConfig,
|
||||
'default': DevelopmentConfig
|
||||
}
|
||||
@@ -0,0 +1,737 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
background-color: white;
|
||||
border: 2px solid #333;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: #333;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.user-info-bar {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
background-color: #f56565;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.logout-btn:hover {
|
||||
background-color: #e53e3e;
|
||||
}
|
||||
|
||||
.question-container {
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.question-title {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.question-subtitle {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.buttons-wrapper {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.buttons-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.answer-button {
|
||||
width: 180px;
|
||||
padding: 15px;
|
||||
background-color: white;
|
||||
border: 2px solid #333;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.answer-button:hover {
|
||||
background-color: #f0f0f0;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.answer-button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.answer-image {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
background-color: #e0e0e0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 48px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.answer-caption {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.prod_code {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
font-weight: normal;
|
||||
color: #666;
|
||||
margin-top: 4px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Product Configuration Form */
|
||||
.product-config-form {
|
||||
margin-top: 20px;
|
||||
padding: 20px;
|
||||
background-color: #f9f9f9;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.product-config-form h3 {
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.config-field {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.config-field label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.config-select {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background-color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.config-select:focus {
|
||||
outline: none;
|
||||
border-color: #333;
|
||||
}
|
||||
|
||||
.config-select:disabled {
|
||||
background-color: #f0f0f0;
|
||||
color: #666;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.config-input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background-color: white;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.config-input:focus {
|
||||
outline: none;
|
||||
border-color: #333;
|
||||
}
|
||||
|
||||
.config-input::placeholder {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.hinge-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.radio-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.radio-label input[type="radio"] {
|
||||
margin-right: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.radio-label span {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.result-container {
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.result-title {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.result-content {
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
margin-bottom: 30px;
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.result-image-section {
|
||||
flex: 0 0 350px;
|
||||
min-width: 350px;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
border: 2px solid #333;
|
||||
object-fit: contain;
|
||||
max-height: 400px;
|
||||
background-color: #f5f5f5;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
/* Layered Image System */
|
||||
.door-configurator {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
min-height: 400px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid #333;
|
||||
overflow: hidden;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.door-configurator img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.layer-base {
|
||||
position: relative !important;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.layer-door {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.layer-hardware {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.layer-overlay {
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.config-preview-notice {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background-color: rgba(255, 152, 0, 0.9);
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
z-index: 10;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.result-details {
|
||||
flex: 1;
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.result-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 20px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.actions-left,
|
||||
.actions-right {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Responsive layout for smaller screens */
|
||||
@media (max-width: 768px) {
|
||||
.result-content {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.result-image-section {
|
||||
flex: 1;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.back-button {
|
||||
padding: 12px 30px;
|
||||
background-color: #333;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
background-color: #555;
|
||||
}
|
||||
|
||||
/* Hide share buttons (temporarily disabled) */
|
||||
.back-button[onclick*="shareCurrentPage"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
padding: 15px 30px;
|
||||
background-color: #f9f9f9;
|
||||
border-bottom: 1px solid #ddd;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.breadcrumb span {
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
.buttons-wrapper::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
.buttons-wrapper::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
.buttons-wrapper::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.buttons-wrapper::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
/* Measurement Type Toggle Buttons */
|
||||
.measurement-type-container {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
margin: 0 auto;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.measurement-toggle {
|
||||
flex: 1;
|
||||
max-width: 200px;
|
||||
min-height: 120px;
|
||||
padding: 20px 15px;
|
||||
background-color: #f0f0f0;
|
||||
border: 3px solid #333;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.measurement-toggle:hover:not(.disabled) {
|
||||
background-color: #e0e0e0;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.measurement-toggle.selected {
|
||||
background-color: #333;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.measurement-toggle.selected:hover {
|
||||
background-color: #444;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.measurement-toggle.selected .toggle-label {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.measurement-toggle.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.toggle-icon {
|
||||
font-size: 48px;
|
||||
line-height: 1;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
display: block;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.measurement-toggle.selected .toggle-icon {
|
||||
filter: grayscale(0%) brightness(200%);
|
||||
}
|
||||
|
||||
/* Form Styles */
|
||||
.form-wrapper {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.dynamic-form {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-group input[type="text"],
|
||||
.form-group input[type="number"],
|
||||
.form-group input[type="email"],
|
||||
.form-group input[type="tel"],
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 2px solid #333;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-family: Arial, sans-serif;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus,
|
||||
.form-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: #555;
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
min-height: 80px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.form-group select {
|
||||
cursor: pointer;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
width: 100%;
|
||||
padding: 12px 30px;
|
||||
background-color: #333;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.submit-button:hover {
|
||||
background-color: #555;
|
||||
}
|
||||
|
||||
/* Scrollbar styling for form wrapper */
|
||||
.form-wrapper::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
.form-wrapper::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
.form-wrapper::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.form-wrapper::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
/* Notification animations */
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(400px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Notes Section */
|
||||
.notes-section {
|
||||
margin: 20px auto;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.notes-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;
|
||||
}
|
||||
|
||||
.notes-toggle:hover {
|
||||
background-color: #e9ecef;
|
||||
border-color: #ced4da;
|
||||
}
|
||||
|
||||
.notes-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.notes-label {
|
||||
flex: 1;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.notes-arrow {
|
||||
font-size: 12px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.notes-content {
|
||||
margin-top: 10px;
|
||||
padding: 16px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.notes-content a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.notes-content a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.notes-content ul {
|
||||
margin: 10px 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.notes-content li {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
@keyframes slideOut {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateX(400px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Accessories list */
|
||||
.accessories-list {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.accessory-item {
|
||||
padding: 12px;
|
||||
margin: 8px 0;
|
||||
background: #f5f5f5;
|
||||
border-left: 3px solid #2196F3;
|
||||
border-radius: 3px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.accessory-item strong {
|
||||
color: #333;
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.accessory-item small {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Product grid for results */
|
||||
.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: 2px solid #333;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.product-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.product-card h3 {
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
font-size: 1.1em;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.product-card p {
|
||||
margin: 10px 0;
|
||||
line-height: 1.6;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.product-card ul {
|
||||
list-style-position: inside;
|
||||
padding-left: 0;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.product-card li {
|
||||
margin: 5px 0;
|
||||
color: #666;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"description": "Bitwise flag system for product classification",
|
||||
"bit_definitions": {
|
||||
"base_attributes": {
|
||||
"bit_0 (1)": "Base Type = Door",
|
||||
"bit_1 (2)": "Base Type = Window",
|
||||
"bit_2 (4)": "Is Accessory",
|
||||
"bit_3 (8)": "Specific Item Link"
|
||||
},
|
||||
"materials": {
|
||||
"bit_4 (16)": "Material = Aluminum",
|
||||
"bit_5 (32)": "Material = Vinyl"
|
||||
},
|
||||
"colors": {
|
||||
"bit_6 (64)": "Color = Black",
|
||||
"bit_7 (128)": "Color = White",
|
||||
"bit_8 (256)": "Color = Bronze",
|
||||
"bit_9 (512)": "Color = Tan",
|
||||
"bit_10 (1024)": "Color = Mill",
|
||||
"bit_11 (2048)": "Color = Sandstone"
|
||||
},
|
||||
"subtypes": {
|
||||
"bit_12 (4096)": "Subtype = Patio Door",
|
||||
"bit_13 (8192)": "Subtype = Primary Window",
|
||||
"bit_14 (16384)": "Subtype = Storm Door",
|
||||
"bit_15 (32768)": "Subtype = Storm Window"
|
||||
}
|
||||
},
|
||||
"usage_examples": {
|
||||
"check_if_door": "bit_value & 1",
|
||||
"check_if_window": "bit_value & 2",
|
||||
"check_if_aluminum": "bit_value & 16",
|
||||
"check_if_white": "bit_value & 128",
|
||||
"check_multiple": "(bit_value & 1) and (bit_value & 16) # Door AND Aluminum",
|
||||
"check_subtype_patio_door": "bit_value & 4096",
|
||||
"check_subtype_primary_window": "bit_value & 8192",
|
||||
"check_subtype_storm_door": "bit_value & 16384",
|
||||
"check_subtype_storm_window": "bit_value & 32768"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
Product_Name,Image_URL,Product_Type,Notes
|
||||
Brass Lever,http://columbiawindows.com/wp-content/uploads/2014/12/CR2DLEC.jpg,Hardware,
|
||||
Brass Lever (White),http://columbiawindows.com/wp-content/uploads/2014/12/CR2DLEC.jpg,Hardware,Also comes in white
|
||||
Brass Lever (MT Gold),http://columbiawindows.com/wp-content/uploads/2014/12/MT-MET-GOLD.jpg,Hardware,
|
||||
Brass Lever (550 Gold),http://columbiawindows.com/wp-content/uploads/2014/12/550-met-gold.jpg,Hardware,
|
||||
Brass Pull,http://columbiawindows.com/wp-content/uploads/2014/12/dx-ecoat.jpg,Hardware,
|
||||
Brass Pull (White),http://columbiawindows.com/wp-content/uploads/2014/12/CR3SEC.jpg,Hardware,Also comes in white
|
||||
Brass Pull (MT Gold),http://columbiawindows.com/wp-content/uploads/2014/12/MT-MET-GOLD.jpg,Hardware,
|
||||
Brass Pull (550 Gold),http://columbiawindows.com/wp-content/uploads/2014/12/550-met-gold.jpg,Hardware,
|
||||
Satin Pull,http://columbiawindows.com/wp-content/uploads/2014/12/dx-satin-finish.jpg,Hardware,
|
||||
Satin Pull (Silver),http://columbiawindows.com/wp-content/uploads/2014/12/MT-satin-silver.jpg,Hardware,
|
||||
Black Pull Handle,http://columbiawindows.com/wp-content/uploads/2014/12/VPBLACK.jpg,Hardware,
|
||||
White Pull Handle,http://columbiawindows.com/wp-content/uploads/2014/12/VPWHITE.jpg,Hardware,
|
||||
Bevel Cut Insert,http://columbiawindows.com/wp-content/uploads/2014/12/bevel.jpg,Insert,
|
||||
Brass Kick Panel,http://columbiawindows.com/wp-content/uploads/2014/12/brasskp.jpg,Panel,
|
||||
|
@@ -0,0 +1,68 @@
|
||||
[
|
||||
{
|
||||
"username": "admin_user",
|
||||
"password": "pbkdf2:sha256:1000000$examplehash$...",
|
||||
"defaultLocation": "LINDS",
|
||||
"active": true,
|
||||
"permissions": {
|
||||
"manage_users": true,
|
||||
"view_reports": true,
|
||||
"create_quotes": true
|
||||
},
|
||||
"locationSettings": {
|
||||
"LINDS": {
|
||||
"accessible": true,
|
||||
"permissions": {
|
||||
"manage_inventory": true,
|
||||
"approve_quotes": true
|
||||
}
|
||||
},
|
||||
"IOLA": {
|
||||
"accessible": true,
|
||||
"permissions": {
|
||||
"manage_inventory": false,
|
||||
"approve_quotes": true
|
||||
}
|
||||
},
|
||||
"KC": {
|
||||
"accessible": false
|
||||
},
|
||||
"BMD": {
|
||||
"accessible": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"username": "standard_user",
|
||||
"password": "pbkdf2:sha256:1000000$examplehash2$...",
|
||||
"defaultLocation": "KC",
|
||||
"active": true,
|
||||
"permissions": {
|
||||
"manage_users": false,
|
||||
"view_reports": true,
|
||||
"create_quotes": true
|
||||
},
|
||||
"locationSettings": {
|
||||
"LINDS": {
|
||||
"accessible": true,
|
||||
"permissions": {
|
||||
"manage_inventory": false,
|
||||
"approve_quotes": false
|
||||
}
|
||||
},
|
||||
"IOLA": {
|
||||
"accessible": true
|
||||
},
|
||||
"KC": {
|
||||
"accessible": true,
|
||||
"permissions": {
|
||||
"manage_inventory": false,
|
||||
"approve_quotes": false
|
||||
}
|
||||
},
|
||||
"BMD": {
|
||||
"accessible": true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"cobrai": {
|
||||
"sourceImage": "images/cobrai",
|
||||
"productCode": "COBRAI",
|
||||
"description": "Storm Door - Full View with Screen",
|
||||
"availableColors": {
|
||||
"white": {"rgb": [255, 255, 255], "name": "White"},
|
||||
"black": {"rgb": [30, 30, 30], "name": "Black"},
|
||||
"bronze": {"rgb": [110, 80, 50], "name": "Bronze"},
|
||||
"sandstone": {"rgb": [210, 190, 165], "name": "Sandstone"}
|
||||
},
|
||||
"colorableRegions": [
|
||||
{
|
||||
"name": "region_name",
|
||||
"topLeft": [24, 23],
|
||||
"bottomRight": [505, 1022],
|
||||
"description": "Width: 481px, Height: 999px"
|
||||
},
|
||||
{
|
||||
"name": "frame",
|
||||
"topLeft": [0, 0],
|
||||
"bottomRight": [100, 1450],
|
||||
"description": "Vertical frame left side"
|
||||
},
|
||||
{
|
||||
"name": "frame_right",
|
||||
"topLeft": [620, 0],
|
||||
"bottomRight": [720, 1450],
|
||||
"description": "Vertical frame right side"
|
||||
},
|
||||
{
|
||||
"name": "top_rail",
|
||||
"topLeft": [0, 0],
|
||||
"bottomRight": [720, 80],
|
||||
"description": "Top horizontal rail"
|
||||
},
|
||||
{
|
||||
"name": "mid_rail",
|
||||
"topLeft": [0, 590],
|
||||
"bottomRight": [720, 680],
|
||||
"description": "Middle horizontal rail"
|
||||
},
|
||||
{
|
||||
"name": "bottom_panel",
|
||||
"topLeft": [100, 1090],
|
||||
"bottomRight": [620, 1450],
|
||||
"description": "Bottom kickplate panel"
|
||||
}
|
||||
],
|
||||
"hardwarePositions": {
|
||||
"handle_right": {
|
||||
"x": 580,
|
||||
"y": 730,
|
||||
"image": "images/hardware/handle-lever.png"
|
||||
},
|
||||
"handle_left": {
|
||||
"x": 140,
|
||||
"y": 730,
|
||||
"image": "images/hardware/handle-lever.png",
|
||||
"flip": true
|
||||
},
|
||||
"lock": {
|
||||
"x": 360,
|
||||
"y": 800,
|
||||
"image": "images/hardware/lock.png"
|
||||
}
|
||||
},
|
||||
"glassRegions": [
|
||||
{
|
||||
"name": "top_glass",
|
||||
"topLeft": [100, 80],
|
||||
"bottomRight": [620, 590],
|
||||
"description": "Top screen/glass area"
|
||||
},
|
||||
{
|
||||
"name": "bottom_glass",
|
||||
"topLeft": [100, 680],
|
||||
"bottomRight": [620, 1090],
|
||||
"description": "Bottom screen/glass area"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"imageWidth": 720,
|
||||
"imageHeight": 1450,
|
||||
"defaultColor": "white",
|
||||
"defaultHinge": "right"
|
||||
},
|
||||
"cache": {
|
||||
"enabled": true,
|
||||
"directory": "cache/product_images",
|
||||
"maxAge": 86400
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,461 @@
|
||||
{
|
||||
"start": {
|
||||
"type": "question",
|
||||
"inputType": "button",
|
||||
"title": "What are you looking for?",
|
||||
"subtitle": "Select the product category",
|
||||
"notes": "<p><strong>Quick Tips:</strong></p><ul><li>Press <strong>Ctrl+Shift+R</strong> (Windows) or <strong>Cmd+Shift+R</strong> (Mac) to hard refresh the page</li><li>Press <strong>F5</strong> to reload the application</li><li>Use the back button or breadcrumbs to navigate</li></ul>",
|
||||
"answers": [
|
||||
{
|
||||
"caption": "Door",
|
||||
"image": "\ud83d\udeaa",
|
||||
"next": "q-door-type",
|
||||
"filter": {
|
||||
"baseType": "Door"
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption": "Window",
|
||||
"image": "\ud83e\ude9f",
|
||||
"next": "q-window-type",
|
||||
"filter": {
|
||||
"baseType": "Window"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"q-door-type": {
|
||||
"type": "question",
|
||||
"inputType": "button",
|
||||
"title": "What type of door?",
|
||||
"subtitle": "Select the door category",
|
||||
"answers": [
|
||||
{
|
||||
"caption": "Patio Door",
|
||||
"image": "\ud83d\udeaa",
|
||||
"next": "q-material-patio-door",
|
||||
"filter": {
|
||||
"baseType": "Door",
|
||||
"subType": "Patio Door"
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption": "Storm Door",
|
||||
"image": "\ud83d\udeaa",
|
||||
"next": "q-color-storm-door-aluminum",
|
||||
"filter": {
|
||||
"baseType": "Door",
|
||||
"subType": "Storm Door",
|
||||
"material": "Aluminum"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"q-material-patio-door": {
|
||||
"type": "question",
|
||||
"inputType": "button",
|
||||
"title": "Select Material",
|
||||
"subtitle": "Choose your preferred material for Patio Door",
|
||||
"conditional": {
|
||||
"type": "material",
|
||||
"fallbackNext": "q-dimensions"
|
||||
},
|
||||
"answers": [
|
||||
{
|
||||
"caption": "Aluminum",
|
||||
"image": "\ud83d\udd29",
|
||||
"next": "q-color-patio-door-aluminum",
|
||||
"filter": {
|
||||
"baseType": "Door",
|
||||
"subType": "Patio Door",
|
||||
"material": "Aluminum"
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption": "Vinyl",
|
||||
"image": "\ud83e\ude9f",
|
||||
"next": "q-color-patio-door-vinyl",
|
||||
"filter": {
|
||||
"baseType": "Door",
|
||||
"subType": "Patio Door",
|
||||
"material": "Vinyl"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"q-color-patio-door-aluminum": {
|
||||
"type": "question",
|
||||
"inputType": "button",
|
||||
"title": "Select Color",
|
||||
"subtitle": "Choose your preferred color for Aluminum Patio Door",
|
||||
"conditional": {
|
||||
"type": "color",
|
||||
"fallbackNext": "q-dimensions"
|
||||
},
|
||||
"answers": [
|
||||
{
|
||||
"caption": "Black",
|
||||
"image": "\u2b1b",
|
||||
"next": "q-dimensions",
|
||||
"filter": {
|
||||
"baseType": "Door",
|
||||
"subType": "Patio Door",
|
||||
"material": "Aluminum",
|
||||
"color": "Black"
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption": "Bronze",
|
||||
"image": "\ud83d\udfeb",
|
||||
"next": "q-dimensions",
|
||||
"filter": {
|
||||
"baseType": "Door",
|
||||
"subType": "Patio Door",
|
||||
"material": "Aluminum",
|
||||
"color": "Bronze"
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption": "Sandstone",
|
||||
"image": "\ud83d\udfe8",
|
||||
"next": "q-dimensions",
|
||||
"filter": {
|
||||
"baseType": "Door",
|
||||
"subType": "Patio Door",
|
||||
"material": "Aluminum",
|
||||
"color": "Sandstone"
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption": "White",
|
||||
"image": "\u2b1c",
|
||||
"next": "q-dimensions",
|
||||
"filter": {
|
||||
"baseType": "Door",
|
||||
"subType": "Patio Door",
|
||||
"material": "Aluminum",
|
||||
"color": "White"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"q-color-patio-door-vinyl": {
|
||||
"type": "question",
|
||||
"inputType": "button",
|
||||
"title": "Select Color",
|
||||
"subtitle": "Choose your preferred color for Vinyl Patio Door",
|
||||
"conditional": {
|
||||
"type": "color",
|
||||
"fallbackNext": "q-dimensions"
|
||||
},
|
||||
"answers": [
|
||||
{
|
||||
"caption": "Tan",
|
||||
"image": "\ud83d\udfe4",
|
||||
"next": "q-dimensions",
|
||||
"filter": {
|
||||
"baseType": "Door",
|
||||
"subType": "Patio Door",
|
||||
"material": "Vinyl",
|
||||
"color": "Tan"
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption": "White",
|
||||
"image": "\u2b1c",
|
||||
"next": "q-dimensions",
|
||||
"filter": {
|
||||
"baseType": "Door",
|
||||
"subType": "Patio Door",
|
||||
"material": "Vinyl",
|
||||
"color": "White"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"q-color-storm-door-aluminum": {
|
||||
"type": "question",
|
||||
"inputType": "button",
|
||||
"title": "Select Color",
|
||||
"subtitle": "Choose your preferred color for Aluminum Storm Door",
|
||||
"conditional": {
|
||||
"type": "color",
|
||||
"fallbackNext": "q-storm-door-sizes"
|
||||
},
|
||||
"answers": [
|
||||
{
|
||||
"caption": "Black",
|
||||
"image": "\u2b1b",
|
||||
"next": "q-storm-door-sizes",
|
||||
"filter": {
|
||||
"baseType": "Door",
|
||||
"subType": "Storm Door",
|
||||
"material": "Aluminum",
|
||||
"color": "Black"
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption": "Bronze",
|
||||
"image": "\ud83d\udfeb",
|
||||
"next": "q-storm-door-sizes",
|
||||
"filter": {
|
||||
"baseType": "Door",
|
||||
"subType": "Storm Door",
|
||||
"material": "Aluminum",
|
||||
"color": "Bronze"
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption": "Sandstone",
|
||||
"image": "\ud83d\udfe8",
|
||||
"next": "q-storm-door-sizes",
|
||||
"filter": {
|
||||
"baseType": "Door",
|
||||
"subType": "Storm Door",
|
||||
"material": "Aluminum",
|
||||
"color": "Sandstone"
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption": "White",
|
||||
"image": "\u2b1c",
|
||||
"next": "q-storm-door-sizes",
|
||||
"filter": {
|
||||
"baseType": "Door",
|
||||
"subType": "Storm Door",
|
||||
"material": "Aluminum",
|
||||
"color": "White"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"q-window-type": {
|
||||
"type": "question",
|
||||
"inputType": "button",
|
||||
"title": "What type of window?",
|
||||
"subtitle": "Select the window category",
|
||||
"answers": [
|
||||
{
|
||||
"caption": "Primary Window",
|
||||
"image": "\ud83e\ude9f",
|
||||
"next": "q-material-primary-window",
|
||||
"filter": {
|
||||
"baseType": "Window",
|
||||
"subType": "Primary Window"
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption": "Storm Window",
|
||||
"image": "\ud83e\ude9f",
|
||||
"next": "q-color-storm-window-aluminum",
|
||||
"filter": {
|
||||
"baseType": "Window",
|
||||
"subType": "Storm Window",
|
||||
"material": "Aluminum"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"q-material-primary-window": {
|
||||
"type": "question",
|
||||
"inputType": "button",
|
||||
"title": "Select Material",
|
||||
"subtitle": "Choose your preferred material for Primary Window",
|
||||
"conditional": {
|
||||
"type": "material",
|
||||
"fallbackNext": "q-dimensions"
|
||||
},
|
||||
"answers": [
|
||||
{
|
||||
"caption": "Aluminum",
|
||||
"image": "\ud83d\udd29",
|
||||
"next": "q-color-primary-window-aluminum",
|
||||
"filter": {
|
||||
"baseType": "Window",
|
||||
"subType": "Primary Window",
|
||||
"material": "Aluminum"
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption": "Vinyl",
|
||||
"image": "\ud83e\ude9f",
|
||||
"next": "q-dimensions",
|
||||
"filter": {
|
||||
"baseType": "Window",
|
||||
"subType": "Primary Window",
|
||||
"material": "Vinyl"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"q-storm-door-sizes": {
|
||||
"type": "question",
|
||||
"inputType": "button",
|
||||
"title": "Select Storm Door Size",
|
||||
"subtitle": "Choose a standard size or enter custom dimensions",
|
||||
"notes": "<p><strong>How to Measure:</strong></p><p>For accurate measurements, please refer to our <a href='https://columbiawindows.com/wp-content/uploads/2014/10/How-to-Measure2.pdf' target='_blank'>How to Measure Guide (PDF)</a>.</p><p><strong>Quick Tips:</strong></p><ul><li>Measure the rough opening, not the existing unit</li><li>Measure width at the top, middle, and bottom - use the smallest measurement</li><li>Measure height on the left, center, and right - use the smallest measurement</li><li>Round down to the nearest inch</li></ul>",
|
||||
"answers": [
|
||||
{
|
||||
"caption": "30\" x 81\"<br /><span class=\"prod_code\">2668</span>",
|
||||
"image": "📏",
|
||||
"next": "results",
|
||||
"dimensions": {
|
||||
"width": 30,
|
||||
"height": 81
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption": "32\" x 81\"<br /><span class=\"prod_code\">2868</span>",
|
||||
"image": "📏",
|
||||
"next": "results",
|
||||
"dimensions": {
|
||||
"width": 32,
|
||||
"height": 81
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption": "36\" x 81\"<br /><span class=\"prod_code\">3068</span>",
|
||||
"image": "📏",
|
||||
"next": "results",
|
||||
"dimensions": {
|
||||
"width": 36,
|
||||
"height": 81
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption": "Custom Size",
|
||||
"image": "✏️",
|
||||
"next": "q-dimensions"
|
||||
}
|
||||
]
|
||||
},
|
||||
"q-color-primary-window-aluminum": {
|
||||
"type": "question",
|
||||
"inputType": "button",
|
||||
"title": "Select Color",
|
||||
"subtitle": "Choose your preferred color for Aluminum Primary Window",
|
||||
"conditional": {
|
||||
"type": "color",
|
||||
"fallbackNext": "q-dimensions"
|
||||
},
|
||||
"answers": [
|
||||
{
|
||||
"caption": "Bronze",
|
||||
"image": "\ud83d\udfeb",
|
||||
"next": "q-dimensions",
|
||||
"filter": {
|
||||
"baseType": "Window",
|
||||
"subType": "Primary Window",
|
||||
"material": "Aluminum",
|
||||
"color": "Bronze"
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption": "White",
|
||||
"image": "\u2b1c",
|
||||
"next": "q-dimensions",
|
||||
"filter": {
|
||||
"baseType": "Window",
|
||||
"subType": "Primary Window",
|
||||
"material": "Aluminum",
|
||||
"color": "White"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"q-color-storm-window-aluminum": {
|
||||
"type": "question",
|
||||
"inputType": "button",
|
||||
"title": "Select Color",
|
||||
"subtitle": "Choose your preferred color for Aluminum Storm Window",
|
||||
"conditional": {
|
||||
"type": "color",
|
||||
"fallbackNext": "q-dimensions"
|
||||
},
|
||||
"answers": [
|
||||
{
|
||||
"caption": "Black",
|
||||
"image": "\u2b1b",
|
||||
"next": "q-dimensions",
|
||||
"filter": {
|
||||
"baseType": "Window",
|
||||
"subType": "Storm Window",
|
||||
"material": "Aluminum",
|
||||
"color": "Black"
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption": "Bronze",
|
||||
"image": "\ud83d\udfeb",
|
||||
"next": "q-dimensions",
|
||||
"filter": {
|
||||
"baseType": "Window",
|
||||
"subType": "Storm Window",
|
||||
"material": "Aluminum",
|
||||
"color": "Bronze"
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption": "Sandstone",
|
||||
"image": "\ud83d\udfe8",
|
||||
"next": "q-dimensions",
|
||||
"filter": {
|
||||
"baseType": "Window",
|
||||
"subType": "Storm Window",
|
||||
"material": "Aluminum",
|
||||
"color": "Sandstone"
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption": "White",
|
||||
"image": "\u2b1c",
|
||||
"next": "q-dimensions",
|
||||
"filter": {
|
||||
"baseType": "Window",
|
||||
"subType": "Storm Window",
|
||||
"material": "Aluminum",
|
||||
"color": "White"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"q-dimensions": {
|
||||
"type": "question",
|
||||
"inputType": "form",
|
||||
"title": "Select Storm Door Size",
|
||||
"subtitle": "Choose a standard size or enter custom dimensions",
|
||||
"notes": "<p><strong>How to Measure:</strong></p><p>For accurate measurements, please refer to our <a href='https://columbiawindows.com/wp-content/uploads/2014/10/How-to-Measure2.pdf' target='_blank'>How to Measure Guide (PDF)</a>.</p><p><strong>Quick Tips:</strong></p><ul><li>Measure the rough opening, not the existing unit</li><li>Measure width at the top, middle, and bottom - use the smallest measurement</li><li>Measure height on the left, center, and right - use the smallest measurement</li><li>Round down to the nearest inch</li></ul>",
|
||||
"measurementType": {
|
||||
"defaultValue": "opening-size",
|
||||
"options": [
|
||||
{
|
||||
"value": "opening-size",
|
||||
"label": "Opening Size",
|
||||
"disabled": false
|
||||
},
|
||||
{
|
||||
"value": "tip-to-tip",
|
||||
"label": "Tip-to-tip",
|
||||
"disabled": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"fields": [
|
||||
{
|
||||
"name": "width",
|
||||
"label": "Width (inches)",
|
||||
"type": "number",
|
||||
"required": true,
|
||||
"placeholder": "26"
|
||||
},
|
||||
{
|
||||
"name": "height",
|
||||
"label": "Height (inches)",
|
||||
"type": "number",
|
||||
"required": true,
|
||||
"placeholder": "81"
|
||||
}
|
||||
],
|
||||
"next": "results"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,922 @@
|
||||
PROD_CODE,BIT_VALUE,BIT_HEX,BIT_BINARY,DISCONTINUED,FLAGS
|
||||
1650-10,16,0x10,10000,False,Aluminum
|
||||
404,35282,0x89D2,1000100111010010,False,Window | Aluminum | Black | White | Bronze | Sandstone | Subtype:Storm Window
|
||||
450,35282,0x89D2,1000100111010010,False,Window | Aluminum | Black | White | Bronze | Sandstone | Subtype:Storm Window
|
||||
606,35282,0x89D2,1000100111010010,False,Window | Aluminum | Black | White | Bronze | Sandstone | Subtype:Storm Window
|
||||
650,33234,0x81D2,1000000111010010,False,Window | Aluminum | Black | White | Bronze | Subtype:Storm Window
|
||||
1400,160,0xA0,10100000,False,Vinyl | White
|
||||
1500,8354,0x20A2,10000010100010,False,Window | Vinyl | White | Subtype:Primary Window
|
||||
1510,160,0xA0,10100000,False,Vinyl | White
|
||||
1650,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
|
||||
1700,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
|
||||
1710,16,0x10,10000,False,Aluminum
|
||||
2000,8594,0x2192,10000110010010,False,Window | Aluminum | White | Bronze | Subtype:Primary Window
|
||||
2100,16,0x10,10000,False,Aluminum
|
||||
2200,16,0x10,10000,False,Aluminum
|
||||
2650,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
|
||||
2700,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
|
||||
2710,16,0x10,10000,False,Aluminum
|
||||
3000,464,0x1D0,111010000,False,Aluminum | Black | White | Bronze
|
||||
3100,464,0x1D0,111010000,False,Aluminum | Black | White | Bronze
|
||||
3200,464,0x1D0,111010000,False,Aluminum | Black | White | Bronze
|
||||
3300,400,0x190,110010000,False,Aluminum | White | Bronze
|
||||
3302,400,0x190,110010000,False,Aluminum | White | Bronze
|
||||
3303,400,0x190,110010000,False,Aluminum | White | Bronze
|
||||
3310,400,0x190,110010000,False,Aluminum | White | Bronze
|
||||
3700,400,0x190,110010000,False,Aluminum | White | Bronze
|
||||
3710,16,0x10,10000,False,Aluminum
|
||||
4700,400,0x190,110010000,False,Aluminum | White | Bronze
|
||||
4710,16,0x10,10000,False,Aluminum
|
||||
5200,4769,0x12A1,1001010100001,False,Door | Vinyl | White | Tan | Subtype:Patio Door
|
||||
5700,400,0x190,110010000,False,Aluminum | White | Bronze
|
||||
6700,16,0x10,10000,False,Aluminum
|
||||
265010,16,0x10,10000,False,Aluminum
|
||||
1400SCR,144,0x90,10010000,False,Aluminum | White
|
||||
1650SCR,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
|
||||
1650VS,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
|
||||
1700CSC,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
|
||||
1700CV,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
|
||||
1700ESC,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
|
||||
1700EV,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
|
||||
1700SCR,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
|
||||
1700VS,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
|
||||
2000SCR,400,0x190,110010000,False,Aluminum | White | Bronze
|
||||
2100EV,400,0x190,110010000,False,Aluminum | White | Bronze
|
||||
2200PDG,16,0x10,10000,False,Aluminum
|
||||
2650SCR,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
|
||||
2650VP,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
|
||||
2700CV,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
|
||||
2700EV,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
|
||||
2700SCR,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
|
||||
2700VS,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
|
||||
3000SCR,464,0x1D0,111010000,False,Aluminum | Black | White | Bronze
|
||||
305INS,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
|
||||
306INS,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
|
||||
3100EV,464,0x1D0,111010000,False,Aluminum | Black | White | Bronze
|
||||
3100SCR,464,0x1D0,111010000,False,Aluminum | Black | White | Bronze
|
||||
310PSCR,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
|
||||
3700SCR,400,0x190,110010000,False,Aluminum | White | Bronze
|
||||
3700VP,400,0x190,110010000,False,Aluminum | White | Bronze
|
||||
404ONE,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
|
||||
450ONE,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
|
||||
4700SCR,400,0x190,110010000,False,Aluminum | White | Bronze
|
||||
606ONE,1488,0x5D0,10111010000,False,Aluminum | Black | White | Bronze | Mill
|
||||
6100I,16785,0x4191,100000110010001,False,Door | Aluminum | White | Bronze | Subtype:Storm Door
|
||||
650ONE,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
|
||||
8100I,16785,0x4191,100000110010001,False,Door | Aluminum | White | Bronze | Subtype:Storm Door
|
||||
BELMONT,672,0x2A0,1010100000,False,Vinyl | White | Tan
|
||||
BGI,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
|
||||
BGI404,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
|
||||
BGI606,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
|
||||
BGIST,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
|
||||
C-500,2448,0x990,100110010000,False,Aluminum | White | Bronze | Sandstone
|
||||
C1150,160,0xA0,10100000,False,Vinyl | White
|
||||
C1621,16,0x10,10000,False,Aluminum
|
||||
C1622,16,0x10,10000,False,Aluminum
|
||||
C1626VA,16,0x10,10000,False,Aluminum
|
||||
C1710,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
|
||||
C1721,16,0x10,10000,False,Aluminum
|
||||
C1722,16,0x10,10000,False,Aluminum
|
||||
C1724,16,0x10,10000,False,Aluminum
|
||||
C1800,33170,0x8192,1000000110010010,False,Window | Aluminum | White | Bronze | Subtype:Storm Window
|
||||
C2021,16,0x10,10000,False,Aluminum
|
||||
C2022,16,0x10,10000,False,Aluminum
|
||||
C2026,16,0x10,10000,False,Aluminum
|
||||
C2710,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
|
||||
C300,16,0x10,10000,False,Aluminum
|
||||
C3221,464,0x1D0,111010000,False,Aluminum | Black | White | Bronze
|
||||
C3222,464,0x1D0,111010000,False,Aluminum | Black | White | Bronze
|
||||
C3223,464,0x1D0,111010000,False,Aluminum | Black | White | Bronze
|
||||
C3224,464,0x1D0,111010000,False,Aluminum | Black | White | Bronze
|
||||
C3300,400,0x190,110010000,False,Aluminum | White | Bronze
|
||||
C3700,16,0x10,10000,False,Aluminum
|
||||
C400,160,0xA0,10100000,False,Vinyl | White
|
||||
C500,2448,0x990,100110010000,False,Aluminum | White | Bronze | Sandstone
|
||||
C500GL,0,0x0,0,False,None
|
||||
C500GLP,0,0x0,0,False,None
|
||||
C500PP,2448,0x990,100110010000,False,Aluminum | White | Bronze | Sandstone
|
||||
C500SCR,2448,0x990,100110010000,False,Aluminum | White | Bronze | Sandstone
|
||||
C500VP,2448,0x990,100110010000,False,Aluminum | White | Bronze | Sandstone
|
||||
C521,400,0x190,110010000,False,Aluminum | White | Bronze
|
||||
C522,400,0x190,110010000,False,Aluminum | White | Bronze
|
||||
C526,400,0x190,110010000,False,Aluminum | White | Bronze
|
||||
C900,400,0x190,110010000,False,Aluminum | White | Bronze
|
||||
C900EV,400,0x190,110010000,False,Aluminum | White | Bronze
|
||||
C900SCR,400,0x190,110010000,False,Aluminum | White | Bronze
|
||||
C910,16,0x10,10000,False,Aluminum
|
||||
C910PP,16,0x10,10000,False,Aluminum
|
||||
C921,400,0x190,110010000,False,Aluminum | White | Bronze
|
||||
C922,400,0x190,110010000,False,Aluminum | White | Bronze
|
||||
C924,400,0x190,110010000,False,Aluminum | White | Bronze
|
||||
C931,400,0x190,110010000,False,Aluminum | White | Bronze
|
||||
C939,400,0x190,110010000,False,Aluminum | White | Bronze
|
||||
C940,400,0x190,110010000,False,Aluminum | White | Bronze
|
||||
C949,400,0x190,110010000,False,Aluminum | White | Bronze
|
||||
C960,16,0x10,10000,False,Aluminum
|
||||
COBRAI,18897,0x49D1,100100111010001,False,Door | Aluminum | Black | White | Bronze | Sandstone | Subtype:Storm Door
|
||||
COBRATI,18897,0x49D1,100100111010001,False,Door | Aluminum | Black | White | Bronze | Sandstone | Subtype:Storm Door
|
||||
CRWNFVI,16401,0x4011,100000000010001,False,Door | Aluminum | Subtype:Storm Door
|
||||
CRWNSDI,2512,0x9D0,100111010000,False,Aluminum | Black | White | Bronze | Sandstone
|
||||
D770,8354,0x20A2,10000010100010,False,Window | Vinyl | White | Subtype:Primary Window
|
||||
DSGLASS,0,0x0,0,False,None
|
||||
DURASEA,0,0x0,0,False,None
|
||||
EXPAND,16,0x10,10000,False,Aluminum
|
||||
FULLSCR,16,0x10,10000,False,Aluminum
|
||||
FVGI,2512,0x9D0,100111010000,False,Aluminum | Black | White | Bronze | Sandstone
|
||||
FVSI,2512,0x9D0,100111010000,False,Aluminum | Black | White | Bronze | Sandstone
|
||||
GOLIATH,16401,0x4011,100000000010001,False,Door | Aluminum | Subtype:Storm Door
|
||||
HERCULE,16401,0x4011,100000000010001,False,Door | Aluminum | Subtype:Storm Door
|
||||
IMPERIL,4561,0x11D1,1000111010001,False,Door | Aluminum | Black | White | Bronze | Subtype:Patio Door
|
||||
INSGLAS,0,0x0,0,False,None
|
||||
ISP,464,0x1D0,111010000,False,Aluminum | Black | White | Bronze
|
||||
IVP,4561,0x11D1,1000111010001,False,Door | Aluminum | Black | White | Bronze | Subtype:Patio Door
|
||||
JET,4497,0x1191,1000110010001,False,Door | Aluminum | White | Bronze | Subtype:Patio Door
|
||||
JSP,400,0x190,110010000,False,Aluminum | White | Bronze
|
||||
JVP,4497,0x1191,1000110010001,False,Door | Aluminum | White | Bronze | Subtype:Patio Door
|
||||
KINGDVI,18897,0x49D1,100100111010001,False,Door | Aluminum | Black | White | Bronze | Sandstone | Subtype:Storm Door
|
||||
KINGFSC,2512,0x9D0,100111010000,False,Aluminum | Black | White | Bronze | Sandstone
|
||||
KINGI,18897,0x49D1,100100111010001,False,Door | Aluminum | Black | White | Bronze | Sandstone | Subtype:Storm Door
|
||||
KINGSDI,18897,0x49D1,100100111010001,False,Door | Aluminum | Black | White | Bronze | Sandstone | Subtype:Storm Door
|
||||
M1200,4497,0x1191,1000110010001,False,Door | Aluminum | White | Bronze | Subtype:Patio Door
|
||||
M306,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
|
||||
OUTSIDE,2512,0x9D0,100111010000,False,Aluminum | Black | White | Bronze | Sandstone
|
||||
PATIOSC,6609,0x19D1,1100111010001,False,Door | Aluminum | Black | White | Bronze | Sandstone | Subtype:Patio Door
|
||||
PDSCRTT,6609,0x19D1,1100111010001,False,Door | Aluminum | Black | White | Bronze | Sandstone | Subtype:Patio Door
|
||||
PRPDS,2512,0x9D0,100111010000,False,Aluminum | Black | White | Bronze | Sandstone
|
||||
PRSCR,3984,0xF90,111110010000,False,Aluminum | White | Bronze | Tan | Mill | Sandstone
|
||||
PRSCR11,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
|
||||
PSINS,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
|
||||
PWS,4048,0xFD0,111111010000,False,Aluminum | Black | White | Bronze | Tan | Mill | Sandstone
|
||||
PWSINS,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
|
||||
R1150,160,0xA0,10100000,False,Vinyl | White
|
||||
R1400,8354,0x20A2,10000010100010,False,Window | Vinyl | White | Subtype:Primary Window
|
||||
R1500,8354,0x20A2,10000010100010,False,Window | Vinyl | White | Subtype:Primary Window
|
||||
R1510,32,0x20,100000,False,Vinyl
|
||||
R2000,8594,0x2192,10000110010010,False,Window | Aluminum | White | Bronze | Subtype:Primary Window
|
||||
R2100,400,0x190,110010000,False,Aluminum | White | Bronze
|
||||
R2100EV,400,0x190,110010000,False,Aluminum | White | Bronze
|
||||
R2200,400,0x190,110010000,False,Aluminum | White | Bronze
|
||||
R300,16,0x10,10000,False,Aluminum
|
||||
R3302,400,0x190,110010000,False,Aluminum | White | Bronze
|
||||
R400,160,0xA0,10100000,False,Vinyl | White
|
||||
R770,8354,0x20A2,10000010100010,False,Window | Vinyl | White | Subtype:Primary Window
|
||||
R770SCR,16,0x10,10000,False,Aluminum
|
||||
RCKTRAP,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
|
||||
REWIRE,16,0x10,10000,False,Aluminum
|
||||
RNDROCK,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
|
||||
ROCKET,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
|
||||
RROCKET,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
|
||||
SCRI,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
|
||||
SCRI404,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
|
||||
SCRI606,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
|
||||
SCRI808,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
|
||||
SSGLASS,0,0x0,0,False,None
|
||||
SSSCR,16,0x10,10000,False,Aluminum
|
||||
TBGI,2512,0x9D0,100111010000,False,Aluminum | Black | White | Bronze | Sandstone
|
||||
TBR,4561,0x11D1,1000111010001,False,Door | Aluminum | Black | White | Bronze | Subtype:Patio Door
|
||||
TGI,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
|
||||
TGI404,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
|
||||
TGI606,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
|
||||
TGIODD,2512,0x9D0,100111010000,False,Aluminum | Black | White | Bronze | Sandstone
|
||||
THOR,16401,0x4011,100000000010001,False,Door | Aluminum | Subtype:Storm Door
|
||||
TVI,18897,0x49D1,100100111010001,False,Door | Aluminum | Black | White | Bronze | Sandstone | Subtype:Storm Door
|
||||
VKI,2512,0x9D0,100111010000,False,Aluminum | Black | White | Bronze | Sandstone
|
||||
VP3700,400,0x190,110010000,False,Aluminum | White | Bronze
|
||||
WINDGAT,672,0x2A0,1010100000,False,Vinyl | White | Tan
|
||||
ZBARS,2512,0x9D0,100111010000,False,Aluminum | Black | White | Bronze | Sandstone
|
||||
COBRATK,18897,0x49D1,100100111010001,False,Door | Aluminum | Black | White | Bronze | Sandstone | Subtype:Storm Door
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
,0,0x0,0,False,None
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"COBRAI": "1100",
|
||||
"COBRATI": "1100",
|
||||
"TVI": "1100",
|
||||
"TRTI": "1100",
|
||||
"1400": "1400",
|
||||
"1500": "1500",
|
||||
"1650": "1650",
|
||||
"2000": "2000",
|
||||
"1700": "1700",
|
||||
"2100": "2100",
|
||||
"2700": "2700",
|
||||
"3100": "3100",
|
||||
"1510": "1510",
|
||||
"2200": "2200",
|
||||
"3200": "3200",
|
||||
"3300": "3300",
|
||||
"3302": "3302",
|
||||
"3303": "3303",
|
||||
"3310": "3310",
|
||||
"2400": "2400"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,998 @@
|
||||
,,,,,,Base,Sub-type,,Accessory,,Materials,,Colors,,,,,,
|
||||
DISCONT,LOC_CODE,PROD_CODE,CATEGORY,CATEGORY_NEW,DESCRIPTION,Type,Door,Window,Yes,This Item,Aluminum,Vinyl,Black,White,Bronze,Tan,Mill,Sandstone,~
|
||||
FALSE,Iola,1650-10,FPPW,,#1650-10 INSULATED FIXED LITE,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,303,STORMS,,#303 DART STORM WINDOWS,Window,,Storm Window,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,350,STORMS,,#350 ARROW STORM WINDOWS,Window,,Storm Window,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,404,STORMS,,#404 FALCON STORM WINDOWS,Window,,Storm Window,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
|
||||
FALSE,Iola,450,STORMS,,#450 RAVEN STORM WINDOWS,Window,,Storm Window,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
|
||||
FALSE,Iola,606,STORMS,,#606 LION STORM WINDOWS,Window,,Storm Window,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
|
||||
FALSE,Iola,650,STORMS,,#650 LYNX STORM WINDOWS,Window,,Storm Window,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,808,STORMS,,808 HAWK STORM WINDOW,Window,,Storm Window,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,850,STORMS,,#850 KENT STORM WINDOWS,Window,,Storm Window,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,1400,SHPW,,SERIES 1400 VINYL SLIDING PRIMARY,,,,FALSE,FALSE,FALSE,TRUE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,1500,SHPT,,SERIES 1500 S.H. TILT VINYL PRIMARY,Window,,Primary Window,FALSE,FALSE,FALSE,TRUE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,1510,FPPW,,1510 VINYL INSULATED FIXED LITE,,,,FALSE,FALSE,FALSE,TRUE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,1650,SHPW,,#1650 INSULATED SINGLE HUNG,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
|
||||
FALSE,Iola,1700,SHPW,,#1700 INSULATED SLIDER,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
|
||||
FALSE,Iola,1710,FPPW,,C-1710 FIXED LITE,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,2000,SHPT,,SERIES 2000 S.H. T.B. TILT PRIMARY,Window,,Primary Window,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,2100,SHPT,,SERIES 2100 THERMAL BREAK SLIDER,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,2200,FPPW,,SERIES 2200 T.B. FIXED LITE,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,2400,PD,,2400 ROYAL CROWN PATIO DOOR,Door,Patio Door,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,2650,SHPW,,#2650 SINGLE HUNG SINGLE GLAZED PRIME,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
|
||||
FALSE,Iola,2700,SHPW,,#2700 SINGLE GLAZED SLIDER,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
|
||||
FALSE,Iola,2710,FPPW,,C-2710 FIXED LITE - SINGLE GLAZED,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,3000,3000DHP,#N/A,SERIES 3000 D.H. T.B REPLACEMENT WD,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,3100,3000DHP,#N/A,SERIES 3100 T.B. REPLACEMENT SLIDER,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,3200,3000FPP,#N/A,SERIES 3200 T.B. PICTURE WINDOW,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,3300,CASEMNT,,3300 1-PANEL T.B. CASEMENT,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,3302,CASEMNT,,3300 2-PANEL T.B. CASEMENT,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,3303,CASEMNT,,3300 3-PANEL T.B. CASEMENT,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,3310,CASEMNT,,3310 FIXED CASEMENT,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,3400,CASEMNT,,3400 1 PANEL VINYL CLAD CASEMENT,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,3402,CASEMNT,,3400 2 PANEL VINYL CLAD CASEMENT,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,3403,CASEMNT,,3400 3 PANEL VINYL CLAD CASEMENT,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,3500,CASEMNT,,3500 1-PANEL WOOD INTERIOR CASEMENT,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,3502,CASEMNT,,3500 2-PANEL WOOD INTERIOR CASEMENT,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,3503,CASEMNT,,3503 3-PANEL WOOD INTERIOR CASEMENT,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,3700,SHPW,,#3700 INSULATED SLIDER,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,3710,FPPW,,C-3710 INSULATED FIXED LITE,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,4100,REPLACE,,SERIES 4100 VINYL SLIDER,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,4700,SHPW,,#4700 SINGLE GLAZED SLIDER,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,4710,FPPW,,#4710 SINGLE GLAZED FIXED LITE,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,5200,PD,,5200 VINYL PATIO DOORS,Door,Patio Door,,FALSE,FALSE,FALSE,TRUE,FALSE,TRUE,FALSE,TRUE,FALSE,FALSE,
|
||||
FALSE,Iola,5700,SHPW,,#5700 INSULATED SLIDER,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,6300,SHPT,,SERIES 6300 S.H. TILT VINYL PRIMARY,Window,,Primary Window,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,6301,SHPW,,SERIES 6301 VINYL SLIDING PRIMARY,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,6700,SHPW,,#6700 SINGLE GLAZED SLIDER,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,265010,FPPW,,#2650-10 SINGLE GLAZED FIXED LITE,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,1400SCR,SHPW,,1400 SCREEN,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,1650SCR,SCREENS,,SCREENS FOR #1650,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
|
||||
FALSE,Iola,1650VS,SHPW,,1650 BOTTOM SASH,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
|
||||
FALSE,Iola,1700CSC,SCREENS,,SCREEN FOR #1700 CENTER VENT SLIDER,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
|
||||
FALSE,Iola,1700CV,SHPW,,1700 CENTER VENT,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
|
||||
FALSE,Iola,1700ESC,SCREENS,,SCREEN FOR #1700 ENDS VENT SLIDER,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
|
||||
FALSE,Iola,1700EV,SHPW,,1700 END VENT,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
|
||||
FALSE,Iola,1700SCR,SCREENS,,SCREEN FOR #1700 SLIDER,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
|
||||
FALSE,Iola,1700VS,SHPW,,#1700 VENT SASH,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
|
||||
FALSE,Iola,2000SCR,SHPT,,C-2000 SCREEN,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,2100EV,2000SLI,#N/A,SERIES 2100 3-PANEL SLIDER,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,2200PDG,FPPW,,SERIES 2200 TB FIXED LITE W/TEMP GLASS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,2650SCR,SCREENS,,SCREENS FOR 2650,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
|
||||
FALSE,Iola,2650VP,SHPW,,#2650 VENT PANEL - SINGLE HUNGE,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
|
||||
FALSE,Iola,2700CV,SHPW,,C-2700 - CENTER VENT,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
|
||||
FALSE,Iola,2700EV,SHPW,,C-2700 - END VENT,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
|
||||
FALSE,Iola,2700SCR,SCREENS,,SCREENS FOR #2700,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
|
||||
FALSE,Iola,2700VS,SHPW,,#2700 VENT SASH,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
|
||||
FALSE,Iola,3000SCR,SCREENS,,SCREENS FOR #3000,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,305INS,INSERTS,,#305 SASH WINDCHECK INSERTS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
|
||||
FALSE,Iola,306INS,INSERTS,,#306 SCHLEGEL GLASS INSERTS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
|
||||
FALSE,Iola,3100EV,3000DHP,#N/A,SERIES 3100 3-PANEL SLIDER,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,3100SCR,SCREENS,,SCREENS FOR #3100,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,310PSCR,SCRINS,,#310 PLAIN SCREENS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
|
||||
FALSE,Iola,3700SCR,SCREENS,,SCREEN FOR #3700 POLE BARN WD,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,3700VP,SHPW,,#3700 VENT PANEL,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,404ONE,STPW,,#404 ONE-LITE ST WD,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
|
||||
FALSE,Iola,450ONE,STPW,,#450 ONE-LITE ST WD,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
|
||||
FALSE,Iola,4700SCR,SCREENS,,SCREENS FOR #4700,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,5000SCR,SCREENS,,SCREEN FOR R5000 SLIDER,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,606ONE,STPW,,#606 ONE-LITE ST WD,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,FALSE,
|
||||
FALSE,Iola,6100I,STD,,STAR 6100 FULL VIEW STORM DOOR,Door,Storm Door,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,6100L,STD,,STAR 6100 FULL VIEW STORM DOOR,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,650ONE,STPW,,#650 ONE-LITE ST WD,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
|
||||
TRUE,Iola,7100I,STD,,STAR 7100 FULL VIEW SELF STORING DOOR,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,7100L,STD,,STAR 7100 FULL VIEW SELF STORING DOOR,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,808ONE,STPW,,#808 ONE-LITE ST WD,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,8100I,STD,,STAR 8100 FULL VIEW STORM DOOR,Door,Storm Door,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,8100L,STD,,STAR 8100 FULL VIEW STORM DOOR,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,850ONE,STPW,,#850 ONE-LITE ST WD,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,BELMONT,3000FPP,#N/A,BELMONT ALLIANCE DOUBLE HUNG WINDOW,,,,FALSE,FALSE,FALSE,TRUE,FALSE,TRUE,FALSE,TRUE,FALSE,FALSE,
|
||||
FALSE,Iola,BGI,INSERTS,,BOTTOM GL INSERTS FOR ECONOMY ST WDS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
|
||||
FALSE,Iola,BGI404,INSERTS,,BOTTOM GL INSERTS FOR #404/450 ST WDS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
|
||||
FALSE,Iola,BGI606,INSERTS,,BOTTOM GL INSERTS FOR #606/650 ST WDS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
|
||||
FALSE,Iola,BGIST,INSERTS,,INSERTS FOR STORM DOORS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
|
||||
FALSE,Iola,C-500,SHPW,,C-500 SH THERMAL BREAK INS.,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,TRUE,
|
||||
FALSE,Iola,C1150,CASEMNT,,C1150-VINYL AWNING WINDOW,,,,FALSE,FALSE,FALSE,TRUE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,C1500AR,CIR TOP,,C-1510 VINYL ARCH TOP-OPERATING WINDOW,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,C1510AR,CIR TOP,,C-1510 VINYL ARCH TOP WINDOW,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,C1521,CIR TOP,,C-1521 VINYL CIRCLE TOP,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,C1526,CIR TOP,,C-1526 VINYL CIRCLE TOP,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,C1621,CIR TOP,,C-1621 INSULATED CIRCLE TOP,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,C1622,CIR TOP,,C-1622 INSULATED CIRCLE TOP,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,C1626VA,CIR TOP,,C-1626VA INSULATED CIRCLE TOPS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,C1710,SHPW,,#1710 PICTURE OVER SLIDER,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
|
||||
FALSE,Iola,C1721,CIR TOP,,C-1721 INSULATED CIRCLE TOPS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,C1722,CIR TOP,,C-1722 INSULATED CIRCLE TOPS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,C1724,CIR TOP,,C-1724 INSULATED CIRCLE TOPS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,C1800,C1800,#N/A,C-1800 INSIDE SLIDING STORM WINDOW,Window,,Storm Window,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,C2021,CIR TOP,,C-2021 T.B CIRCLE TOPS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,C2022,CIR TOP,,C-2022 T.B. CIRCLE TOPS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,C2026,CIR TOP,,C-2026 T.B. CIRCLE TOPS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,C2710,SHPW,,#2710 PICTURE OVER SLIDER,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
|
||||
FALSE,Iola,C300,BSMT,#N/A,C-300 ALUM INSERTS FOR BASEMENT BUCKS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,C3221,CIR TOP,,C-3221 T.B CIRCLE TOPS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,C3222,CIR TOP,,C-3222 T.B. CIRCLE TOPS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,C3223,CIR TOP,,C-3223 T.B. CIRCLE TOPS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,C3224,CIR TOP,,C-3224 T.B. CIRCLE TOPS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,C3300,CASEMNT,,3300 1-PANEL T.B. CASEMENT,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,C3700,SHPW,,C-3700 INSULATED SLIDING POLE BARN WND,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,C400,BSMT,#N/A,C-400 VINYL INSERT FOR BASEMENT BUCKS,,,,FALSE,FALSE,FALSE,TRUE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,C4000,REPLACE,,C-4000 SINGLE HUNGE PRIMARY WINDOW,Window,,Primary Window,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,C4260,PD,,C-4260 STEEL MIRROR DOOR,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,C500,SHPW,,C-500 INS THERMAL BREAK SINGLE HUNG,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,TRUE,
|
||||
FALSE,Iola,C500GL,VENTS,,LITES OF INSULATED GLASS FOR C-500'S,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,C500GLP,VENTS,,LITES OF INSULATED GLASS FOR C-500'S,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,C500PP,SHPW,,C-500 INS THERMAL BREAK SINGLE HUNG,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,TRUE,
|
||||
FALSE,Iola,C500SCR,SCREENS,,C-500 SCREEN,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,TRUE,
|
||||
FALSE,Iola,C500VP,VENTS,,VENT PANELS FOR C-500,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,TRUE,
|
||||
FALSE,Iola,C521,CIR TOP,,C-521 T.B. CIRCLE TOPS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,C522,CIR TOP,,C-522 T.B. CIRCLE TOPS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,C526,CIR TOP,,C-526 T.B. CIRCLE TOPS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,C610,CASEMNT,,C610 VINYL CASEMENT WINDOW,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,C620,FPPW,,C620 VINYL AWNING WINDOWS,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,C621,CIR TOP,,C-621 VINYL CIRCLE TOP,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,C626,CIR TOP,,C-626 VINYL CIRCLE TOP,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,C640,FPPW,,C-640 VINYL FIXED CASEMENT WINDOW,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,C826,CIR TOP,,C-826 VINYL CIRCLE TOP,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,C828,CIR TOP,,C-828 VINYL CIRCLE TOP,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,C8321,CIR TOP,,C-8321 VINYL CIRCLE TOP,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,C8326,CIR TOP,,C-8326 VINYL CIRCLE TOP,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,C900,SHPW,,C-900 INS THERMAL BREAK SLIDER,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,C900EV,SHPW,,C-900 ENDS VENT SLIDER,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,C900SCR,SCREENS,,SCREEN FOR C-900,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,C910,FPPW,,C-910 INSULATED T.B. FIXED LITE,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,C910PP,FPPW,,C-910 ARCH TOP,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,C921,CIR TOP,,C-921 T.B. CIRCLE TOPS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,C922,CIR TOP,,C-922 T.B. CIRCLE TOPS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,C924,CIR TOP,,C-924 T.B. CIRCLE TOPS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,C931,CIR TOP,,C-931 T.B. ROUND PRIMARY WINDOW,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,C939,CIR TOP,,C-939 T.B. ROUND PRIMARY WINDOW,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,C940,CIR TOP,,C-940 T.B. OCTAGON PRIMARY,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,C949,CIR TOP,,C949 OCTAGON THERMAL BREAK WINDOW,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,C960,FPPW,,C-960 INSULATED T.B. CIRCLE TOP,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,COBRAI,STD,,COLUMBIA COBRA STORM DOOR,Door,Storm Door,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
|
||||
TRUE,Iola,COBRAL,STD,,COLUMBIA COBRA STORM DOOR,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,COBRATI,STD,,COLUMBIA COBRA STORM DOOR,Door,Storm Door,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
|
||||
TRUE,Iola,COBRATL,STD,,COLUMBIA COBRA STORM DOOR,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,CRWNFVI,STD,,CROWN FULL VIEW STORM DOOR,Door,Storm Door,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,CRWNFVL,STD,,CROWN FULL VIEW STORM DOOR,Door,Storm Door,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,CRWNSDI,STD,,COLUMBIA CROWN SCREEN DOOR,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
|
||||
TRUE,Iola,CRWNSDL,STD,,COLUMBIA CROWN SCREEN DOOR,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,D770,SHPT,,D770 D.H. TILT VINYL PRIMARY WINDOWS,Window,,Primary Window,FALSE,FALSE,FALSE,TRUE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,D780,SHPW,,D780 DOUBLE SLIDE VINYL PRIMARY,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,D830,SHPT,,D830 D.H.TILT VINYL PRIMARY WINDOWS,Window,,Primary Window,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,D830SCR,SHPT,,D830 SCREEN,Window,,Primary Window,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,D832,FPPW,,D832 FIXED VINYL PRIMARY WINDOWS,Window,,Primary Window,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,DSGLASS,GLASS,,DOUBLE STRENGTH GLASS,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,DURASEA,FPPW,,"DURASEAL 5/8"" (GRAY) PER REEL",,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,EXPAND,EXPANDR,,SILL EXPANDERS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,FULLSCR,SCREENS,,FULL SCREEN,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,FV10I,STD,,KING FV-10 DECORATOR STORM DOOR,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,FV10L,STD,,KING FV-10 DECORATOR STORM DOOR,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,FV3I,STD,,KING FV-3 DECORATOR STORM DOOR,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,FV3L,STD,,KING FV-3 DECORATOR STORM DOOR,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,FVGI,INSERTS,,GLASS INSERTS FOR KING ONE LITE,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
|
||||
FALSE,Iola,FVSI,INSERTS,,FULL SCREENS ONLY FOR KING ONE-LITES,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
|
||||
TRUE,Iola,G3000,GARDEN,,COLUMBIA COMFORT 3000 VINYL GARDEN WD,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,GOLIATH,STD,,GOLIATH STORM DOOR,Door,Storm Door,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,HERCULE,STD,,HERCULES STORM DOOR,Door,Storm Door,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,IMPERIL,PD,,IMPERIAL PATIO DOORS,Door,Patio Door,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,INSGLAS,GLASS,,INSULATED GLASS,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,ISP,PD,,STATIONARY PANELS FOR IMPERIAL DOORS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,IVP,PD,,VENT PANELS FOR IMPERIAL PATIO DOORS,Door,Patio Door,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,JET,PD,,COLUMBIA JET PATIO DOORS,Door,Patio Door,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,JSP,PD,,STATIONARY PANELS FOR JET DOORS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,JVP,PD,,VENT PANELS FOR JET PATIO DOORS,Door,Patio Door,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,KINGDVI,STD,,KING DUAL VENT STORM DOORS,Door,Storm Door,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
|
||||
TRUE,Iola,KINGDVL,STD,,KING DUAL VENT STORM DOORS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,KINGFSC,INSERTS,,FULL SCREEN FOR KING ONE-LITE,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
|
||||
FALSE,Iola,KINGI,STD,,KING ONE-LITE STORM DOORS,Door,Storm Door,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
|
||||
TRUE,Iola,KINGL,STD,,KING ONE-LITE STORM DOORS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,KINGSDI,STD,,KING ONE-LITE SCREEN DOOR ONLY,Door,Storm Door,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
|
||||
TRUE,Iola,KINGSDL,STD,,KING ONE-LITE SCREEN DOOR ONLY,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,LINCOLN,3000DHP,#N/A,LINCOLN PRIMARY WOOD WINDOWS,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,LINCPDR,PD,,LINCOLN FRENCH PATIO DOOR,Door,Patio Door,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,M1200,PD,,M1200 PATIO STORM DOOR,Door,Patio Door,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,M306,INSERTS,,M-306 SCHLEGEL GLASS INSERTS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
|
||||
FALSE,Iola,OUTSIDE,STDMISC,,OUTSIDE DOOR SWEEPS - ALUM + VINYL,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
|
||||
FALSE,Iola,PATIOSC,SCREENS,,PATIO DOOR SCREENS,Door,Patio Door,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
|
||||
FALSE,Iola,PDSCRTT,SCREENS,,SPECIAL SIZE SCREENS FOR PATIO DOORS,Door,Patio Door,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
|
||||
FALSE,Iola,PRPDS,SCREENS,,SCREENS MADE FROM PLAIN PATIO SCR RAIL,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
|
||||
FALSE,Iola,PRSCR,PSCREEN,,SCREENS FOR PRIME MADE FROM #19-88,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,TRUE,TRUE,TRUE,
|
||||
FALSE,Iola,PRSCR11,PSCREEN,,SCREEN FOR PRIME MADE FROM #19-11,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
|
||||
FALSE,Iola,PSINS,INSERTS,,PLAIN SASH INSERTS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
|
||||
FALSE,Iola,PWS,STPW,,PIN-ON PICTURE WINDOWS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,TRUE,TRUE,TRUE,
|
||||
FALSE,Iola,PWSINS,INSERTS,,INSERTS ONLY FOR PIN-ON PICTURE WINDOW,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
|
||||
FALSE,Iola,R1150,REPLACE,,R-1150 VINYL AWNING WINDOWS,,,,FALSE,FALSE,FALSE,TRUE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,R1400,REPLACE,,SERIES 1400 VINYL SLIDING PRIMARY,Window,,Primary Window,FALSE,FALSE,FALSE,TRUE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,R1500,REPLACE,,SERIES 1500 S.H. TILT VINYL PRIMARY,Window,,Primary Window,FALSE,FALSE,FALSE,TRUE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,R1510,FPPW,,SERIES 1510 FIXED LITE VINYL PRIMARY,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,R2000,REPLACE,,SERIES 2000 S.H. T.B. TILT PRIMARY,Window,,Primary Window,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,R2100,REPLACE,,SERIES 2100 THERMAL BREAK SLIDER,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,R2100EV,R2000SL,,SERIES 2100 3-PANEL T.B. SLIDER,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,R2200,RFPPW,,SERIES 2200 T.B. FIXED LITE,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,R300,RBSMT,,C-300 ALUM INSERTS FOR BASEMENT BUCKS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,R3302,CASEMNT,,3302 2-PANEL T.B. CASEMENT,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,R400,RBSMT,,C-400 VINYL INSERT FOR BASEMENT BUCKS,,,,FALSE,FALSE,FALSE,TRUE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,R5000,REPLACE,,R-5000 INSULATED ALUMINUM SLIDER,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,R770,REPLACE,,R770 D.H. TILT VINYL PRIMARY WINDOWS,Window,,Primary Window,FALSE,FALSE,FALSE,TRUE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,R770SCR,SCREENS,,FULL SCREEN,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,R780,REPLACE,,R780 VINYL SLIDING PRIMARY,Window,,Primary Window,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,R820,REPLACE,,R820 S.H. TILT VINYL PRIMARY WINDOWS,Window,,Primary Window,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,R821,REPLACE,,S821 SINGLE SLIDE VINYL PRIMARY,Window,,Primary Window,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,R822,FPPW,,S822 FIXED VINYL PRIMARY WINDOWS,Window,,Primary Window,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,R830,REPLACE,,R830 D.H.TILT VINYL PRIMARY WINDOWS,Window,,Primary Window,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,R832,FPPW,,R832 FIXED VINYL PRIMARY WINDOWS,Window,,Primary Window,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,RCKTRAP,INSERTS,,ROCKET TRAPEZOIDS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
|
||||
FALSE,Iola,REWIRE,SCREENS,,REWIRED SCREENS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,RNDROCK,INSERTS,,ROUND ROCKET INSERT,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
|
||||
FALSE,Iola,ROCKET,INSERTS,,ROCKET INSERTS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
|
||||
TRUE,Iola,ROYAL,STD,,COLUMBIA ROYAL STORM DOOR,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,RROCKET,INSERTS,,RADIUS ROCKETS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
|
||||
TRUE,Iola,S820,SHPT,,S820 S.H. TILT VINYL PRIMARY WINDOWS,Window,,Primary Window,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,S821,SHPW,,S821 SINGLE SLIDE VINYL PRIMARY,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,S822,FPPW,,S822 FIXED VINYL PRIMARY WINDOWS,Window,,Primary Window,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,SCRI,INSERTS,,SCREEN INSERTS FOR ECONOMY STORM WDS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
|
||||
FALSE,Iola,SCRI404,INSERTS,,SCREEN INSERTS FOR #404/450 STORM WDS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
|
||||
FALSE,Iola,SCRI606,INSERTS,,SCREEN INSERTS FOR #606/650 STORM WDS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
|
||||
FALSE,Iola,SCRI808,INSERTS,,SCREEN INSERTS FOR #808/850 STORM WIND,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
|
||||
TRUE,Iola,SS10I,STD,,COBRA SS-10 DECORATOR STORM DOOR,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,SS10L,STD,,COBRA SS-10 DECORATOR STORM DOOR,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,SS3I,STD,,COBRA SS-3 DECORATOR STORM DOOR,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,SS3L,STD,,COBRA SS-3 DECORATOR STORM DOOR,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,SSGLASS,GLASS,,SINGLE STRENGTH GLASS,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,SSSCR,INSERTS,,SCREEN INSERT FOR SELF STORING DOORS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,TBGI,INSERTS,,TEMPERED GLASS INSERTS FOR SS DOORS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
|
||||
FALSE,Iola,TBR,PD,,IMPERIAL T.B.R. REPLACEMENT PATIO DOOR,Door,Patio Door,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,TGI,INSERTS,,TOP GL INSERTS FOR ECONOMY ST WDS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
|
||||
FALSE,Iola,TGI404,INSERTS,,TOP GLASS INSERTS FOR #404/450 ST WDS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
|
||||
FALSE,Iola,TGI606,INSERTS,,TOP GLASS INSERTS FOR #606/650 ST WDS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
|
||||
FALSE,Iola,TGIODD,INSERTS,,TOP GLASS INSERTS FOR STORM DOORS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
|
||||
FALSE,Iola,THOR,STD,,THOR - STORM DOOR,Door,Storm Door,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,TIARAI,STD,,COLUMBIA TIARA SELF STORING STORM DOOR,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,TIARAL,STD,,COLUMBIA TIARA SELF STORING STORM DOOR,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
TRUE,Iola,TVGROOV,INSERTS,,TEMPERED V-GROOVE ONE-LITE INSERTS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,TVI,STD,,COLUMBIA COBRA TWIN VENT STORM DOOR,Door,Storm Door,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
|
||||
TRUE,Iola,TVL,STD,,COLUMBIA COBRA TWIN VENT STORM DOOR,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,VKI,STOCK,,VENTILATOR KICKPANEL FOR KING ONELITE,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
|
||||
TRUE,Iola,VKS,STOCK,,VENTILATOR SCREEN FOR KING ONE LITE,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
|
||||
FALSE,Iola,VP3700,VENTS,,VENT PANELS FOR #3700,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,WINDGAT,VACW,,ALLIANCE WINDGATE CASEMENT WINDOW,,,,FALSE,FALSE,FALSE,TRUE,FALSE,TRUE,FALSE,TRUE,FALSE,FALSE,
|
||||
TRUE,Iola,XBUCKIN,INSERTS,,TEMPERED GLASS INSERTS FOR CROSSBUCKS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,Iola,ZBARS,STDMISC,,Z-BARS FOR STORM DOORS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
|
||||
FALSE,KC,COBRATK,STD,,Cobra aluminum storm door with Glass Kick Panel,Door,Storm Door,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
|
||||
|
+16794
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,523 @@
|
||||
{
|
||||
"start": {
|
||||
"type": "question",
|
||||
"inputType": "button",
|
||||
"title": "What are you looking for?",
|
||||
"subtitle": "Select the product category",
|
||||
"notes": "<p><strong>Quick Tips:</strong></p><ul><li>Press <strong>Ctrl+Shift+R</strong> (Windows) or <strong>Cmd+Shift+R</strong> (Mac) to hard refresh the page</li><li>Press <strong>F5</strong> to reload the application</li><li>Use the back button or breadcrumbs to navigate</li></ul>",
|
||||
"answers": [
|
||||
{
|
||||
"caption": "Window",
|
||||
"image": "🪟",
|
||||
"next": "q-dimensions"
|
||||
},
|
||||
{
|
||||
"caption": "Door",
|
||||
"image": "🚪",
|
||||
"next": "q-dimensions"
|
||||
},
|
||||
{
|
||||
"caption": "Patio Door",
|
||||
"image": "🚪",
|
||||
"next": "q-dimensions"
|
||||
}
|
||||
]
|
||||
},
|
||||
"q-dimensions": {
|
||||
"type": "question",
|
||||
"inputType": "form",
|
||||
"title": "Select Storm Door Size",
|
||||
"subtitle": "Choose a standard size or enter custom dimensions",
|
||||
"notes": "<p><strong>Need help measuring?</strong></p><p>For accurate measurements, please refer to our <a href='https://columbiawindows.com/wp-content/uploads/2014/10/How-to-Measure2.pdf' target='_blank'>How to Measure Guide</a>.</p><p><strong>Tips:</strong></p><ul><li>Measure the rough opening, not the existing unit</li><li>Measure width at the top, middle, and bottom - use the smallest measurement</li><li>Measure height on the left, center, and right - use the smallest measurement</li><li>Round down to the nearest inch</li></ul>",
|
||||
"measurementType": {
|
||||
"defaultValue": "opening-size",
|
||||
"options": [
|
||||
{
|
||||
"value": "opening-size",
|
||||
"label": "Opening Size",
|
||||
"disabled": false
|
||||
},
|
||||
{
|
||||
"value": "tip-to-tip",
|
||||
"label": "Tip-to-tip",
|
||||
"disabled": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"fields": [
|
||||
{
|
||||
"name": "width",
|
||||
"label": "Width (inches)",
|
||||
"type": "number",
|
||||
"required": true,
|
||||
"placeholder": "26"
|
||||
},
|
||||
{
|
||||
"name": "height",
|
||||
"label": "Height (inches)",
|
||||
"type": "number",
|
||||
"required": true,
|
||||
"placeholder": "81"
|
||||
}
|
||||
],
|
||||
"next": "q-material"
|
||||
},
|
||||
"q-material": {
|
||||
"type": "question",
|
||||
"inputType": "form",
|
||||
"title": "Material & Color Preferences",
|
||||
"subtitle": "Customize your selection",
|
||||
"fields": [
|
||||
{
|
||||
"name": "material",
|
||||
"label": "Material",
|
||||
"type": "select",
|
||||
"required": true,
|
||||
"options": [
|
||||
{ "value": "", "label": "-- Select Material --" },
|
||||
{ "value": "vinyl", "label": "Vinyl" },
|
||||
{ "value": "aluminum", "label": "Aluminum" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "color",
|
||||
"label": "Color",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"placeholder": "e.g., White, Beige, etc."
|
||||
}
|
||||
],
|
||||
"next": "q-material-conditional"
|
||||
},
|
||||
"q-window-type": {
|
||||
"type": "question",
|
||||
"inputType": "button",
|
||||
"title": "What type of window?",
|
||||
"subtitle": "Select the category that best matches your needs",
|
||||
"answers": [
|
||||
{
|
||||
"caption": "Single Hung Window",
|
||||
"image": "🪟",
|
||||
"next": "q-single-hung"
|
||||
},
|
||||
{
|
||||
"caption": "Slider Window",
|
||||
"image": "↔️",
|
||||
"next": "q-slider"
|
||||
},
|
||||
{
|
||||
"caption": "Fixed Lite",
|
||||
"image": "🔲",
|
||||
"next": "q-fixed"
|
||||
},
|
||||
{
|
||||
"caption": "Casement Window",
|
||||
"image": "🚪",
|
||||
"next": "q-casement"
|
||||
}
|
||||
]
|
||||
},
|
||||
"q-door-type": {
|
||||
"type": "question",
|
||||
"inputType": "button",
|
||||
"title": "What type of door?",
|
||||
"subtitle": "Select your door style",
|
||||
"answers": [
|
||||
{
|
||||
"caption": "Entry Door",
|
||||
"image": "🚪",
|
||||
"next": "prod-entry-door"
|
||||
},
|
||||
{
|
||||
"caption": "Storm Door",
|
||||
"image": "🚪",
|
||||
"next": "prod-cobra-1100"
|
||||
},
|
||||
{
|
||||
"caption": "Patio Door",
|
||||
"image": "🚪",
|
||||
"next": "q-patio"
|
||||
}
|
||||
]
|
||||
},
|
||||
"q-single-hung": {
|
||||
"type": "question",
|
||||
"inputType": "button",
|
||||
"title": "What features do you need?",
|
||||
"subtitle": "Choose your single hung window configuration",
|
||||
"answers": [
|
||||
{
|
||||
"caption": "Vinyl Sliding Primary",
|
||||
"image": "🪟",
|
||||
"next": "prod-1400"
|
||||
},
|
||||
{
|
||||
"caption": "Tilt Vinyl Primary",
|
||||
"image": "🪟",
|
||||
"next": "prod-1500"
|
||||
},
|
||||
{
|
||||
"caption": "Insulated Single Hung",
|
||||
"image": "🪟",
|
||||
"next": "prod-1650"
|
||||
},
|
||||
{
|
||||
"caption": "Thermal Break Tilt",
|
||||
"image": "🪟",
|
||||
"next": "prod-2000"
|
||||
}
|
||||
]
|
||||
},
|
||||
"q-slider": {
|
||||
"type": "question",
|
||||
"inputType": "button",
|
||||
"title": "What type of slider do you need?",
|
||||
"subtitle": "Select your slider window configuration",
|
||||
"answers": [
|
||||
{
|
||||
"caption": "Insulated Slider",
|
||||
"image": "↔️",
|
||||
"next": "prod-1700"
|
||||
},
|
||||
{
|
||||
"caption": "Thermal Break Slider",
|
||||
"image": "↔️",
|
||||
"next": "prod-2100"
|
||||
},
|
||||
{
|
||||
"caption": "Single Glazed Slider",
|
||||
"image": "↔️",
|
||||
"next": "prod-2700"
|
||||
},
|
||||
{
|
||||
"caption": "Replacement Slider",
|
||||
"image": "↔️",
|
||||
"next": "prod-3100"
|
||||
}
|
||||
]
|
||||
},
|
||||
"q-fixed": {
|
||||
"type": "question",
|
||||
"inputType": "button",
|
||||
"title": "Which fixed lite option?",
|
||||
"subtitle": "Choose your fixed window configuration",
|
||||
"answers": [
|
||||
{
|
||||
"caption": "Vinyl Insulated Fixed",
|
||||
"image": "🔲",
|
||||
"next": "prod-1510"
|
||||
},
|
||||
{
|
||||
"caption": "Thermal Break Fixed",
|
||||
"image": "🔲",
|
||||
"next": "prod-2200"
|
||||
},
|
||||
{
|
||||
"caption": "Picture Window",
|
||||
"image": "🔲",
|
||||
"next": "prod-3200"
|
||||
}
|
||||
]
|
||||
},
|
||||
"q-casement": {
|
||||
"type": "question",
|
||||
"inputType": "button",
|
||||
"title": "How many panels?",
|
||||
"subtitle": "Select the number of casement panels",
|
||||
"answers": [
|
||||
{
|
||||
"caption": "1-Panel Casement",
|
||||
"image": "🚪",
|
||||
"next": "prod-3300"
|
||||
},
|
||||
{
|
||||
"caption": "2-Panel Casement",
|
||||
"image": "🚪",
|
||||
"next": "prod-3302"
|
||||
},
|
||||
{
|
||||
"caption": "3-Panel Casement",
|
||||
"image": "🚪",
|
||||
"next": "prod-3303"
|
||||
},
|
||||
{
|
||||
"caption": "Fixed Casement",
|
||||
"image": "🚪",
|
||||
"next": "prod-3310"
|
||||
}
|
||||
]
|
||||
},
|
||||
"q-patio": {
|
||||
"type": "question",
|
||||
"inputType": "button",
|
||||
"title": "Select your patio door",
|
||||
"subtitle": "Choose the right patio door for your space",
|
||||
"answers": [
|
||||
{
|
||||
"caption": "Royal Crown Patio Door",
|
||||
"image": "🚪",
|
||||
"next": "prod-2400"
|
||||
}
|
||||
]
|
||||
},
|
||||
"prod-entry-door": {
|
||||
"type": "product",
|
||||
"code": "ENTRY-001",
|
||||
"title": "Custom Entry Door",
|
||||
"category": "DOOR",
|
||||
"image": "https://images.unsplash.com/photo-1572025442646-866d16c84a54?w=400&h=400&fit=crop",
|
||||
"url": "/products/entry-door",
|
||||
"description": "Custom entry door based on your specifications.",
|
||||
"features": [
|
||||
"Custom dimensions",
|
||||
"Choice of materials",
|
||||
"Security features",
|
||||
"Weather resistant"
|
||||
]
|
||||
},
|
||||
"prod-cobra-1100": {
|
||||
"type": "product",
|
||||
"code": "1100",
|
||||
"title": "Cobra Aluminum Storm Door",
|
||||
"category": "STORM DOOR",
|
||||
"image": "https://columbiawindows.com/wp-content/uploads/2014/10/Cobra-Ali-Storm-Door.jpg",
|
||||
"url": "https://columbiawindows.com/product-detail/cobra-aluminum-storm-doors-1100/",
|
||||
"description": "The Columbia Cobra aluminum storm door has a 1-1/4″ master frame. Double-track door with removable inserts. Available in black, bronze, sandstone or white baked on enamel finish.",
|
||||
"features": [
|
||||
"1-1/4″ master frame",
|
||||
"Double-track with removable inserts",
|
||||
"Baked on enamel finish",
|
||||
"Available in Black, Bronze, Sandstone, White",
|
||||
"Standard sizes: 2′-6″ x 6′-8″, 2′-8″ x 6′-8″, 3′-0″ x 6′-8″",
|
||||
"Custom sizes available by special order"
|
||||
],
|
||||
"brochure": "https://columbiawindows.com./wp-content/uploads/2014/10/Accessories-08-18-2010s.pdf",
|
||||
"measureGuide": "https://columbiawindows.com./wp-content/uploads/2014/10/How-to-Measure2.pdf"
|
||||
},
|
||||
"prod-1400": {
|
||||
"type": "product",
|
||||
"code": "1400",
|
||||
"title": "Series 1400 Vinyl Sliding Primary",
|
||||
"category": "SHPW",
|
||||
"image": "https://images.unsplash.com/photo-1545259741-2ea3ebf61fa3?w=400&h=400&fit=crop",
|
||||
"url": "/products/1400-vinyl-sliding",
|
||||
"description": "High-quality vinyl sliding window with excellent insulation properties. Perfect for residential applications.",
|
||||
"features": [
|
||||
"Vinyl construction for durability",
|
||||
"Smooth sliding operation",
|
||||
"Energy efficient design",
|
||||
"Low maintenance"
|
||||
]
|
||||
},
|
||||
"prod-1500": {
|
||||
"type": "product",
|
||||
"code": "1500",
|
||||
"title": "Series 1500 S.H. Tilt Vinyl Primary",
|
||||
"category": "SHPT",
|
||||
"url": "/products/1500-tilt-vinyl",
|
||||
"description": "Single hung window with convenient tilt feature for easy cleaning.",
|
||||
"features": [
|
||||
"Tilt-in sashes for easy cleaning",
|
||||
"Vinyl construction",
|
||||
"Energy efficient",
|
||||
"Security features"
|
||||
]
|
||||
},
|
||||
"prod-1650": {
|
||||
"type": "product",
|
||||
"code": "1650",
|
||||
"title": "#1650 Insulated Single Hung",
|
||||
"category": "SHPW",
|
||||
"url": "/products/1650-insulated",
|
||||
"description": "Premium insulated single hung window for maximum energy efficiency.",
|
||||
"features": [
|
||||
"Superior insulation",
|
||||
"Double-pane glass",
|
||||
"Weatherstripping",
|
||||
"Durable vinyl frame"
|
||||
]
|
||||
},
|
||||
"prod-2000": {
|
||||
"type": "product",
|
||||
"code": "2000",
|
||||
"title": "Series 2000 S.H. T.B. Tilt Primary",
|
||||
"category": "SHPT",
|
||||
"url": "/products/2000-thermal-break",
|
||||
"description": "Thermal break single hung window with tilt feature.",
|
||||
"features": [
|
||||
"Thermal break technology",
|
||||
"Tilt-in sashes",
|
||||
"Enhanced energy efficiency",
|
||||
"Condensation resistant"
|
||||
]
|
||||
},
|
||||
"prod-1700": {
|
||||
"type": "product",
|
||||
"code": "1700",
|
||||
"title": "#1700 Insulated Slider",
|
||||
"category": "SHPW",
|
||||
"url": "/products/1700-slider",
|
||||
"description": "Insulated sliding window with smooth operation.",
|
||||
"features": [
|
||||
"Insulated glass",
|
||||
"Smooth gliding action",
|
||||
"Multiple vent configurations",
|
||||
"Screens available"
|
||||
]
|
||||
},
|
||||
"prod-2100": {
|
||||
"type": "product",
|
||||
"code": "2100",
|
||||
"title": "Series 2100 Thermal Break Slider",
|
||||
"category": "SHPT",
|
||||
"url": "/products/2100-thermal-slider",
|
||||
"description": "Advanced thermal break slider for superior performance.",
|
||||
"features": [
|
||||
"Thermal break construction",
|
||||
"Energy Star rated",
|
||||
"Heavy-duty rollers",
|
||||
"Available in 3-panel configuration"
|
||||
]
|
||||
},
|
||||
"prod-2700": {
|
||||
"type": "product",
|
||||
"code": "2700",
|
||||
"title": "#2700 Single Glazed Slider",
|
||||
"category": "SHPW",
|
||||
"url": "/products/2700-single-glazed",
|
||||
"description": "Single glazed sliding window for standard applications.",
|
||||
"features": [
|
||||
"Cost-effective solution",
|
||||
"Reliable operation",
|
||||
"Multiple configurations",
|
||||
"Durable construction"
|
||||
]
|
||||
},
|
||||
"prod-3100": {
|
||||
"type": "product",
|
||||
"code": "3100",
|
||||
"title": "Series 3100 T.B. Replacement Slider",
|
||||
"category": "3000DHP",
|
||||
"url": "/products/3100-replacement",
|
||||
"description": "Thermal break replacement slider for existing window openings.",
|
||||
"features": [
|
||||
"Replacement window design",
|
||||
"Thermal break technology",
|
||||
"Easy installation",
|
||||
"Energy efficient"
|
||||
]
|
||||
},
|
||||
"prod-1510": {
|
||||
"type": "product",
|
||||
"code": "1510",
|
||||
"title": "1510 Vinyl Insulated Fixed Lite",
|
||||
"category": "FPPW",
|
||||
"url": "/products/1510-fixed-lite",
|
||||
"description": "Fixed window panel with insulated glass.",
|
||||
"features": [
|
||||
"Non-operable design",
|
||||
"Insulated glass",
|
||||
"Vinyl frame",
|
||||
"Low maintenance"
|
||||
]
|
||||
},
|
||||
"prod-2200": {
|
||||
"type": "product",
|
||||
"code": "2200",
|
||||
"title": "Series 2200 T.B. Fixed Lite",
|
||||
"category": "FPPW",
|
||||
"url": "/products/2200-thermal-fixed",
|
||||
"description": "Thermal break fixed lite window for maximum efficiency.",
|
||||
"features": [
|
||||
"Thermal break frame",
|
||||
"Fixed glass panel",
|
||||
"Superior insulation",
|
||||
"Tempered glass option available"
|
||||
]
|
||||
},
|
||||
"prod-3200": {
|
||||
"type": "product",
|
||||
"code": "3200",
|
||||
"title": "Series 3200 T.B. Picture Window",
|
||||
"category": "3000FPP",
|
||||
"url": "/products/3200-picture-window",
|
||||
"description": "Large picture window with thermal break technology.",
|
||||
"features": [
|
||||
"Expansive glass area",
|
||||
"Thermal break construction",
|
||||
"Unobstructed views",
|
||||
"Energy efficient"
|
||||
]
|
||||
},
|
||||
"prod-3300": {
|
||||
"type": "product",
|
||||
"code": "3300",
|
||||
"title": "3300 1-Panel T.B. Casement",
|
||||
"category": "CASEMNT",
|
||||
"url": "/products/3300-casement-1panel",
|
||||
"description": "Single panel casement window with thermal break.",
|
||||
"features": [
|
||||
"Hinged operation",
|
||||
"Thermal break frame",
|
||||
"Full opening capability",
|
||||
"Secure locking system"
|
||||
]
|
||||
},
|
||||
"prod-3302": {
|
||||
"type": "product",
|
||||
"code": "3302",
|
||||
"title": "3300 2-Panel T.B. Casement",
|
||||
"category": "CASEMNT",
|
||||
"url": "/products/3302-casement-2panel",
|
||||
"description": "Two-panel casement window configuration.",
|
||||
"features": [
|
||||
"Dual panels",
|
||||
"Thermal break technology",
|
||||
"Flexible ventilation",
|
||||
"Energy efficient"
|
||||
]
|
||||
},
|
||||
"prod-3303": {
|
||||
"type": "product",
|
||||
"code": "3303",
|
||||
"title": "3300 3-Panel T.B. Casement",
|
||||
"category": "CASEMNT",
|
||||
"url": "/products/3303-casement-3panel",
|
||||
"description": "Three-panel casement window for larger openings.",
|
||||
"features": [
|
||||
"Triple panel design",
|
||||
"Maximum ventilation",
|
||||
"Thermal break construction",
|
||||
"Architectural appeal"
|
||||
]
|
||||
},
|
||||
"prod-3310": {
|
||||
"type": "product",
|
||||
"code": "3310",
|
||||
"title": "3310 Fixed Casement",
|
||||
"category": "CASEMNT",
|
||||
"url": "/products/3310-fixed-casement",
|
||||
"description": "Fixed casement window panel.",
|
||||
"features": [
|
||||
"Non-operable design",
|
||||
"Casement styling",
|
||||
"Energy efficient",
|
||||
"Durable construction"
|
||||
]
|
||||
},
|
||||
"prod-2400": {
|
||||
"type": "product",
|
||||
"code": "2400",
|
||||
"title": "2400 Royal Crown Patio Door",
|
||||
"category": "PD",
|
||||
"image": "https://images.unsplash.com/photo-1585412727339-b82d6d394f51?w=400&h=400&fit=crop",
|
||||
"url": "/products/2400-patio-door",
|
||||
"description": "Premium patio door system with superior performance.",
|
||||
"features": [
|
||||
"Wide opening",
|
||||
"Smooth operation",
|
||||
"Security features",
|
||||
"Energy efficient glass"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
[
|
||||
{
|
||||
"username": "Master",
|
||||
"password": "pbkdf2:sha256:1000000$tPRmseRGKLveXTxS$87eda9a20db29d12838fcb607f1f3536141f743ec9879702dc2a0dd893b4078d",
|
||||
"defaultLocation": "KC",
|
||||
"permissions": {
|
||||
"manage_users": true,
|
||||
"view_reports": true,
|
||||
"create_quotes": true,
|
||||
"approve_quotes": true,
|
||||
"manage_products": true,
|
||||
"manage_inventory": true
|
||||
},
|
||||
"locationSettings": {
|
||||
"LINDS": {
|
||||
"accessible": true
|
||||
},
|
||||
"IOLA": {
|
||||
"accessible": true
|
||||
},
|
||||
"KC": {
|
||||
"accessible": true
|
||||
},
|
||||
"BMD": {
|
||||
"accessible": true
|
||||
}
|
||||
},
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"username": "Darlene",
|
||||
"password": "pbkdf2:sha256:1000000$8sblSWLWIvsjcta2$d974d13956f67cc42e1fa88191d68c896d4daa663ae2d76d7988beb569647d27",
|
||||
"defaultLocation": "IOLA",
|
||||
"permissions": {
|
||||
"manage_users": false,
|
||||
"view_reports": true,
|
||||
"create_quotes": true,
|
||||
"approve_quotes": false,
|
||||
"manage_products": false,
|
||||
"manage_inventory": false
|
||||
},
|
||||
"locationSettings": {
|
||||
"LINDS": {
|
||||
"accessible": true
|
||||
},
|
||||
"IOLA": {
|
||||
"accessible": true
|
||||
},
|
||||
"KC": {
|
||||
"accessible": true
|
||||
},
|
||||
"BMD": {
|
||||
"accessible": false
|
||||
}
|
||||
},
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"username": "Toni",
|
||||
"password": "pbkdf2:sha256:1000000$5k3daJKZt7hqNAkq$e77819a2b2ee4bf9955dc10b3b4da7b14de2b4ecda5ce5f553daf3783b0a316c",
|
||||
"defaultLocation": "LINDS",
|
||||
"locationSettings": {
|
||||
"LINDS": {
|
||||
"accessible": true
|
||||
},
|
||||
"IOLA": {
|
||||
"accessible": false
|
||||
},
|
||||
"KC": {
|
||||
"accessible": false
|
||||
},
|
||||
"BMD": {
|
||||
"accessible": false
|
||||
}
|
||||
},
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"username": "Jason",
|
||||
"password": "pbkdf2:sha256:1000000$rNSYLqRWZ6tdny1G$cad2afda9193b7cb50b9e9423f95ff83bc14428c8a2d98607cd6e5ce17cb70c7",
|
||||
"defaultLocation": "KC",
|
||||
"locationSettings": {
|
||||
"LINDS": {
|
||||
"accessible": true
|
||||
},
|
||||
"IOLA": {
|
||||
"accessible": true
|
||||
},
|
||||
"KC": {
|
||||
"accessible": true
|
||||
},
|
||||
"BMD": {
|
||||
"accessible": true
|
||||
}
|
||||
},
|
||||
"active": false
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,111 @@
|
||||
"""
|
||||
Debug startup script - writes detailed logs to file
|
||||
Upload this and temporarily set it as the startup file to diagnose issues
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
# Create log file
|
||||
log_file = '/home/bmdwtjuw/product-finder/startup_debug.log'
|
||||
|
||||
def log(message):
|
||||
with open(log_file, 'a') as f:
|
||||
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
f.write(f"[{timestamp}] {message}\n")
|
||||
|
||||
try:
|
||||
log("="*70)
|
||||
log("STARTUP DEBUG - BEGIN")
|
||||
log("="*70)
|
||||
|
||||
# Python info
|
||||
log(f"Python version: {sys.version}")
|
||||
log(f"Python executable: {sys.executable}")
|
||||
log(f"Current directory: {os.getcwd()}")
|
||||
|
||||
# Environment variables
|
||||
log("\nEnvironment Variables:")
|
||||
log(f" APPLICATION_ROOT: {os.environ.get('APPLICATION_ROOT', 'NOT SET')}")
|
||||
log(f" FLASK_ENV: {os.environ.get('FLASK_ENV', 'NOT SET')}")
|
||||
|
||||
# Python path
|
||||
log(f"\nPython path: {sys.path}")
|
||||
|
||||
# Check Flask installation
|
||||
log("\nChecking Flask installation...")
|
||||
try:
|
||||
import flask
|
||||
log(f" ✓ Flask version: {flask.__version__ if hasattr(flask, '__version__') else 'unknown'}")
|
||||
except ImportError as e:
|
||||
log(f" ✗ Flask NOT installed: {e}")
|
||||
log(" RUN: pip install Flask==3.0.0 Werkzeug==3.0.1")
|
||||
|
||||
# Check Werkzeug
|
||||
try:
|
||||
import werkzeug
|
||||
log(f" ✓ Werkzeug version: {werkzeug.__version__ if hasattr(werkzeug, '__version__') else 'unknown'}")
|
||||
except ImportError as e:
|
||||
log(f" ✗ Werkzeug NOT installed: {e}")
|
||||
|
||||
# Check if app.py exists
|
||||
log("\nChecking files...")
|
||||
app_py_path = os.path.join(os.path.dirname(__file__), 'app.py')
|
||||
if os.path.exists(app_py_path):
|
||||
log(f" ✓ app.py exists at {app_py_path}")
|
||||
else:
|
||||
log(f" ✗ app.py NOT FOUND at {app_py_path}")
|
||||
|
||||
# Try importing app
|
||||
log("\nAttempting to import Flask app...")
|
||||
try:
|
||||
from app import app
|
||||
log(f" ✓ Successfully imported Flask app")
|
||||
log(f" APPLICATION_ROOT config: {app.config.get('APPLICATION_ROOT', 'NOT SET')}")
|
||||
|
||||
# Count routes
|
||||
routes = list(app.url_map.iter_rules())
|
||||
log(f" ✓ Registered routes: {len(routes)}")
|
||||
|
||||
# Create WSGI application
|
||||
def application(environ, start_response):
|
||||
log(f"\nRequest received: {environ.get('PATH_INFO', '/')}")
|
||||
return app(environ, start_response)
|
||||
|
||||
log("\n✓ APPLICATION READY - Check site now")
|
||||
log("="*70)
|
||||
|
||||
except Exception as e:
|
||||
log(f" ✗ Failed to import app:")
|
||||
log(f" Error: {e}")
|
||||
|
||||
import traceback
|
||||
log(f"\nFull traceback:")
|
||||
log(traceback.format_exc())
|
||||
|
||||
# Create error application
|
||||
def application(environ, start_response):
|
||||
status = '500 Internal Server Error'
|
||||
output = f'Import Error - Check {log_file}\n\n{traceback.format_exc()}'.encode('utf-8')
|
||||
response_headers = [('Content-type', 'text/plain'),
|
||||
('Content-Length', str(len(output)))]
|
||||
start_response(status, response_headers)
|
||||
return [output]
|
||||
|
||||
log("="*70)
|
||||
|
||||
except Exception as e:
|
||||
# Catch-all for any errors
|
||||
import traceback
|
||||
log(f"\nFATAL ERROR during startup:")
|
||||
log(str(e))
|
||||
log(traceback.format_exc())
|
||||
log("="*70)
|
||||
|
||||
def application(environ, start_response):
|
||||
status = '500 Internal Server Error'
|
||||
output = f'Startup Error - Check {log_file}'.encode('utf-8')
|
||||
response_headers = [('Content-type', 'text/plain'),
|
||||
('Content-Length', str(len(output)))]
|
||||
start_response(status, response_headers)
|
||||
return [output]
|
||||
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Fix Passenger Configuration
|
||||
============================
|
||||
This script renames the corrupted passenger_wsgi.py file so Passenger
|
||||
will use the control panel settings instead.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
def main():
|
||||
print("=" * 70)
|
||||
print("FIXING PASSENGER CONFIGURATION")
|
||||
print("=" * 70)
|
||||
|
||||
# Check if passenger_wsgi.py exists
|
||||
if os.path.exists('passenger_wsgi.py'):
|
||||
print("\nFound passenger_wsgi.py - this file is causing the problem")
|
||||
print("Renaming it to passenger_wsgi.py.DISABLED...")
|
||||
|
||||
try:
|
||||
os.rename('passenger_wsgi.py', 'passenger_wsgi.py.DISABLED')
|
||||
print("SUCCESS: passenger_wsgi.py renamed to passenger_wsgi.py.DISABLED")
|
||||
print("\nPassenger will now use the control panel settings:")
|
||||
print(" Application startup file: wsgi.py")
|
||||
print(" Application Entry point: app")
|
||||
except Exception as e:
|
||||
print(f"ERROR: Could not rename file: {e}")
|
||||
return 1
|
||||
else:
|
||||
print("\npassenger_wsgi.py not found - already fixed or doesn't exist")
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print("NEXT STEPS:")
|
||||
print("=" * 70)
|
||||
print("1. Restart your Python application in the control panel")
|
||||
print("2. Visit: https://columbiawindows.com/product-finder/test")
|
||||
print("3. If it works, you're done!")
|
||||
print("=" * 70)
|
||||
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,64 @@
|
||||
#!/bin/bash
|
||||
# Fix Passenger lock file permissions
|
||||
# Run this on the server to resolve "Can't acquire lock" error
|
||||
|
||||
echo "=========================================="
|
||||
echo "FIXING PASSENGER LOCK FILE ISSUES"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# Navigate to app directory
|
||||
cd ~/product-finder
|
||||
|
||||
# Remove any stale lock files
|
||||
echo "🧹 Cleaning stale lock files..."
|
||||
if [ -d "tmp" ]; then
|
||||
rm -rf tmp/*
|
||||
echo " ✓ tmp directory cleaned"
|
||||
else
|
||||
echo " ⚠️ tmp directory doesn't exist - will create it"
|
||||
fi
|
||||
|
||||
# Create tmp directory with correct permissions
|
||||
echo ""
|
||||
echo "📁 Setting up tmp directory..."
|
||||
mkdir -p tmp
|
||||
chmod 755 tmp
|
||||
echo " ✓ tmp directory created with 755 permissions"
|
||||
|
||||
# Create restart file
|
||||
echo ""
|
||||
echo "🔄 Restarting Passenger..."
|
||||
touch tmp/restart.txt
|
||||
chmod 644 tmp/restart.txt
|
||||
echo " ✓ tmp/restart.txt created"
|
||||
|
||||
# Set correct permissions for app files
|
||||
echo ""
|
||||
echo "🔒 Setting file permissions..."
|
||||
chmod 755 .
|
||||
chmod 644 passenger_wsgi.py
|
||||
chmod 644 app.py
|
||||
chmod 644 .htaccess
|
||||
chmod -R 755 data 2>/dev/null || mkdir -p data && chmod 755 data
|
||||
chmod -R 755 templates
|
||||
chmod -R 755 static 2>/dev/null || true
|
||||
echo " ✓ File permissions set"
|
||||
|
||||
# Show current tmp directory status
|
||||
echo ""
|
||||
echo "📊 Current tmp directory status:"
|
||||
ls -la tmp/
|
||||
echo ""
|
||||
|
||||
echo "=========================================="
|
||||
echo "✓ LOCK FILE ISSUE FIXED"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo "1. Wait 30 seconds for Passenger to restart"
|
||||
echo "2. Test: https://columbiawindows.com/product-finder/"
|
||||
echo ""
|
||||
echo "If still failing, check error log:"
|
||||
echo "tail -50 ~/logs/error_log"
|
||||
echo ""
|
||||
@@ -0,0 +1,295 @@
|
||||
"""
|
||||
Generate Bitwise Helper File for Product Classification
|
||||
Creates a helper file with bitwise flags based on product attributes (columns G-S)
|
||||
"""
|
||||
|
||||
import csv
|
||||
import json
|
||||
from collections import defaultdict
|
||||
|
||||
# Bit position definitions
|
||||
BIT_DEFINITIONS = {
|
||||
# Base Type (Bits 0-1)
|
||||
'base_door': 0, # 2^0 = 1
|
||||
'base_window': 1, # 2^1 = 2
|
||||
|
||||
# Product Flags (Bits 2-3)
|
||||
'is_accessory': 2, # 2^2 = 4
|
||||
'specific_item': 3, # 2^3 = 8
|
||||
|
||||
# Materials (Bits 4-5)
|
||||
'material_aluminum': 4, # 2^4 = 16
|
||||
'material_vinyl': 5, # 2^5 = 32
|
||||
|
||||
# Colors (Bits 6-11)
|
||||
'color_black': 6, # 2^6 = 64
|
||||
'color_white': 7, # 2^7 = 128
|
||||
'color_bronze': 8, # 2^8 = 256
|
||||
'color_tan': 9, # 2^9 = 512
|
||||
'color_mill': 10, # 2^10 = 1024
|
||||
'color_sandstone': 11, # 2^11 = 2048
|
||||
}
|
||||
|
||||
# Sub-type bits will be assigned dynamically (starting at bit 12)
|
||||
SUBTYPE_BIT_START = 12
|
||||
|
||||
def parse_bool(value):
|
||||
"""Parse TRUE/FALSE string to boolean"""
|
||||
if isinstance(value, str):
|
||||
return value.strip().upper() == 'TRUE'
|
||||
return bool(value)
|
||||
|
||||
def calculate_bit_value(row, subtype_map):
|
||||
"""Calculate the bitwise value for a product row"""
|
||||
bit_value = 0
|
||||
explanation = []
|
||||
|
||||
# Base Type (Column G - index 6)
|
||||
base_type = row[6].strip() if len(row) > 6 and row[6] else ""
|
||||
if base_type.lower() == 'door':
|
||||
bit_value |= (1 << BIT_DEFINITIONS['base_door'])
|
||||
explanation.append('Door')
|
||||
elif base_type.lower() == 'window':
|
||||
bit_value |= (1 << BIT_DEFINITIONS['base_window'])
|
||||
explanation.append('Window')
|
||||
|
||||
# Accessory Flag (Column J - index 9)
|
||||
if len(row) > 9 and parse_bool(row[9]):
|
||||
bit_value |= (1 << BIT_DEFINITIONS['is_accessory'])
|
||||
explanation.append('Accessory')
|
||||
|
||||
# Specific Item Link (Column K - index 10)
|
||||
if len(row) > 10 and parse_bool(row[10]):
|
||||
bit_value |= (1 << BIT_DEFINITIONS['specific_item'])
|
||||
explanation.append('SpecificItem')
|
||||
|
||||
# Materials
|
||||
if len(row) > 11 and parse_bool(row[11]): # Aluminum (Column L)
|
||||
bit_value |= (1 << BIT_DEFINITIONS['material_aluminum'])
|
||||
explanation.append('Aluminum')
|
||||
|
||||
if len(row) > 12 and parse_bool(row[12]): # Vinyl (Column M)
|
||||
bit_value |= (1 << BIT_DEFINITIONS['material_vinyl'])
|
||||
explanation.append('Vinyl')
|
||||
|
||||
# Colors
|
||||
color_columns = [
|
||||
(13, 'color_black', 'Black'),
|
||||
(14, 'color_white', 'White'),
|
||||
(15, 'color_bronze', 'Bronze'),
|
||||
(16, 'color_tan', 'Tan'),
|
||||
(17, 'color_mill', 'Mill'),
|
||||
(18, 'color_sandstone', 'Sandstone'),
|
||||
]
|
||||
|
||||
for col_idx, bit_key, color_name in color_columns:
|
||||
if len(row) > col_idx and parse_bool(row[col_idx]):
|
||||
bit_value |= (1 << BIT_DEFINITIONS[bit_key])
|
||||
explanation.append(color_name)
|
||||
|
||||
# Sub-types (Columns H and I - indices 7 and 8)
|
||||
door_subtype = row[7].strip() if len(row) > 7 and row[7] else ""
|
||||
window_subtype = row[8].strip() if len(row) > 8 and row[8] else ""
|
||||
|
||||
if door_subtype and door_subtype in subtype_map:
|
||||
bit_pos = subtype_map[door_subtype]
|
||||
bit_value |= (1 << bit_pos)
|
||||
explanation.append(f'Subtype:{door_subtype}')
|
||||
|
||||
if window_subtype and window_subtype in subtype_map:
|
||||
bit_pos = subtype_map[window_subtype]
|
||||
bit_value |= (1 << bit_pos)
|
||||
explanation.append(f'Subtype:{window_subtype}')
|
||||
|
||||
return bit_value, explanation
|
||||
|
||||
def collect_subtypes(csv_path):
|
||||
"""Collect all unique sub-types from the CSV"""
|
||||
subtypes = set()
|
||||
|
||||
with open(csv_path, 'r', encoding='utf-8') as f:
|
||||
reader = csv.reader(f)
|
||||
next(reader) # Skip first header row
|
||||
next(reader) # Skip second header row
|
||||
|
||||
for row in reader:
|
||||
if len(row) < 8:
|
||||
continue
|
||||
|
||||
# Skip discontinued items
|
||||
if len(row) > 0 and parse_bool(row[0]):
|
||||
continue
|
||||
|
||||
# Collect door subtype (column H - index 7)
|
||||
if len(row) > 7 and row[7].strip():
|
||||
subtypes.add(row[7].strip())
|
||||
|
||||
# Collect window subtype (column I - index 8)
|
||||
if len(row) > 8 and row[8].strip():
|
||||
subtypes.add(row[8].strip())
|
||||
|
||||
return sorted(subtypes)
|
||||
|
||||
def generate_helper_files(csv_path):
|
||||
"""Generate helper CSV and JSON files with bitwise values"""
|
||||
|
||||
print("Analyzing product data...")
|
||||
|
||||
# First pass: collect all unique sub-types
|
||||
subtypes = collect_subtypes(csv_path)
|
||||
|
||||
# Create subtype bit mapping
|
||||
subtype_map = {}
|
||||
for idx, subtype in enumerate(subtypes):
|
||||
subtype_map[subtype] = SUBTYPE_BIT_START + idx
|
||||
|
||||
print(f"Found {len(subtypes)} unique sub-types")
|
||||
|
||||
# Second pass: calculate bit values
|
||||
results = []
|
||||
|
||||
with open(csv_path, 'r', encoding='utf-8') as f:
|
||||
reader = csv.reader(f)
|
||||
next(reader) # Skip first header row
|
||||
next(reader) # Skip second header row
|
||||
|
||||
for row in reader:
|
||||
if len(row) < 3:
|
||||
continue
|
||||
|
||||
prod_code = row[2].strip() # Column C - PROD_CODE
|
||||
discontinued = parse_bool(row[0]) # Column A - DISCONT
|
||||
|
||||
bit_value, explanation = calculate_bit_value(row, subtype_map)
|
||||
|
||||
results.append({
|
||||
'PROD_CODE': prod_code,
|
||||
'BIT_VALUE': bit_value,
|
||||
'BIT_HEX': f"0x{bit_value:X}",
|
||||
'BIT_BINARY': f"{bit_value:b}",
|
||||
'DISCONTINUED': discontinued,
|
||||
'FLAGS': ' | '.join(explanation) if explanation else 'None'
|
||||
})
|
||||
|
||||
# Filter out discontinued products for output files
|
||||
active_results = [r for r in results if not r['DISCONTINUED']]
|
||||
|
||||
print(f"Total products processed: {len(results)}")
|
||||
print(f"Active products: {len(active_results)}")
|
||||
print(f"Discontinued products (filtered out): {len(results) - len(active_results)}")
|
||||
|
||||
# Write CSV helper file (only active products)
|
||||
csv_output = 'data/product_bitwise.csv'
|
||||
with open(csv_output, 'w', newline='', encoding='utf-8') as f:
|
||||
writer = csv.DictWriter(f, fieldnames=['PROD_CODE', 'BIT_VALUE', 'BIT_HEX', 'BIT_BINARY', 'DISCONTINUED', 'FLAGS'])
|
||||
writer.writeheader()
|
||||
writer.writerows(active_results)
|
||||
|
||||
print(f"✓ Created {csv_output}")
|
||||
|
||||
# Write JSON helper file (only active products)
|
||||
json_output = 'data/product_bitwise.json'
|
||||
with open(json_output, 'w', encoding='utf-8') as f:
|
||||
json.dump(active_results, f, indent=2)
|
||||
|
||||
print(f"✓ Created {json_output}")
|
||||
|
||||
# Create bit legend/documentation
|
||||
legend = {
|
||||
'description': 'Bitwise flag system for product classification',
|
||||
'bit_definitions': {
|
||||
'base_attributes': {
|
||||
'bit_0 (1)': 'Base Type = Door',
|
||||
'bit_1 (2)': 'Base Type = Window',
|
||||
'bit_2 (4)': 'Is Accessory',
|
||||
'bit_3 (8)': 'Specific Item Link',
|
||||
},
|
||||
'materials': {
|
||||
'bit_4 (16)': 'Material = Aluminum',
|
||||
'bit_5 (32)': 'Material = Vinyl',
|
||||
},
|
||||
'colors': {
|
||||
'bit_6 (64)': 'Color = Black',
|
||||
'bit_7 (128)': 'Color = White',
|
||||
'bit_8 (256)': 'Color = Bronze',
|
||||
'bit_9 (512)': 'Color = Tan',
|
||||
'bit_10 (1024)': 'Color = Mill',
|
||||
'bit_11 (2048)': 'Color = Sandstone',
|
||||
},
|
||||
'subtypes': {}
|
||||
},
|
||||
'usage_examples': {
|
||||
'check_if_door': 'bit_value & 1',
|
||||
'check_if_window': 'bit_value & 2',
|
||||
'check_if_aluminum': 'bit_value & 16',
|
||||
'check_if_white': 'bit_value & 128',
|
||||
'check_multiple': '(bit_value & 1) and (bit_value & 16) # Door AND Aluminum',
|
||||
}
|
||||
}
|
||||
|
||||
# Add subtype mappings to legend
|
||||
for subtype, bit_pos in sorted(subtype_map.items(), key=lambda x: x[1]):
|
||||
bit_value = 1 << bit_pos
|
||||
legend['bit_definitions']['subtypes'][f'bit_{bit_pos} ({bit_value})'] = f'Subtype = {subtype}'
|
||||
legend['usage_examples'][f'check_subtype_{subtype.replace(" ", "_").lower()}'] = f'bit_value & {bit_value}'
|
||||
|
||||
# Write legend file
|
||||
legend_output = 'data/bitwise_legend.json'
|
||||
with open(legend_output, 'w', encoding='utf-8') as f:
|
||||
json.dump(legend, f, indent=2)
|
||||
|
||||
print(f"✓ Created {legend_output}")
|
||||
|
||||
# Generate statistics
|
||||
stats = {
|
||||
'total_products': len([r for r in results if not r['DISCONTINUED']]),
|
||||
'total_discontinued': len([r for r in results if r['DISCONTINUED']]),
|
||||
'total_accessories': len([r for r in results if not r['DISCONTINUED'] and (r['BIT_VALUE'] & 4)]),
|
||||
'doors': len([r for r in results if not r['DISCONTINUED'] and (r['BIT_VALUE'] & 1)]),
|
||||
'windows': len([r for r in results if not r['DISCONTINUED'] and (r['BIT_VALUE'] & 2)]),
|
||||
'aluminum_products': len([r for r in results if not r['DISCONTINUED'] and (r['BIT_VALUE'] & 16)]),
|
||||
'vinyl_products': len([r for r in results if not r['DISCONTINUED'] and (r['BIT_VALUE'] & 32)]),
|
||||
'multi_material': len([r for r in results if not r['DISCONTINUED'] and (r['BIT_VALUE'] & 16) and (r['BIT_VALUE'] & 32)]),
|
||||
}
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("STATISTICS")
|
||||
print("="*60)
|
||||
print(f"Total Active Products: {stats['total_products']}")
|
||||
print(f"Total Discontinued: {stats['total_discontinued']}")
|
||||
print(f"Accessories: {stats['total_accessories']}")
|
||||
print(f"Doors: {stats['doors']}")
|
||||
print(f"Windows: {stats['windows']}")
|
||||
print(f"Aluminum Products: {stats['aluminum_products']}")
|
||||
print(f"Vinyl Products: {stats['vinyl_products']}")
|
||||
print(f"Multi-Material Products: {stats['multi_material']}")
|
||||
print(f"Unique Sub-types: {len(subtypes)}")
|
||||
print("="*60)
|
||||
|
||||
# Show example products
|
||||
print("\nEXAMPLE PRODUCTS:")
|
||||
print("-"*60)
|
||||
for i, result in enumerate(active_results[:5]):
|
||||
print(f"{result['PROD_CODE']:12} | {result['BIT_VALUE']:6} | {result['BIT_HEX']:8} | {result['FLAGS']}")
|
||||
print("-"*60)
|
||||
|
||||
return active_results, legend, subtype_map
|
||||
|
||||
if __name__ == '__main__':
|
||||
csv_path = 'data/products.csv'
|
||||
|
||||
try:
|
||||
results, legend, subtype_map = generate_helper_files(csv_path)
|
||||
print("\n✓ All helper files generated successfully!")
|
||||
print("\nGenerated files:")
|
||||
print(" - data/product_bitwise.csv (CSV format with bit values)")
|
||||
print(" - data/product_bitwise.json (JSON format with bit values)")
|
||||
print(" - data/bitwise_legend.json (Bit mapping documentation)")
|
||||
|
||||
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()
|
||||
@@ -0,0 +1,297 @@
|
||||
"""
|
||||
Dynamic Product Image Generator
|
||||
Generates product images with configurable colors, hardware positions, and other options.
|
||||
Uses PIL/Pillow for image manipulation.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import hashlib
|
||||
from io import BytesIO
|
||||
from PIL import Image, ImageDraw, ImageEnhance, ImageColor
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class ProductImageGenerator:
|
||||
"""Handles dynamic generation of product images based on configuration."""
|
||||
|
||||
def __init__(self, config_file='data/image_configs.json'):
|
||||
"""Initialize with configuration file."""
|
||||
self.config_file = config_file
|
||||
self.configs = self._load_configs()
|
||||
self.cache_dir = Path('cache/product_images')
|
||||
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def _load_configs(self):
|
||||
"""Load image configuration from JSON file."""
|
||||
try:
|
||||
with open(self.config_file, 'r') as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
print(f"Error loading image configs: {e}")
|
||||
return {}
|
||||
|
||||
def _find_image(self, image_path):
|
||||
"""
|
||||
Find image file, trying different extensions if needed.
|
||||
|
||||
Args:
|
||||
image_path: Path to image (e.g., 'images/cobrai.jpg')
|
||||
|
||||
Returns:
|
||||
Actual path to image file, or None if not found
|
||||
"""
|
||||
# Try the exact path first
|
||||
if os.path.exists(image_path):
|
||||
return image_path
|
||||
|
||||
# Get base path without extension
|
||||
base_path = os.path.splitext(image_path)[0]
|
||||
|
||||
# Try common image extensions
|
||||
for ext in ['.png', '.jpg', '.jpeg', '.PNG', '.JPG', '.JPEG']:
|
||||
test_path = base_path + ext
|
||||
if os.path.exists(test_path):
|
||||
return test_path
|
||||
|
||||
return None
|
||||
|
||||
def generate_product_image(self, product_code, color='white', hinge='right',
|
||||
material='aluminum', use_cache=True):
|
||||
"""
|
||||
Generate a product image with specified configuration.
|
||||
|
||||
Args:
|
||||
product_code: Product identifier (e.g., 'cobrai')
|
||||
color: Color name (e.g., 'white', 'black', 'bronze')
|
||||
hinge: Hinge side ('right' or 'left')
|
||||
material: Material type (for future use)
|
||||
use_cache: Whether to use cached images
|
||||
|
||||
Returns:
|
||||
PIL Image object
|
||||
"""
|
||||
product_code = product_code.lower()
|
||||
|
||||
# Check if product config exists
|
||||
if product_code not in self.configs:
|
||||
raise ValueError(f"Product {product_code} not found in configuration")
|
||||
|
||||
config = self.configs[product_code]
|
||||
|
||||
# Check cache first
|
||||
if use_cache:
|
||||
cache_key = self._get_cache_key(product_code, color, hinge, material)
|
||||
cached_image = self._get_cached_image(cache_key)
|
||||
if cached_image:
|
||||
return cached_image
|
||||
|
||||
# Load base image
|
||||
base_image_path = config['sourceImage']
|
||||
|
||||
# Try to find the image with different extensions if needed
|
||||
image_path = self._find_image(base_image_path)
|
||||
|
||||
if not image_path:
|
||||
raise FileNotFoundError(f"Could not load base image: {base_image_path}. Tried .jpg, .png, .jpeg")
|
||||
|
||||
try:
|
||||
base_image = Image.open(image_path).convert('RGBA')
|
||||
except Exception as e:
|
||||
raise FileNotFoundError(f"Could not load base image: {image_path}. Error: {e}")
|
||||
|
||||
# Apply color to regions
|
||||
if color in config['availableColors']:
|
||||
color_rgb = tuple(config['availableColors'][color]['rgb'])
|
||||
base_image = self._apply_color_to_regions(
|
||||
base_image,
|
||||
config['colorableRegions'],
|
||||
color_rgb
|
||||
)
|
||||
|
||||
# Apply hardware (handles, locks)
|
||||
if hinge in config.get('hardwarePositions', {}):
|
||||
base_image = self._apply_hardware(
|
||||
base_image,
|
||||
config['hardwarePositions'],
|
||||
hinge
|
||||
)
|
||||
|
||||
# Flip image if left hinge
|
||||
if hinge == 'left':
|
||||
base_image = base_image.transpose(Image.FLIP_LEFT_RIGHT)
|
||||
|
||||
# Cache the result
|
||||
if use_cache:
|
||||
self._cache_image(cache_key, base_image)
|
||||
|
||||
return base_image
|
||||
|
||||
def _apply_color_to_regions(self, image, regions, target_color):
|
||||
"""
|
||||
Apply color to specific regions of the image.
|
||||
|
||||
This method attempts to recolor the frame/panel areas while preserving
|
||||
shadows, highlights, and texture.
|
||||
"""
|
||||
img_copy = image.copy()
|
||||
pixels = img_copy.load()
|
||||
width, height = img_copy.size
|
||||
|
||||
# Get the average brightness of the target color
|
||||
target_brightness = sum(target_color) / 3
|
||||
|
||||
for region in regions:
|
||||
x1, y1 = region['topLeft']
|
||||
x2, y2 = region['bottomRight']
|
||||
|
||||
# Ensure coordinates are within bounds
|
||||
x1 = max(0, min(x1, width - 1))
|
||||
x2 = max(0, min(x2, width))
|
||||
y1 = max(0, min(y1, height - 1))
|
||||
y2 = max(0, min(y2, height))
|
||||
|
||||
# Apply color to region while preserving luminosity
|
||||
for y in range(y1, y2):
|
||||
for x in range(x1, x2):
|
||||
try:
|
||||
r, g, b, a = pixels[x, y]
|
||||
|
||||
# Calculate original brightness
|
||||
original_brightness = (r + g + b) / 3
|
||||
|
||||
# Preserve relative brightness
|
||||
if original_brightness > 0:
|
||||
brightness_factor = original_brightness / 255.0
|
||||
|
||||
# Apply target color with brightness preservation
|
||||
new_r = int(target_color[0] * brightness_factor)
|
||||
new_g = int(target_color[1] * brightness_factor)
|
||||
new_b = int(target_color[2] * brightness_factor)
|
||||
|
||||
# Ensure values are in valid range
|
||||
new_r = max(0, min(255, new_r))
|
||||
new_g = max(0, min(255, new_g))
|
||||
new_b = max(0, min(255, new_b))
|
||||
|
||||
pixels[x, y] = (new_r, new_g, new_b, a)
|
||||
except IndexError:
|
||||
continue
|
||||
|
||||
return img_copy
|
||||
|
||||
def _apply_hardware(self, image, hardware_positions, hinge_side):
|
||||
"""Apply hardware (handles, locks) to the image."""
|
||||
img_copy = image.copy()
|
||||
|
||||
hardware_key = f"handle_{hinge_side}"
|
||||
if hardware_key in hardware_positions:
|
||||
hardware_config = hardware_positions[hardware_key]
|
||||
hardware_path = hardware_config.get('image')
|
||||
|
||||
if hardware_path:
|
||||
# Try to find the hardware image with different extensions
|
||||
actual_path = self._find_image(hardware_path)
|
||||
|
||||
if actual_path:
|
||||
try:
|
||||
hardware_img = Image.open(actual_path).convert('RGBA')
|
||||
|
||||
# Flip if needed
|
||||
if hardware_config.get('flip', False):
|
||||
hardware_img = hardware_img.transpose(Image.FLIP_LEFT_RIGHT)
|
||||
|
||||
# Paste hardware at specified position
|
||||
x, y = hardware_config['x'], hardware_config['y']
|
||||
img_copy.paste(hardware_img, (x, y), hardware_img)
|
||||
except Exception as e:
|
||||
print(f"Could not apply hardware: {e}")
|
||||
|
||||
return img_copy
|
||||
|
||||
def _get_cache_key(self, product_code, color, hinge, material):
|
||||
"""Generate a unique cache key for the configuration."""
|
||||
key_string = f"{product_code}_{color}_{hinge}_{material}"
|
||||
return hashlib.md5(key_string.encode()).hexdigest()
|
||||
|
||||
def _get_cached_image(self, cache_key):
|
||||
"""Retrieve cached image if available."""
|
||||
cache_path = self.cache_dir / f"{cache_key}.png"
|
||||
if cache_path.exists():
|
||||
try:
|
||||
return Image.open(cache_path)
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
def _cache_image(self, cache_key, image):
|
||||
"""Save image to cache."""
|
||||
cache_path = self.cache_dir / f"{cache_key}.png"
|
||||
try:
|
||||
image.save(cache_path, 'PNG', optimize=True)
|
||||
except Exception as e:
|
||||
print(f"Could not cache image: {e}")
|
||||
|
||||
def get_product_config(self, product_code):
|
||||
"""Get configuration for a specific product."""
|
||||
return self.configs.get(product_code.lower())
|
||||
|
||||
def clear_cache(self, product_code=None):
|
||||
"""Clear cached images. If product_code provided, only clear that product's cache."""
|
||||
if product_code:
|
||||
# Clear specific product cache (would need to track cache keys)
|
||||
pass
|
||||
else:
|
||||
# Clear all cache
|
||||
for cache_file in self.cache_dir.glob('*.png'):
|
||||
try:
|
||||
cache_file.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def recolor_image_advanced(image, source_color_range, target_color):
|
||||
"""
|
||||
Advanced recoloring that replaces pixels within a color range.
|
||||
|
||||
Args:
|
||||
image: PIL Image
|
||||
source_color_range: Dict with 'min' and 'max' RGB tuples
|
||||
target_color: Target RGB tuple
|
||||
"""
|
||||
img_copy = image.copy()
|
||||
pixels = img_copy.load()
|
||||
width, height = img_copy.size
|
||||
|
||||
min_r, min_g, min_b = source_color_range['min']
|
||||
max_r, max_g, max_b = source_color_range['max']
|
||||
|
||||
for y in range(height):
|
||||
for x in range(width):
|
||||
r, g, b, a = pixels[x, y]
|
||||
|
||||
# Check if pixel is within source color range
|
||||
if (min_r <= r <= max_r and
|
||||
min_g <= g <= max_g and
|
||||
min_b <= b <= max_b):
|
||||
|
||||
# Calculate brightness factor
|
||||
brightness = (r + g + b) / 3 / 255.0
|
||||
|
||||
# Apply target color with brightness
|
||||
new_r = int(target_color[0] * brightness)
|
||||
new_g = int(target_color[1] * brightness)
|
||||
new_b = int(target_color[2] * brightness)
|
||||
|
||||
pixels[x, y] = (new_r, new_g, new_b, a)
|
||||
|
||||
return img_copy
|
||||
|
||||
|
||||
def image_to_base64(image, format='PNG'):
|
||||
"""Convert PIL Image to base64 string."""
|
||||
import base64
|
||||
buffered = BytesIO()
|
||||
image.save(buffered, format=format)
|
||||
img_str = base64.b64encode(buffered.getvalue()).decode()
|
||||
return f"data:image/{format.lower()};base64,{img_str}"
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 477 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 959 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 969 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 922 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.8 MiB |
@@ -0,0 +1,451 @@
|
||||
/**
|
||||
* Product Encoding/Decoding Helper
|
||||
*
|
||||
* Provides functions to encode and decode product configurations into compact
|
||||
* hex-based URL strings. Uses bit-packing for efficient, stable encoding.
|
||||
*
|
||||
* Format: v-T-C-M-HHHH-HHHH-HHHH
|
||||
* See /planning/ENCODING_SYSTEM.md for detailed documentation
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// LOOKUP TABLES
|
||||
// ============================================================================
|
||||
|
||||
const ENCODING_VERSION = 0;
|
||||
|
||||
// Main Product Attributes (single hex character, 0-15)
|
||||
const PRODUCT_TYPE = {
|
||||
'PATIO_DOOR': 1,
|
||||
'STORM_DOOR': 3,
|
||||
'STORM_WINDOW': 5,
|
||||
'PRIMARY_WINDOW': 7
|
||||
};
|
||||
|
||||
const PRODUCT_TYPE_REVERSE = {
|
||||
1: 'PATIO_DOOR',
|
||||
3: 'STORM_DOOR',
|
||||
5: 'STORM_WINDOW',
|
||||
7: 'PRIMARY_WINDOW'
|
||||
};
|
||||
|
||||
const PRODUCT_COLOR = {
|
||||
'BLACK': 1,
|
||||
'BRONZE': 2,
|
||||
'SANDSTONE': 3,
|
||||
'WHITE': 4,
|
||||
'TAN': 5,
|
||||
'MILL': 6
|
||||
};
|
||||
|
||||
const PRODUCT_COLOR_REVERSE = {
|
||||
1: 'BLACK',
|
||||
2: 'BRONZE',
|
||||
3: 'SANDSTONE',
|
||||
4: 'WHITE',
|
||||
5: 'TAN',
|
||||
6: 'MILL'
|
||||
};
|
||||
|
||||
const PRODUCT_MATERIAL = {
|
||||
'ALUMINUM': 1,
|
||||
'VINYL': 2
|
||||
};
|
||||
|
||||
const PRODUCT_MATERIAL_REVERSE = {
|
||||
1: 'ALUMINUM',
|
||||
2: 'VINYL'
|
||||
};
|
||||
|
||||
// Hardware Fields (5 bits each, 0-31)
|
||||
const HARDWARE_TYPE = {
|
||||
'NONE': 0,
|
||||
'LEVER': 1,
|
||||
'PULL': 2,
|
||||
'PULL_HANDLE': 3,
|
||||
'DEADBOLT': 4,
|
||||
'HINGE': 5
|
||||
};
|
||||
|
||||
const HARDWARE_TYPE_REVERSE = {
|
||||
0: 'NONE',
|
||||
1: 'LEVER',
|
||||
2: 'PULL',
|
||||
3: 'PULL_HANDLE',
|
||||
4: 'DEADBOLT',
|
||||
5: 'HINGE'
|
||||
};
|
||||
|
||||
const HARDWARE_STYLE = {
|
||||
'NONE': 0,
|
||||
'STANDARD': 1,
|
||||
'PUSH': 2,
|
||||
'ALTERNATIVE': 3,
|
||||
'CONTEMPORARY': 4,
|
||||
'TRADITIONAL': 5
|
||||
};
|
||||
|
||||
const HARDWARE_STYLE_REVERSE = {
|
||||
0: 'NONE',
|
||||
1: 'STANDARD',
|
||||
2: 'PUSH',
|
||||
3: 'ALTERNATIVE',
|
||||
4: 'CONTEMPORARY',
|
||||
5: 'TRADITIONAL'
|
||||
};
|
||||
|
||||
const HARDWARE_COLOR = {
|
||||
'NONE': 0,
|
||||
'BRASS': 1,
|
||||
'WHITE': 2,
|
||||
'BLACK': 3,
|
||||
'SATIN': 4,
|
||||
'NICKEL': 5,
|
||||
'BRONZE': 6
|
||||
};
|
||||
|
||||
const HARDWARE_COLOR_REVERSE = {
|
||||
0: 'NONE',
|
||||
1: 'BRASS',
|
||||
2: 'WHITE',
|
||||
3: 'BLACK',
|
||||
4: 'SATIN',
|
||||
5: 'NICKEL',
|
||||
6: 'BRONZE'
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// ENCODING FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Encode a hardware configuration into a 4-character hex string
|
||||
* Uses 5 bits per field (32 values each)
|
||||
*
|
||||
* @param {number} type - Hardware type (0-31)
|
||||
* @param {number} style - Hardware style (0-31)
|
||||
* @param {number} color - Hardware color (0-31)
|
||||
* @returns {string} 4-character hex string (e.g., "0421")
|
||||
*/
|
||||
function encodeHardware(type, style, color) {
|
||||
// Validate inputs
|
||||
if (type < 0 || type > 31) throw new Error(`Invalid hardware type: ${type} (must be 0-31)`);
|
||||
if (style < 0 || style > 31) throw new Error(`Invalid hardware style: ${style} (must be 0-31)`);
|
||||
if (color < 0 || color > 31) throw new Error(`Invalid hardware color: ${color} (must be 0-31)`);
|
||||
|
||||
// Pack into 16 bits: [reserved(1)][type(5)][style(5)][color(5)]
|
||||
const value = ((type & 0x1F) << 10) | // Bits 10-14
|
||||
((style & 0x1F) << 5) | // Bits 5-9
|
||||
(color & 0x1F); // Bits 0-4
|
||||
|
||||
return value.toString(16).toUpperCase().padStart(4, '0');
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a complete product configuration into a URL string
|
||||
*
|
||||
* @param {Object} config - Product configuration
|
||||
* @param {number} config.type - Product type (0-15)
|
||||
* @param {number} config.color - Product color (0-15)
|
||||
* @param {number} config.material - Product material (0-15)
|
||||
* @param {Array<Object>} config.hardware - Array of hardware items (optional)
|
||||
* @param {number} config.hardware[].type - Hardware type (0-31)
|
||||
* @param {number} config.hardware[].style - Hardware style (0-31)
|
||||
* @param {number} config.hardware[].color - Hardware color (0-31)
|
||||
* @returns {string} Encoded URL string (e.g., "0-1-4-1-0421")
|
||||
*/
|
||||
function encodeProduct(config) {
|
||||
// Validate main attributes
|
||||
if (config.type < 0 || config.type > 15) {
|
||||
throw new Error(`Invalid product type: ${config.type} (must be 0-15)`);
|
||||
}
|
||||
if (config.color < 0 || config.color > 15) {
|
||||
throw new Error(`Invalid product color: ${config.color} (must be 0-15)`);
|
||||
}
|
||||
if (config.material < 0 || config.material > 15) {
|
||||
throw new Error(`Invalid product material: ${config.material} (must be 0-15)`);
|
||||
}
|
||||
|
||||
// Build base string: version-type-color-material
|
||||
const parts = [
|
||||
ENCODING_VERSION.toString(16).toUpperCase(),
|
||||
config.type.toString(16).toUpperCase(),
|
||||
config.color.toString(16).toUpperCase(),
|
||||
config.material.toString(16).toUpperCase()
|
||||
];
|
||||
|
||||
// Add hardware items if present
|
||||
if (config.hardware && config.hardware.length > 0) {
|
||||
config.hardware.forEach(hw => {
|
||||
parts.push(encodeHardware(hw.type, hw.style, hw.color));
|
||||
});
|
||||
}
|
||||
|
||||
return parts.join('-');
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode using named constants (convenience function)
|
||||
*
|
||||
* @param {Object} config - Product configuration with named values
|
||||
* @param {string} config.type - Product type name (e.g., 'PATIO_DOOR')
|
||||
* @param {string} config.color - Color name (e.g., 'WHITE')
|
||||
* @param {string} config.material - Material name (e.g., 'ALUMINUM')
|
||||
* @param {Array<Object>} config.hardware - Array of hardware items (optional)
|
||||
* @returns {string} Encoded URL string
|
||||
*/
|
||||
function encodeProductByName(config) {
|
||||
const numericConfig = {
|
||||
type: PRODUCT_TYPE[config.type],
|
||||
color: PRODUCT_COLOR[config.color],
|
||||
material: PRODUCT_MATERIAL[config.material],
|
||||
hardware: []
|
||||
};
|
||||
|
||||
if (config.hardware && config.hardware.length > 0) {
|
||||
numericConfig.hardware = config.hardware.map(hw => ({
|
||||
type: HARDWARE_TYPE[hw.type],
|
||||
style: HARDWARE_STYLE[hw.style],
|
||||
color: HARDWARE_COLOR[hw.color]
|
||||
}));
|
||||
}
|
||||
|
||||
return encodeProduct(numericConfig);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DECODING FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Decode a hardware hex string back to its components
|
||||
*
|
||||
* @param {string} hex - 4-character hex string (e.g., "0421")
|
||||
* @returns {Object} Decoded hardware configuration
|
||||
*/
|
||||
function decodeHardware(hex) {
|
||||
if (typeof hex !== 'string' || hex.length !== 4) {
|
||||
throw new Error(`Invalid hardware hex string: ${hex} (must be 4 characters)`);
|
||||
}
|
||||
|
||||
const value = parseInt(hex, 16);
|
||||
|
||||
if (isNaN(value)) {
|
||||
throw new Error(`Invalid hex value: ${hex}`);
|
||||
}
|
||||
|
||||
return {
|
||||
type: (value >> 10) & 0x1F, // Extract bits 10-14
|
||||
style: (value >> 5) & 0x1F, // Extract bits 5-9
|
||||
color: value & 0x1F // Extract bits 0-4
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a complete product URL string
|
||||
*
|
||||
* @param {string} encodedString - Encoded URL string (e.g., "0-1-4-1-0421")
|
||||
* @returns {Object} Decoded product configuration
|
||||
*/
|
||||
function decodeProduct(encodedString) {
|
||||
if (typeof encodedString !== 'string') {
|
||||
throw new Error('Invalid encoded string: must be a string');
|
||||
}
|
||||
|
||||
const parts = encodedString.split('-');
|
||||
|
||||
if (parts.length < 4) {
|
||||
throw new Error(`Invalid encoded string: ${encodedString} (must have at least 4 parts)`);
|
||||
}
|
||||
|
||||
// Parse version
|
||||
const version = parseInt(parts[0], 16);
|
||||
if (version !== ENCODING_VERSION) {
|
||||
throw new Error(`Unsupported encoding version: ${version} (current: ${ENCODING_VERSION})`);
|
||||
}
|
||||
|
||||
// Parse main attributes
|
||||
const config = {
|
||||
version: version,
|
||||
type: parseInt(parts[1], 16),
|
||||
color: parseInt(parts[2], 16),
|
||||
material: parseInt(parts[3], 16),
|
||||
hardware: []
|
||||
};
|
||||
|
||||
// Parse hardware items (if present)
|
||||
for (let i = 4; i < parts.length; i++) {
|
||||
config.hardware.push(decodeHardware(parts[i]));
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode and return named values (convenience function)
|
||||
*
|
||||
* @param {string} encodedString - Encoded URL string
|
||||
* @returns {Object} Decoded product with named values
|
||||
*/
|
||||
function decodeProductToNames(encodedString) {
|
||||
const numeric = decodeProduct(encodedString);
|
||||
|
||||
return {
|
||||
version: numeric.version,
|
||||
type: PRODUCT_TYPE_REVERSE[numeric.type] || `UNKNOWN_${numeric.type}`,
|
||||
color: PRODUCT_COLOR_REVERSE[numeric.color] || `UNKNOWN_${numeric.color}`,
|
||||
material: PRODUCT_MATERIAL_REVERSE[numeric.material] || `UNKNOWN_${numeric.material}`,
|
||||
hardware: numeric.hardware.map(hw => ({
|
||||
type: HARDWARE_TYPE_REVERSE[hw.type] || `UNKNOWN_${hw.type}`,
|
||||
style: HARDWARE_STYLE_REVERSE[hw.style] || `UNKNOWN_${hw.style}`,
|
||||
color: HARDWARE_COLOR_REVERSE[hw.color] || `UNKNOWN_${hw.color}`
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// UTILITY FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get the binary representation of a hex string (for debugging)
|
||||
*
|
||||
* @param {string} hex - Hex string
|
||||
* @returns {string} Binary string with spaces every 4 bits
|
||||
*/
|
||||
function hexToBinary(hex) {
|
||||
const value = parseInt(hex, 16);
|
||||
const binary = value.toString(2).padStart(16, '0');
|
||||
return binary.match(/.{1,4}/g).join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a configuration uses only defined values
|
||||
*
|
||||
* @param {Object} config - Product configuration
|
||||
* @returns {Object} Validation result with errors array
|
||||
*/
|
||||
function validateConfiguration(config) {
|
||||
const errors = [];
|
||||
|
||||
if (!PRODUCT_TYPE_REVERSE[config.type]) {
|
||||
errors.push(`Invalid product type: ${config.type}`);
|
||||
}
|
||||
if (!PRODUCT_COLOR_REVERSE[config.color]) {
|
||||
errors.push(`Invalid product color: ${config.color}`);
|
||||
}
|
||||
if (!PRODUCT_MATERIAL_REVERSE[config.material]) {
|
||||
errors.push(`Invalid product material: ${config.material}`);
|
||||
}
|
||||
|
||||
if (config.hardware) {
|
||||
config.hardware.forEach((hw, index) => {
|
||||
if (!HARDWARE_TYPE_REVERSE[hw.type]) {
|
||||
errors.push(`Invalid hardware ${index + 1} type: ${hw.type}`);
|
||||
}
|
||||
if (!HARDWARE_STYLE_REVERSE[hw.style]) {
|
||||
errors.push(`Invalid hardware ${index + 1} style: ${hw.style}`);
|
||||
}
|
||||
if (!HARDWARE_COLOR_REVERSE[hw.color]) {
|
||||
errors.push(`Invalid hardware ${index + 1} color: ${hw.color}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors: errors
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EXPORTS (for Node.js / Module usage)
|
||||
// ============================================================================
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = {
|
||||
// Constants
|
||||
ENCODING_VERSION,
|
||||
PRODUCT_TYPE,
|
||||
PRODUCT_COLOR,
|
||||
PRODUCT_MATERIAL,
|
||||
HARDWARE_TYPE,
|
||||
HARDWARE_STYLE,
|
||||
HARDWARE_COLOR,
|
||||
|
||||
// Encoding functions
|
||||
encodeHardware,
|
||||
encodeProduct,
|
||||
encodeProductByName,
|
||||
|
||||
// Decoding functions
|
||||
decodeHardware,
|
||||
decodeProduct,
|
||||
decodeProductToNames,
|
||||
|
||||
// Utilities
|
||||
hexToBinary,
|
||||
validateConfiguration
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EXAMPLE USAGE
|
||||
// ============================================================================
|
||||
|
||||
/*
|
||||
// Example 1: Encode by numeric values
|
||||
const encoded1 = encodeProduct({
|
||||
type: 1, // Patio Door
|
||||
color: 4, // White
|
||||
material: 1, // Aluminum
|
||||
hardware: [
|
||||
{ type: 1, style: 1, color: 1 } // Lever, Standard, Brass
|
||||
]
|
||||
});
|
||||
console.log(encoded1); // "0-1-4-1-0421"
|
||||
|
||||
// Example 2: Encode by named values
|
||||
const encoded2 = encodeProductByName({
|
||||
type: 'STORM_DOOR',
|
||||
color: 'WHITE',
|
||||
material: 'ALUMINUM',
|
||||
hardware: [
|
||||
{ type: 'LEVER', style: 'STANDARD', color: 'BRASS' },
|
||||
{ type: 'PULL_HANDLE', style: 'ALTERNATIVE', color: 'SATIN' }
|
||||
]
|
||||
});
|
||||
console.log(encoded2); // "0-3-4-1-0421-0C64"
|
||||
|
||||
// Example 3: Decode back to numeric values
|
||||
const decoded1 = decodeProduct("0-1-4-1-0421");
|
||||
console.log(decoded1);
|
||||
// { version: 0, type: 1, color: 4, material: 1, hardware: [{ type: 1, style: 1, color: 1 }] }
|
||||
|
||||
// Example 4: Decode to named values
|
||||
const decoded2 = decodeProductToNames("0-3-4-1-0421-0C64");
|
||||
console.log(decoded2);
|
||||
// {
|
||||
// version: 0,
|
||||
// type: 'STORM_DOOR',
|
||||
// color: 'WHITE',
|
||||
// material: 'ALUMINUM',
|
||||
// hardware: [
|
||||
// { type: 'LEVER', style: 'STANDARD', color: 'BRASS' },
|
||||
// { type: 'PULL_HANDLE', style: 'ALTERNATIVE', color: 'SATIN' }
|
||||
// ]
|
||||
// }
|
||||
|
||||
// Example 5: Debug hardware encoding
|
||||
const hwHex = encodeHardware(3, 12, 20);
|
||||
console.log(hwHex); // "0D94"
|
||||
console.log(hexToBinary(hwHex)); // "0000 1101 1001 0100"
|
||||
console.log(decodeHardware(hwHex)); // { type: 3, style: 12, color: 20 }
|
||||
|
||||
// Example 6: Validate configuration
|
||||
const validation = validateConfiguration({
|
||||
type: 1, color: 4, material: 1,
|
||||
hardware: [{ type: 1, style: 1, color: 1 }]
|
||||
});
|
||||
console.log(validation); // { valid: true, errors: [] }
|
||||
*/
|
||||
+1444
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,652 @@
|
||||
"""
|
||||
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()
|
||||
@@ -0,0 +1,47 @@
|
||||
"""
|
||||
Passenger WSGI file for Flask application
|
||||
Compatible with Python 3.13+
|
||||
Auto-detects /product-finder subdirectory deployment
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Get the directory where this file is located
|
||||
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# Add the application directory to Python path
|
||||
sys.path.insert(0, CURRENT_DIR)
|
||||
|
||||
# Import the Flask application directly from app.py
|
||||
# Note: app.py now auto-detects production environment
|
||||
try:
|
||||
from app import app as application
|
||||
print("✓ Flask application loaded successfully")
|
||||
|
||||
except ImportError as e:
|
||||
# Fallback error handler that shows the actual error
|
||||
def application(environ, start_response):
|
||||
import traceback
|
||||
status = '500 Internal Server Error'
|
||||
error_details = f'Import Error: {str(e)}\n\nTraceback:\n{traceback.format_exc()}\n\nPython Path:\n{sys.path}'
|
||||
output = error_details.encode('utf-8')
|
||||
response_headers = [('Content-type', 'text/plain; charset=utf-8'),
|
||||
('Content-Length', str(len(output)))]
|
||||
start_response(status, response_headers)
|
||||
return [output]
|
||||
print(f"✗ Failed to import Flask app: {e}")
|
||||
|
||||
except Exception as e:
|
||||
# Catch any other startup errors
|
||||
def application(environ, start_response):
|
||||
import traceback
|
||||
status = '500 Internal Server Error'
|
||||
error_details = f'Startup Error: {str(e)}\n\nTraceback:\n{traceback.format_exc()}'
|
||||
output = error_details.encode('utf-8')
|
||||
response_headers = [('Content-type', 'text/plain; charset=utf-8'),
|
||||
('Content-Length', str(len(output)))]
|
||||
start_response(status, response_headers)
|
||||
return [output]
|
||||
print(f"✗ Error starting app: {e}")
|
||||
|
||||
# Passenger requires the application object to be named 'application'
|
||||
@@ -0,0 +1,189 @@
|
||||
"""
|
||||
Interactive Region Coordinate Helper
|
||||
Opens an image and helps you find pixel coordinates for colorable regions.
|
||||
Click on the image to see coordinates.
|
||||
|
||||
Usage:
|
||||
python region_helper.py images/cobrai.jpg
|
||||
"""
|
||||
|
||||
import sys
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import tkinter as tk
|
||||
from tkinter import Canvas
|
||||
from PIL import ImageTk
|
||||
|
||||
|
||||
class RegionHelper:
|
||||
def __init__(self, image_path):
|
||||
self.image_path = image_path
|
||||
self.image = Image.open(image_path)
|
||||
self.width, self.height = self.image.size
|
||||
|
||||
# Setup UI
|
||||
self.root = tk.Tk()
|
||||
self.root.title(f"Region Helper - {image_path}")
|
||||
|
||||
# Scale image if too large
|
||||
self.scale = 1.0
|
||||
max_display_width = 1000
|
||||
max_display_height = 800
|
||||
|
||||
if self.width > max_display_width or self.height > max_display_height:
|
||||
scale_w = max_display_width / self.width
|
||||
scale_h = max_display_height / self.height
|
||||
self.scale = min(scale_w, scale_h)
|
||||
|
||||
new_width = int(self.width * self.scale)
|
||||
new_height = int(self.height * self.scale)
|
||||
self.display_image = self.image.resize((new_width, new_height), Image.LANCZOS)
|
||||
else:
|
||||
self.display_image = self.image.copy()
|
||||
|
||||
# Setup canvas
|
||||
self.canvas = Canvas(
|
||||
self.root,
|
||||
width=self.display_image.width,
|
||||
height=self.display_image.height
|
||||
)
|
||||
self.canvas.pack(side=tk.LEFT)
|
||||
|
||||
# Display image
|
||||
self.photo = ImageTk.PhotoImage(self.display_image)
|
||||
self.canvas.create_image(0, 0, anchor=tk.NW, image=self.photo)
|
||||
|
||||
# Info panel
|
||||
info_frame = tk.Frame(self.root, width=300)
|
||||
info_frame.pack(side=tk.RIGHT, fill=tk.BOTH, padx=10, pady=10)
|
||||
|
||||
tk.Label(info_frame, text="Image Dimensions:", font=('Arial', 12, 'bold')).pack(anchor=tk.W, pady=5)
|
||||
tk.Label(info_frame, text=f"{self.width} x {self.height} pixels").pack(anchor=tk.W)
|
||||
|
||||
if self.scale != 1.0:
|
||||
tk.Label(info_frame, text=f"Scaled: {self.scale:.2%}").pack(anchor=tk.W)
|
||||
|
||||
tk.Label(info_frame, text="\nClick to get coordinates:", font=('Arial', 12, 'bold')).pack(anchor=tk.W, pady=(20, 5))
|
||||
|
||||
self.coord_label = tk.Label(info_frame, text="X: -, Y: -", font=('Arial', 14))
|
||||
self.coord_label.pack(anchor=tk.W, pady=5)
|
||||
|
||||
tk.Label(info_frame, text="\nFirst click = Top-Left", fg='blue').pack(anchor=tk.W)
|
||||
tk.Label(info_frame, text="Second click = Bottom-Right", fg='blue').pack(anchor=tk.W)
|
||||
|
||||
tk.Label(info_frame, text="\nRegion JSON:", font=('Arial', 12, 'bold')).pack(anchor=tk.W, pady=(20, 5))
|
||||
|
||||
self.json_text = tk.Text(info_frame, height=15, width=35, font=('Courier', 9))
|
||||
self.json_text.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
tk.Button(info_frame, text="Clear Points", command=self.clear_points).pack(pady=10)
|
||||
tk.Button(info_frame, text="Copy JSON", command=self.copy_json).pack()
|
||||
|
||||
# Bind events
|
||||
self.canvas.bind('<Motion>', self.on_mouse_move)
|
||||
self.canvas.bind('<Button-1>', self.on_click)
|
||||
|
||||
# Store points
|
||||
self.points = []
|
||||
self.point_ids = []
|
||||
|
||||
self.update_json()
|
||||
|
||||
def on_mouse_move(self, event):
|
||||
"""Show current mouse coordinates."""
|
||||
x = int(event.x / self.scale)
|
||||
y = int(event.y / self.scale)
|
||||
self.coord_label.config(text=f"X: {x}, Y: {y}")
|
||||
|
||||
def on_click(self, event):
|
||||
"""Record clicked point."""
|
||||
x = int(event.x / self.scale)
|
||||
y = int(event.y / self.scale)
|
||||
|
||||
# Limit to 2 points (top-left, bottom-right)
|
||||
if len(self.points) >= 2:
|
||||
self.clear_points()
|
||||
|
||||
self.points.append((x, y))
|
||||
|
||||
# Draw point on canvas
|
||||
color = 'blue' if len(self.points) == 1 else 'red'
|
||||
point_id = self.canvas.create_oval(
|
||||
event.x - 5, event.y - 5,
|
||||
event.x + 5, event.y + 5,
|
||||
fill=color, outline='white', width=2
|
||||
)
|
||||
self.point_ids.append(point_id)
|
||||
|
||||
# Draw rectangle if we have both points
|
||||
if len(self.points) == 2:
|
||||
x1, y1 = self.points[0]
|
||||
x2, y2 = self.points[1]
|
||||
|
||||
# Draw on canvas (scaled)
|
||||
rect_id = self.canvas.create_rectangle(
|
||||
x1 * self.scale, y1 * self.scale,
|
||||
x2 * self.scale, y2 * self.scale,
|
||||
outline='green', width=2, dash=(5, 5)
|
||||
)
|
||||
self.point_ids.append(rect_id)
|
||||
|
||||
self.update_json()
|
||||
|
||||
def clear_points(self):
|
||||
"""Clear all points and rectangles."""
|
||||
for point_id in self.point_ids:
|
||||
self.canvas.delete(point_id)
|
||||
|
||||
self.points = []
|
||||
self.point_ids = []
|
||||
self.update_json()
|
||||
|
||||
def update_json(self):
|
||||
"""Update the JSON display."""
|
||||
self.json_text.delete('1.0', tk.END)
|
||||
|
||||
if len(self.points) == 0:
|
||||
self.json_text.insert('1.0', '{\n "name": "region_name",\n "topLeft": [?, ?],\n "bottomRight": [?, ?],\n "description": "..."\n}')
|
||||
elif len(self.points) == 1:
|
||||
x, y = self.points[0]
|
||||
self.json_text.insert('1.0', f'{{\n "name": "region_name",\n "topLeft": [{x}, {y}],\n "bottomRight": [?, ?],\n "description": "..."\n}}')
|
||||
else:
|
||||
x1, y1 = self.points[0]
|
||||
x2, y2 = self.points[1]
|
||||
|
||||
# Calculate dimensions
|
||||
width = abs(x2 - x1)
|
||||
height = abs(y2 - y1)
|
||||
|
||||
json_str = f'''{{\n "name": "region_name",\n "topLeft": [{x1}, {y1}],\n "bottomRight": [{x2}, {y2}],\n "description": "Width: {width}px, Height: {height}px"\n}}'''
|
||||
self.json_text.insert('1.0', json_str)
|
||||
|
||||
def copy_json(self):
|
||||
"""Copy JSON to clipboard."""
|
||||
json_str = self.json_text.get('1.0', tk.END).strip()
|
||||
self.root.clipboard_clear()
|
||||
self.root.clipboard_append(json_str)
|
||||
self.root.update()
|
||||
|
||||
def run(self):
|
||||
"""Start the GUI."""
|
||||
self.root.mainloop()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python region_helper.py <image_path>")
|
||||
print("Example: python region_helper.py images/cobrai.jpg")
|
||||
sys.exit(1)
|
||||
|
||||
image_path = sys.argv[1]
|
||||
|
||||
try:
|
||||
helper = RegionHelper(image_path)
|
||||
helper.run()
|
||||
except FileNotFoundError:
|
||||
print(f"Error: Image file not found: {image_path}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,4 @@
|
||||
Flask>=3.0.0
|
||||
Werkzeug>=3.0.0
|
||||
# Pillow>=10.0.0 # Optional - only needed for dynamic image generation
|
||||
# If you need image generation, install separately: pip install Pillow
|
||||
@@ -0,0 +1,77 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>404 - Page Not Found</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.error-container {
|
||||
text-align: center;
|
||||
background-color: white;
|
||||
padding: 60px 40px;
|
||||
border: 2px solid #333;
|
||||
border-radius: 8px;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 120px;
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 32px;
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
margin-bottom: 30px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: inline-block;
|
||||
padding: 12px 30px;
|
||||
background-color: #333;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background-color: #555;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="error-container">
|
||||
<h1>404</h1>
|
||||
<h2>Page Not Found</h2>
|
||||
<p>Sorry, the page you're looking for doesn't exist or has been moved.</p>
|
||||
<a href="/" class="button">Go Home</a>
|
||||
<a href="/quiz" class="button" style="margin-left: 10px;">Start Quiz</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,133 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Access Denied - CGW Product Finder</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.access-denied-container {
|
||||
background: white;
|
||||
padding: 50px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||
text-align: center;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 80px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #f56565;
|
||||
margin-bottom: 15px;
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.message {
|
||||
color: #666;
|
||||
margin-bottom: 10px;
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.permission-info {
|
||||
background: #fed7d7;
|
||||
color: #742a2a;
|
||||
padding: 12px;
|
||||
border-radius: 5px;
|
||||
margin: 20px 0;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
border-left: 4px solid #f56565;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 12px 30px;
|
||||
margin: 10px 5px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #5568d3;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #e0e0e0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #d0d0d0;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.contact-info {
|
||||
margin-top: 30px;
|
||||
padding-top: 20px;
|
||||
border-top: 2px solid #e0e0e0;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="access-denied-container">
|
||||
<div class="icon">🚫</div>
|
||||
<h1>Access Denied</h1>
|
||||
<p class="message">
|
||||
You do not have permission to access this page or perform this action.
|
||||
</p>
|
||||
|
||||
{% if required_permission %}
|
||||
<div class="permission-info">
|
||||
Required Permission: <strong>{{ required_permission }}</strong>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<p class="message">
|
||||
If you believe this is an error, please contact your administrator.
|
||||
</p>
|
||||
|
||||
<div style="margin-top: 30px;">
|
||||
<a href="javascript:history.back()" class="btn btn-secondary">← Go Back</a>
|
||||
<a href="{{ url_for('index') }}" class="btn btn-primary">Return to Home</a>
|
||||
</div>
|
||||
|
||||
<div class="contact-info">
|
||||
Need help? Contact your system administrator to request access.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,381 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Dynamic Image Generation - Test Page</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #666;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 30px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
background: #fafafa;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.image-container h3 {
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
max-height: 500px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.loading {
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
background: #f9f9f9;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.control-group label {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.control-group select,
|
||||
.control-group input {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.radio-group label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #333;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: #e3f2fd;
|
||||
border-left: 4px solid #2196F3;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.info-box h4 {
|
||||
color: #1976D2;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.info-box p {
|
||||
color: #555;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.url-display {
|
||||
background: #f5f5f5;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
word-break: break-all;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #ffebee;
|
||||
border-left: 4px solid #f44336;
|
||||
padding: 15px;
|
||||
margin: 20px 0;
|
||||
border-radius: 4px;
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.demo-section {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🎨 Dynamic Image Generation API</h1>
|
||||
<p class="subtitle">Test the product image generation endpoints</p>
|
||||
|
||||
<div class="info-box">
|
||||
<h4>How It Works</h4>
|
||||
<p>
|
||||
This demo uses server-side image processing to dynamically generate product images
|
||||
with different colors and configurations. Select options below to see the image update in real-time.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<h3>Product Configuration</h3>
|
||||
|
||||
<div class="control-group">
|
||||
<label for="product-select">Product:</label>
|
||||
<select id="product-select">
|
||||
<option value="cobrai">COBRAI - Storm Door</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label for="color-select">Color:</label>
|
||||
<select id="color-select">
|
||||
<option value="white">White</option>
|
||||
<option value="black">Black</option>
|
||||
<option value="bronze">Bronze</option>
|
||||
<option value="sandstone">Sandstone</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Hinge Location:</label>
|
||||
<div class="radio-group">
|
||||
<label>
|
||||
<input type="radio" name="hinge" value="right" checked>
|
||||
Right Hinge
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="hinge" value="left">
|
||||
Left Hinge
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>
|
||||
<input type="checkbox" id="cache-checkbox" checked>
|
||||
Use Cache
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button onclick="updateImages()">🔄 Update Images</button>
|
||||
<button onclick="clearCache()">🗑️ Clear Cache</button>
|
||||
<button onclick="getConfig()">📋 Get Config</button>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<div class="image-container">
|
||||
<h3>Direct Image URL Method</h3>
|
||||
<p style="color: #666; font-size: 14px; margin-bottom: 10px;">
|
||||
Using: <code><img src="..."></code>
|
||||
</p>
|
||||
<img id="direct-image" class="product-image" src="" alt="Product">
|
||||
<div class="url-display" id="direct-url">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div class="image-container">
|
||||
<h3>JSON/Base64 Method</h3>
|
||||
<p style="color: #666; font-size: 14px; margin-bottom: 10px;">
|
||||
Using: <code>fetch() + base64</code>
|
||||
</p>
|
||||
<img id="json-image" class="product-image" src="" alt="Product">
|
||||
<div class="url-display" id="json-url">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="error-box" style="display: none;" class="error"></div>
|
||||
|
||||
<div id="config-box" style="display: none; margin-top: 20px;">
|
||||
<h3>Product Configuration</h3>
|
||||
<pre id="config-display" style="background: #f5f5f5; padding: 15px; border-radius: 4px; overflow-x: auto;"></pre>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 40px; padding-top: 20px; border-top: 1px solid #ddd;">
|
||||
<h3>API Endpoints Used</h3>
|
||||
<ul style="line-height: 2;">
|
||||
<li><code>GET /api/product-image/{product}?color={color}&hinge={hinge}</code></li>
|
||||
<li><code>GET /api/product-image/{product}?color={color}&hinge={hinge}&format=json</code></li>
|
||||
<li><code>GET /api/product-config/{product}</code></li>
|
||||
<li><code>POST /api/clear-image-cache</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Initialize on load
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
updateImages();
|
||||
});
|
||||
|
||||
// Auto-update when controls change
|
||||
document.getElementById('product-select').addEventListener('change', updateImages);
|
||||
document.getElementById('color-select').addEventListener('change', updateImages);
|
||||
document.querySelectorAll('[name="hinge"]').forEach(radio => {
|
||||
radio.addEventListener('change', updateImages);
|
||||
});
|
||||
|
||||
function updateImages() {
|
||||
const product = document.getElementById('product-select').value;
|
||||
const color = document.getElementById('color-select').value;
|
||||
const hinge = document.querySelector('[name="hinge"]:checked').value;
|
||||
const useCache = document.getElementById('cache-checkbox').checked;
|
||||
|
||||
hideError();
|
||||
|
||||
// Method 1: Direct image URL
|
||||
updateDirectImage(product, color, hinge, useCache);
|
||||
|
||||
// Method 2: JSON with base64
|
||||
updateJsonImage(product, color, hinge, useCache);
|
||||
}
|
||||
|
||||
function updateDirectImage(product, color, hinge, useCache) {
|
||||
const url = `/api/product-image/${product}?color=${color}&hinge=${hinge}&cache=${useCache}`;
|
||||
|
||||
const img = document.getElementById('direct-image');
|
||||
img.src = url;
|
||||
|
||||
document.getElementById('direct-url').textContent = url;
|
||||
}
|
||||
|
||||
async function updateJsonImage(product, color, hinge, useCache) {
|
||||
const url = `/api/product-image/${product}?color=${color}&hinge=${hinge}&format=json&cache=${useCache}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'success') {
|
||||
document.getElementById('json-image').src = data.image;
|
||||
document.getElementById('json-url').textContent = url;
|
||||
} else {
|
||||
showError(`Error: ${data.error || 'Unknown error'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
showError(`Fetch error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function getConfig() {
|
||||
const product = document.getElementById('product-select').value;
|
||||
const url = `/api/product-config/${product}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'success') {
|
||||
document.getElementById('config-display').textContent =
|
||||
JSON.stringify(data.config, null, 2);
|
||||
document.getElementById('config-box').style.display = 'block';
|
||||
} else {
|
||||
showError(`Error: ${data.error || 'Unknown error'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
showError(`Fetch error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function clearCache() {
|
||||
const product = document.getElementById('product-select').value;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/clear-image-cache', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ productCode: product })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'success') {
|
||||
alert(data.message);
|
||||
updateImages();
|
||||
} else {
|
||||
showError(`Error: ${data.error || 'Unknown error'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
showError(`Fetch error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
const errorBox = document.getElementById('error-box');
|
||||
errorBox.textContent = message;
|
||||
errorBox.style.display = 'block';
|
||||
}
|
||||
|
||||
function hideError() {
|
||||
document.getElementById('error-box').style.display = 'none';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,230 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Product Details Form</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
background-color: white;
|
||||
padding: 20px;
|
||||
border: 2px solid #333;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="number"] {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #333;
|
||||
font-size: 14px;
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background-color: #d0d0ff;
|
||||
border: 1px solid #333;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #b0b0ff;
|
||||
}
|
||||
|
||||
.specifications-section {
|
||||
border: 3px solid #4CAF50;
|
||||
padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
background-color: #f0fff0;
|
||||
}
|
||||
|
||||
.specifications-section.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.conditional-field {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.conditional-field.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.spec-row {
|
||||
display: grid;
|
||||
grid-template-columns: 120px 1fr;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.spec-row label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.spec-row input {
|
||||
border: 1px solid #333;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.questions-section {
|
||||
border: 1px solid #333;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.questions-section p {
|
||||
margin-bottom: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.questions-section .placeholder {
|
||||
font-style: italic;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="form-group">
|
||||
<label>Type (door, window, other)</label>
|
||||
<select id="typeSelect" onchange="toggleOtherField()">
|
||||
<option value="">-- Select Type --</option>
|
||||
<option value="door">Door</option>
|
||||
<option value="window">Window</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group conditional-field" id="otherField">
|
||||
<label>Please specify</label>
|
||||
<input type="text" placeholder="Enter type details">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Description</label>
|
||||
<textarea placeholder="textfield"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Notes</label>
|
||||
<textarea placeholder="textfield"></textarea>
|
||||
</div>
|
||||
|
||||
<button onclick="toggleSpecifications()">Match and Review</button>
|
||||
|
||||
<div class="specifications-section hidden" id="specificationsSection">
|
||||
<div class="spec-row">
|
||||
<label>Product Code:</label>
|
||||
<input type="text" placeholder="" readonly>
|
||||
</div>
|
||||
|
||||
<div class="spec-row">
|
||||
<label>Width:</label>
|
||||
<input type="text" placeholder="">
|
||||
</div>
|
||||
|
||||
<div class="spec-row">
|
||||
<label>Height</label>
|
||||
<input type="text" placeholder="">
|
||||
</div>
|
||||
|
||||
<div class="spec-row">
|
||||
<label>Color</label>
|
||||
<input type="text" placeholder="">
|
||||
</div>
|
||||
|
||||
<div class="spec-row">
|
||||
<label>Frame:</label>
|
||||
<input type="text" placeholder="">
|
||||
</div>
|
||||
|
||||
<div class="spec-row">
|
||||
<label>Screen:</label>
|
||||
<input type="text" placeholder="">
|
||||
</div>
|
||||
|
||||
<div class="spec-row">
|
||||
<label>Etc...</label>
|
||||
<input type="text" placeholder="">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Quantity</label>
|
||||
<input type="number" min="1" value="1" placeholder="">
|
||||
</div>
|
||||
|
||||
<button>Confirm and Add</button>
|
||||
|
||||
<div class="questions-section">
|
||||
<p>Possible questions for customers based on what is selected or missing</p>
|
||||
<div class="placeholder">[list - checkbox per question]</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleSpecifications() {
|
||||
const section = document.getElementById('specificationsSection');
|
||||
section.classList.toggle('hidden');
|
||||
}
|
||||
|
||||
function toggleOtherField() {
|
||||
const typeSelect = document.getElementById('typeSelect');
|
||||
const otherField = document.getElementById('otherField');
|
||||
|
||||
if (typeSelect.value === 'other') {
|
||||
otherField.classList.add('visible');
|
||||
} else {
|
||||
otherField.classList.remove('visible');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,75 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Product Finder</title>
|
||||
<link rel="stylesheet" href="css/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>Product Finder</h1>
|
||||
<div class="user-info-bar" id="userInfoBar">
|
||||
<span id="userLocationInfo"></span>
|
||||
<button onclick="logout()" class="logout-btn">Logout</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="breadcrumb" id="breadcrumb">
|
||||
Start
|
||||
</div>
|
||||
|
||||
<div id="content">
|
||||
<!-- Content will be dynamically loaded here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Base URL for API calls (supports subdirectory deployments)
|
||||
const BASE_URL = '{{ base_url }}';
|
||||
|
||||
// Load user session info
|
||||
async function loadUserInfo() {
|
||||
try {
|
||||
const response = await fetch(BASE_URL + '/api/session');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'success') {
|
||||
const locationNames = {
|
||||
'LINDS': 'Lindsborg',
|
||||
'IOLA': 'Iola',
|
||||
'KC': 'KC',
|
||||
'BMD': 'BMD'
|
||||
};
|
||||
|
||||
const currentLoc = locationNames[data.currentLocation] || data.currentLocation;
|
||||
document.getElementById('userLocationInfo').textContent =
|
||||
`👤 ${data.username} | 📍 ${currentLoc}`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading user info:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
if (confirm('Are you sure you want to log out?')) {
|
||||
try {
|
||||
await fetch(BASE_URL + '/api/logout', { method: 'POST' });
|
||||
window.location.href = BASE_URL + '/login?message=' + encodeURIComponent('Successfully logged out');
|
||||
} catch (error) {
|
||||
window.location.href = BASE_URL + '/login';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load user info when page loads
|
||||
loadUserInfo();
|
||||
</script>
|
||||
<script>
|
||||
// Pass BASE_URL to script.js by setting it as a window variable
|
||||
window.APP_BASE_URL = BASE_URL;
|
||||
</script>
|
||||
<script src="{{ url_for('serve_js', filename='script.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,255 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login - CGW Product Finder</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
background: white;
|
||||
padding: 40px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.login-header h1 {
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.login-header p {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #5568d3;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
background: #cbd5e0;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 12px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 20px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.alert.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background: #fed7d7;
|
||||
color: #742a2a;
|
||||
border-left: 4px solid #f56565;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background: #bee3f8;
|
||||
color: #2c5282;
|
||||
border-left: 4px solid #4299e1;
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.footer-links a {
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.footer-links a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid #667eea;
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
animation: spin 1s linear infinite;
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
vertical-align: middle;
|
||||
display: none;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<div class="login-header">
|
||||
<h1>🔐 CGW Product Finder</h1>
|
||||
<p>Please sign in to continue</p>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-error" id="errorAlert"></div>
|
||||
|
||||
<form id="loginForm">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username" required autofocus
|
||||
placeholder="Enter your username">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" required
|
||||
placeholder="Enter your password">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn" id="loginBtn">
|
||||
Sign In
|
||||
<span class="loading-spinner" id="loadingSpinner"></span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="footer-links">
|
||||
<a href="{{ url_for('user_manager') }}">User Management</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Base URL for API calls (supports subdirectory deployments)
|
||||
const BASE_URL = '{{ base_url }}';
|
||||
|
||||
document.getElementById('loginForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const username = document.getElementById('username').value;
|
||||
const password = document.getElementById('password').value;
|
||||
const loginBtn = document.getElementById('loginBtn');
|
||||
const spinner = document.getElementById('loadingSpinner');
|
||||
|
||||
// Disable button and show spinner
|
||||
loginBtn.disabled = true;
|
||||
spinner.style.display = 'inline-block';
|
||||
|
||||
try {
|
||||
const response = await fetch(BASE_URL + '/api/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'success') {
|
||||
// Check if user needs to select a location
|
||||
if (data.requiresLocationSelection) {
|
||||
// Redirect to location selection page
|
||||
window.location.href = '/select-location';
|
||||
} else {
|
||||
// Redirect to main app
|
||||
window.location.href = '/';
|
||||
}
|
||||
} else {
|
||||
showAlert(data.message || 'Invalid username or password');
|
||||
loginBtn.disabled = false;
|
||||
spinner.style.display = 'none';
|
||||
}
|
||||
} catch (error) {
|
||||
showAlert('Error connecting to server. Please try again.');
|
||||
loginBtn.disabled = false;
|
||||
spinner.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
function showAlert(message) {
|
||||
const alert = document.getElementById('errorAlert');
|
||||
alert.textContent = message;
|
||||
alert.classList.add('show');
|
||||
|
||||
setTimeout(() => {
|
||||
alert.classList.remove('show');
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Check if there's a message in URL (e.g., after logout)
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const message = urlParams.get('message');
|
||||
if (message) {
|
||||
showAlert(message);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,359 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Select Location - CGW Product Finder</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.location-container {
|
||||
background: white;
|
||||
padding: 40px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.location-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.location-header h1 {
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.location-header p {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
background: #f7fafc;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 25px;
|
||||
border-left: 4px solid #667eea;
|
||||
}
|
||||
|
||||
.user-info p {
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.user-info strong {
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.location-list {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.location-item {
|
||||
background: #f7fafc;
|
||||
padding: 15px 20px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
border: 2px solid transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.location-item:hover {
|
||||
background: #e6f2ff;
|
||||
border-color: #667eea;
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.location-item input[type="radio"] {
|
||||
margin-right: 15px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.location-item label {
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.location-badge {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #5568d3;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
background: #cbd5e0;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.btn-logout {
|
||||
background: #f56565;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.btn-logout:hover {
|
||||
background: #e53e3e;
|
||||
box-shadow: 0 5px 15px rgba(245, 101, 101, 0.4);
|
||||
}
|
||||
|
||||
.btn-admin {
|
||||
background: #48bb78;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.btn-admin:hover {
|
||||
background: #38a169;
|
||||
box-shadow: 0 5px 15px rgba(72, 187, 120, 0.4);
|
||||
}
|
||||
|
||||
.btn-admin.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 12px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 20px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.alert.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background: #fed7d7;
|
||||
color: #742a2a;
|
||||
border-left: 4px solid #f56565;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="location-container">
|
||||
<div class="location-header">
|
||||
<h1>📍 Select Your Location</h1>
|
||||
<p>Choose a location to access the Product Finder</p>
|
||||
</div>
|
||||
|
||||
<div class="user-info" id="userInfo">
|
||||
<p><strong>Welcome:</strong> <span id="userName"></span></p>
|
||||
<p><strong>Default Location:</strong> <span id="defaultLocation"></span></p>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-error" id="errorAlert"></div>
|
||||
|
||||
<form id="locationForm">
|
||||
<div class="location-list" id="locationList">
|
||||
<!-- Locations will be populated here -->
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn" id="continueBtn">
|
||||
Continue to Product Finder
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<button class="btn btn-admin hidden" id="userManagementBtn" onclick="goToUserManagement()">
|
||||
⚙️ User Management
|
||||
</button>
|
||||
|
||||
<button class="btn btn-logout" onclick="logout()">
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Base URL for API calls (supports subdirectory deployments)
|
||||
const BASE_URL = '{{ base_url }}';
|
||||
|
||||
const LOCATION_NAMES = {
|
||||
'LINDS': 'Lindsborg',
|
||||
'IOLA': 'Iola',
|
||||
'KC': 'KC',
|
||||
'BMD': 'BMD'
|
||||
};
|
||||
|
||||
let sessionData = null;
|
||||
|
||||
async function loadLocationData() {
|
||||
try {
|
||||
const response = await fetch(BASE_URL + '/api/session');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'success') {
|
||||
sessionData = data;
|
||||
|
||||
// Populate user info
|
||||
document.getElementById('userName').textContent = data.username;
|
||||
document.getElementById('defaultLocation').textContent =
|
||||
LOCATION_NAMES[data.defaultLocation] || data.defaultLocation;
|
||||
|
||||
// Populate location list
|
||||
const locationList = document.getElementById('locationList');
|
||||
const locations = data.accessibleLocations || [];
|
||||
|
||||
if (locations.length === 0) {
|
||||
showAlert('No accessible locations found. Please contact an administrator.');
|
||||
document.getElementById('continueBtn').disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
locationList.innerHTML = locations.map((code, index) => {
|
||||
const isDefault = code === data.defaultLocation;
|
||||
const checked = isDefault ? 'checked' : '';
|
||||
|
||||
return `
|
||||
<div class="location-item" onclick="selectLocation('${code}')">
|
||||
<input type="radio" name="location" value="${code}"
|
||||
id="loc_${code}" ${checked}>
|
||||
<label for="loc_${code}">
|
||||
${LOCATION_NAMES[code] || code}
|
||||
</label>
|
||||
${isDefault ? '<span class="location-badge">Default</span>' : ''}
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
} else {
|
||||
// Not logged in or session expired
|
||||
window.location.href = BASE_URL + '/login?message=' + encodeURIComponent('Please log in first');
|
||||
}
|
||||
|
||||
// Check if user has manage_users permission
|
||||
await checkUserManagementPermission();
|
||||
} catch (error) {
|
||||
showAlert('Error loading location data. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
async function checkUserManagementPermission() {
|
||||
try {
|
||||
const response = await fetch(BASE_URL + '/api/check-permission', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ permission: 'manage_users' })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'success' && data.hasPermission) {
|
||||
document.getElementById('userManagementBtn').classList.remove('hidden');
|
||||
}
|
||||
} catch (error) {
|
||||
// Permission check failed, keep button hidden
|
||||
console.error('Error checking permission:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function goToUserManagement() {
|
||||
window.location.href = BASE_URL + '/users';
|
||||
}
|
||||
|
||||
function selectLocation(code) {
|
||||
document.getElementById(`loc_${code}`).checked = true;
|
||||
}
|
||||
|
||||
document.getElementById('locationForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const selectedLocation = document.querySelector('input[name="location"]:checked')?.value;
|
||||
|
||||
if (!selectedLocation) {
|
||||
showAlert('Please select a location');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(BASE_URL + '/api/select-location', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ location: selectedLocation })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'success') {
|
||||
// Redirect to main app
|
||||
window.location.href = BASE_URL + '/';
|
||||
} else {
|
||||
showAlert(data.message || 'Error selecting location');
|
||||
}
|
||||
} catch (error) {
|
||||
showAlert('Error connecting to server. Please try again.');
|
||||
}
|
||||
});
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
await fetch(BASE_URL + '/api/logout', { method: 'POST' });
|
||||
window.location.href = BASE_URL + '/login?message=' + encodeURIComponent('Successfully logged out');
|
||||
} catch (error) {
|
||||
window.location.href = BASE_URL + '/login';
|
||||
}
|
||||
}
|
||||
|
||||
function showAlert(message) {
|
||||
const alert = document.getElementById('errorAlert');
|
||||
alert.textContent = message;
|
||||
alert.classList.add('show');
|
||||
|
||||
setTimeout(() => {
|
||||
alert.classList.remove('show');
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Load location data on page load
|
||||
loadLocationData();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,976 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>User Manager - CGW Product Finder</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
margin-bottom: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 2px solid #667eea;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.location-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.location-table th {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
border: 1px solid #5568d3;
|
||||
}
|
||||
|
||||
.location-table th.checkbox-header {
|
||||
text-align: center;
|
||||
width: 100px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.location-table th.checkbox-header:hover {
|
||||
background: #5568d3;
|
||||
}
|
||||
|
||||
.location-table td {
|
||||
padding: 12px;
|
||||
border: 1px solid #e0e0e0;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.location-table td.checkbox-cell {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.location-table tr:hover td {
|
||||
background: #f7fafc;
|
||||
}
|
||||
|
||||
.location-table input[type="checkbox"],
|
||||
.location-table input[type="radio"] {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.location-table label {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.location-name {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.tooltip-header {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.tooltip-header .tooltip-text {
|
||||
visibility: hidden;
|
||||
width: 200px;
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
bottom: 125%;
|
||||
left: 50%;
|
||||
margin-left: -100px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.tooltip-header .tooltip-text::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
margin-left: -5px;
|
||||
border-width: 5px;
|
||||
border-style: solid;
|
||||
border-color: #333 transparent transparent transparent;
|
||||
}
|
||||
|
||||
.tooltip-header:hover .tooltip-text {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 30px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #5568d3;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #48bb78;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #38a169;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(72, 187, 120, 0.4);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #f56565;
|
||||
color: white;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #e53e3e;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: #ed8936;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-warning:hover {
|
||||
background: #dd6b20;
|
||||
}
|
||||
|
||||
.user-list {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.user-item {
|
||||
background: #f7fafc;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-left: 4px solid #667eea;
|
||||
}
|
||||
|
||||
.user-item.inactive {
|
||||
opacity: 0.6;
|
||||
border-left-color: #cbd5e0;
|
||||
}
|
||||
|
||||
.user-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.active-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.active-toggle input[type="checkbox"] {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.active-label {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.active-label.active {
|
||||
color: #48bb78;
|
||||
}
|
||||
|
||||
.active-label.inactive {
|
||||
color: #f56565;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.user-info strong {
|
||||
color: #333;
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.user-info span {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 20px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.alert.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: #c6f6d5;
|
||||
color: #22543d;
|
||||
border-left: 4px solid #48bb78;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background: #fed7d7;
|
||||
color: #742a2a;
|
||||
border-left: 4px solid #f56565;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 2px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
display: inline-block;
|
||||
margin-top: 20px;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
padding: 10px 20px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 5px;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.back-link:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal.show {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 28px;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
color: #f56565;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.security-note {
|
||||
background: #fef5e7;
|
||||
border-left: 4px solid #ed8936;
|
||||
padding: 12px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
color: #744210;
|
||||
}
|
||||
|
||||
.security-note strong {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🔐 User Manager</h1>
|
||||
<p>Create and manage user accounts with secure password encoding</p>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-success" id="successAlert"></div>
|
||||
<div class="alert alert-error" id="errorAlert"></div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Add New User</h2>
|
||||
<form id="userForm">
|
||||
<div class="form-group">
|
||||
<label for="username">Username *</label>
|
||||
<input type="text" id="username" name="username" required
|
||||
placeholder="Enter username (e.g., john_doe)">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password *</label>
|
||||
<input type="password" id="password" name="password" required
|
||||
placeholder="Enter a secure password" minlength="6">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Location Configuration</label>
|
||||
<table class="location-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Default Location *</th>
|
||||
<th class="checkbox-header">
|
||||
<label class="tooltip-header" style="display: flex; align-items: center; justify-content: center; cursor: pointer; margin: 0;">
|
||||
<input type="checkbox" id="headerAccessible" onchange="toggleAllCheckboxes('accessible')" style="margin: 0;">
|
||||
<span class="tooltip-text">Can access location</span>
|
||||
</label>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<label>
|
||||
<input type="radio" name="defaultLocation" value="LINDS" required style="margin-right: 8px;">
|
||||
<span class="location-name">Lindsborg</span>
|
||||
</label>
|
||||
</td>
|
||||
<td class="checkbox-cell">
|
||||
<input type="checkbox" class="accessible-checkbox" data-location="LINDS" onchange="updateHeaderCheckbox('accessible')">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label>
|
||||
<input type="radio" name="defaultLocation" value="IOLA" required style="margin-right: 8px;">
|
||||
<span class="location-name">Iola</span>
|
||||
</label>
|
||||
</td>
|
||||
<td class="checkbox-cell">
|
||||
<input type="checkbox" class="accessible-checkbox" data-location="IOLA" onchange="updateHeaderCheckbox('accessible')">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label>
|
||||
<input type="radio" name="defaultLocation" value="KC" required style="margin-right: 8px;">
|
||||
<span class="location-name">KC</span>
|
||||
</label>
|
||||
</td>
|
||||
<td class="checkbox-cell">
|
||||
<input type="checkbox" class="accessible-checkbox" data-location="KC" onchange="updateHeaderCheckbox('accessible')">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label>
|
||||
<input type="radio" name="defaultLocation" value="BMD" required style="margin-right: 8px;">
|
||||
<span class="location-name">BMD</span>
|
||||
</label>
|
||||
</td>
|
||||
<td class="checkbox-cell">
|
||||
<input type="checkbox" class="accessible-checkbox" data-location="BMD" onchange="updateHeaderCheckbox('accessible')">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Add User</button>
|
||||
</form>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number" id="userCount">0</div>
|
||||
<div class="stat-label">Total Users</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>User List</h2>
|
||||
<div class="user-list" id="userList">
|
||||
<p style="color: #666; text-align: center;">No users added yet.</p>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button class="btn btn-success" id="downloadBtn" onclick="downloadUsers()">
|
||||
📥 Download Users JSON
|
||||
</button>
|
||||
<button class="btn btn-danger" id="clearBtn" onclick="clearAllUsers()">
|
||||
🗑️ Clear All Users
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="{{ url_for('index') }}" class="back-link">← Back to Product Finder</a>
|
||||
</div>
|
||||
|
||||
<!-- Change Password Modal -->
|
||||
<div id="changePasswordModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>🔑 Change Password</h2>
|
||||
<button class="close-btn" onclick="closePasswordModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="security-note">
|
||||
<strong>🔒 Security Verification Required</strong>
|
||||
You must enter your current password to change another user's password.
|
||||
</div>
|
||||
<form id="changePasswordForm">
|
||||
<div class="form-group">
|
||||
<label>User to Update</label>
|
||||
<input type="text" id="targetUsername" readonly style="background: #f0f0f0;">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="newPassword">New Password for User *</label>
|
||||
<input type="password" id="newPassword" required
|
||||
placeholder="Enter new password for this user">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="confirmPassword">Confirm New Password *</label>
|
||||
<input type="password" id="confirmPassword" required
|
||||
placeholder="Re-enter new password">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="currentUserPassword">Your Password (Current User) *</label>
|
||||
<input type="password" id="currentUserPassword" required
|
||||
placeholder="Enter YOUR password to verify">
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
Change Password
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger" onclick="closePasswordModal()">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Base URL for API calls (supports subdirectory deployments)
|
||||
const BASE_URL = '{{ base_url }}';
|
||||
|
||||
let users = [];
|
||||
|
||||
const LOCATIONS = [
|
||||
{ name: 'Lindsborg', code: 'LINDS' },
|
||||
{ name: 'Iola', code: 'IOLA' },
|
||||
{ name: 'KC', code: 'KC' },
|
||||
{ name: 'BMD', code: 'BMD' }
|
||||
];
|
||||
|
||||
// Toggle all checkboxes in a column
|
||||
function toggleAllCheckboxes(type) {
|
||||
const headerCheckbox = document.getElementById(`header${type.charAt(0).toUpperCase() + type.slice(1)}`);
|
||||
const checkboxes = document.querySelectorAll(`.${type}-checkbox`);
|
||||
|
||||
// Get the state AFTER the checkbox was clicked (it's already toggled by the browser)
|
||||
const newState = headerCheckbox.checked;
|
||||
|
||||
// Apply this state to all row checkboxes
|
||||
checkboxes.forEach(cb => cb.checked = newState);
|
||||
|
||||
// Clear indeterminate state since we're setting all to the same value
|
||||
headerCheckbox.indeterminate = false;
|
||||
}
|
||||
|
||||
// Update header checkbox state based on individual checkboxes
|
||||
function updateHeaderCheckbox(type) {
|
||||
const headerCheckbox = document.getElementById(`header${type.charAt(0).toUpperCase() + type.slice(1)}`);
|
||||
const checkboxes = document.querySelectorAll(`.${type}-checkbox`);
|
||||
const checkedCount = Array.from(checkboxes).filter(cb => cb.checked).length;
|
||||
|
||||
if (checkedCount === 0) {
|
||||
headerCheckbox.checked = false;
|
||||
headerCheckbox.indeterminate = false;
|
||||
} else if (checkedCount === checkboxes.length) {
|
||||
headerCheckbox.checked = true;
|
||||
headerCheckbox.indeterminate = false;
|
||||
} else {
|
||||
headerCheckbox.checked = false;
|
||||
headerCheckbox.indeterminate = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Load existing users on page load
|
||||
async function loadUsers() {
|
||||
try {
|
||||
const response = await fetch(BASE_URL + '/api/users');
|
||||
const data = await response.json();
|
||||
if (data.status === 'success') {
|
||||
users = data.users || [];
|
||||
renderUsers();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading users:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Render users in the list
|
||||
function renderUsers() {
|
||||
const userList = document.getElementById('userList');
|
||||
const userCount = document.getElementById('userCount');
|
||||
|
||||
userCount.textContent = users.length;
|
||||
|
||||
if (users.length === 0) {
|
||||
userList.innerHTML = '<p style="color: #666; text-align: center;">No users added yet.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
userList.innerHTML = users.map((user, index) => {
|
||||
// Build location info string
|
||||
let locationInfo = '';
|
||||
|
||||
if (user.locationSettings) {
|
||||
const defaultLoc = LOCATIONS.find(l => l.code === user.defaultLocation);
|
||||
const defaultName = defaultLoc ? defaultLoc.name : user.defaultLocation;
|
||||
|
||||
const accessible = Object.entries(user.locationSettings)
|
||||
.filter(([code, settings]) => settings.accessible)
|
||||
.map(([code]) => LOCATIONS.find(l => l.code === code)?.name || code);
|
||||
|
||||
locationInfo = `Default: ${defaultName}`;
|
||||
if (accessible.length > 0) locationInfo += ` | Access: ${accessible.join(', ')}`;
|
||||
} else if (user.mainLocation) {
|
||||
// Backwards compatibility
|
||||
const mainLoc = LOCATIONS.find(l => l.code === user.mainLocation);
|
||||
locationInfo = `Main: ${mainLoc ? mainLoc.name : user.mainLocation}`;
|
||||
if (user.alternateLocations && user.alternateLocations.length > 0) {
|
||||
const altNames = user.alternateLocations.map(code =>
|
||||
LOCATIONS.find(l => l.code === code)?.name || code
|
||||
);
|
||||
locationInfo += ` | Alt: ${altNames.join(', ')}`;
|
||||
}
|
||||
} else if (user.location) {
|
||||
// Old format
|
||||
locationInfo = user.location;
|
||||
}
|
||||
|
||||
const isActive = user.active !== false; // Default to active if not specified
|
||||
const activeClass = isActive ? '' : ' inactive';
|
||||
const activeStatus = isActive ? 'active' : 'inactive';
|
||||
const activeLabel = isActive ? 'Active' : 'Inactive';
|
||||
|
||||
return `
|
||||
<div class="user-item${activeClass}">
|
||||
<div class="user-info">
|
||||
<strong>👤 ${user.username}</strong>
|
||||
<span>📍 ${locationInfo}</span>
|
||||
</div>
|
||||
<div class="user-actions">
|
||||
<div class="active-toggle">
|
||||
<input type="checkbox" ${isActive ? 'checked' : ''}
|
||||
onchange="toggleUserActive(${index})"
|
||||
id="userActive${index}">
|
||||
<label for="userActive${index}" class="active-label ${activeStatus}">${activeLabel}</label>
|
||||
</div>
|
||||
<button class="btn btn-warning" onclick="openPasswordModal(${index})" style="margin: 0 5px; padding: 8px 16px;">
|
||||
Change Password
|
||||
</button>
|
||||
<button class="btn btn-danger" onclick="deleteUser(${index})" style="margin: 0; padding: 8px 16px;">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Handle form submission
|
||||
document.getElementById('userForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Get default location (radio button)
|
||||
const defaultLocation = document.querySelector('input[name="defaultLocation"]:checked')?.value;
|
||||
|
||||
// Get location settings
|
||||
const locationSettings = {};
|
||||
LOCATIONS.forEach(loc => {
|
||||
const accessibleCheckbox = document.querySelector(`.accessible-checkbox[data-location="${loc.code}"]`);
|
||||
|
||||
locationSettings[loc.code] = {
|
||||
accessible: accessibleCheckbox ? accessibleCheckbox.checked : false
|
||||
};
|
||||
});
|
||||
|
||||
const formData = {
|
||||
username: document.getElementById('username').value,
|
||||
password: document.getElementById('password').value,
|
||||
defaultLocation: defaultLocation,
|
||||
locationSettings: locationSettings,
|
||||
active: true // New users are active by default
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(BASE_URL + '/api/users', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'success') {
|
||||
showAlert('success', `User "${formData.username}" added successfully!`);
|
||||
users = data.users;
|
||||
renderUsers();
|
||||
document.getElementById('userForm').reset();
|
||||
// Reset header checkboxes
|
||||
document.getElementById('headerAccessible').checked = false;
|
||||
document.getElementById('headerAccessible').indeterminate = false;
|
||||
} else {
|
||||
showAlert('error', data.message || 'Error adding user');
|
||||
}
|
||||
} catch (error) {
|
||||
showAlert('error', 'Error connecting to server: ' + error.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Toggle user active status
|
||||
async function toggleUserActive(index) {
|
||||
try {
|
||||
const isActive = document.getElementById(`userActive${index}`).checked;
|
||||
|
||||
const response = await fetch(BASE_URL + `/api/users/${index}/active`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ active: isActive })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'success') {
|
||||
users = data.users;
|
||||
renderUsers();
|
||||
} else {
|
||||
showAlert('error', data.message || 'Error updating user status');
|
||||
// Revert checkbox on error
|
||||
document.getElementById(`userActive${index}`).checked = !isActive;
|
||||
}
|
||||
} catch (error) {
|
||||
showAlert('error', 'Error connecting to server: ' + error.message);
|
||||
// Revert checkbox on error
|
||||
const checkbox = document.getElementById(`userActive${index}`);
|
||||
checkbox.checked = !checkbox.checked;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete a specific user
|
||||
async function deleteUser(index) {
|
||||
if (!confirm('Are you sure you want to delete this user?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(BASE_URL + `/api/users/${index}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'success') {
|
||||
showAlert('success', 'User deleted successfully!');
|
||||
users = data.users;
|
||||
renderUsers();
|
||||
} else {
|
||||
showAlert('error', data.message || 'Error deleting user');
|
||||
}
|
||||
} catch (error) {
|
||||
showAlert('error', 'Error connecting to server: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear all users
|
||||
async function clearAllUsers() {
|
||||
if (!confirm('Are you sure you want to delete ALL users? This cannot be undone!')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(BASE_URL + '/api/users/clear', {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'success') {
|
||||
showAlert('success', 'All users cleared successfully!');
|
||||
users = [];
|
||||
renderUsers();
|
||||
} else {
|
||||
showAlert('error', data.message || 'Error clearing users');
|
||||
}
|
||||
} catch (error) {
|
||||
showAlert('error', 'Error connecting to server: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Download users as JSON
|
||||
function downloadUsers() {
|
||||
if (users.length === 0) {
|
||||
showAlert('error', 'No users to download!');
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.href = BASE_URL + '/api/users/download';
|
||||
showAlert('success', 'Downloading users.json...');
|
||||
}
|
||||
|
||||
// Password change modal functions
|
||||
let targetUserIndex = null;
|
||||
|
||||
function openPasswordModal(userIndex) {
|
||||
targetUserIndex = userIndex;
|
||||
const user = users[userIndex];
|
||||
document.getElementById('targetUsername').value = user.username;
|
||||
document.getElementById('changePasswordModal').classList.add('show');
|
||||
document.getElementById('changePasswordForm').reset();
|
||||
document.getElementById('targetUsername').value = user.username;
|
||||
}
|
||||
|
||||
function closePasswordModal() {
|
||||
document.getElementById('changePasswordModal').classList.remove('show');
|
||||
document.getElementById('changePasswordForm').reset();
|
||||
targetUserIndex = null;
|
||||
}
|
||||
|
||||
// Handle password change form submission
|
||||
document.getElementById('changePasswordForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const newPassword = document.getElementById('newPassword').value;
|
||||
const confirmPassword = document.getElementById('confirmPassword').value;
|
||||
const currentUserPassword = document.getElementById('currentUserPassword').value;
|
||||
|
||||
// Validate passwords match
|
||||
if (newPassword !== confirmPassword) {
|
||||
showAlert('error', 'New passwords do not match!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate password length
|
||||
if (newPassword.length < 6) {
|
||||
showAlert('error', 'Password must be at least 6 characters long!');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(BASE_URL + `/api/users/${targetUserIndex}/change-password`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
newPassword: newPassword,
|
||||
currentUserPassword: currentUserPassword
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'success') {
|
||||
showAlert('success', data.message);
|
||||
closePasswordModal();
|
||||
} else {
|
||||
showAlert('error', data.message || 'Error changing password');
|
||||
}
|
||||
} catch (error) {
|
||||
showAlert('error', 'Error connecting to server. Please try again.');
|
||||
}
|
||||
});
|
||||
|
||||
// Close modal when clicking outside of it
|
||||
window.onclick = function(event) {
|
||||
const modal = document.getElementById('changePasswordModal');
|
||||
if (event.target === modal) {
|
||||
closePasswordModal();
|
||||
}
|
||||
};
|
||||
|
||||
// Show alert messages
|
||||
function showAlert(type, message) {
|
||||
const alertId = type === 'success' ? 'successAlert' : 'errorAlert';
|
||||
const alert = document.getElementById(alertId);
|
||||
|
||||
alert.textContent = message;
|
||||
alert.classList.add('show');
|
||||
|
||||
setTimeout(() => {
|
||||
alert.classList.remove('show');
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Load users when page loads
|
||||
loadUsers();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+228
@@ -0,0 +1,228 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Comprehensive app test script
|
||||
Run this through control panel to diagnose issues
|
||||
Writes detailed logs to test_app.log
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
# Setup logging
|
||||
log_file = os.path.join(os.path.dirname(__file__), 'test_app.log')
|
||||
|
||||
def log(message, also_print=True):
|
||||
"""Write to log file and optionally print"""
|
||||
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
full_message = f"[{timestamp}] {message}"
|
||||
|
||||
with open(log_file, 'a') as f:
|
||||
f.write(full_message + '\n')
|
||||
|
||||
if also_print:
|
||||
print(full_message)
|
||||
|
||||
try:
|
||||
log("="*70)
|
||||
log("APP TEST SCRIPT - START")
|
||||
log("="*70)
|
||||
|
||||
# 1. Environment Info
|
||||
log("\n1. PYTHON ENVIRONMENT")
|
||||
log(f" Python: {sys.version}")
|
||||
log(f" Executable: {sys.executable}")
|
||||
log(f" CWD: {os.getcwd()}")
|
||||
|
||||
# 2. Check environment variables
|
||||
log("\n2. ENVIRONMENT VARIABLES")
|
||||
log(f" APPLICATION_ROOT: {os.environ.get('APPLICATION_ROOT', 'NOT SET')}")
|
||||
log(f" FLASK_ENV: {os.environ.get('FLASK_ENV', 'NOT SET')}")
|
||||
log(f" SECRET_KEY: {'SET' if os.environ.get('SECRET_KEY') else 'NOT SET'}")
|
||||
|
||||
# 3. Check critical files
|
||||
log("\n3. FILE STRUCTURE CHECK")
|
||||
files_to_check = [
|
||||
'app.py',
|
||||
'passenger_wsgi.py',
|
||||
'config.py',
|
||||
'data/users.json',
|
||||
'templates/login.html',
|
||||
'templates/index2.html',
|
||||
'templates/select_location.html',
|
||||
'templates/user_manager.html'
|
||||
]
|
||||
|
||||
all_files_exist = True
|
||||
for file_path in files_to_check:
|
||||
full_path = os.path.join(os.path.dirname(__file__), file_path)
|
||||
exists = os.path.exists(full_path)
|
||||
status = "✓" if exists else "✗ MISSING"
|
||||
log(f" {status} {file_path}")
|
||||
if not exists:
|
||||
all_files_exist = False
|
||||
|
||||
if not all_files_exist:
|
||||
log("\n⚠️ CRITICAL: Missing files detected!")
|
||||
|
||||
# 4. Check dependencies
|
||||
log("\n4. PYTHON PACKAGES")
|
||||
packages = [
|
||||
('flask', 'Flask'),
|
||||
('werkzeug', 'Werkzeug')
|
||||
]
|
||||
|
||||
all_packages_ok = True
|
||||
for module_name, display_name in packages:
|
||||
try:
|
||||
module = __import__(module_name)
|
||||
try:
|
||||
from importlib.metadata import version
|
||||
ver = version(module_name)
|
||||
except:
|
||||
ver = getattr(module, '__version__', 'unknown')
|
||||
log(f" ✓ {display_name}: {ver}")
|
||||
except ImportError as e:
|
||||
log(f" ✗ {display_name}: NOT INSTALLED ({e})")
|
||||
all_packages_ok = False
|
||||
|
||||
if not all_packages_ok:
|
||||
log("\n⚠️ CRITICAL: Missing packages! Run: pip install Flask==3.0.0 Werkzeug==3.0.1")
|
||||
sys.exit(1)
|
||||
|
||||
# 5. Try importing app
|
||||
log("\n5. APP IMPORT TEST")
|
||||
log(" Attempting to import Flask app...")
|
||||
|
||||
try:
|
||||
# Make sure current directory is in path
|
||||
current_dir = os.path.dirname(__file__)
|
||||
if current_dir not in sys.path:
|
||||
sys.path.insert(0, current_dir)
|
||||
|
||||
from app import app
|
||||
log(" ✓ Flask app imported successfully!")
|
||||
|
||||
# Check app config
|
||||
log("\n6. APP CONFIGURATION")
|
||||
log(f" APPLICATION_ROOT: {app.config.get('APPLICATION_ROOT', 'NOT SET')}")
|
||||
log(f" DEBUG: {app.config.get('DEBUG')}")
|
||||
log(f" SECRET_KEY: {'SET (' + str(len(app.config.get('SECRET_KEY', ''))) + ' chars)' if app.config.get('SECRET_KEY') else 'NOT SET'}")
|
||||
|
||||
# Check if middleware applied
|
||||
log(f" WSGI App Type: {type(app.wsgi_app).__name__}")
|
||||
if 'PrefixMiddleware' in str(type(app.wsgi_app)):
|
||||
log(" ✓ PrefixMiddleware is active")
|
||||
else:
|
||||
log(" ⚠️ PrefixMiddleware NOT active (may cause issues at /product-finder)")
|
||||
|
||||
# Count routes
|
||||
log("\n7. ROUTES CHECK")
|
||||
routes = list(app.url_map.iter_rules())
|
||||
log(f" Total routes: {len(routes)}")
|
||||
|
||||
# Check critical routes
|
||||
critical_routes = ['/login', '/api/login', '/users', '/select-location']
|
||||
log(" Critical routes:")
|
||||
for route_path in critical_routes:
|
||||
found = any(str(rule) == route_path or str(rule).startswith(route_path + '<') for rule in routes)
|
||||
status = "✓" if found else "✗ MISSING"
|
||||
log(f" {status} {route_path}")
|
||||
|
||||
# 8. Test loading users
|
||||
log("\n8. USER DATA TEST")
|
||||
try:
|
||||
users_file = os.path.join(current_dir, 'data', 'users.json')
|
||||
if os.path.exists(users_file):
|
||||
import json
|
||||
with open(users_file, 'r') as f:
|
||||
users = json.load(f)
|
||||
log(f" ✓ Users file loaded: {len(users)} users")
|
||||
log(f" Usernames: {', '.join([u.get('username', '?') for u in users])}")
|
||||
else:
|
||||
log(f" ✗ Users file NOT FOUND at {users_file}")
|
||||
except Exception as e:
|
||||
log(f" ✗ Error loading users: {e}")
|
||||
|
||||
# 9. Test a simple request
|
||||
log("\n9. REQUEST TEST")
|
||||
log(" Testing if app can handle requests...")
|
||||
|
||||
with app.test_client() as client:
|
||||
# Test root redirect
|
||||
try:
|
||||
response = client.get('/')
|
||||
log(f" GET / -> Status: {response.status_code}")
|
||||
if response.status_code == 302:
|
||||
log(f" Redirects to: {response.location}")
|
||||
except Exception as e:
|
||||
log(f" ✗ Error testing /: {e}")
|
||||
|
||||
# Test login page
|
||||
try:
|
||||
response = client.get('/login')
|
||||
log(f" GET /login -> Status: {response.status_code}")
|
||||
if response.status_code != 200:
|
||||
log(f" ⚠️ Login page returned {response.status_code} instead of 200")
|
||||
except Exception as e:
|
||||
log(f" ✗ Error testing /login: {e}")
|
||||
|
||||
# 10. Summary
|
||||
log("\n" + "="*70)
|
||||
log("TEST SUMMARY")
|
||||
log("="*70)
|
||||
|
||||
if all_files_exist and all_packages_ok:
|
||||
log("✓ All files present")
|
||||
log("✓ All packages installed")
|
||||
log("✓ App imports successfully")
|
||||
log(f"✓ {len(routes)} routes registered")
|
||||
|
||||
app_root = app.config.get('APPLICATION_ROOT', '/')
|
||||
if app_root == '/product-finder':
|
||||
log("✓ Configured for /product-finder deployment")
|
||||
else:
|
||||
log(f"⚠️ APPLICATION_ROOT is '{app_root}' (should be '/product-finder' for production)")
|
||||
|
||||
log("\n🎉 APP APPEARS READY!")
|
||||
log("\nIf still getting 500 errors:")
|
||||
log("1. Check that data/users.json exists")
|
||||
log("2. Verify APPLICATION_ROOT env var is set to /product-finder")
|
||||
log("3. Check Apache error log for runtime errors")
|
||||
log("4. Ensure .htaccess is correct")
|
||||
|
||||
else:
|
||||
log("\n⚠️ ISSUES FOUND - Fix these before deployment")
|
||||
|
||||
log("="*70)
|
||||
log(f"\nLog saved to: {log_file}")
|
||||
|
||||
except ImportError as e:
|
||||
log(f" ✗ FAILED to import app!")
|
||||
log(f" Error: {e}")
|
||||
|
||||
import traceback
|
||||
log("\nFull traceback:")
|
||||
log(traceback.format_exc())
|
||||
|
||||
log("\n❌ CRITICAL ERROR: Cannot import Flask app")
|
||||
log("Check that app.py exists and has no syntax errors")
|
||||
|
||||
except Exception as e:
|
||||
log(f" ✗ Unexpected error during import!")
|
||||
log(f" Error: {e}")
|
||||
|
||||
import traceback
|
||||
log("\nFull traceback:")
|
||||
log(traceback.format_exc())
|
||||
|
||||
except Exception as e:
|
||||
log(f"\n❌ FATAL ERROR: {e}")
|
||||
import traceback
|
||||
log(traceback.format_exc())
|
||||
|
||||
finally:
|
||||
log("\n" + "="*70)
|
||||
log("TEST COMPLETE")
|
||||
log("="*70)
|
||||
print(f"\n📄 Full log saved to: {log_file}")
|
||||
print("Download this file to see all test results")
|
||||
@@ -0,0 +1,171 @@
|
||||
"""
|
||||
Deployment diagnostic script for /product-finder
|
||||
Run this on the server to verify configuration
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
print("=" * 70)
|
||||
print("CGW PRODUCT FINDER - DEPLOYMENT DIAGNOSTIC")
|
||||
print("=" * 70)
|
||||
print()
|
||||
|
||||
# Check Python version
|
||||
print("📌 Python Environment:")
|
||||
print(f" Version: {sys.version}")
|
||||
print(f" Executable: {sys.executable}")
|
||||
print(f" Current Directory: {os.getcwd()}")
|
||||
print()
|
||||
|
||||
# Check environment variable
|
||||
print("📌 Environment Variables:")
|
||||
app_root_env = os.environ.get('APPLICATION_ROOT')
|
||||
if app_root_env:
|
||||
print(f" APPLICATION_ROOT: {app_root_env} ✓")
|
||||
else:
|
||||
print(f" APPLICATION_ROOT: NOT SET ⚠️")
|
||||
print(f" Setting temporarily for test: /product-finder")
|
||||
os.environ['APPLICATION_ROOT'] = '/product-finder'
|
||||
print()
|
||||
|
||||
# Try importing Flask
|
||||
print("📌 Flask Installation:")
|
||||
try:
|
||||
import flask
|
||||
print(f" Flask version: {flask.__version__} ✓")
|
||||
except ImportError as e:
|
||||
print(f" ✗ Flask not installed: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Try importing app
|
||||
print()
|
||||
print("📌 Application Import:")
|
||||
try:
|
||||
from app import app
|
||||
print(f" ✓ App imported successfully")
|
||||
except Exception as e:
|
||||
print(f" ✗ Error importing app:")
|
||||
print(f" {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
# Check app configuration
|
||||
print()
|
||||
print("📌 Application Configuration:")
|
||||
print(f" APPLICATION_ROOT: {app.config.get('APPLICATION_ROOT')}")
|
||||
print(f" SECRET_KEY set: {'Yes' if app.config.get('SECRET_KEY') else 'No'}")
|
||||
print(f" DEBUG mode: {app.config.get('DEBUG')}")
|
||||
print(f" ENV: {app.config.get('ENV', 'not set')}")
|
||||
print()
|
||||
|
||||
# Check middleware
|
||||
print("📌 WSGI Middleware:")
|
||||
wsgi_app_type = type(app.wsgi_app).__name__
|
||||
if 'PrefixMiddleware' in str(type(app.wsgi_app)):
|
||||
print(f" ✓ PrefixMiddleware applied")
|
||||
print(f" Type: {wsgi_app_type}")
|
||||
elif wsgi_app_type == 'Flask':
|
||||
print(f" ⚠️ No middleware applied (running at root)")
|
||||
print(f" Type: {wsgi_app_type}")
|
||||
else:
|
||||
print(f" Middleware type: {wsgi_app_type}")
|
||||
print()
|
||||
|
||||
# Check routes
|
||||
print("📌 Registered Routes:")
|
||||
routes_count = len(list(app.url_map.iter_rules()))
|
||||
print(f" Total routes: {routes_count}")
|
||||
|
||||
# Check critical routes
|
||||
critical_routes = ['/login', '/api/login', '/users', '/api/session']
|
||||
print(f" Critical routes:")
|
||||
for route in critical_routes:
|
||||
found = any(str(rule) == route for rule in app.url_map.iter_rules())
|
||||
status = "✓" if found else "✗"
|
||||
print(f" {status} {route}")
|
||||
print()
|
||||
|
||||
# Check file structure
|
||||
print("📌 File Structure:")
|
||||
critical_files = [
|
||||
'app.py',
|
||||
'passenger_wsgi.py',
|
||||
'config.py',
|
||||
'templates/login.html',
|
||||
'templates/index2.html',
|
||||
'data/users.json'
|
||||
]
|
||||
|
||||
for file_path in critical_files:
|
||||
exists = os.path.exists(file_path)
|
||||
status = "✓" if exists else "✗"
|
||||
print(f" {status} {file_path}")
|
||||
print()
|
||||
|
||||
# Check dependencies
|
||||
print("📌 Python Dependencies:")
|
||||
deps = [
|
||||
('werkzeug', 'Werkzeug'),
|
||||
('flask', 'Flask'),
|
||||
]
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
deps.append(('PIL', 'Pillow'))
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
for module_name, display_name in deps:
|
||||
try:
|
||||
module = __import__(module_name)
|
||||
version = getattr(module, '__version__', 'unknown')
|
||||
print(f" ✓ {display_name}: {version}")
|
||||
except ImportError:
|
||||
print(f" ✗ {display_name}: NOT INSTALLED")
|
||||
print()
|
||||
|
||||
# Summary
|
||||
print("=" * 70)
|
||||
print("DEPLOYMENT STATUS")
|
||||
print("=" * 70)
|
||||
|
||||
issues = []
|
||||
|
||||
if not os.environ.get('APPLICATION_ROOT'):
|
||||
issues.append("APPLICATION_ROOT not set in environment")
|
||||
|
||||
if app.config.get('APPLICATION_ROOT') == '/':
|
||||
issues.append("APPLICATION_ROOT is '/' but should be '/product-finder' for production")
|
||||
|
||||
if not os.path.exists('templates/login.html'):
|
||||
issues.append("Template files missing")
|
||||
|
||||
if not os.path.exists('data/users.json'):
|
||||
issues.append("User data file missing")
|
||||
|
||||
if issues:
|
||||
print()
|
||||
print("⚠️ ISSUES FOUND:")
|
||||
for issue in issues:
|
||||
print(f" - {issue}")
|
||||
print()
|
||||
print("📝 NEXT STEPS:")
|
||||
print(" 1. Upload missing files to server")
|
||||
print(" 2. Set APPLICATION_ROOT environment variable")
|
||||
print(" 3. Restart Passenger: touch tmp/restart.txt")
|
||||
print(" 4. Check error logs for details")
|
||||
else:
|
||||
print()
|
||||
print("✓ All checks passed!")
|
||||
print()
|
||||
print("🚀 DEPLOYMENT READY")
|
||||
print()
|
||||
print("To restart Passenger:")
|
||||
print(" mkdir -p tmp && touch tmp/restart.txt")
|
||||
print()
|
||||
print("To test:")
|
||||
print(" https://columbiawindows.com/product-finder/")
|
||||
|
||||
print()
|
||||
print("=" * 70)
|
||||
@@ -0,0 +1,86 @@
|
||||
"""
|
||||
Quick test to verify Flask routes are registered correctly
|
||||
Run this from the app folder: python test_routes.py
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add current directory to path
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
try:
|
||||
from app import app
|
||||
|
||||
print("=" * 60)
|
||||
print("FLASK ROUTES TEST")
|
||||
print("=" * 60)
|
||||
print()
|
||||
print("✓ Flask app imported successfully")
|
||||
print()
|
||||
print("Registered routes:")
|
||||
print("-" * 60)
|
||||
|
||||
routes = []
|
||||
for rule in app.url_map.iter_rules():
|
||||
routes.append({
|
||||
'endpoint': rule.endpoint,
|
||||
'methods': ', '.join(sorted(rule.methods - {'HEAD', 'OPTIONS'})),
|
||||
'path': str(rule)
|
||||
})
|
||||
|
||||
# Sort by path
|
||||
routes.sort(key=lambda x: x['path'])
|
||||
|
||||
# Find all login-related routes
|
||||
login_routes = [r for r in routes if 'login' in r['path'].lower() or 'login' in r['endpoint'].lower()]
|
||||
user_routes = [r for r in routes if 'user' in r['path'].lower()]
|
||||
|
||||
print("\n📝 Login & Authentication Routes:")
|
||||
for route in login_routes:
|
||||
print(f" {route['path']:40} [{route['methods']:15}] -> {route['endpoint']}")
|
||||
|
||||
print("\n👥 User Management Routes:")
|
||||
for route in user_routes:
|
||||
print(f" {route['path']:40} [{route['methods']:15}] -> {route['endpoint']}")
|
||||
|
||||
print("\n📊 Total routes registered:", len(routes))
|
||||
print()
|
||||
print("=" * 60)
|
||||
print("✓ All routes loaded successfully!")
|
||||
print("=" * 60)
|
||||
print()
|
||||
print("To start the server:")
|
||||
print(" python app.py")
|
||||
print()
|
||||
print("Then access:")
|
||||
print(" http://localhost:8080/login")
|
||||
print(" http://localhost:8080/users")
|
||||
print()
|
||||
|
||||
except ImportError as e:
|
||||
print("=" * 60)
|
||||
print("❌ IMPORT ERROR")
|
||||
print("=" * 60)
|
||||
print()
|
||||
print(f"Failed to import Flask app: {e}")
|
||||
print()
|
||||
print("This might be because:")
|
||||
print(" 1. Flask is not installed: pip install Flask")
|
||||
print(" 2. Werkzeug is not installed: pip install Werkzeug")
|
||||
print(" 3. There's a syntax error in app.py")
|
||||
print()
|
||||
print("Try running:")
|
||||
print(" pip install -r requirements.txt")
|
||||
print()
|
||||
|
||||
except Exception as e:
|
||||
print("=" * 60)
|
||||
print("❌ ERROR")
|
||||
print("=" * 60)
|
||||
print()
|
||||
print(f"Error: {e}")
|
||||
print()
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
print()
|
||||
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Server troubleshooting script for 403/404 errors
|
||||
Run this on the server to diagnose Apache/Passenger issues
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
print("=" * 70)
|
||||
print("TROUBLESHOOTING 403/404 ERRORS")
|
||||
print("=" * 70)
|
||||
print()
|
||||
|
||||
# Check current directory
|
||||
current_dir = os.getcwd()
|
||||
print(f"📁 Current directory: {current_dir}")
|
||||
print()
|
||||
|
||||
# Check file permissions
|
||||
print("📌 File Permissions Check:")
|
||||
files_to_check = [
|
||||
'passenger_wsgi.py',
|
||||
'.htaccess',
|
||||
'app.py',
|
||||
'data/users.json',
|
||||
'templates/login.html'
|
||||
]
|
||||
|
||||
for file in files_to_check:
|
||||
if os.path.exists(file):
|
||||
stat_info = os.stat(file)
|
||||
perms = oct(stat_info.st_mode)[-3:]
|
||||
print(f" ✓ {file:<30} Permissions: {perms}")
|
||||
else:
|
||||
print(f" ✗ {file:<30} NOT FOUND")
|
||||
print()
|
||||
|
||||
# Check directory permissions
|
||||
print("📌 Directory Permissions:")
|
||||
dirs_to_check = ['.', 'templates', 'data', 'static', 'tmp']
|
||||
for dir_path in dirs_to_check:
|
||||
if os.path.exists(dir_path):
|
||||
stat_info = os.stat(dir_path)
|
||||
perms = oct(stat_info.st_mode)[-3:]
|
||||
print(f" ✓ {dir_path:<30} Permissions: {perms}")
|
||||
else:
|
||||
print(f" ⚠️ {dir_path:<30} NOT FOUND (might be OK)")
|
||||
print()
|
||||
|
||||
# Check for tmp/restart.txt
|
||||
print("📌 Passenger Restart Check:")
|
||||
if os.path.exists('tmp/restart.txt'):
|
||||
from datetime import datetime
|
||||
mtime = os.path.getmtime('tmp/restart.txt')
|
||||
mtime_str = datetime.fromtimestamp(mtime).strftime('%Y-%m-%d %H:%M:%S')
|
||||
print(f" ✓ tmp/restart.txt exists (last modified: {mtime_str})")
|
||||
else:
|
||||
print(f" ✗ tmp/restart.txt NOT FOUND")
|
||||
print(f" Create it with: mkdir -p tmp && touch tmp/restart.txt")
|
||||
print()
|
||||
|
||||
# Check environment variables
|
||||
print("📌 Environment Variables:")
|
||||
print(f" APPLICATION_ROOT: {os.environ.get('APPLICATION_ROOT', 'NOT SET')}")
|
||||
print(f" HOME: {os.environ.get('HOME', 'NOT SET')}")
|
||||
print(f" USER: {os.environ.get('USER', 'NOT SET')}")
|
||||
print()
|
||||
|
||||
# Check .htaccess content
|
||||
print("📌 .htaccess Configuration:")
|
||||
if os.path.exists('.htaccess'):
|
||||
print(f" ✓ .htaccess exists")
|
||||
with open('.htaccess', 'r') as f:
|
||||
content = f.read()
|
||||
if 'PassengerEnabled' in content:
|
||||
print(f" ✓ PassengerEnabled directive found")
|
||||
else:
|
||||
print(f" ✗ PassengerEnabled directive NOT FOUND")
|
||||
|
||||
if 'PassengerAppRoot' in content:
|
||||
print(f" ✓ PassengerAppRoot directive found")
|
||||
# Extract the path
|
||||
for line in content.split('\n'):
|
||||
if 'PassengerAppRoot' in line:
|
||||
print(f" Value: {line.strip()}")
|
||||
else:
|
||||
print(f" ✗ PassengerAppRoot NOT FOUND")
|
||||
|
||||
if 'PassengerPython' in content:
|
||||
print(f" ✓ PassengerPython directive found")
|
||||
for line in content.split('\n'):
|
||||
if 'PassengerPython' in line:
|
||||
print(f" Value: {line.strip()}")
|
||||
python_path = line.split()[-1] if len(line.split()) > 1 else ''
|
||||
if os.path.exists(python_path):
|
||||
print(f" ✓ Python executable exists")
|
||||
else:
|
||||
print(f" ✗ Python executable NOT FOUND at {python_path}")
|
||||
else:
|
||||
print(f" ✗ .htaccess NOT FOUND")
|
||||
print()
|
||||
|
||||
# Try importing Flask
|
||||
print("📌 Flask Import Test:")
|
||||
try:
|
||||
sys.path.insert(0, current_dir)
|
||||
from app import app
|
||||
print(f" ✓ Flask app imported successfully")
|
||||
print(f" Routes registered: {len(list(app.url_map.iter_rules()))}")
|
||||
except Exception as e:
|
||||
print(f" ✗ Error importing app: {e}")
|
||||
print()
|
||||
|
||||
# Recommendations
|
||||
print("=" * 70)
|
||||
print("COMMON FIXES FOR 403/404 ERRORS:")
|
||||
print("=" * 70)
|
||||
print()
|
||||
print("1️⃣ RESTART PASSENGER:")
|
||||
print(" mkdir -p tmp")
|
||||
print(" touch tmp/restart.txt")
|
||||
print()
|
||||
print("2️⃣ CHECK FILE PERMISSIONS:")
|
||||
print(" chmod 644 passenger_wsgi.py")
|
||||
print(" chmod 644 .htaccess")
|
||||
print(" chmod 755 .")
|
||||
print()
|
||||
print("3️⃣ VERIFY PATHS IN .htaccess:")
|
||||
print(" PassengerAppRoot should point to: " + current_dir)
|
||||
print()
|
||||
print("4️⃣ CHECK APACHE ERROR LOG:")
|
||||
print(" tail -50 ~/logs/error_log")
|
||||
print(" (Look for Passenger errors)")
|
||||
print()
|
||||
print("5️⃣ VERIFY .htaccess IS BEING READ:")
|
||||
print(" If AllowOverride is not set in Apache config, .htaccess is ignored")
|
||||
print()
|
||||
print("6️⃣ SIMPLER .htaccess (if still failing):")
|
||||
print(" Try creating a minimal .htaccess with just:")
|
||||
print(" PassengerEnabled on")
|
||||
print()
|
||||
print("=" * 70)
|
||||
@@ -0,0 +1,8 @@
|
||||
"""
|
||||
WSGI entry point for production deployment
|
||||
Use with Gunicorn: gunicorn wsgi:app
|
||||
"""
|
||||
from app import app
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
||||
Reference in New Issue
Block a user