Initial
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
# Flask Configuration
|
||||
FLASK_APP=app.py
|
||||
FLASK_ENV=development
|
||||
SECRET_KEY="3pgys!#g$wA7V#%sS6GiDKEA!gwHjGR2"
|
||||
|
||||
# Server Configuration
|
||||
HOST=0.0.0.0
|
||||
PORT=8080
|
||||
|
||||
# Database (if needed in future)
|
||||
# DATABASE_URL=sqlite:///products.db
|
||||
|
||||
|
||||
# source /home/bmdwtjuw/virtualenv/product-finder/3.6/bin/activate && cd /home/bmdwtjuw/product-finder
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# Flask
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# IDEs
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.DS_Store
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Database
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# Testing
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
||||
# Production
|
||||
*.pid
|
||||
*.sock
|
||||
@@ -0,0 +1,93 @@
|
||||
# BEGIN Force HTTPS
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
|
||||
# Skip ACME challenge
|
||||
RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge/
|
||||
|
||||
# Redirect HTTP to HTTPS
|
||||
RewriteCond %{HTTPS} off
|
||||
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
|
||||
</IfModule>
|
||||
# END Force HTTPS
|
||||
|
||||
# BEGIN LSCACHE
|
||||
## LITESPEED WP CACHE PLUGIN - Do not edit the contents of this block! ##
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine on
|
||||
RewriteRule litespeed/debug/.*\.log$ - [F,L]
|
||||
RewriteRule \.litespeed_conf\.dat - [F,L]
|
||||
</IfModule>
|
||||
<IfModule LiteSpeed>
|
||||
CacheLookup on
|
||||
RewriteRule .* - [E=Cache-Control:no-autoflush]
|
||||
|
||||
### marker ASYNC start ###
|
||||
RewriteCond %{REQUEST_URI} /wp-admin/admin-ajax\.php
|
||||
RewriteCond %{QUERY_STRING} action=async_litespeed
|
||||
RewriteRule .* - [E=noabort:1]
|
||||
### marker ASYNC end ###
|
||||
|
||||
### marker DROPQS start ###
|
||||
CacheKeyModify -qs:fbclid
|
||||
CacheKeyModify -qs:gclid
|
||||
CacheKeyModify -qs:utm*
|
||||
CacheKeyModify -qs:_ga
|
||||
### marker DROPQS end ###
|
||||
|
||||
</IfModule>
|
||||
## LITESPEED WP CACHE PLUGIN - Do not edit the contents of this block! ##
|
||||
# END LSCACHE
|
||||
# BEGIN NON_LSCACHE
|
||||
## LITESPEED WP CACHE PLUGIN - Do not edit the contents of this block! ##
|
||||
## LITESPEED WP CACHE PLUGIN - Do not edit the contents of this block! ##
|
||||
# END NON_LSCACHE
|
||||
|
||||
|
||||
# BEGIN Security Block
|
||||
# Block the include-only files.
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
RewriteBase /
|
||||
RewriteRule ^wp-admin/includes/ - [F,L]
|
||||
RewriteRule !^wp-includes/ - [S=3]
|
||||
RewriteRule ^wp-includes/[^/]+\.php$ - [F,L]
|
||||
RewriteRule ^wp-includes/js/tinymce/langs/.+\.php - [F,L]
|
||||
RewriteRule ^wp-includes/theme-compat/ - [F,L]
|
||||
</IfModule>
|
||||
|
||||
# Disable directory listing
|
||||
Options All -Indexes
|
||||
|
||||
# Remove header with PHP version
|
||||
Header always unset X-Powered-By
|
||||
Header unset X-Powered-By
|
||||
# END Security Block
|
||||
|
||||
# BEGIN LiteSpeed
|
||||
# The directives (lines) between "BEGIN LiteSpeed" and "END LiteSpeed" are
|
||||
# dynamically generated, and should only be modified via WordPress filters.
|
||||
# Any changes to the directives between these markers will be overwritten.
|
||||
<IfModule Litespeed>
|
||||
SetEnv noabort 1
|
||||
</IfModule>
|
||||
# END LiteSpeed
|
||||
# BEGIN WordPress
|
||||
# The directives (lines) between "BEGIN WordPress" and "END WordPress" are
|
||||
# dynamically generated, and should only be modified via WordPress filters.
|
||||
# Any changes to the directives between these markers will be overwritten.
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
|
||||
RewriteBase /
|
||||
RewriteRule ^index\.php$ - [L]
|
||||
|
||||
# *** IMPORTANT: Exclude product-finder from WordPress routing ***
|
||||
RewriteCond %{REQUEST_URI} !^/product-finder/
|
||||
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule . /index.php [L]
|
||||
</IfModule>
|
||||
|
||||
# END WordPress
|
||||
@@ -0,0 +1,243 @@
|
||||
# Dynamic Image Generation - Quick Start
|
||||
|
||||
## What Was Created
|
||||
|
||||
I've built a complete server-side image generation system that can dynamically create product images with different colors and configurations. Here's what's included:
|
||||
|
||||
### Files Created:
|
||||
1. **`app/image_generator.py`** - Core image processing logic using PIL/Pillow
|
||||
2. **`app/data/image_configs.json`** - Configuration for COBRAI product with colorable regions
|
||||
3. **`app/templates/image_test.html`** - Test page to demonstrate the API
|
||||
4. **`information/DYNAMIC_IMAGE_API.md`** - Complete documentation
|
||||
|
||||
### Files Modified:
|
||||
1. **`app/app.py`** - Added 4 new API endpoints
|
||||
2. **`app/requirements.txt`** - Added Pillow dependency
|
||||
|
||||
## ⚠️ Important: Image File
|
||||
|
||||
You need to manually save the storm door image as:
|
||||
```
|
||||
app/images/cobrai.jpg
|
||||
```
|
||||
|
||||
I created a placeholder file, but you need to replace it with the actual image from your screenshot (the white storm door with two screen panels).
|
||||
|
||||
## Setup & Testing
|
||||
|
||||
### 1. Install Dependencies
|
||||
```bash
|
||||
cd app
|
||||
pip install Pillow
|
||||
```
|
||||
|
||||
### 2. Add the Image
|
||||
Save the storm door image as `app/images/cobrai.jpg`
|
||||
|
||||
### 3. Start the Server
|
||||
```bash
|
||||
python app.py
|
||||
```
|
||||
|
||||
### 4. Test the API
|
||||
|
||||
#### Option A: Visit Test Page
|
||||
Open browser to:
|
||||
```
|
||||
http://localhost:8080/image-test
|
||||
```
|
||||
|
||||
This interactive page lets you:
|
||||
- Select colors (White, Black, Bronze, Sandstone)
|
||||
- Choose hinge side (Left/Right)
|
||||
- See both image rendering methods
|
||||
- View the product configuration
|
||||
- Clear the cache
|
||||
|
||||
#### Option B: Direct URL Test
|
||||
Visit these URLs in your browser:
|
||||
```
|
||||
http://localhost:8080/api/product-image/cobrai?color=black&hinge=right
|
||||
http://localhost:8080/api/product-image/cobrai?color=bronze&hinge=left
|
||||
http://localhost:8080/api/product-config/cobrai
|
||||
```
|
||||
|
||||
#### Option C: Using cURL
|
||||
```bash
|
||||
# Get black door image
|
||||
curl http://localhost:8080/api/product-image/cobrai?color=black > test_black.png
|
||||
|
||||
# Get bronze left-hinge door
|
||||
curl http://localhost:8080/api/product-image/cobrai?color=bronze&hinge=left > test_bronze_left.png
|
||||
|
||||
# Get configuration
|
||||
curl http://localhost:8080/api/product-config/cobrai
|
||||
|
||||
# Get as JSON with base64
|
||||
curl http://localhost:8080/api/product-image/cobrai?color=white&format=json
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
### The Configuration
|
||||
The `image_configs.json` file defines:
|
||||
|
||||
1. **Colorable Regions** - Rectangular areas to recolor:
|
||||
- Left frame: [0, 0] to [100, 1450]
|
||||
- Right frame: [620, 0] to [720, 1450]
|
||||
- Top rail: [0, 0] to [720, 80]
|
||||
- Middle rail: [0, 590] to [720, 680]
|
||||
- Bottom panel: [100, 1090] to [620, 1450]
|
||||
|
||||
2. **Available Colors**:
|
||||
- White: RGB [255, 255, 255]
|
||||
- Black: RGB [30, 30, 30]
|
||||
- Bronze: RGB [110, 80, 50]
|
||||
- Sandstone: RGB [210, 190, 165]
|
||||
|
||||
3. **Hardware Positions** - Where handles go (placeholder for future):
|
||||
- Right hinge: position [580, 730]
|
||||
- Left hinge: position [140, 730] (flipped)
|
||||
|
||||
### The Process
|
||||
1. Load white storm door image
|
||||
2. For each colorable region:
|
||||
- Calculate pixel brightness
|
||||
- Apply target color while preserving brightness
|
||||
- This maintains shadows/highlights
|
||||
3. Optionally flip for left hinge
|
||||
4. Cache the result for fast subsequent requests
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### 1. Generate Image
|
||||
```
|
||||
GET /api/product-image/<product_code>?color=<color>&hinge=<hinge>
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `color`: white, black, bronze, sandstone
|
||||
- `hinge`: left, right
|
||||
- `format`: image (default) or json
|
||||
- `cache`: true (default) or false
|
||||
|
||||
**Returns:** PNG image or JSON with base64
|
||||
|
||||
### 2. Get Configuration
|
||||
```
|
||||
GET /api/product-config/<product_code>
|
||||
```
|
||||
|
||||
**Returns:** JSON with all product config (colors, regions, etc.)
|
||||
|
||||
### 3. Clear Cache
|
||||
```
|
||||
POST /api/clear-image-cache
|
||||
```
|
||||
|
||||
**Body (optional):**
|
||||
```json
|
||||
{"productCode": "cobrai"}
|
||||
```
|
||||
|
||||
## Adjusting the Configuration
|
||||
|
||||
### If you need to change color regions:
|
||||
|
||||
1. Open the image in an image editor (Paint, Photoshop, etc.)
|
||||
2. Note the pixel coordinates of the area you want to color
|
||||
3. Edit `app/data/image_configs.json`:
|
||||
```json
|
||||
{
|
||||
"name": "frame",
|
||||
"topLeft": [x1, y1],
|
||||
"bottomRight": [x2, y2]
|
||||
}
|
||||
```
|
||||
|
||||
### If you need to adjust colors:
|
||||
|
||||
Edit the RGB values in `availableColors`:
|
||||
```json
|
||||
"bronze": {
|
||||
"rgb": [110, 80, 50], // Adjust these numbers
|
||||
"name": "Bronze"
|
||||
}
|
||||
```
|
||||
|
||||
### If hardware images exist:
|
||||
|
||||
Place hardware PNGs in `app/images/hardware/` and update config:
|
||||
```json
|
||||
"hardwarePositions": {
|
||||
"handle_right": {
|
||||
"x": 580,
|
||||
"y": 730,
|
||||
"image": "images/hardware/handle-lever.png"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
### To integrate into your product finder:
|
||||
|
||||
Replace the layered image system with dynamic generation:
|
||||
|
||||
```javascript
|
||||
// Instead of static layers
|
||||
imageDisplayHtml = `<img src="/api/product-image/${prodCode}?color=${color}&hinge=${hinge}">`;
|
||||
|
||||
// Or update existing image on change
|
||||
function updateProductPreview(productCode) {
|
||||
const color = document.getElementById('config-color').value;
|
||||
const hinge = document.querySelector('[name="hinge-location"]:checked').value;
|
||||
|
||||
document.getElementById('product-image').src =
|
||||
`/api/product-image/${productCode}?color=${color}&hinge=${hinge}`;
|
||||
}
|
||||
```
|
||||
|
||||
### To add more products:
|
||||
|
||||
1. Take photo of product in white/neutral color
|
||||
2. Save as `app/images/productcode.jpg`
|
||||
3. Add configuration to `image_configs.json`
|
||||
4. Define colorable regions using image editor coordinates
|
||||
5. Test at `/image-test`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**"PIL/Pillow not installed"**
|
||||
```bash
|
||||
pip install Pillow
|
||||
```
|
||||
|
||||
**"Product cobrai not found"**
|
||||
- Check that image exists at `app/images/cobrai.jpg`
|
||||
- Check that config exists in `image_configs.json`
|
||||
|
||||
**Colors look weird**
|
||||
- Adjust RGB values in config
|
||||
- Ensure source image is white or neutral
|
||||
- Check that region coordinates are correct
|
||||
|
||||
**Image not changing**
|
||||
- Try clearing cache
|
||||
- Set `cache=false` in URL parameter
|
||||
- Check browser console for errors
|
||||
|
||||
## Performance
|
||||
|
||||
- **First request**: ~100-300ms (generates and caches)
|
||||
- **Cached requests**: ~10-50ms (serves from cache)
|
||||
- **Cache location**: `app/cache/product_images/`
|
||||
|
||||
## Full Documentation
|
||||
|
||||
See `information/DYNAMIC_IMAGE_API.md` for complete documentation including:
|
||||
- Advanced configuration options
|
||||
- Production deployment tips
|
||||
- Frontend integration examples
|
||||
- Adding glass tinting, textures, etc.
|
||||
- Batch generation scripts
|
||||
@@ -0,0 +1,51 @@
|
||||
# Product Finder Application
|
||||
|
||||
A Flask-based web application for finding products through an interactive quiz interface.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 🚀 Running the Application
|
||||
|
||||
**Option 1: Using VS Code Tasks (Recommended)**
|
||||
- Press `Ctrl+Shift+P` → Type "Tasks: Run Task" → Select "Start Flask Server"
|
||||
- Open http://127.0.0.1:8080
|
||||
|
||||
**Option 2: Command Line**
|
||||
```bash
|
||||
cd app
|
||||
python app.py
|
||||
```
|
||||
|
||||
### 📊 Updating Product Data
|
||||
|
||||
When you update `app/data/products.csv`:
|
||||
- Press `Ctrl+Shift+B` to process the data
|
||||
- All JSON files will be regenerated automatically
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
📁 app/ - All active project files (application code, assets, data)
|
||||
📁 information/ - Documentation and deprecated files
|
||||
```
|
||||
|
||||
See [information/FOLDER_STRUCTURE.md](information/FOLDER_STRUCTURE.md) for detailed organization.
|
||||
|
||||
## Documentation
|
||||
|
||||
- **[FOLDER_STRUCTURE.md](information/FOLDER_STRUCTURE.md)** - Complete workspace organization
|
||||
- **[QUICKSTART.md](information/QUICKSTART.md)** - Getting started guide
|
||||
- **[BITWISE_USAGE_GUIDE.md](information/BITWISE_USAGE_GUIDE.md)** - Bitwise filtering documentation
|
||||
- **[START_HERE.md](information/START_HERE.md)** - Project overview
|
||||
|
||||
## Development
|
||||
|
||||
All development happens in the `app/` folder. The project uses:
|
||||
- **Flask** for the web server
|
||||
- **Python** for data processing
|
||||
- **Vanilla JavaScript** for the frontend
|
||||
|
||||
---
|
||||
|
||||
**Version:** 2.0
|
||||
**Last Updated:** March 26, 2026
|
||||
@@ -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()
|
||||
@@ -0,0 +1,42 @@
|
||||
# Simple Flask Test App
|
||||
|
||||
A minimal 2-page Flask application to test Passenger WSGI deployment.
|
||||
|
||||
## Pages
|
||||
- `/` - Home page (Hello World)
|
||||
- `/about` - About page
|
||||
- `/test` - Test page (shows system info)
|
||||
|
||||
## Local Testing
|
||||
```bash
|
||||
cd app_simple
|
||||
python app.py
|
||||
```
|
||||
Visit: http://localhost:8080/
|
||||
|
||||
## Server Deployment
|
||||
|
||||
### Upload these files to: `/home/bmdwtjuw/product-finder/`
|
||||
- `app.py`
|
||||
- `wsgi.py`
|
||||
- `templates/home.html`
|
||||
- `templates/about.html`
|
||||
|
||||
### Control Panel Configuration
|
||||
- **Application startup file:** `wsgi.py`
|
||||
- **Application Entry point:** `app`
|
||||
- **Python version:** 3.13.11
|
||||
|
||||
### Test URLs (after deployment)
|
||||
- https://columbiawindows.com/product-finder/
|
||||
- https://columbiawindows.com/product-finder/about
|
||||
- https://columbiawindows.com/product-finder/test
|
||||
|
||||
## What This Tests
|
||||
✓ Python 3.13.11 compatibility
|
||||
✓ Flask framework loading
|
||||
✓ Template rendering
|
||||
✓ URL routing
|
||||
✓ Passenger WSGI configuration
|
||||
|
||||
Once this works, you can add back the login/user management features from `app2` folder.
|
||||
@@ -0,0 +1,37 @@
|
||||
from flask import Flask, render_template
|
||||
|
||||
# Initialize Flask
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/')
|
||||
def home():
|
||||
"""Home page - Hello World"""
|
||||
return render_template('home.html')
|
||||
|
||||
@app.route('/about')
|
||||
def about():
|
||||
"""About page"""
|
||||
return render_template('about.html')
|
||||
|
||||
@app.route('/test')
|
||||
def test():
|
||||
"""Test route - no template needed"""
|
||||
import sys
|
||||
return f"""
|
||||
<html>
|
||||
<head><title>Test Page</title></head>
|
||||
<body style="font-family: Arial; padding: 20px;">
|
||||
<h1>Flask App is Running!</h1>
|
||||
<ul>
|
||||
<li><strong>Python:</strong> {sys.version}</li>
|
||||
<li><strong>Flask:</strong> Working</li>
|
||||
<li><strong>Routes:</strong> {len(list(app.url_map.iter_rules()))}</li>
|
||||
</ul>
|
||||
<p><a href="/">Home</a> | <a href="/about">About</a></p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Run locally on port 8080
|
||||
app.run(debug=True, host='0.0.0.0', port=8080)
|
||||
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Deployment Check Script
|
||||
Run this on the server to verify configuration
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
print("=" * 70)
|
||||
print("DEPLOYMENT CONFIGURATION CHECK")
|
||||
print("=" * 70)
|
||||
|
||||
# Check Python version
|
||||
print(f"\nPython Version: {sys.version}")
|
||||
print(f"Python Executable: {sys.executable}")
|
||||
|
||||
# Check current directory
|
||||
print(f"\nCurrent Directory: {os.getcwd()}")
|
||||
|
||||
# Check for required files
|
||||
print("\n" + "=" * 70)
|
||||
print("CHECKING FILES")
|
||||
print("=" * 70)
|
||||
|
||||
files_to_check = ['app.py', 'wsgi.py', 'templates/home.html', 'templates/about.html']
|
||||
for file in files_to_check:
|
||||
exists = "✓ EXISTS" if os.path.exists(file) else "✗ MISSING"
|
||||
print(f"{exists}: {file}")
|
||||
|
||||
# Check for passenger_wsgi.py (should NOT exist)
|
||||
if os.path.exists('passenger_wsgi.py'):
|
||||
print("✗ WARNING: passenger_wsgi.py exists - DELETE THIS FILE!")
|
||||
else:
|
||||
print("✓ GOOD: passenger_wsgi.py does not exist")
|
||||
|
||||
# Try importing Flask
|
||||
print("\n" + "=" * 70)
|
||||
print("CHECKING FLASK")
|
||||
print("=" * 70)
|
||||
try:
|
||||
import flask
|
||||
print(f"✓ Flask version: {flask.__version__}")
|
||||
except ImportError as e:
|
||||
print(f"✗ Flask not installed: {e}")
|
||||
|
||||
# Try importing the app
|
||||
print("\n" + "=" * 70)
|
||||
print("CHECKING APP IMPORT")
|
||||
print("=" * 70)
|
||||
try:
|
||||
from app import app
|
||||
print("✓ App imported successfully")
|
||||
print(f"✓ App name: {app.name}")
|
||||
print(f"✓ Routes count: {len(list(app.url_map.iter_rules()))}")
|
||||
except Exception as e:
|
||||
print(f"✗ Error importing app: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print("DONE")
|
||||
print("=" * 70)
|
||||
@@ -0,0 +1,2 @@
|
||||
Flask==3.0.0
|
||||
Werkzeug==3.0.1
|
||||
@@ -0,0 +1,62 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>About - Simple Flask App</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 50px auto;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
color: white;
|
||||
}
|
||||
.container {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 40px;
|
||||
border-radius: 10px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
h1 {
|
||||
margin: 0 0 20px 0;
|
||||
font-size: 2.5em;
|
||||
}
|
||||
a {
|
||||
color: #ffd700;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.nav {
|
||||
margin-top: 30px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>About This App</h1>
|
||||
<p>This is a simple 2-page Flask application created to test server deployment configuration.</p>
|
||||
<p><strong>Purpose:</strong> Verify that Passenger WSGI configuration works correctly.</p>
|
||||
|
||||
<h2>Configuration:</h2>
|
||||
<ul>
|
||||
<li><strong>Startup file:</strong> wsgi.py</li>
|
||||
<li><strong>Entry point:</strong> app</li>
|
||||
<li><strong>Framework:</strong> Flask 3.0.0</li>
|
||||
<li><strong>Python:</strong> 3.13.11</li>
|
||||
</ul>
|
||||
|
||||
<div class="nav">
|
||||
<a href="/">Home</a> |
|
||||
<a href="/about">About</a> |
|
||||
<a href="/test">Test</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,54 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Home - Simple Flask App</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 50px auto;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
.container {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 40px;
|
||||
border-radius: 10px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
h1 {
|
||||
margin: 0 0 20px 0;
|
||||
font-size: 2.5em;
|
||||
}
|
||||
a {
|
||||
color: #ffd700;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.nav {
|
||||
margin-top: 30px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Hello World!</h1>
|
||||
<p>Welcome to the simple Flask application.</p>
|
||||
<p>This is a minimal test to verify the server configuration works correctly.</p>
|
||||
|
||||
<div class="nav">
|
||||
<a href="/">Home</a> |
|
||||
<a href="/about">About</a> |
|
||||
<a href="/test">Test</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,4 @@
|
||||
from app import app
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
||||
@@ -0,0 +1,723 @@
|
||||
# AI Data Parsing Instructions
|
||||
|
||||
## 📋 Quick Reference
|
||||
|
||||
**Input**: `data/products.csv` (1000 rows with product and accessory data)
|
||||
|
||||
**Outputs**:
|
||||
1. `data/navigation.json` - Smart navigation flow with conditional questions
|
||||
2. `data/products.json` - Product catalog with materials, colors, and accessory links
|
||||
3. `data/accessories.json` - Accessory options with compatibility rules
|
||||
|
||||
**Key Features**:
|
||||
- ✅ Conditional material questions (only show if multiple materials exist in sub-type)
|
||||
- ✅ Conditional color questions (only show if multiple colors available)
|
||||
- ✅ Auto-skip questions when only one option exists
|
||||
- ✅ Smart product-accessory linking by sub-type, category, or specific product code
|
||||
- 🔄 Multi-group assignment using bit values (optional enhancement)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
Parse the `data/products.csv` file to generate three separate JSON files that structure product data, navigation, and accessory options for a doors and windows e-commerce application.
|
||||
|
||||
## Source File Structure
|
||||
|
||||
### CSV File: `data/products.csv`
|
||||
The CSV contains product and accessory data with the following column structure:
|
||||
|
||||
**Row 1**: Category headers (informational)
|
||||
**Row 2**: Column names (actual headers)
|
||||
|
||||
### Column Mapping (0-indexed):
|
||||
- **A (0)**: DISCONT - Discontinued flag (TRUE/FALSE)
|
||||
- **B (1)**: LOC_CODE - Location code (e.g., "Iola")
|
||||
- **C (2)**: PROD_CODE - Product code (unique identifier)
|
||||
- **D (3)**: CATEGORY - Category code
|
||||
- **E (4)**: CATEGORY_NEW - New category (usually empty)
|
||||
- **F (5)**: DESCRIPTION - Full product description
|
||||
- **G (6)**: Base Type - Primary type ("Window", "Door", or empty)
|
||||
- **H (7)**: Sub-type Door - Door subcategory (e.g., "Storm Door")
|
||||
- **I (8)**: Sub-type Window - Window subcategory (e.g., "Storm Window")
|
||||
- **J (9)**: Accessory Yes - Boolean indicating if item is an accessory
|
||||
- **K (10)**: This Item - Boolean for specific item relationship
|
||||
- **L+ (11+)**: Material and Color availability (Aluminum, Vinyl, Black, White, Bronze, Tan, Mill, Sandstone, etc.)
|
||||
|
||||
---
|
||||
|
||||
## Output File 1: `data/navigation.json`
|
||||
|
||||
### Purpose
|
||||
Generate a navigation flow with questions and button options to guide users through product selection.
|
||||
|
||||
### ⚡ Key Navigation Principles
|
||||
|
||||
1. **Question 1 (Start)**: Built from Column G (Base Type) - Door, Window, etc.
|
||||
2. **Question 2 (Sub-Type)**: Built from Column H (Doors) OR Column I (Windows) based on Q1 selection
|
||||
3. **Question 3 (Material)**: **CONDITIONAL** - Only show if sub-type has products with multiple materials (Aluminum AND Vinyl)
|
||||
- Example: Storm Doors (all Aluminum) → SKIP this question
|
||||
- Example: Mixed Windows (some Aluminum, some Vinyl) → SHOW this question
|
||||
4. **Question 4 (Color)**: **CONDITIONAL** - Only show if filtered products have multiple color options
|
||||
5. **Question 5 (Dimensions)**: Always show - final step before product display
|
||||
6. **Dynamic Linking**: The "next" property must skip questions that aren't needed for that path
|
||||
|
||||
### Structure
|
||||
```json
|
||||
{
|
||||
"start": {
|
||||
"type": "question",
|
||||
"inputType": "button",
|
||||
"title": "What are you looking for?",
|
||||
"subtitle": "Select the product category",
|
||||
"answers": [
|
||||
{
|
||||
"caption": "PRODUCT_TYPE",
|
||||
"image": "EMOJI",
|
||||
"next": "NEXT_QUESTION_ID",
|
||||
"filter": {
|
||||
"baseType": "VALUE"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"q-QUESTION_ID": {
|
||||
"type": "question",
|
||||
"inputType": "button|form",
|
||||
"title": "Question Title",
|
||||
"subtitle": "Question subtitle",
|
||||
"answers": [...],
|
||||
"fields": [...]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Generation Rules
|
||||
|
||||
#### Question 1: Base Type Selection (Start)
|
||||
**Source**: Column G (Base Type)
|
||||
1. Extract all unique values from column G where DISCONT=FALSE
|
||||
2. Create button option for each unique base type (e.g., "Window", "Door")
|
||||
3. Assign appropriate emojis (🪟 for windows, 🚪 for doors)
|
||||
4. Each answer links to the corresponding sub-type question
|
||||
|
||||
**Example**: "What are you looking for?" → Window / Door options
|
||||
|
||||
#### Question 2: Sub-Type Selection
|
||||
**Source**: Column H (for Doors) OR Column I (for Windows)
|
||||
1. **For Door selection**: Use column H (Sub-type Door)
|
||||
- Extract unique values where column G = "Door" and DISCONT=FALSE
|
||||
- Examples: "Storm Door", "Patio Door", etc.
|
||||
|
||||
2. **For Window selection**: Use column I (Sub-type Window)
|
||||
- Extract unique values where column G = "Window" and DISCONT=FALSE
|
||||
- Examples: "Storm Window", "Casement", etc.
|
||||
|
||||
3. Create separate question branches:
|
||||
- `q-door-type`: For door sub-types
|
||||
- `q-window-type`: For window sub-types
|
||||
|
||||
4. Each answer links to either material question (if needed) or dimensions question
|
||||
|
||||
**Example**: "What type of door?" → Storm Door / Patio Door options
|
||||
|
||||
#### Question 3: Material Selection (CONDITIONAL)
|
||||
**Source**: Columns L (Aluminum) and M (Vinyl)
|
||||
**Important**: This question should **only appear** if products in the selected sub-type have multiple material options.
|
||||
|
||||
**Logic**:
|
||||
1. For each sub-type group, count materials:
|
||||
- Count products where Aluminum (column L) = TRUE
|
||||
- Count products where Vinyl (column M) = TRUE
|
||||
|
||||
2. **Show material question** if:
|
||||
- Some products have Aluminum=TRUE AND some have Vinyl=TRUE
|
||||
- OR any single product has BOTH Aluminum=TRUE AND Vinyl=TRUE
|
||||
|
||||
3. **Skip material question** if:
|
||||
- ALL products in the sub-type have only one material type
|
||||
- Example: All "Storm Door" products only have Aluminum=TRUE → Skip material question
|
||||
|
||||
4. If shown, create radio buttons or select dropdown with:
|
||||
- Only materials that exist in the sub-type group
|
||||
- Link to color question or dimensions
|
||||
|
||||
**Example Skip Case**: Storm Doors (all Aluminum only) → Skip directly to dimensions
|
||||
**Example Show Case**: Windows (some Aluminum, some Vinyl) → Show material selection
|
||||
|
||||
#### Question 4: Color Selection (CONDITIONAL)
|
||||
**Source**: Color columns (N through S+)
|
||||
Similar conditional logic as materials:
|
||||
1. Only show if products in the filtered group have multiple color options
|
||||
2. Present only colors available for the selected material (if material was selected)
|
||||
3. Skip if all products have the same single color
|
||||
|
||||
#### Question 5: Dimensions Entry (Always Show)
|
||||
**Source**: User input
|
||||
1. Width input field (number, required)
|
||||
2. Height input field (number, required)
|
||||
3. This is typically the final question before showing filtered products
|
||||
|
||||
#### Filter Integration
|
||||
Each answer should accumulate filter criteria:
|
||||
```json
|
||||
"filter": {
|
||||
"baseType": "Window|Door",
|
||||
"subType": "Storm Door|etc",
|
||||
"material": "Aluminum|Vinyl", // Only if material question was shown
|
||||
"color": "White|Bronze|etc", // Only if color question was shown
|
||||
"category": "CATEGORY_CODE"
|
||||
}
|
||||
```
|
||||
|
||||
#### Navigation Flow Schema
|
||||
```
|
||||
start (Column G)
|
||||
→ q-door-type (Column H) OR q-window-type (Column I)
|
||||
→ q-material (Columns L,M) [CONDITIONAL - only if multiple materials exist]
|
||||
→ q-color (Columns N+) [CONDITIONAL - only if multiple colors exist]
|
||||
→ q-dimensions (User Input)
|
||||
→ results (Filtered products)
|
||||
```
|
||||
|
||||
**Dynamic Linking**: The "next" value for each question must be calculated based on whether the next conditional question is needed:
|
||||
- If material question not needed → link sub-type directly to color or dimensions
|
||||
- If color question not needed → link material (or sub-type) directly to dimensions
|
||||
|
||||
---
|
||||
|
||||
## Output File 2: `data/products.json`
|
||||
|
||||
### Purpose
|
||||
Store all product data for display on product detail pages.
|
||||
|
||||
### Structure
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "PROD_CODE",
|
||||
"productCode": "PROD_CODE",
|
||||
"category": "CATEGORY",
|
||||
"description": "DESCRIPTION",
|
||||
"discontinued": false,
|
||||
"location": "LOC_CODE",
|
||||
"baseType": "Window|Door",
|
||||
"subType": {
|
||||
"door": "VALUE_OR_NULL",
|
||||
"window": "VALUE_OR_NULL"
|
||||
},
|
||||
"materials": ["Aluminum", "Vinyl"],
|
||||
"colors": ["Black", "White", "Bronze", "Tan", "Mill", "Sandstone"],
|
||||
"isAccessory": false,
|
||||
"compatibleAccessories": ["PROD_CODE1", "PROD_CODE2"]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Generation Rules
|
||||
1. **Include all rows** where `DISCONT` (column A) is FALSE
|
||||
2. **Skip accessories** (column J = TRUE) - those go in accessories.json
|
||||
3. **Materials array**: Include all material columns (L+) where value is TRUE
|
||||
4. **Colors array**: Include all color columns where value is TRUE
|
||||
5. **baseType**: Copy from column G
|
||||
6. **subType**: Create object with "door" and "window" keys from columns H and I
|
||||
7. **compatibleAccessories**: Populate based on accessories linked to this product (see Accessories section)
|
||||
|
||||
---
|
||||
|
||||
## Output File 3: `data/accessories.json`
|
||||
|
||||
### Purpose
|
||||
Store accessory/option data that can be applied to products (e.g., handles for storm doors, glass inserts).
|
||||
|
||||
### Structure
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "PROD_CODE",
|
||||
"accessoryCode": "PROD_CODE",
|
||||
"category": "CATEGORY",
|
||||
"description": "DESCRIPTION",
|
||||
"discontinued": false,
|
||||
"location": "LOC_CODE",
|
||||
"baseType": "Window|Door",
|
||||
"materials": ["Aluminum"],
|
||||
"colors": ["Black", "White"],
|
||||
"compatibilityRules": {
|
||||
"type": "subType|category|specific",
|
||||
"subTypeDoor": "Storm Door",
|
||||
"subTypeWindow": null,
|
||||
"categories": ["STD", "INSERTS"],
|
||||
"specificProducts": ["6100I", "8100I"],
|
||||
"requiresMatch": {
|
||||
"material": true,
|
||||
"color": false
|
||||
}
|
||||
},
|
||||
"optionType": "handle|insert|hardware|glass",
|
||||
"metadata": {
|
||||
"specificItemLink": false
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Generation Rules
|
||||
1. **Include all rows** where `Accessory Yes` (column J) is TRUE
|
||||
2. **Skip discontinued** items (column A = TRUE)
|
||||
3. **compatibilityRules.type**: Determine based on available data
|
||||
- If column H (Sub-type Door) or column I (Sub-type Window) has value → "subType"
|
||||
- If column K (This Item) is TRUE → "specific" (requires product linking)
|
||||
- Otherwise → "category"
|
||||
|
||||
4. **compatibilityRules.subTypeDoor/Window**: Copy from columns H and I
|
||||
5. **compatibilityRules.categories**: Extract from column D (CATEGORY)
|
||||
6. **compatibilityRules.specificProducts**: To be populated based on column K logic
|
||||
- If column K is TRUE, this accessory is for specific products
|
||||
- You may need to analyze patterns in PROD_CODE or CATEGORY to determine relationships
|
||||
- Example: "BGIST" (inserts for storm doors) might be compatible with all storm door products
|
||||
|
||||
7. **optionType**: Infer from DESCRIPTION or CATEGORY
|
||||
- If DESCRIPTION contains "HANDLE" → "handle"
|
||||
- If DESCRIPTION contains "INSERT" → "insert"
|
||||
- If DESCRIPTION contains "HARDWARE" → "hardware"
|
||||
- If DESCRIPTION contains "GLASS" → "glass"
|
||||
- Default → "option"
|
||||
|
||||
8. **requiresMatch**: Set based on whether accessory materials/colors must match product
|
||||
- For handles/hardware: material matching often required
|
||||
- For inserts: usually flexible
|
||||
|
||||
---
|
||||
|
||||
## Multi-Group Assignment (Bit Values Consideration)
|
||||
|
||||
### Current Structure
|
||||
Currently, each product belongs to a single category/sub-type based on columns G, H, and I.
|
||||
|
||||
### Proposed Enhancement: Bit Flags
|
||||
To allow products to appear in multiple navigation groups, consider implementing bit flags for categories:
|
||||
|
||||
```json
|
||||
{
|
||||
"productCode": "EXAMPLE",
|
||||
"description": "Multi-purpose product",
|
||||
"categoryFlags": 7, // Binary: 111 (belongs to groups 1, 2, and 4)
|
||||
"categoryBits": {
|
||||
"residential": 1, // 2^0 = 1
|
||||
"commercial": 2, // 2^1 = 2
|
||||
"industrial": 4, // 2^2 = 4
|
||||
"custom": 8 // 2^3 = 8
|
||||
},
|
||||
"navigationGroups": ["residential", "commercial", "industrial"]
|
||||
}
|
||||
```
|
||||
|
||||
### Implementation Options
|
||||
|
||||
#### Option A: Additional CSV Columns
|
||||
Add bit value columns to track multiple group memberships:
|
||||
- Column U: Navigation Group Bits (integer)
|
||||
- Column V: Secondary Category
|
||||
- Column W: Tertiary Category
|
||||
|
||||
#### Option B: Parse from Description/Category
|
||||
Analyze DESCRIPTION and CATEGORY fields to identify products that could belong to multiple groups:
|
||||
- Keywords indicating dual-purpose (e.g., "residential/commercial")
|
||||
- Multiple category codes separated by delimiter
|
||||
|
||||
#### Option C: JSON-Only Enhancement
|
||||
Generate single-group assignments from CSV, then manually or programmatically enhance JSON with additional group memberships based on business rules.
|
||||
|
||||
### Navigation Impact
|
||||
With bit values:
|
||||
1. Multiple sub-type paths could lead to the same product
|
||||
2. Products appear in search results for multiple filter combinations
|
||||
3. Requires modification to filter logic to use bitwise operations
|
||||
|
||||
**Example**: A storm door that works for both residential and commercial applications could appear in both navigation paths.
|
||||
|
||||
### Refinement Needed
|
||||
This feature requires:
|
||||
- ✅ Business rules for multi-group assignment
|
||||
- ✅ Decision on implementation approach (CSV vs JSON)
|
||||
- ✅ Filter logic updates to handle bitwise comparisons
|
||||
- ✅ Testing to ensure products appear in correct groups
|
||||
- ✅ UI considerations (showing product appears in multiple categories)
|
||||
|
||||
---
|
||||
|
||||
## Data Relationships
|
||||
|
||||
### Products ↔ Accessories Linking
|
||||
|
||||
1. **By Sub-Type**: Accessories with matching sub-type values (columns H or I) are compatible
|
||||
- Example: Accessory with subTypeDoor="Storm Door" → compatible with all products where subType.door="Storm Door"
|
||||
|
||||
2. **By Category**: Accessories linked to specific CATEGORY codes
|
||||
- Example: Accessory with category="INSERTS" might be compatible with products in "STD" category
|
||||
|
||||
3. **By Specific Product Code**: Use column K (This Item) flag
|
||||
- If TRUE, requires manual mapping or pattern analysis
|
||||
- Consider adding a "specificProductCodes" field to link directly
|
||||
|
||||
### Recommendation Algorithm
|
||||
When displaying accessories for a product:
|
||||
```
|
||||
1. Match by specificProducts first (exact match)
|
||||
2. Match by subType (door or window)
|
||||
3. Match by category
|
||||
4. Filter by material/color compatibility if requiresMatch is true
|
||||
5. Exclude discontinued accessories
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Processing Steps
|
||||
|
||||
### Step 1: Parse CSV
|
||||
1. Read products.csv starting from row 3 (skip header rows 1-2)
|
||||
2. Split each row by comma delimiter
|
||||
3. Handle empty fields appropriately
|
||||
4. Parse boolean values (TRUE/FALSE → true/false in JSON)
|
||||
|
||||
### Step 2: Categorize Rows
|
||||
1. Separate products (column J = FALSE) from accessories (column J = TRUE)
|
||||
2. Filter out discontinued items (column A = TRUE) or include with flag
|
||||
|
||||
### Step 3: Extract Navigation Data
|
||||
1. Collect unique values from columns G, H, I
|
||||
2. For each sub-type group, analyze material and color diversity
|
||||
3. Build question hierarchy with conditional logic:
|
||||
- Level 1: Base Type (Window, Door) - from column G
|
||||
- Level 2: Sub-Type (Storm Door, Casement, etc.) - from columns H/I
|
||||
- Level 3: Material (CONDITIONAL) - from columns L, M
|
||||
- Level 4: Color (CONDITIONAL) - from color columns
|
||||
- Level 5: Dimensions & final inputs
|
||||
4. Generate question flow with proper linking (next values)
|
||||
|
||||
#### Algorithm: Determine Material Question Necessity
|
||||
```
|
||||
For each sub-type group:
|
||||
products = filter products where subType matches AND DISCONT=FALSE
|
||||
|
||||
aluminumCount = count products where Aluminum (col L) = TRUE
|
||||
vinylCount = count products where Vinyl (col M) = TRUE
|
||||
bothCount = count products where Aluminum=TRUE AND Vinyl=TRUE
|
||||
|
||||
IF (aluminumCount > 0 AND vinylCount > 0) OR bothCount > 0:
|
||||
SHOW material question for this sub-type
|
||||
Create q-material-{subtype} with options for available materials
|
||||
ELSE:
|
||||
SKIP material question
|
||||
Link sub-type answer directly to color question or dimensions
|
||||
Auto-apply the single material to filter
|
||||
```
|
||||
|
||||
#### Algorithm: Determine Color Question Necessity
|
||||
```
|
||||
For each (sub-type, material) combination:
|
||||
products = filter products where match AND DISCONT=FALSE
|
||||
|
||||
availableColors = []
|
||||
For each color column (N through S+):
|
||||
IF any product has this color = TRUE:
|
||||
add color to availableColors
|
||||
|
||||
IF len(availableColors) > 1:
|
||||
SHOW color question for this path
|
||||
ELSE IF len(availableColors) = 1:
|
||||
SKIP color question
|
||||
Auto-apply the single color to filter
|
||||
ELSE:
|
||||
SKIP color question (no color data)
|
||||
```
|
||||
|
||||
### Step 4: Build Products Array
|
||||
1. For each non-accessory, non-discontinued row:
|
||||
- Extract all product fields
|
||||
- Parse material/color columns into arrays
|
||||
- Create product object
|
||||
- Add to products array
|
||||
|
||||
### Step 5: Build Accessories Array
|
||||
1. For each accessory row:
|
||||
- Extract accessory fields
|
||||
- Determine compatibility rules
|
||||
- Infer option type from description
|
||||
- Create accessory object
|
||||
- Add to accessories array
|
||||
|
||||
### Step 6: Link Products to Accessories
|
||||
1. For each product, find compatible accessories based on:
|
||||
- Sub-type matching
|
||||
- Category matching
|
||||
- Specific product code matching
|
||||
2. Populate compatibleAccessories array in products.json
|
||||
3. Verify bidirectional relationships
|
||||
|
||||
### Step 7: Validate Output
|
||||
1. Ensure all JSON is valid and properly formatted
|
||||
2. Check that all question flows have valid "next" links
|
||||
3. Verify product-accessory relationships are logical
|
||||
4. Confirm no duplicate IDs exist
|
||||
|
||||
---
|
||||
|
||||
## Advanced Considerations
|
||||
|
||||
### Column Extensions
|
||||
If additional columns are added beyond column T (~):
|
||||
- Check for additional material/color flags
|
||||
- Look for price, availability, or specification data
|
||||
- Include in metadata or as new product properties
|
||||
|
||||
### Future Enhancements
|
||||
You may want to extend the schema with:
|
||||
1. **Pricing**: Add price fields to products and accessories
|
||||
2. **Images**: Add image URLs or filenames
|
||||
3. **Specifications**: Add detailed specs (dimensions, ratings, etc.)
|
||||
4. **Availability**: Add stock levels or lead times
|
||||
5. **Sorting**: Add sort order or priority fields
|
||||
|
||||
---
|
||||
|
||||
## Example Outputs
|
||||
|
||||
### Example Product Object
|
||||
```json
|
||||
{
|
||||
"id": "6100I",
|
||||
"productCode": "6100I",
|
||||
"category": "STD",
|
||||
"description": "STAR 6100 FULL VIEW STORM DOOR",
|
||||
"discontinued": false,
|
||||
"location": "Iola",
|
||||
"baseType": "Door",
|
||||
"subType": {
|
||||
"door": "Storm Door",
|
||||
"window": null
|
||||
},
|
||||
"materials": ["Aluminum"],
|
||||
"colors": ["White", "Bronze"],
|
||||
"isAccessory": false,
|
||||
"compatibleAccessories": ["BGIST", "TGIODD"]
|
||||
}
|
||||
```
|
||||
|
||||
### Example Accessory Object
|
||||
```json
|
||||
{
|
||||
"id": "BGIST",
|
||||
"accessoryCode": "BGIST",
|
||||
"category": "INSERTS",
|
||||
"description": "INSERTS FOR STORM DOORS",
|
||||
"discontinued": false,
|
||||
"location": "Iola",
|
||||
"baseType": "Door",
|
||||
"materials": ["Aluminum"],
|
||||
"colors": ["Black", "White", "Bronze", "Mill", "Sandstone"],
|
||||
"compatibilityRules": {
|
||||
"type": "subType",
|
||||
"subTypeDoor": "Storm Door",
|
||||
"subTypeWindow": null,
|
||||
"categories": ["STD", "PD"],
|
||||
"specificProducts": [],
|
||||
"requiresMatch": {
|
||||
"material": true,
|
||||
"color": false
|
||||
}
|
||||
},
|
||||
"optionType": "insert",
|
||||
"metadata": {
|
||||
"specificItemLink": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example Navigation Flow
|
||||
```json
|
||||
{
|
||||
"start": {
|
||||
"type": "question",
|
||||
"inputType": "button",
|
||||
"title": "What are you looking for?",
|
||||
"subtitle": "Select the product category",
|
||||
"answers": [
|
||||
{
|
||||
"caption": "Door",
|
||||
"image": "🚪",
|
||||
"next": "q-door-type",
|
||||
"filter": { "baseType": "Door" }
|
||||
},
|
||||
{
|
||||
"caption": "Window",
|
||||
"image": "🪟",
|
||||
"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": "Storm Door",
|
||||
"image": "🚪",
|
||||
"next": "q-dimensions",
|
||||
"filter": {
|
||||
"baseType": "Door",
|
||||
"subType": "Storm Door",
|
||||
"material": "Aluminum"
|
||||
},
|
||||
"note": "Material question skipped - all storm doors are aluminum only"
|
||||
},
|
||||
{
|
||||
"caption": "Patio Door",
|
||||
"image": "🚪",
|
||||
"next": "q-material-patio",
|
||||
"filter": {
|
||||
"baseType": "Door",
|
||||
"subType": "Patio Door"
|
||||
},
|
||||
"note": "Material question shown - patio doors have multiple material options"
|
||||
}
|
||||
]
|
||||
},
|
||||
"q-material-patio": {
|
||||
"type": "question",
|
||||
"inputType": "button",
|
||||
"title": "Select Material",
|
||||
"subtitle": "Choose your preferred material for Patio Door",
|
||||
"answers": [
|
||||
{
|
||||
"caption": "Aluminum",
|
||||
"image": "🔩",
|
||||
"next": "q-color-patio-aluminum",
|
||||
"filter": {
|
||||
"baseType": "Door",
|
||||
"subType": "Patio Door",
|
||||
"material": "Aluminum"
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption": "Vinyl",
|
||||
"image": "🪟",
|
||||
"next": "q-color-patio-vinyl",
|
||||
"filter": {
|
||||
"baseType": "Door",
|
||||
"subType": "Patio Door",
|
||||
"material": "Vinyl"
|
||||
}
|
||||
}
|
||||
],
|
||||
"conditional": {
|
||||
"showIf": "multipleOptionsExist",
|
||||
"check": "materials",
|
||||
"fallbackNext": "q-dimensions"
|
||||
}
|
||||
},
|
||||
"q-color-patio-aluminum": {
|
||||
"type": "question",
|
||||
"inputType": "button",
|
||||
"title": "Select Color",
|
||||
"subtitle": "Choose your preferred color",
|
||||
"answers": [
|
||||
{
|
||||
"caption": "White",
|
||||
"next": "q-dimensions",
|
||||
"filter": {
|
||||
"baseType": "Door",
|
||||
"subType": "Patio Door",
|
||||
"material": "Aluminum",
|
||||
"color": "White"
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption": "Bronze",
|
||||
"next": "q-dimensions",
|
||||
"filter": {
|
||||
"baseType": "Door",
|
||||
"subType": "Patio Door",
|
||||
"material": "Aluminum",
|
||||
"color": "Bronze"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key Points in Example**:
|
||||
- Storm Door skips material question (goes directly to dimensions)
|
||||
- Patio Door shows material question (multiple materials available)
|
||||
- Material selection leads to color-specific questions
|
||||
- All paths eventually reach dimensions entry
|
||||
|
||||
---
|
||||
|
||||
## Final Notes
|
||||
|
||||
1. **Data Quality**: Some rows may have inconsistent data. Handle gracefully with defaults.
|
||||
2. **Empty Values**: Treat empty strings as null in JSON
|
||||
3. **Boolean Conversion**: CSV TRUE/FALSE should become JSON true/false
|
||||
4. **Unique IDs**: PROD_CODE serves as the unique identifier
|
||||
5. **Relationships**: The linking between products and accessories may require iterative refinement based on business rules
|
||||
|
||||
## Questions to Consider
|
||||
|
||||
When implementing, clarify:
|
||||
1. Should discontinued items be included in ANY output file?
|
||||
2. How should accessories with column K=TRUE be specifically linked?
|
||||
3. Are there additional columns beyond column S that need parsing?
|
||||
4. Should colors and materials be validated against a master list?
|
||||
5. What should happen if a product has no compatible accessories?
|
||||
6. **Should the navigation include filters for materials/colors early, or determine dynamically based on product availability?** ✅ RESOLVED: Dynamic based on availability
|
||||
7. **For sub-types with only one material option, should that material be auto-applied to the filter?** → YES, skip question and auto-apply
|
||||
8. **Multi-group assignment (bit values):**
|
||||
- Should products be able to appear in multiple navigation paths?
|
||||
- If yes, how should this be indicated in the CSV? (new column, description parsing, manual JSON editing?)
|
||||
- What business rules determine multi-group membership?
|
||||
- Should search results indicate a product appears in multiple categories?
|
||||
9. **Material/Color question threshold:**
|
||||
- Current logic: Show if > 1 option exists
|
||||
- Alternative: Show only if > X% of products have multiple options (e.g., 20% threshold)
|
||||
- Should we show questions even if only 1-2 products have alternative options?
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
The parsing is complete when:
|
||||
- ✅ All three JSON files are generated and valid
|
||||
- ✅ Products.json contains all non-accessory, non-discontinued products
|
||||
- ✅ Accessories.json contains all accessory items with proper compatibility rules
|
||||
- ✅ Navigation.json provides a complete question flow from start to product selection
|
||||
- ✅ Product-accessory relationships are established and logical
|
||||
- ✅ All materials and colors are properly extracted into arrays
|
||||
- ✅ No data loss from the original CSV
|
||||
@@ -0,0 +1,221 @@
|
||||
# Bitwise Helper Files - Usage Guide
|
||||
|
||||
## 📁 Generated Files
|
||||
|
||||
1. **`data/product_bitwise.csv`** - CSV format with bit values for each product
|
||||
2. **`data/product_bitwise.json`** - JSON format with bit values for each product
|
||||
3. **`data/bitwise_legend.json`** - Complete bit mapping documentation
|
||||
|
||||
## 🔢 Bit System Overview
|
||||
|
||||
Each product gets a single integer value that encodes multiple attributes using bitwise flags.
|
||||
|
||||
### Bit Positions (0-11): Base Attributes, Materials, Colors
|
||||
|
||||
| Bit | Value | Attribute |
|
||||
|-----|-------|-----------|
|
||||
| 0 | 1 | Door |
|
||||
| 1 | 2 | Window |
|
||||
| 2 | 4 | Is Accessory |
|
||||
| 3 | 8 | Specific Item Link |
|
||||
| 4 | 16 | Aluminum Material |
|
||||
| 5 | 32 | Vinyl Material |
|
||||
| 6 | 64 | Black Color |
|
||||
| 7 | 128 | White Color |
|
||||
| 8 | 256 | Bronze Color |
|
||||
| 9 | 512 | Tan Color |
|
||||
| 10 | 1024 | Mill Color |
|
||||
| 11 | 2048 | Sandstone Color |
|
||||
|
||||
### Bit Positions (12+): Sub-types
|
||||
|
||||
| Bit | Value | Sub-type |
|
||||
|-----|-------|----------|
|
||||
| 12 | 4096 | Patio Door |
|
||||
| 13 | 8192 | Primary Window |
|
||||
| 14 | 16384 | Storm Door |
|
||||
| 15 | 32768 | Storm Window |
|
||||
|
||||
## 💡 Usage Examples
|
||||
|
||||
### Example 1: Storm Door Insert (BGIST)
|
||||
```
|
||||
Product Code: BGIST
|
||||
Description: INSERTS FOR STORM DOORS
|
||||
Bit Value: 17553
|
||||
Binary: 0100010010010001
|
||||
|
||||
Decoded:
|
||||
✓ Door (bit 0 = 1)
|
||||
✗ Window (bit 1 = 0)
|
||||
✗ Accessory (bit 2 = 0)
|
||||
✗ Specific Item (bit 3 = 0)
|
||||
✓ Aluminum (bit 4 = 1)
|
||||
✗ Vinyl (bit 5 = 0)
|
||||
✓ Black (bit 6 = 1)
|
||||
✓ White (bit 7 = 1)
|
||||
✓ Bronze (bit 8 = 1)
|
||||
✗ Tan (bit 9 = 0)
|
||||
✓ Mill (bit 10 = 1)
|
||||
✓ Sandstone (bit 11 = 1)
|
||||
✗ Patio Door (bit 12 = 0)
|
||||
✗ Primary Window (bit 13 = 0)
|
||||
✓ Storm Door (bit 14 = 1)
|
||||
✗ Storm Window (bit 15 = 0)
|
||||
```
|
||||
|
||||
### Example 2: Check Attributes in Code
|
||||
|
||||
#### Python
|
||||
```python
|
||||
import json
|
||||
|
||||
# Load helper file
|
||||
with open('data/product_bitwise.json') as f:
|
||||
products = json.load(f)
|
||||
|
||||
# Get a product
|
||||
product = next(p for p in products if p['PROD_CODE'] == 'BGIST')
|
||||
bit_value = product['BIT_VALUE']
|
||||
|
||||
# Check individual flags
|
||||
is_door = bool(bit_value & 1)
|
||||
is_aluminum = bool(bit_value & 16)
|
||||
is_white = bool(bit_value & 128)
|
||||
is_storm_door = bool(bit_value & 16384)
|
||||
|
||||
print(f"BGIST is door: {is_door}")
|
||||
print(f"BGIST is aluminum: {is_aluminum}")
|
||||
print(f"BGIST is white: {is_white}")
|
||||
print(f"BGIST is storm door subtype: {is_storm_door}")
|
||||
|
||||
# Filter products by multiple criteria
|
||||
# Example: Find all aluminum doors with white color
|
||||
aluminum_white_doors = [
|
||||
p for p in products
|
||||
if (p['BIT_VALUE'] & 1) and # Is a door
|
||||
(p['BIT_VALUE'] & 16) and # Has aluminum
|
||||
(p['BIT_VALUE'] & 128) and # Has white
|
||||
not p['DISCONTINUED'] # Not discontinued
|
||||
]
|
||||
|
||||
print(f"Found {len(aluminum_white_doors)} aluminum white doors")
|
||||
```
|
||||
|
||||
#### JavaScript
|
||||
```javascript
|
||||
// Load the JSON file
|
||||
fetch('data/product_bitwise.json')
|
||||
.then(response => response.json())
|
||||
.then(products => {
|
||||
// Check individual flags
|
||||
const product = products.find(p => p.PROD_CODE === 'BGIST');
|
||||
const bitValue = product.BIT_VALUE;
|
||||
|
||||
const isDoor = !!(bitValue & 1);
|
||||
const isAluminum = !!(bitValue & 16);
|
||||
const isWhite = !!(bitValue & 128);
|
||||
const isStormDoor = !!(bitValue & 16384);
|
||||
|
||||
console.log(`BGIST is door: ${isDoor}`);
|
||||
console.log(`BGIST is aluminum: ${isAluminum}`);
|
||||
console.log(`BGIST is white: ${isWhite}`);
|
||||
console.log(`BGIST is storm door: ${isStormDoor}`);
|
||||
|
||||
// Filter products
|
||||
const aluminumWhiteDoors = products.filter(p =>
|
||||
(p.BIT_VALUE & 1) && // Is a door
|
||||
(p.BIT_VALUE & 16) && // Has aluminum
|
||||
(p.BIT_VALUE & 128) && // Has white
|
||||
!p.DISCONTINUED // Not discontinued
|
||||
);
|
||||
|
||||
console.log(`Found ${aluminumWhiteDoors.length} aluminum white doors`);
|
||||
});
|
||||
```
|
||||
|
||||
## 🎯 Benefits of Bitwise Classification
|
||||
|
||||
### 1. **Multi-Group Assignment**
|
||||
Products can belong to multiple categories simultaneously:
|
||||
- A product can be both Door AND Window (rare but possible)
|
||||
- A product can have multiple materials (Aluminum AND Vinyl)
|
||||
- A product can have multiple colors
|
||||
|
||||
### 2. **Fast Filtering**
|
||||
Bitwise operations are extremely fast:
|
||||
```python
|
||||
# Instead of:
|
||||
if product.baseType == 'Door' and 'Aluminum' in product.materials and 'White' in product.colors:
|
||||
|
||||
# Use:
|
||||
if (bit_value & 1) and (bit_value & 16) and (bit_value & 128):
|
||||
```
|
||||
|
||||
### 3. **Compact Storage**
|
||||
One integer stores multiple attributes:
|
||||
- Single integer vs. multiple boolean fields
|
||||
- Easy to index and search in databases
|
||||
- Efficient for large datasets
|
||||
|
||||
### 4. **Easy Matching**
|
||||
Perfect for accessory compatibility:
|
||||
```python
|
||||
# Check if accessory matches product materials
|
||||
accessory_materials = 48 # Aluminum (16) + Vinyl (32)
|
||||
product_materials = 16 # Aluminum only
|
||||
|
||||
# Check if they share any materials
|
||||
if accessory_materials & product_materials:
|
||||
print("Compatible!") # True because both have Aluminum
|
||||
```
|
||||
|
||||
## 🔄 Integration with Navigation System
|
||||
|
||||
Use bitwise values to:
|
||||
1. Dynamically generate navigation options
|
||||
2. Filter products in real-time based on user selections
|
||||
3. Match accessories to products efficiently
|
||||
4. Handle complex "OR" queries (multiple categories)
|
||||
|
||||
### Example: Dynamic Material Question
|
||||
```python
|
||||
# Get all products for "Storm Door" subtype
|
||||
storm_door_products = [p for p in products if p['BIT_VALUE'] & 16384]
|
||||
|
||||
# Check materials
|
||||
has_aluminum = any(p['BIT_VALUE'] & 16 for p in storm_door_products)
|
||||
has_vinyl = any(p['BIT_VALUE'] & 32 for p in storm_door_products)
|
||||
|
||||
# Show material question only if multiple materials exist
|
||||
if has_aluminum and has_vinyl:
|
||||
show_material_question()
|
||||
else:
|
||||
skip_to_next_question()
|
||||
```
|
||||
|
||||
## 📝 Maintenance
|
||||
|
||||
### Regenerating Helper Files
|
||||
When products.csv changes:
|
||||
```bash
|
||||
python generate_bitwise_helper.py
|
||||
```
|
||||
|
||||
### Adding New Attributes
|
||||
To add new bit flags, edit `generate_bitwise_helper.py`:
|
||||
1. Add to `BIT_DEFINITIONS` dictionary
|
||||
2. Update `calculate_bit_value()` function
|
||||
3. Regenerate files
|
||||
|
||||
### Adding New Sub-types
|
||||
Sub-types are automatically detected! Just add them to columns H or I in the CSV, then regenerate.
|
||||
|
||||
## 🚀 Performance Notes
|
||||
|
||||
- Bitwise AND (`&`) - Check if flag is set: `bit_value & 16`
|
||||
- Bitwise OR (`|`) - Combine flags: `16 | 128` = Aluminum + White
|
||||
- Bitwise NOT (`~`) - Invert flags (advanced)
|
||||
- Bitwise XOR (`^`) - Toggle flags (advanced)
|
||||
|
||||
All bitwise operations are O(1) - constant time!
|
||||
@@ -0,0 +1,238 @@
|
||||
# Deployment Guide - Files to Upload to Server
|
||||
|
||||
## 🚀 Updated Files for Login System & Permission System
|
||||
|
||||
To deploy the new login, location selection, user management, and permission system to your server, you'll need to upload the following files:
|
||||
|
||||
### ✅ Essential Updated Files
|
||||
|
||||
#### **1. Main Application Files**
|
||||
These core files have been modified and MUST be uploaded:
|
||||
- `app/app.py` - Main Flask application with authentication, sessions, and permissions
|
||||
- `app/config.py` - Configuration (verify SECRET_KEY is set)
|
||||
- `app/requirements.txt` - Python dependencies (may need to run `pip install -r requirements.txt` on server)
|
||||
|
||||
#### **2. HTML Templates** (all in `app/templates/`)
|
||||
- `app/templates/login.html` - Login page
|
||||
- `app/templates/select_location.html` - Location selection page (with User Management link)
|
||||
- `app/templates/user_manager.html` - User management interface (with password change)
|
||||
- `app/templates/index2.html` - Main app with user info header
|
||||
- `app/templates/access_denied.html` - Permission denied page
|
||||
|
||||
#### **3. CSS Files**
|
||||
- `app/css/styles.css` - Updated styles for user-info-bar and logout button
|
||||
|
||||
#### **4. User Data**
|
||||
- `app/data/users.json` - User accounts with permissions
|
||||
- `app/data/example_user_structure.json` - Example data format (documentation only)
|
||||
|
||||
⚠️ **Important**: If you have existing users on the server, back them up first, then merge the permission structure into existing user accounts.
|
||||
|
||||
### 📁 New Folders/Files Created
|
||||
|
||||
#### **Admin Utilities** (optional, but recommended)
|
||||
- `app/admin/` - New folder
|
||||
- `app/admin/README.md` - Admin utilities documentation
|
||||
- `app/admin/fix_default_locations.py` - User maintenance tool
|
||||
- `app/admin/test_password_security.py` - Password security demo
|
||||
|
||||
⚠️ **Security Note**: The `app/admin/` folder should NOT be web-accessible. Configure your server to block access to this directory.
|
||||
|
||||
### 📚 Documentation Files (moved to information/)
|
||||
These files are for reference only and do NOT need to be uploaded to the production server:
|
||||
- `information/LOGIN_SYSTEM_README.md`
|
||||
- `information/PERMISSIONS_SYSTEM.md`
|
||||
- `information/PERMISSIONS_QUICKSTART.md`
|
||||
- `information/USER_MANAGEMENT_README.md`
|
||||
|
||||
### 🔧 Server Configuration
|
||||
|
||||
#### **Python Dependencies**
|
||||
After uploading files, install/update dependencies on the server:
|
||||
```bash
|
||||
cd /path/to/app
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
**Key dependencies** (will be installed from requirements.txt):
|
||||
- Flask >= 3.0.0
|
||||
- Werkzeug (for password hashing)
|
||||
|
||||
#### **Secret Key Configuration**
|
||||
Ensure `app/config.py` has a strong SECRET_KEY:
|
||||
```python
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY') or 'your-production-secret-key-here'
|
||||
```
|
||||
|
||||
For production, use environment variable or generate with:
|
||||
```python
|
||||
import secrets
|
||||
print(secrets.token_urlsafe(32))
|
||||
```
|
||||
|
||||
#### **File Permissions**
|
||||
Set appropriate permissions on the server:
|
||||
```bash
|
||||
# Data directory should be writable by the web server
|
||||
chmod 755 app/data/
|
||||
chmod 644 app/data/users.json
|
||||
|
||||
# Admin directory should NOT be web accessible
|
||||
chmod 700 app/admin/
|
||||
```
|
||||
|
||||
#### **WSGI Configuration**
|
||||
If using WSGI (Passenger, uWSGI, etc.):
|
||||
- `app/passenger_wsgi.py` - Already exists (Passenger)
|
||||
- `app/wsgi.py` - Already exists (generic WSGI)
|
||||
|
||||
Make sure your server is configured to use the appropriate WSGI file.
|
||||
|
||||
### 🔐 Security Checklist Before Deployment
|
||||
|
||||
- [ ] Change SECRET_KEY in config.py to a secure random value
|
||||
- [ ] Verify users.json has ONLY hashed passwords (no plain text)
|
||||
- [ ] Block web access to `/app/admin/` directory
|
||||
- [ ] Block web access to `/app/data/` directory (except through API)
|
||||
- [ ] Enable HTTPS/SSL on the server
|
||||
- [ ] Set `SESSION_COOKIE_SECURE = True` in config.py if using HTTPS
|
||||
- [ ] Set appropriate file permissions (755/644)
|
||||
- [ ] Test login functionality after deployment
|
||||
- [ ] Verify permissions system works (try accessing /users without permission)
|
||||
|
||||
### 📤 Upload Methods
|
||||
|
||||
#### **Option 1: FTP/SFTP**
|
||||
Upload all the files listed above using your FTP client, maintaining the directory structure.
|
||||
|
||||
#### **Option 2: Git**
|
||||
If using Git:
|
||||
```bash
|
||||
git add app/app.py app/templates/* app/css/* app/data/users.json app/admin/*
|
||||
git commit -m "Add login system, permissions, and user management"
|
||||
git push
|
||||
```
|
||||
Then on server:
|
||||
```bash
|
||||
git pull
|
||||
pip install -r app/requirements.txt
|
||||
# Restart web server
|
||||
```
|
||||
|
||||
#### **Option 3: ZIP Archive**
|
||||
Create a ZIP of the entire `app/` folder and extract on the server.
|
||||
|
||||
### 🔄 Migration Steps for Existing Server
|
||||
|
||||
If you already have a running server:
|
||||
|
||||
1. **Backup Current Installation**
|
||||
```bash
|
||||
cp -r app/ app_backup_$(date +%Y%m%d)/
|
||||
```
|
||||
|
||||
2. **Upload New Files**
|
||||
Upload all files listed in "Essential Updated Files" section
|
||||
|
||||
3. **Update Dependencies**
|
||||
```bash
|
||||
pip install -r app/requirements.txt
|
||||
```
|
||||
|
||||
4. **Update Existing Users** (if applicable)
|
||||
If you have existing users without the permission structure, add permissions:
|
||||
```json
|
||||
{
|
||||
"username": "existing_user",
|
||||
"password": "existing_hash",
|
||||
"permissions": {
|
||||
"manage_users": false,
|
||||
"view_reports": true,
|
||||
"create_quotes": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
5. **Test in Maintenance Mode**
|
||||
- Test login at `/login`
|
||||
- Test user management at `/users` (as admin)
|
||||
- Test main app still works
|
||||
- Test permission checks
|
||||
|
||||
6. **Restart Web Server**
|
||||
```bash
|
||||
# Apache with Passenger
|
||||
touch tmp/restart.txt
|
||||
|
||||
# Or systemctl
|
||||
sudo systemctl restart your-service-name
|
||||
```
|
||||
|
||||
### 🧪 Post-Deployment Testing
|
||||
|
||||
Test these flows after deployment:
|
||||
|
||||
1. **Login Flow**
|
||||
- Navigate to `/login`
|
||||
- Login with correct credentials
|
||||
- Verify redirect to location selection (if multiple locations)
|
||||
- Verify redirect to main app
|
||||
|
||||
2. **Permission System**
|
||||
- Login as admin user (Master)
|
||||
- Access `/users` - should work
|
||||
- Login as non-admin user
|
||||
- Try to access `/users` - should see "Access Denied"
|
||||
|
||||
3. **Password Change**
|
||||
- Login as admin
|
||||
- Go to User Management
|
||||
- Click "Change Password" on a user
|
||||
- Verify your password is required
|
||||
- Change password and verify new password works
|
||||
|
||||
4. **Location Selection**
|
||||
- Login as user with multiple locations
|
||||
- Verify location selection page shows
|
||||
- Verify User Management button shows only for admins
|
||||
- Select location and verify redirect to main app
|
||||
|
||||
### ❗ Troubleshooting
|
||||
|
||||
**"500 Internal Server Error" after deployment:**
|
||||
- Check server error logs
|
||||
- Verify SECRET_KEY is set
|
||||
- Ensure all dependencies installed
|
||||
- Check file permissions
|
||||
|
||||
**"Users not loading" or "No users shown":**
|
||||
- Verify `data/users.json` uploaded correctly
|
||||
- Check JSON format is valid
|
||||
- Ensure web server can read the file
|
||||
|
||||
**"Permission denied" when accessing files:**
|
||||
- Check file ownership (should be web server user)
|
||||
- Set correct permissions (755 for directories, 644 for files)
|
||||
|
||||
**Session not persisting:**
|
||||
- Verify SECRET_KEY is consistent
|
||||
- Check cookie settings in config
|
||||
- Ensure HTTPS if SESSION_COOKIE_SECURE is True
|
||||
|
||||
### 📞 Support
|
||||
|
||||
After deployment, keep these files handy:
|
||||
- Server error logs (usually in `/var/log/apache2/` or similar)
|
||||
- `information/LOGIN_SYSTEM_README.md` - Login system documentation
|
||||
- `information/PERMISSIONS_SYSTEM.md` - Permission system documentation
|
||||
|
||||
### 🎉 Success Indicators
|
||||
|
||||
You'll know deployment was successful when:
|
||||
- ✅ Accessing `/` redirects to `/login` (if not logged in)
|
||||
- ✅ Login with correct credentials works
|
||||
- ✅ Master user can access `/users`
|
||||
- ✅ Non-admin users see "Access Denied" at `/users`
|
||||
- ✅ User info header shows in main app
|
||||
- ✅ Logout works and redirects to login
|
||||
- ✅ Password change requires admin verification
|
||||
@@ -0,0 +1,476 @@
|
||||
# Dynamic Image Generation API
|
||||
|
||||
## Overview
|
||||
This system generates product images on-the-fly with configurable colors, hardware positions, and other options. It uses server-side image processing with PIL/Pillow to composite and recolor product images dynamically.
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Install Dependencies
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
This will install:
|
||||
- Flask (web framework)
|
||||
- Pillow (image processing)
|
||||
- Other dependencies
|
||||
|
||||
### 2. Prepare Product Images
|
||||
|
||||
For the COBRAI demo product:
|
||||
1. Save the white storm door image as `app/images/cobrai.jpg`
|
||||
2. The configuration is already in `app/data/image_configs.json`
|
||||
|
||||
### 3. Create Cache Directory
|
||||
The cache directory will be created automatically when the app runs:
|
||||
```
|
||||
app/cache/product_images/
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### 1. Generate Product Image
|
||||
|
||||
**Endpoint:** `GET /api/product-image/<product_code>`
|
||||
|
||||
**Parameters:**
|
||||
- `color` - Color name (default: 'white')
|
||||
- Options: white, black, bronze, sandstone
|
||||
- `hinge` - Hinge side (default: 'right')
|
||||
- Options: left, right
|
||||
- `material` - Material type (default: 'aluminum')
|
||||
- For future use
|
||||
- `format` - Return format (default: 'image')
|
||||
- Options: image, json
|
||||
- `cache` - Use caching (default: 'true')
|
||||
- Options: true, false
|
||||
|
||||
**Examples:**
|
||||
|
||||
Return image directly (for use in `<img>` tags):
|
||||
```
|
||||
GET http://localhost:8080/api/product-image/cobrai?color=black&hinge=right
|
||||
GET http://localhost:8080/api/product-image/cobrai?color=bronze&hinge=left
|
||||
```
|
||||
|
||||
Return JSON with base64 image:
|
||||
```
|
||||
GET http://localhost:8080/api/product-image/cobrai?color=white&hinge=right&format=json
|
||||
```
|
||||
|
||||
**Response (format=image):**
|
||||
Raw PNG image file
|
||||
|
||||
**Response (format=json):**
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"productCode": "cobrai",
|
||||
"color": "black",
|
||||
"hinge": "left",
|
||||
"material": "aluminum",
|
||||
"image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...",
|
||||
"format": "base64"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Get Product Configuration
|
||||
|
||||
**Endpoint:** `GET /api/product-config/<product_code>`
|
||||
|
||||
Returns the complete configuration for a product including available colors, colorable regions, hardware positions, etc.
|
||||
|
||||
**Example:**
|
||||
```
|
||||
GET http://localhost:8080/api/product-config/cobrai
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"productCode": "cobrai",
|
||||
"config": {
|
||||
"sourceImage": "images/cobrai.jpg",
|
||||
"productCode": "COBRAI",
|
||||
"availableColors": {
|
||||
"white": {"rgb": [255, 255, 255], "name": "White"},
|
||||
"black": {"rgb": [30, 30, 30], "name": "Black"},
|
||||
"bronze": {"rgb": [110, 80, 50], "name": "Bronze"}
|
||||
},
|
||||
"colorableRegions": [...],
|
||||
"hardwarePositions": {...},
|
||||
"metadata": {...}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Clear Image Cache
|
||||
|
||||
**Endpoint:** `POST /api/clear-image-cache`
|
||||
|
||||
Clears cached images. Useful when updating source images or configurations.
|
||||
|
||||
**Request Body (Optional):**
|
||||
```json
|
||||
{
|
||||
"productCode": "cobrai"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Cache cleared for cobrai"
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration File Format
|
||||
|
||||
Location: `app/data/image_configs.json`
|
||||
|
||||
### Structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"product_code": {
|
||||
"sourceImage": "path/to/source.jpg",
|
||||
"productCode": "PRODUCT_CODE",
|
||||
"description": "Product description",
|
||||
|
||||
"availableColors": {
|
||||
"color_name": {
|
||||
"rgb": [R, G, B],
|
||||
"name": "Display Name"
|
||||
}
|
||||
},
|
||||
|
||||
"colorableRegions": [
|
||||
{
|
||||
"name": "region_identifier",
|
||||
"topLeft": [x, y],
|
||||
"bottomRight": [x, y],
|
||||
"description": "What this region is"
|
||||
}
|
||||
],
|
||||
|
||||
"hardwarePositions": {
|
||||
"handle_right": {
|
||||
"x": 580,
|
||||
"y": 730,
|
||||
"image": "path/to/hardware.png",
|
||||
"flip": false
|
||||
}
|
||||
},
|
||||
|
||||
"glassRegions": [
|
||||
{
|
||||
"name": "glass_area",
|
||||
"topLeft": [x, y],
|
||||
"bottomRight": [x, y],
|
||||
"description": "Glass/screen area"
|
||||
}
|
||||
],
|
||||
|
||||
"metadata": {
|
||||
"imageWidth": 720,
|
||||
"imageHeight": 1450,
|
||||
"defaultColor": "white",
|
||||
"defaultHinge": "right"
|
||||
},
|
||||
|
||||
"cache": {
|
||||
"enabled": true,
|
||||
"directory": "cache/product_images",
|
||||
"maxAge": 86400
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Key Concepts:
|
||||
|
||||
#### Colorable Regions
|
||||
Define rectangular areas to be recolored. The algorithm:
|
||||
1. Preserves brightness/luminosity
|
||||
2. Applies target color
|
||||
3. Maintains shadows and highlights
|
||||
|
||||
Coordinates are `[x, y]` where:
|
||||
- `topLeft`: Upper-left corner
|
||||
- `bottomRight`: Lower-right corner
|
||||
|
||||
#### Hardware Positions
|
||||
Define where handles, locks, etc. should be placed:
|
||||
- `x, y`: Position to place hardware image
|
||||
- `image`: Path to hardware PNG (with transparency)
|
||||
- `flip`: Whether to flip horizontally (for left hinge)
|
||||
|
||||
#### Glass Regions
|
||||
Define areas that should remain unchanged (glass, screens).
|
||||
Currently for documentation purposes; future feature.
|
||||
|
||||
## Frontend Integration
|
||||
|
||||
### Option 1: Direct Image URL
|
||||
```html
|
||||
<img src="/api/product-image/cobrai?color=black&hinge=left" alt="Storm Door">
|
||||
```
|
||||
|
||||
### Option 2: JavaScript with Base64
|
||||
```javascript
|
||||
async function loadProductImage(productCode, color, hinge) {
|
||||
const response = await fetch(
|
||||
`/api/product-image/${productCode}?color=${color}&hinge=${hinge}&format=json`
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'success') {
|
||||
document.getElementById('product-img').src = data.image;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
loadProductImage('cobrai', 'black', 'left');
|
||||
```
|
||||
|
||||
### Option 3: Dynamic URL Switching
|
||||
```javascript
|
||||
function updateProductImage(color, hinge) {
|
||||
const img = document.getElementById('product-img');
|
||||
img.src = `/api/product-image/cobrai?color=${color}&hinge=${hinge}`;
|
||||
}
|
||||
|
||||
// On color change
|
||||
document.getElementById('color-select').addEventListener('change', (e) => {
|
||||
const color = e.target.value;
|
||||
const hinge = document.querySelector('[name="hinge"]:checked').value;
|
||||
updateProductImage(color, hinge);
|
||||
});
|
||||
```
|
||||
|
||||
## How Image Processing Works
|
||||
|
||||
### Color Application
|
||||
1. **Load base image** - White or neutral colored product photo
|
||||
2. **Define regions** - Specify rectangles for frame, panels, etc.
|
||||
3. **Calculate brightness** - For each pixel, determine relative brightness
|
||||
4. **Apply target color** - Colorize while preserving brightness variations
|
||||
5. **Result** - Natural-looking colored product with preserved shadows/highlights
|
||||
|
||||
### Brightness Preservation
|
||||
```python
|
||||
# For each pixel in region:
|
||||
original_brightness = (r + g + b) / 3
|
||||
brightness_factor = original_brightness / 255.0
|
||||
|
||||
new_r = target_color_r * brightness_factor
|
||||
new_g = target_color_g * brightness_factor
|
||||
new_b = target_color_b * brightness_factor
|
||||
```
|
||||
|
||||
This maintains shadows (darker pixels stay darker) and highlights (lighter pixels stay lighter).
|
||||
|
||||
### Hardware Application
|
||||
1. Load hardware PNG with transparency
|
||||
2. Optionally flip horizontally for left hinge
|
||||
3. Composite onto product image at specified position
|
||||
|
||||
### Caching
|
||||
- Generated images are cached using MD5 hash of parameters
|
||||
- Cache key: `{product_code}_{color}_{hinge}_{material}`
|
||||
- Stored as PNG in `cache/product_images/`
|
||||
- Subsequent requests return cached version instantly
|
||||
|
||||
## Testing the Endpoints
|
||||
|
||||
### Using cURL
|
||||
|
||||
Test basic image generation:
|
||||
```bash
|
||||
curl http://localhost:8080/api/product-image/cobrai?color=black > test_black.png
|
||||
```
|
||||
|
||||
Test with different configurations:
|
||||
```bash
|
||||
curl http://localhost:8080/api/product-image/cobrai?color=bronze&hinge=left > test_bronze_left.png
|
||||
```
|
||||
|
||||
Get JSON response:
|
||||
```bash
|
||||
curl http://localhost:8080/api/product-image/cobrai?color=white&format=json
|
||||
```
|
||||
|
||||
Get configuration:
|
||||
```bash
|
||||
curl http://localhost:8080/api/product-config/cobrai
|
||||
```
|
||||
|
||||
Clear cache:
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/clear-image-cache -H "Content-Type: application/json" -d '{"productCode":"cobrai"}'
|
||||
```
|
||||
|
||||
### Using Browser
|
||||
Simply visit:
|
||||
```
|
||||
http://localhost:8080/api/product-image/cobrai?color=black&hinge=left
|
||||
```
|
||||
|
||||
### Using JavaScript Fetch
|
||||
```javascript
|
||||
// Get image as blob
|
||||
fetch('/api/product-image/cobrai?color=bronze&hinge=left')
|
||||
.then(response => response.blob())
|
||||
.then(blob => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
document.getElementById('img').src = url;
|
||||
});
|
||||
|
||||
// Get as JSON
|
||||
fetch('/api/product-image/cobrai?color=black&format=json')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
document.getElementById('img').src = data.image;
|
||||
});
|
||||
```
|
||||
|
||||
## Adding New Products
|
||||
|
||||
### Step 1: Prepare Source Image
|
||||
- Photograph product in white or neutral color
|
||||
- Clean background
|
||||
- Good lighting
|
||||
- High resolution (will be resized)
|
||||
- Save as JPG in `app/images/`
|
||||
|
||||
### Step 2: Determine Regions
|
||||
Open image in image editor and note pixel coordinates:
|
||||
- Frame edges (left, right, top, bottom)
|
||||
- Panels/kickplates
|
||||
- Rails/dividers
|
||||
|
||||
Record as `topLeft [x, y]` and `bottomRight [x, y]`
|
||||
|
||||
### Step 3: Add to Configuration
|
||||
Add entry to `image_configs.json`:
|
||||
```json
|
||||
{
|
||||
"your_product": {
|
||||
"sourceImage": "images/your_product.jpg",
|
||||
"productCode": "YOUR-PRODUCT",
|
||||
"availableColors": {
|
||||
"white": {"rgb": [255, 255, 255], "name": "White"},
|
||||
"black": {"rgb": [30, 30, 30], "name": "Black"}
|
||||
},
|
||||
"colorableRegions": [
|
||||
{
|
||||
"name": "frame",
|
||||
"topLeft": [0, 0],
|
||||
"bottomRight": [100, 1450]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"imageWidth": 720,
|
||||
"imageHeight": 1450,
|
||||
"defaultColor": "white"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Test
|
||||
```bash
|
||||
curl http://localhost:8080/api/product-image/your_product?color=black > test.png
|
||||
```
|
||||
|
||||
## Performance Notes
|
||||
|
||||
### First Request
|
||||
- ~100-500ms (depending on image size and region count)
|
||||
- Includes image loading, processing, and caching
|
||||
|
||||
### Cached Requests
|
||||
- ~10-50ms
|
||||
- Just file system read and serve
|
||||
|
||||
### Memory Usage
|
||||
- Base image kept in memory during processing
|
||||
- Minimal memory footprint when served from cache
|
||||
|
||||
### Optimization Tips
|
||||
1. **Use caching** - Enabled by default
|
||||
2. **Smaller source images** - 1000x1600px is usually sufficient
|
||||
3. **Fewer regions** - Combine adjacent areas when possible
|
||||
4. **CDN** - Serve cached images from CDN in production
|
||||
|
||||
## Production Deployment
|
||||
|
||||
### Recommended Setup
|
||||
1. **Pre-generate** common combinations on deploy
|
||||
2. **CDN** to serve cached images
|
||||
3. **Redis cache** instead of filesystem (optional)
|
||||
4. **Image optimization** - Use WebP format where supported
|
||||
5. **Rate limiting** on generation endpoint
|
||||
|
||||
### Pre-generation Script
|
||||
```python
|
||||
from image_generator import ProductImageGenerator
|
||||
|
||||
generator = ProductImageGenerator()
|
||||
products = ['cobrai', 'product2']
|
||||
colors = ['white', 'black', 'bronze']
|
||||
hinges = ['left', 'right']
|
||||
|
||||
for product in products:
|
||||
for color in colors:
|
||||
for hinge in hinges:
|
||||
img = generator.generate_product_image(product, color, hinge)
|
||||
print(f'Generated: {product} {color} {hinge}')
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "PIL/Pillow not installed"
|
||||
```bash
|
||||
pip install Pillow
|
||||
```
|
||||
|
||||
### "Product not found in configuration"
|
||||
Check that product code in URL matches key in `image_configs.json`
|
||||
|
||||
### "Could not load base image"
|
||||
Verify `sourceImage` path in config and that file exists
|
||||
|
||||
### Colors look wrong
|
||||
Adjust RGB values in `availableColors` section
|
||||
|
||||
### Regions not coloring
|
||||
1. Verify coordinates are within image bounds
|
||||
2. Check that region isn't transparent
|
||||
3. Ensure topLeft is actually top-left of bottomRight
|
||||
|
||||
### Cache not working
|
||||
Check that `cache/product_images/` directory is writable
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Planned Features
|
||||
- [ ] Material textures (wood grain, brushed metal)
|
||||
- [ ] Glass tinting/color
|
||||
- [ ] Shadow/lighting adjustments based on color
|
||||
- [ ] Multiple hardware styles
|
||||
- [ ] Size variations
|
||||
- [ ] Decorative glass patterns
|
||||
- [ ] WebP format support
|
||||
- [ ] Batch generation CLI tool
|
||||
- [ ] Admin UI for region configuration
|
||||
- [ ] Automatic region detection (AI/ML)
|
||||
|
||||
### Integration Ideas
|
||||
- Direct integration with product configurator
|
||||
- Real-time preview as user selects options
|
||||
- Download high-res configured images
|
||||
- Email configured image to customer
|
||||
- Social media sharing with custom image
|
||||
@@ -0,0 +1,81 @@
|
||||
# Workspace Folder Structure
|
||||
|
||||
This workspace is organized into clean, logical folders:
|
||||
|
||||
## 📁 Root Directory
|
||||
|
||||
```
|
||||
├── .env.example # Environment configuration template
|
||||
├── .gitignore # Git ignore rules
|
||||
├── .vscode/ # VS Code settings and tasks
|
||||
├── app/ # 🚀 Active project files (MAIN APPLICATION)
|
||||
└── information/ # 📚 Documentation and deprecated files
|
||||
```
|
||||
|
||||
## 🚀 app/ - Active Project Files
|
||||
|
||||
**All working application code and assets**
|
||||
|
||||
- **app.py** - Main Flask application
|
||||
- **config.py** - Application configuration
|
||||
- **wsgi.py** / **passenger_wsgi.py** - Production WSGI servers
|
||||
- **requirements.txt** - Python dependencies
|
||||
- **parse_csv_to_json.py** - CSV to JSON data processor
|
||||
- **generate_bitwise_helper.py** - Bitwise filtering data generator
|
||||
- **css/** - Stylesheets
|
||||
- **js/** - JavaScript files
|
||||
- **templates/** - HTML templates
|
||||
- **data/** - JSON data files
|
||||
- **images/** - Image assets
|
||||
|
||||
## 📚 information/ - Documentation
|
||||
|
||||
**All project documentation and guides**
|
||||
|
||||
- AI_PARSING_INSTRUCTIONS.md
|
||||
- BITWISE_USAGE_GUIDE.md
|
||||
- QUICKSTART.md
|
||||
- README.md
|
||||
- REQUIRED_CODE_CHANGES.md
|
||||
- START_HERE.md
|
||||
- URL_EXAMPLES.md
|
||||
- URL_IMPLEMENTATION_SUMMARY.md
|
||||
- URL_STATE_MANAGEMENT.md
|
||||
|
||||
### information/deprecated/ - Old Files
|
||||
|
||||
**Deprecated files kept for reference**
|
||||
|
||||
- index.html / index2.html (replaced by templates/)
|
||||
- codes.text (old data)
|
||||
- run.bat (old batch file)
|
||||
- Procfile (old deployment config)
|
||||
- temp/ (temporary files)
|
||||
|
||||
---
|
||||
|
||||
## Working with the Project
|
||||
|
||||
### Running Tasks
|
||||
|
||||
All VS Code tasks are configured to work with the new structure:
|
||||
|
||||
- **Ctrl+Shift+B** - Process All Data (updates JSON from CSV)
|
||||
- **Ctrl+Shift+P** → "Tasks: Run Task" → "Start Flask Server"
|
||||
|
||||
### Updating Data
|
||||
|
||||
When you update `app/data/products.csv`:
|
||||
1. Press **Ctrl+Shift+B** to run "Process All Data"
|
||||
2. This generates fresh JSON files in `app/data/`
|
||||
|
||||
### Starting the Server
|
||||
|
||||
1. Press **Ctrl+Shift+P**
|
||||
2. Type "Tasks: Run Task"
|
||||
3. Select "Start Flask Server"
|
||||
4. Open http://127.0.0.1:8080
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** March 26, 2026
|
||||
@@ -0,0 +1,195 @@
|
||||
# Layered Image System Guide
|
||||
|
||||
## Overview
|
||||
The product finder supports a layered image system that allows dynamic product configuration (changing colors, materials, hinge location, etc.) without requiring a separate photo for every combination.
|
||||
|
||||
## How It Works
|
||||
|
||||
### System Architecture
|
||||
Images are stacked in layers (like Photoshop layers):
|
||||
1. **Base Layer** - House/frame (JPG) - the static background
|
||||
2. **Door Layer** - The door panel (PNG with transparency) - changes with color selection
|
||||
3. **Hardware Layer** - Handle/lock set (PNG with transparency) - can flip for left/right hinge
|
||||
4. **Overlay Layer** - Glass view/decorative elements (PNG with transparency) - optional
|
||||
|
||||
### Fallback Behavior
|
||||
- **No layered config**: Shows the standard flat `image` field
|
||||
- **Layered enabled but missing files**: Shows base layer + warning banner
|
||||
- **User selects unavailable option**: Displays "Preview not available for this configuration"
|
||||
|
||||
## JSON Configuration
|
||||
|
||||
### Standard Product (Flat Image)
|
||||
```json
|
||||
{
|
||||
"productCode": "450",
|
||||
"description": "#450 RAVEN STORM WINDOWS",
|
||||
"image": "images/450.jpg",
|
||||
"colors": ["White", "Black", "Bronze"],
|
||||
"materials": ["Aluminum"]
|
||||
}
|
||||
```
|
||||
|
||||
### Product with Layered Images
|
||||
```json
|
||||
{
|
||||
"productCode": "404",
|
||||
"description": "#404 FALCON STORM WINDOWS",
|
||||
"image": "images/404.jpg",
|
||||
"colors": ["White", "Black", "Bronze", "Sandstone"],
|
||||
"materials": ["Aluminum"],
|
||||
"imageConfig": {
|
||||
"layered": true,
|
||||
"basePath": "images/products/404/",
|
||||
"layers": {
|
||||
"base": "base.jpg",
|
||||
"door": {
|
||||
"white": "door-white.png",
|
||||
"black": "door-black.png",
|
||||
"bronze": "door-bronze.png",
|
||||
"sandstone": "door-sandstone.png"
|
||||
},
|
||||
"hardware": "handle.png",
|
||||
"overlay": {
|
||||
"inside": "view-inside.png",
|
||||
"outside": "view-outside.png"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Partial Layered Images (Some Colors Available)
|
||||
```json
|
||||
{
|
||||
"productCode": "505",
|
||||
"description": "#505 DOOR",
|
||||
"image": "images/505.jpg",
|
||||
"colors": ["White", "Black", "Bronze", "Tan"],
|
||||
"materials": ["Aluminum", "Vinyl"],
|
||||
"imageConfig": {
|
||||
"layered": true,
|
||||
"basePath": "images/products/505/",
|
||||
"layers": {
|
||||
"base": "base.jpg",
|
||||
"door": {
|
||||
"white": "door-white.png",
|
||||
"black": "door-black.png"
|
||||
// Bronze and Tan not available yet - will show warning
|
||||
},
|
||||
"hardware": "handle.png"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## File Structure
|
||||
|
||||
### Recommended Directory Layout
|
||||
```
|
||||
app/images/products/
|
||||
├── 404/
|
||||
│ ├── base.jpg # Frame/house background
|
||||
│ ├── door-white.png # White door panel (transparent BG)
|
||||
│ ├── door-black.png # Black door panel (transparent BG)
|
||||
│ ├── door-bronze.png # Bronze door panel (transparent BG)
|
||||
│ ├── door-sandstone.png # Sandstone door panel (transparent BG)
|
||||
│ ├── handle.png # Hardware (transparent BG)
|
||||
│ ├── view-inside.png # Optional inside view overlay
|
||||
│ └── view-outside.png # Optional outside view overlay
|
||||
├── 450/
|
||||
│ ├── base.jpg
|
||||
│ └── door-white.png
|
||||
└── [other products]/
|
||||
```
|
||||
|
||||
## Creating Layered Images
|
||||
|
||||
### Requirements
|
||||
- **Base image**: JPG format, includes frame, glass, background
|
||||
- **Layer images**: PNG format with transparency
|
||||
- **Consistent dimensions**: All layers for a product should be the same size
|
||||
- **Alignment**: Layers must align perfectly when stacked
|
||||
|
||||
### Photoshop/GIMP Workflow
|
||||
1. Start with full product photo
|
||||
2. Create separate layers for each component
|
||||
3. Remove background from door/hardware layers
|
||||
4. Export:
|
||||
- Base layer → JPG
|
||||
- Component layers → PNG (with transparency)
|
||||
5. Create color variants by adjusting door layer
|
||||
|
||||
### Photography Tips
|
||||
- Use consistent lighting
|
||||
- Photograph against neutral background (for easy removal)
|
||||
- Keep camera/product position identical for all shots
|
||||
- Consider photographing white version first, then recolor digitally
|
||||
|
||||
## Features
|
||||
|
||||
### Auto-Selection
|
||||
- If product has only one color: auto-selected and dropdown disabled
|
||||
- If no colors available: shows "N/A" and disabled
|
||||
|
||||
### Hinge Location
|
||||
- Right/Left hinge radio buttons flip the hardware layer horizontally
|
||||
- Works with both flat and layered images
|
||||
|
||||
### Dynamic Updates
|
||||
- Color changes update the door layer instantly
|
||||
- Missing images show warning instead of breaking
|
||||
|
||||
### Sorting
|
||||
- Materials: Alphabetically sorted
|
||||
- Colors: Alphabetically sorted with White always at bottom
|
||||
|
||||
## Testing Your Setup
|
||||
|
||||
### 1. Test Flat Fallback
|
||||
Set `"layered": false` or remove `imageConfig` entirely - should show standard image
|
||||
|
||||
### 2. Test Missing Layer
|
||||
Remove a color file - should show base + warning banner
|
||||
|
||||
### 3. Test All Colors
|
||||
Select each color - should swap door layer smoothly
|
||||
|
||||
### 4. Test Hinge Flip
|
||||
Toggle left/right hinge - hardware should flip horizontally
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Images not showing
|
||||
- Check file paths in `basePath` and layer filenames
|
||||
- Verify files exist in `app/images/products/[code]/`
|
||||
- Check browser console for 404 errors
|
||||
|
||||
### Colors not matching
|
||||
- Ensure color keys in JSON match available color names
|
||||
- Keys should be lowercase in the door config (e.g., `"white"` not `"White"`)
|
||||
|
||||
### Layers misaligned
|
||||
- All images must be same dimensions
|
||||
- Check that transparent PNGs aren't cropped differently
|
||||
|
||||
### Warning banner always showing
|
||||
- Verify the selected color exists in `layers.door` object
|
||||
- Check that color value from dropdown matches JSON key
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
### Phase 1: Keep Flat Images
|
||||
Keep existing flat images as fallback while creating layered versions
|
||||
|
||||
### Phase 2: Add Layered for Key Products
|
||||
Focus on best-selling products first, add `imageConfig` gradually
|
||||
|
||||
### Phase 3: Full Migration
|
||||
Once all images ready, can remove flat images (but recommend keeping as fallback)
|
||||
|
||||
## Performance Notes
|
||||
- PNG layers are cached by browser
|
||||
- Base image loads once, only door layer changes on color switch
|
||||
- Much smaller file size than separate photos for each combination
|
||||
- Example: Instead of 4 full photos (1MB each = 4MB), use 1 base (800KB) + 4 doors (200KB each = 800KB) = 1.6MB total
|
||||
@@ -0,0 +1,312 @@
|
||||
# Login System Documentation
|
||||
|
||||
## Overview
|
||||
A complete authentication system for the CGW Product Finder that integrates with the user management system. Users must log in with their credentials to access the product finder application.
|
||||
|
||||
## Features
|
||||
|
||||
### 🔐 Secure Authentication
|
||||
- Password verification using PBKDF2-SHA256 hashing
|
||||
- Session-based authentication with HTTP-only cookies
|
||||
- Automatic session management
|
||||
- Active/inactive user account checking
|
||||
|
||||
### 📍 Multi-Location Support
|
||||
- Automatic location selection for single-location users
|
||||
- Location selection page for users with multiple accessible locations
|
||||
- Default location preference
|
||||
- Location-based access control
|
||||
|
||||
### 🛡️ Security Features
|
||||
- Login required decorator protects all main routes
|
||||
- Inactive accounts are automatically blocked
|
||||
- Sessions expire on logout or server restart
|
||||
- Secure session cookies (HTTP-only, SameSite)
|
||||
|
||||
## User Flow
|
||||
|
||||
### 1. Login Process
|
||||
1. User visits the app → Redirected to `/login`
|
||||
2. User enters **username** and **password**
|
||||
3. System validates credentials against `users.json`
|
||||
4. System checks if user account is **active**
|
||||
5. If valid and active:
|
||||
- Session is created
|
||||
- User accessible locations are loaded
|
||||
|
||||
### 2. Location Selection (if applicable)
|
||||
- **Single Accessible Location**: User goes directly to main app (location selection bypassed)
|
||||
- **Multiple Accessible Locations**: User is redirected to `/select-location`
|
||||
- Shows all accessible locations
|
||||
- Default location is pre-selected
|
||||
- User can choose their working location
|
||||
- Selection is saved to session
|
||||
- **Note**: Default location is automatically marked as accessible when user is created
|
||||
|
||||
### 3. Main Application Access
|
||||
- User accesses the Product Finder
|
||||
- User info displayed in header (username + current location)
|
||||
- Logout button available in header
|
||||
|
||||
## Routes
|
||||
|
||||
### Public Routes (No Login Required)
|
||||
- `GET /login` - Login page
|
||||
- `POST /api/login` - Login endpoint
|
||||
- `GET /users` - User management page
|
||||
|
||||
### Protected Routes (Login Required)
|
||||
- `GET /` - Main product finder app
|
||||
- `GET /quiz` - Quiz page (alias for main app)
|
||||
- `GET /image-test` - Image generation test page
|
||||
- `GET /select-location` - Location selection page
|
||||
- `POST /api/select-location` - Set current location
|
||||
|
||||
### Session Routes
|
||||
- `GET /api/session` - Get current session info
|
||||
- `POST /api/logout` - Logout and clear session
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### POST /api/login
|
||||
Authenticate user and create session.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"username": "john_doe",
|
||||
"password": "password123"
|
||||
}
|
||||
```
|
||||
|
||||
**Success Response:**
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Login successful",
|
||||
"requiresLocationSelection": true
|
||||
}
|
||||
```
|
||||
|
||||
**Error Responses:**
|
||||
```json
|
||||
{
|
||||
"status": "error",
|
||||
"message": "Invalid username or password"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "error",
|
||||
"message": "Account is inactive. Please contact an administrator."
|
||||
}
|
||||
```
|
||||
|
||||
### GET /api/session
|
||||
Get current user session information.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"username": "john_doe",
|
||||
"defaultLocation": "LINDS",
|
||||
"currentLocation": "KC",
|
||||
"accessibleLocations": ["LINDS", "KC", "IOLA"]
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/select-location
|
||||
Select a location for the current session.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"location": "KC"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Location selected",
|
||||
"currentLocation": "KC"
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/logout
|
||||
Log out and clear session.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Logged out successfully"
|
||||
}
|
||||
```
|
||||
|
||||
## Session Data
|
||||
|
||||
The session stores:
|
||||
- `user_id`: Index of user in users.json
|
||||
- `username`: Username string
|
||||
- `defaultLocation`: User's default location code
|
||||
- `currentLocation`: Currently selected location code
|
||||
- `accessibleLocations`: Array of location codes user can access
|
||||
|
||||
## Authentication Decorator
|
||||
|
||||
The `@login_required` decorator protects routes:
|
||||
|
||||
```python
|
||||
@app.route('/protected-page')
|
||||
@login_required
|
||||
def protected_page():
|
||||
return render_template('protected.html')
|
||||
```
|
||||
|
||||
The decorator:
|
||||
1. Checks if user is logged in (has `user_id` in session)
|
||||
2. Validates user still exists in users.json
|
||||
3. Checks if user account is still active
|
||||
4. Redirects to login if any check fails
|
||||
|
||||
## Getting Current User in Routes
|
||||
|
||||
```python
|
||||
@app.route('/my-route')
|
||||
@login_required
|
||||
def my_route():
|
||||
user = get_current_user()
|
||||
username = session.get('username')
|
||||
current_location = session.get('currentLocation')
|
||||
|
||||
# Use user data...
|
||||
return render_template('page.html')
|
||||
```
|
||||
|
||||
## Location-Based Access Control
|
||||
|
||||
Users can only access locations they have permission for:
|
||||
|
||||
```python
|
||||
# In users.json
|
||||
{
|
||||
"username": "john_doe",
|
||||
"locationSettings": {
|
||||
"LINDS": { "accessible": true },
|
||||
"IOLA": { "accessible": false },
|
||||
"KC": { "accessible": true },
|
||||
"BMD": { "accessible": false }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This user can access:
|
||||
- ✅ Lindsborg (LINDS)
|
||||
- ✅ KC (KC)
|
||||
- ❌ Iola (IOLA)
|
||||
- ❌ BMD (BMD)
|
||||
|
||||
## User Interface Components
|
||||
|
||||
### Login Page (`/login`)
|
||||
- Clean, centered login form
|
||||
- Username and password fields
|
||||
- Submit button with loading spinner
|
||||
- Link to User Management page
|
||||
- Error message display
|
||||
|
||||
### Location Selection Page (`/select-location`)
|
||||
- Shows logged-in username
|
||||
- Shows default location
|
||||
- Radio buttons for each accessible location
|
||||
- Default location is pre-selected
|
||||
- Continue and Logout buttons
|
||||
|
||||
### Main App Header
|
||||
- User info display: `👤 username | 📍 location`
|
||||
- Logout button in header
|
||||
- Positioned in top-right corner
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Password Security
|
||||
- Passwords are hashed using PBKDF2-SHA256
|
||||
- Hashes are never reversed or displayed
|
||||
- Hash verification happens server-side only
|
||||
|
||||
### Session Security
|
||||
- Sessions use secure random keys
|
||||
- Cookies are HTTP-only (not accessible via JavaScript)
|
||||
- SameSite cookie policy prevents CSRF attacks
|
||||
- Sessions cleared on logout
|
||||
|
||||
### Account Status
|
||||
- Inactive accounts cannot log in
|
||||
- If account is deactivated while logged in, next request will log them out
|
||||
- User must have at least one accessible location
|
||||
|
||||
## Testing the Login System
|
||||
|
||||
### Test User Creation
|
||||
1. Go to `/users`
|
||||
2. Create a test user:
|
||||
- Username: `testuser`
|
||||
- Password: `password123`
|
||||
- Default Location: Lindsborg
|
||||
- Check "Accessible" for Lindsborg and KC
|
||||
|
||||
### Test Login Flow
|
||||
1. Go to `/` (should redirect to `/login`)
|
||||
2. Enter credentials: `testuser` / `password123`
|
||||
3. Click "Sign In"
|
||||
4. Since user has 2 accessible locations → redirected to `/select-location`
|
||||
5. Choose a location and click "Continue"
|
||||
6. Now viewing main Product Finder app
|
||||
7. See user info in header
|
||||
8. Click "Logout" to end session
|
||||
|
||||
### Test Single Location User
|
||||
1. Create user with only 1 accessible location
|
||||
2. Log in
|
||||
3. Should go directly to main app (skip location selection)
|
||||
|
||||
### Test Inactive User
|
||||
1. Create and log in as a user
|
||||
2. In User Management, toggle user to "Inactive"
|
||||
3. Try to log in → Should see "Account is inactive" message
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Please log in first" on all pages
|
||||
- Session may have expired
|
||||
- Server may have restarted (sessions are in-memory)
|
||||
- Clear browser cookies and log in again
|
||||
|
||||
### "User not found" error
|
||||
- User may have been deleted while logged in
|
||||
- Log out and log back in
|
||||
|
||||
### Can't access certain locations
|
||||
- Check user's "Accessible" checkboxes in User Management
|
||||
- User must have at least one accessible location
|
||||
|
||||
### Stuck on location selection page
|
||||
- User must have multiple accessible locations
|
||||
- If this shouldn't happen, check user's location settings
|
||||
- Or click "Sign Out" and contact administrator
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential additions:
|
||||
- [ ] Remember me checkbox (persistent sessions)
|
||||
- [ ] Password reset functionality
|
||||
- [ ] Session timeout after inactivity
|
||||
- [ ] Login attempt limiting (brute force protection)
|
||||
- [ ] Two-factor authentication
|
||||
- [ ] Session management dashboard
|
||||
- [ ] Location switching without re-login
|
||||
- [ ] Audit log of login attempts
|
||||
@@ -0,0 +1,264 @@
|
||||
# WordPress-Style Permission System - Quick Reference
|
||||
|
||||
## ✅ What Was Implemented
|
||||
|
||||
The CGW Product Finder now has a complete WordPress-style permission system with:
|
||||
|
||||
1. **Permission Checking Functions** (Backend - Python)
|
||||
- `can_user(permission, location=None)` - Check single permission
|
||||
- `user_has_any_permission(permissions, location=None)` - Check if user has ANY permission
|
||||
- `user_has_all_permissions(permissions, location=None)` - Check if user has ALL permissions
|
||||
- `get_user_permissions(location=None)` - Get all user permissions
|
||||
- `@permission_required(permission, location=None)` - Route decorator for permission protection
|
||||
|
||||
2. **Permission Check Endpoints** (Frontend - API)
|
||||
- `POST /api/check-permission` - Check if user has specific permission
|
||||
- `GET /api/user-permissions` - Get all user permissions
|
||||
|
||||
3. **Access Denial**
|
||||
- Beautiful access denied page at `templates/access_denied.html`
|
||||
- Shows required permission and helpful navigation
|
||||
|
||||
4. **Protected Routes**
|
||||
- `/users` - User management page (requires `manage_users`)
|
||||
- `/api/users` (GET, POST, PATCH, DELETE) - All user management endpoints protected
|
||||
|
||||
5. **User Data Structure**
|
||||
- Global permissions: `user.permissions`
|
||||
- Location-specific permissions: `user.locationSettings[LOCATION].permissions`
|
||||
|
||||
## 🚀 Quick Start Usage
|
||||
|
||||
### Backend (Python)
|
||||
|
||||
```python
|
||||
# Check permission
|
||||
if can_user('create_quotes'):
|
||||
# User can create quotes
|
||||
pass
|
||||
|
||||
# Protect a route
|
||||
@app.route('/admin/reports')
|
||||
@login_required
|
||||
@permission_required('view_reports')
|
||||
def admin_reports():
|
||||
return render_template('reports.html')
|
||||
|
||||
# Check at specific location
|
||||
if can_user('manage_inventory', location='LINDS'):
|
||||
# User can manage inventory at Lindsborg
|
||||
pass
|
||||
```
|
||||
|
||||
### Frontend (JavaScript)
|
||||
|
||||
```javascript
|
||||
// Check single permission
|
||||
const response = await fetch('/api/check-permission', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ permission: 'create_quotes' })
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.hasPermission) {
|
||||
// Show create button
|
||||
}
|
||||
|
||||
// Get all permissions
|
||||
const response = await fetch('/api/user-permissions');
|
||||
const data = await response.json();
|
||||
console.log(data.permissions); // { manage_users: true, create_quotes: true, ... }
|
||||
```
|
||||
|
||||
## 📁 Files Modified/Created
|
||||
|
||||
### Created:
|
||||
- `app/templates/access_denied.html` - Access denial page
|
||||
- `app/PERMISSIONS_SYSTEM.md` - Comprehensive documentation
|
||||
|
||||
### Modified:
|
||||
- `app/app.py` - Added permission checking functions and protected routes
|
||||
- `app/data/users.json` - Added permissions to existing users
|
||||
- `app/data/example_user_structure.json` - Updated with permission examples
|
||||
|
||||
## 👥 Current Users & Permissions
|
||||
|
||||
### Master (Admin)
|
||||
- **Password**: Master
|
||||
- **Location**: KC (access to all locations)
|
||||
- **Permissions**: Full access
|
||||
- manage_users ✅
|
||||
- view_reports ✅
|
||||
- create_quotes ✅
|
||||
- approve_quotes ✅
|
||||
- manage_products ✅
|
||||
- manage_inventory ✅
|
||||
|
||||
### Darlene (Standard User)
|
||||
- **Password**: Darlene
|
||||
- **Location**: IOLA (access to LINDS, IOLA, KC)
|
||||
- **Permissions**: Limited access
|
||||
- manage_users ✅
|
||||
- view_reports ✅
|
||||
- create_quotes ✅
|
||||
- approve_quotes ❌
|
||||
- manage_products ❌
|
||||
- manage_inventory ❌
|
||||
|
||||
## 🔒 Permission Hierarchy
|
||||
|
||||
```
|
||||
Check Order:
|
||||
1. Is user logged in? → If no: return False
|
||||
2. Is user active? → If no: return False
|
||||
3. Check global permissions (user.permissions) → If found: return value
|
||||
4. Check location-specific permissions → If found: return value
|
||||
5. Default: return False
|
||||
```
|
||||
|
||||
## 🎯 Common Permission Names
|
||||
|
||||
Recommended permissions for your system:
|
||||
|
||||
**User Management:**
|
||||
- `manage_users` - Create, edit, delete users (already implemented)
|
||||
- `view_users` - View user list
|
||||
- `reset_passwords` - Reset passwords
|
||||
|
||||
**Products & Inventory:**
|
||||
- `manage_products` - Add/edit/delete products
|
||||
- `view_products` - View product catalog
|
||||
- `manage_inventory` - Adjust inventory
|
||||
- `view_inventory` - View inventory
|
||||
|
||||
**Quotes & Orders:**
|
||||
- `create_quotes` - Create quotes
|
||||
- `view_quotes` - View quotes
|
||||
- `approve_quotes` - Approve/reject quotes
|
||||
- `edit_quotes` - Edit quotes
|
||||
|
||||
**Reports:**
|
||||
- `view_reports` - Access reports
|
||||
- `export_data` - Export data
|
||||
- `view_analytics` - View analytics
|
||||
|
||||
## 🧪 Testing the System
|
||||
|
||||
### Test 1: User Management Access
|
||||
```bash
|
||||
1. Start Flask server: python app/app.py
|
||||
2. Login as Master (password: Master)
|
||||
3. Navigate to /users
|
||||
4. Should see user management interface ✅
|
||||
```
|
||||
|
||||
### Test 2: Permission Denied
|
||||
```bash
|
||||
1. Create a new user without manage_users permission
|
||||
2. Login as that user
|
||||
3. Navigate to /users
|
||||
4. Should see "Access Denied" page ✅
|
||||
```
|
||||
|
||||
### Test 3: API Permission Check
|
||||
```bash
|
||||
# In browser console after login:
|
||||
const response = await fetch('/api/check-permission', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({permission: 'manage_users'})
|
||||
});
|
||||
const data = await response.json();
|
||||
console.log(data.hasPermission); // Should be true for Master
|
||||
```
|
||||
|
||||
### Test 4: Get All Permissions
|
||||
```bash
|
||||
# In browser console after login:
|
||||
const response = await fetch('/api/user-permissions');
|
||||
const data = await response.json();
|
||||
console.log(data.permissions); // Should show all user's permissions
|
||||
```
|
||||
|
||||
## 📝 Next Steps
|
||||
|
||||
To add permissions to new features:
|
||||
|
||||
1. **Define Permission Name**
|
||||
```python
|
||||
# Choose a descriptive name like 'create_quotes'
|
||||
```
|
||||
|
||||
2. **Protect Backend Route**
|
||||
```python
|
||||
@app.route('/quotes/new')
|
||||
@login_required
|
||||
@permission_required('create_quotes')
|
||||
def new_quote():
|
||||
return render_template('new_quote.html')
|
||||
```
|
||||
|
||||
3. **Check in Code**
|
||||
```python
|
||||
if can_user('create_quotes'):
|
||||
# Allow quote creation
|
||||
```
|
||||
|
||||
4. **Hide/Show Frontend Elements**
|
||||
```javascript
|
||||
const perms = await fetch('/api/user-permissions').then(r => r.json());
|
||||
if (perms.permissions.create_quotes) {
|
||||
document.getElementById('createBtn').style.display = 'block';
|
||||
}
|
||||
```
|
||||
|
||||
5. **Add to User Data**
|
||||
```json
|
||||
{
|
||||
"username": "user",
|
||||
"permissions": {
|
||||
"create_quotes": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🛠️ Troubleshooting
|
||||
|
||||
**Access Denied even with permission:**
|
||||
- Check spelling of permission name (case-sensitive)
|
||||
- Verify user is active in users.json
|
||||
- Clear browser cookies and re-login
|
||||
- Check server logs for errors
|
||||
|
||||
**Permission check returns False:**
|
||||
- Ensure user is logged in
|
||||
- Verify permission exists in users.json
|
||||
- Check if using correct location context
|
||||
- Confirm session is valid
|
||||
|
||||
**Frontend shows button but backend denies:**
|
||||
- This is correct! Frontend checks are for UX only
|
||||
- Backend always enforces permissions
|
||||
- Never trust client-side permission checks
|
||||
|
||||
## 📚 Full Documentation
|
||||
|
||||
See `app/PERMISSIONS_SYSTEM.md` for complete documentation including:
|
||||
- Detailed examples
|
||||
- Best practices
|
||||
- Security notes
|
||||
- Migration guide
|
||||
- Advanced usage patterns
|
||||
|
||||
## 🎉 Summary
|
||||
|
||||
You now have a fully functional WordPress-style permission system that allows:
|
||||
- ✅ Fine-grained access control
|
||||
- ✅ Global and location-specific permissions
|
||||
- ✅ Easy permission checks in code
|
||||
- ✅ Protected routes with decorators
|
||||
- ✅ Frontend permission checking
|
||||
- ✅ Beautiful access denied pages
|
||||
- ✅ Flexible permission inheritance
|
||||
|
||||
The system is secure, scalable, and follows WordPress best practices!
|
||||
@@ -0,0 +1,376 @@
|
||||
# Permission System Documentation
|
||||
|
||||
The CGW Product Finder uses a WordPress-style permission system that allows fine-grained control over what users can do both globally and at specific locations.
|
||||
|
||||
## Overview
|
||||
|
||||
Permissions can be set at two levels:
|
||||
1. **Global Permissions**: Apply across all locations (stored in user's `permissions` field)
|
||||
2. **Location-Specific Permissions**: Apply only at specific locations (stored in `locationSettings[LOCATION].permissions`)
|
||||
|
||||
Permission checks follow this hierarchy:
|
||||
- First checks global permissions
|
||||
- Then checks location-specific permissions
|
||||
- Location-specific permissions can override global permissions
|
||||
- If a permission isn't found anywhere, it defaults to `false`
|
||||
|
||||
## User Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "john_doe",
|
||||
"password": "pbkdf2:sha256:1000000$...",
|
||||
"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": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Backend Usage (Python)
|
||||
|
||||
### Checking Permissions in Code
|
||||
|
||||
```python
|
||||
from app import can_user
|
||||
|
||||
# Check if user has permission at current location
|
||||
if can_user('create_quotes'):
|
||||
# User can create quotes
|
||||
pass
|
||||
|
||||
# Check if user has permission at specific location
|
||||
if can_user('manage_inventory', location='LINDS'):
|
||||
# User can manage inventory at Lindsborg
|
||||
pass
|
||||
|
||||
# Check only global permissions (ignore location-specific)
|
||||
if can_user('manage_users', location='global'):
|
||||
# User has global user management permission
|
||||
pass
|
||||
```
|
||||
|
||||
### Protecting Routes with Decorators
|
||||
|
||||
```python
|
||||
from app import permission_required, login_required
|
||||
|
||||
@app.route('/admin/users')
|
||||
@login_required
|
||||
@permission_required('manage_users')
|
||||
def admin_users():
|
||||
"""Only users with manage_users permission can access"""
|
||||
return render_template('admin_users.html')
|
||||
|
||||
# Check permission at specific location
|
||||
@app.route('/inventory/<location>')
|
||||
@login_required
|
||||
@permission_required('manage_inventory', location_param='location')
|
||||
def location_inventory(location):
|
||||
"""Permission checked for the location in URL parameter"""
|
||||
return render_template('inventory.html')
|
||||
```
|
||||
|
||||
### Multiple Permission Checks
|
||||
|
||||
```python
|
||||
from app import user_has_any_permission, user_has_all_permissions
|
||||
|
||||
# Check if user has ANY of these permissions
|
||||
if user_has_any_permission(['create_quotes', 'approve_quotes']):
|
||||
# User can either create OR approve quotes
|
||||
pass
|
||||
|
||||
# Check if user has ALL of these permissions
|
||||
if user_has_all_permissions(['manage_users', 'view_reports']):
|
||||
# User has both permissions
|
||||
pass
|
||||
```
|
||||
|
||||
### Getting All User Permissions
|
||||
|
||||
```python
|
||||
from app import get_user_permissions
|
||||
|
||||
# Get all permissions (global + current location)
|
||||
permissions = get_user_permissions()
|
||||
# Returns: {'manage_users': True, 'create_quotes': True, ...}
|
||||
|
||||
# Get permissions for specific location
|
||||
permissions = get_user_permissions(location='LINDS')
|
||||
|
||||
# Get only global permissions
|
||||
permissions = get_user_permissions(location='global')
|
||||
```
|
||||
|
||||
## Frontend Usage (JavaScript)
|
||||
|
||||
### Checking Single Permission
|
||||
|
||||
```javascript
|
||||
async function checkPermission(permissionName, location = null) {
|
||||
try {
|
||||
const response = await fetch('/api/check-permission', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
permission: permissionName,
|
||||
location: location // Optional
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
return data.hasPermission;
|
||||
} catch (error) {
|
||||
console.error('Error checking permission:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
if (await checkPermission('create_quotes')) {
|
||||
// Show create quote button
|
||||
document.getElementById('createQuoteBtn').style.display = 'block';
|
||||
}
|
||||
```
|
||||
|
||||
### Getting All User Permissions
|
||||
|
||||
```javascript
|
||||
async function getUserPermissions(location = null) {
|
||||
try {
|
||||
const url = location
|
||||
? `/api/user-permissions?location=${location}`
|
||||
: '/api/user-permissions';
|
||||
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
return data.permissions;
|
||||
} catch (error) {
|
||||
console.error('Error fetching permissions:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const permissions = await getUserPermissions();
|
||||
if (permissions.manage_users) {
|
||||
// Show admin menu
|
||||
}
|
||||
```
|
||||
|
||||
### Show/Hide Elements Based on Permissions
|
||||
|
||||
```javascript
|
||||
async function initializePermissions() {
|
||||
const permissions = await getUserPermissions();
|
||||
|
||||
// Show/hide elements
|
||||
document.querySelectorAll('[data-permission]').forEach(element => {
|
||||
const requiredPermission = element.dataset.permission;
|
||||
if (!permissions[requiredPermission]) {
|
||||
element.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// In HTML:
|
||||
// <button data-permission="create_quotes">Create Quote</button>
|
||||
// <div data-permission="manage_users">Admin Panel</div>
|
||||
```
|
||||
|
||||
## Common Permissions
|
||||
|
||||
Here are some suggested permission names for the Product Finder:
|
||||
|
||||
### User Management
|
||||
- `manage_users` - Create, edit, delete users
|
||||
- `view_users` - View user list
|
||||
- `reset_passwords` - Reset other users' passwords
|
||||
|
||||
### Product & Inventory
|
||||
- `manage_products` - Add/edit/delete products
|
||||
- `view_products` - View product catalog
|
||||
- `manage_inventory` - Adjust inventory levels
|
||||
- `view_inventory` - View inventory levels
|
||||
|
||||
### Quotes & Orders
|
||||
- `create_quotes` - Create new quotes
|
||||
- `view_quotes` - View quotes
|
||||
- `approve_quotes` - Approve/reject quotes
|
||||
- `edit_quotes` - Edit existing quotes
|
||||
- `delete_quotes` - Delete quotes
|
||||
|
||||
### Reports & Data
|
||||
- `view_reports` - Access reporting tools
|
||||
- `export_data` - Export data to CSV/Excel
|
||||
- `view_analytics` - View analytics dashboard
|
||||
|
||||
### System Settings
|
||||
- `manage_settings` - Change system settings
|
||||
- `manage_locations` - Add/edit location settings
|
||||
- `view_logs` - View system logs
|
||||
|
||||
## Permission Flow Examples
|
||||
|
||||
### Example 1: Creating a Quote
|
||||
|
||||
```python
|
||||
@app.route('/api/quotes', methods=['POST'])
|
||||
@login_required
|
||||
@permission_required('create_quotes')
|
||||
def create_quote():
|
||||
# User needs create_quotes permission at their current location
|
||||
data = request.json
|
||||
# Create quote logic...
|
||||
return jsonify({'status': 'success'})
|
||||
```
|
||||
|
||||
### Example 2: Approving Quotes (Location-Specific)
|
||||
|
||||
```python
|
||||
@app.route('/api/quotes/<quote_id>/approve', methods=['POST'])
|
||||
@login_required
|
||||
def approve_quote(quote_id):
|
||||
# Check permission at the quote's location
|
||||
quote = get_quote(quote_id)
|
||||
|
||||
if not can_user('approve_quotes', location=quote['location']):
|
||||
return render_template('access_denied.html',
|
||||
required_permission='approve_quotes'), 403
|
||||
|
||||
# Approve quote logic...
|
||||
return jsonify({'status': 'success'})
|
||||
```
|
||||
|
||||
### Example 3: Multi-Location Access
|
||||
|
||||
```python
|
||||
@app.route('/api/inventory/transfer', methods=['POST'])
|
||||
@login_required
|
||||
def transfer_inventory():
|
||||
data = request.json
|
||||
from_location = data['from_location']
|
||||
to_location = data['to_location']
|
||||
|
||||
# User must have manage_inventory at BOTH locations
|
||||
if not user_has_all_permissions(['manage_inventory'], location=from_location):
|
||||
return jsonify({'error': 'No permission at source location'}), 403
|
||||
|
||||
if not user_has_all_permissions(['manage_inventory'], location=to_location):
|
||||
return jsonify({'error': 'No permission at destination location'}), 403
|
||||
|
||||
# Transfer logic...
|
||||
return jsonify({'status': 'success'})
|
||||
```
|
||||
|
||||
## Access Denied Page
|
||||
|
||||
When a user lacks permission, they see an access denied page that shows:
|
||||
- Clear "Access Denied" message
|
||||
- The specific permission that was required
|
||||
- Options to go back or return home
|
||||
- Contact information for requesting access
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Be Specific**: Use descriptive permission names like `create_quotes` instead of `quotes`
|
||||
2. **Granular Control**: Separate permissions (create, view, edit, delete) rather than one "manage" permission
|
||||
3. **Check Early**: Check permissions at route level with decorators when possible
|
||||
4. **Check Often**: Re-check permissions before critical operations, not just at page load
|
||||
5. **Fail Secure**: Default to denying access if permission isn't explicitly granted
|
||||
6. **Location Context**: Always consider whether a permission should be global or location-specific
|
||||
7. **UI Feedback**: Hide/disable UI elements users can't use based on permissions
|
||||
8. **Clear Errors**: Show helpful error messages when permission is denied
|
||||
|
||||
## Adding New Permissions
|
||||
|
||||
To add a new permission:
|
||||
|
||||
1. **Define the permission** in your data structure (add to user's `permissions` or `locationSettings[LOCATION].permissions`)
|
||||
2. **Protect routes** with `@permission_required('new_permission')`
|
||||
3. **Check in code** with `can_user('new_permission')`
|
||||
4. **Update frontend** to show/hide elements based on permission
|
||||
5. **Document** the permission in this file
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Permission check returns False but user should have access
|
||||
- Check if permission is spelled correctly (case-sensitive)
|
||||
- Verify user is logged in (`session['user_id']` exists)
|
||||
- Check user's `active` status
|
||||
- Verify permission exists in either global or location-specific permissions
|
||||
- Check if using correct location (current vs specific vs global)
|
||||
|
||||
### Access denied page shows even for users with permission
|
||||
- Ensure decorators are in correct order: `@login_required` before `@permission_required`
|
||||
- Check that permission name matches exactly
|
||||
- Verify user data was saved correctly in users.json
|
||||
- Clear browser cache/cookies if session is stale
|
||||
|
||||
### Frontend shows elements but backend denies access
|
||||
- Frontend permission checks are for UX only - always enforce in backend
|
||||
- Make sure frontend is checking the same permission name
|
||||
- Ensure frontend is checking at the same location context
|
||||
|
||||
## Security Notes
|
||||
|
||||
- **Never trust frontend permission checks** - they're for UI only
|
||||
- **Always validate permissions on the backend** before performing operations
|
||||
- **Session security** - permissions are loaded from session, which is server-side
|
||||
- **Password hashing** - uses PBKDF2-SHA256 with 1M iterations
|
||||
- **HTTP-only cookies** - session cookies cannot be accessed by JavaScript
|
||||
- **Permission inheritance** - location-specific permissions override global ones
|
||||
|
||||
## Migration Guide
|
||||
|
||||
If you have existing users without permissions, you can add default permissions:
|
||||
|
||||
```python
|
||||
import json
|
||||
|
||||
def add_default_permissions():
|
||||
with open('data/users.json', 'r') as f:
|
||||
users = json.load(f)
|
||||
|
||||
for user in users:
|
||||
# Add global permissions if missing
|
||||
if 'permissions' not in user:
|
||||
user['permissions'] = {
|
||||
'view_products': True,
|
||||
'create_quotes': True,
|
||||
'manage_users': False # Admin only
|
||||
}
|
||||
|
||||
# Add location permissions if missing
|
||||
for location in user.get('locationSettings', {}):
|
||||
if 'permissions' not in user['locationSettings'][location]:
|
||||
user['locationSettings'][location]['permissions'] = {
|
||||
'manage_inventory': False,
|
||||
'approve_quotes': False
|
||||
}
|
||||
|
||||
with open('data/users.json', 'w') as f:
|
||||
json.dump(users, f, indent=2)
|
||||
```
|
||||
@@ -0,0 +1,334 @@
|
||||
# /product-finder Deployment Fix Guide
|
||||
|
||||
## 🔴 Problem
|
||||
|
||||
The application works locally at `http://localhost:8080/` but fails on the server at `https://columbiawindows.com/product-finder/`.
|
||||
|
||||
The redirect from `/product-finder` → `/product-finder/login` works, but then the login page or subsequent routes fail.
|
||||
|
||||
## ✅ Solution Applied
|
||||
|
||||
Three critical changes were made to fix subdirectory deployment:
|
||||
|
||||
### 1. **Added PrefixMiddleware to app.py**
|
||||
|
||||
This middleware tells Flask about the `/product-finder` prefix by setting `SCRIPT_NAME` in the WSGI environment:
|
||||
|
||||
```python
|
||||
class PrefixMiddleware:
|
||||
"""Middleware to handle subdirectory deployments"""
|
||||
def __init__(self, app, prefix=''):
|
||||
self.app = app
|
||||
self.prefix = prefix.rstrip('/')
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
if self.prefix and self.prefix != '/':
|
||||
path = environ.get('PATH_INFO', '')
|
||||
script_name = environ.get('SCRIPT_NAME', '')
|
||||
|
||||
if not script_name.startswith(self.prefix):
|
||||
environ['SCRIPT_NAME'] = self.prefix + script_name
|
||||
if path.startswith(self.prefix):
|
||||
environ['PATH_INFO'] = path[len(self.prefix):]
|
||||
|
||||
return self.app(environ, start_response)
|
||||
```
|
||||
|
||||
This is automatically applied when `APPLICATION_ROOT` is set.
|
||||
|
||||
### 2. **Updated passenger_wsgi.py**
|
||||
|
||||
Sets the `APPLICATION_ROOT` environment variable before importing the app:
|
||||
|
||||
```python
|
||||
if 'APPLICATION_ROOT' not in os.environ:
|
||||
os.environ['APPLICATION_ROOT'] = '/product-finder'
|
||||
```
|
||||
|
||||
### 3. **Updated config.py**
|
||||
|
||||
Production configuration now defaults to `/product-finder`:
|
||||
|
||||
```python
|
||||
class ProductionConfig(Config):
|
||||
APPLICATION_ROOT = os.environ.get('APPLICATION_ROOT', '/product-finder')
|
||||
```
|
||||
|
||||
Development still uses `/` for local testing.
|
||||
|
||||
## 📤 Files to Upload
|
||||
|
||||
Upload these updated files to your server:
|
||||
|
||||
1. **`app/app.py`** - Contains PrefixMiddleware
|
||||
2. **`app/passenger_wsgi.py`** - Sets APPLICATION_ROOT env var
|
||||
3. **`app/config.py`** - Production defaults to /product-finder
|
||||
4. **`app/.htaccess`** - Apache configuration (new file)
|
||||
5. **All template files** - Already updated with url_for() and BASE_URL
|
||||
|
||||
## 🔧 Server Configuration
|
||||
|
||||
### Option A: Using .htaccess (Recommended)
|
||||
|
||||
The `.htaccess` file is already configured for `/product-finder`. Upload it to your `app/` folder on the server.
|
||||
|
||||
**Important:** Update these lines in `.htaccess`:
|
||||
```apache
|
||||
PassengerAppRoot /home/USERNAME/public_html/product-finder
|
||||
PassengerPython /usr/bin/python3 # Update to your Python path
|
||||
```
|
||||
|
||||
### Option B: Apache Virtual Host Configuration
|
||||
|
||||
If you have access to Apache config, add this to your virtual host:
|
||||
|
||||
```apache
|
||||
<Directory "/home/USERNAME/public_html/product-finder">
|
||||
SetEnv APPLICATION_ROOT /product-finder
|
||||
PassengerEnabled on
|
||||
PassengerAppRoot /home/USERNAME/public_html/product-finder
|
||||
PassengerPython /usr/bin/python3
|
||||
Allow from all
|
||||
Options -MultiViews
|
||||
</Directory>
|
||||
```
|
||||
|
||||
## 🚀 Deployment Steps
|
||||
|
||||
1. **Backup Current Installation**
|
||||
```bash
|
||||
mv product-finder product-finder.backup
|
||||
```
|
||||
|
||||
2. **Upload Updated Files**
|
||||
- Upload entire `app/` folder to server
|
||||
- Or just upload the 5 changed files listed above
|
||||
|
||||
3. **Restart Passenger**
|
||||
```bash
|
||||
# In product-finder folder
|
||||
mkdir -p tmp
|
||||
touch tmp/restart.txt
|
||||
```
|
||||
|
||||
4. **Test the Application**
|
||||
- Go to: `https://columbiawindows.com/product-finder/`
|
||||
- Should redirect to: `https://columbiawindows.com/product-finder/login`
|
||||
- Login page should load with all CSS/JS
|
||||
- Login should work and redirect properly
|
||||
- All routes should work: `/product-finder/users`, etc.
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Issue: Still getting 404 on login page
|
||||
|
||||
**Check server error logs:**
|
||||
```bash
|
||||
tail -f ~/logs/error_log # or wherever your error logs are
|
||||
```
|
||||
|
||||
**Verify APPLICATION_ROOT is set:**
|
||||
Add this test route to app.py temporarily:
|
||||
```python
|
||||
@app.route('/debug-config')
|
||||
def debug_config():
|
||||
return jsonify({
|
||||
'APPLICATION_ROOT': app.config.get('APPLICATION_ROOT'),
|
||||
'script_root': request.script_root,
|
||||
'url_root': request.url_root,
|
||||
'base_url': request.base_url
|
||||
})
|
||||
```
|
||||
|
||||
Then visit: `https://columbiawindows.com/product-finder/debug-config`
|
||||
|
||||
**Expected response:**
|
||||
```json
|
||||
{
|
||||
"APPLICATION_ROOT": "/product-finder",
|
||||
"script_root": "/product-finder",
|
||||
"url_root": "https://columbiawindows.com/product-finder/",
|
||||
"base_url": "https://columbiawindows.com/product-finder/debug-config"
|
||||
}
|
||||
```
|
||||
|
||||
### Issue: CSS/JS files not loading
|
||||
|
||||
**Check that routes are working:**
|
||||
- Visit: `https://columbiawindows.com/product-finder/css/styles.css`
|
||||
- Should return CSS file, not 404
|
||||
|
||||
**Check .htaccess MIME types:**
|
||||
Ensure these lines are in `.htaccess`:
|
||||
```apache
|
||||
AddType text/css .css
|
||||
AddType application/javascript .js
|
||||
AddType application/json .json
|
||||
```
|
||||
|
||||
### Issue: Login works but redirects are wrong
|
||||
|
||||
**Check redirect code in templates:**
|
||||
All JavaScript should use `BASE_URL`:
|
||||
```javascript
|
||||
const BASE_URL = '{{ base_url }}';
|
||||
window.location.href = BASE_URL + '/login';
|
||||
```
|
||||
|
||||
All Python redirects should use `url_for()`:
|
||||
```python
|
||||
return redirect(url_for('login_page'))
|
||||
```
|
||||
|
||||
### Issue: Works locally, fails on server
|
||||
|
||||
**Verify environment:**
|
||||
```bash
|
||||
# SSH to server
|
||||
cd ~/public_html/product-finder
|
||||
python3 -c "import os; print(os.environ.get('APPLICATION_ROOT', 'NOT SET'))"
|
||||
```
|
||||
|
||||
Should print: `/product-finder`
|
||||
|
||||
**Check Passenger is using correct Python:**
|
||||
```bash
|
||||
which python3
|
||||
# Use this path in PassengerPython directive
|
||||
```
|
||||
|
||||
### Issue: Sessions not persisting
|
||||
|
||||
**Check SECRET_KEY:**
|
||||
```python
|
||||
# In config.py, production should have a fixed SECRET_KEY
|
||||
SECRET_KEY = 'your-fixed-secret-key-here' # Don't use secrets.token_hex() in production
|
||||
```
|
||||
|
||||
**Check cookie settings:**
|
||||
Session cookies need to work with the subdirectory path.
|
||||
|
||||
### Issue: API calls return 404
|
||||
|
||||
**Check browser console:**
|
||||
Press F12, go to Network tab, and check the actual URLs being called.
|
||||
|
||||
**Should see:**
|
||||
```
|
||||
https://columbiawindows.com/product-finder/api/login
|
||||
https://columbiawindows.com/product-finder/api/session
|
||||
```
|
||||
|
||||
**If you see:**
|
||||
```
|
||||
https://columbiawindows.com/api/login ❌ Missing prefix
|
||||
```
|
||||
|
||||
Then `BASE_URL` is not set correctly in template.
|
||||
|
||||
## ✅ Verification Checklist
|
||||
|
||||
After deployment, test these in order:
|
||||
|
||||
- [ ] Visit `https://columbiawindows.com/product-finder/`
|
||||
- Should redirect to `/product-finder/login` ✓
|
||||
|
||||
- [ ] Login page loads
|
||||
- CSS styled correctly ✓
|
||||
- No 404s in browser console ✓
|
||||
|
||||
- [ ] Login with Master/Master
|
||||
- Should redirect to `/product-finder/select-location` ✓
|
||||
|
||||
- [ ] Select a location
|
||||
- Should redirect to `/product-finder/` ✓
|
||||
|
||||
- [ ] User info shows in header ✓
|
||||
|
||||
- [ ] Click "User Management" (if Master user)
|
||||
- Should go to `/product-finder/users` ✓
|
||||
|
||||
- [ ] Logout
|
||||
- Should return to `/product-finder/login` ✓
|
||||
|
||||
## 📝 Key Points
|
||||
|
||||
1. **The middleware is critical** - It tells Flask about the `/product-finder` prefix
|
||||
2. **passenger_wsgi.py sets the env var** - Before importing the app
|
||||
3. **All templates use BASE_URL** - For JavaScript fetch calls
|
||||
4. **All routes use url_for()** - For Python redirects
|
||||
5. **Production config defaults to /product-finder** - Development stays at /
|
||||
|
||||
## 🔄 Rolling Back
|
||||
|
||||
If something goes wrong:
|
||||
|
||||
```bash
|
||||
# Remove new files
|
||||
rm -rf product-finder
|
||||
|
||||
# Restore backup
|
||||
mv product-finder.backup product-finder
|
||||
|
||||
# Restart Passenger
|
||||
touch product-finder/tmp/restart.txt
|
||||
```
|
||||
|
||||
## 📞 Still Having Issues?
|
||||
|
||||
Run this diagnostic script on the server:
|
||||
|
||||
```python
|
||||
# Save as test_deployment.py in product-finder folder
|
||||
import os
|
||||
import sys
|
||||
|
||||
print("=" * 60)
|
||||
print("DEPLOYMENT DIAGNOSTIC")
|
||||
print("=" * 60)
|
||||
print(f"Python Version: {sys.version}")
|
||||
print(f"Current Directory: {os.getcwd()}")
|
||||
print(f"APPLICATION_ROOT env: {os.environ.get('APPLICATION_ROOT', 'NOT SET')}")
|
||||
print()
|
||||
|
||||
try:
|
||||
os.environ['APPLICATION_ROOT'] = '/product-finder'
|
||||
from app import app
|
||||
print("✓ App imported successfully")
|
||||
print(f"APPLICATION_ROOT config: {app.config.get('APPLICATION_ROOT')}")
|
||||
print(f"Middleware applied: {'PrefixMiddleware' in str(type(app.wsgi_app))}")
|
||||
except Exception as e:
|
||||
print(f"✗ Error importing app: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
```
|
||||
|
||||
Run with: `python3 test_deployment.py`
|
||||
|
||||
## 🎯 Expected Behavior
|
||||
|
||||
**Before these changes:**
|
||||
- Redirect works: `/product-finder` → `/product-finder/login` ✓
|
||||
- Login page loads BUT Flask doesn't know about `/product-finder` prefix
|
||||
- All `url_for()` calls generate `/login` instead of `/product-finder/login` ❌
|
||||
- Result: 404 errors on subpages
|
||||
|
||||
**After these changes:**
|
||||
- Flask knows it's at `/product-finder` via middleware ✓
|
||||
- All `url_for()` generates `/product-finder/login` ✓
|
||||
- All templates use `BASE_URL = '/product-finder'` ✓
|
||||
- Result: Everything works ✓
|
||||
|
||||
## 🆘 Quick Fix Checklist
|
||||
|
||||
If deployed and not working:
|
||||
|
||||
1. [ ] Uploaded `app/app.py` with PrefixMiddleware?
|
||||
2. [ ] Uploaded `app/passenger_wsgi.py` with env var setting?
|
||||
3. [ ] Uploaded `app/.htaccess` with SetEnv directive?
|
||||
4. [ ] Ran `touch tmp/restart.txt` to restart Passenger?
|
||||
5. [ ] Checked error logs for Python errors?
|
||||
6. [ ] Tested `/product-finder/debug-config` route?
|
||||
7. [ ] Verified cookies are being set (F12 > Application > Cookies)?
|
||||
|
||||
If all checked and still failing, check server error logs for the actual Python error.
|
||||
@@ -0,0 +1,47 @@
|
||||
# Product Finder Quiz - Quick Start Guide
|
||||
|
||||
## Installation and Running
|
||||
|
||||
### Step 1: Install Python
|
||||
Make sure Python 3.8+ is installed:
|
||||
```bash
|
||||
python --version
|
||||
```
|
||||
|
||||
### Step 2: Install Dependencies
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### Step 3: Run the Application
|
||||
```bash
|
||||
python app.py
|
||||
```
|
||||
|
||||
### Step 4: Access the Application
|
||||
Open your browser and go to:
|
||||
- **Quiz Page**: http://localhost:8080/quiz
|
||||
- **Main Page**: http://localhost:8080/
|
||||
|
||||
## Stopping the Application
|
||||
Press `Ctrl+C` in the terminal to stop the server.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**"Port already in use" error:**
|
||||
Change the port in `app.py`:
|
||||
```python
|
||||
app.run(debug=True, host='0.0.0.0', port=8081)
|
||||
```
|
||||
|
||||
**Missing modules:**
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
**Templates not found:**
|
||||
Make sure the `templates` folder contains `index2.html` and other HTML files.
|
||||
|
||||
## For Production Deployment
|
||||
|
||||
See the detailed [README.md](README.md) file for deployment instructions.
|
||||
@@ -0,0 +1,171 @@
|
||||
# Product Finder Quiz - Flask Web Application
|
||||
|
||||
A Python Flask web application for product selection through an interactive quiz interface.
|
||||
|
||||
## Features
|
||||
|
||||
- Interactive quiz with button-based and form-based questions
|
||||
- Dynamic conditional logic based on user responses
|
||||
- Product recommendations with images
|
||||
- Responsive design
|
||||
- Memory storage for user answers
|
||||
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.8 or higher
|
||||
- pip (Python package installer)
|
||||
|
||||
### Setup
|
||||
|
||||
1. **Install dependencies:**
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
2. **Create environment file (optional):**
|
||||
```bash
|
||||
copy .env.example .env
|
||||
```
|
||||
Edit `.env` with your configuration.
|
||||
|
||||
3. **Run the application:**
|
||||
```bash
|
||||
python app.py
|
||||
```
|
||||
|
||||
4. **Access the application:**
|
||||
Open your browser and navigate to:
|
||||
- Main page: `http://localhost:8080/`
|
||||
- Quiz page: `http://localhost:8080/quiz`
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
project/
|
||||
├── app.py # Main Flask application
|
||||
├── config.py # Configuration settings
|
||||
├── wsgi.py # WSGI entry point for production
|
||||
├── requirements.txt # Python dependencies
|
||||
├── templates/ # HTML templates
|
||||
│ └── index2.html # Quiz page
|
||||
├── css/ # Stylesheets
|
||||
│ └── styles.css # Main stylesheet
|
||||
├── js/ # JavaScript files
|
||||
│ └── script.js # Quiz logic and data
|
||||
└── images/ # Product images (optional)
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
### Using Gunicorn (Production)
|
||||
|
||||
1. **Install Gunicorn:**
|
||||
```bash
|
||||
pip install gunicorn
|
||||
```
|
||||
|
||||
2. **Run with Gunicorn:**
|
||||
```bash
|
||||
gunicorn -w 4 -b 0.0.0.0:8080 wsgi:app
|
||||
```
|
||||
|
||||
### Deployment on Web Panels
|
||||
|
||||
#### cPanel/Plesk
|
||||
1. Upload all files to your hosting directory
|
||||
2. Install Python dependencies via terminal or SSH
|
||||
3. Configure the web server to use Python WSGI
|
||||
4. Set the application entry point to `wsgi:app`
|
||||
|
||||
#### PythonAnywhere
|
||||
1. Upload files or clone from repository
|
||||
2. Create a new web app (Flask)
|
||||
3. Set WSGI file path to `wsgi.py`
|
||||
4. Install requirements in virtual environment
|
||||
5. Reload the web app
|
||||
|
||||
#### Heroku
|
||||
1. Create `Procfile`:
|
||||
```
|
||||
web: gunicorn wsgi:app
|
||||
```
|
||||
2. Deploy using Git:
|
||||
```bash
|
||||
git init
|
||||
git add .
|
||||
git commit -m "Initial commit"
|
||||
heroku create
|
||||
git push heroku main
|
||||
```
|
||||
|
||||
#### Docker
|
||||
1. Create `Dockerfile`:
|
||||
```dockerfile
|
||||
FROM python:3.11-slim
|
||||
WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
RUN pip install -r requirements.txt
|
||||
COPY . .
|
||||
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8080", "wsgi:app"]
|
||||
```
|
||||
2. Build and run:
|
||||
```bash
|
||||
docker build -t product-finder .
|
||||
docker run -p 8080:8080 product-finder
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Edit `config.py` to modify application settings:
|
||||
- `DEBUG`: Enable/disable debug mode
|
||||
- `SECRET_KEY`: Set a secure secret key for production
|
||||
- Add database URLs, API keys, etc.
|
||||
|
||||
## API Endpoints
|
||||
|
||||
- `GET /` - Main landing page
|
||||
- `GET /quiz` - Product finder quiz
|
||||
- `POST /api/save-selection` - Save user selections (for future use)
|
||||
- `GET /api/get-products` - Get product data (for future use)
|
||||
|
||||
## Customization
|
||||
|
||||
### Adding New Products
|
||||
Edit `js/script.js` and add new product entries to the `questionData` object.
|
||||
|
||||
### Styling
|
||||
Modify `css/styles.css` to change colors, layouts, and appearance.
|
||||
|
||||
### Questions and Logic
|
||||
Update the quiz flow in `js/script.js` by modifying the question structure and conditional logic.
|
||||
|
||||
## Development
|
||||
|
||||
Run in development mode with auto-reload:
|
||||
```bash
|
||||
python app.py
|
||||
```
|
||||
|
||||
The application will be available at `http://localhost:8080`
|
||||
|
||||
## Production Checklist
|
||||
|
||||
- [ ] Set `DEBUG = False` in config
|
||||
- [ ] Use a strong `SECRET_KEY`
|
||||
- [ ] Configure proper database (if needed)
|
||||
- [ ] Set up SSL/HTTPS
|
||||
- [ ] Configure proper logging
|
||||
- [ ] Set up error monitoring
|
||||
- [ ] Enable CORS if needed
|
||||
- [ ] Configure environment variables
|
||||
- [ ] Test all routes and functionality
|
||||
|
||||
## License
|
||||
|
||||
Proprietary - All rights reserved
|
||||
|
||||
## Support
|
||||
|
||||
For issues and questions, contact your development team.
|
||||
@@ -0,0 +1,791 @@
|
||||
# Required Code Changes for Conditional Navigation
|
||||
|
||||
## Overview
|
||||
To support conditional material/color questions and dynamic navigation, the following changes are needed in your application code.
|
||||
|
||||
---
|
||||
|
||||
## 1. JavaScript Changes (`js/script.js`)
|
||||
|
||||
### Current State
|
||||
- Loads `questions.json` statically
|
||||
- Has hardcoded conditional for `q-material-conditional`
|
||||
- No product data loading
|
||||
|
||||
### Required Changes
|
||||
|
||||
#### A. Load Multiple Data Files
|
||||
```javascript
|
||||
// At the top of script.js - modify loadQuestionData() to load both files
|
||||
let questionData = {};
|
||||
let productData = [];
|
||||
let accessoryData = [];
|
||||
let bitwiseData = {};
|
||||
let accumulatedBitValue = 0;
|
||||
|
||||
function init() {
|
||||
// Load all data files
|
||||
Promise.all([
|
||||
fetch('data/navigation.json').then(r => r.json()),
|
||||
fetch('data/products.json').then(r => r.json()),
|
||||
fetch('data/accessories.json').then(r => r.json()),
|
||||
fetch('data/product_bitwise.json').then(r => r.json())
|
||||
])
|
||||
.then(([questions, products, accessories, bitwise]) => {
|
||||
questionData = questions;
|
||||
productData = products;
|
||||
accessoryData = accessories;
|
||||
|
||||
// Index bitwise data by product code
|
||||
bitwiseData = {};
|
||||
bitwise.forEach(item => {
|
||||
bitwiseData[item.PROD_CODE] = item.BIT_VALUE;
|
||||
});
|
||||
|
||||
// Check if there's state in URL
|
||||
initFromURL();
|
||||
|
||||
// If no URL state, start normally
|
||||
if (!window.location.search) {
|
||||
loadContent('start');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading data:', error);
|
||||
// Show error message
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
#### B. Add Dynamic Next Resolution Function
|
||||
```javascript
|
||||
// Add this new function to handle conditional navigation
|
||||
function resolveNextQuestion(currentKey, selectedAnswer, answerObject) {
|
||||
// Get the current accumulated filters
|
||||
const currentFilters = buildFilterFromAnswers();
|
||||
|
||||
// Add the new filter from this answer
|
||||
if (answerObject.filter) {
|
||||
Object.assign(currentFilters, answerObject.filter);
|
||||
}
|
||||
|
||||
// Check if next question in chain should be shown
|
||||
const nextKey = answerObject.next;
|
||||
const nextQuestion = questionData[nextKey];
|
||||
|
||||
// If next question has conditional flag, evaluate it
|
||||
if (nextQuestion && nextQuestion.conditional) {
|
||||
return evaluateConditional(nextQuestion, currentFilters);
|
||||
}
|
||||
|
||||
return nextKey;
|
||||
}
|
||||
|
||||
// Evaluate if a conditional question should be shown
|
||||
function evaluateConditional(question, filters) {
|
||||
if (question.conditional.type === 'material') {
|
||||
// Check if products matching current filters have multiple materials
|
||||
const matchingProducts = filterProducts(filters);
|
||||
const materials = getAvailableMaterials(matchingProducts);
|
||||
|
||||
if (materials.length <= 1) {
|
||||
// Skip this question, auto-apply material if exists
|
||||
if (materials.length === 1) {
|
||||
userAnswers[question.id + '.material'] = materials[0];
|
||||
}
|
||||
// Return the fallback next
|
||||
return question.conditional.fallbackNext || question.next;
|
||||
}
|
||||
}
|
||||
|
||||
if (question.conditional.type === 'color') {
|
||||
// Similar logic for colors
|
||||
const matchingProducts = filterProducts(filters);
|
||||
const colors = getAvailableColors(matchingProducts);
|
||||
|
||||
if (colors.length <= 1) {
|
||||
if (colors.length === 1) {
|
||||
userAnswers[question.id + '.color'] = colors[0];
|
||||
}
|
||||
return question.conditional.fallbackNext || question.next;
|
||||
}
|
||||
}
|
||||
|
||||
// Show the question
|
||||
return question.id;
|
||||
}
|
||||
|
||||
// Build current filter object from user answers
|
||||
function buildFilterFromAnswers() {
|
||||
const filters = {};
|
||||
|
||||
// Extract filter info from answers
|
||||
for (const [key, value] of Object.entries(userAnswers)) {
|
||||
if (key === 'start') {
|
||||
filters.baseType = value;
|
||||
} else if (key.includes('type')) {
|
||||
filters.subType = value;
|
||||
} else if (key.includes('material')) {
|
||||
filters.material = value;
|
||||
} else if (key.includes('color')) {
|
||||
filters.color = value;
|
||||
}
|
||||
}
|
||||
|
||||
return filters;
|
||||
}
|
||||
|
||||
// Filter products based on criteria
|
||||
function filterProducts(filters) {
|
||||
return productData.filter(product => {
|
||||
if (filters.baseType && product.baseType !== filters.baseType) {
|
||||
return false;
|
||||
}
|
||||
if (filters.subType) {
|
||||
const subType = product.subType.door || product.subType.window;
|
||||
if (subType !== filters.subType) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (filters.material && !product.materials.includes(filters.material)) {
|
||||
return false;
|
||||
}
|
||||
if (filters.color && !product.colors.includes(filters.color)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// Get available materials from product set
|
||||
function getAvailableMaterials(products) {
|
||||
const materials = new Set();
|
||||
products.forEach(p => {
|
||||
p.materials.forEach(m => materials.add(m));
|
||||
});
|
||||
return Array.from(materials);
|
||||
}
|
||||
|
||||
// Get available colors from product set
|
||||
function getAvailableColors(products) {
|
||||
const colors = new Set();
|
||||
products.forEach(p => {
|
||||
p.colors.forEach(c => colors.add(c));
|
||||
});
|
||||
return Array.from(colors);
|
||||
}
|
||||
```
|
||||
|
||||
#### C. Update handleAnswer Function
|
||||
```javascript
|
||||
// Replace the existing handleAnswer function
|
||||
function handleAnswer(currentKey, answerValue, answerIndex) {
|
||||
const currentQuestion = questionData[currentKey];
|
||||
const answerObject = currentQuestion.answers[answerIndex];
|
||||
|
||||
// Store the answer
|
||||
userAnswers[currentKey] = answerValue;
|
||||
|
||||
// Update bit value based on selection
|
||||
updateBitValue(answerObject);
|
||||
|
||||
// Resolve the next question (handles conditionals)
|
||||
const nextKey = resolveNextQuestion(currentKey, answerValue, answerObject);
|
||||
|
||||
history.push(nextKey);
|
||||
loadContent(nextKey);
|
||||
}
|
||||
```
|
||||
|
||||
#### D. Update renderQuestion to Pass Answer Index
|
||||
```javascript
|
||||
// In renderQuestion function, change the button onclick
|
||||
// FROM:
|
||||
// onclick="handleAnswer('${currentKey}', '${answer.caption}', '${answer.next}')"
|
||||
|
||||
// TO:
|
||||
data.answers.forEach((answer, index) => {
|
||||
html += `
|
||||
<button class="answer-button" onclick="handleAnswer('${currentKey}', '${answer.caption}', ${index})">
|
||||
<div class="answer-image">${answer.image}</div>
|
||||
<div class="answer-caption">${answer.caption}</div>
|
||||
</button>
|
||||
`;
|
||||
});
|
||||
```
|
||||
|
||||
#### D2. Add URL State Management
|
||||
```javascript
|
||||
// Track accumulated bit value from user selections
|
||||
let accumulatedBitValue = 0;
|
||||
|
||||
// Load bitwise helper data
|
||||
let bitwiseData = {};
|
||||
|
||||
function loadBitwiseData() {
|
||||
return fetch('data/product_bitwise.json')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
bitwiseData = {};
|
||||
data.forEach(item => {
|
||||
bitwiseData[item.PROD_CODE] = item.BIT_VALUE;
|
||||
});
|
||||
return bitwiseData;
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize from URL on page load
|
||||
function initFromURL() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
|
||||
// Check for bit value in URL
|
||||
const bitValue = params.get('b');
|
||||
if (bitValue) {
|
||||
accumulatedBitValue = parseInt(bitValue, 10);
|
||||
// Restore state based on bit value
|
||||
restoreStateFromBitValue(accumulatedBitValue);
|
||||
}
|
||||
|
||||
// Check for product code in URL
|
||||
const productCode = params.get('p');
|
||||
if (productCode) {
|
||||
showProductByCode(productCode);
|
||||
}
|
||||
}
|
||||
|
||||
// Update URL with current state
|
||||
function updateURL() {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (accumulatedBitValue > 0) {
|
||||
params.set('b', accumulatedBitValue);
|
||||
}
|
||||
|
||||
// Get current question
|
||||
if (history.length > 0) {
|
||||
const currentKey = history[history.length - 1];
|
||||
params.set('q', currentKey);
|
||||
}
|
||||
|
||||
// Update URL without reloading page
|
||||
const newURL = window.location.pathname + '?' + params.toString();
|
||||
window.history.replaceState({ bitValue: accumulatedBitValue }, '', newURL);
|
||||
}
|
||||
|
||||
// Update bit value based on user selection
|
||||
function updateBitValue(answerObject) {
|
||||
if (answerObject.filter) {
|
||||
// Add bits based on filter
|
||||
if (answerObject.filter.baseType === 'Door') {
|
||||
accumulatedBitValue |= 1; // Bit 0
|
||||
} else if (answerObject.filter.baseType === 'Window') {
|
||||
accumulatedBitValue |= 2; // Bit 1
|
||||
}
|
||||
|
||||
if (answerObject.filter.material === 'Aluminum') {
|
||||
accumulatedBitValue |= 16; // Bit 4
|
||||
} else if (answerObject.filter.material === 'Vinyl') {
|
||||
accumulatedBitValue |= 32; // Bit 5
|
||||
}
|
||||
|
||||
// Add color bits
|
||||
const colorBits = {
|
||||
'Black': 64,
|
||||
'White': 128,
|
||||
'Bronze': 256,
|
||||
'Tan': 512,
|
||||
'Mill': 1024,
|
||||
'Sandstone': 2048
|
||||
};
|
||||
|
||||
if (answerObject.filter.color && colorBits[answerObject.filter.color]) {
|
||||
accumulatedBitValue |= colorBits[answerObject.filter.color];
|
||||
}
|
||||
|
||||
// Add subtype bits (based on bitwise_legend.json)
|
||||
const subtypeBits = {
|
||||
'Patio Door': 4096,
|
||||
'Primary Window': 8192,
|
||||
'Storm Door': 16384,
|
||||
'Storm Window': 32768
|
||||
};
|
||||
|
||||
if (answerObject.filter.subType && subtypeBits[answerObject.filter.subType]) {
|
||||
accumulatedBitValue |= subtypeBits[answerObject.filter.subType];
|
||||
}
|
||||
}
|
||||
|
||||
updateURL();
|
||||
}
|
||||
|
||||
// Restore state from bit value
|
||||
function restoreStateFromBitValue(bitValue) {
|
||||
// Decode bit value back to user selections
|
||||
const selections = {};
|
||||
|
||||
if (bitValue & 1) selections.baseType = 'Door';
|
||||
if (bitValue & 2) selections.baseType = 'Window';
|
||||
|
||||
if (bitValue & 16) selections.material = 'Aluminum';
|
||||
if (bitValue & 32) selections.material = 'Vinyl';
|
||||
|
||||
const colors = ['Black', 'White', 'Bronze', 'Tan', 'Mill', 'Sandstone'];
|
||||
const colorBits = [64, 128, 256, 512, 1024, 2048];
|
||||
|
||||
colors.forEach((color, idx) => {
|
||||
if (bitValue & colorBits[idx]) {
|
||||
selections.color = color;
|
||||
}
|
||||
});
|
||||
|
||||
// Store in userAnswers
|
||||
if (selections.baseType) userAnswers['start'] = selections.baseType;
|
||||
if (selections.material) userAnswers['q-material.material'] = selections.material;
|
||||
if (selections.color) userAnswers['q-color.color'] = selections.color;
|
||||
|
||||
// Navigate to appropriate question from URL param
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const questionKey = params.get('q') || 'start';
|
||||
loadContent(questionKey);
|
||||
}
|
||||
|
||||
// Share current state
|
||||
function shareCurrentState() {
|
||||
const url = window.location.href;
|
||||
|
||||
// Copy to clipboard
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
alert('Link copied to clipboard! Share this link to return to this exact state.');
|
||||
});
|
||||
} else {
|
||||
// Fallback: show URL
|
||||
prompt('Copy this URL to share:', url);
|
||||
}
|
||||
}
|
||||
|
||||
// Add share button to breadcrumb
|
||||
function updateBreadcrumbWithShare() {
|
||||
const breadcrumbDiv = document.getElementById('breadcrumb');
|
||||
const shareButton = `
|
||||
<button onclick="shareCurrentState()"
|
||||
style="float: right; padding: 5px 10px; cursor: pointer;">
|
||||
🔗 Share
|
||||
</button>
|
||||
`;
|
||||
breadcrumbDiv.innerHTML += shareButton;
|
||||
}
|
||||
```
|
||||
|
||||
#### E. Add Product Results Page
|
||||
```javascript
|
||||
// Add new function to show filtered products
|
||||
function showProductResults(filters) {
|
||||
const matchingProducts = filterProducts(filters);
|
||||
|
||||
const contentDiv = document.getElementById('content');
|
||||
|
||||
if (matchingProducts.length === 0) {
|
||||
contentDiv.innerHTML = `
|
||||
<div class="result-container">
|
||||
<div class="result-title">No Products Found</div>
|
||||
<div class="result-details">
|
||||
No products match your specifications. Please try different options.
|
||||
</div>
|
||||
<button class="back-button" onclick="goBack()">← Go Back</button>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
let html = `
|
||||
<div class="result-container">
|
||||
<div class="result-title">Found ${matchingProducts.length} Product(s)</div>
|
||||
<div class="products-grid">
|
||||
`;
|
||||
|
||||
matchingProducts.forEach(product => {
|
||||
// Get compatible accessories
|
||||
const compatibleAccessories = accessoryData.filter(acc =>
|
||||
product.compatibleAccessories.includes(acc.id)
|
||||
);
|
||||
|
||||
html += `
|
||||
<div class="product-card" onclick="showProductDetail('${product.productCode}')">
|
||||
<h3>${product.description}</h3>
|
||||
<p><strong>Code:</strong> ${product.productCode}</p>
|
||||
<p><strong>Materials:</strong> ${product.materials.join(', ')}</p>
|
||||
<p><strong>Colors:</strong> ${product.colors.join(', ')}</p>
|
||||
${compatibleAccessories.length > 0 ? `
|
||||
<p><strong>Available Options:</strong></p>
|
||||
<ul>
|
||||
${compatibleAccessories.map(acc =>
|
||||
`<li>${acc.description}</li>`
|
||||
).join('')}
|
||||
</ul>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
html += `
|
||||
</div>
|
||||
<div class="result-actions">
|
||||
<button class="back-button" onclick="goBack()">← Go Back</button>
|
||||
<button class="back-button" onclick="startOver()">Start Over</button>
|
||||
<button class="back-button" onclick="shareCurrentState()">🔗 Share Results</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
contentDiv.innerHTML = html;
|
||||
}
|
||||
|
||||
// Show individual product detail with URL update
|
||||
function showProductDetail(productCode) {
|
||||
const product = productData.find(p => p.productCode === productCode);
|
||||
if (!product) return;
|
||||
|
||||
// Update URL with product code
|
||||
const params = new URLSearchParams();
|
||||
params.set('p', productCode);
|
||||
if (accumulatedBitValue > 0) {
|
||||
params.set('b', accumulatedBitValue);
|
||||
}
|
||||
window.history.pushState({ productCode }, '', '?' + params.toString());
|
||||
|
||||
// Get compatible accessories
|
||||
const compatibleAccessories = accessoryData.filter(acc =>
|
||||
product.compatibleAccessories.includes(acc.id)
|
||||
);
|
||||
|
||||
const contentDiv = document.getElementById('content');
|
||||
let html = `
|
||||
<div class="result-container">
|
||||
<div class="result-title">${product.description}</div>
|
||||
<div class="result-content">
|
||||
<div class="result-details">
|
||||
<p><strong>Product Code:</strong> ${product.productCode}</p>
|
||||
<p><strong>Category:</strong> ${product.category}</p>
|
||||
<p><strong>Base Type:</strong> ${product.baseType || 'N/A'}</p>
|
||||
<p><strong>Materials:</strong> ${product.materials.join(', ') || 'N/A'}</p>
|
||||
<p><strong>Colors:</strong> ${product.colors.join(', ') || 'N/A'}</p>
|
||||
|
||||
${compatibleAccessories.length > 0 ? `
|
||||
<h3 style="margin-top: 20px;">Compatible Accessories</h3>
|
||||
<div class="accessories-list">
|
||||
${compatibleAccessories.map(acc => `
|
||||
<div class="accessory-item">
|
||||
<strong>${acc.description}</strong><br>
|
||||
<small>Code: ${acc.accessoryCode}</small><br>
|
||||
<small>Materials: ${acc.materials.join(', ')}</small>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="result-actions">
|
||||
<button class="back-button" onclick="goBack()">← Back to Results</button>
|
||||
<button class="back-button" onclick="startOver()">Start Over</button>
|
||||
<button class="back-button" onclick="shareCurrentState()">🔗 Share Product</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
contentDiv.innerHTML = html;
|
||||
}
|
||||
|
||||
// Show product by code (from URL)
|
||||
function showProductByCode(productCode) {
|
||||
const product = productData.find(p => p.productCode === productCode);
|
||||
if (product) {
|
||||
showProductDetail(productCode);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. JSON Structure Changes (`data/navigation.json`)
|
||||
|
||||
### Add Conditional Metadata to Questions
|
||||
|
||||
Questions that might be skipped need a `conditional` property:
|
||||
|
||||
```json
|
||||
{
|
||||
"q-material-storm-door": {
|
||||
"type": "question",
|
||||
"inputType": "button",
|
||||
"title": "Select Material",
|
||||
"subtitle": "Choose your preferred material",
|
||||
"conditional": {
|
||||
"type": "material",
|
||||
"fallbackNext": "q-dimensions",
|
||||
"autoApply": true
|
||||
},
|
||||
"answers": [
|
||||
{
|
||||
"caption": "Aluminum",
|
||||
"next": "q-color",
|
||||
"filter": { "material": "Aluminum" }
|
||||
},
|
||||
{
|
||||
"caption": "Vinyl",
|
||||
"next": "q-color",
|
||||
"filter": { "material": "Vinyl" }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Add Filter Properties to Answers
|
||||
|
||||
Each answer should include filter criteria:
|
||||
|
||||
```json
|
||||
{
|
||||
"caption": "Storm Door",
|
||||
"image": "🚪",
|
||||
"next": "q-material-storm-door",
|
||||
"filter": {
|
||||
"baseType": "Door",
|
||||
"subType": "Storm Door"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. CSS Changes (`css/styles.css`)
|
||||
|
||||
Add styles for product grid:
|
||||
|
||||
```css
|
||||
.products-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
margin: 20px 0;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.product-card {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.product-card h3 {
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.product-card p {
|
||||
margin: 10px 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.product-card ul {
|
||||
list-style-position: inside;
|
||||
padding-left: 0;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Python Changes (OPTIONAL)
|
||||
|
||||
If you want server-side filtering:
|
||||
|
||||
```python
|
||||
# Add to app.py
|
||||
import json
|
||||
import csv
|
||||
|
||||
@app.route('/api/filter-products', methods=['POST'])
|
||||
def filter_products():
|
||||
"""Filter products based on criteria"""
|
||||
filters = request.json
|
||||
|
||||
# Load products
|
||||
with open('data/products.json', 'r') as f:
|
||||
products = json.load(f)
|
||||
|
||||
# Apply filters
|
||||
filtered = []
|
||||
for product in products:
|
||||
if filters.get('baseType') and product['baseType'] != filters['baseType']:
|
||||
continue
|
||||
if filters.get('subType'):
|
||||
sub = product['subType'].get('door') or product['subType'].get('window')
|
||||
if sub != filters['subType']:
|
||||
continue
|
||||
if filters.get('material') and filters['material'] not in product['materials']:
|
||||
continue
|
||||
if filters.get('color') and filters['color'] not in product['colors']:
|
||||
continue
|
||||
|
||||
filtered.append(product)
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'count': len(filtered),
|
||||
'products': filtered
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. CSV Parsing Script Changes
|
||||
|
||||
Create a Python script to generate the JSON files:
|
||||
|
||||
```python
|
||||
# create_json_files.py
|
||||
import csv
|
||||
import json
|
||||
from collections import defaultdict
|
||||
|
||||
def parse_products_csv(csv_path):
|
||||
"""Parse products.csv and generate three JSON files"""
|
||||
|
||||
products = []
|
||||
accessories = []
|
||||
nav_data = {
|
||||
"start": {
|
||||
"type": "question",
|
||||
"inputType": "button",
|
||||
"title": "What are you looking for?",
|
||||
"subtitle": "Select the product category",
|
||||
"answers": []
|
||||
}
|
||||
}
|
||||
|
||||
# Track unique values for navigation
|
||||
base_types = set()
|
||||
door_subtypes = defaultdict(int) # Track material diversity per subtype
|
||||
window_subtypes = defaultdict(int)
|
||||
|
||||
with open(csv_path, 'r', encoding='utf-8') as f:
|
||||
reader = csv.DictReader(f, skipinitialspace=True)
|
||||
|
||||
for row in reader:
|
||||
# Skip discontinued
|
||||
if row['DISCONT'].upper() == 'TRUE':
|
||||
continue
|
||||
|
||||
# Parse materials
|
||||
materials = []
|
||||
if row['Aluminum'].upper() == 'TRUE':
|
||||
materials.append('Aluminum')
|
||||
if row['Vinyl'].upper() == 'TRUE':
|
||||
materials.append('Vinyl')
|
||||
|
||||
# Parse colors
|
||||
colors = []
|
||||
for color in ['Black', 'White', 'Bronze', 'Tan', 'Mill', 'Sandstone']:
|
||||
if row.get(color, '').upper() == 'TRUE':
|
||||
colors.append(color)
|
||||
|
||||
# Determine if accessory
|
||||
is_accessory = row['Accessory\\nYes'].upper() == 'TRUE'
|
||||
|
||||
# Build sub-type object
|
||||
sub_type = {
|
||||
"door": row['Sub-type\\nDoor'].strip() if row['Sub-type\\nDoor'] else None,
|
||||
"window": row['Sub-type\\nWindow'].strip() if row['Sub-type\\nWindow'] else None
|
||||
}
|
||||
|
||||
# Common data
|
||||
item_data = {
|
||||
"id": row['PROD_CODE'],
|
||||
"productCode": row['PROD_CODE'],
|
||||
"category": row['CATEGORY'],
|
||||
"description": row['DESCRIPTION'],
|
||||
"discontinued": False,
|
||||
"location": row['LOC_CODE'],
|
||||
"baseType": row['Base\\nType'].strip() if row['Base\\nType'] else None,
|
||||
"subType": sub_type,
|
||||
"materials": materials,
|
||||
"colors": colors
|
||||
}
|
||||
|
||||
if is_accessory:
|
||||
# Add to accessories
|
||||
accessories.append({
|
||||
**item_data,
|
||||
"compatibilityRules": {
|
||||
"type": "subType",
|
||||
"subTypeDoor": sub_type['door'],
|
||||
"subTypeWindow": sub_type['window'],
|
||||
# ... more rules
|
||||
}
|
||||
})
|
||||
else:
|
||||
# Add to products
|
||||
products.append({
|
||||
**item_data,
|
||||
"isAccessory": False,
|
||||
"compatibleAccessories": []
|
||||
})
|
||||
|
||||
# Track for navigation
|
||||
if item_data['baseType']:
|
||||
base_types.add(item_data['baseType'])
|
||||
|
||||
# Build navigation structure
|
||||
# ... (logic to create navigation.json based on analysis)
|
||||
|
||||
# Write JSON files
|
||||
with open('data/products.json', 'w') as f:
|
||||
json.dump(products, f, indent=2)
|
||||
|
||||
with open('data/accessories.json', 'w') as f:
|
||||
json.dump(accessories, f, indent=2)
|
||||
|
||||
with open('data/navigation.json', 'w') as f:
|
||||
json.dump(nav_data, f, indent=2)
|
||||
|
||||
if __name__ == '__main__':
|
||||
parse_products_csv('data/products.csv')
|
||||
print("JSON files generated successfully!")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
- [ ] Update `js/script.js` with conditional navigation logic
|
||||
- [ ] Create parsing script to generate JSON files from CSV
|
||||
- [ ] Run parsing script to create `navigation.json`, `products.json`, `accessories.json`
|
||||
- [ ] Update CSS for product display
|
||||
- [ ] Test navigation flow with different product types
|
||||
- [ ] Verify material questions are skipped when appropriate
|
||||
- [ ] Verify color questions are skipped when appropriate
|
||||
- [ ] Test product filtering and results display
|
||||
- [ ] Add accessory display on product pages
|
||||
|
||||
---
|
||||
|
||||
## Testing Scenarios
|
||||
|
||||
1. **Storm Door Selection**: Should skip material question (all Aluminum)
|
||||
2. **Window Selection**: Should show material question (mixed materials)
|
||||
3. **Single Color Products**: Should skip color question
|
||||
4. **Multi-Color Products**: Should show color question
|
||||
5. **Product Results**: Should show filtered products with compatible accessories
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
**CSV**: No changes needed (unless adding bit values)
|
||||
**JavaScript**: Major refactoring for conditional logic
|
||||
**JSON**: New structure with filter objects and conditional flags
|
||||
**Python**: Optional parsing script to generate JSON from CSV
|
||||
@@ -0,0 +1,184 @@
|
||||
# 🎉 Flask Web Application Successfully Created!
|
||||
|
||||
Your HTML/JavaScript quiz has been converted to a Python Flask web application.
|
||||
|
||||
## ✅ What Was Created
|
||||
|
||||
### Core Application Files
|
||||
- **app.py** - Main Flask application with routes
|
||||
- **wsgi.py** - Production WSGI entry point
|
||||
- **config.py** - Configuration management
|
||||
- **requirements.txt** - Python dependencies
|
||||
|
||||
### Templates & Static Files
|
||||
- **templates/** - HTML files (index.html, index2.html, 404.html)
|
||||
- **css/** - Stylesheets (styles.css)
|
||||
- **js/** - JavaScript files (script.js)
|
||||
- **images/** - Product images folder
|
||||
|
||||
### Documentation & Utilities
|
||||
- **README.md** - Complete documentation
|
||||
- **QUICKSTART.md** - Quick start guide
|
||||
- **run.bat** - Windows startup script
|
||||
- **.gitignore** - Git ignore file
|
||||
- **.env.example** - Environment template
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Option 1: Using Batch File (Windows)
|
||||
Double-click `run.bat` to install dependencies and start the server.
|
||||
|
||||
### Option 2: Manual Start
|
||||
```bash
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Run the application
|
||||
python app.py
|
||||
```
|
||||
|
||||
### Access Your Application
|
||||
Open in browser: **http://localhost:8080/quiz**
|
||||
|
||||
## 🌐 Current Status
|
||||
|
||||
✅ Flask is currently running on: http://localhost:8080
|
||||
✅ Quiz page: http://localhost:8080/quiz
|
||||
✅ Debug mode: Enabled (auto-reloads on file changes)
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
project/
|
||||
├── app.py ← Main Flask app (START HERE)
|
||||
├── wsgi.py ← For production deployment
|
||||
├── config.py ← Settings & configuration
|
||||
├── requirements.txt ← Python packages needed
|
||||
├── run.bat ← Windows startup script
|
||||
│
|
||||
├── templates/ ← HTML files (Flask requires this folder)
|
||||
│ ├── index.html ← Main landing page
|
||||
│ ├── index2.html ← Quiz page
|
||||
│ └── 404.html ← Error page
|
||||
│
|
||||
├── css/ ← Stylesheets
|
||||
│ └── styles.css ← Main CSS file
|
||||
│
|
||||
├── js/ ← JavaScript files
|
||||
│ └── script.js ← Quiz logic and data
|
||||
│
|
||||
└── images/ ← Product images (add your images here)
|
||||
```
|
||||
|
||||
## 🎯 Key Features
|
||||
|
||||
✅ **Dynamic Routing** - Flask handles all page requests
|
||||
✅ **Static File Serving** - CSS, JS, and images properly served
|
||||
✅ **Error Handling** - Custom 404 page
|
||||
✅ **API Endpoints** - Ready for future backend features
|
||||
✅ **Production Ready** - WSGI config included
|
||||
✅ **All Original Features** - Quiz, forms, conditional logic, memory storage
|
||||
|
||||
## 🔧 Customization
|
||||
|
||||
### Change Port
|
||||
Edit `app.py`, line with `app.run()`:
|
||||
```python
|
||||
app.run(debug=True, host='0.0.0.0', port=YOUR_PORT)
|
||||
```
|
||||
|
||||
### Add New Routes
|
||||
In `app.py`:
|
||||
```python
|
||||
@app.route('/your-page')
|
||||
def your_page():
|
||||
return render_template('your-page.html')
|
||||
```
|
||||
|
||||
### Update Quiz Questions
|
||||
Edit `js/script.js` - modify the `questionData` object
|
||||
|
||||
### Change Styling
|
||||
Edit `css/styles.css`
|
||||
|
||||
## 📦 Deployment Options
|
||||
|
||||
### 1. Web Panels (cPanel, Plesk)
|
||||
- Upload all files
|
||||
- Install requirements: `pip install -r requirements.txt`
|
||||
- Point to `wsgi.py`
|
||||
|
||||
### 2. Cloud Platforms
|
||||
- **Heroku**: Add `Procfile` and push to Git
|
||||
- **PythonAnywhere**: Upload and configure WSGI
|
||||
- **AWS/Azure**: Use with Gunicorn
|
||||
|
||||
### 3. Docker
|
||||
See README.md for Dockerfile example
|
||||
|
||||
### 4. Production Server
|
||||
```bash
|
||||
pip install gunicorn
|
||||
gunicorn -w 4 -b 0.0.0.0:8080 wsgi:app
|
||||
```
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Port Already in Use
|
||||
Change port in app.py or kill the process:
|
||||
```bash
|
||||
# Windows
|
||||
netstat -ano | findstr :8080
|
||||
taskkill /PID <PID> /F
|
||||
```
|
||||
|
||||
### Templates Not Found
|
||||
Make sure HTML files are in `templates/` folder
|
||||
|
||||
### Static Files Not Loading
|
||||
Check that `css/` and `js/` folders exist in root directory
|
||||
|
||||
### Module Not Found
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## 📚 Next Steps
|
||||
|
||||
1. **Test the application** - Visit http://localhost:8080/quiz
|
||||
2. **Add your product images** - Place images in `images/` folder
|
||||
3. **Customize the quiz** - Edit `js/script.js`
|
||||
4. **Update styling** - Modify `css/styles.css`
|
||||
5. **Deploy** - Follow README.md deployment guide
|
||||
|
||||
## 🔒 Security Notes for Production
|
||||
|
||||
Before deploying to production:
|
||||
- [ ] Set `DEBUG = False` in config.py
|
||||
- [ ] Change `SECRET_KEY` to a strong random value
|
||||
- [ ] Use environment variables for sensitive data
|
||||
- [ ] Set up HTTPS/SSL
|
||||
- [ ] Use a production WSGI server (Gunicorn, uWSGI)
|
||||
- [ ] Configure proper logging
|
||||
- [ ] Set up database backups (if using a database)
|
||||
|
||||
## 💡 Tips
|
||||
|
||||
- Flask auto-reloads when you edit files (in debug mode)
|
||||
- Press `Ctrl+C` to stop the server
|
||||
- Check terminal for error messages
|
||||
- Use browser DevTools to debug JavaScript
|
||||
- All original quiz functionality is preserved
|
||||
|
||||
## 📞 Need Help?
|
||||
|
||||
- Check **README.md** for detailed documentation
|
||||
- Check **QUICKSTART.md** for simple instructions
|
||||
- Review Flask logs in terminal for errors
|
||||
- Test API endpoints using browser or Postman
|
||||
|
||||
---
|
||||
|
||||
**Your Flask app is ready to use! 🎊**
|
||||
|
||||
Visit: http://localhost:8080/quiz
|
||||
@@ -0,0 +1,311 @@
|
||||
# Subdirectory Deployment Guide
|
||||
|
||||
This guide explains how to deploy the CGW Product Finder to a subdirectory on your web server (e.g., `http://example.com/cgwproducts/`).
|
||||
|
||||
## 🎯 Overview
|
||||
|
||||
The application is now configured to support subdirectory deployments through the `APPLICATION_ROOT` configuration variable. All templates use relative paths and `url_for()` to ensure proper routing regardless of deployment location.
|
||||
|
||||
## 🛠️ Configuration
|
||||
|
||||
### Method 1: Environment Variable (Recommended for Production)
|
||||
|
||||
Set the `APPLICATION_ROOT` environment variable before starting the application:
|
||||
|
||||
**Linux/Mac (Apache with Passenger):**
|
||||
```bash
|
||||
export APPLICATION_ROOT="/cgwproducts"
|
||||
```
|
||||
|
||||
**Windows (IIS):**
|
||||
Add to web.config or set in IIS environment variables:
|
||||
```
|
||||
APPLICATION_ROOT=/cgwproducts
|
||||
```
|
||||
|
||||
**Apache .htaccess or Virtual Host:**
|
||||
```apache
|
||||
SetEnv APPLICATION_ROOT /cgwproducts
|
||||
```
|
||||
|
||||
### Method 2: Modify config.py
|
||||
|
||||
Edit `app/config.py` and change the APPLICATION_ROOT line:
|
||||
|
||||
```python
|
||||
class Config:
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY') or 'your-secret-here'
|
||||
|
||||
# Change this line to your subdirectory path
|
||||
APPLICATION_ROOT = '/cgwproducts' # Or whatever your path is
|
||||
```
|
||||
|
||||
**Important:** The path should:
|
||||
- Start with `/`
|
||||
- NOT end with `/`
|
||||
- Match your web server configuration
|
||||
|
||||
### Method 3: Server Configuration
|
||||
|
||||
#### Apache with Passenger
|
||||
|
||||
If deploying to `http://example.com/cgwproducts/`:
|
||||
|
||||
```apache
|
||||
<VirtualHost *:80>
|
||||
ServerName example.com
|
||||
|
||||
# Document root is one level up from app folder
|
||||
DocumentRoot /path/to/CGW Product Finder
|
||||
|
||||
# Set subdirectory as alias to app folder
|
||||
Alias /cgwproducts /path/to/CGW Product Finder/app
|
||||
|
||||
<Directory "/path/to/CGW Product Finder/app">
|
||||
Allow from all
|
||||
Options -MultiViews
|
||||
|
||||
# Set APPLICATION_ROOT environment variable
|
||||
SetEnv APPLICATION_ROOT /cgwproducts
|
||||
|
||||
# Enable Passenger
|
||||
PassengerEnabled on
|
||||
PassengerAppRoot /path/to/CGW Product Finder/app
|
||||
PassengerPython /path/to/python3
|
||||
</Directory>
|
||||
|
||||
# Block access to admin and data folders
|
||||
<Directory "/path/to/CGW Product Finder/app/admin">
|
||||
Require all denied
|
||||
</Directory>
|
||||
|
||||
<Directory "/path/to/CGW Product Finder/app/data">
|
||||
# Allow access only through Flask API
|
||||
<FilesMatch "\\.json$">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
</Directory>
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
#### Nginx with uWSGI
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name example.com;
|
||||
|
||||
location /cgwproducts {
|
||||
# Strip the /cgwproducts prefix when passing to Flask
|
||||
rewrite ^/cgwproducts(.*)$ $1 break;
|
||||
|
||||
include uwsgi_params;
|
||||
uwsgi_pass unix:/tmp/cgw-product-finder.sock;
|
||||
|
||||
# Set APPLICATION_ROOT
|
||||
uwsgi_param APPLICATION_ROOT /cgwproducts;
|
||||
}
|
||||
|
||||
# Block admin folder
|
||||
location /cgwproducts/admin {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 📝 Python 3.13.11 Considerations
|
||||
|
||||
For **Python 3.13.11**, some packages may not have pre-built wheels yet.
|
||||
|
||||
### Updated requirements.txt
|
||||
|
||||
The requirements.txt has been updated to make Pillow optional:
|
||||
|
||||
```txt
|
||||
Flask>=3.0.0
|
||||
Werkzeug>=3.0.0
|
||||
# Pillow>=10.0.0 # Optional - only for image generation
|
||||
```
|
||||
|
||||
### Installation Steps
|
||||
|
||||
```bash
|
||||
# Upgrade pip first
|
||||
python -m pip install --upgrade pip
|
||||
|
||||
# Install core requirements (will work without Pillow)
|
||||
pip install Flask>=3.0.0 Werkzeug>=3.0.0
|
||||
|
||||
# Try to install Pillow (optional)
|
||||
pip install Pillow
|
||||
|
||||
# If Pillow fails, the app will still work but disable image generation
|
||||
```
|
||||
|
||||
**Note:** Pillow build errors on Python 3.13.11 are common. If you don't need dynamic image generation, you can skip it.
|
||||
|
||||
## ✅ Testing Your Deployment
|
||||
|
||||
### Step 1: Verify Configuration
|
||||
|
||||
Check that APPLICATION_ROOT is set correctly:
|
||||
|
||||
```python
|
||||
# Run in Python console
|
||||
import os
|
||||
print(os.environ.get('APPLICATION_ROOT', '/'))
|
||||
```
|
||||
|
||||
### Step 2: Test Routes
|
||||
|
||||
If deployed to `/cgwproducts/`, test these URLs:
|
||||
|
||||
- ✅ `http://example.com/cgwproducts/` → Should redirect to login
|
||||
- ✅ `http://example.com/cgwproducts/login` → Should show login page
|
||||
- ✅ `http://example.com/cgwproducts/api/session` → Should return JSON
|
||||
- ✅ `http://example.com/cgwproducts/css/styles.css` → Should load CSS
|
||||
- ✅ `http://example.com/cgwproducts/js/script.js` → Should load JS
|
||||
- ✅ `http://example.com/cgwproducts/data/products.json` → Should load data
|
||||
|
||||
### Step 3: Test Login Flow
|
||||
|
||||
1. Go to `/cgwproducts/login`
|
||||
2. Login with Master / Master
|
||||
3. Should redirect to `/cgwproducts/select-location`
|
||||
4. Select a location
|
||||
5. Should redirect to `/cgwproducts/`
|
||||
6. User info should display in header
|
||||
7. Click logout
|
||||
8. Should return to `/cgwproducts/login`
|
||||
|
||||
### Step 4: Test User Management
|
||||
|
||||
1. Login as Master
|
||||
2. On location selection page, click "User Management"
|
||||
3. Should go to `/cgwproducts/users`
|
||||
4. All buttons should work (Add User, Change Password, etc.)
|
||||
|
||||
## 🔧 Troubleshooting
|
||||
|
||||
### Issue: 404 on CSS/JS files
|
||||
|
||||
**Cause:** Static files not being served correctly
|
||||
|
||||
**Fix:** Ensure Flask routes for /css/, /js/, /data/ are working:
|
||||
|
||||
```bash
|
||||
# Test directly
|
||||
curl http://example.com/cgwproducts/css/styles.css
|
||||
curl http://example.com/cgwproducts/js/script.js
|
||||
```
|
||||
|
||||
### Issue: Login redirects to wrong path
|
||||
|
||||
**Cause:** APPLICATION_ROOT not set or incorrect
|
||||
|
||||
**Fix:**
|
||||
1. Check environment variable: `echo $APPLICATION_ROOT`
|
||||
2. Verify it matches your URL path
|
||||
3. Restart web server after changing
|
||||
|
||||
### Issue: API calls return 404
|
||||
|
||||
**Cause:** API routes need APPLICATION_ROOT prefix
|
||||
|
||||
**Fix:** All templates now use `BASE_URL` variable:
|
||||
```javascript
|
||||
const BASE_URL = '{{ base_url }}'; // Automatically set by Flask
|
||||
fetch(BASE_URL + '/api/login', {...})
|
||||
```
|
||||
|
||||
### Issue: Cannot access /users or other protected pages
|
||||
|
||||
**Cause:** Session not persisting across requests
|
||||
|
||||
**Fix:**
|
||||
1. Verify SECRET_KEY is set and doesn't change between restarts
|
||||
2. Check cookie settings (SESSION_COOKIE_PATH should match APPLICATION_ROOT)
|
||||
3. Ensure browser accepts cookies from subdirectory
|
||||
|
||||
### Issue: Module import errors after pip install
|
||||
|
||||
**Cause:** Pillow build failed on Python 3.13.11
|
||||
|
||||
**Fix:** Pillow is now optional. Application will work without it:
|
||||
```bash
|
||||
# Install without Pillow
|
||||
pip install Flask>=3.0.0 Werkzeug>=3.0.0
|
||||
|
||||
# App will show: "Image generation not available"
|
||||
# But all other features work
|
||||
```
|
||||
|
||||
## 📂 Files Modified for Subdirectory Support
|
||||
|
||||
The following files have been updated to support subdirectory deployments:
|
||||
|
||||
### Backend:
|
||||
- `app/config.py` - Added APPLICATION_ROOT configuration
|
||||
- `app/app.py` - Added context processor for base_url
|
||||
- `app/requirements.txt` - Made Pillow optional
|
||||
|
||||
### Templates (use {{ base_url }} and {{ url_for() }}):
|
||||
- `app/templates/login.html`
|
||||
- `app/templates/select_location.html`
|
||||
- `app/templates/user_manager.html`
|
||||
- `app/templates/index2.html`
|
||||
- `app/templates/access_denied.html`
|
||||
|
||||
### JavaScript Updates:
|
||||
All templates now define `BASE_URL` at the top of their scripts:
|
||||
```javascript
|
||||
const BASE_URL = '{{ base_url }}';
|
||||
```
|
||||
|
||||
All fetch calls use: `fetch(BASE_URL + '/api/endpoint', ...)`
|
||||
|
||||
## 🚀 Deployment Checklist
|
||||
|
||||
Before deploying to a subdirectory:
|
||||
|
||||
- [ ] Set APPLICATION_ROOT environment variable or update config.py
|
||||
- [ ] Install Flask and Werkzeug: `pip install Flask>=3.0.0 Werkzeug>=3.0.0`
|
||||
- [ ] (Optional) Install Pillow: `pip install Pillow`
|
||||
- [ ] Upload all modified files to server
|
||||
- [ ] Configure web server (Apache/Nginx) with subdirectory path
|
||||
- [ ] Set secure SECRET_KEY in production
|
||||
- [ ] Block public access to /admin/ and /data/ folders
|
||||
- [ ] Test all routes with subdirectory prefix
|
||||
- [ ] Test login flow and session persistence
|
||||
- [ ] Verify CSS/JS/images load correctly
|
||||
- [ ] Test API endpoints return correct responses
|
||||
|
||||
## 📞 Need Help?
|
||||
|
||||
Common deployment paths:
|
||||
- Root: `/` (default, no configuration needed)
|
||||
- Application subdirectory: `/cgwproducts`
|
||||
- User subdirectory: `/~username/cgwproducts`
|
||||
- Domain subdirectory: `/app`
|
||||
|
||||
Whatever path you choose, set it as APPLICATION_ROOT and ensure your web server passes requests to Flask with that prefix.
|
||||
|
||||
## 🔐 Security Notes
|
||||
|
||||
When deploying to a subdirectory:
|
||||
|
||||
1. **SECRET_KEY** - Must be set and persistent across restarts
|
||||
2. **Session Cookies** - Will be scoped to the subdirectory path
|
||||
3. **Admin Folder** - Must be blocked from web access
|
||||
4. **Data Folder** - JSON files should only be accessible through API
|
||||
5. **HTTPS** - Use SSL/TLS in production and set SESSION_COOKIE_SECURE = True
|
||||
|
||||
## ✨ Benefits of This Approach
|
||||
|
||||
- ✅ Deploy to any path without code changes
|
||||
- ✅ Works at root `/` or subdirectory `/cgwproducts/`
|
||||
- ✅ All routes automatically adjust to deployment path
|
||||
- ✅ No hardcoded URLs in templates or JavaScript
|
||||
- ✅ Compatible with Apache, Nginx, IIS
|
||||
- ✅ Passenger and uWSGI compatible
|
||||
- ✅ Works with Python 3.13.11 (Pillow optional)
|
||||
@@ -0,0 +1,294 @@
|
||||
# Troubleshooting Guide - 404 Errors & Installation Issues
|
||||
|
||||
## 🔴 Issue 1: pip install requirements.txt fails
|
||||
|
||||
### Error Message:
|
||||
```
|
||||
error: subprocess-exited-with-error
|
||||
× Getting requirements to build wheel did not run successfully.
|
||||
```
|
||||
|
||||
### Solution:
|
||||
|
||||
This error usually happens with **Pillow** on Windows systems that don't have build tools installed.
|
||||
|
||||
#### Option 1: Install Pre-built Pillow (Recommended)
|
||||
```bash
|
||||
# Upgrade pip first
|
||||
python -m pip install --upgrade pip
|
||||
|
||||
# Install Pillow from pre-built wheels
|
||||
pip install --upgrade Pillow
|
||||
|
||||
# Then install other requirements
|
||||
pip install Flask>=3.0.0
|
||||
pip install Werkzeug>=3.0.0
|
||||
```
|
||||
|
||||
#### Option 2: Install from updated requirements.txt
|
||||
The requirements.txt has been updated to use version ranges instead of exact versions:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
#### Option 3: Skip Pillow (if you don't need image generation)
|
||||
If you don't need the dynamic image generation feature, you can install without Pillow:
|
||||
```bash
|
||||
pip install Flask>=3.0.0
|
||||
pip install Werkzeug>=3.0.0
|
||||
```
|
||||
|
||||
The app will still work - it will just disable the image generation features.
|
||||
|
||||
---
|
||||
|
||||
## 🔴 Issue 2: Login Page Returns 404 Error
|
||||
|
||||
### Common Causes & Solutions:
|
||||
|
||||
### Cause 1: Flask Server Not Running
|
||||
**Check if the server is running:**
|
||||
```bash
|
||||
# Navigate to app folder
|
||||
cd "C:\Users\Work\Desktop\CGW Product Finder\app"
|
||||
|
||||
# Run the Flask app
|
||||
python app.py
|
||||
```
|
||||
|
||||
You should see:
|
||||
```
|
||||
✓ Image generation available
|
||||
* Running on http://127.0.0.1:8080
|
||||
```
|
||||
|
||||
**Then access:** http://localhost:8080/login
|
||||
|
||||
---
|
||||
|
||||
### Cause 2: Wrong URL or Port
|
||||
**Common mistakes:**
|
||||
|
||||
❌ `http://localhost/login` - Missing port number
|
||||
✅ `http://localhost:8080/login` - Correct
|
||||
|
||||
❌ `http://localhost:5000/login` - Wrong port
|
||||
✅ `http://localhost:8080/login` - Correct (app uses port 8080)
|
||||
|
||||
❌ `/login` in browser address bar
|
||||
✅ `http://localhost:8080/login` - Need full URL
|
||||
|
||||
---
|
||||
|
||||
### Cause 3: Flask App Import Error
|
||||
**Test if routes are registered:**
|
||||
```bash
|
||||
cd app
|
||||
python test_routes.py
|
||||
```
|
||||
|
||||
This will show:
|
||||
- If Flask imports successfully
|
||||
- All registered routes including /login
|
||||
- What went wrong if there's an error
|
||||
|
||||
**Expected output:**
|
||||
```
|
||||
✓ Flask app imported successfully
|
||||
|
||||
📝 Login & Authentication Routes:
|
||||
/login [GET] -> login_page
|
||||
/api/login [POST] -> login
|
||||
/api/logout [POST] -> logout
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Cause 4: Web Server Configuration (Apache/Passenger/IIS)
|
||||
If you're running through a web server instead of `python app.py`:
|
||||
|
||||
**Check your server configuration:**
|
||||
|
||||
For **Apache + Passenger:**
|
||||
- Verify passenger_wsgi.py is being used
|
||||
- Check if Python path is correct in config
|
||||
- Ensure the app folder is set as DocumentRoot
|
||||
|
||||
For **IIS:**
|
||||
- Verify web.config is correct
|
||||
- Check if Python handler is configured
|
||||
- Ensure proper app folder path
|
||||
|
||||
**Test directly first:**
|
||||
Always test with `python app.py` first to verify the app works before troubleshooting web server issues.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Step-by-Step Troubleshooting
|
||||
|
||||
### Step 1: Verify Installation
|
||||
```bash
|
||||
# Check Python version (need 3.7+)
|
||||
python --version
|
||||
|
||||
# Check if Flask is installed
|
||||
python -c "import flask; print(flask.__version__)"
|
||||
|
||||
# Check if Werkzeug is installed
|
||||
python -c "import werkzeug; print(werkzeug.__version__)"
|
||||
```
|
||||
|
||||
### Step 2: Fresh Install
|
||||
```bash
|
||||
cd "C:\Users\Work\Desktop\CGW Product Finder\app"
|
||||
|
||||
# Upgrade pip
|
||||
python -m pip install --upgrade pip
|
||||
|
||||
# Install requirements
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Or manual install
|
||||
pip install Flask>=3.0.0 Werkzeug>=3.0.0 Pillow>=10.0.0
|
||||
```
|
||||
|
||||
### Step 3: Test Routes
|
||||
```bash
|
||||
cd app
|
||||
python test_routes.py
|
||||
```
|
||||
|
||||
Expected: List of routes including /login
|
||||
|
||||
### Step 4: Start Server
|
||||
```bash
|
||||
python app.py
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```
|
||||
✓ Image generation available
|
||||
* Running on http://127.0.0.1:8080
|
||||
* Running on http://192.168.x.x:8080
|
||||
```
|
||||
|
||||
### Step 5: Access in Browser
|
||||
Open browser: http://localhost:8080/login
|
||||
|
||||
Expected: Login page with username/password fields
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Common Errors & Fixes
|
||||
|
||||
### Error: "ModuleNotFoundError: No module named 'flask'"
|
||||
```bash
|
||||
pip install Flask>=3.0.0
|
||||
```
|
||||
|
||||
### Error: "ModuleNotFoundError: No module named 'werkzeug'"
|
||||
```bash
|
||||
pip install Werkzeug>=3.0.0
|
||||
```
|
||||
|
||||
### Error: "Address already in use" / "Port 8080 is already in use"
|
||||
```bash
|
||||
# Find what's using port 8080
|
||||
netstat -ano | findstr :8080
|
||||
|
||||
# Kill the process (replace PID with actual process ID)
|
||||
taskkill /PID <PID> /F
|
||||
|
||||
# Or edit app.py to use different port (line 911):
|
||||
app.run(debug=True, host='0.0.0.0', port=8090)
|
||||
```
|
||||
|
||||
### Error: "Template Not Found: login.html"
|
||||
```bash
|
||||
# Verify template exists
|
||||
dir templates\login.html
|
||||
|
||||
# If missing, the file needs to be uploaded to the server
|
||||
```
|
||||
|
||||
### Error: "Working outside of application context"
|
||||
This means Flask app isn't initialized properly. Run `python test_routes.py` to diagnose.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Verification Checklist
|
||||
|
||||
After fixing issues, verify:
|
||||
|
||||
- [ ] Flask server starts without errors: `python app.py`
|
||||
- [ ] http://localhost:8080/ redirects to http://localhost:8080/login ✓
|
||||
- [ ] http://localhost:8080/login shows login page ✓
|
||||
- [ ] Can login with username: `Master` password: `Master` ✓
|
||||
- [ ] After login, see location selection page ✓
|
||||
- [ ] Can access user management (Master user only) ✓
|
||||
- [ ] Logout works and returns to login page ✓
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Production Deployment
|
||||
|
||||
For production servers (not localhost):
|
||||
|
||||
1. **Use proper WSGI server** (not `python app.py`)
|
||||
- Passenger (Apache/Nginx)
|
||||
- uWSGI
|
||||
- Gunicorn (Linux)
|
||||
|
||||
2. **Set secure SECRET_KEY** in config.py
|
||||
```python
|
||||
# Don't use secrets.token_hex() in production
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY') or 'your-secure-random-key-here'
|
||||
```
|
||||
|
||||
3. **Enable HTTPS** and update config:
|
||||
```python
|
||||
SESSION_COOKIE_SECURE = True # Only send cookies over HTTPS
|
||||
```
|
||||
|
||||
4. **Set Debug=False** for production
|
||||
|
||||
---
|
||||
|
||||
## 📞 Still Having Issues?
|
||||
|
||||
1. Run the test script:
|
||||
```bash
|
||||
python test_routes.py
|
||||
```
|
||||
|
||||
2. Check Flask server output for errors:
|
||||
```bash
|
||||
python app.py
|
||||
```
|
||||
Look for red error messages
|
||||
|
||||
3. Test with curl:
|
||||
```bash
|
||||
curl http://localhost:8080/login
|
||||
```
|
||||
Should return HTML, not 404
|
||||
|
||||
4. Check browser console for JavaScript errors (F12)
|
||||
|
||||
5. Verify file permissions (server can read templates/)
|
||||
|
||||
---
|
||||
|
||||
## 📁 Files Needed for Login System
|
||||
|
||||
Ensure these files exist and are uploaded:
|
||||
- ✅ app/app.py
|
||||
- ✅ app/templates/login.html
|
||||
- ✅ app/templates/user_manager.html
|
||||
- ✅ app/templates/access_denied.html
|
||||
- ✅ app/templates/select_location.html
|
||||
- ✅ app/templates/index2.html
|
||||
- ✅ app/css/styles.css
|
||||
- ✅ app/data/users.json
|
||||
|
||||
Missing any of these will cause 404 or errors.
|
||||
@@ -0,0 +1,352 @@
|
||||
# URL State Example - Navigation Flow
|
||||
|
||||
## 📍 Visual Journey Through URL Changes
|
||||
|
||||
### Step 1: Page Load
|
||||
```
|
||||
URL: https://yoursite.com/
|
||||
State: Fresh start, no parameters
|
||||
Bit Value: 0
|
||||
```
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ What are you looking for? │
|
||||
│ ┌─────┐ ┌─────┐ │
|
||||
│ │ 🚪 │ │ 🪟 │ │
|
||||
│ │Door │ │Window│ │
|
||||
│ └─────┘ └─────┘ │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: User Selects "Door"
|
||||
```
|
||||
URL: https://yoursite.com/?b=1&q=q-door-type
|
||||
↑
|
||||
Bit 0 set (Door)
|
||||
State: Door selected
|
||||
Bit Value: 1 (binary: 1)
|
||||
Bits Active: Door
|
||||
```
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ What type of door? │
|
||||
│ ┌──────────┐ ┌──────────┐ │
|
||||
│ │Storm Door│ │Patio Door│ │
|
||||
│ └──────────┘ └──────────┘ │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: User Selects "Storm Door"
|
||||
```
|
||||
URL: https://yoursite.com/?b=16385&q=q-material-storm-door
|
||||
↑
|
||||
16385 = 1 (Door) + 16384 (Storm Door)
|
||||
State: Door + Storm Door selected
|
||||
Bit Value: 16385 (binary: 100000000000001)
|
||||
Bits Active: Door, Storm Door subtype
|
||||
|
||||
Calculation:
|
||||
Bit 0 (Door) = 1
|
||||
Bit 14 (Storm Door)= 16384
|
||||
Total = 16385
|
||||
```
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ Select Material │
|
||||
│ CONDITIONAL: Only showing │
|
||||
│ because some storm doors have │
|
||||
│ multiple materials │
|
||||
│ ┌─────────┐ ┌─────────┐ │
|
||||
│ │Aluminum │ │ Vinyl │ │
|
||||
│ └─────────┘ └─────────┘ │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 4: User Selects "Aluminum"
|
||||
```
|
||||
URL: https://yoursite.com/?b=16401&q=q-color-storm-door
|
||||
↑
|
||||
16401 = 1 + 16 + 16384
|
||||
State: Door + Aluminum + Storm Door
|
||||
Bit Value: 16401 (binary: 100000000010001)
|
||||
Bits Active: Door, Aluminum, Storm Door subtype
|
||||
|
||||
Calculation:
|
||||
Bit 0 (Door) = 1
|
||||
Bit 4 (Aluminum) = 16
|
||||
Bit 14 (Storm Door)= 16384
|
||||
Total = 16401
|
||||
```
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ Select Color │
|
||||
│ ┌───────┐ ┌───────┐ ┌───────┐ │
|
||||
│ │ Black │ │ White │ │Bronze │ │
|
||||
│ └───────┘ └───────┘ └───────┘ │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 5: User Selects "White"
|
||||
```
|
||||
URL: https://yoursite.com/?b=16529&q=q-dimensions
|
||||
↑
|
||||
16529 = 1 + 16 + 128 + 16384
|
||||
State: Door + Aluminum + White + Storm Door
|
||||
Bit Value: 16529 (binary: 100000010010001)
|
||||
Bits Active: Door, Aluminum, White, Storm Door
|
||||
|
||||
Calculation:
|
||||
Bit 0 (Door) = 1
|
||||
Bit 4 (Aluminum) = 16
|
||||
Bit 7 (White) = 128
|
||||
Bit 14 (Storm Door)= 16384
|
||||
Total = 16529
|
||||
```
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ Enter Dimensions │
|
||||
│ Width: [______] inches │
|
||||
│ Height: [______] inches │
|
||||
│ [Continue →] │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 6: View Results
|
||||
```
|
||||
URL: https://yoursite.com/?b=16529&q=results
|
||||
State: Showing filtered products
|
||||
Bit Value: 16529
|
||||
Products Matched: All doors that are:
|
||||
✓ Storm Door type (bit 14)
|
||||
✓ Aluminum material (bit 4)
|
||||
✓ Available in White (bit 7)
|
||||
```
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ Found 3 Products │
|
||||
│ ┌─────────────────────────┐ │
|
||||
│ │ STAR 6100 FULL VIEW │ │
|
||||
│ │ Code: 6100I │ │
|
||||
│ │ Aluminum, White, Bronze │ │
|
||||
│ │ [View Details] │ │
|
||||
│ └─────────────────────────┘ │
|
||||
│ ┌─────────────────────────┐ │
|
||||
│ │ COLUMBIA COBRA │ │
|
||||
│ │ Code: COBRAI │ │
|
||||
│ │ [View Details] │ │
|
||||
│ └─────────────────────────┘ │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 7: Click Product Detail
|
||||
```
|
||||
URL: https://yoursite.com/?p=6100I&b=16529
|
||||
↑
|
||||
Product code added
|
||||
State: Viewing specific product 6100I
|
||||
Bit Value: 16529 (preserved for "back" navigation)
|
||||
```
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ STAR 6100 FULL VIEW STORM DOOR │
|
||||
│ ────────────────────────────────── │
|
||||
│ Product Code: 6100I │
|
||||
│ Category: STD │
|
||||
│ Materials: Aluminum │
|
||||
│ Colors: White, Bronze │
|
||||
│ │
|
||||
│ Compatible Accessories: │
|
||||
│ • BGIST - Inserts for Storm Doors │
|
||||
│ • TGIODD - Top Glass Inserts │
|
||||
│ │
|
||||
│ [← Back] [Start Over] [🔗 Share] │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 8: Copy & Share URL
|
||||
```
|
||||
User clicks "🔗 Share" button
|
||||
Copies: https://yoursite.com/?p=6100I&b=16529
|
||||
|
||||
Someone else pastes this URL in their browser:
|
||||
→ Instantly shows product 6100I
|
||||
→ Navigation state preserved (b=16529)
|
||||
→ Back button navigates through original path
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 9: Browser Back Button
|
||||
```
|
||||
User clicks browser back button
|
||||
|
||||
URL Changes:
|
||||
https://yoursite.com/?p=6100I&b=16529
|
||||
↓
|
||||
https://yoursite.com/?b=16529&q=results
|
||||
↓
|
||||
https://yoursite.com/?b=16529&q=q-dimensions
|
||||
↓
|
||||
https://yoursite.com/?b=16401&q=q-color-storm-door
|
||||
↓
|
||||
https://yoursite.com/?b=16385&q=q-material-storm-door
|
||||
↓
|
||||
https://yoursite.com/?b=1&q=q-door-type
|
||||
↓
|
||||
https://yoursite.com/
|
||||
|
||||
Each step backward:
|
||||
✓ Restores question state
|
||||
✓ Maintains bit value
|
||||
✓ Shows correct UI
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 10: Refresh Page
|
||||
```
|
||||
At any point, user presses F5 to refresh
|
||||
|
||||
Before Refresh: https://yoursite.com/?b=16529&q=q-dimensions
|
||||
After Refresh: https://yoursite.com/?b=16529&q=q-dimensions
|
||||
|
||||
Result:
|
||||
✓ Returns to same question
|
||||
✓ Previous answers restored
|
||||
✓ Accumulated selections preserved
|
||||
✓ Can continue where they left off
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Bit Value Breakdown Reference
|
||||
|
||||
### Common Values You'll See
|
||||
|
||||
| Bit Value | Decimal | What It Means |
|
||||
|-----------|---------|---------------|
|
||||
| `0000...0001` | 1 | Door selected |
|
||||
| `0000...0010` | 2 | Window selected |
|
||||
| `0001...0001` | 16385 | Door + Storm Door |
|
||||
| `0001...0011` | 16387 | Door + Window + Storm Door |
|
||||
| `0001...0011` | 16401 | Door + Aluminum + Storm Door |
|
||||
| `0010...0010` | 16529 | Door + Aluminum + White + Storm Door |
|
||||
|
||||
### Quick Decode Formula
|
||||
```
|
||||
To check if a bit is set:
|
||||
if (bitValue & (1 << bitPosition)) {
|
||||
// That attribute is selected
|
||||
}
|
||||
|
||||
Examples:
|
||||
16529 & 1 = 1 → Door is selected ✓
|
||||
16529 & 2 = 0 → Window is NOT selected
|
||||
16529 & 16 = 16 → Aluminum is selected ✓
|
||||
16529 & 128 = 128 → White is selected ✓
|
||||
16529 & 16384 = 16384 → Storm Door is selected ✓
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 URL Patterns
|
||||
|
||||
### Navigation State
|
||||
```
|
||||
Pattern: /?b={bitValue}&q={questionKey}
|
||||
Example: /?b=16529&q=q-dimensions
|
||||
Use: Bookmark navigation progress
|
||||
```
|
||||
|
||||
### Product View
|
||||
```
|
||||
Pattern: /?p={productCode}&b={bitValue}
|
||||
Example: /?p=6100I&b=16529
|
||||
Use: Direct link to product with context
|
||||
```
|
||||
|
||||
### Initial State
|
||||
```
|
||||
Pattern: /
|
||||
Example: https://yoursite.com/
|
||||
Use: Fresh start, no parameters
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 Power User Features
|
||||
|
||||
### Hack the URL
|
||||
Users can manually modify URLs:
|
||||
|
||||
```
|
||||
Original: /?b=16529&q=q-dimensions
|
||||
(Door + Aluminum + White + Storm Door)
|
||||
|
||||
Modified: /?b=34&q=q-dimensions
|
||||
(Vinyl only)
|
||||
|
||||
Result: Jumps directly to Vinyl products
|
||||
```
|
||||
|
||||
### Preset Configurations
|
||||
Your sales team can create bookmarks:
|
||||
|
||||
```
|
||||
Residential Storm Doors:
|
||||
/?b=16401&q=results
|
||||
|
||||
Commercial Windows:
|
||||
/?b=8194&q=results
|
||||
|
||||
Budget Options (Vinyl):
|
||||
/?b=32&q=start
|
||||
```
|
||||
|
||||
### Analytics Tracking
|
||||
Track which combinations are popular:
|
||||
|
||||
```
|
||||
Top URLs:
|
||||
/?b=16529 → White Aluminum Storm Doors (2,453 views)
|
||||
/?b=8226 → White Vinyl Windows (1,834 views)
|
||||
/?b=272 → Bronze products (892 views)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎓 Learning Example
|
||||
|
||||
### Try This Exercise:
|
||||
1. Start at homepage (b=0)
|
||||
2. Select Window (b=2)
|
||||
3. Select Primary Window (b=8194)
|
||||
4. Select Vinyl (b=8226)
|
||||
5. Select White (b=8354)
|
||||
|
||||
Your URL should be: `/?b=8354&q=q-dimensions`
|
||||
|
||||
Decode: 8354 in binary = 10000010100010
|
||||
- Bit 1 (2) = Window ✓
|
||||
- Bit 5 (32) = Vinyl ✓
|
||||
- Bit 7 (128) = White ✓
|
||||
- Bit 13 (8192) = Primary Window ✓
|
||||
Total: 2 + 32 + 128 + 8192 = 8354 ✓
|
||||
|
||||
---
|
||||
|
||||
Ready to implement! 🚀
|
||||
@@ -0,0 +1,230 @@
|
||||
# Implementation Summary - URL State Management
|
||||
|
||||
## 🎯 What You Get
|
||||
|
||||
Your application will support **shareable URLs** that preserve:
|
||||
- ✅ User's navigation progress (bitwise value)
|
||||
- ✅ Current question position
|
||||
- ✅ Direct product links
|
||||
- ✅ Browser refresh without data loss
|
||||
- ✅ Browser back/forward buttons
|
||||
- ✅ Copy/share functionality
|
||||
|
||||
## 🔗 URL Examples
|
||||
|
||||
```
|
||||
# Initial state (no params)
|
||||
https://yoursite.com/
|
||||
|
||||
# After selecting Door + Storm Door + Aluminum
|
||||
https://yoursite.com/?b=16401&q=q-dimensions
|
||||
↑ ↑
|
||||
| Current question
|
||||
Accumulated bit value (Door + Aluminum + Storm Door)
|
||||
|
||||
# Viewing specific product
|
||||
https://yoursite.com/?p=BGIST&b=16401
|
||||
↑
|
||||
Product code
|
||||
|
||||
# Bit value 16401 decodes to:
|
||||
# Bit 0 (1) = Door
|
||||
# Bit 4 (16) = Aluminum
|
||||
# Bit 14 (16384) = Storm Door subtype
|
||||
# Total: 1 + 16 + 16384 = 16401
|
||||
```
|
||||
|
||||
## 📋 Implementation Checklist
|
||||
|
||||
### Phase 1: Core URL Functionality (Essential)
|
||||
- [ ] Add bitwise data loading to `init()` function
|
||||
- [ ] Add `accumulatedBitValue` variable
|
||||
- [ ] Add `BIT_DEFINITIONS` constants
|
||||
- [ ] Implement `updateURL()` function
|
||||
- [ ] Implement `updateBitValue()` function
|
||||
- [ ] Update `handleAnswer()` to call `updateBitValue()`
|
||||
- [ ] Implement `initFromURL()` function
|
||||
- [ ] Implement `restoreStateFromBitValue()` function
|
||||
- [ ] Update `startOver()` to clear URL
|
||||
|
||||
### Phase 2: Product Deep Linking (Recommended)
|
||||
- [ ] Implement `showProductByCode()` function
|
||||
- [ ] Update `showProductDetail()` to update URL
|
||||
- [ ] Make product cards clickable
|
||||
- [ ] Add URL params when viewing products
|
||||
|
||||
### Phase 3: Share Functionality (Nice to Have)
|
||||
- [ ] Implement `shareCurrentPage()` function
|
||||
- [ ] Add `showNotification()` helper
|
||||
- [ ] Add "Share" buttons to UI
|
||||
- [ ] Add CSS for notification animations
|
||||
|
||||
### Phase 4: Browser Navigation (Polish)
|
||||
- [ ] Add `popstate` event listener
|
||||
- [ ] Test browser back button
|
||||
- [ ] Test browser forward button
|
||||
- [ ] Test refresh behavior
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Step 1: Add Global Variables
|
||||
Add to top of `js/script.js`:
|
||||
```javascript
|
||||
let accumulatedBitValue = 0;
|
||||
let bitwiseData = {};
|
||||
|
||||
const BIT_DEFINITIONS = {
|
||||
'base_door': 0, 'base_window': 1,
|
||||
'material_aluminum': 4, 'material_vinyl': 5,
|
||||
'color_black': 6, 'color_white': 7, 'color_bronze': 8,
|
||||
'color_tan': 9, 'color_mill': 10, 'color_sandstone': 11,
|
||||
'subtype_patio_door': 12, 'subtype_primary_window': 13,
|
||||
'subtype_storm_door': 14, 'subtype_storm_window': 15
|
||||
};
|
||||
```
|
||||
|
||||
### Step 2: Load Bitwise Data
|
||||
Update `init()` to load `product_bitwise.json`:
|
||||
```javascript
|
||||
Promise.all([
|
||||
fetch('data/navigation.json').then(r => r.json()),
|
||||
fetch('data/products.json').then(r => r.json()),
|
||||
fetch('data/accessories.json').then(r => r.json()),
|
||||
fetch('data/product_bitwise.json').then(r => r.json()) // ADD THIS
|
||||
])
|
||||
.then(([questions, products, accessories, bitwise]) => {
|
||||
// ... existing code ...
|
||||
|
||||
// Index bitwise data
|
||||
bitwiseData = {};
|
||||
bitwise.forEach(item => {
|
||||
bitwiseData[item.PROD_CODE] = item.BIT_VALUE;
|
||||
});
|
||||
|
||||
// Check for URL state
|
||||
initFromURL();
|
||||
});
|
||||
```
|
||||
|
||||
### Step 3: Add URL Functions
|
||||
Copy these three key functions from [URL_STATE_MANAGEMENT.md](URL_STATE_MANAGEMENT.md):
|
||||
1. `updateURL()` - Updates browser URL
|
||||
2. `updateBitValue()` - Calculates bit value from selection
|
||||
3. `initFromURL()` - Restores state from URL on load
|
||||
|
||||
### Step 4: Update handleAnswer
|
||||
Add one line to `handleAnswer()`:
|
||||
```javascript
|
||||
function handleAnswer(currentKey, answerValue, answerIndex) {
|
||||
// ... existing code ...
|
||||
|
||||
updateBitValue(answerObject); // ADD THIS LINE
|
||||
|
||||
const nextKey = resolveNextQuestion(currentKey, answerValue, answerObject);
|
||||
history.push(nextKey);
|
||||
|
||||
updateURL(nextKey); // ADD THIS LINE
|
||||
|
||||
loadContent(nextKey);
|
||||
}
|
||||
```
|
||||
|
||||
### Step 5: Test
|
||||
1. Run your app
|
||||
2. Navigate through questions
|
||||
3. Check URL updates after each selection
|
||||
4. Copy URL and paste in new tab
|
||||
5. Should restore to same state ✅
|
||||
|
||||
## 📖 Documentation Files
|
||||
|
||||
All details are in these files:
|
||||
- **[URL_STATE_MANAGEMENT.md](URL_STATE_MANAGEMENT.md)** - Complete implementation guide
|
||||
- **[REQUIRED_CODE_CHANGES.md](REQUIRED_CODE_CHANGES.md)** - Updated with URL features
|
||||
- **[BITWISE_USAGE_GUIDE.md](BITWISE_USAGE_GUIDE.md)** - How bitwise system works
|
||||
|
||||
## 🔧 Key Functions Reference
|
||||
|
||||
| Function | Purpose | When Called |
|
||||
|----------|---------|-------------|
|
||||
| `initFromURL()` | Read URL params on page load | Once at startup |
|
||||
| `updateURL()` | Write current state to URL | After each answer |
|
||||
| `updateBitValue()` | Add selection to bit value | After each answer |
|
||||
| `restoreStateFromBitValue()` | Decode bit value to selections | On page load from URL |
|
||||
| `shareCurrentPage()` | Copy URL to clipboard | User clicks "Share" |
|
||||
|
||||
## 🎨 URL Format Design
|
||||
|
||||
### Why Bitwise?
|
||||
- **Compact**: `?b=16401` vs `?door=true&aluminum=true&storm=true`
|
||||
- **Fast**: Single integer comparison
|
||||
- **Flexible**: Easy to add new attributes
|
||||
- **Shareable**: Short URLs
|
||||
- **Reversible**: Can decode back to selections
|
||||
|
||||
### Parameters Chosen
|
||||
- `b` = bit value (short, recognizable)
|
||||
- `q` = question (short, clear purpose)
|
||||
- `p` = product (short, clear purpose)
|
||||
|
||||
### Alternative Considered
|
||||
Could use hash fragments instead:
|
||||
```
|
||||
https://yoursite.com/#/door/storm-door/aluminum
|
||||
```
|
||||
But query params are better for:
|
||||
- Server-side rendering
|
||||
- Analytics tracking
|
||||
- SEO (if products are indexed)
|
||||
|
||||
## ⚠️ Important Notes
|
||||
|
||||
1. **Bit Definitions Must Match**: The `BIT_DEFINITIONS` in JavaScript must match the Python script
|
||||
2. **URL Length Limits**: URLs have practical limits (~2000 chars), but bit values are small
|
||||
3. **No Sensitive Data**: Don't put sensitive info in URL (it's visible and loggable)
|
||||
4. **Test Thoroughly**: Test all navigation paths and browser actions
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### URL Not Updating
|
||||
- Check `updateURL()` is called after `handleAnswer()`
|
||||
- Check browser console for errors
|
||||
- Verify `accumulatedBitValue` is being set
|
||||
|
||||
### State Not Restoring
|
||||
- Check `initFromURL()` is called in `init()`
|
||||
- Verify URL has `b` and `q` parameters
|
||||
- Check `restoreStateFromBitValue()` logic
|
||||
|
||||
### Wrong Bit Values
|
||||
- Verify `BIT_DEFINITIONS` matches `generate_bitwise_helper.py`
|
||||
- Check `bitwise_legend.json` for correct bit positions
|
||||
- Use browser console: `console.log(accumulatedBitValue)`
|
||||
|
||||
### Share Button Not Working
|
||||
- Check clipboard API support: `navigator.clipboard`
|
||||
- Fallback to `prompt()` for older browsers
|
||||
- Test in HTTPS (clipboard API requires secure context)
|
||||
|
||||
## 📈 Benefits Summary
|
||||
|
||||
| Feature | User Benefit | Business Benefit |
|
||||
|---------|-------------|------------------|
|
||||
| Shareable URLs | Share configurations | Viral marketing |
|
||||
| Bookmarks | Save favorites | Return visitors |
|
||||
| Refresh-safe | No data loss | Better UX |
|
||||
| Deep linking | Direct to product | SEO indexing |
|
||||
| Browser nav | Back/forward works | Expected behavior |
|
||||
| Short URLs | Easy to share | More sharing |
|
||||
|
||||
## 🎯 Next Steps
|
||||
|
||||
1. ✅ Implement Phase 1 (core URL functionality)
|
||||
2. ✅ Test basic URL state restoration
|
||||
3. ✅ Add Phase 2 (product deep linking)
|
||||
4. ✅ Add Phase 3 (share buttons)
|
||||
5. ✅ Add Phase 4 (browser navigation)
|
||||
6. ✅ Test all scenarios thoroughly
|
||||
7. ✅ Add analytics tracking (optional)
|
||||
|
||||
Good luck! 🚀
|
||||
@@ -0,0 +1,514 @@
|
||||
# URL State Management Implementation
|
||||
|
||||
## Overview
|
||||
This feature allows users to:
|
||||
1. **Bookmark** their progress through the navigation flow
|
||||
2. **Refresh** the page without losing their selections
|
||||
3. **Share** URLs with specific products or navigation states
|
||||
4. **Deep link** directly to products
|
||||
|
||||
## URL Parameter Structure
|
||||
|
||||
### Query Parameters
|
||||
- `b` - Bitwise value representing all selections (integer)
|
||||
- `q` - Current question ID (string)
|
||||
- `p` - Product code for direct product view (string)
|
||||
|
||||
### Examples
|
||||
```
|
||||
# At start
|
||||
https://yoursite.com/
|
||||
|
||||
# After selecting Door > Storm Door > Aluminum
|
||||
https://yoursite.com/?b=16401&q=q-dimensions
|
||||
|
||||
# Viewing a specific product
|
||||
https://yoursite.com/?p=BGIST&b=16401
|
||||
|
||||
# Bit value 16401 decodes to:
|
||||
# - Door (bit 0)
|
||||
# - Aluminum (bit 4)
|
||||
# - Storm Door subtype (bit 14)
|
||||
```
|
||||
|
||||
## Implementation Code
|
||||
|
||||
### 1. Add to Global Variables (top of script.js)
|
||||
```javascript
|
||||
// Add these to existing global variables
|
||||
let accumulatedBitValue = 0;
|
||||
let bitwiseData = {};
|
||||
|
||||
// Bit definitions (must match generate_bitwise_helper.py)
|
||||
const BIT_DEFINITIONS = {
|
||||
// Base Types
|
||||
'base_door': 0, // 1
|
||||
'base_window': 1, // 2
|
||||
|
||||
// Flags
|
||||
'is_accessory': 2, // 4
|
||||
'specific_item': 3, // 8
|
||||
|
||||
// Materials
|
||||
'material_aluminum': 4, // 16
|
||||
'material_vinyl': 5, // 32
|
||||
|
||||
// Colors
|
||||
'color_black': 6, // 64
|
||||
'color_white': 7, // 128
|
||||
'color_bronze': 8, // 256
|
||||
'color_tan': 9, // 512
|
||||
'color_mill': 10, // 1024
|
||||
'color_sandstone': 11, // 2048
|
||||
|
||||
// Subtypes (from bitwise_legend.json)
|
||||
'subtype_patio_door': 12, // 4096
|
||||
'subtype_primary_window': 13, // 8192
|
||||
'subtype_storm_door': 14, // 16384
|
||||
'subtype_storm_window': 15 // 32768
|
||||
};
|
||||
```
|
||||
|
||||
### 2. Update init() Function
|
||||
```javascript
|
||||
function init() {
|
||||
// Load all data files (including bitwise)
|
||||
Promise.all([
|
||||
fetch('data/navigation.json').then(r => r.json()),
|
||||
fetch('data/products.json').then(r => r.json()),
|
||||
fetch('data/accessories.json').then(r => r.json()),
|
||||
fetch('data/product_bitwise.json').then(r => r.json())
|
||||
])
|
||||
.then(([questions, products, accessories, bitwise]) => {
|
||||
questionData = questions;
|
||||
productData = products;
|
||||
accessoryData = accessories;
|
||||
|
||||
// Index bitwise data by product code
|
||||
bitwiseData = {};
|
||||
bitwise.forEach(item => {
|
||||
bitwiseData[item.PROD_CODE] = item.BIT_VALUE;
|
||||
});
|
||||
|
||||
// Initialize from URL or start fresh
|
||||
initFromURL();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading data:', error);
|
||||
showError('Failed to load application data.');
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 3. URL Initialization
|
||||
```javascript
|
||||
function initFromURL() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
|
||||
// Priority 1: Direct product view
|
||||
const productCode = params.get('p');
|
||||
if (productCode) {
|
||||
accumulatedBitValue = parseInt(params.get('b') || '0', 10);
|
||||
showProductByCode(productCode);
|
||||
return;
|
||||
}
|
||||
|
||||
// Priority 2: Restore navigation state
|
||||
const bitValue = params.get('b');
|
||||
const questionKey = params.get('q');
|
||||
|
||||
if (bitValue && questionKey) {
|
||||
accumulatedBitValue = parseInt(bitValue, 10);
|
||||
restoreStateFromBitValue(accumulatedBitValue, questionKey);
|
||||
return;
|
||||
}
|
||||
|
||||
// Priority 3: Start fresh
|
||||
loadContent('start');
|
||||
}
|
||||
|
||||
function restoreStateFromBitValue(bitValue, questionKey) {
|
||||
// Decode bit value back to user selections
|
||||
|
||||
// Base type
|
||||
if (bitValue & 1) {
|
||||
userAnswers['start'] = 'Door';
|
||||
} else if (bitValue & 2) {
|
||||
userAnswers['start'] = 'Window';
|
||||
}
|
||||
|
||||
// Materials
|
||||
if (bitValue & 16) {
|
||||
userAnswers['q-material.material'] = 'Aluminum';
|
||||
} else if (bitValue & 32) {
|
||||
userAnswers['q-material.material'] = 'Vinyl';
|
||||
}
|
||||
|
||||
// Colors
|
||||
const colorMap = {
|
||||
64: 'Black',
|
||||
128: 'White',
|
||||
256: 'Bronze',
|
||||
512: 'Tan',
|
||||
1024: 'Mill',
|
||||
2048: 'Sandstone'
|
||||
};
|
||||
|
||||
for (const [bit, color] of Object.entries(colorMap)) {
|
||||
if (bitValue & parseInt(bit)) {
|
||||
userAnswers['q-color.color'] = color;
|
||||
break; // Only store first color found
|
||||
}
|
||||
}
|
||||
|
||||
// Subtypes
|
||||
const subtypeMap = {
|
||||
4096: 'Patio Door',
|
||||
8192: 'Primary Window',
|
||||
16384: 'Storm Door',
|
||||
32768: 'Storm Window'
|
||||
};
|
||||
|
||||
for (const [bit, subtype] of Object.entries(subtypeMap)) {
|
||||
if (bitValue & parseInt(bit)) {
|
||||
userAnswers['q-subtype'] = subtype;
|
||||
break; // Only store first subtype found
|
||||
}
|
||||
}
|
||||
|
||||
// Navigate to the saved question
|
||||
history = ['start'];
|
||||
if (questionKey !== 'start') {
|
||||
history.push(questionKey);
|
||||
}
|
||||
loadContent(questionKey);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Update URL on Each Selection
|
||||
```javascript
|
||||
function updateURL(questionKey = null) {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (accumulatedBitValue > 0) {
|
||||
params.set('b', accumulatedBitValue.toString());
|
||||
}
|
||||
|
||||
if (questionKey) {
|
||||
params.set('q', questionKey);
|
||||
} else if (history.length > 0) {
|
||||
params.set('q', history[history.length - 1]);
|
||||
}
|
||||
|
||||
const newURL = window.location.pathname + (params.toString() ? '?' + params.toString() : '');
|
||||
window.history.replaceState(
|
||||
{
|
||||
bitValue: accumulatedBitValue,
|
||||
questionKey: questionKey
|
||||
},
|
||||
'',
|
||||
newURL
|
||||
);
|
||||
}
|
||||
|
||||
function updateBitValue(answerObject) {
|
||||
if (!answerObject.filter) return;
|
||||
|
||||
const filter = answerObject.filter;
|
||||
|
||||
// Base type
|
||||
if (filter.baseType === 'Door') {
|
||||
accumulatedBitValue |= (1 << BIT_DEFINITIONS['base_door']);
|
||||
} else if (filter.baseType === 'Window') {
|
||||
accumulatedBitValue |= (1 << BIT_DEFINITIONS['base_window']);
|
||||
}
|
||||
|
||||
// Materials
|
||||
if (filter.material === 'Aluminum') {
|
||||
accumulatedBitValue |= (1 << BIT_DEFINITIONS['material_aluminum']);
|
||||
} else if (filter.material === 'Vinyl') {
|
||||
accumulatedBitValue |= (1 << BIT_DEFINITIONS['material_vinyl']);
|
||||
}
|
||||
|
||||
// Colors
|
||||
const colorKey = filter.color ? 'color_' + filter.color.toLowerCase() : null;
|
||||
if (colorKey && BIT_DEFINITIONS[colorKey]) {
|
||||
accumulatedBitValue |= (1 << BIT_DEFINITIONS[colorKey]);
|
||||
}
|
||||
|
||||
// Subtypes
|
||||
const subtypeKey = filter.subType ? 'subtype_' + filter.subType.toLowerCase().replace(/ /g, '_') : null;
|
||||
if (subtypeKey && BIT_DEFINITIONS[subtypeKey]) {
|
||||
accumulatedBitValue |= (1 << BIT_DEFINITIONS[subtypeKey]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Update handleAnswer Function
|
||||
```javascript
|
||||
function handleAnswer(currentKey, answerValue, answerIndex) {
|
||||
const currentQuestion = questionData[currentKey];
|
||||
const answerObject = currentQuestion.answers[answerIndex];
|
||||
|
||||
// Store the answer
|
||||
userAnswers[currentKey] = answerValue;
|
||||
|
||||
// Update bit value based on selection
|
||||
updateBitValue(answerObject);
|
||||
|
||||
// Resolve the next question (handles conditionals)
|
||||
const nextKey = resolveNextQuestion(currentKey, answerValue, answerObject);
|
||||
|
||||
history.push(nextKey);
|
||||
|
||||
// Update URL with new state
|
||||
updateURL(nextKey);
|
||||
|
||||
loadContent(nextKey);
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Product View with URL
|
||||
```javascript
|
||||
function showProductByCode(productCode) {
|
||||
const product = productData.find(p => p.productCode === productCode);
|
||||
if (!product) {
|
||||
showError(`Product ${productCode} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
showProductDetail(product);
|
||||
}
|
||||
|
||||
function showProductDetail(product) {
|
||||
// Update URL with product code
|
||||
const params = new URLSearchParams();
|
||||
params.set('p', product.productCode);
|
||||
if (accumulatedBitValue > 0) {
|
||||
params.set('b', accumulatedBitValue.toString());
|
||||
}
|
||||
|
||||
window.history.pushState(
|
||||
{ productCode: product.productCode },
|
||||
'',
|
||||
'?' + params.toString()
|
||||
);
|
||||
|
||||
// Render product detail
|
||||
const compatibleAccessories = accessoryData.filter(acc =>
|
||||
product.compatibleAccessories && product.compatibleAccessories.includes(acc.id)
|
||||
);
|
||||
|
||||
const contentDiv = document.getElementById('content');
|
||||
let html = `
|
||||
<div class="result-container">
|
||||
<div class="result-title">${product.description}</div>
|
||||
<div class="result-content">
|
||||
<div class="result-details">
|
||||
<p><strong>Product Code:</strong> ${product.productCode}</p>
|
||||
<p><strong>Category:</strong> ${product.category}</p>
|
||||
<p><strong>Base Type:</strong> ${product.baseType || 'N/A'}</p>
|
||||
<p><strong>Materials:</strong> ${product.materials.join(', ') || 'N/A'}</p>
|
||||
<p><strong>Colors:</strong> ${product.colors.join(', ') || 'N/A'}</p>
|
||||
|
||||
${compatibleAccessories.length > 0 ? `
|
||||
<h3 style="margin-top: 20px;">Compatible Accessories</h3>
|
||||
<div class="accessories-list">
|
||||
${compatibleAccessories.map(acc => `
|
||||
<div class="accessory-item">
|
||||
<strong>${acc.description}</strong><br>
|
||||
<small>Code: ${acc.accessoryCode} | ${acc.materials.join(', ')}</small>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="result-actions">
|
||||
<button class="back-button" onclick="goBack()">← Go Back</button>
|
||||
<button class="back-button" onclick="startOver()">Start Over</button>
|
||||
<button class="back-button" onclick="shareCurrentPage()">🔗 Share Product</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
contentDiv.innerHTML = html;
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Share Functionality
|
||||
```javascript
|
||||
function shareCurrentPage() {
|
||||
const url = window.location.href;
|
||||
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
showNotification('Link copied to clipboard!');
|
||||
}).catch(() => {
|
||||
showUrlPrompt(url);
|
||||
});
|
||||
} else {
|
||||
showUrlPrompt(url);
|
||||
}
|
||||
}
|
||||
|
||||
function showUrlPrompt(url) {
|
||||
const message = prompt('Copy this URL to share:', url);
|
||||
}
|
||||
|
||||
function showNotification(message) {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = 'notification';
|
||||
notification.textContent = message;
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
padding: 15px 20px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||
z-index: 10000;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.style.animation = 'slideOut 0.3s ease-in';
|
||||
setTimeout(() => notification.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
```
|
||||
|
||||
### 8. Update startOver Function
|
||||
```javascript
|
||||
function startOver() {
|
||||
history = ['start'];
|
||||
|
||||
// Clear all stored answers
|
||||
for (const key in userAnswers) {
|
||||
delete userAnswers[key];
|
||||
}
|
||||
|
||||
// Clear bit value
|
||||
accumulatedBitValue = 0;
|
||||
|
||||
// Clear URL (return to root)
|
||||
window.history.replaceState({}, '', window.location.pathname);
|
||||
|
||||
loadContent('start');
|
||||
}
|
||||
```
|
||||
|
||||
### 9. Handle Browser Back Button
|
||||
```javascript
|
||||
// Add this to init() or as separate event listener
|
||||
window.addEventListener('popstate', function(event) {
|
||||
if (event.state) {
|
||||
if (event.state.productCode) {
|
||||
showProductByCode(event.state.productCode);
|
||||
} else if (event.state.questionKey) {
|
||||
accumulatedBitValue = event.state.bitValue || 0;
|
||||
loadContent(event.state.questionKey);
|
||||
} else {
|
||||
startOver();
|
||||
}
|
||||
} else {
|
||||
// No state, check URL
|
||||
initFromURL();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 10. Add CSS for Notification
|
||||
```css
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(400px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideOut {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateX(400px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.accessory-item {
|
||||
padding: 10px;
|
||||
margin: 5px 0;
|
||||
background: #f5f5f5;
|
||||
border-left: 3px solid #2196F3;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.product-card {
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.product-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Scenarios
|
||||
|
||||
### Test 1: Basic Navigation
|
||||
1. Start quiz
|
||||
2. Select Door → Storm Door → Aluminum → White
|
||||
3. Check URL contains `?b=XXXX&q=YYYY`
|
||||
4. Copy URL
|
||||
5. Open in new tab → Should restore to same point
|
||||
|
||||
### Test 2: Product Deep Link
|
||||
1. Navigate to product BGIST
|
||||
2. Check URL contains `?p=BGIST&b=XXXX`
|
||||
3. Copy URL
|
||||
4. Open in new tab → Should show product directly
|
||||
|
||||
### Test 3: Browser Refresh
|
||||
1. Navigate through several questions
|
||||
2. Press F5 to refresh
|
||||
3. Should restore to same question with selections intact
|
||||
|
||||
### Test 4: Browser Back Button
|
||||
1. Navigate forward through questions
|
||||
2. Press browser back button
|
||||
3. Should step backward through questions
|
||||
4. URL should update accordingly
|
||||
|
||||
### Test 5: Share Button
|
||||
1. Complete navigation flow
|
||||
2. Click "Share" button
|
||||
3. Should copy URL to clipboard
|
||||
4. Paste in new browser → Should restore state
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **User Experience**: Users don't lose progress on refresh
|
||||
2. **Shareability**: Users can share specific configurations
|
||||
3. **Bookmarking**: Useful configurations can be saved
|
||||
4. **SEO**: Product pages are directly linkable
|
||||
5. **Analytics**: Track specific navigation paths via URL parameters
|
||||
6. **Support**: Users can share URLs when asking for help
|
||||
|
||||
## URL Encoding Notes
|
||||
|
||||
- Bit values are stored as decimal integers (more compact than hex for small values)
|
||||
- Product codes are URL-safe (no special encoding needed)
|
||||
- Question IDs are alphanumeric (q-dimensions, etc.)
|
||||
- Special characters in product codes should be URL-encoded if present
|
||||
@@ -0,0 +1,343 @@
|
||||
# User Management System
|
||||
|
||||
## Overview
|
||||
A secure user management front-end that allows you to create and manage users. Each user record contains:
|
||||
- **Username**: Unique identifier
|
||||
- **Password**: Securely hashed using PBKDF2-SHA256 (industry-standard)
|
||||
- **Default Location**: Primary location (required) - one of: Lindsborg, Iola, KC, or BMD
|
||||
- **Active Status**: Whether the user account is active or inactive (toggleable in user list)
|
||||
- **Location Settings**: Per-location configuration with:
|
||||
- **Accessible**: Whether the user can access this location
|
||||
|
||||
## User Interface Features
|
||||
|
||||
### Table-Based Location Configuration
|
||||
The form uses an intuitive table layout with:
|
||||
- **Default Location Column**: Radio buttons to select ONE primary location (required)
|
||||
- **Accessible Column**: Checkboxes to mark which locations the user can access
|
||||
|
||||
### User List with Active Toggle
|
||||
Each user in the list shows:
|
||||
- **Username** and **Location Information**
|
||||
- **Active/Inactive Toggle**: Click to enable or disable the user account
|
||||
- Active users have full border color
|
||||
- Inactive users are dimmed with reduced opacity
|
||||
- **Delete Button**: Remove the user permanently
|
||||
|
||||
### Smart Header Checkboxes
|
||||
The accessible column has a header checkbox that:
|
||||
- Shows three states: checked ✓, unchecked ☐, or indeterminate ⊟ (mixed)
|
||||
- **Clicking cycles**: If off or mixed → all on, if on → all off
|
||||
- **Auto-updates**: When you check/uncheck individual rows, the header shows:
|
||||
- ✓ if all are checked
|
||||
- ☐ if none are checked
|
||||
- ⊟ if some are checked (mixed state)
|
||||
|
||||
### Extensible Design
|
||||
The table structure is designed to easily add more columns in the future:
|
||||
- Permission columns (Permission 1, Permission 2, etc.)
|
||||
- Custom attributes
|
||||
- Feature flags
|
||||
- Any other per-location settings
|
||||
|
||||
Each new column can have the same header checkbox behavior.
|
||||
|
||||
## Security Features
|
||||
- **Password Hashing**: Passwords are hashed using `pbkdf2:sha256` algorithm
|
||||
- **Not Plain Text**: Passwords are never stored in plain text
|
||||
- **Cryptographically Secure**: Uses Werkzeug's secure password hashing
|
||||
- **Cannot Be Decoded**: Hashed passwords cannot be reversed back to plain text
|
||||
|
||||
## How to Use
|
||||
|
||||
### 1. Access the User Manager
|
||||
Navigate to: `http://localhost:8080/users`
|
||||
|
||||
### 2. Add New Users
|
||||
- Fill in the form with username, password, and location
|
||||
- Click "Add User" button
|
||||
- User will be added with a securely hashed password
|
||||
|
||||
### 3. View Users
|
||||
- All users are displayed in a list showing username and location
|
||||
- Password hashes are NOT displayed for security
|
||||
|
||||
### 4. Download Users JSON
|
||||
- Click "📥 Download Users JSON" button
|
||||
- Downloads a `users.json` file containing all users
|
||||
- Password field contains the secure hash (not plain text)
|
||||
|
||||
### 5. Delete Users
|
||||
- Click "Delete" next to any user to remove them
|
||||
- Click "🗑️ Clear All Users" to remove all users at once
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### GET /users
|
||||
Displays the user management interface.
|
||||
|
||||
### GET /api/users
|
||||
Returns all users in JSON format.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"users": [
|
||||
{
|
||||
"username": "john_doe",
|
||||
"password": "pbkdf2:sha256:600000$...",
|
||||
"defaultLocation": "LINDS",
|
||||
"active": true,
|
||||
"locationSettings": {
|
||||
"LINDS": { "accessible": true },
|
||||
"IOLA": { "accessible": true },
|
||||
"KC": { "accessible": false },
|
||||
"BMD": { "accessible": false }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/users
|
||||
Adds a new user.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"username": "jane_smith",
|
||||
"password": "mySecurePassword123",
|
||||
"defaultLocation": "KC",
|
||||
"active": true,
|
||||
"locationSettings": {
|
||||
"LINDS": { "accessible": true },
|
||||
"IOLA": { "accessible": true },
|
||||
"KC": { "accessible": true },
|
||||
"BMD": { "accessible": true }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "User added successfully",
|
||||
"users": [...]
|
||||
}
|
||||
```
|
||||
|
||||
### DELETE /api/users/{index}
|
||||
Deletes a user by their index position.
|
||||
|
||||
### PATCH /api/users/{index}/active
|
||||
Toggle user active status.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"active": true
|
||||
}
|
||||
```
|
||||
|
||||
### DELETE /api/users/clear
|
||||
Clears all users from the system.
|
||||
|
||||
### GET /api/users/download
|
||||
Downloads all users as a JSON file.
|
||||
|
||||
## Password Security
|
||||
|
||||
### How Passwords Are Stored
|
||||
Passwords are hashed using PBKDF2-SHA256 with the following properties:
|
||||
- **Algorithm**: PBKDF2 (Password-Based Key Derivation Function 2)
|
||||
- **Hash Function**: SHA-256
|
||||
- **Iterations**: 600,000+ (computationally expensive for attackers)
|
||||
- **Salt**: Automatically generated unique salt per password
|
||||
|
||||
### Example Hash Format
|
||||
```
|
||||
pbkdf2:sha256:600000$AbCdEfGh$1234567890abcdef...
|
||||
```
|
||||
|
||||
Components:
|
||||
- `pbkdf2:sha256` - Algorithm identifier
|
||||
- `600000` - Number of iterations
|
||||
- `$AbCdEfGh` - Random salt
|
||||
- `$1234567890abcdef...` - Actual hash
|
||||
|
||||
### Password Verification
|
||||
To verify a password, use Werkzeug's `check_password_hash()`:
|
||||
|
||||
```python
|
||||
from werkzeug.security import check_password_hash
|
||||
|
||||
# user['password'] contains the hash
|
||||
if check_password_hash(user['password'], provided_password):
|
||||
print("Password is correct!")
|
||||
```
|
||||
|
||||
## Data Storage
|
||||
Users are stored in: `app/data/users.json`
|
||||
|
||||
**Example users.json:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "pbkdf2:sha256:600000$r7K8L9M0$a1b2c3d4e5f6...",
|
||||
"defaultLocation": "LINDS",
|
||||
"active": true,
|
||||
"locationSettings": {
|
||||
"LINDS": { "accessible": true },
|
||||
"IOLA": { "accessible": true },
|
||||
"KC": { "accessible": false },
|
||||
"BMD": { "accessible": false }
|
||||
}
|
||||
},
|
||||
{
|
||||
"username": "user1",
|
||||
"password": "pbkdf2:sha256:600000$n5O6P7Q8$x9y8z7w6v5u4...",
|
||||
"defaultLocation": "KC",
|
||||
"active": false,
|
||||
"locationSettings": {
|
||||
"LINDS": { "accessible": true },
|
||||
"IOLA": { "accessible": true },
|
||||
"KC": { "accessible": true },
|
||||
"BMD": { "accessible": true }
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Location Codes
|
||||
- **LINDS** = Lindsborg
|
||||
- **IOLA** = Iola
|
||||
- **KC** = KC
|
||||
- **BMD** = BMD
|
||||
|
||||
## Integration Example
|
||||
|
||||
### Authenticate and Get User Info
|
||||
|
||||
```python
|
||||
from werkzeug.security import check_password_hash
|
||||
import json
|
||||
|
||||
def authenticate_user(username, password):
|
||||
"""Authenticate a user by username and password"""
|
||||
with open('app/data/users.json', 'r') as f:
|
||||
users = json.load(f)
|
||||
|
||||
# Find user
|
||||
user = next((u for u in users if u['username'] == username), None)
|
||||
|
||||
if user and check_password_hash(user['password'], password):
|
||||
return True, user
|
||||
return False, None
|
||||
|
||||
# Usage
|
||||
success, user_data = authenticate_user('john_doe', 'password123')
|
||||
if success:
|
||||
print(f"Welcome {user_data['username']}!")
|
||||
print(f"Default location: {user_data['defaultLocation']}")
|
||||
print(f"Account active: {user_data.get('active', True)}")
|
||||
|
||||
# Check if user can access a location
|
||||
if user_data['locationSettings']['KC']['accessible']:
|
||||
print("User can access KC")
|
||||
```
|
||||
|
||||
### Check User Permissions for a Location
|
||||
|
||||
```python
|
||||
def can_access_location(user_data, location_code):
|
||||
"""Check if user can access a specific location"""
|
||||
return user_data.get('locationSettings', {}).get(location_code, {}).get('accessible', False)
|
||||
|
||||
def is_user_active(user_data):
|
||||
"""Check if user account is active"""
|
||||
return user_data.get('active', True)
|
||||
|
||||
# Usage
|
||||
if not is_user_active(user_data):
|
||||
print("User account is inactive")
|
||||
return
|
||||
|
||||
if can_access_location(user_data, 'LINDS'):
|
||||
print("User can access Lindsborg")
|
||||
```
|
||||
|
||||
### Get All Accessible Locations for a User
|
||||
|
||||
```python
|
||||
def get_accessible_locations(user_data):
|
||||
"""Get all locations where user has access"""
|
||||
accessible_locations = []
|
||||
for location_code, settings in user_data.get('locationSettings', {}).items():
|
||||
if settings.get('accessible', False):
|
||||
accessible_locations.append(location_code)
|
||||
return accessible_locations
|
||||
|
||||
# Usage
|
||||
accessible = get_accessible_locations(user_data)
|
||||
print(f"User can access: {', '.join(accessible)}")
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Username must be unique
|
||||
- Minimum password length: 6 characters
|
||||
- Default location is required (one of: Lindsborg, Iola, KC, BMD)
|
||||
- **Default location is automatically marked as accessible** when creating a user
|
||||
- Accessible checkboxes are optional for other locations
|
||||
- New users are **active by default**
|
||||
- Toggle active status in the user list section
|
||||
- Users are stored locally in JSON format
|
||||
- This is a separate endpoint from the main Product Finder app
|
||||
|
||||
## Adding New Permissions/Columns
|
||||
|
||||
The system is designed to be easily extensible. To add new permission columns:
|
||||
|
||||
### 1. Update the HTML table
|
||||
Add a new column header and cells in `user_manager.html`:
|
||||
|
||||
```html
|
||||
<!-- In the table header -->
|
||||
<th class="checkbox-header" onclick="toggleHeaderCheckbox('newPermission')">
|
||||
<input type="checkbox" id="headerNewPermission"
|
||||
onclick="event.stopPropagation(); toggleAllCheckboxes('newPermission')">
|
||||
Permission Name
|
||||
</th>
|
||||
|
||||
<!-- In each table row -->
|
||||
<td class="checkbox-cell">
|
||||
<input type="checkbox" class="newPermission-checkbox"
|
||||
data-location="LINDS" onchange="updateHeaderCheckbox('newPermission')">
|
||||
</td>
|
||||
```
|
||||
|
||||
### 2. Update the JavaScript form submission
|
||||
Modify the form submission handler to collect the new permission:
|
||||
|
||||
```javascript
|
||||
locationSettings[loc.code] = {
|
||||
active: activeCheckbox ? activeCheckbox.checked : false,
|
||||
accessible: accessibleCheckbox ? accessibleCheckbox.checked : false,
|
||||
newPermission: newPermCheckbox ? newPermCheckbox.checked : false // Add this
|
||||
};
|
||||
```
|
||||
|
||||
### 3. Update the backend (optional)
|
||||
The backend already handles any properties in `locationSettings`, so no changes are required unless you want validation.
|
||||
|
||||
### 4. Reset header checkbox on form submit
|
||||
Add to the form reset section:
|
||||
|
||||
```javascript
|
||||
document.getElementById('headerNewPermission').checked = false;
|
||||
document.getElementById('headerNewPermission').indeterminate = false;
|
||||
```
|
||||
|
||||
That's it! The system will automatically save and load the new permission data.
|
||||
@@ -0,0 +1 @@
|
||||
web: gunicorn wsgi:app
|
||||
@@ -0,0 +1,262 @@
|
||||
PROD_CODE CATEGORY DESCRIPTION
|
||||
--------------------------------------------------------------------------------
|
||||
1400 SHPW SERIES 1400 VINYL SLIDING PRIMARY
|
||||
1400SCR SHPW 1400 SCREEN
|
||||
1500 SHPT SERIES 1500 S.H. TILT VINYL PRIMARY
|
||||
1510 FPPW 1510 VINYL INSULATED FIXED LITE
|
||||
1650 SHPW #1650 INSULATED SINGLE HUNG
|
||||
1650-10 FPPW #1650-10 INSULATED FIXED LITE
|
||||
1650SCR SCREENS SCREENS FOR #1650
|
||||
1650VS SHPW 1650 BOTTOM SASH
|
||||
1700 SHPW #1700 INSULATED SLIDER
|
||||
1700CSC SCREENS SCREEN FOR #1700 CENTER VENT SLIDER
|
||||
1700CV SHPW 1700 CENTER VENT
|
||||
1700ESC SCREENS SCREEN FOR #1700 ENDS VENT SLIDER
|
||||
1700EV SHPW 1700 END VENT
|
||||
1700SCR SCREENS SCREEN FOR #1700 SLIDER
|
||||
1700VS SHPW #1700 VENT SASH
|
||||
1710 FPPW C-1710 FIXED LITE
|
||||
2000 SHPT SERIES 2000 S.H. T.B. TILT PRIMARY
|
||||
2000SCR SHPT C-2000 SCREEN
|
||||
2100 SHPT SERIES 2100 THERMAL BREAK SLIDER
|
||||
2100EV 2000SLI SERIES 2100 3-PANEL SLIDER
|
||||
2200 FPPW SERIES 2200 T.B. FIXED LITE
|
||||
2200PDG FPPW SERIES 2200 TB FIXED LITE W/TEMP GLASS
|
||||
2400 PD 2400 ROYAL CROWN PATIO DOOR
|
||||
2650 SHPW #2650 SINGLE HUNG SINGLE GLAZED PRIME
|
||||
265010 FPPW #2650-10 SINGLE GLAZED FIXED LITE
|
||||
2650SCR SCREENS SCREENS FOR 2650
|
||||
2650VP SHPW #2650 VENT PANEL - SINGLE HUNGE
|
||||
2700 SHPW #2700 SINGLE GLAZED SLIDER
|
||||
2700CV SHPW C-2700 - CENTER VENT
|
||||
2700EV SHPW C-2700 - END VENT
|
||||
2700SCR SCREENS SCREENS FOR #2700
|
||||
2700VS SHPW #2700 VENT SASH
|
||||
2710 FPPW C-2710 FIXED LITE - SINGLE GLAZED
|
||||
3000 3000DHP SERIES 3000 D.H. T.B REPLACEMENT WD
|
||||
3000SCR SCREENS SCREENS FOR #3000
|
||||
303 STORMS #303 DART STORM WINDOWS
|
||||
305INS INSERTS #305 SASH WINDCHECK INSERTS
|
||||
306INS INSERTS #306 SCHLEGEL GLASS INSERTS
|
||||
3100 3000DHP SERIES 3100 T.B. REPLACEMENT SLIDER
|
||||
3100EV 3000DHP SERIES 3100 3-PANEL SLIDER
|
||||
3100SCR SCREENS SCREENS FOR #3100
|
||||
310PSCR SCRINS #310 PLAIN SCREENS
|
||||
3200 3000FPP SERIES 3200 T.B. PICTURE WINDOW
|
||||
3300 CASEMNT 3300 1-PANEL T.B. CASEMENT
|
||||
3302 CASEMNT 3300 2-PANEL T.B. CASEMENT
|
||||
3303 CASEMNT 3300 3-PANEL T.B. CASEMENT
|
||||
3310 CASEMNT 3310 FIXED CASEMENT
|
||||
3400 CASEMNT 3400 1 PANEL VINYL CLAD CASEMENT
|
||||
3402 CASEMNT 3400 2 PANEL VINYL CLAD CASEMENT
|
||||
3403 CASEMNT 3400 3 PANEL VINYL CLAD CASEMENT
|
||||
350 STORMS #350 ARROW STORM WINDOWS
|
||||
3500 CASEMNT 3500 1-PANEL WOOD INTERIOR CASEMENT
|
||||
3502 CASEMNT 3500 2-PANEL WOOD INTERIOR CASEMENT
|
||||
3503 CASEMNT 3503 3-PANEL WOOD INTERIOR CASEMENT
|
||||
3700 SHPW #3700 INSULATED SLIDER
|
||||
3700SCR SCREENS SCREEN FOR #3700 POLE BARN WD
|
||||
3700VP SHPW #3700 VENT PANEL
|
||||
3710 FPPW C-3710 INSULATED FIXED LITE
|
||||
404 STORMS #404 FALCON STORM WINDOWS
|
||||
404ONE STPW #404 ONE-LITE ST WD
|
||||
4100 REPLACE SERIES 4100 VINYL SLIDER
|
||||
450 STORMS #450 RAVEN STORM WINDOWS
|
||||
450ONE STPW #450 ONE-LITE ST WD
|
||||
4700 SHPW #4700 SINGLE GLAZED SLIDER
|
||||
4700SCR SCREENS SCREENS FOR #4700
|
||||
4710 FPPW #4710 SINGLE GLAZED FIXED LITE
|
||||
5000SCR SCREENS SCREEN FOR R5000 SLIDER
|
||||
5200 PD 5200 VINYL PATIO DOORS
|
||||
5700 SHPW #5700 INSULATED SLIDER
|
||||
606 STORMS #606 LION STORM WINDOWS
|
||||
606ONE STPW #606 ONE-LITE ST WD
|
||||
6100I STD STAR 6100 FULL VIEW STORM DOOR
|
||||
6100L STD STAR 6100 FULL VIEW STORM DOOR
|
||||
6300 SHPT SERIES 6300 S.H. TILT VINYL PRIMARY
|
||||
6301 SHPW SERIES 6301 VINYL SLIDING PRIMARY
|
||||
650 STORMS #650 LYNX STORM WINDOWS
|
||||
650ONE STPW #650 ONE-LITE ST WD
|
||||
6700 SHPW #6700 SINGLE GLAZED SLIDER
|
||||
7100I STD STAR 7100 FULL VIEW SELF STORING DOOR
|
||||
7100L STD STAR 7100 FULL VIEW SELF STORING DOOR
|
||||
808 STORMS 808 HAWK STORM WINDOW
|
||||
808ONE STPW #808 ONE-LITE ST WD
|
||||
8100I STD STAR 8100 FULL VIEW STORM DOOR
|
||||
8100L STD STAR 8100 FULL VIEW STORM DOOR
|
||||
850 STORMS #850 KENT STORM WINDOWS
|
||||
850ONE STPW #850 ONE-LITE ST WD
|
||||
BELMONT 3000FPP BELMONT ALLIANCE DOUBLE HUNG WINDOW
|
||||
BGI INSERTS BOTTOM GL INSERTS FOR ECONOMY ST WDS
|
||||
BGI404 INSERTS BOTTOM GL INSERTS FOR #404/450 ST WDS
|
||||
BGI606 INSERTS BOTTOM GL INSERTS FOR #606/650 ST WDS
|
||||
BGIST INSERTS INSERTS FOR STORM DOORS
|
||||
C-500 SHPW C-500 SH THERMAL BREAK INS.
|
||||
C1150 CASEMNT C1150-VINYL AWNING WINDOW
|
||||
C1500AR CIR TOP C-1510 VINYL ARCH TOP-OPERATING WINDOW
|
||||
C1510AR CIR TOP C-1510 VINYL ARCH TOP WINDOW
|
||||
C1521 CIR TOP C-1521 VINYL CIRCLE TOP
|
||||
C1526 CIR TOP C-1526 VINYL CIRCLE TOP
|
||||
C1621 CIR TOP C-1621 INSULATED CIRCLE TOP
|
||||
C1622 CIR TOP C-1622 INSULATED CIRCLE TOP
|
||||
C1626VA CIR TOP C-1626VA INSULATED CIRCLE TOPS
|
||||
C1710 SHPW #1710 PICTURE OVER SLIDER
|
||||
C1721 CIR TOP C-1721 INSULATED CIRCLE TOPS
|
||||
C1722 CIR TOP C-1722 INSULATED CIRCLE TOPS
|
||||
C1724 CIR TOP C-1724 INSULATED CIRCLE TOPS
|
||||
C1800 C1800 C-1800 INSIDE SLIDING STORM WINDOW
|
||||
C2021 CIR TOP C-2021 T.B CIRCLE TOPS
|
||||
C2022 CIR TOP C-2022 T.B. CIRCLE TOPS
|
||||
C2026 CIR TOP C-2026 T.B. CIRCLE TOPS
|
||||
C2710 SHPW #2710 PICTURE OVER SLIDER
|
||||
C300 BSMT C-300 ALUM INSERTS FOR BASEMENT BUCKS
|
||||
C3221 CIR TOP C-3221 T.B CIRCLE TOPS
|
||||
C3222 CIR TOP C-3222 T.B. CIRCLE TOPS
|
||||
C3223 CIR TOP C-3223 T.B. CIRCLE TOPS
|
||||
C3224 CIR TOP C-3224 T.B. CIRCLE TOPS
|
||||
C3300 CASEMNT 3300 1-PANEL T.B. CASEMENT
|
||||
C3700 SHPW C-3700 INSULATED SLIDING POLE BARN WND
|
||||
C400 BSMT C-400 VINYL INSERT FOR BASEMENT BUCKS
|
||||
C4000 REPLACE C-4000 SINGLE HUNGE PRIMARY WINDOW
|
||||
C4260 PD C-4260 STEEL MIRROR DOOR
|
||||
C500 SHPW C-500 INS THERMAL BREAK SINGLE HUNG
|
||||
C500GL VENTS LITES OF INSULATED GLASS FOR C-500'S
|
||||
C500GLP VENTS LITES OF INSULATED GLASS FOR C-500'S
|
||||
C500PP SHPW C-500 INS THERMAL BREAK SINGLE HUNG
|
||||
C500SCR SCREENS C-500 SCREEN
|
||||
C500VP VENTS VENT PANELS FOR C-500
|
||||
C521 CIR TOP C-521 T.B. CIRCLE TOPS
|
||||
C522 CIR TOP C-522 T.B. CIRCLE TOPS
|
||||
C526 CIR TOP C-526 T.B. CIRCLE TOPS
|
||||
C610 CASEMNT C610 VINYL CASEMENT WINDOW
|
||||
C620 FPPW C620 VINYL AWNING WINDOWS
|
||||
C621 CIR TOP C-621 VINYL CIRCLE TOP
|
||||
C626 CIR TOP C-626 VINYL CIRCLE TOP
|
||||
C640 FPPW C-640 VINYL FIXED CASEMENT WINDOW
|
||||
C826 CIR TOP C-826 VINYL CIRCLE TOP
|
||||
C828 CIR TOP C-828 VINYL CIRCLE TOP
|
||||
C8321 CIR TOP C-8321 VINYL CIRCLE TOP
|
||||
C8326 CIR TOP C-8326 VINYL CIRCLE TOP
|
||||
C900 SHPW C-900 INS THERMAL BREAK SLIDER
|
||||
C900EV SHPW C-900 ENDS VENT SLIDER
|
||||
C900SCR SCREENS SCREEN FOR C-900
|
||||
C910 FPPW C-910 INSULATED T.B. FIXED LITE
|
||||
C910PP FPPW C-910 ARCH TOP
|
||||
C921 CIR TOP C-921 T.B. CIRCLE TOPS
|
||||
C922 CIR TOP C-922 T.B. CIRCLE TOPS
|
||||
C924 CIR TOP C-924 T.B. CIRCLE TOPS
|
||||
C931 CIR TOP C-931 T.B. ROUND PRIMARY WINDOW
|
||||
C939 CIR TOP C-939 T.B. ROUND PRIMARY WINDOW
|
||||
C940 CIR TOP C-940 T.B. OCTAGON PRIMARY
|
||||
C949 CIR TOP C949 OCTAGON THERMAL BREAK WINDOW
|
||||
C960 FPPW C-960 INSULATED T.B. CIRCLE TOP
|
||||
COBRAI STD COLUMBIA COBRA STORM DOOR
|
||||
COBRAL STD COLUMBIA COBRA STORM DOOR
|
||||
COBRATI STD COLUMBIA COBRA STORM DOOR
|
||||
COBRATL STD COLUMBIA COBRA STORM DOOR
|
||||
CRWNFVI STD CROWN FULL VIEW STORM DOOR
|
||||
CRWNFVL STD CROWN FULL VIEW STORM DOOR
|
||||
CRWNSDI STD COLUMBIA CROWN SCREEN DOOR
|
||||
CRWNSDL STD COLUMBIA CROWN SCREEN DOOR
|
||||
D770 SHPT D770 D.H. TILT VINYL PRIMARY WINDOWS
|
||||
D780 SHPW D780 DOUBLE SLIDE VINYL PRIMARY
|
||||
D830 SHPT D830 D.H.TILT VINYL PRIMARY WINDOWS
|
||||
D830SCR SHPT D830 SCREEN
|
||||
D832 FPPW D832 FIXED VINYL PRIMARY WINDOWS
|
||||
DSGLASS GLASS DOUBLE STRENGTH GLASS
|
||||
DURASEA FPPW DURASEAL 5/8" (GRAY) PER REEL
|
||||
EXPAND EXPANDR SILL EXPANDERS
|
||||
FULLSCR SCREENS FULL SCREEN
|
||||
FV10I STD KING FV-10 DECORATOR STORM DOOR
|
||||
FV10L STD KING FV-10 DECORATOR STORM DOOR
|
||||
FV3I STD KING FV-3 DECORATOR STORM DOOR
|
||||
FV3L STD KING FV-3 DECORATOR STORM DOOR
|
||||
FVGI INSERTS GLASS INSERTS FOR KING ONE LITE
|
||||
FVSI INSERTS FULL SCREENS ONLY FOR KING ONE-LITES
|
||||
G3000 GARDEN COLUMBIA COMFORT 3000 VINYL GARDEN WD
|
||||
GOLIATH STD GOLIATH STORM DOOR
|
||||
HERCULE STD HERCULES STORM DOOR
|
||||
IMPERIL PD IMPERIAL PATIO DOORS
|
||||
INSGLAS GLASS INSULATED GLASS
|
||||
ISP PD STATIONARY PANELS FOR IMPERIAL DOORS
|
||||
IVP PD VENT PANELS FOR IMPERIAL PATIO DOORS
|
||||
JET PD COLUMBIA JET PATIO DOORS
|
||||
JSP PD STATIONARY PANELS FOR JET DOORS
|
||||
JVP PD VENT PANELS FOR JET PATIO DOORS
|
||||
KINGDVI STD KING DUAL VENT STORM DOORS
|
||||
KINGDVL STD KING DUAL VENT STORM DOORS
|
||||
KINGFSC INSERTS FULL SCREEN FOR KING ONE-LITE
|
||||
KINGI STD KING ONE-LITE STORM DOORS
|
||||
KINGL STD KING ONE-LITE STORM DOORS
|
||||
KINGSDI STD KING ONE-LITE SCREEN DOOR ONLY
|
||||
KINGSDL STD KING ONE-LITE SCREEN DOOR ONLY
|
||||
LINCOLN 3000DHP LINCOLN PRIMARY WOOD WINDOWS
|
||||
LINCPDR PD LINCOLN FRENCH PATIO DOOR
|
||||
M1200 PD M1200 PATIO STORM DOOR
|
||||
M306 INSERTS M-306 SCHLEGEL GLASS INSERTS
|
||||
OUTSIDE STDMISC OUTSIDE DOOR SWEEPS - ALUM + VINYL
|
||||
PATIOSC SCREENS PATIO DOOR SCREENS
|
||||
PDSCRTT SCREENS SPECIAL SIZE SCREENS FOR PATIO DOORS
|
||||
PRPDS SCREENS SCREENS MADE FROM PLAIN PATIO SCR RAIL
|
||||
PRSCR PSCREEN SCREENS FOR PRIME MADE FROM #19-88
|
||||
PRSCR11 PSCREEN SCREEN FOR PRIME MADE FROM #19-11
|
||||
PSINS INSERTS PLAIN SASH INSERTS
|
||||
PWS STPW PIN-ON PICTURE WINDOWS
|
||||
PWSINS INSERTS INSERTS ONLY FOR PIN-ON PICTURE WINDOW
|
||||
R1150 REPLACE R-1150 VINYL AWNING WINDOWS
|
||||
R1400 REPLACE SERIES 1400 VINYL SLIDING PRIMARY
|
||||
R1500 REPLACE SERIES 1500 S.H. TILT VINYL PRIMARY
|
||||
R1510 FPPW SERIES 1510 FIXED LITE VINYL PRIMARY
|
||||
R2000 REPLACE SERIES 2000 S.H. T.B. TILT PRIMARY
|
||||
R2100 REPLACE SERIES 2100 THERMAL BREAK SLIDER
|
||||
R2100EV R2000SL SERIES 2100 3-PANEL T.B. SLIDER
|
||||
R2200 RFPPW SERIES 2200 T.B. FIXED LITE
|
||||
R300 RBSMT C-300 ALUM INSERTS FOR BASEMENT BUCKS
|
||||
R3302 CASEMNT 3302 2-PANEL T.B. CASEMENT
|
||||
R400 RBSMT C-400 VINYL INSERT FOR BASEMENT BUCKS
|
||||
R5000 REPLACE R-5000 INSULATED ALUMINUM SLIDER
|
||||
R770 REPLACE R770 D.H. TILT VINYL PRIMARY WINDOWS
|
||||
R770SCR SCREENS FULL SCREEN
|
||||
R780 REPLACE R780 VINYL SLIDING PRIMARY
|
||||
R820 REPLACE R820 S.H. TILT VINYL PRIMARY WINDOWS
|
||||
R821 REPLACE S821 SINGLE SLIDE VINYL PRIMARY
|
||||
R822 FPPW S822 FIXED VINYL PRIMARY WINDOWS
|
||||
R830 REPLACE R830 D.H.TILT VINYL PRIMARY WINDOWS
|
||||
R832 FPPW R832 FIXED VINYL PRIMARY WINDOWS
|
||||
RCKTRAP INSERTS ROCKET TRAPEZOIDS
|
||||
REWIRE SCREENS REWIRED SCREENS
|
||||
RNDROCK INSERTS ROUND ROCKET INSERT
|
||||
ROCKET INSERTS ROCKET INSERTS
|
||||
ROYAL STD COLUMBIA ROYAL STORM DOOR
|
||||
RROCKET INSERTS RADIUS ROCKETS
|
||||
S820 SHPT S820 S.H. TILT VINYL PRIMARY WINDOWS
|
||||
S821 SHPW S821 SINGLE SLIDE VINYL PRIMARY
|
||||
S822 FPPW S822 FIXED VINYL PRIMARY WINDOWS
|
||||
SCRI INSERTS SCREEN INSERTS FOR ECONOMY STORM WDS
|
||||
SCRI404 INSERTS SCREEN INSERTS FOR #404/450 STORM WDS
|
||||
SCRI606 INSERTS SCREEN INSERTS FOR #606/650 STORM WDS
|
||||
SCRI808 INSERTS SCREEN INSERTS FOR #808/850 STORM WIND
|
||||
SS10I STD COBRA SS-10 DECORATOR STORM DOOR
|
||||
SS10L STD COBRA SS-10 DECORATOR STORM DOOR
|
||||
SS3I STD COBRA SS-3 DECORATOR STORM DOOR
|
||||
SS3L STD COBRA SS-3 DECORATOR STORM DOOR
|
||||
SSGLASS GLASS SINGLE STRENGTH GLASS
|
||||
SSSCR INSERTS SCREEN INSERT FOR SELF STORING DOORS
|
||||
TBGI INSERTS TEMPERED GLASS INSERTS FOR SS DOORS
|
||||
TBR PD IMPERIAL T.B.R. REPLACEMENT PATIO DOOR
|
||||
TGI INSERTS TOP GL INSERTS FOR ECONOMY ST WDS
|
||||
TGI404 INSERTS TOP GLASS INSERTS FOR #404/450 ST WDS
|
||||
TGI606 INSERTS TOP GLASS INSERTS FOR #606/650 ST WDS
|
||||
TGIODD INSERTS TOP GLASS INSERTS FOR STORM DOORS
|
||||
THOR STD THOR - STORM DOOR
|
||||
TIARAI STD COLUMBIA TIARA SELF STORING STORM DOOR
|
||||
TIARAL STD COLUMBIA TIARA SELF STORING STORM DOOR
|
||||
TVGROOV INSERTS TEMPERED V-GROOVE ONE-LITE INSERTS
|
||||
TVI STD COLUMBIA COBRA TWIN VENT STORM DOOR
|
||||
TVL STD COLUMBIA COBRA TWIN VENT STORM DOOR
|
||||
VKI STOCK VENTILATOR KICKPANEL FOR KING ONELITE
|
||||
VKS STOCK VENTILATOR SCREEN FOR KING ONE LITE
|
||||
VP3700 VENTS VENT PANELS FOR #3700
|
||||
WINDGAT VACW ALLIANCE WINDGATE CASEMENT WINDOW
|
||||
XBUCKIN INSERTS TEMPERED GLASS INSERTS FOR CROSSBUCKS
|
||||
ZBARS STDMISC Z-BARS FOR STORM DOORS
|
||||
@@ -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,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Product Finder Quiz</title>
|
||||
<link rel="stylesheet" href="css/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>Product Finder</h1>
|
||||
</div>
|
||||
|
||||
<div class="breadcrumb" id="breadcrumb">
|
||||
Start
|
||||
</div>
|
||||
|
||||
<div id="content">
|
||||
<!-- Content will be dynamically loaded here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="js/script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,31 @@
|
||||
@echo off
|
||||
echo ====================================
|
||||
echo Product Finder Quiz - Flask App
|
||||
echo ====================================
|
||||
echo.
|
||||
echo Checking Python installation...
|
||||
python --version
|
||||
if errorlevel 1 (
|
||||
echo ERROR: Python is not installed or not in PATH
|
||||
echo Please install Python 3.8 or higher
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo.
|
||||
echo Installing dependencies...
|
||||
pip install -r requirements.txt
|
||||
if errorlevel 1 (
|
||||
echo ERROR: Failed to install dependencies
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo.
|
||||
echo Starting Flask application...
|
||||
echo.
|
||||
echo Application will be available at:
|
||||
echo http://localhost:8080/quiz
|
||||
echo.
|
||||
echo Press Ctrl+C to stop the server
|
||||
echo.
|
||||
python app.py
|
||||
pause
|
||||
@@ -0,0 +1,13 @@
|
||||
https://columbiawindows.com/product-detail/cobra-aluminum-storm-doors-1100/
|
||||
|
||||
https://columbiawindows.com./wp-content/uploads/2014/10/How-to-Measure2.pdf
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
HINGE LOCATION:
|
||||
Face your door opening from outside the house. If hinges are needed on the left
|
||||
side, specify a left hinge door. If hinges are needed on the right, specify a right
|
||||
hinge door
|
||||
@@ -0,0 +1,106 @@
|
||||
[
|
||||
{
|
||||
"id": "404",
|
||||
"productCode": "404",
|
||||
"category": "STORMS",
|
||||
"description": "#404 FALCON STORM WINDOWS",
|
||||
"discontinued": false,
|
||||
"location": "Iola",
|
||||
"baseType": "Window",
|
||||
"subType": {
|
||||
"door": null,
|
||||
"window": "Storm Window"
|
||||
},
|
||||
"materials": [
|
||||
"Aluminum"
|
||||
],
|
||||
"colors": [
|
||||
"Black",
|
||||
"White",
|
||||
"Bronze",
|
||||
"Sandstone"
|
||||
],
|
||||
"isAccessory": false,
|
||||
"compatibleAccessories": [],
|
||||
"image": "images/404.jpg",
|
||||
"imageConfig": {
|
||||
"layered": true,
|
||||
"basePath": "images/products/404/",
|
||||
"layers": {
|
||||
"base": "base.jpg",
|
||||
"door": {
|
||||
"white": "door-white.png",
|
||||
"black": "door-black.png",
|
||||
"bronze": "door-bronze.png",
|
||||
"sandstone": "door-sandstone.png"
|
||||
},
|
||||
"hardware": "handle.png"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "450",
|
||||
"productCode": "450",
|
||||
"category": "STORMS",
|
||||
"description": "#450 RAVEN STORM WINDOWS",
|
||||
"discontinued": false,
|
||||
"location": "Iola",
|
||||
"baseType": "Window",
|
||||
"subType": {
|
||||
"door": null,
|
||||
"window": "Storm Window"
|
||||
},
|
||||
"materials": [
|
||||
"Aluminum"
|
||||
],
|
||||
"colors": [
|
||||
"Black",
|
||||
"Bronze"
|
||||
],
|
||||
"isAccessory": false,
|
||||
"compatibleAccessories": [],
|
||||
"image": "images/450.jpg"
|
||||
},
|
||||
{
|
||||
"id": "505",
|
||||
"productCode": "505",
|
||||
"category": "DOORS",
|
||||
"description": "#505 STORM DOOR",
|
||||
"discontinued": false,
|
||||
"location": "Iola",
|
||||
"baseType": "Door",
|
||||
"subType": {
|
||||
"door": "Storm Door",
|
||||
"window": null
|
||||
},
|
||||
"materials": [
|
||||
"Aluminum",
|
||||
"Vinyl"
|
||||
],
|
||||
"colors": [
|
||||
"White",
|
||||
"Black",
|
||||
"Bronze",
|
||||
"Tan"
|
||||
],
|
||||
"isAccessory": false,
|
||||
"compatibleAccessories": ["ACC-001", "ACC-002"],
|
||||
"image": "images/505.jpg",
|
||||
"imageConfig": {
|
||||
"layered": true,
|
||||
"basePath": "images/products/505/",
|
||||
"layers": {
|
||||
"base": "base.jpg",
|
||||
"door": {
|
||||
"white": "door-white.png",
|
||||
"black": "door-black.png"
|
||||
},
|
||||
"hardware": "handle-lever.png",
|
||||
"overlay": {
|
||||
"inside": "view-inside.png",
|
||||
"outside": "view-outside.png"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,239 @@
|
||||
# Product Encoding System
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the hex-based encoding system for creating stable, bookmarkable URLs for product configurations. The system uses bit-packing to create compact strings that encode product type, color, material, and hardware options.
|
||||
|
||||
## Design Goals
|
||||
|
||||
1. **Stable URLs**: Old bookmarks continue to work even as new options are added
|
||||
2. **Compact**: Keep URLs under 32-64 characters
|
||||
3. **Extensible**: Reserve space for future expansion
|
||||
4. **Reversible**: Each encoded string can be uniquely decoded back to its components
|
||||
5. **Version-aware**: Support future format changes without breaking old URLs
|
||||
|
||||
## Encoding Format
|
||||
|
||||
```
|
||||
Format: v-T-C-M-HHHH-HHHH-HHHH
|
||||
│ │ │ │ │ │ └── Hardware 3 (optional)
|
||||
│ │ │ │ │ └─────── Hardware 2 (optional)
|
||||
│ │ │ │ └──────────── Hardware 1
|
||||
│ │ │ └────────────── Material (1 hex char, 0-F)
|
||||
│ │ └──────────────── Color (1 hex char, 0-F)
|
||||
│ └────────────────── Type (1 hex char, 0-F)
|
||||
└──────────────────── Version (1 hex char, 0-F)
|
||||
|
||||
Example: "0-1-4-1-0D94"
|
||||
- Version: 0 (using 5-bit encoding per field)
|
||||
- Type: 1 (Patio Door)
|
||||
- Color: 4 (White)
|
||||
- Material: 1 (Aluminum)
|
||||
- Hardware 1: 0D94 (Lever, Standard, Brass)
|
||||
|
||||
Max Length: ~24 characters (with 3 hardware items)
|
||||
```
|
||||
|
||||
## Version 0 Encoding (Current)
|
||||
|
||||
### Main Product Attributes (Single Hex Character Each)
|
||||
|
||||
Each main attribute uses a single hex character (0-F = 0-15):
|
||||
|
||||
#### Type Values (1 hex char)
|
||||
```
|
||||
1 = Patio Door
|
||||
2 = Reserved
|
||||
3 = Storm Door
|
||||
4 = Reserved
|
||||
5 = Storm Window
|
||||
6 = Reserved
|
||||
7 = Primary Window
|
||||
8-F = Reserved (8 slots for future door/window types)
|
||||
```
|
||||
|
||||
#### Color Values (1 hex char)
|
||||
```
|
||||
1 = Black
|
||||
2 = Bronze
|
||||
3 = Sandstone
|
||||
4 = White
|
||||
5 = Tan
|
||||
6 = Mill
|
||||
7-F = Reserved (9 slots for future colors)
|
||||
```
|
||||
|
||||
#### Material Values (1 hex char)
|
||||
```
|
||||
1 = Aluminum
|
||||
2 = Vinyl
|
||||
3-F = Reserved (13 slots for future materials)
|
||||
```
|
||||
|
||||
### Hardware Encoding (4 Hex Characters = FFFF)
|
||||
|
||||
Each hardware item uses **4 hex characters** (16 bits) split into fields using **5 bits per field**:
|
||||
|
||||
```
|
||||
16 bits total:
|
||||
- Bits 10-14: Type (5 bits, 0-31 values)
|
||||
- Bits 5-9: Style (5 bits, 0-31 values)
|
||||
- Bits 0-4: Color (5 bits, 0-31 values)
|
||||
- Bit 15: Reserved (1 bit)
|
||||
```
|
||||
|
||||
#### Hardware Type Values (5 bits = 0-31)
|
||||
```
|
||||
0 = None/Not Set
|
||||
1 = Lever
|
||||
2 = Pull
|
||||
3 = Pull Handle
|
||||
4 = Deadbolt
|
||||
5 = Hinge
|
||||
6-31 = Reserved (26 slots)
|
||||
```
|
||||
|
||||
#### Hardware Style Values (5 bits = 0-31)
|
||||
```
|
||||
0 = None/Not Set
|
||||
1 = Standard
|
||||
2 = Push
|
||||
3 = Alternative
|
||||
4 = Contemporary
|
||||
5 = Traditional
|
||||
6-31 = Reserved (26 slots)
|
||||
```
|
||||
|
||||
#### Hardware Color Values (5 bits = 0-31)
|
||||
```
|
||||
0 = None/Not Set
|
||||
1 = Brass
|
||||
2 = White
|
||||
3 = Black
|
||||
4 = Satin
|
||||
5 = Nickel
|
||||
6 = Bronze
|
||||
7-31 = Reserved (25 slots)
|
||||
```
|
||||
|
||||
## Bit Math Examples
|
||||
|
||||
### Encoding Hardware
|
||||
|
||||
```
|
||||
Example: Lever, Standard, Brass
|
||||
- Type: 1 (Lever)
|
||||
- Style: 1 (Standard)
|
||||
- Color: 1 (Brass)
|
||||
|
||||
Binary calculation:
|
||||
Type (1) = 00001 (bits 10-14)
|
||||
Style (1) = 00001 (bits 5-9)
|
||||
Color (1) = 00001 (bits 0-4)
|
||||
|
||||
Combined: 0000010000100001 = 0x0421
|
||||
Hex: "0421"
|
||||
```
|
||||
|
||||
```
|
||||
Example: Pull Handle, Alternative, Satin
|
||||
- Type: 3 (Pull Handle)
|
||||
- Style: 3 (Alternative)
|
||||
- Color: 4 (Satin)
|
||||
|
||||
Binary calculation:
|
||||
Type (3) = 00011 (bits 10-14)
|
||||
Style (3) = 00011 (bits 5-9)
|
||||
Color (4) = 00100 (bits 0-4)
|
||||
|
||||
Combined: 0000110001100100 = 0x0C64
|
||||
Hex: "0C64"
|
||||
```
|
||||
|
||||
### Decoding Hardware
|
||||
|
||||
```
|
||||
Given hex: "0D94"
|
||||
Binary: 0000110110010100
|
||||
|
||||
Extract fields:
|
||||
Type = bits 10-14 = 00011 = 3 (Pull Handle)
|
||||
Style = bits 5-9 = 01100 = 12
|
||||
Color = bits 0-4 = 10100 = 20
|
||||
```
|
||||
|
||||
## URL Examples
|
||||
|
||||
### Simple Products (No Hardware)
|
||||
|
||||
```
|
||||
"0-1-4-1" = Patio Door, White, Aluminum
|
||||
"0-3-4-1" = Storm Door, White, Aluminum
|
||||
"0-7-1-1" = Primary Window, Black, Aluminum
|
||||
"0-5-2-2" = Storm Window, Bronze, Vinyl
|
||||
```
|
||||
|
||||
### Products with Hardware
|
||||
|
||||
```
|
||||
"0-3-4-1-0421" = Storm Door, White, Aluminum, Lever-Standard-Brass
|
||||
"0-3-1-1-0421-0C64" = Storm Door, Black, Aluminum,
|
||||
Hardware1: Lever-Standard-Brass,
|
||||
Hardware2: Pull Handle-Alternative-Satin
|
||||
```
|
||||
|
||||
## Expansion Strategy
|
||||
|
||||
### Adding New Options (Compatible)
|
||||
|
||||
When adding new options within existing capacity (0-15 for main attributes, 0-31 for hardware fields):
|
||||
|
||||
```
|
||||
Old system: Color 1-6 defined, 7-15 reserved
|
||||
New system: Add Color 7 = Charcoal
|
||||
|
||||
Old URLs still work: "0-1-4-1" still means Patio Door, White, Aluminum
|
||||
New URLs use new values: "0-1-7-1" means Patio Door, Charcoal, Aluminum
|
||||
```
|
||||
|
||||
### Version 1 (Future Expansion)
|
||||
|
||||
If we exceed 16 main attribute values or 32 hardware field values:
|
||||
|
||||
```
|
||||
Version 1: Use 6 bits per hardware field (64 values each)
|
||||
Format: "1-TT-CC-MM-HHHHH-HHHHH"
|
||||
|
||||
Changes:
|
||||
- Main attributes expand to 2 hex chars each (256 values)
|
||||
- Hardware expands to 5 hex chars each (18 bits = 6 bits per field)
|
||||
```
|
||||
|
||||
## Compatibility Rules
|
||||
|
||||
1. **Never reuse reserved values** until a new version is released
|
||||
2. **Never change existing value meanings** (e.g., don't make "1" mean something else)
|
||||
3. **Always decode based on version character**
|
||||
4. **Maintain lookup tables** for each version
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Use bit-shifting operators (`<<`, `>>`) for performance
|
||||
- Use bitwise AND (`&`) with masks to extract fields
|
||||
- Pad hex strings with leading zeros to fixed width
|
||||
- Validate decoded values against valid ranges
|
||||
- Return error for unknown version codes
|
||||
|
||||
## Benefits
|
||||
|
||||
✅ **Stable**: Values 0-15 remain the same even if we expand to 0-31
|
||||
✅ **Compact**: 13-24 characters for most products
|
||||
✅ **Readable**: Hex is human-debuggable
|
||||
✅ **Efficient**: Single parse operation, no string splitting
|
||||
✅ **Extensible**: Version prefix allows future format changes
|
||||
✅ **Reliable**: Bit operations are deterministic and reversible
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: March 26, 2026
|
||||
**Current Version**: 0 (5-bit hardware encoding)
|
||||
@@ -0,0 +1,505 @@
|
||||
# Encoding Table Generation System
|
||||
|
||||
## Problem Statement
|
||||
|
||||
Currently, encoding lookup tables require manual maintenance of both forward and reverse mappings:
|
||||
|
||||
```javascript
|
||||
// Must maintain both of these manually
|
||||
const PRODUCT_COLOR = {
|
||||
'BLACK': 1,
|
||||
'BRONZE': 2,
|
||||
// ... add new color here
|
||||
};
|
||||
|
||||
const PRODUCT_COLOR_REVERSE = {
|
||||
1: 'BLACK',
|
||||
2: 'BRONZE',
|
||||
// ... and remember to add it here too!
|
||||
};
|
||||
```
|
||||
|
||||
**Issues:**
|
||||
- ❌ Prone to human error (forgetting reverse mapping)
|
||||
- ❌ Risk of mismatches between forward/reverse
|
||||
- ❌ No validation for duplicate values
|
||||
- ❌ No documentation of available slots
|
||||
- ❌ Difficult for non-technical users to add values
|
||||
|
||||
## Proposed Solution: Python-Generated Tables
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
User edits simple source file
|
||||
↓
|
||||
Python validation script
|
||||
↓
|
||||
Generate JavaScript + Python constants
|
||||
↓
|
||||
Auto-served by Flask (or manual build step)
|
||||
```
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
app/
|
||||
data/
|
||||
encoding_values.json (or .yaml) ← USER EDITS THIS
|
||||
js/
|
||||
encoding_tables.js ← AUTO-GENERATED
|
||||
generate_encoding_tables.py ← GENERATOR SCRIPT
|
||||
```
|
||||
|
||||
## Source File Format
|
||||
|
||||
### Option 1: JSON with Comments Support
|
||||
|
||||
**`app/data/encoding_values.json`**
|
||||
```json
|
||||
{
|
||||
"metadata": {
|
||||
"version": 0,
|
||||
"description": "Product encoding value definitions"
|
||||
},
|
||||
"tables": {
|
||||
"product_type": {
|
||||
"max_value": 15,
|
||||
"bit_size": 4,
|
||||
"values": [
|
||||
{"name": "PATIO_DOOR", "value": 1, "display": "Patio Door"},
|
||||
{"name": "STORM_DOOR", "value": 3, "display": "Storm Door"},
|
||||
{"name": "STORM_WINDOW", "value": 5, "display": "Storm Window"},
|
||||
{"name": "PRIMARY_WINDOW", "value": 7, "display": "Primary Window"}
|
||||
]
|
||||
},
|
||||
"product_color": {
|
||||
"max_value": 15,
|
||||
"bit_size": 4,
|
||||
"values": [
|
||||
{"name": "BLACK", "value": 1, "display": "Black"},
|
||||
{"name": "BRONZE", "value": 2, "display": "Bronze"},
|
||||
{"name": "SANDSTONE", "value": 3, "display": "Sandstone"},
|
||||
{"name": "WHITE", "value": 4, "display": "White"},
|
||||
{"name": "TAN", "value": 5, "display": "Tan"},
|
||||
{"name": "MILL", "value": 6, "display": "Mill"}
|
||||
]
|
||||
},
|
||||
"product_material": {
|
||||
"max_value": 15,
|
||||
"bit_size": 4,
|
||||
"values": [
|
||||
{"name": "ALUMINUM", "value": 1, "display": "Aluminum"},
|
||||
{"name": "VINYL", "value": 2, "display": "Vinyl"}
|
||||
]
|
||||
},
|
||||
"hardware_type": {
|
||||
"max_value": 31,
|
||||
"bit_size": 5,
|
||||
"values": [
|
||||
{"name": "NONE", "value": 0, "display": "None"},
|
||||
{"name": "LEVER", "value": 1, "display": "Lever"},
|
||||
{"name": "PULL", "value": 2, "display": "Pull"},
|
||||
{"name": "PULL_HANDLE", "value": 3, "display": "Pull Handle"},
|
||||
{"name": "DEADBOLT", "value": 4, "display": "Deadbolt"},
|
||||
{"name": "HINGE", "value": 5, "display": "Hinge"}
|
||||
]
|
||||
},
|
||||
"hardware_style": {
|
||||
"max_value": 31,
|
||||
"bit_size": 5,
|
||||
"values": [
|
||||
{"name": "NONE", "value": 0, "display": "None"},
|
||||
{"name": "STANDARD", "value": 1, "display": "Standard"},
|
||||
{"name": "PUSH", "value": 2, "display": "Push"},
|
||||
{"name": "ALTERNATIVE", "value": 3, "display": "Alternative"},
|
||||
{"name": "CONTEMPORARY", "value": 4, "display": "Contemporary"},
|
||||
{"name": "TRADITIONAL", "value": 5, "display": "Traditional"}
|
||||
]
|
||||
},
|
||||
"hardware_color": {
|
||||
"max_value": 31,
|
||||
"bit_size": 5,
|
||||
"values": [
|
||||
{"name": "NONE", "value": 0, "display": "None"},
|
||||
{"name": "BRASS", "value": 1, "display": "Brass"},
|
||||
{"name": "WHITE", "value": 2, "display": "White"},
|
||||
{"name": "BLACK", "value": 3, "display": "Black"},
|
||||
{"name": "SATIN", "value": 4, "display": "Satin"},
|
||||
{"name": "NICKEL", "value": 5, "display": "Nickel"},
|
||||
{"name": "BRONZE", "value": 6, "display": "Bronze"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Option 2: YAML (Better for Comments)
|
||||
|
||||
**`app/data/encoding_values.yaml`**
|
||||
```yaml
|
||||
version: 0
|
||||
description: Product encoding value definitions
|
||||
|
||||
tables:
|
||||
product_type:
|
||||
max_value: 15
|
||||
bit_size: 4
|
||||
values:
|
||||
- name: PATIO_DOOR
|
||||
value: 1
|
||||
display: Patio Door
|
||||
- name: STORM_DOOR
|
||||
value: 3
|
||||
display: Storm Door
|
||||
# Reserved: 2, 4, 6, 8-15
|
||||
|
||||
product_color:
|
||||
max_value: 15
|
||||
bit_size: 4
|
||||
values:
|
||||
- name: BLACK
|
||||
value: 1
|
||||
display: Black
|
||||
- name: BRONZE
|
||||
value: 2
|
||||
display: Bronze
|
||||
# ... etc
|
||||
# Reserved: 7-15 (9 slots available)
|
||||
```
|
||||
|
||||
## User Workflow
|
||||
|
||||
### Adding a New Color
|
||||
|
||||
**Before (Manual):**
|
||||
1. Open `encoding_helper.js`
|
||||
2. Find `PRODUCT_COLOR` object
|
||||
3. Add new entry with unused value
|
||||
4. Find `PRODUCT_COLOR_REVERSE` object
|
||||
5. Add matching reverse entry
|
||||
6. Hope you didn't make a mistake
|
||||
|
||||
**After (Generated):**
|
||||
1. Open `encoding_values.json`
|
||||
2. Add one line: `{"name": "CHARCOAL", "value": 7, "display": "Charcoal"}`
|
||||
3. Run `python generate_encoding_tables.py` (or auto-regenerated by Flask)
|
||||
4. Done! JavaScript and Python constants updated
|
||||
|
||||
## Generator Script
|
||||
|
||||
**`app/generate_encoding_tables.py`**
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate JavaScript and Python encoding tables from source JSON/YAML.
|
||||
|
||||
Usage:
|
||||
python generate_encoding_tables.py
|
||||
|
||||
Or add to Flask app for auto-generation on file change.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
def load_source_file(filepath):
|
||||
"""Load encoding values from JSON or YAML."""
|
||||
with open(filepath, 'r') as f:
|
||||
if filepath.endswith('.yaml') or filepath.endswith('.yml'):
|
||||
import yaml
|
||||
return yaml.safe_load(f)
|
||||
else:
|
||||
return json.load(f)
|
||||
|
||||
def validate_table(table_name, table_data):
|
||||
"""Validate a single table for errors."""
|
||||
errors = []
|
||||
max_value = table_data['max_value']
|
||||
values = table_data['values']
|
||||
|
||||
used_values = set()
|
||||
used_names = set()
|
||||
|
||||
for item in values:
|
||||
name = item['name']
|
||||
value = item['value']
|
||||
|
||||
# Check for duplicate values
|
||||
if value in used_values:
|
||||
errors.append(f"{table_name}: Duplicate value {value}")
|
||||
used_values.add(value)
|
||||
|
||||
# Check for duplicate names
|
||||
if name in used_names:
|
||||
errors.append(f"{table_name}: Duplicate name '{name}'")
|
||||
used_names.add(name)
|
||||
|
||||
# Check value range
|
||||
if value < 0 or value > max_value:
|
||||
errors.append(f"{table_name}: Value {value} out of range (0-{max_value})")
|
||||
|
||||
# Validate naming convention (uppercase with underscores)
|
||||
if not name.isupper() or not name.replace('_', '').isalpha():
|
||||
errors.append(f"{table_name}: Invalid name format '{name}' (use UPPERCASE_WITH_UNDERSCORES)")
|
||||
|
||||
# Report available slots
|
||||
available = sorted(set(range(max_value + 1)) - used_values)
|
||||
info = f"{table_name}: {len(used_values)}/{max_value + 1} slots used, {len(available)} available"
|
||||
if available and len(available) <= 10:
|
||||
info += f" {available}"
|
||||
|
||||
return errors, info
|
||||
|
||||
def generate_javascript(data, output_path):
|
||||
"""Generate JavaScript constants file."""
|
||||
lines = [
|
||||
"// AUTO-GENERATED FILE - DO NOT EDIT MANUALLY",
|
||||
"// Generated from: app/data/encoding_values.json",
|
||||
f"// Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
|
||||
"//",
|
||||
"// To add new values:",
|
||||
"// 1. Edit app/data/encoding_values.json",
|
||||
"// 2. Run: python app/generate_encoding_tables.py",
|
||||
"// 3. Restart Flask server (or it auto-reloads)",
|
||||
"",
|
||||
"// ============================================================================",
|
||||
"// ENCODING CONSTANTS",
|
||||
"// ============================================================================",
|
||||
"",
|
||||
f"const ENCODING_VERSION = {data['metadata']['version']};",
|
||||
""
|
||||
]
|
||||
|
||||
for table_name, table_data in data['tables'].items():
|
||||
const_name = table_name.upper()
|
||||
|
||||
# Generate forward mapping
|
||||
lines.append(f"// {const_name} (max: {table_data['max_value']}, bits: {table_data['bit_size']})")
|
||||
lines.append(f"const {const_name} = {{")
|
||||
for item in table_data['values']:
|
||||
lines.append(f" '{item['name']}': {item['value']},")
|
||||
lines.append("};")
|
||||
lines.append("")
|
||||
|
||||
# Generate reverse mapping
|
||||
lines.append(f"const {const_name}_REVERSE = {{")
|
||||
for item in table_data['values']:
|
||||
lines.append(f" {item['value']}: '{item['name']}',")
|
||||
lines.append("};")
|
||||
lines.append("")
|
||||
|
||||
# Generate display mapping
|
||||
lines.append(f"const {const_name}_DISPLAY = {{")
|
||||
for item in table_data['values']:
|
||||
lines.append(f" '{item['name']}': '{item['display']}',")
|
||||
lines.append("};")
|
||||
lines.append("")
|
||||
|
||||
# Write file
|
||||
with open(output_path, 'w') as f:
|
||||
f.write('\n'.join(lines))
|
||||
|
||||
print(f"✓ Generated: {output_path}")
|
||||
|
||||
def generate_python(data, output_path):
|
||||
"""Generate Python constants file (optional)."""
|
||||
lines = [
|
||||
'"""',
|
||||
"AUTO-GENERATED FILE - DO NOT EDIT MANUALLY",
|
||||
"Generated from: app/data/encoding_values.json",
|
||||
f"Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
|
||||
'"""',
|
||||
"",
|
||||
f"ENCODING_VERSION = {data['metadata']['version']}",
|
||||
""
|
||||
]
|
||||
|
||||
for table_name, table_data in data['tables'].items():
|
||||
const_name = table_name.upper()
|
||||
|
||||
lines.append(f"# {const_name} (max: {table_data['max_value']}, bits: {table_data['bit_size']})")
|
||||
lines.append(f"{const_name} = {{")
|
||||
for item in table_data['values']:
|
||||
lines.append(f" '{item['name']}': {item['value']},")
|
||||
lines.append("}")
|
||||
lines.append("")
|
||||
|
||||
lines.append(f"{const_name}_REVERSE = {{")
|
||||
for item in table_data['values']:
|
||||
lines.append(f" {item['value']}: '{item['name']}',")
|
||||
lines.append("}")
|
||||
lines.append("")
|
||||
|
||||
with open(output_path, 'w') as f:
|
||||
f.write('\n'.join(lines))
|
||||
|
||||
print(f"✓ Generated: {output_path}")
|
||||
|
||||
def generate_documentation(data, output_path):
|
||||
"""Generate markdown documentation."""
|
||||
lines = [
|
||||
"# Encoding Values Reference",
|
||||
"",
|
||||
f"**Version:** {data['metadata']['version']} ",
|
||||
f"**Generated:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} ",
|
||||
"",
|
||||
"## Overview",
|
||||
"",
|
||||
"This document lists all currently defined encoding values.",
|
||||
"",
|
||||
"## Quick Reference",
|
||||
""
|
||||
]
|
||||
|
||||
for table_name, table_data in data['tables'].items():
|
||||
lines.append(f"### {table_name.replace('_', ' ').title()}")
|
||||
lines.append("")
|
||||
lines.append(f"**Capacity:** {len(table_data['values'])}/{table_data['max_value'] + 1} slots used ")
|
||||
lines.append(f"**Bit Size:** {table_data['bit_size']} bits ")
|
||||
lines.append("")
|
||||
lines.append("| Value | Name | Display |")
|
||||
lines.append("|-------|------|---------|")
|
||||
|
||||
for item in sorted(table_data['values'], key=lambda x: x['value']):
|
||||
lines.append(f"| {item['value']} | `{item['name']}` | {item['display']} |")
|
||||
|
||||
lines.append("")
|
||||
|
||||
with open(output_path, 'w') as f:
|
||||
f.write('\n'.join(lines))
|
||||
|
||||
print(f"✓ Generated: {output_path}")
|
||||
|
||||
def main():
|
||||
"""Main generation function."""
|
||||
base_dir = Path(__file__).parent
|
||||
source_file = base_dir / 'data' / 'encoding_values.json'
|
||||
|
||||
# Load source data
|
||||
print(f"Loading: {source_file}")
|
||||
data = load_source_file(source_file)
|
||||
|
||||
# Validate all tables
|
||||
print("\nValidating tables...")
|
||||
all_errors = []
|
||||
for table_name, table_data in data['tables'].items():
|
||||
errors, info = validate_table(table_name, table_data)
|
||||
print(f" {info}")
|
||||
all_errors.extend(errors)
|
||||
|
||||
if all_errors:
|
||||
print("\n❌ VALIDATION ERRORS:")
|
||||
for error in all_errors:
|
||||
print(f" - {error}")
|
||||
return 1
|
||||
|
||||
print("\n✓ All tables valid")
|
||||
|
||||
# Generate files
|
||||
print("\nGenerating files...")
|
||||
generate_javascript(data, base_dir / 'js' / 'encoding_tables.js')
|
||||
generate_python(data, base_dir / 'encoding_tables_constants.py')
|
||||
generate_documentation(data, base_dir.parent / 'planning' / 'ENCODING_VALUES_REFERENCE.md')
|
||||
|
||||
print("\n✓ Generation complete!")
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
exit(main())
|
||||
```
|
||||
|
||||
## Flask Integration (Auto-Generation)
|
||||
|
||||
Add to `app.py`:
|
||||
|
||||
```python
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
@app.before_request
|
||||
def check_encoding_tables():
|
||||
"""Auto-regenerate encoding tables if source has changed."""
|
||||
source_file = 'data/encoding_values.json'
|
||||
output_file = 'js/encoding_tables.js'
|
||||
|
||||
# Check if regeneration needed
|
||||
if not os.path.exists(output_file):
|
||||
print("Encoding tables not found, generating...")
|
||||
os.system('python generate_encoding_tables.py')
|
||||
else:
|
||||
source_mtime = os.path.getmtime(source_file)
|
||||
output_mtime = os.path.getmtime(output_file)
|
||||
|
||||
if source_mtime > output_mtime:
|
||||
print("Encoding values changed, regenerating tables...")
|
||||
os.system('python generate_encoding_tables.py')
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
### For Non-Technical Users
|
||||
- ✅ Edit simple JSON file (no JavaScript knowledge needed)
|
||||
- ✅ Clear structure with examples
|
||||
- ✅ Automatic validation catches errors
|
||||
- ✅ Can't create mismatches between forward/reverse
|
||||
|
||||
### For Developers
|
||||
- ✅ Single source of truth
|
||||
- ✅ Auto-generated documentation
|
||||
- ✅ Validation prevents bugs
|
||||
- ✅ Easy to add new tables
|
||||
- ✅ Can generate for multiple targets (JS, Python, docs)
|
||||
|
||||
### For Maintenance
|
||||
- ✅ Version control friendly (one file to track)
|
||||
- ✅ Easy to review changes (just JSON diff)
|
||||
- ✅ Reports available slots automatically
|
||||
- ✅ Enforces naming conventions
|
||||
|
||||
## Migration Path
|
||||
|
||||
### Phase 1: Create Source File
|
||||
1. Extract existing values to `encoding_values.json`
|
||||
2. Test generator script
|
||||
3. Verify output matches current `encoding_helper.js`
|
||||
|
||||
### Phase 2: Integrate Generator
|
||||
1. Add generator script to project
|
||||
2. Update build process / Flask app
|
||||
3. Test auto-generation
|
||||
|
||||
### Phase 3: Switch to Generated Tables
|
||||
1. Update `encoding_helper.js` to import generated file
|
||||
2. Remove manual mappings
|
||||
3. Update documentation
|
||||
|
||||
### Phase 4: User Documentation
|
||||
1. Create guide for adding new values
|
||||
2. Document validation rules
|
||||
3. Add examples
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Web UI for editing values (no need to edit JSON directly)
|
||||
- Export to CSV for client review
|
||||
- Import from Excel/CSV
|
||||
- Visual capacity indicators
|
||||
- Conflict detection across tables
|
||||
- Automatic value assignment (find next available slot)
|
||||
|
||||
## Related Files
|
||||
|
||||
- [ENCODING_SYSTEM.md](ENCODING_SYSTEM.md) - Technical specification
|
||||
- `app/js/encoding_helper.js` - Current manual implementation
|
||||
- `app/js/encoding_tables.js` - Future generated file
|
||||
|
||||
---
|
||||
|
||||
**Status:** Planning
|
||||
**Priority:** Medium
|
||||
**Effort:** ~4-6 hours implementation + testing
|
||||
**Dependencies:** None
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,208 @@
|
||||
# Product Finder - Project Features Summary
|
||||
|
||||
**Date:** March 25, 2026
|
||||
**Project:** Product Finder Application for Columbia Metals
|
||||
**Participants:** Jesse Salmon, Jason Soltys, Darlene Lampe
|
||||
|
||||
---
|
||||
|
||||
## Core Application Features
|
||||
|
||||
### 1. Product Selection Workflow
|
||||
- **Multi-step navigation system** using button-based interface
|
||||
- **Goal:** Minimize text entry - "tap, tap, tap" workflow
|
||||
- **JSON-driven configuration** for flexible navigation without code changes
|
||||
- Sequential selection process: Color → Size → Model/Type → Accessories
|
||||
|
||||
### 2. Color Selection
|
||||
- **Visual color buttons** (similar to current implementation showing Black, Bronze, Mill, etc.)
|
||||
- Filters available products based on color selection
|
||||
- **Mill finish to be removed** (not available for any door)
|
||||
- Default hardware colors match door colors (e.g., bronze door = black pull, white door = white pull)
|
||||
|
||||
### 3. Size Selection System
|
||||
|
||||
#### Standard Sizes
|
||||
- **Three standard sizes for storm doors:**
|
||||
- 2668 (26" x 68")
|
||||
- 2868 (28" x 68")
|
||||
- 3068 (36" x 81")
|
||||
- Display both nominal size (e.g., "3068") and actual dimensions (e.g., "36 x 81")
|
||||
- **Button-based interface** (similar to color selection)
|
||||
- Standard sizes auto-populate width and height fields (grayed out/non-editable)
|
||||
|
||||
#### Custom Sizes
|
||||
- **Custom option button** reveals width/height input fields
|
||||
- **Storm doors:** Opening size ONLY (no tip-to-tip)
|
||||
- Display warning in RED: "Opening size only - not tip to tip"
|
||||
- **Patio doors:** Either opening size or tip-to-tip allowed
|
||||
- Toggle between standard size buttons and custom input
|
||||
|
||||
### 4. Product Categories
|
||||
|
||||
#### Storm Doors
|
||||
- **Models:** Cobra, King, King Dual Vent
|
||||
- Category filtering based on selected criteria
|
||||
- Product images for each model
|
||||
- **Measurement requirement:** Opening size only for custom orders
|
||||
|
||||
#### Patio Doors
|
||||
- Standard size: 5069
|
||||
- Can use opening size or tip-to-tip for custom
|
||||
|
||||
#### Products to Exclude
|
||||
- Parts (Z-bars, inserts)
|
||||
- Discontinued items (marked in database)
|
||||
- M1200 patio storm door
|
||||
- Storm door inserts
|
||||
|
||||
### 5. Hardware & Accessories
|
||||
|
||||
#### Hardware Selection
|
||||
- **Types:** Brass lever, Satin silver, Black pull, White pull
|
||||
- Labeled as "Hardware" in CGW system
|
||||
- Default hardware based on door color
|
||||
- Optional customization via dropdown
|
||||
|
||||
#### Hinge Side Selection
|
||||
- **Required field** - no default setting
|
||||
- Options: Left hand or Right hand
|
||||
- **Measurement instruction:** "Facing the door from the outside, which side are the hinges on?"
|
||||
- Visual prompt/popup to ensure customer understands measurement perspective
|
||||
|
||||
### 6. Visual Elements
|
||||
|
||||
#### Product Images
|
||||
- Display product photos for each door model
|
||||
- **Door image flipping:** Mirror image based on hinge side selection (JavaScript/CSS)
|
||||
- Images sourced from website and database
|
||||
|
||||
#### Outside View Indicator
|
||||
- **Proposed design:** Doghouse illustration with:
|
||||
- Small roof/overhang
|
||||
- Plant or bush beside door
|
||||
- Wall indication
|
||||
- Purpose: Help customers understand they're viewing from outside
|
||||
- Simple, rough design acceptable
|
||||
|
||||
#### Navigation Elements
|
||||
- Button-based interface throughout
|
||||
- Visual color swatches
|
||||
- Product thumbnails
|
||||
- Continue/Next buttons
|
||||
|
||||
### 7. Data Management
|
||||
|
||||
#### Database Integration
|
||||
- **Source:** CGW (Columbia Gateway Windows) database
|
||||
- **Locations:** Iola, Kansas City, Lindsberg
|
||||
- Google Sheets for product data management
|
||||
- Product codes exported from CGW
|
||||
|
||||
#### Data Filtering
|
||||
- Filter discontinued items (marked in spreadsheet)
|
||||
- Remove accessories from main product list
|
||||
- **Column H (Door Type/Subtype):** Delete to remove from product list
|
||||
- **Columns J & K:** Accessory/hardware flags
|
||||
|
||||
#### Product Naming Consistency
|
||||
- Challenge: Same products have different model numbers across locations
|
||||
- **Solution:** Use catalog names (customer-facing)
|
||||
- Internal CGW names vary by plant
|
||||
- Need standardization project for matching products across locations
|
||||
|
||||
### 8. User Experience Features
|
||||
|
||||
#### Add to Cart/Order
|
||||
- Button to add selected product to sales order
|
||||
- Appears after all selections made
|
||||
- Label: "Add to [cart/sales order/hardware]"
|
||||
|
||||
#### Measurement Instructions
|
||||
- **Link to PDF** with measurement guidelines
|
||||
- Accessible from size selection screen
|
||||
- Shows how to determine hinge side, measure openings, etc.
|
||||
|
||||
#### Validation & Warnings
|
||||
- Opening size requirement for custom storm doors (highlighted in red)
|
||||
- Required field indicators (hinge side selection)
|
||||
- Visual cues for defaults vs. custom selections
|
||||
|
||||
### 9. Architecture & Technical Approach
|
||||
|
||||
#### Navigation System
|
||||
- JSON file manages all navigation flow
|
||||
- Changes made via JSON manipulation (no code changes needed)
|
||||
- Flexible architecture to support iteration
|
||||
- **Current limitation:** Toggle functionality requires code changes
|
||||
|
||||
#### Development Philosophy
|
||||
- Hard-code initial features for rapid iteration
|
||||
- Avoid premature optimization
|
||||
- Build architecture after understanding full requirements
|
||||
- Keep front-end decoupled from back-end structure
|
||||
|
||||
### 10. Future Considerations
|
||||
|
||||
#### Windows
|
||||
- Primary windows and storm windows have different measurement rules
|
||||
- "Whole different ball game" - to be addressed later
|
||||
|
||||
#### Parts/Accessories Integration
|
||||
- Parts sheet for storm door inserts
|
||||
- Separate handling from complete products
|
||||
- Discontinued items may need parts availability
|
||||
|
||||
#### Multi-Location Support
|
||||
- Consolidate product naming across plants
|
||||
- Maintain location-specific inventory
|
||||
- Unified catalog names for customers
|
||||
|
||||
---
|
||||
|
||||
## Key Business Rules
|
||||
|
||||
1. **Storm doors (custom):** Opening size only
|
||||
2. **Patio doors (custom):** Opening size OR tip-to-tip
|
||||
3. **Standard sizes:** Auto-populate dimensions (read-only)
|
||||
4. **Hinge side:** Required selection with instructional prompt
|
||||
5. **Hardware:** Defaults based on color, optional customization
|
||||
6. **Discontinued items:** Filter from product display
|
||||
7. **Parts/accessories:** Exclude from main product categories
|
||||
|
||||
---
|
||||
|
||||
## Implementation Priorities
|
||||
|
||||
### High Priority
|
||||
1. Size selection with standard sizes as buttons
|
||||
2. Remove discontinued items from display
|
||||
3. Hinge side selection with flip image functionality
|
||||
4. Hardware dropdown with defaults
|
||||
5. Custom size with opening-size-only warning for storm doors
|
||||
|
||||
### Medium Priority
|
||||
1. Outside view visual indicator (doghouse design)
|
||||
2. Link to measurement instructions PDF
|
||||
3. Add to cart/order button
|
||||
4. Product naming standardization across locations
|
||||
|
||||
### Low Priority (Future)
|
||||
1. Windows integration
|
||||
2. Parts/accessories handling
|
||||
3. Advanced filtering options
|
||||
4. Multi-location inventory management
|
||||
|
||||
---
|
||||
|
||||
## Quality & Accuracy Concerns
|
||||
|
||||
- **Arnie's primary concern:** Accuracy of the system
|
||||
- Emphasis on preventing measurement errors
|
||||
- Visual aids to reduce confusion
|
||||
- Clear labeling and validation messages
|
||||
- "It's too complicated" - need to prove simplicity through good UX
|
||||
|
||||
---
|
||||
|
||||
*This summary was generated from the March 25, 2026 project planning meeting transcript.*
|
||||
Binary file not shown.
@@ -0,0 +1,147 @@
|
||||
# Product App - Modular Flask Application
|
||||
|
||||
## 🏗️ Structure
|
||||
|
||||
```
|
||||
product_app/
|
||||
├── app.py # Main application file
|
||||
├── passenger_wsgi.py # Passenger WSGI entry point for production
|
||||
├── requirements.txt # Python dependencies
|
||||
├── blueprints/ # Modular routes organized by feature
|
||||
│ ├── __init__.py
|
||||
│ ├── auth.py # Login/logout/session management
|
||||
│ └── users.py # User CRUD operations
|
||||
├── templates/ # HTML templates
|
||||
│ ├── login.html
|
||||
│ └── user_manager.html
|
||||
└── data/ # Data storage
|
||||
└── users.json # User accounts with hashed passwords
|
||||
```
|
||||
|
||||
## 🎯 Features Implemented
|
||||
|
||||
### ✅ Authentication System (blueprints/auth.py)
|
||||
- Login with username/password
|
||||
- Session management
|
||||
- Password hashing (PBKDF2-SHA256, 1M iterations)
|
||||
- Login required decorator
|
||||
- Permission checking: `can_user('manage_users')`
|
||||
- User status checking (active/inactive)
|
||||
|
||||
### ✅ User Management (blueprints/users.py)
|
||||
- View all users
|
||||
- Add new users
|
||||
- Delete users
|
||||
- Toggle active/inactive status
|
||||
- Change passwords (requires admin verification)
|
||||
- Location-based access control
|
||||
- Permission system (manage_users, create_quotes, etc.)
|
||||
- Download users as JSON
|
||||
|
||||
## 🔐 Default Login
|
||||
|
||||
- **Username:** `Master`
|
||||
- **Password:** `Master`
|
||||
- **Permissions:** Full access (manage_users)
|
||||
|
||||
## 🚀 Local Testing
|
||||
|
||||
```bash
|
||||
cd product_app
|
||||
python app.py
|
||||
```
|
||||
|
||||
Visit: http://localhost:8080/
|
||||
|
||||
## Routes
|
||||
|
||||
### Main Routes
|
||||
- `/` - Home (redirects to login if not authenticated)
|
||||
- `/test` - Test page to verify app is running
|
||||
|
||||
### Authentication Routes (auth_bp)
|
||||
- `/login` - Login page
|
||||
- `/logout` - Logout
|
||||
- `/api/login` - POST: Login API
|
||||
- `/api/session` - GET: Current session info
|
||||
|
||||
### User Management Routes (users_bp)
|
||||
- `/users/` - User management page
|
||||
- `/users/api` - GET: List all users, POST: Add user
|
||||
- `/users/api/<index>` - DELETE: Delete user
|
||||
- `/users/api/<index>/active` - PATCH: Toggle active status
|
||||
- `/users/api/<index>/change-password` - POST: Change password
|
||||
- `/users/api/download` - GET: Download users.json
|
||||
|
||||
## 📦 Production Deployment
|
||||
|
||||
### Upload to Server: `/home/bmdwtjuw/product-finder/`
|
||||
|
||||
Files to upload:
|
||||
- `app.py`
|
||||
- `passenger_wsgi.py` (or just use existing)
|
||||
- `blueprints/` (entire folder)
|
||||
- `__init__.py`
|
||||
- `auth.py`
|
||||
- `users.py`
|
||||
- `templates/` (entire folder)
|
||||
- `login.html`
|
||||
- `user_manager.html`
|
||||
- `data/users.json`
|
||||
|
||||
### Control Panel Settings
|
||||
|
||||
- **Application startup file:** `passenger_wsgi.py` (or `app.py`)
|
||||
- **Application Entry point:** `application`
|
||||
- **Python version:** 3.13.11
|
||||
|
||||
### Test After Deployment:
|
||||
1. https://columbiawindows.com/product-finder/test
|
||||
2. https://columbiawindows.com/product-finder/login
|
||||
3. Login with Master/Master
|
||||
4. Test user management
|
||||
|
||||
## 🔧 Adding New Features
|
||||
|
||||
### Create a New Blueprint
|
||||
|
||||
1. Create `blueprints/your_feature.py`:
|
||||
|
||||
```python
|
||||
from flask import Blueprint, render_template
|
||||
from blueprints.auth import login_required, can_user
|
||||
|
||||
your_feature_bp = Blueprint('your_feature', __name__, url_prefix='/your-feature')
|
||||
|
||||
@your_feature_bp.route('/')
|
||||
@login_required
|
||||
def index():
|
||||
return render_template('your_feature.html')
|
||||
```
|
||||
|
||||
2. Register in `app.py`:
|
||||
|
||||
```python
|
||||
from blueprints.your_feature import your_feature_bp
|
||||
app.register_blueprint(your_feature_bp)
|
||||
```
|
||||
|
||||
3. Create `templates/your_feature.html`
|
||||
|
||||
4. Test locally, then upload to server
|
||||
|
||||
## 📝 Benefits of Blueprint Structure
|
||||
|
||||
✅ **Separation of Concerns:** Each feature in its own file
|
||||
✅ **Easy to Maintain:** Find and edit specific features quickly
|
||||
✅ **Scalable:** Add new features without touching existing code
|
||||
✅ **Testable:** Each blueprint can be tested independently
|
||||
✅ **Reusable:** Share decorators (login_required, permission_required) across blueprints
|
||||
|
||||
## 🛠️ Next Steps
|
||||
|
||||
- Add more blueprints for other features (quotes, products, etc.)
|
||||
- Add more templates as needed
|
||||
- Extend permission system
|
||||
- Add location selection page
|
||||
- Add the main product finder quiz
|
||||
@@ -0,0 +1,87 @@
|
||||
from flask import Flask, session, redirect, url_for
|
||||
import secrets
|
||||
import os
|
||||
|
||||
# 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'
|
||||
|
||||
# Import and register blueprints
|
||||
from blueprints.auth import auth_bp
|
||||
from blueprints.users import users_bp
|
||||
|
||||
app.register_blueprint(auth_bp)
|
||||
app.register_blueprint(users_bp)
|
||||
|
||||
# Home route
|
||||
@app.route('/')
|
||||
def home():
|
||||
"""Redirect to login if not authenticated, otherwise show home"""
|
||||
if 'user_id' not in session:
|
||||
return redirect(url_for('auth.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('auth.select_location_page'))
|
||||
|
||||
return f"""
|
||||
<html>
|
||||
<head><title>Home</title></head>
|
||||
<body style="font-family: Arial; padding: 20px;">
|
||||
<h1>Welcome, {session.get('username', 'User')}!</h1>
|
||||
<p>Current Location: {session.get('currentLocation', 'Not selected')}</p>
|
||||
<ul>
|
||||
<li><a href="{url_for('users.user_manager')}">Manage Users</a></li>
|
||||
<li><a href="{url_for('auth.select_location_page')}">Change Location</a></li>
|
||||
<li><a href="{url_for('auth.logout')}">Logout</a></li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
@app.route('/test')
|
||||
def test():
|
||||
"""Test route to verify app is running"""
|
||||
import sys
|
||||
return f"""
|
||||
<html>
|
||||
<head><title>Test Page</title></head>
|
||||
<body style="font-family: Arial; padding: 20px;">
|
||||
<h1>Flask App is Running!</h1>
|
||||
<ul>
|
||||
<li><strong>Python:</strong> {sys.version}</li>
|
||||
<li><strong>Routes:</strong> {len(list(app.url_map.iter_rules()))}</li>
|
||||
</ul>
|
||||
<p><a href="{url_for('auth.login_page')}">Login</a></p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
# Error handlers
|
||||
@app.errorhandler(404)
|
||||
def page_not_found(e):
|
||||
return "<h1>404 - Page Not Found</h1>", 404
|
||||
|
||||
@app.errorhandler(500)
|
||||
def internal_error(e):
|
||||
return "<h1>500 - Internal Server Error</h1>", 500
|
||||
|
||||
# This is the critical line for Passenger
|
||||
application = app
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Create necessary directories
|
||||
os.makedirs(os.path.join(basedir, 'templates'), exist_ok=True)
|
||||
os.makedirs(os.path.join(basedir, 'data'), exist_ok=True)
|
||||
|
||||
# Run the application
|
||||
app.run(debug=True, host='0.0.0.0', port=8080)
|
||||
@@ -0,0 +1 @@
|
||||
# Blueprint package
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user