from flask import Blueprint, render_template, redirect, url_for, flash, request from flask_login import login_required from flask_wtf import FlaskForm from wtforms import StringField, SelectField, SubmitField from wtforms.validators import DataRequired, Length from app.extensions import db from app.models.category import Category categories_bp = Blueprint('categories', __name__, url_prefix='/categories') CATEGORY_TYPES = [ ('expense', 'Expense'), ('income', 'Income'), ('both', 'Both'), ] CATEGORY_COLORS = [ '#6366f1', '#f59e0b', '#3b82f6', '#8b5cf6', '#ef4444', '#ec4899', '#f97316', '#14b8a6', '#64748b', '#10b981', '#0ea5e9', '#84cc16', '#a78bfa', '#94a3b8', '#f43f5e', ] CATEGORY_ICONS = [ 'bi-house', 'bi-cup-hot', 'bi-car-front', 'bi-lightning-charge', 'bi-heart-pulse', 'bi-controller', 'bi-bag', 'bi-book', 'bi-shield-check', 'bi-person-heart', 'bi-airplane', 'bi-repeat', 'bi-gift', 'bi-three-dots', 'bi-briefcase', 'bi-laptop', 'bi-building', 'bi-graph-up-arrow', 'bi-house-door', 'bi-tag', 'bi-cart', 'bi-music-note', 'bi-phone', 'bi-tools', ] class CategoryForm(FlaskForm): name = StringField('Name', validators=[DataRequired(), Length(1, 100)]) category_type = SelectField('Type', choices=CATEGORY_TYPES, validators=[DataRequired()]) color = StringField('Color', default='#6B7280') icon = StringField('Icon', default='bi-tag') submit = SubmitField('Save') @categories_bp.route('/') @login_required def index(): expense_cats = Category.query.filter( Category.category_type.in_(['expense', 'both']), Category.is_active == True, Category.parent_id == None ).order_by(Category.is_system.desc(), Category.name).all() income_cats = Category.query.filter( Category.category_type.in_(['income', 'both']), Category.is_active == True, Category.parent_id == None ).order_by(Category.is_system.desc(), Category.name).all() return render_template('categories/index.html', expense_cats=expense_cats, income_cats=income_cats) @categories_bp.route('/new', methods=['GET', 'POST']) @login_required def new(): form = CategoryForm() # Pre-select type from query param if request.method == 'GET' and request.args.get('type'): form.category_type.data = request.args.get('type') if form.validate_on_submit(): cat = Category( name=form.name.data.strip(), category_type=form.category_type.data, color=form.color.data or '#6B7280', icon=form.icon.data or 'bi-tag', is_system=False, ) db.session.add(cat) db.session.commit() flash(f'Category "{cat.name}" created.', 'success') return redirect(url_for('categories.index')) return render_template('categories/form.html', form=form, title='New Category', colors=CATEGORY_COLORS, icons=CATEGORY_ICONS) @categories_bp.route('//edit', methods=['GET', 'POST']) @login_required def edit(id): cat = db.get_or_404(Category, id) form = CategoryForm(obj=cat) if form.validate_on_submit(): if cat.is_system and cat.name != form.name.data.strip(): flash('System category names cannot be changed.', 'warning') else: cat.name = form.name.data.strip() cat.category_type = form.category_type.data cat.color = form.color.data or cat.color cat.icon = form.icon.data or cat.icon db.session.commit() flash(f'Category "{cat.name}" updated.', 'success') return redirect(url_for('categories.index')) return render_template('categories/form.html', form=form, title='Edit Category', category=cat, colors=CATEGORY_COLORS, icons=CATEGORY_ICONS) @categories_bp.route('//delete', methods=['POST']) @login_required def delete(id): cat = db.get_or_404(Category, id) if cat.is_system: flash('System categories cannot be deleted.', 'warning') return redirect(url_for('categories.index')) # Check if in use if cat.transactions.count() > 0: flash('Cannot delete category with existing transactions. Deactivate instead.', 'warning') return redirect(url_for('categories.index')) cat.is_active = False db.session.commit() flash(f'Category "{cat.name}" removed.', 'info') return redirect(url_for('categories.index'))