from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify, current_app from flask_login import login_required, current_user from models import db, Product, Category, ProductImage, Review, SizeEnum, User, Order, OrderItem import os from werkzeug.utils import secure_filename import json from functools import wraps from datetime import datetime from sqlalchemy import func admin_bp = Blueprint('admin', __name__, url_prefix='/admin') def admin_required(func): @wraps(func) def wrapper(*args, **kwargs): if not current_user.is_authenticated: flash("Please login to access admin area", "danger") return redirect(url_for('auth.login')) if not current_user.is_admin: flash("Admin access only!", "danger") return redirect(url_for('main.home')) return func(*args, **kwargs) return wrapper def allowed_file(filename): ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'} return '.' in filename and \ filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS def get_or_create_default_category(): default = Category.query.filter_by(name='Uncategorized').first() if not default: default = Category(name='Uncategorized') db.session.add(default) db.session.commit() return default def get_categories(): categories = Category.query.all() if not categories: default_cat = get_or_create_default_category() categories = [default_cat] return categories def get_admin_stats(): try: total_revenue_result = db.session.query( func.sum(OrderItem.price_at_purchase * OrderItem.quantity) ).join(Order).filter(Order.status.in_(['delivered', 'completed'])).scalar() total_revenue = float( total_revenue_result) if total_revenue_result else 0.0 except: total_revenue = 0.0 stats = { 'total_products': Product.query.count(), 'total_categories': Category.query.count(), 'total_reviews': Review.query.count(), 'total_orders': Order.query.count() if hasattr(Order, 'query') else 0, 'total_users': User.query.count() if hasattr(User, 'query') else 1, 'total_revenue': total_revenue, 'low_stock': Product.query.filter(Product.stock > 0, Product.stock <= 5).count(), 'out_of_stock': Product.query.filter(Product.stock == 0).count(), 'total_value': db.session.query(func.sum(Product.price * Product.stock)).scalar() or 0, } return stats @admin_bp.route('/') @login_required @admin_required def dashboard(): page = request.args.get('page', 1, type=int) per_page = 20 products = Product.query.order_by( Product.id.desc()).paginate(page=page, per_page=per_page) stats = get_admin_stats() return render_template('admin/dashboard.html', products=products, stats=stats, SizeEnum=SizeEnum) @admin_bp.route('/add', methods=['GET', 'POST']) @login_required @admin_required def add_product(): if request.method == 'POST': try: name = request.form['name'] description = request.form.get('description', '') price = float(request.form['price']) stock = int(request.form.get('stock', 0)) sku = request.form.get('sku', '') color = request.form.get('color', '') size = request.form.get('size', '') product_type = request.form.get('product_type', '') material = request.form.get('material', '') company = request.form.get('company', '') weight = request.form.get('weight') dimensions = request.form.get('dimensions', '') if weight: try: weight = float(weight) except ValueError: weight = None else: weight = None category_id = request.form.get('category_id') if not category_id: category = get_or_create_default_category() category_id = category.id product = Product( name=name, description=description, price=price, stock=stock, sku=sku, color=color, size=size, material=material, company=company, weight=weight, dimensions=dimensions, category_id=category_id ) db.session.add(product) db.session.flush() if 'images[]' in request.files: files = request.files.getlist('images[]') for i, file in enumerate(files): if file and file.filename and allowed_file(file.filename): upload_dir = 'static/uploads/products' os.makedirs(upload_dir, exist_ok=True) filename = secure_filename(file.filename) name_part, ext = os.path.splitext(filename) unique_filename = f"{name_part}_{product.id}_{i}{ext}" file_path = os.path.join(upload_dir, unique_filename) file.save(file_path) product_image = ProductImage( product_id=product.id, filename=unique_filename, is_primary=(i == 0), display_order=i ) db.session.add(product_image) db.session.commit() flash(f"Product '{name}' added successfully!", "success") return redirect(url_for('admin.dashboard')) except Exception as e: db.session.rollback() flash(f"Error adding product: {str(e)}", "danger") import traceback traceback.print_exc() categories = get_categories() return render_template('admin/add_product.html', categories=categories, sizes=SizeEnum.ALL) @admin_bp.route('/edit/', methods=['GET', 'POST']) @login_required @admin_required def edit_product(id): product = Product.query.get_or_404(id) if request.method == 'POST': try: product.name = request.form['name'] product.description = request.form.get('description', '') product.price = float(request.form['price']) product.stock = int(request.form.get('stock', 0)) product.sku = request.form.get('sku', '') product.color = request.form.get('color', '') product.size = request.form.get('size', '') product.material = request.form.get('material', '') product.company = request.form.get('company', '') weight = request.form.get('weight') if weight: try: product.weight = float(weight) except ValueError: product.weight = None else: product.weight = None product.dimensions = request.form.get('dimensions', '') category_id = request.form.get('category_id') if category_id: product.category_id = category_id if 'images[]' in request.files: files = request.files.getlist('images[]') for i, file in enumerate(files): if file and file.filename and allowed_file(file.filename): upload_dir = 'static/uploads/products' os.makedirs(upload_dir, exist_ok=True) filename = secure_filename(file.filename) name_part, ext = os.path.splitext(filename) unique_filename = f"{name_part}_{product.id}_{len(product.images) + i}{ext}" file_path = os.path.join(upload_dir, unique_filename) file.save(file_path) product_image = ProductImage( product_id=product.id, filename=unique_filename, display_order=len(product.images) + i ) db.session.add(product_image) if 'deleted_images' in request.form: deleted_images_str = request.form['deleted_images'] if deleted_images_str: try: deleted_ids = json.loads(deleted_images_str) for img_id in deleted_ids: image = ProductImage.query.get(img_id) if image and image.product_id == product.id: file_path = os.path.join( 'static/uploads/products', image.filename) if os.path.exists(file_path): os.remove(file_path) db.session.delete(image) except json.JSONDecodeError: pass if 'primary_image' in request.form: primary_id = request.form['primary_image'] if primary_id: try: primary_id = int(primary_id) for img in product.images: img.is_primary = (img.id == primary_id) except ValueError: pass db.session.commit() flash("Product updated successfully!", "success") return redirect(url_for('admin.dashboard')) except Exception as e: db.session.rollback() flash(f"Error updating product: {str(e)}", "danger") import traceback traceback.print_exc() categories = get_categories() return render_template('admin/edit_product.html', product=product, categories=categories, sizes=SizeEnum.ALL) @admin_bp.route('/delete/', methods=['POST']) @login_required @admin_required def delete_product(id): product = Product.query.get_or_404(id) try: for image in product.images: file_path = os.path.join('static/uploads/products', image.filename) if os.path.exists(file_path): os.remove(file_path) db.session.delete(product) db.session.commit() flash("Product deleted successfully!", "info") except Exception as e: flash(f"Error deleting product: {str(e)}", "danger") return redirect(url_for('admin.dashboard')) @admin_bp.route('/image/delete/', methods=['POST']) @login_required @admin_required def delete_image(id): try: image = ProductImage.query.get_or_404(id) file_path = os.path.join('static/uploads/products', image.filename) if os.path.exists(file_path): os.remove(file_path) db.session.delete(image) db.session.commit() return jsonify({'success': True}) except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 @admin_bp.route('/reviews') @login_required @admin_required def manage_reviews(): reviews = Review.query.order_by(Review.created_at.desc()).all() return render_template('admin/reviews.html', reviews=reviews) @admin_bp.route('/review/delete/', methods=['POST']) @login_required @admin_required def delete_review(id): review = Review.query.get_or_404(id) try: db.session.delete(review) db.session.commit() flash("Review deleted successfully!", "info") except Exception as e: flash(f"Error deleting review: {str(e)}", "danger") return redirect(url_for('admin.manage_reviews')) @admin_bp.route('/categories') @login_required @admin_required def manage_categories(): categories = Category.query.order_by(Category.name.asc()).all() return render_template('admin/categories.html', categories=categories) @admin_bp.route('/category/add', methods=['POST']) @login_required @admin_required def add_category(): name = request.form.get('name', '').strip() if not name: flash("Category name cannot be empty", "danger") return redirect(url_for('admin.manage_categories')) existing = Category.query.filter_by(name=name).first() if existing: flash(f"Category '{name}' already exists", "warning") return redirect(url_for('admin.manage_categories')) try: category = Category(name=name) db.session.add(category) db.session.commit() flash(f"Category '{name}' added successfully!", "success") except Exception as e: flash(f"Error adding category: {str(e)}", "danger") return redirect(url_for('admin.manage_categories')) @admin_bp.route('/category/delete/', methods=['POST']) @login_required @admin_required def delete_category(id): category = Category.query.get_or_404(id) if category.products: flash( f"Cannot delete category '{category.name}' because it has {len(category.products)} product(s)", "danger") return redirect(url_for('admin.manage_categories')) try: db.session.delete(category) db.session.commit() flash(f"Category '{category.name}' deleted successfully!", "info") except Exception as e: flash(f"Error deleting category: {str(e)}", "danger") return redirect(url_for('admin.manage_categories')) @admin_bp.route('/stats') @login_required @admin_required def stats(): stats_data = get_admin_stats() return render_template('admin/stats.html', stats=stats_data) @admin_bp.route('/users') @login_required @admin_required def manage_users(): users = User.query.order_by(User.id.desc()).all() return render_template('admin/users.html', users=users) @admin_bp.route('/user/toggle_admin/', methods=['POST']) @login_required @admin_required def toggle_user_admin(id): user = User.query.get_or_404(id) if user.id == current_user.id: flash("You cannot remove admin privileges from yourself", "warning") return redirect(url_for('admin.manage_users')) try: user.is_admin = not user.is_admin db.session.commit() status = "granted" if user.is_admin else "removed" flash(f"Admin privileges {status} for {user.email}", "success") except Exception as e: flash(f"Error updating user: {str(e)}", "danger") return redirect(url_for('admin.manage_users')) @admin_bp.route('/user/delete/', methods=['POST']) @login_required @admin_required def delete_user(id): user = User.query.get_or_404(id) if user.id == current_user.id: flash("You cannot delete your own account", "warning") return redirect(url_for('admin.manage_users')) try: db.session.delete(user) db.session.commit() flash(f"User {user.email} deleted successfully", "info") except Exception as e: flash(f"Error deleting user: {str(e)}", "danger") return redirect(url_for('admin.manage_users')) @admin_bp.route('/orders') @login_required @admin_required def manage_orders(): orders = Order.query.order_by(Order.created_at.desc()).all() return render_template('admin/orders.html', orders=orders) @admin_bp.route('/order/') @login_required @admin_required def order_detail(id): order = Order.query.get_or_404(id) return render_template('admin/order_detail.html', order=order) @admin_bp.route('/order/update_status/', methods=['POST']) @login_required @admin_required def update_order_status(id): order = Order.query.get_or_404(id) new_status = request.form.get('status') if new_status in ['pending', 'processing', 'shipped', 'delivered', 'cancelled']: try: order.status = new_status db.session.commit() flash(f"Order #{order.id} status updated to {new_status}", "success") except Exception as e: flash(f"Error updating order: {str(e)}", "danger") else: flash("Invalid status", "danger") return redirect(url_for('admin.order_detail', id=id)) @admin_bp.route('/quick/restock_low') @login_required @admin_required def quick_restock_low(): """Quick action to restock low inventory items""" try: low_stock_products = Product.query.filter( Product.stock > 0, Product.stock <= 5 ).all() count = 0 for product in low_stock_products: product.stock += 10 count += 1 if count > 0: db.session.commit() flash(f"Restocked {count} low inventory products", "success") else: flash("No low inventory products found", "info") except Exception as e: flash(f"Error restocking products: {str(e)}", "danger") return redirect(url_for('admin.dashboard')) @admin_bp.route('/quick/update_prices', methods=['POST']) @login_required @admin_required def quick_update_prices(): """Quick action to update prices by percentage""" try: percentage = float(request.form.get('percentage', 0)) if percentage != 0: multiplier = 1 + (percentage / 100) products = Product.query.all() for product in products: product.price = round(product.price * multiplier, 2) db.session.commit() action = "increased" if percentage > 0 else "decreased" flash( f"Prices {action} by {abs(percentage)}% for all products", "success") else: flash("No price change specified", "warning") except Exception as e: flash(f"Error updating prices: {str(e)}", "danger") return redirect(url_for('admin.dashboard'))