361 lines
12 KiB
Python
361 lines
12 KiB
Python
"""
|
|
Product Finder Blueprint
|
|
Handles the product quiz and recommendations
|
|
"""
|
|
from flask import Blueprint, render_template, send_from_directory, session, jsonify, redirect, url_for, request
|
|
from blueprints.auth import login_required, get_current_user, can_user
|
|
import data_access as da
|
|
import os
|
|
import json
|
|
import uuid
|
|
from datetime import datetime, timezone
|
|
import config
|
|
|
|
products_bp = Blueprint('products', __name__, url_prefix='/quiz')
|
|
|
|
# Get base directory
|
|
basedir = os.path.dirname(os.path.dirname(__file__))
|
|
|
|
@products_bp.route('/')
|
|
@login_required
|
|
def product_finder():
|
|
"""Serve the product finder quiz page"""
|
|
return render_template('product_finder.html')
|
|
|
|
@products_bp.route('/index')
|
|
@login_required
|
|
def quiz_index():
|
|
"""Alternative route for product finder"""
|
|
return render_template('product_finder.html')
|
|
|
|
@products_bp.route('/advanced-search')
|
|
@login_required
|
|
def advanced_search():
|
|
"""Serve the advanced search page"""
|
|
return render_template('advanced_search.html')
|
|
|
|
@products_bp.route('/list')
|
|
@login_required
|
|
def product_list():
|
|
"""Serve the product listing page with search and pagination"""
|
|
# Check if user has permission to manage products
|
|
if not can_user('manage_products'):
|
|
return redirect(url_for('products.product_finder'))
|
|
return render_template('product_list.html')
|
|
|
|
@products_bp.route('/manage')
|
|
@login_required
|
|
def product_manager():
|
|
"""Serve the product management page"""
|
|
# Check if user has permission to manage products
|
|
if not can_user('manage_products'):
|
|
return redirect(url_for('products.product_finder'))
|
|
return render_template('product_manager.html')
|
|
|
|
@products_bp.route('/api/user-session')
|
|
@login_required
|
|
def get_user_session():
|
|
"""Return current user's session data including accessible locations"""
|
|
user = get_current_user()
|
|
permissions = user.get('permissions', {}) if user else {}
|
|
|
|
return jsonify({
|
|
'username': session.get('username'),
|
|
'userId': None, # Not needed with username-based sessions
|
|
'currentLocation': session.get('currentLocation'),
|
|
'accessibleLocations': session.get('accessibleLocations', []),
|
|
'role': session.get('role', 'user'),
|
|
'permissions': permissions
|
|
})
|
|
|
|
# Product API Endpoints
|
|
@products_bp.route('/api/products', methods=['GET'])
|
|
@login_required
|
|
def get_products():
|
|
"""Get all products or filter by location"""
|
|
location = request.args.get('location')
|
|
products = da.get_all_products(location=location)
|
|
return jsonify({
|
|
'status': 'success',
|
|
'products': products
|
|
})
|
|
|
|
@products_bp.route('/api/products/search', methods=['GET'])
|
|
@login_required
|
|
def search_products():
|
|
"""Search products with pagination and filtering"""
|
|
# Get query parameters
|
|
search_query = request.args.get('q', '').strip().lower()
|
|
page = int(request.args.get('page', 1))
|
|
per_page = int(request.args.get('per_page', 20))
|
|
location = request.args.get('location')
|
|
status_filter = request.args.get('status', 'all') # 'all', 'active', 'discontinued'
|
|
|
|
# Get all products
|
|
all_products = da.get_all_products(location=location)
|
|
|
|
# Filter by search query (description and product code)
|
|
if search_query:
|
|
filtered_products = [
|
|
p for p in all_products
|
|
if search_query in p.get('description', '').lower()
|
|
or search_query in p.get('productCode', '').lower()
|
|
]
|
|
else:
|
|
filtered_products = all_products
|
|
|
|
# Filter by status
|
|
if status_filter == 'active':
|
|
filtered_products = [p for p in filtered_products if not p.get('discontinued', False)]
|
|
elif status_filter == 'discontinued':
|
|
filtered_products = [p for p in filtered_products if p.get('discontinued', False)]
|
|
|
|
# Calculate pagination
|
|
total_products = len(filtered_products)
|
|
total_pages = (total_products + per_page - 1) // per_page if per_page > 0 else 1
|
|
start_idx = (page - 1) * per_page
|
|
end_idx = start_idx + per_page
|
|
|
|
# Get page slice
|
|
products_page = filtered_products[start_idx:end_idx]
|
|
|
|
return jsonify({
|
|
'status': 'success',
|
|
'products': products_page,
|
|
'pagination': {
|
|
'page': page,
|
|
'per_page': per_page,
|
|
'total_products': total_products,
|
|
'total_pages': total_pages,
|
|
'has_prev': page > 1,
|
|
'has_next': page < total_pages
|
|
}
|
|
})
|
|
|
|
@products_bp.route('/api/products/<product_code>', methods=['GET'])
|
|
@login_required
|
|
def get_product(product_code):
|
|
"""Get a specific product by code"""
|
|
product = da.get_product_by_code(product_code)
|
|
|
|
if not product:
|
|
return jsonify({
|
|
'status': 'error',
|
|
'message': 'Product not found'
|
|
}), 404
|
|
|
|
return jsonify({
|
|
'status': 'success',
|
|
'product': product
|
|
})
|
|
|
|
@products_bp.route('/api/products', methods=['POST'])
|
|
@login_required
|
|
def create_product():
|
|
"""Create a new product"""
|
|
if not can_user('manage_products'):
|
|
return jsonify({
|
|
'status': 'error',
|
|
'message': 'Permission denied'
|
|
}), 403
|
|
|
|
try:
|
|
data = request.json
|
|
|
|
# Create product via data access layer
|
|
product = da.create_product(data)
|
|
|
|
if not product:
|
|
return jsonify({
|
|
'status': 'error',
|
|
'message': 'Product code already exists'
|
|
}), 400
|
|
|
|
return jsonify({
|
|
'status': 'success',
|
|
'message': 'Product created successfully',
|
|
'product': product
|
|
})
|
|
|
|
except Exception as e:
|
|
da.rollback()
|
|
return jsonify({
|
|
'status': 'error',
|
|
'message': str(e)
|
|
}), 500
|
|
|
|
@products_bp.route('/api/products/<product_code>', methods=['PUT'])
|
|
@login_required
|
|
def update_product(product_code):
|
|
"""Update an existing product"""
|
|
if not can_user('manage_products'):
|
|
return jsonify({
|
|
'status': 'error',
|
|
'message': 'Permission denied'
|
|
}), 403
|
|
|
|
try:
|
|
data = request.json
|
|
|
|
# Update product via data access layer
|
|
product = da.update_product(product_code, data)
|
|
|
|
if not product:
|
|
return jsonify({
|
|
'status': 'error',
|
|
'message': 'Product not found'
|
|
}), 404
|
|
|
|
return jsonify({
|
|
'status': 'success',
|
|
'message': 'Product updated successfully',
|
|
'product': product
|
|
})
|
|
|
|
except Exception as e:
|
|
da.rollback()
|
|
return jsonify({
|
|
'status': 'error',
|
|
'message': str(e)
|
|
}), 500
|
|
|
|
@products_bp.route('/api/product-attributes', methods=['GET'])
|
|
@login_required
|
|
def get_product_attributes():
|
|
"""Get product attributes configuration"""
|
|
attributes = da.get_product_attributes()
|
|
|
|
return jsonify({
|
|
'status': 'success',
|
|
'attributes': attributes
|
|
})
|
|
|
|
@products_bp.route('/api/submit-quote', methods=['POST'])
|
|
@login_required
|
|
def submit_quote():
|
|
"""Submit current order items as a handoff file for downstream processing"""
|
|
user = get_current_user()
|
|
if not user:
|
|
return jsonify({
|
|
'status': 'error',
|
|
'message': 'Not logged in'
|
|
}), 401
|
|
|
|
# Allow explicit quote permission or super admin
|
|
if not can_user('create_quotes') and not user.get('superAdmin', False):
|
|
return jsonify({
|
|
'status': 'error',
|
|
'message': 'Permission denied'
|
|
}), 403
|
|
|
|
try:
|
|
payload = request.get_json(silent=True) or {}
|
|
items = payload.get('items', [])
|
|
|
|
if not isinstance(items, list) or len(items) == 0:
|
|
return jsonify({
|
|
'status': 'error',
|
|
'message': 'No order items to submit'
|
|
}), 400
|
|
|
|
# Normalize each item to a safe subset for handoff
|
|
normalized_items = []
|
|
for item in items:
|
|
if not isinstance(item, dict):
|
|
continue
|
|
normalized_items.append({
|
|
'productCode': str(item.get('productCode', '')).strip(),
|
|
'description': str(item.get('description', '')).strip(),
|
|
'material': str(item.get('material', '')).strip(),
|
|
'color': str(item.get('color', '')).strip(),
|
|
'size': str(item.get('size', '')).strip(),
|
|
'hingeLocation': str(item.get('hingeLocation', '')).strip(),
|
|
'quantity': int(item.get('quantity', 1) or 1)
|
|
})
|
|
|
|
normalized_items = [i for i in normalized_items if i['productCode']]
|
|
if not normalized_items:
|
|
return jsonify({
|
|
'status': 'error',
|
|
'message': 'Order items are invalid'
|
|
}), 400
|
|
|
|
submission_id = f"{datetime.now(timezone.utc).strftime('%Y%m%d-%H%M%S')}-{uuid.uuid4().hex[:8]}"
|
|
created_at = datetime.now(timezone.utc).isoformat()
|
|
|
|
quote_data = {
|
|
'schemaVersion': 1,
|
|
'submissionId': submission_id,
|
|
'createdAt': created_at,
|
|
'createdBy': {
|
|
'username': session.get('username'),
|
|
'location': session.get('currentLocation'),
|
|
'accessibleLocations': session.get('accessibleLocations', [])
|
|
},
|
|
'source': {
|
|
'app': 'product-finder',
|
|
'route': payload.get('route', ''),
|
|
'url': payload.get('url', '')
|
|
},
|
|
'context': {
|
|
'answers': payload.get('answers', {}),
|
|
'bitValue': payload.get('bitValue', 0)
|
|
},
|
|
'items': normalized_items
|
|
}
|
|
|
|
outgoing_dir = config.QUOTE_OUTGOING_DIR
|
|
os.makedirs(outgoing_dir, exist_ok=True)
|
|
|
|
final_filename = f"{submission_id}.json"
|
|
final_path = os.path.join(outgoing_dir, final_filename)
|
|
tmp_path = final_path + '.tmp'
|
|
|
|
# Atomic handoff write
|
|
with open(tmp_path, 'w', encoding='utf-8') as f:
|
|
json.dump(quote_data, f, indent=2)
|
|
os.replace(tmp_path, final_path)
|
|
|
|
return jsonify({
|
|
'status': 'success',
|
|
'message': 'Quote submitted successfully',
|
|
'submissionId': submission_id,
|
|
'fileName': final_filename,
|
|
'filePath': final_path
|
|
})
|
|
|
|
except Exception as e:
|
|
return jsonify({
|
|
'status': 'error',
|
|
'message': str(e)
|
|
}), 500
|
|
|
|
@products_bp.route('/css/<path:filename>')
|
|
def serve_css(filename):
|
|
"""Serve CSS files"""
|
|
return send_from_directory(os.path.join(basedir, 'static', 'css'), filename)
|
|
|
|
@products_bp.route('/js/<path:filename>')
|
|
def serve_js(filename):
|
|
"""Serve JavaScript files"""
|
|
return send_from_directory(os.path.join(basedir, 'static', 'js'), filename)
|
|
|
|
@products_bp.route('/images/<path:filename>')
|
|
def serve_images(filename):
|
|
"""Serve image files"""
|
|
return send_from_directory(os.path.join(basedir, 'static', 'images'), filename)
|
|
|
|
@products_bp.route('/data/<path:filename>')
|
|
def serve_data(filename):
|
|
"""Serve data files (JSON)"""
|
|
return send_from_directory(os.path.join(basedir, 'data'), filename)
|
|
|
|
@products_bp.route('/product/<product_code>')
|
|
@login_required
|
|
def product_detail(product_code):
|
|
"""
|
|
Serve product detail page with URL-based navigation
|
|
This allows direct linking to products via /quiz/product/404
|
|
"""
|
|
return render_template('product_finder.html', initial_product=product_code)
|