from flask import Blueprint, render_template, redirect, url_for, flash, request, current_app, jsonify, make_response from flask_login import login_required, current_user from app import create_app, db from ..forms import QuotationForm from ..models import User, Quotation, CostFactor, CostFactorHistory from datetime import datetime, timedelta, timezone from sqlalchemy import desc, text import logging from app import limiter, cache bp = Blueprint('admin', __name__, url_prefix='/admin') @bp.before_request def before_request(): if not current_user.is_authenticated or not current_user.is_admin: flash('Access denied. Admin privileges required.', 'danger') return redirect(url_for('main.home')) @bp.route('/dashboard') @login_required @limiter.limit("30 per minute") @cache.cached(timeout=60, key_prefix='admin_dashboard') def dashboard(): try: # Get statistics total_users = User.query.count() total_quotations = Quotation.query.count() recent_quotations = Quotation.query.order_by(desc(Quotation.created_at)).limit(5).all() # Get user statistics active_users = User.query.filter( User.last_login >= datetime.utcnow() - timedelta(days=30) ).count() # Get quotation statistics by country (using mold_shop_country) country_stats = db.session.query( Quotation.mold_shop_country, db.func.count(Quotation.id) ).group_by(Quotation.mold_shop_country).all() return render_template('admin/dashboard.html', total_users=total_users, total_quotations=total_quotations, active_users=active_users, recent_quotations=recent_quotations, country_stats=country_stats ) except Exception as e: current_app.logger.error(f'Admin dashboard error: {str(e)}') flash('An error occurred while loading the dashboard.', 'error') return redirect(url_for('main.home')) @bp.route('/users') @login_required @limiter.limit("30 per minute") @cache.cached(timeout=60, key_prefix='admin_users') def users(): try: page = request.args.get('page', 1, type=int) search = request.args.get('search', '').strip() per_page = 20 # Build query with search functionality query = User.query if search: # Search by username or email (case-insensitive) search_filter = f"%{search.lower()}%" query = query.filter( db.or_( User.username.ilike(search_filter), User.email.ilike(search_filter) ) ) users = query.order_by(User.created_at.desc()).paginate( page=page, per_page=per_page, error_out=False ) return render_template('admin/users.html', users=users) except Exception as e: current_app.logger.error(f'Admin users error: {str(e)}') flash('An error occurred while loading users.', 'error') return redirect(url_for('admin.dashboard')) @bp.route('/quotations') @login_required @limiter.limit("30 per minute") def quotations(): try: page = request.args.get('page', 1, type=int) per_page = 20 # Get filter parameters country = request.args.get('country') mold_type = request.args.get('mold_type') date_from_str = request.args.get('date_from') date_to_str = request.args.get('date_to') # Convert date strings to datetime objects date_from = None date_to = None today = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0) try: if date_from_str: date_from = datetime.strptime(date_from_str, '%Y-%m-%d') if date_to_str: date_to = datetime.strptime(date_to_str, '%Y-%m-%d') except ValueError: flash('Invalid date format. Please use YYYY-MM-DD format.', 'error') date_from = None date_to = None # Build query query = Quotation.query if country: query = query.filter(Quotation.mold_shop_country == country) if mold_type: query = query.filter(Quotation.mold_main_type == mold_type) if date_from: query = query.filter(Quotation.created_at >= date_from) if date_to: # Add one day to include the end date fully next_day = date_to + timedelta(days=1) query = query.filter(Quotation.created_at < next_day) # Get unique countries and mold types for filters countries = db.session.query(Quotation.mold_shop_country).distinct().all() countries = [c[0] for c in countries if c[0]] mold_types = db.session.query(Quotation.mold_main_type).distinct().all() mold_types = [t[0] for t in mold_types if t[0]] quotations = query.order_by(desc(Quotation.created_at)).paginate( page=page, per_page=per_page, error_out=False ) return render_template('admin/quotations.html', quotations=quotations, countries=sorted(countries), mold_types=sorted(mold_types), today=today, filters={ 'country': country, 'mold_type': mold_type, 'date_from': date_from, 'date_to': date_to } ) except Exception as e: current_app.logger.error(f'Admin quotations error: {str(e)}') flash('An error occurred while loading quotations.', 'error') return redirect(url_for('admin.dashboard')) @bp.route('/user/') @login_required def user_detail(user_id): try: user = User.query.get_or_404(user_id) quotations = Quotation.query.filter_by(user_id=user_id).order_by( desc(Quotation.created_at) ).all() return render_template('admin/user_detail.html', user=user, quotations=quotations ) except Exception as e: current_app.logger.error(f'Admin user detail error: {str(e)}') flash('An error occurred while loading user details.', 'error') return redirect(url_for('admin.users')) @bp.route('/quotation/') @login_required @limiter.limit("30 per minute") @cache.cached(timeout=60, key_prefix='admin_quotation_detail') def quotation_detail(id): try: quotation = Quotation.query.get_or_404(id) return render_template('admin/quotation_detail.html', quotation=quotation ) except Exception as e: current_app.logger.error(f'Admin quotation detail error: {str(e)}') flash('An error occurred while loading quotation details.', 'error') return redirect(url_for('admin.quotations')) @bp.route('/user//toggle', methods=['POST']) @login_required def toggle_user(user_id): try: user = User.query.get_or_404(user_id) user.is_active = not user.is_active db.session.commit() return jsonify({ 'status': 'success', 'is_active': user.is_active }) except Exception as e: current_app.logger.error(f'Admin toggle user error: {str(e)}') db.session.rollback() return jsonify({ 'status': 'error', 'message': 'An error occurred while updating user status' }), 500 @bp.route('/delete_user', methods=['POST']) @login_required @limiter.limit("10 per minute") def delete_user(): if not request.is_json: return jsonify({'error': 'Invalid request format'}), 400 data = request.get_json() user_id = data.get('user_id') if not user_id: return jsonify({'error': 'User ID is required'}), 400 try: user = User.query.get_or_404(user_id) if user.id == current_user.id: return jsonify({'error': 'Cannot delete your own account'}), 400 db.session.delete(user) db.session.commit() cache.delete_memoized(users) current_app.logger.info(f'User {user.email} deleted by admin {current_user.email}') return jsonify({'message': 'User deleted successfully'}) except Exception as e: db.session.rollback() current_app.logger.error(f'Error deleting user: {str(e)}') return jsonify({'error': 'An error occurred while deleting the user'}), 500 @bp.route('/delete_quotation', methods=['POST']) @login_required @limiter.limit("10 per minute") def delete_quotation(): if not request.is_json: return jsonify({'error': 'Invalid request format'}), 400 data = request.get_json() quotation_id = data.get('quotation_id') if not quotation_id: return jsonify({'error': 'Quotation ID is required'}), 400 try: quotation = Quotation.query.get_or_404(quotation_id) db.session.delete(quotation) db.session.commit() cache.delete_memoized(quotations) current_app.logger.info(f'Quotation {quotation.id} deleted by admin {current_user.email}') return jsonify({'message': 'Quotation deleted successfully'}) except Exception as e: db.session.rollback() current_app.logger.error(f'Error deleting quotation: {str(e)}') return jsonify({'error': 'An error occurred while deleting the quotation'}), 500 @bp.route('/cost-factors') @login_required @limiter.limit("30 per minute") def cost_factors(): """Display and manage cost factors""" try: from ..utils.cost_factor_loader import initialize_default_cost_factors from flask import current_app # Add debug logging current_app.logger.info("Accessing cost_factors route") # Verify database connection try: db.session.execute(text('SELECT 1')) except Exception as e: current_app.logger.error(f"Database connection error: {str(e)}") flash('Database connection error', 'error') return redirect(url_for('admin.dashboard')) # Initialize default factors if none exist try: initialize_default_cost_factors() except Exception as e: current_app.logger.error(f"Error initializing default cost factors: {str(e)}") flash('Error initializing cost factors', 'error') return redirect(url_for('admin.dashboard')) # Get all cost factors grouped by category try: mold_factors = CostFactor.query.filter_by(category='mold', is_active=True).order_by(CostFactor.name).all() complexity_factors = CostFactor.query.filter_by(category='complexity', is_active=True).order_by(CostFactor.name).all() texture_factors = CostFactor.query.filter_by(category='texture', is_active=True).order_by(CostFactor.name).all() tax_factors = CostFactor.query.filter_by(category='tax', is_active=True).order_by(CostFactor.name).all() base_factors = CostFactor.query.filter_by(category='base', is_active=True).order_by(CostFactor.name).all() except Exception as e: current_app.logger.error(f"Error querying cost factors: {str(e)}") flash('Error loading cost factors from database', 'error') return redirect(url_for('admin.dashboard')) # Log the number of factors found current_app.logger.info(f"Found factors - Mold: {len(mold_factors)}, Complexity: {len(complexity_factors)}, " f"Texture: {len(texture_factors)}, Tax: {len(tax_factors)}, Base: {len(base_factors)}") # Log individual factors for debugging current_app.logger.debug("Base Factors: %s", [f"{f.name}: {f.factor_value}" for f in base_factors]) current_app.logger.debug("Mold Factors: %s", [f"{f.name}: {f.factor_value}" for f in mold_factors]) current_app.logger.debug("Complexity Factors: %s", [f"{f.name}: {f.factor_value}" for f in complexity_factors]) current_app.logger.debug("Texture Factors: %s", [f"{f.name}: {f.factor_value}" for f in texture_factors]) current_app.logger.debug("Tax Factors: %s", [f"{f.name}: {f.factor_value}" for f in tax_factors]) # Prepare template context template_context = { 'mold_factors': mold_factors, 'complexity_factors': complexity_factors, 'texture_factors': texture_factors, 'tax_factors': tax_factors, 'base_factors': base_factors } # Log template context current_app.logger.debug("Template context: %s", template_context) try: response = make_response(render_template( 'admin/cost_factors.html', **template_context )) except Exception as e: current_app.logger.error(f"Template rendering error: {str(e)}") flash('Error rendering the page', 'error') return redirect(url_for('admin.dashboard')) # Add headers to prevent caching response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate' response.headers['Pragma'] = 'no-cache' response.headers['Expires'] = '0' return response except Exception as e: current_app.logger.error(f"Unhandled error in cost_factors route: {str(e)}") flash('An unexpected error occurred while loading cost factors.', 'error') return redirect(url_for('admin.dashboard')) @bp.route('/cost-factors/edit/', methods=['GET', 'POST']) @login_required @limiter.limit("30 per minute") def edit_cost_factor(factor_id): """Edit a specific cost factor""" try: factor = CostFactor.query.get_or_404(factor_id) if request.method == 'POST': old_value = factor.factor_value new_value = float(request.form.get('factor_value')) change_reason = request.form.get('change_reason', '') # Update the factor factor.factor_value = new_value factor.updated_by = current_user.id # Create history record history = CostFactorHistory( cost_factor_id=factor.id, old_value=old_value, new_value=new_value, changed_by=current_user.id, change_reason=change_reason ) db.session.add(history) db.session.commit() # Clear the cache cache.delete_memoized(cost_factors) cache.delete('admin_cost_factors') flash(f'Cost factor "{factor.name}" updated successfully.', 'success') return redirect(url_for('admin.cost_factors')) return render_template('admin/edit_cost_factor.html', factor=factor) except Exception as e: current_app.logger.error(f'Admin edit cost factor error: {str(e)}') flash('An error occurred while editing the cost factor.', 'error') return redirect(url_for('admin.cost_factors')) @bp.route('/cost-factors/history/') @login_required @limiter.limit("30 per minute") def cost_factor_history(factor_id): """View history of changes for a cost factor""" try: factor = CostFactor.query.get_or_404(factor_id) history = CostFactorHistory.query.filter_by(cost_factor_id=factor_id)\ .order_by(CostFactorHistory.changed_at.desc()).all() return render_template('admin/cost_factor_history.html', factor=factor, history=history ) except Exception as e: current_app.logger.error(f'Admin cost factor history error: {str(e)}') flash('An error occurred while loading cost factor history.', 'error') return redirect(url_for('admin.cost_factors')) @bp.route('/cost-factors/reset', methods=['POST']) @login_required @limiter.limit("10 per minute") def reset_cost_factors(): """Reset cost factors to default values""" try: from ..utils.cost_factor_loader import get_default_cost_factors # Get default values default_factors, default_tax_rates, default_base = get_default_cost_factors() # Create a mapping of all default values default_values = {} # Add mold factors for name, value in default_factors['mold'].items(): default_values[name] = value # Add complexity factors for name, value in default_factors['complexity'].items(): default_values[name] = value # Add texture factors for name, value in default_factors['texture'].items(): default_values[name] = value # Add tax factors for name, value in default_tax_rates.items(): default_values[name] = value # Add base factor default_values['DEFAULT_NET_BASE'] = default_base # Update existing cost factors to default values for factor in CostFactor.query.all(): if factor.name in default_values: old_value = factor.factor_value new_value = default_values[factor.name] if old_value != new_value: # Only update if value changed factor.factor_value = new_value factor.updated_by = current_user.id # Create history record for the reset history = CostFactorHistory( cost_factor_id=factor.id, old_value=old_value, new_value=new_value, changed_by=current_user.id, change_reason='Reset to default value' ) db.session.add(history) db.session.commit() # Clear the cache cache.delete_memoized(cost_factors) cache.delete('admin_cost_factors') flash('Cost factors have been reset to default values.', 'success') return redirect(url_for('admin.cost_factors')) except Exception as e: current_app.logger.error(f'Admin reset cost factors error: {str(e)}') db.session.rollback() flash('An error occurred while resetting cost factors.', 'error') return redirect(url_for('admin.cost_factors')) @bp.route('/user//reset-password', methods=['POST']) @login_required @limiter.limit("10 per minute") def admin_reset_password(user_id): """Admin function to reset a user's password""" try: user = User.query.get_or_404(user_id) # Generate a temporary password import secrets import string # Generate a secure temporary password (12 characters with mix of letters, numbers, symbols) temp_password = ''.join(secrets.choice(string.ascii_letters + string.digits + '!@#$%^&*') for _ in range(12)) # Set the new password user.set_password(temp_password) # Generate a reset token for the user (optional - they can use this to set their own password) reset_token = user.generate_reset_token() db.session.commit() # Log the password reset current_app.logger.info(f'Admin {current_user.email} reset password for user {user.email}') # Create reset URL reset_url = url_for('auth.reset_password', token=reset_token, _external=True) return jsonify({ 'status': 'success', 'message': 'Password reset successfully', 'temporary_password': temp_password, 'reset_url': reset_url, 'username': user.username, 'email': user.email }) except Exception as e: db.session.rollback() current_app.logger.error(f'Admin password reset error: {str(e)}') return jsonify({ 'status': 'error', 'message': 'An error occurred while resetting the password' }), 500 @bp.route('/debug-endpoints') def debug_endpoints(): from flask import current_app with open('logs/endpoints.log', 'w') as f: for rule in current_app.url_map.iter_rules(): f.write(f"{rule.endpoint} {rule.methods} {rule.rule}\n") return 'Endpoints written to logs/endpoints.log', 200