05/31 Phase 2
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
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'))
|
||||
Reference in New Issue
Block a user