1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-07-25 08:09:38 +02:00

Budgeting V1 (#1609)

* Budgeting V1

* Basic UI template

* Fully scaffolded budgeting v1

* Basic working budget

* Finalize donut chart for budgets

* Allow categorization of loan payments for budget

* Include loan payments in incomes_and_expenses scope

* Add budget allocations progress

* Empty states

* Clean up budget methods

* Category aggregation queries

* Handle overage scenarios in form

* Finalize budget donut chart controller

* Passing tests

* Fix allocation naming

* Add income category migration

* Native support for uncategorized budget category

* Formatting

* Fix subcategory sort order, padding

* Fix calculation for category rollups in budget
This commit is contained in:
Zach Gollwitzer 2025-01-16 14:36:37 -05:00 committed by GitHub
parent 413ec6cbed
commit 195ec85d96
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
61 changed files with 2044 additions and 140 deletions

View file

@ -0,0 +1,35 @@
class BudgetCategoriesController < ApplicationController
def index
@budget = Current.family.budgets.find(params[:budget_id])
render layout: "wizard"
end
def show
@budget = Current.family.budgets.find(params[:budget_id])
@recent_transactions = @budget.entries
if params[:id] == BudgetCategory.uncategorized.id
@budget_category = @budget.uncategorized_budget_category
@recent_transactions = @recent_transactions.where(account_transactions: { category_id: nil })
else
@budget_category = Current.family.budget_categories.find(params[:id])
@recent_transactions = @recent_transactions.joins("LEFT JOIN categories ON categories.id = account_transactions.category_id")
.where("categories.id = ? OR categories.parent_id = ?", @budget_category.category.id, @budget_category.category.id)
end
@recent_transactions = @recent_transactions.order("account_entries.date DESC, ABS(account_entries.amount) DESC").take(3)
end
def update
@budget_category = Current.family.budget_categories.find(params[:id])
@budget_category.update!(budget_category_params)
redirect_to budget_budget_categories_path(@budget_category.budget)
end
private
def budget_category_params
params.require(:budget_category).permit(:budgeted_spending)
end
end

View file

@ -0,0 +1,55 @@
class BudgetsController < ApplicationController
before_action :set_budget, only: %i[show edit update]
def index
redirect_to_current_month_budget
end
def show
@next_budget = @budget.next_budget
@previous_budget = @budget.previous_budget
@latest_budget = Budget.find_or_bootstrap(Current.family)
render layout: with_sidebar
end
def edit
render layout: "wizard"
end
def update
@budget.update!(budget_params)
redirect_to budget_budget_categories_path(@budget)
end
def create
start_date = Date.parse(budget_create_params[:start_date])
@budget = Budget.find_or_bootstrap(Current.family, date: start_date)
redirect_to budget_path(@budget)
end
def picker
render partial: "budgets/picker", locals: {
family: Current.family,
year: params[:year].to_i || Date.current.year
}
end
private
def budget_create_params
params.require(:budget).permit(:start_date)
end
def budget_params
params.require(:budget).permit(:budgeted_spending, :expected_income)
end
def set_budget
@budget = Current.family.budgets.find(params[:id])
@budget.sync_budget_categories
end
def redirect_to_current_month_budget
current_budget = Budget.find_or_bootstrap(Current.family)
redirect_to budget_path(current_budget)
end
end

View file

@ -19,9 +19,15 @@ class CategoriesController < ApplicationController
if @category.save
@transaction.update(category_id: @category.id) if @transaction
redirect_back_or_to categories_path, notice: t(".success")
flash[:notice] = t(".success")
redirect_target_url = request.referer || categories_path
respond_to do |format|
format.html { redirect_back_or_to categories_path, notice: t(".success") }
format.turbo_stream { render turbo_stream: turbo_stream.action(:redirect, redirect_target_url) }
end
else
@categories = Current.family.categories.alphabetically.where(parent_id: nil)
@categories = Current.family.categories.alphabetically.where(parent_id: nil).where.not(id: @category.id)
render :new, status: :unprocessable_entity
end
end
@ -60,6 +66,6 @@ class CategoriesController < ApplicationController
end
def category_params
params.require(:category).permit(:name, :color, :parent_id)
params.require(:category).permit(:name, :color, :parent_id, :classification, :lucide_icon)
end
end

View file

@ -3,7 +3,7 @@ class TransactionsController < ApplicationController
def index
@q = search_params
search_query = Current.family.transactions.search(@q).includes(:entryable).reverse_chronological
search_query = Current.family.transactions.search(@q).reverse_chronological
@pagy, @transaction_entries = pagy(search_query, limit: params[:per_page] || "50")
totals_query = search_query.incomes_and_expenses

View file

@ -8,6 +8,7 @@ class TransfersController < ApplicationController
end
def show
@categories = Current.family.categories.expenses
end
def create
@ -37,7 +38,11 @@ class TransfersController < ApplicationController
end
def update
@transfer.update!(transfer_update_params)
Transfer.transaction do
@transfer.update!(transfer_update_params.except(:category_id))
@transfer.outflow_transaction.update!(category_id: transfer_update_params[:category_id])
end
respond_to do |format|
format.html { redirect_back_or_to transactions_url, notice: t(".success") }
format.turbo_stream
@ -61,6 +66,6 @@ class TransfersController < ApplicationController
end
def transfer_update_params
params.require(:transfer).permit(:notes, :status)
params.require(:transfer).permit(:notes, :status, :category_id)
end
end