""" 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/', 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/', 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/') def serve_css(filename): """Serve CSS files""" return send_from_directory(os.path.join(basedir, 'static', 'css'), filename) @products_bp.route('/js/') def serve_js(filename): """Serve JavaScript files""" return send_from_directory(os.path.join(basedir, 'static', 'js'), filename) @products_bp.route('/images/') def serve_images(filename): """Serve image files""" return send_from_directory(os.path.join(basedir, 'static', 'images'), filename) @products_bp.route('/data/') def serve_data(filename): """Serve data files (JSON)""" return send_from_directory(os.path.join(basedir, 'data'), filename) @products_bp.route('/product/') @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)