Initial
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user