Files
2026-05-31 11:17:53 -04:00

120 lines
4.4 KiB
Python

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('/<int:id>/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('/<int:id>/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'))