120 lines
4.4 KiB
Python
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'))
|